From 3072bcc35136d768914b03873d619382323a0d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Sugawara=20=28=E2=88=A9=EF=BD=80-=C2=B4=29?= =?UTF-8?q?=E2=8A=83=E2=94=81=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E=E7=82=8E?= Date: Wed, 21 Feb 2024 14:46:41 -0800 Subject: [PATCH] Remove the default use of the legacy signer --- .../presigner/DefaultPollyPresigner.java | 197 +++++++++++++----- .../presigner/DefaultPollyPresignerTest.java | 42 +++- 2 files changed, 184 insertions(+), 55 deletions(-) diff --git a/services/polly/src/main/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresigner.java b/services/polly/src/main/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresigner.java index a02a7a94be58..44b99005b5b5 100644 --- a/services/polly/src/main/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresigner.java +++ b/services/polly/src/main/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresigner.java @@ -16,14 +16,17 @@ package software.amazon.awssdk.services.polly.internal.presigner; import static java.util.stream.Collectors.toMap; -import static software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute.PRESIGNER_EXPIRATION; import java.net.URI; +import java.time.Clock; +import java.time.Duration; import java.time.Instant; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; @@ -41,6 +44,7 @@ import software.amazon.awssdk.awscore.presigner.PresignRequest; import software.amazon.awssdk.awscore.presigner.PresignedRequest; import software.amazon.awssdk.core.ClientType; +import software.amazon.awssdk.core.SelectedAuthScheme; import software.amazon.awssdk.core.interceptor.ExecutionAttributes; import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; @@ -48,9 +52,17 @@ import software.amazon.awssdk.core.signer.Signer; import software.amazon.awssdk.http.SdkHttpFullRequest; import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.auth.aws.scheme.AwsV4AuthScheme; +import software.amazon.awssdk.http.auth.aws.signer.AwsV4FamilyHttpSigner; +import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner; import software.amazon.awssdk.http.auth.spi.scheme.AuthScheme; +import software.amazon.awssdk.http.auth.spi.scheme.AuthSchemeOption; +import software.amazon.awssdk.http.auth.spi.signer.HttpSigner; +import software.amazon.awssdk.http.auth.spi.signer.SignRequest; +import software.amazon.awssdk.http.auth.spi.signer.SignedRequest; import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.identity.spi.Identity; import software.amazon.awssdk.identity.spi.IdentityProvider; import software.amazon.awssdk.identity.spi.IdentityProviders; import software.amazon.awssdk.profiles.ProfileFile; @@ -74,8 +86,7 @@ public final class DefaultPollyPresigner implements PollyPresigner { private static final String SIGNING_NAME = "polly"; private static final String SERVICE_NAME = "polly"; - private static final Aws4Signer DEFAULT_SIGNER = Aws4Signer.create(); - + private final Clock signingClock; private final Supplier profileFile; private final String profileName; private final Region region; @@ -85,6 +96,8 @@ public final class DefaultPollyPresigner implements PollyPresigner { private final Boolean fipsEnabled; private DefaultPollyPresigner(BuilderImpl builder) { + this.signingClock = builder.signingClock != null ? builder.signingClock + : Clock.systemUTC(); this.profileFile = ProfileFile::defaultProfileFile; this.profileName = ProfileFileSystemSetting.AWS_PROFILE.getStringValueOrThrow(); this.region = builder.region != null ? builder.region @@ -124,22 +137,28 @@ public void close() { IoUtils.closeIfCloseable(credentialsProvider, null); } + // For testing + static PollyPresigner.Builder builder(Clock signingClock) { + return new BuilderImpl() + .signingClock(signingClock); + } + public static PollyPresigner.Builder builder() { return new BuilderImpl(); } @Override public PresignedSynthesizeSpeechRequest presignSynthesizeSpeech( - SynthesizeSpeechPresignRequest synthesizeSpeechPresignRequest) { + SynthesizeSpeechPresignRequest synthesizeSpeechPresignRequest) { return presign(PresignedSynthesizeSpeechRequest.builder(), synthesizeSpeechPresignRequest, synthesizeSpeechPresignRequest.synthesizeSpeechRequest(), SynthesizeSpeechRequestMarshaller.getInstance()::marshall) - .build(); + .build(); } private SdkHttpFullRequest marshallRequest( - T request, Function marshalFn) { + T request, Function marshalFn) { SdkHttpFullRequest.Builder requestBuilder = marshalFn.apply(request); applyOverrideHeadersAndQueryParams(requestBuilder, request); applyEndpoint(requestBuilder); @@ -150,13 +169,19 @@ private SdkHttpFullRequest marshallRequest( * Generate a {@link PresignedRequest} from a {@link PresignedRequest} and {@link PollyRequest}. */ private T presign(T presignedRequest, - PresignRequest presignRequest, - U requestToPresign, - Function requestMarshaller) { + PresignRequest presignRequest, + U requestToPresign, + Function requestMarshaller) { ExecutionAttributes execAttrs = createExecutionAttributes(presignRequest, requestToPresign); - SdkHttpFullRequest marshalledRequest = marshallRequest(requestToPresign, requestMarshaller); - SdkHttpFullRequest signedHttpRequest = presignRequest(requestToPresign, marshalledRequest, execAttrs); + Presigner presigner = resolvePresigner(requestToPresign); + SdkHttpFullRequest signedHttpRequest = null; + if (presigner != null) { + signedHttpRequest = presignRequest(presigner, requestToPresign, marshalledRequest, execAttrs); + } else { + SelectedAuthScheme authScheme = selectedAuthScheme(requestToPresign, execAttrs); + signedHttpRequest = doSraPresign(marshalledRequest, authScheme, presignRequest.signatureDuration()); + } initializePresignedRequest(presignedRequest, execAttrs, signedHttpRequest); return presignedRequest; } @@ -167,54 +192,111 @@ private void initializePresignedRequest(PresignedRequest.Builder presignedReques List signedHeadersQueryParam = signedHttpRequest.firstMatchingRawQueryParameters("X-Amz-SignedHeaders"); Map> signedHeaders = - signedHeadersQueryParam.stream() - .flatMap(h -> Stream.of(h.split(";"))) - .collect(toMap(h -> h, h -> signedHttpRequest.firstMatchingHeader(h) - .map(Collections::singletonList) - .orElseGet(ArrayList::new))); + signedHeadersQueryParam.stream() + .flatMap(h -> Stream.of(h.split(";"))) + .collect(toMap(h -> h, h -> signedHttpRequest.firstMatchingHeader(h) + .map(Collections::singletonList) + .orElseGet(ArrayList::new))); boolean isBrowserExecutable = signedHttpRequest.method() == SdkHttpMethod.GET && - (signedHeaders.isEmpty() || - (signedHeaders.size() == 1 && signedHeaders.containsKey("host"))); + (signedHeaders.isEmpty() || + (signedHeaders.size() == 1 && signedHeaders.containsKey("host"))); - presignedRequest.expiration(execAttrs.getAttribute(PRESIGNER_EXPIRATION)) - .isBrowserExecutable(isBrowserExecutable) - .httpRequest(signedHttpRequest) - .signedHeaders(signedHeaders); + presignedRequest.expiration(execAttrs.getAttribute(AwsSignerExecutionAttribute.PRESIGNER_EXPIRATION)) + .isBrowserExecutable(isBrowserExecutable) + .httpRequest(signedHttpRequest) + .signedHeaders(signedHeaders); } - private SdkHttpFullRequest presignRequest(PollyRequest requestToPresign, + private SdkHttpFullRequest presignRequest(Presigner presigner, + PollyRequest requestToPresign, SdkHttpFullRequest marshalledRequest, ExecutionAttributes executionAttributes) { - Presigner presigner = resolvePresigner(requestToPresign); SdkHttpFullRequest presigned = presigner.presign(marshalledRequest, executionAttributes); List signedHeadersQueryParam = presigned.firstMatchingRawQueryParameters("X-Amz-SignedHeaders"); Validate.validState(!signedHeadersQueryParam.isEmpty(), - "Only SigV4 presigners are supported at this time, but the configured " - + "presigner (%s) did not seem to generate a SigV4 signature.", presigner); + "Only SigV4 presigners are supported at this time, but the configured " + + "presigner (%s) did not seem to generate a SigV4 signature.", presigner); return presigned; } + private SdkHttpFullRequest doSraPresign(SdkHttpFullRequest request, + SelectedAuthScheme selectedAuthScheme, + Duration expirationDuration) { + CompletableFuture identityFuture = selectedAuthScheme.identity(); + T identity = CompletableFutureUtils.joinLikeSync(identityFuture); + + // presigned url puts auth info in query string, does not sign the payload, and has an expiry. + SignRequest.Builder signRequestBuilder = SignRequest + .builder(identity) + .putProperty(AwsV4FamilyHttpSigner.AUTH_LOCATION, AwsV4FamilyHttpSigner.AuthLocation.QUERY_STRING) + .putProperty(AwsV4FamilyHttpSigner.EXPIRATION_DURATION, expirationDuration) + .putProperty(HttpSigner.SIGNING_CLOCK, signingClock) + .request(request) + .payload(request.contentStreamProvider().orElse(null)); + AuthSchemeOption authSchemeOption = selectedAuthScheme.authSchemeOption(); + authSchemeOption.forEachSignerProperty(signRequestBuilder::putProperty); + + HttpSigner signer = selectedAuthScheme.signer(); + SignedRequest signedRequest = signer.sign(signRequestBuilder.build()); + return toSdkHttpFullRequest(signedRequest); + } + + private SelectedAuthScheme selectedAuthScheme(PollyRequest requestToPresign, + ExecutionAttributes attributes) { + AuthScheme authScheme = AwsV4AuthScheme.create(); + AwsCredentialsIdentity credentialsIdentity = resolveCredentials(resolveCredentialsProvider(requestToPresign)); + AuthSchemeOption.Builder optionBuilder = AuthSchemeOption.builder() + .schemeId(authScheme.schemeId()); + optionBuilder.putSignerProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, SERVICE_NAME); + String region = attributes.getAttribute(AwsExecutionAttribute.AWS_REGION).id(); + optionBuilder.putSignerProperty(AwsV4HttpSigner.REGION_NAME, region); + return new SelectedAuthScheme<>(CompletableFuture.completedFuture(credentialsIdentity), authScheme.signer(), + optionBuilder.build()); + } + + private SdkHttpFullRequest toSdkHttpFullRequest(SignedRequest signedRequest) { + SdkHttpRequest request = signedRequest.request(); + + return SdkHttpFullRequest.builder() + .contentStreamProvider(signedRequest.payload().orElse(null)) + .protocol(request.protocol()) + .method(request.method()) + .host(request.host()) + .port(request.port()) + .encodedPath(request.encodedPath()) + .applyMutation(r -> request.forEachHeader(r::putHeader)) + .applyMutation(r -> request.forEachRawQueryParameter(r::putRawQueryParameter)) + .build(); + } + private ExecutionAttributes createExecutionAttributes(PresignRequest presignRequest, PollyRequest requestToPresign) { - Instant signatureExpiration = Instant.now().plus(presignRequest.signatureDuration()); + // A fixed signingClock is used, so that the current time used by the signing logic, as well as to determine expiration + // are the same. + Instant signingInstant = signingClock.instant(); + Clock signingClockOverride = Clock.fixed(signingInstant, ZoneOffset.UTC); + Duration expirationDuration = presignRequest.signatureDuration(); + Instant signatureExpiration = signingInstant.plus(expirationDuration); + AwsCredentialsIdentity credentials = resolveCredentials(resolveCredentialsProvider(requestToPresign)); Validate.validState(credentials != null, "Credential providers must never return null."); return new ExecutionAttributes() - .putAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS, CredentialUtils.toCredentials(credentials)) - .putAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, SIGNING_NAME) - .putAttribute(AwsExecutionAttribute.AWS_REGION, region) - .putAttribute(AwsSignerExecutionAttribute.SIGNING_REGION, region) - .putAttribute(SdkInternalExecutionAttribute.IS_FULL_DUPLEX, false) - .putAttribute(SdkExecutionAttribute.CLIENT_TYPE, ClientType.SYNC) - .putAttribute(SdkExecutionAttribute.SERVICE_NAME, SERVICE_NAME) - .putAttribute(PRESIGNER_EXPIRATION, signatureExpiration) - .putAttribute(SdkInternalExecutionAttribute.AUTH_SCHEME_RESOLVER, PollyAuthSchemeProvider.defaultProvider()) - .putAttribute(SdkInternalExecutionAttribute.AUTH_SCHEMES, authSchemes()) - .putAttribute(SdkInternalExecutionAttribute.IDENTITY_PROVIDERS, - IdentityProviders.builder() - .putIdentityProvider(credentialsProvider()) - .build()); + .putAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS, CredentialUtils.toCredentials(credentials)) + .putAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME, SIGNING_NAME) + .putAttribute(AwsSignerExecutionAttribute.SIGNING_CLOCK, signingClockOverride) + .putAttribute(AwsSignerExecutionAttribute.PRESIGNER_EXPIRATION, signatureExpiration) + .putAttribute(AwsExecutionAttribute.AWS_REGION, region) + .putAttribute(AwsSignerExecutionAttribute.SIGNING_REGION, region) + .putAttribute(SdkInternalExecutionAttribute.IS_FULL_DUPLEX, false) + .putAttribute(SdkExecutionAttribute.CLIENT_TYPE, ClientType.SYNC) + .putAttribute(SdkExecutionAttribute.SERVICE_NAME, SERVICE_NAME) + .putAttribute(SdkInternalExecutionAttribute.AUTH_SCHEME_RESOLVER, PollyAuthSchemeProvider.defaultProvider()) + .putAttribute(SdkInternalExecutionAttribute.AUTH_SCHEMES, authSchemes()) + .putAttribute(SdkInternalExecutionAttribute.IDENTITY_PROVIDERS, + IdentityProviders.builder() + .putIdentityProvider(credentialsProvider()) + .build()); } private Map> authSchemes() { @@ -224,7 +306,7 @@ private Map> authSchemes() { private IdentityProvider resolveCredentialsProvider(PollyRequest request) { return request.overrideConfiguration().flatMap(AwsRequestOverrideConfiguration::credentialsIdentityProvider) - .orElse(credentialsProvider); + .orElse(credentialsProvider); } private AwsCredentialsIdentity resolveCredentials(IdentityProvider credentialsProvider) { @@ -233,10 +315,13 @@ private AwsCredentialsIdentity resolveCredentials(IdentityProvider credentialsProvider; private URI endpointOverride; private Boolean dualstackEnabled; private Boolean fipsEnabled; + public Builder signingClock(Clock signingClock) { + this.signingClock = signingClock; + return this; + } + @Override public Builder region(Region region) { this.region = region; diff --git a/services/polly/src/test/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresignerTest.java b/services/polly/src/test/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresignerTest.java index 30491ef287ea..09c9133d7156 100644 --- a/services/polly/src/test/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresignerTest.java +++ b/services/polly/src/test/java/software/amazon/awssdk/services/polly/internal/presigner/DefaultPollyPresignerTest.java @@ -23,12 +23,16 @@ import java.io.IOException; import java.net.URI; import java.net.URL; +import java.time.Clock; import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.auth.signer.Aws4Signer; import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; import software.amazon.awssdk.identity.spi.IdentityProvider; @@ -43,6 +47,7 @@ * Tests for {@link DefaultPollyPresigner}. */ class DefaultPollyPresignerTest { + private static final String EXPECTED_SIGNATURE = "06e9f816eaa937ba5a7eb5e536d3339276bd5f90c2a351b9e21cd57a0a6921be"; private static final SynthesizeSpeechRequest BASIC_SYNTHESIZE_SPEECH_REQUEST = SynthesizeSpeechRequest.builder() .voiceId("Salli") .outputFormat(OutputFormat.PCM) @@ -62,7 +67,8 @@ void presign_requestLevelCredentials_honored() { AwsBasicCredentials.create("akid2", "skid2") ); - PollyPresigner presigner = DefaultPollyPresigner.builder() + Instant fixedTime = Instant.parse("2024-02-20T22:00:00Z"); + PollyPresigner presigner = DefaultPollyPresigner.builder(Clock.fixed(fixedTime, ZoneId.of("UTC"))) .region(Region.US_EAST_1) .credentialsProvider(credentialsProvider) .build(); @@ -78,8 +84,40 @@ void presign_requestLevelCredentials_honored() { .build(); PresignedSynthesizeSpeechRequest presignedSynthesizeSpeechRequest = presigner.presignSynthesizeSpeech(presignRequest); + String query = presignedSynthesizeSpeechRequest.url().getQuery(); + assertThat(query).contains("X-Amz-Credential=akid2"); + assertThat(query).contains("X-Amz-Signature=" + EXPECTED_SIGNATURE); + } + + @Test + void presign_requestLevelSingerAndCredentials_honored() { + IdentityProvider requestCedentialsProvider = StaticCredentialsProvider.create( + AwsBasicCredentials.create("akid2", "skid2") + ); + + Instant fixedTime = Instant.parse("2024-02-20T22:00:00Z"); + PollyPresigner presigner = DefaultPollyPresigner.builder(Clock.fixed(fixedTime, ZoneId.of("UTC"))) + .region(Region.US_EAST_1) + .credentialsProvider(credentialsProvider) + .build(); + + SynthesizeSpeechRequest synthesizeSpeechRequest = + BASIC_SYNTHESIZE_SPEECH_REQUEST.toBuilder() + .overrideConfiguration(AwsRequestOverrideConfiguration + .builder() + .signer(Aws4Signer.create()) + .credentialsProvider(requestCedentialsProvider).build()) + .build(); - assertThat(presignedSynthesizeSpeechRequest.url().getQuery()).contains("X-Amz-Credential=akid2"); + SynthesizeSpeechPresignRequest presignRequest = SynthesizeSpeechPresignRequest.builder() + .synthesizeSpeechRequest(synthesizeSpeechRequest) + .signatureDuration(Duration.ofHours(3)) + .build(); + + PresignedSynthesizeSpeechRequest presignedSynthesizeSpeechRequest = presigner.presignSynthesizeSpeech(presignRequest); + String query = presignedSynthesizeSpeechRequest.url().getQuery(); + assertThat(query).contains("X-Amz-Credential=akid2"); + assertThat(query).contains("X-Amz-Signature=" + EXPECTED_SIGNATURE); } @Test