diff --git a/bundles/org.openhab.binding.salus/NOTICE b/bundles/org.openhab.binding.salus/NOTICE
index 1982cf70cb3..fe1d4c3df5e 100644
--- a/bundles/org.openhab.binding.salus/NOTICE
+++ b/bundles/org.openhab.binding.salus/NOTICE
@@ -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
diff --git a/bundles/org.openhab.binding.salus/pom.xml b/bundles/org.openhab.binding.salus/pom.xml
index 2eeb214e9d1..7cedb7998f4 100644
--- a/bundles/org.openhab.binding.salus/pom.xml
+++ b/bundles/org.openhab.binding.salus/pom.xml
@@ -34,14 +34,12 @@
compile
-
- software.amazon.awssdk.crt
- aws-crt
- 0.29.19
+ uk.co.lucasweb
+ aws-v4-signer-java
+ 1.3
compile
-
ch.qos.logback
@@ -61,5 +59,11 @@
5.11.0
test
+
+ software.amazon.awssdk.crt
+ aws-crt
+ 0.29.19
+ test
+
diff --git a/bundles/org.openhab.binding.salus/src/main/java/org/openhab/binding/salus/internal/aws/http/AwsSalusApi.java b/bundles/org.openhab.binding.salus/src/main/java/org/openhab/binding/salus/internal/aws/http/AwsSalusApi.java
index 0223226469b..1ece5747930 100644
--- a/bundles/org.openhab.binding.salus/src/main/java/org/openhab/binding/salus/internal/aws/http/AwsSalusApi.java
+++ b/bundles/org.openhab.binding.salus/src/main/java/org/openhab/binding/salus/internal/aws/http/AwsSalusApi.java
@@ -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 {
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 {
return new TreeSet<>(mapper.parseAwsDeviceProperties(response));
}
- private AwsSigningResult buildSigningResult(String dsn, ZonedDateTime time)
- throws SalusApiException, AuthSalusApiException {
+ private Map 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
diff --git a/bundles/org.openhab.binding.salus/src/main/java/org/openhab/binding/salus/internal/aws/http/AwsSigner.java b/bundles/org.openhab.binding.salus/src/main/java/org/openhab/binding/salus/internal/aws/http/AwsSigner.java
new file mode 100644
index 00000000000..435d4bb1aad
--- /dev/null
+++ b/bundles/org.openhab.binding.salus/src/main/java/org/openhab/binding/salus/internal/aws/http/AwsSigner.java
@@ -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 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);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.salus/src/test/java/org/openhab/binding/salus/internal/aws/http/AwsSignerTest.java b/bundles/org.openhab.binding.salus/src/test/java/org/openhab/binding/salus/internal/aws/http/AwsSignerTest.java
new file mode 100644
index 00000000000..65ca6de843c
--- /dev/null
+++ b/bundles/org.openhab.binding.salus/src/test/java/org/openhab/binding/salus/internal/aws/http/AwsSignerTest.java
@@ -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 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));
+ }
+ }
+}