]> git.basschouten.com Git - openhab-addons.git/commitdiff
AWS signing without AWS (#16840)
authorMartin <martin.grzeslowski@gmail.com>
Fri, 14 Jun 2024 19:06:44 +0000 (21:06 +0200)
committerGitHub <noreply@github.com>
Fri, 14 Jun 2024 19:06:44 +0000 (21:06 +0200)
Signed-off-by: Martin Grześlowski <martin.grzeslowski@gmail.com>
bundles/org.openhab.binding.salus/NOTICE
bundles/org.openhab.binding.salus/pom.xml
bundles/org.openhab.binding.salus/src/main/java/org/openhab/binding/salus/internal/aws/http/AwsSalusApi.java
bundles/org.openhab.binding.salus/src/main/java/org/openhab/binding/salus/internal/aws/http/AwsSigner.java [new file with mode: 0644]
bundles/org.openhab.binding.salus/src/test/java/org/openhab/binding/salus/internal/aws/http/AwsSignerTest.java [new file with mode: 0644]

index 1982cf70cb386a37b2f2b4384037d169e87e51d9..fe1d4c3df5e71e67fe91f00188e44018fb43135a 100644 (file)
@@ -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
index 2eeb214e9d1da6ef1a64d73a38a96ddcb353672b..7cedb7998f4e8547e0628b85a5c4d3335d7c5f4b 100644 (file)
       <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>
       <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>
index 0223226469b72e021747eb102e5d0ed03c2ebb30..1ece57479302f19fb883157cbfacfbfce13225a1 100644 (file)
@@ -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
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 (file)
index 0000000..435d4bb
--- /dev/null
@@ -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);
+        }
+    }
+}
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 (file)
index 0000000..65ca6de
--- /dev/null
@@ -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));
+        }
+    }
+}