mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
AWS signing without AWS (#16840)
Signed-off-by: Martin Grześlowski <martin.grzeslowski@gmail.com>
This commit is contained in:
parent
f288150843
commit
c2d1789cd4
@ -29,7 +29,7 @@ checker-qual
|
||||
* Project: https://checkerframework.org/
|
||||
* Source: https://github.com/typetools/checker-framework
|
||||
|
||||
aws-crt
|
||||
aws-v4-signer-java
|
||||
* License: Apache License 2.0
|
||||
* Project: https://github.com/awslabs/aws-crt-java
|
||||
* Source: https://github.com/awslabs/aws-crt-java
|
||||
* Project: https://github.com/lucasweb78/aws-v4-signer-java
|
||||
* Source: https://github.com/lucasweb78/aws-v4-signer-java
|
||||
|
@ -34,14 +34,12 @@
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<!-- END caffeine -->
|
||||
<!-- START AWS -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk.crt</groupId>
|
||||
<artifactId>aws-crt</artifactId>
|
||||
<version>0.29.19</version>
|
||||
<groupId>uk.co.lucasweb</groupId>
|
||||
<artifactId>aws-v4-signer-java</artifactId>
|
||||
<version>1.3</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<!-- END AWS -->
|
||||
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
@ -61,5 +59,11 @@
|
||||
<version>5.11.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk.crt</groupId>
|
||||
<artifactId>aws-crt</artifactId>
|
||||
<version>0.29.19</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@ -15,15 +15,16 @@ package org.openhab.binding.salus.internal.aws.http;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.time.ZoneOffset.UTC;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.openhab.binding.salus.internal.aws.http.AwsSigner.*;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@ -37,13 +38,6 @@ import org.openhab.binding.salus.internal.rest.exceptions.SalusApiException;
|
||||
import org.openhab.binding.salus.internal.rest.exceptions.UnsuportedSalusApiException;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
|
||||
import software.amazon.awssdk.crt.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.crt.auth.signing.AwsSigner;
|
||||
import software.amazon.awssdk.crt.auth.signing.AwsSigningConfig;
|
||||
import software.amazon.awssdk.crt.auth.signing.AwsSigningResult;
|
||||
import software.amazon.awssdk.crt.http.HttpHeader;
|
||||
import software.amazon.awssdk.crt.http.HttpRequest;
|
||||
|
||||
/**
|
||||
* The SalusApi class is responsible for interacting with a REST API to perform various operations related to the Salus
|
||||
* system. It handles authentication, token management, and provides methods to retrieve and manipulate device
|
||||
@ -146,11 +140,10 @@ public class AwsSalusApi extends AbstractSalusApi<Authentication> {
|
||||
throws SalusApiException, AuthSalusApiException {
|
||||
var path = "https://%s.iot.%s.amazonaws.com/things/%s/shadow".formatted(awsService, region, dsn);
|
||||
var time = ZonedDateTime.now(clock).withZoneSameInstant(ZoneId.of("UTC"));
|
||||
var signingResult = buildSigningResult(dsn, time);
|
||||
var headers = signingResult.getSignedRequest()//
|
||||
.getHeaders()//
|
||||
var signingResult = buildSigningResult("/things/%s/shadow".formatted(dsn), time, null);
|
||||
var headers = signingResult.entrySet()//
|
||||
.stream()//
|
||||
.map(header -> new RestClient.Header(header.getName(), header.getValue()))//
|
||||
.map(header -> new RestClient.Header(header.getKey(), header.getValue()))//
|
||||
.toList()//
|
||||
.toArray(new RestClient.Header[0]);
|
||||
var response = get(path, headers);
|
||||
@ -161,24 +154,10 @@ public class AwsSalusApi extends AbstractSalusApi<Authentication> {
|
||||
return new TreeSet<>(mapper.parseAwsDeviceProperties(response));
|
||||
}
|
||||
|
||||
private AwsSigningResult buildSigningResult(String dsn, ZonedDateTime time)
|
||||
throws SalusApiException, AuthSalusApiException {
|
||||
private Map<String, String> buildSigningResult(String pathAndQuery, ZonedDateTime time, @Nullable String body)
|
||||
throws AuthSalusApiException, SalusApiException {
|
||||
refreshAccessToken();
|
||||
HttpRequest httpRequest = new HttpRequest("GET", "/things/%s/shadow".formatted(dsn),
|
||||
new HttpHeader[] { new HttpHeader("host", "") }, null);
|
||||
var localCredentials = requireNonNull(cogitoCredentials);
|
||||
try (var config = new AwsSigningConfig()) {
|
||||
config.setRegion(region);
|
||||
config.setService("iotdevicegateway");
|
||||
config.setCredentialsProvider(new StaticCredentialsProvider.StaticCredentialsProviderBuilder()
|
||||
.withAccessKeyId(localCredentials.accessKeyId().getBytes(UTF_8))
|
||||
.withSecretAccessKey(localCredentials.secretKey().getBytes(UTF_8))
|
||||
.withSessionToken(localCredentials.sessionToken().getBytes(UTF_8)).build());
|
||||
config.setTime(time.toInstant().toEpochMilli());
|
||||
return AwsSigner.sign(httpRequest, config).get();
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
throw new SalusApiException("Cannot build AWS signature!", e);
|
||||
}
|
||||
return sign(pathAndQuery, time, requireNonNull(cogitoCredentials), region, "iotdevicegateway", body);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.salus.internal.aws.http;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.salus.internal.rest.exceptions.SalusApiException;
|
||||
|
||||
import uk.co.lucasweb.aws.v4.signer.HttpRequest;
|
||||
import uk.co.lucasweb.aws.v4.signer.Signer;
|
||||
import uk.co.lucasweb.aws.v4.signer.credentials.AwsCredentials;
|
||||
import uk.co.lucasweb.aws.v4.signer.hash.Sha256;
|
||||
|
||||
/**
|
||||
* @author Martin Grześlowski - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class AwsSigner {
|
||||
static Map<String, String> sign(String pathAndQuery, ZonedDateTime time, CogitoCredentials cogitoCredentials,
|
||||
String region, String service, @Nullable String body) throws SalusApiException {
|
||||
try {
|
||||
var contentSha256 = Sha256.get(body != null ? body : "", UTF_8);
|
||||
var request = new HttpRequest("GET", pathAndQuery);
|
||||
var isoDate = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'").format(time);
|
||||
var signer = Signer.builder().region(region)//
|
||||
.awsCredentials(new AwsCredentials(cogitoCredentials.accessKeyId(), cogitoCredentials.secretKey()))//
|
||||
.header("host", "")//
|
||||
.header("X-Amz-Date", isoDate)//
|
||||
.header("X-Amz-Security-Token", cogitoCredentials.sessionToken())//
|
||||
.build(request, service, contentSha256).getSignature();
|
||||
return Map.of(//
|
||||
"Authorization", signer, //
|
||||
"X-Amz-Date", isoDate, //
|
||||
"host", "", //
|
||||
"X-Amz-Security-Token", cogitoCredentials.sessionToken());
|
||||
} catch (Exception e) {
|
||||
throw new SalusApiException("Cannot build AWS signature!", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.salus.internal.aws.http;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import software.amazon.awssdk.crt.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.crt.auth.signing.AwsSigningConfig;
|
||||
import software.amazon.awssdk.crt.http.HttpRequest;
|
||||
|
||||
/**
|
||||
* @author Martin Grześlowski - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class AwsSignerTest {
|
||||
@Test
|
||||
@DisplayName("should generate same signature headers as AWS SDK")
|
||||
void shouldGenerateSameSignatureHeadersAsAWSSDK() throws Exception {
|
||||
// given
|
||||
var pathAndQuery = "/things/xyz/shadow";
|
||||
var time = ZonedDateTime.now(ZoneId.of("UTC"));
|
||||
var credentials = new CogitoCredentials("AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
"IQoJb3JpZ2luX2VjEAIaDGV1LWNlbnRyYWwtMSJIMEYCIQDE7EzyAzhN1zhbH6cHEyA3pc0V2wDHnUyPxRd57WwDAQIhAK6exf3NjDynJT68N8oQVzm3HAC0hEKLJDFy/Lq0c2XeKt8ECIv//////////wEQAhoMMDU2NzE2MDkxODE0Igzmsy2iRkqAqUqV4LwqswTGsPbNATSsxQ8epT4uD8xEgdQJ3KANDsqRWPi/u2Nr7oBcnFH0KbqChpSO8FEshdBLpKgCju0VEghg/K0N79qFqvD0fRvij4G8k6zyLsS51y4MpW2TSe0i9rMOSB0yN4I7Gp3a4u96GUiZs/8b+S1wN3H9bTjMeCO7zC0VXWj7icWIv9UckgX9IRaCBj0GQ0Q+oHwzgtVKK4onwWxZO/7r0n39WLIBf0SQHsybWfK3YEj/OwVudsISUWxSfwoBK56PvqxUqUfx9ASKroTaS41K45j9/v7HaKFIp/6RKsP9Ls8jc0kTExar/Ch3ZNfCRK3TLP2XjDe/DfSWr5VdihwF4E3vJQ4L05/rN8lieEZPuWJEbz+8i/EiRBjDgtzl+Rt3R2Esa4bzRfK4UywZjVpUMatMpKk/+MooXaOE8SC8yMWK4GgEorVMcQUJGdZ+KH/3sO5IARplVrOiynwksTIgFIJ1NKIDMfmm966U1q7ClaotOCRt0kqsTXU+0cllAXksc67T0d1Pc4tt7Q+yw/HSyKVZlK1bvQ4LLU1NnUDcJiCUe5Q+A61wWSGjEWxAXjggxhro+1W0gRHgXILZnr/GkM8/kT/UczkAnGb0LFTh1haFlXYgqxlA3SzAiXMDVyzWqD7EOq1S/fSYZ9vrxDJPYuiYVBdDsQDlUGGePdHPmxZBfZC7tnHJkzOgRfijYA7TXfVkAftYLxbldAA/I5Wd6Xlw9OjytBn8MOXNifVZjgsURTDUmPayBjqEAq0ZbjC4sf7hQE+2wwSot9oANqJQq6nB/RitNWtENuEss02L1Fk/GmD+tWW3AVY/+a/8xrN8reyQDSUaKb39UesTxaBQ7/MdJQNgGkdIVmSF7rBedOXzjqaqvqLylQR1NnsVl3veAnsmGnE03m0punceAxH2V0S6iAcjYyMwVBeTYpJ3jQbEYvvtQqyoo7koiR2MkdqSD5YND5D8CoaWlWPvI4Oy326srm2eeQVpALyKzEu5XKWL45mnYpLLFDYzAdErjkuMDY6tBZIKnADSoPPj17fbjVFOwL44c1xXKkA7xvaMATCeNl3pkwxHCg1LpXW2vVkzWE/jB2NNYZmHjayb8x1G");
|
||||
var region = "eu-central-1";
|
||||
|
||||
// when
|
||||
var sign = AwsSigner.sign(pathAndQuery, time, credentials, region, "iotdevicegateway", null);
|
||||
|
||||
// then
|
||||
assertThat(sign).isEqualTo(rawAwsSign(pathAndQuery, time, credentials, region));
|
||||
}
|
||||
|
||||
public static Map<String, String> rawAwsSign(String pathAndQuery, ZonedDateTime time,
|
||||
CogitoCredentials cogitoCredentials, String region) throws Exception {
|
||||
HttpRequest httpRequest = new HttpRequest("GET", pathAndQuery,
|
||||
new software.amazon.awssdk.crt.http.HttpHeader[] {
|
||||
new software.amazon.awssdk.crt.http.HttpHeader("host", "") },
|
||||
null);
|
||||
var localCredentials = requireNonNull(cogitoCredentials);
|
||||
try (var config = new AwsSigningConfig()) {
|
||||
config.setRegion(region);
|
||||
config.setService("iotdevicegateway");
|
||||
config.setCredentialsProvider(new StaticCredentialsProvider.StaticCredentialsProviderBuilder()
|
||||
.withAccessKeyId(localCredentials.accessKeyId().getBytes(UTF_8))
|
||||
.withSecretAccessKey(localCredentials.secretKey().getBytes(UTF_8))
|
||||
.withSessionToken(localCredentials.sessionToken().getBytes(UTF_8)).build());
|
||||
config.setTime(time.toInstant().toEpochMilli());
|
||||
return software.amazon.awssdk.crt.auth.signing.AwsSigner.sign(httpRequest, config).get().getSignedRequest()
|
||||
.getHeaders().stream().collect(Collectors.toMap(software.amazon.awssdk.crt.http.HttpHeader::getName,
|
||||
software.amazon.awssdk.crt.http.HttpHeader::getValue));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user