AWS signing without AWS (#16840)

Signed-off-by: Martin Grześlowski <martin.grzeslowski@gmail.com>
This commit is contained in:
Martin 2024-06-14 21:06:44 +02:00 committed by GitHub
parent f288150843
commit c2d1789cd4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 150 additions and 37 deletions

View 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

View File

@ -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>

View 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

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}