From f3cc9d76d2bd65a9e7ba4bb8ae1df0c6d5e95548 Mon Sep 17 00:00:00 2001 From: jencymaryjoseph <35571282+jencymaryjoseph@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:50:04 -0700 Subject: [PATCH 1/2] Enable checksum validation for presigned URL downloads --- .../AsyncPresignedUrlExtensionTestSuite.java | 56 ++++++ .../handlers/GetObjectInterceptor.java | 4 +- .../DefaultAsyncPresignedUrlExtension.java | 36 +++- ...PresignedUrlDownloadRequestMarshaller.java | 39 +++- .../AsyncPresignedUrlExtension.java | 8 + .../services/s3/presigner/S3Presigner.java | 6 + ...DefaultAsyncPresignedUrlExtensionTest.java | 65 +++++++ ...gnedUrlChecksumValidationWiremockTest.java | 169 ++++++++++++++++++ ...ignedUrlDownloadRequestMarshallerTest.java | 63 +++++++ 9 files changed, 439 insertions(+), 7 deletions(-) create mode 100644 services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/presignedurl/PresignedUrlChecksumValidationWiremockTest.java diff --git a/services/s3/src/it/java/software/amazon/awssdk/services/s3/presignedurl/AsyncPresignedUrlExtensionTestSuite.java b/services/s3/src/it/java/software/amazon/awssdk/services/s3/presignedurl/AsyncPresignedUrlExtensionTestSuite.java index d32c7038317a..1ff737b52850 100644 --- a/services/s3/src/it/java/software/amazon/awssdk/services/s3/presignedurl/AsyncPresignedUrlExtensionTestSuite.java +++ b/services/s3/src/it/java/software/amazon/awssdk/services/s3/presignedurl/AsyncPresignedUrlExtensionTestSuite.java @@ -45,6 +45,7 @@ import software.amazon.awssdk.metrics.MetricPublisher; import software.amazon.awssdk.services.s3.S3AsyncClient; import software.amazon.awssdk.services.s3.S3IntegrationTestBase; +import software.amazon.awssdk.services.s3.model.ChecksumMode; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectResponse; import software.amazon.awssdk.services.s3.model.PutObjectRequest; @@ -280,6 +281,61 @@ void getObject_emptyObject_withRange_shouldThrow416() throws Exception { .hasCauseInstanceOf(S3Exception.class); } + @Test + void getObject_withChecksumModeEnabled_returnsChecksumHeaders() throws Exception { + PresignedGetObjectRequest presigned = presigner.presignGetObject(r -> r + .getObjectRequest(req -> req.bucket(testBucket).key(testGetObjectKey) + .checksumMode(ChecksumMode.ENABLED)) + .signatureDuration(Duration.ofMinutes(10))); + + PresignedUrlDownloadRequest request = PresignedUrlDownloadRequest.builder() + .presignedUrl(presigned.url()) + .build(); + + ResponseBytes response = + presignedUrlExtension.getObject(request, AsyncResponseTransformer.toBytes()) + .get(30, TimeUnit.SECONDS); + + assertThat(response.asUtf8String()).isEqualTo(testObjectContent); + assertThat(response.response().checksumTypeAsString()).isNotNull(); + } + + @Test + void getObject_withoutChecksumMode_doesNotReturnChecksumHeaders() throws Exception { + PresignedUrlDownloadRequest request = createRequestForKey(testGetObjectKey); + + ResponseBytes response = + presignedUrlExtension.getObject(request, AsyncResponseTransformer.toBytes()) + .get(30, TimeUnit.SECONDS); + + assertThat(response.asUtf8String()).isEqualTo(testObjectContent); + assertThat(response.response().checksumTypeAsString()).isNull(); + } + + @Test + void getObject_withChecksumModeEnabled_requestContainsChecksumHeader() throws Exception { + PresignedGetObjectRequest presigned = presigner.presignGetObject(r -> r + .getObjectRequest(req -> req.bucket(testBucket).key(testGetObjectKey) + .checksumMode(ChecksumMode.ENABLED)) + .signatureDuration(Duration.ofMinutes(10))); + + // Verify the presigned URL has checksum-mode in SignedHeaders + assertThat(presigned.signedHeaders()).containsKey("x-amz-checksum-mode"); + assertThat(presigned.isBrowserExecutable()).isFalse(); + + PresignedUrlDownloadRequest request = PresignedUrlDownloadRequest.builder() + .presignedUrl(presigned.url()) + .build(); + + // Download should succeed (proves marshaller sent the header) + ResponseBytes response = + presignedUrlExtension.getObject(request, AsyncResponseTransformer.toBytes()) + .get(30, TimeUnit.SECONDS); + + assertThat(response).isNotNull(); + assertThat(response.asUtf8String()).isEqualTo(testObjectContent); + } + static Stream basicFunctionalityTestData() { return Stream.of( Arguments.of("getObject_withValidUrl_returnsContent", diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/GetObjectInterceptor.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/GetObjectInterceptor.java index 7e13f6b97473..5929765d3b29 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/GetObjectInterceptor.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/handlers/GetObjectInterceptor.java @@ -28,6 +28,7 @@ import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; import software.amazon.awssdk.core.internal.util.HttpChecksumUtils; import software.amazon.awssdk.http.SdkHttpResponse; +import software.amazon.awssdk.services.s3.internal.presignedurl.model.PresignedUrlDownloadRequestWrapper; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectResponse; import software.amazon.awssdk.utils.Pair; @@ -43,7 +44,8 @@ public class GetObjectInterceptor implements ExecutionInterceptor { @Override public void afterTransmission(Context.AfterTransmission context, ExecutionAttributes executionAttributes) { - if (!(context.request() instanceof GetObjectRequest)) { + if (!(context.request() instanceof GetObjectRequest) + && !(context.request() instanceof PresignedUrlDownloadRequestWrapper)) { return; } ChecksumSpecs resolvedChecksumSpecs = executionAttributes.getAttribute(SdkExecutionAttribute.RESOLVED_CHECKSUM_SPECS); diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presignedurl/DefaultAsyncPresignedUrlExtension.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presignedurl/DefaultAsyncPresignedUrlExtension.java index d640482c5c36..fafe9dfd3623 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presignedurl/DefaultAsyncPresignedUrlExtension.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presignedurl/DefaultAsyncPresignedUrlExtension.java @@ -27,6 +27,7 @@ import software.amazon.awssdk.annotations.SdkInternalApi; import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.awscore.internal.AwsProtocolMetadata; +import software.amazon.awssdk.checksums.DefaultChecksumAlgorithm; import software.amazon.awssdk.core.async.AsyncResponseTransformer; import software.amazon.awssdk.core.async.AsyncResponseTransformerUtils; import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; @@ -37,6 +38,7 @@ import software.amazon.awssdk.core.exception.SdkClientException; import software.amazon.awssdk.core.http.HttpResponseHandler; import software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute; +import software.amazon.awssdk.core.interceptor.trait.HttpChecksum; import software.amazon.awssdk.core.metrics.CoreMetric; import software.amazon.awssdk.core.signer.NoOpSigner; import software.amazon.awssdk.metrics.MetricCollector; @@ -61,12 +63,31 @@ public final class DefaultAsyncPresignedUrlExtension implements AsyncPresignedUrlExtension { private static final Logger log = LoggerFactory.getLogger(DefaultAsyncPresignedUrlExtension.class); + /** + * Checksum configuration matching the codegen-produced GetObject operation. + * Enables the HttpChecksumValidationInterceptor to validate response checksums. + */ + private static final HttpChecksum RESPONSE_CHECKSUM_CONFIG = HttpChecksum.builder() + .requestValidationMode("ENABLED") + .responseAlgorithmsV2( + DefaultChecksumAlgorithm.XXHASH3, + DefaultChecksumAlgorithm.XXHASH128, + DefaultChecksumAlgorithm.CRC64NVME, + DefaultChecksumAlgorithm.CRC32C, + DefaultChecksumAlgorithm.CRC32, + DefaultChecksumAlgorithm.XXHASH64, + DefaultChecksumAlgorithm.SHA512, + DefaultChecksumAlgorithm.SHA256, + DefaultChecksumAlgorithm.SHA1, + DefaultChecksumAlgorithm.MD5) + .build(); + private final AsyncClientHandler clientHandler; private final AwsS3ProtocolFactory protocolFactory; private final SdkClientConfiguration clientConfiguration; private final List metricPublishers; private final AwsProtocolMetadata protocolMetadata; - + public DefaultAsyncPresignedUrlExtension(AsyncClientHandler clientHandler, AwsS3ProtocolFactory protocolFactory, SdkClientConfiguration clientConfiguration, @@ -122,6 +143,8 @@ public CompletableFuture getObject( .withMetricCollector(apiCallMetricCollector) // TODO: Deprecate IS_DISCOVERED_ENDPOINT, use new SKIP_ENDPOINT_RESOLUTION for better semantics .putExecutionAttribute(SdkInternalExecutionAttribute.IS_DISCOVERED_ENDPOINT, true) + .putExecutionAttribute(SdkInternalExecutionAttribute.HTTP_CHECKSUM, + checksumConfigForUrl(presignedUrlDownloadRequest.presignedUrl())) .withMarshaller(new PresignedUrlDownloadRequestMarshaller(protocolFactory)), asyncResponseTransformer); @@ -151,4 +174,15 @@ private SdkClientConfiguration updateSdkClientConfiguration(SdkClientConfigurati return configBuilder.build(); } + /** + * Returns the checksum validation config if the presigned URL was signed with checksum mode, + * or null otherwise. + */ + private static HttpChecksum checksumConfigForUrl(java.net.URL presignedUrl) { + if (PresignedUrlDownloadRequestMarshaller.hasChecksumModeInSignedHeaders(presignedUrl.getQuery())) { + return RESPONSE_CHECKSUM_CONFIG; + } + return null; + } + } diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presignedurl/PresignedUrlDownloadRequestMarshaller.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presignedurl/PresignedUrlDownloadRequestMarshaller.java index f0aecfce87da..b37aec335184 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presignedurl/PresignedUrlDownloadRequestMarshaller.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presignedurl/PresignedUrlDownloadRequestMarshaller.java @@ -63,14 +63,43 @@ public SdkHttpFullRequest marshall(PresignedUrlDownloadRequestWrapper presignedU .createProtocolMarshaller(SDK_OPERATION_BINDING); URI presignedUri = presignedUrlDownloadRequestWrapper.url().toURI(); - return protocolMarshaller.marshall(presignedUrlDownloadRequestWrapper) - .toBuilder() - .uri(presignedUri) - .build(); + SdkHttpFullRequest.Builder requestBuilder = protocolMarshaller + .marshall(presignedUrlDownloadRequestWrapper) + .toBuilder() + .uri(presignedUri); + + addChecksumModeHeaderIfSignedInUrl(requestBuilder, presignedUri); + + return requestBuilder.build(); } catch (Exception e) { throw SdkClientException.builder() .message("Unable to marshall pre-signed URL Request: " + e.getMessage()) .cause(e).build(); } } -} \ No newline at end of file + + /** + * If the presigned URL's X-Amz-SignedHeaders contains "x-amz-checksum-mode", automatically add + * the header with value "ENABLED" so S3 returns checksum headers in the response. + */ + private void addChecksumModeHeaderIfSignedInUrl(SdkHttpFullRequest.Builder requestBuilder, URI uri) { + if (hasChecksumModeInSignedHeaders(uri.getQuery())) { + requestBuilder.putHeader("x-amz-checksum-mode", "ENABLED"); + } + } + + /** + * Returns true if the decoded query string's X-Amz-SignedHeaders parameter contains "x-amz-checksum-mode". + */ + static boolean hasChecksumModeInSignedHeaders(String query) { + if (query == null) { + return false; + } + for (String param : query.split("&")) { + if (param.startsWith("X-Amz-SignedHeaders=")) { + return param.substring("X-Amz-SignedHeaders=".length()).contains("x-amz-checksum-mode"); + } + } + return false; + } +} diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/presignedurl/AsyncPresignedUrlExtension.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/presignedurl/AsyncPresignedUrlExtension.java index 545eb838c35e..11103a101580 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/presignedurl/AsyncPresignedUrlExtension.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/presignedurl/AsyncPresignedUrlExtension.java @@ -29,6 +29,14 @@ /** * Interface for executing S3 operations asynchronously using presigned URLs. This can be accessed using * {@link S3AsyncClient#presignedUrlExtension()}. + * + *

Checksum Validation: If the presigned URL was generated with + * {@link software.amazon.awssdk.services.s3.presigner.S3Presigner#presignGetObject} using + * {@code checksumMode(ChecksumMode.ENABLED)}, the SDK automatically sends the required header + * and S3 returns checksums for full object downloads (HTTP 200). For ranged downloads (HTTP 206), + * checksums are only returned for multipart-uploaded objects when the range aligns with original + * upload part boundaries. The downloader cannot enable checksums if the URL was not presigned + * with checksum mode. */ @SdkPublicApi public interface AsyncPresignedUrlExtension { diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/presigner/S3Presigner.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/presigner/S3Presigner.java index e23516178e92..b5fd634255fa 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/presigner/S3Presigner.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/presigner/S3Presigner.java @@ -294,6 +294,12 @@ static Builder builder() { * signing or authentication. *

* + * Checksum support: Setting {@code checksumMode(ChecksumMode.ENABLED)} on the + * {@code GetObjectRequest} enables the downloader to receive checksums from S3 for data integrity + * validation. The resulting URL will not be browser-executable (requires the + * {@code x-amz-checksum-mode} header at download time, which the SDK sends automatically). + *

+ * * Example Usage *

* diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/presignedurl/DefaultAsyncPresignedUrlExtensionTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/presignedurl/DefaultAsyncPresignedUrlExtensionTest.java index 91681fbb8e15..9fed4335aa5a 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/presignedurl/DefaultAsyncPresignedUrlExtensionTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/presignedurl/DefaultAsyncPresignedUrlExtensionTest.java @@ -341,6 +341,71 @@ private void assertSuccessfulGetObject(PresignedUrlDownloadRequest request) { } } + @ParameterizedTest(name = "{0}") + @MethodSource("checksumValidationTestCases") + void getObject_withChecksumHeaderInResponse_shouldDownloadSuccessfully( + String testName, + HttpExecuteResponse response, + String testUrl, + boolean expectSuccess) throws Exception { + + mockHttpClient.stubNextResponse(response); + + URL presignedUrl = new URL(testUrl); + PresignedUrlDownloadRequest request = PresignedUrlDownloadRequest.builder() + .presignedUrl(presignedUrl) + .build(); + + CompletableFuture> future = + presignedUrlExtension.getObject(request, AsyncResponseTransformer.toBytes()); + + ResponseBytes result = future.get(); + assertThat(result).isNotNull(); + assertThat(result.asUtf8String()).isEqualTo(TEST_CONTENT); + } + + private static Stream checksumValidationTestCases() { + // CRC32 of "test-content" = 0x6B59FCDE → base64 = a1n83g== + String correctCrc32 = "a1n83g=="; + String urlWithChecksumSigned = "https://test-bucket.s3.us-east-1.amazonaws.com/test-key?" + + "X-Amz-Algorithm=AWS4-HMAC-SHA256&" + + "X-Amz-Date=20250707T000000Z&" + + "X-Amz-SignedHeaders=host%3Bx-amz-checksum-mode&" + + "X-Amz-Signature=test-signature&" + + "X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20250707%2Fus-east-1%2Fs3%2Faws4_request&" + + "X-Amz-Expires=600"; + + return Stream.of( + Arguments.of( + "With checksum in response - should succeed", + createResponseWithChecksum("x-amz-checksum-crc32", correctCrc32), + urlWithChecksumSigned, + true + ), + Arguments.of( + "No checksum header in response - should succeed without validation", + createSuccessResponse(), + TEST_URL, + true + ) + ); + } + + private static HttpExecuteResponse createResponseWithChecksum(String checksumHeader, String checksumValue) { + SdkHttpFullResponse httpResponse = SdkHttpFullResponse.builder() + .statusCode(200) + .putHeader("Content-Length", "12") + .putHeader("ETag", "\"test-etag\"") + .putHeader("Content-Type", "text/plain") + .putHeader(checksumHeader, checksumValue) + .build(); + return HttpExecuteResponse.builder() + .response(httpResponse) + .responseBody(AbortableInputStream.create( + new ByteArrayInputStream(TEST_CONTENT.getBytes(StandardCharsets.UTF_8)))) + .build(); + } + private static HttpExecuteResponse createSuccessResponse() { SdkHttpFullResponse httpResponse = SdkHttpFullResponse.builder() .statusCode(200) diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/presignedurl/PresignedUrlChecksumValidationWiremockTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/presignedurl/PresignedUrlChecksumValidationWiremockTest.java new file mode 100644 index 000000000000..0cbea3bcf60d --- /dev/null +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/presignedurl/PresignedUrlChecksumValidationWiremockTest.java @@ -0,0 +1,169 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.awssdk.services.s3.internal.presignedurl; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.presignedurl.model.PresignedUrlDownloadRequest; + +/** + * WireMock test verifying checksum validation behavior for presigned URL downloads. + */ +@WireMockTest +class PresignedUrlChecksumValidationWiremockTest { + + private static final String BODY = "test-content-for-checksum"; + // Correct CRC32 of "test-content-for-checksum" encoded as base64 + private static final String CORRECT_CRC32 = "HUUjuQ=="; // CRC32 of "test-content-for-checksum" + private static final String INCORRECT_CRC32 = "AAAAAAAA=="; + + private S3AsyncClient s3Client; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmInfo) { + s3Client = S3AsyncClient.builder() + .endpointOverride(java.net.URI.create(wmInfo.getHttpBaseUrl())) + .region(software.amazon.awssdk.regions.Region.US_EAST_1) + .credentialsProvider(software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider.create()) + .forcePathStyle(true) + .build(); + } + + @AfterEach + void tearDown() { + if (s3Client != null) { + s3Client.close(); + } + } + + @Test + void getObject_withMatchingChecksum_shouldSucceed(WireMockRuntimeInfo wmInfo) throws Exception { + stubFor(get(urlPathEqualTo("/test-key")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Length", String.valueOf(BODY.length())) + .withHeader("x-amz-checksum-crc32", CORRECT_CRC32) + .withHeader("x-amz-checksum-type", "FULL_OBJECT") + .withBody(BODY))); + + URL presignedUrl = new URL(wmInfo.getHttpBaseUrl() + "/test-key?" + + "X-Amz-Algorithm=AWS4-HMAC-SHA256&" + + "X-Amz-SignedHeaders=host%3Bx-amz-checksum-mode&" + + "X-Amz-Signature=fake&" + + "X-Amz-Expires=600"); + + ResponseBytes result = s3Client.presignedUrlExtension() + .getObject( + PresignedUrlDownloadRequest.builder().presignedUrl(presignedUrl).build(), + AsyncResponseTransformer.toBytes()) + .join(); + + assertThat(result.asUtf8String()).isEqualTo(BODY); + } + + @Test + void getObject_withMismatchingChecksum_shouldThrow(WireMockRuntimeInfo wmInfo) throws Exception { + stubFor(get(urlPathEqualTo("/test-key")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Length", String.valueOf(BODY.length())) + .withHeader("x-amz-checksum-crc32", INCORRECT_CRC32) + .withHeader("x-amz-checksum-type", "FULL_OBJECT") + .withBody(BODY))); + + URL presignedUrl = new URL(wmInfo.getHttpBaseUrl() + "/test-key?" + + "X-Amz-Algorithm=AWS4-HMAC-SHA256&" + + "X-Amz-SignedHeaders=host%3Bx-amz-checksum-mode&" + + "X-Amz-Signature=fake&" + + "X-Amz-Expires=600"); + + CompletableFuture> future = s3Client.presignedUrlExtension() + .getObject( + PresignedUrlDownloadRequest.builder().presignedUrl(presignedUrl).build(), + AsyncResponseTransformer.toBytes()); + + assertThatThrownBy(future::join) + .hasCauseInstanceOf(SdkClientException.class) + .hasMessageContaining("checksum"); + } + + @Test + void getObject_withNoChecksumHeader_shouldSucceedWithoutValidation(WireMockRuntimeInfo wmInfo) throws Exception { + stubFor(get(urlPathEqualTo("/test-key")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Length", String.valueOf(BODY.length())) + .withBody(BODY))); + + URL presignedUrl = new URL(wmInfo.getHttpBaseUrl() + "/test-key?" + + "X-Amz-Algorithm=AWS4-HMAC-SHA256&" + + "X-Amz-SignedHeaders=host&" + + "X-Amz-Signature=fake&" + + "X-Amz-Expires=600"); + + ResponseBytes result = s3Client.presignedUrlExtension() + .getObject( + PresignedUrlDownloadRequest.builder().presignedUrl(presignedUrl).build(), + AsyncResponseTransformer.toBytes()) + .join(); + + assertThat(result.asUtf8String()).isEqualTo(BODY); + } + + @Test + void getObject_withCompositeChecksum_shouldSkipValidationAndSucceed(WireMockRuntimeInfo wmInfo) throws Exception { + // COMPOSITE checksum (suffix -3 indicates 3 parts) — cannot be validated by raw byte CRC + stubFor(get(urlPathEqualTo("/test-key")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Length", String.valueOf(BODY.length())) + .withHeader("x-amz-checksum-crc32", "i0T6cA==-3") + .withHeader("x-amz-checksum-type", "COMPOSITE") + .withBody(BODY))); + + URL presignedUrl = new URL(wmInfo.getHttpBaseUrl() + "/test-key?" + + "X-Amz-Algorithm=AWS4-HMAC-SHA256&" + + "X-Amz-SignedHeaders=host%3Bx-amz-checksum-mode&" + + "X-Amz-Signature=fake&" + + "X-Amz-Expires=600"); + + ResponseBytes result = s3Client.presignedUrlExtension() + .getObject( + PresignedUrlDownloadRequest.builder().presignedUrl(presignedUrl).build(), + AsyncResponseTransformer.toBytes()) + .join(); + + assertThat(result.asUtf8String()).isEqualTo(BODY); + } +} diff --git a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/presignedurl/PresignedUrlDownloadRequestMarshallerTest.java b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/presignedurl/PresignedUrlDownloadRequestMarshallerTest.java index b89794a2a59c..04c047cb4cfa 100644 --- a/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/presignedurl/PresignedUrlDownloadRequestMarshallerTest.java +++ b/services/s3/src/test/java/software/amazon/awssdk/services/s3/internal/presignedurl/PresignedUrlDownloadRequestMarshallerTest.java @@ -158,6 +158,69 @@ void marshall_withNullRequest_shouldThrowException() { .hasMessageContaining("presignedUrlDownloadRequestWrapper must not be null"); } + @Test + void marshall_withChecksumModeInSignedHeaders_shouldAddChecksumHeader() throws Exception { + URL urlWithChecksum = new URL("https://test-bucket.s3.us-east-1.amazonaws.com/test-key?" + + "X-Amz-Algorithm=AWS4-HMAC-SHA256&" + + "X-Amz-SignedHeaders=host%3Bx-amz-checksum-mode&" + + "X-Amz-Signature=abc123&" + + "X-Amz-Expires=600"); + + SdkHttpFullRequest baseRequest = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .protocol("https") + .host("example.com") + .build(); + when(mockProtocolMarshaller.marshall(any(PresignedUrlDownloadRequestWrapper.class))) + .thenReturn(baseRequest); + + PresignedUrlDownloadRequestWrapper request = PresignedUrlDownloadRequestWrapper.builder() + .url(urlWithChecksum) + .build(); + SdkHttpFullRequest result = marshaller.marshall(request); + + assertThat(result.headers()).containsKey("x-amz-checksum-mode"); + assertThat(result.firstMatchingHeader("x-amz-checksum-mode")).hasValue("ENABLED"); + } + + @Test + void marshall_withoutChecksumModeInSignedHeaders_shouldNotAddChecksumHeader() throws Exception { + SdkHttpFullRequest baseRequest = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .protocol("https") + .host("example.com") + .build(); + when(mockProtocolMarshaller.marshall(any(PresignedUrlDownloadRequestWrapper.class))) + .thenReturn(baseRequest); + + PresignedUrlDownloadRequestWrapper request = PresignedUrlDownloadRequestWrapper.builder() + .url(testUrl) + .build(); + SdkHttpFullRequest result = marshaller.marshall(request); + + assertThat(result.headers()).doesNotContainKey("x-amz-checksum-mode"); + } + + @Test + void marshall_withNoQueryString_shouldNotAddChecksumHeader() throws Exception { + URL urlNoQuery = new URL("https://test-bucket.s3.us-east-1.amazonaws.com/test-key"); + + SdkHttpFullRequest baseRequest = SdkHttpFullRequest.builder() + .method(SdkHttpMethod.GET) + .protocol("https") + .host("example.com") + .build(); + when(mockProtocolMarshaller.marshall(any(PresignedUrlDownloadRequestWrapper.class))) + .thenReturn(baseRequest); + + PresignedUrlDownloadRequestWrapper request = PresignedUrlDownloadRequestWrapper.builder() + .url(urlNoQuery) + .build(); + SdkHttpFullRequest result = marshaller.marshall(request); + + assertThat(result.headers()).doesNotContainKey("x-amz-checksum-mode"); + } + @Test void marshall_withMalformedUrl_shouldThrowSdkClientException() throws Exception { // Setup the mock marshaller to return a properly configured request From 2b4ebe3dc1931b77612e14a373b317ba0b549268 Mon Sep 17 00:00:00 2001 From: jencymaryjoseph <35571282+jencymaryjoseph@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:13:06 -0700 Subject: [PATCH 2/2] Add public method to return all checksum algorithms --- .../checksums/DefaultChecksumAlgorithm.java | 9 ++++++++ .../DefaultAsyncPresignedUrlExtension.java | 22 +++++++------------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/core/checksums/src/main/java/software/amazon/awssdk/checksums/DefaultChecksumAlgorithm.java b/core/checksums/src/main/java/software/amazon/awssdk/checksums/DefaultChecksumAlgorithm.java index 65a2940b8aa2..dae3f27db564 100644 --- a/core/checksums/src/main/java/software/amazon/awssdk/checksums/DefaultChecksumAlgorithm.java +++ b/core/checksums/src/main/java/software/amazon/awssdk/checksums/DefaultChecksumAlgorithm.java @@ -15,6 +15,8 @@ package software.amazon.awssdk.checksums; +import java.util.Collection; +import java.util.Collections; import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; import software.amazon.awssdk.annotations.SdkProtectedApi; @@ -52,6 +54,13 @@ public static ChecksumAlgorithm fromValue(String algorithm) { return ChecksumAlgorithmsCache.VALUES.get(algorithm.toUpperCase(Locale.US)); } + /** + * Returns all registered checksum algorithms. + */ + public static Collection values() { + return Collections.unmodifiableCollection(ChecksumAlgorithmsCache.VALUES.values()); + } + private static final class ChecksumAlgorithmsCache { private static final ConcurrentHashMap VALUES = new ConcurrentHashMap<>(); diff --git a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presignedurl/DefaultAsyncPresignedUrlExtension.java b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presignedurl/DefaultAsyncPresignedUrlExtension.java index fafe9dfd3623..d2a7088aa865 100644 --- a/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presignedurl/DefaultAsyncPresignedUrlExtension.java +++ b/services/s3/src/main/java/software/amazon/awssdk/services/s3/internal/presignedurl/DefaultAsyncPresignedUrlExtension.java @@ -28,6 +28,7 @@ import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.awscore.internal.AwsProtocolMetadata; import software.amazon.awssdk.checksums.DefaultChecksumAlgorithm; +import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm; import software.amazon.awssdk.core.async.AsyncResponseTransformer; import software.amazon.awssdk.core.async.AsyncResponseTransformerUtils; import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption; @@ -67,20 +68,13 @@ public final class DefaultAsyncPresignedUrlExtension implements AsyncPresignedUr * Checksum configuration matching the codegen-produced GetObject operation. * Enables the HttpChecksumValidationInterceptor to validate response checksums. */ - private static final HttpChecksum RESPONSE_CHECKSUM_CONFIG = HttpChecksum.builder() - .requestValidationMode("ENABLED") - .responseAlgorithmsV2( - DefaultChecksumAlgorithm.XXHASH3, - DefaultChecksumAlgorithm.XXHASH128, - DefaultChecksumAlgorithm.CRC64NVME, - DefaultChecksumAlgorithm.CRC32C, - DefaultChecksumAlgorithm.CRC32, - DefaultChecksumAlgorithm.XXHASH64, - DefaultChecksumAlgorithm.SHA512, - DefaultChecksumAlgorithm.SHA256, - DefaultChecksumAlgorithm.SHA1, - DefaultChecksumAlgorithm.MD5) - .build(); + private static final HttpChecksum RESPONSE_CHECKSUM_CONFIG = + HttpChecksum.builder() + .requestValidationMode("ENABLED") + .responseAlgorithmsV2( + DefaultChecksumAlgorithm.values() + .toArray(new ChecksumAlgorithm[0])) + .build(); private final AsyncClientHandler clientHandler; private final AwsS3ProtocolFactory protocolFactory;