From c2d1789cd4e9d16b9d67472388eb528456b45721 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 14 Jun 2024 21:06:44 +0200 Subject: [PATCH] AWS signing without AWS (#16840) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Martin Grześlowski --- bundles/org.openhab.binding.salus/NOTICE | 6 +- bundles/org.openhab.binding.salus/pom.xml | 14 ++-- .../salus/internal/aws/http/AwsSalusApi.java | 37 ++-------- .../salus/internal/aws/http/AwsSigner.java | 56 ++++++++++++++ .../internal/aws/http/AwsSignerTest.java | 74 +++++++++++++++++++ 5 files changed, 150 insertions(+), 37 deletions(-) create mode 100644 bundles/org.openhab.binding.salus/src/main/java/org/openhab/binding/salus/internal/aws/http/AwsSigner.java create mode 100644 bundles/org.openhab.binding.salus/src/test/java/org/openhab/binding/salus/internal/aws/http/AwsSignerTest.java 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)); + } + } +}