diff --git a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml index 4e0524deaf52..c906a7ecd30c 100644 --- a/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml +++ b/build-tools/src/main/resources/software/amazon/awssdk/spotbugs-suppressions.xml @@ -135,6 +135,12 @@ + + + + + + diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/DefaultAwsCrtV4aHttpSigner.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/DefaultAwsCrtV4aHttpSigner.java index 6bfb3c9d04e5..15ac20a81ef0 100644 --- a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/DefaultAwsCrtV4aHttpSigner.java +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/DefaultAwsCrtV4aHttpSigner.java @@ -15,13 +15,23 @@ package software.amazon.awssdk.http.auth.aws.crt; +import static software.amazon.awssdk.crt.auth.signing.AwsSigningConfig.AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS; +import static software.amazon.awssdk.crt.auth.signing.AwsSigningConfig.AwsSignedBodyHeaderType.X_AMZ_CONTENT_SHA256; +import static software.amazon.awssdk.crt.auth.signing.AwsSigningConfig.AwsSignedBodyValue.STREAMING_AWS4_ECDSA_P256_SHA256_PAYLOAD; +import static software.amazon.awssdk.crt.auth.signing.AwsSigningConfig.AwsSignedBodyValue.STREAMING_AWS4_ECDSA_P256_SHA256_PAYLOAD_TRAILER; +import static software.amazon.awssdk.crt.auth.signing.AwsSigningConfig.AwsSignedBodyValue.STREAMING_UNSIGNED_PAYLOAD_TRAILER; +import static software.amazon.awssdk.crt.auth.signing.AwsSigningConfig.AwsSignedBodyValue.UNSIGNED_PAYLOAD; +import static software.amazon.awssdk.crt.auth.signing.AwsSigningConfig.AwsSigningAlgorithm.SIGV4_ASYMMETRIC; import static software.amazon.awssdk.http.auth.aws.crt.internal.CrtHttpRequestConverter.toRequest; import static software.amazon.awssdk.http.auth.aws.crt.internal.CrtUtils.sanitizeRequest; import static software.amazon.awssdk.http.auth.aws.crt.internal.CrtUtils.toCredentials; +import static software.amazon.awssdk.http.auth.aws.util.CredentialUtils.sanitizeCredentials; import static software.amazon.awssdk.http.auth.aws.util.SignerConstant.PRESIGN_URL_MAX_EXPIRATION_DURATION; +import static software.amazon.awssdk.http.auth.aws.util.SignerConstant.X_AMZ_TRAILER; import java.time.Clock; import java.time.Duration; +import java.time.Instant; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.crt.auth.signing.AwsSigner; @@ -31,8 +41,11 @@ import software.amazon.awssdk.http.ContentStreamProvider; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.auth.aws.AwsV4aHttpSigner; +import software.amazon.awssdk.http.auth.aws.crt.internal.signer.AwsChunkedV4aPayloadSigner; import software.amazon.awssdk.http.auth.aws.crt.internal.signer.V4aContext; import software.amazon.awssdk.http.auth.aws.crt.internal.signer.V4aPayloadSigner; +import software.amazon.awssdk.http.auth.aws.crt.internal.signer.V4aProperties; +import software.amazon.awssdk.http.auth.aws.signer.CredentialScope; import software.amazon.awssdk.http.auth.aws.util.CredentialUtils; import software.amazon.awssdk.http.auth.spi.AsyncSignRequest; import software.amazon.awssdk.http.auth.spi.AsyncSignedRequest; @@ -49,11 +62,43 @@ @SdkProtectedApi public final class DefaultAwsCrtV4aHttpSigner implements AwsV4aHttpSigner { - /** - * Returns a default implementation for {@link AwsV4aHttpSigner}. - */ - public static AwsV4aHttpSigner create() { - return new DefaultAwsCrtV4aHttpSigner(); + private static final int DEFAULT_CHUNK_SIZE_IN_BYTES = 128 * 1024; + + private static V4aProperties v4aProperties(SignRequest request) { + Clock signingClock = request.requireProperty(SIGNING_CLOCK, Clock.systemUTC()); + Instant signingInstant = signingClock.instant(); + AwsCredentialsIdentity credentials = sanitizeCredentials(request.identity()); + String regionName = request.requireProperty(REGION_NAME); + String serviceSigningName = request.requireProperty(SERVICE_SIGNING_NAME); + CredentialScope credentialScope = new CredentialScope(regionName, serviceSigningName, signingInstant); + boolean doubleUrlEncode = request.requireProperty(DOUBLE_URL_ENCODE, true); + boolean normalizePath = request.requireProperty(NORMALIZE_PATH, true); + + return V4aProperties + .builder() + .credentials(credentials) + .credentialScope(credentialScope) + .signingClock(signingClock) + .doubleUrlEncode(doubleUrlEncode) + .normalizePath(normalizePath) + .build(); + } + + private static V4aPayloadSigner v4aPayloadSigner( + SignRequest request, + V4aProperties v4aProperties) { + + boolean isChunkEncoding = request.requireProperty(CHUNK_ENCODING_ENABLED, false); + + if (isChunkEncoding) { + return new AwsChunkedV4aPayloadSigner(v4aProperties.getCredentialScope(), DEFAULT_CHUNK_SIZE_IN_BYTES); + } + + return V4aPayloadSigner.create(); + } + + private static boolean hasTrailer(SdkHttpRequest request) { + return request.firstMatchingHeader(X_AMZ_TRAILER).isPresent(); } private static Duration validateExpirationDuration(Duration expirationDuration) { @@ -67,6 +112,77 @@ private static Duration validateExpirationDuration(Duration expirationDuration) return expirationDuration; } + private static AwsSigningConfig signingConfig( + SignRequest request, + V4aProperties v4aProperties) { + + AuthLocation authLocation = request.requireProperty(AUTH_LOCATION, AuthLocation.HEADER); + Duration expirationDuration = request.property(EXPIRATION_DURATION); + boolean isPayloadSigning = request.requireProperty(PAYLOAD_SIGNING_ENABLED, true); + boolean isChunkEncoding = request.requireProperty(CHUNK_ENCODING_ENABLED, false); + boolean isTrailing = hasTrailer(request.request()); + + AwsSigningConfig signingConfig = new AwsSigningConfig(); + signingConfig.setCredentials(toCredentials(v4aProperties.getCredentials())); + signingConfig.setService(v4aProperties.getCredentialScope().getService()); + signingConfig.setRegion(v4aProperties.getCredentialScope().getRegion()); + signingConfig.setAlgorithm(SIGV4_ASYMMETRIC); + signingConfig.setTime(v4aProperties.getCredentialScope().getInstant().toEpochMilli()); + signingConfig.setUseDoubleUriEncode(v4aProperties.shouldDoubleUrlEncode()); + signingConfig.setShouldNormalizeUriPath(v4aProperties.shouldNormalizePath()); + signingConfig.setSignedBodyHeader(X_AMZ_CONTENT_SHA256); + + switch (authLocation) { + case HEADER: + signingConfig.setSignatureType(AwsSigningConfig.AwsSignatureType.HTTP_REQUEST_VIA_HEADERS); + if (request.hasProperty(EXPIRATION_DURATION)) { + throw new UnsupportedOperationException( + String.format("%s is not supported for %s.", EXPIRATION_DURATION, AuthLocation.HEADER) + ); + } + break; + case QUERY_STRING: + signingConfig.setSignatureType(HTTP_REQUEST_VIA_QUERY_PARAMS); + if (request.hasProperty(EXPIRATION_DURATION)) { + signingConfig.setExpirationInSeconds(validateExpirationDuration(expirationDuration).getSeconds()); + } + break; + default: + throw new UnsupportedOperationException("Unknown auth-location: " + authLocation); + } + + if (isPayloadSigning) { + configurePayloadSigning(signingConfig, isChunkEncoding, isTrailing); + } else { + configureUnsignedPayload(signingConfig, isChunkEncoding, isTrailing); + } + + return signingConfig; + } + + private static void configureUnsignedPayload(AwsSigningConfig signingConfig, boolean isChunkEncoding, boolean isTrailing) { + if (isChunkEncoding) { + if (isTrailing) { + signingConfig.setSignedBodyValue(STREAMING_UNSIGNED_PAYLOAD_TRAILER); + } else { + throw new UnsupportedOperationException("Chunk-Encoding without Payload-Signing must have a trailer!"); + } + } else { + signingConfig.setSignedBodyValue(UNSIGNED_PAYLOAD); + } + } + + private static void configurePayloadSigning(AwsSigningConfig signingConfig, boolean isChunkEncoding, boolean isTrailing) { + if (isChunkEncoding) { + if (isTrailing) { + signingConfig.setSignedBodyValue(STREAMING_AWS4_ECDSA_P256_SHA256_PAYLOAD_TRAILER); + } else { + signingConfig.setSignedBodyValue(STREAMING_AWS4_ECDSA_P256_SHA256_PAYLOAD); + } + } + // if not chunked encoding, then signed-payload simply means the sha256 hash is included in the canonical request + } + private static SyncSignedRequest doSign(SyncSignRequest request, AwsSigningConfig signingConfig, V4aPayloadSigner payloadSigner) { @@ -86,7 +202,7 @@ private static SyncSignedRequest doSign(SyncSignRequest request) { - AwsSigningConfig signingConfig = signingConfig(request); - V4aPayloadSigner payloadSigner = V4aPayloadSigner.create(); + V4aProperties v4aProperties = v4aProperties(request); + AwsSigningConfig signingConfig = signingConfig(request, v4aProperties); + V4aPayloadSigner payloadSigner = v4aPayloadSigner(request, v4aProperties); return doSign(request, signingConfig, payloadSigner); } - private AwsSigningConfig signingConfig(SignRequest request) { - String regionName = request.requireProperty(REGION_NAME); - String serviceSigningName = request.requireProperty(SERVICE_SIGNING_NAME); - Clock signingClock = request.requireProperty(SIGNING_CLOCK, Clock.systemUTC()); - boolean doubleUrlEncode = request.requireProperty(DOUBLE_URL_ENCODE, true); - boolean normalizePath = request.requireProperty(NORMALIZE_PATH, true); - AuthLocation authLocation = request.requireProperty(AUTH_LOCATION, AuthLocation.HEADER); - Duration expirationDuration = request.property(EXPIRATION_DURATION); - boolean isPayloadSigning = request.requireProperty(PAYLOAD_SIGNING_ENABLED, true); - - AwsSigningConfig signingConfig = new AwsSigningConfig(); - signingConfig.setCredentials(toCredentials(request.identity())); - signingConfig.setService(serviceSigningName); - signingConfig.setRegion(regionName); - signingConfig.setAlgorithm(AwsSigningConfig.AwsSigningAlgorithm.SIGV4_ASYMMETRIC); - signingConfig.setTime(signingClock.instant().toEpochMilli()); - signingConfig.setUseDoubleUriEncode(doubleUrlEncode); - signingConfig.setShouldNormalizeUriPath(normalizePath); - - switch (authLocation) { - case HEADER: - signingConfig.setSignatureType(AwsSigningConfig.AwsSignatureType.HTTP_REQUEST_VIA_HEADERS); - if (request.hasProperty(EXPIRATION_DURATION)) { - throw new UnsupportedOperationException( - String.format("%s is not supported for %s.", EXPIRATION_DURATION, AuthLocation.HEADER) - ); - } - break; - case QUERY_STRING: - signingConfig.setSignatureType(AwsSigningConfig.AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS); - if (request.hasProperty(EXPIRATION_DURATION)) { - signingConfig.setExpirationInSeconds(validateExpirationDuration(expirationDuration).getSeconds()); - } - break; - default: - throw new UnsupportedOperationException("Unknown auth-location: " + authLocation); - } - - if (!isPayloadSigning) { - signingConfig.setSignedBodyValue(AwsSigningConfig.AwsSignedBodyValue.UNSIGNED_PAYLOAD); - } - - return signingConfig; - } - @Override public CompletableFuture signAsync(AsyncSignRequest request) { // There isn't currently a concept of async for crt signers diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/CrtHttpRequestConverter.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/CrtHttpRequestConverter.java index 8c94e38bdfa7..50de66c52a26 100644 --- a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/CrtHttpRequestConverter.java +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/CrtHttpRequestConverter.java @@ -34,8 +34,6 @@ @SdkInternalApi public final class CrtHttpRequestConverter { - private static final int READ_BUFFER_SIZE = 4096; - private CrtHttpRequestConverter() { } @@ -52,7 +50,7 @@ public static HttpRequest toRequest(SdkHttpRequest request, ContentStreamProvide HttpRequestBodyStream crtInputStream = null; if (payload != null) { - crtInputStream = new CrtInputStream(payload, READ_BUFFER_SIZE); + crtInputStream = new CrtInputStream(payload); } return new HttpRequest(method, encodedPath + encodedQueryString, crtHeaderArray, crtInputStream); @@ -147,6 +145,6 @@ private static String encodedPathFromCrtFormat(String sdkEncodedPath, String crt } public static HttpRequestBodyStream toCrtStream(byte[] data) { - return new CrtInputStream(() -> new ByteArrayInputStream(data), READ_BUFFER_SIZE); + return new CrtInputStream(() -> new ByteArrayInputStream(data)); } } diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/CrtInputStream.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/CrtInputStream.java index f22bc7219a33..eed76aa63514 100644 --- a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/CrtInputStream.java +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/CrtInputStream.java @@ -25,14 +25,15 @@ @SdkInternalApi public final class CrtInputStream implements HttpRequestBodyStream { + private static final int READ_BUFFER_SIZE = 4096; private final ContentStreamProvider provider; private final int bufSize; private final byte[] readBuffer; private InputStream providerStream; - CrtInputStream(ContentStreamProvider provider, int bufSize) { + public CrtInputStream(ContentStreamProvider provider) { this.provider = provider; - this.bufSize = bufSize; + this.bufSize = READ_BUFFER_SIZE; this.readBuffer = new byte[bufSize]; } @@ -49,6 +50,8 @@ public boolean sendRequestBody(ByteBuffer bodyBytesOut) { if (read > 0) { bodyBytesOut.put(readBuffer, 0, read); + } else { + FunctionalUtils.invokeSafely(providerStream::close); } return read < 0; diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/Chunk.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/Chunk.java new file mode 100644 index 000000000000..aa0bea4f4a32 --- /dev/null +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/Chunk.java @@ -0,0 +1,44 @@ +/* + * 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.http.auth.aws.crt.internal.chunkedencoding; + +import java.io.InputStream; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.utils.SdkAutoCloseable; + +/** + * An interface which defines a "chunk" of data. + */ +@SdkInternalApi +public interface Chunk extends SdkAutoCloseable { + /** + * Get a default implementation of a chunk, which wraps a stream with a fixed size; + */ + static Chunk create(InputStream data, int sizeInBytes) { + return new DefaultChunk(new ChunkInputStream(data, sizeInBytes)); + } + + /** + * Get the underlying stream of data for a chunk. + */ + InputStream stream(); + + /** + * Whether the logical end of a chunk has been reached. + */ + boolean hasRemaining(); + +} diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/ChunkExtensionProvider.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/ChunkExtensionProvider.java new file mode 100644 index 000000000000..340ce0b8a7e7 --- /dev/null +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/ChunkExtensionProvider.java @@ -0,0 +1,36 @@ +/* + * 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.http.auth.aws.crt.internal.chunkedencoding; + +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.utils.Pair; + +/** + * A functional interface for defining an extension of a chunk, where the extension is a key-value pair. + *

+ * An extension usually depends on the chunk-data itself (checksum, signature, etc.), but is not required to. + * Per RFC-7230 The chunk-extension is defined as: + *

+ *     chunk-ext      = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
+ *     chunk-ext-name = token
+ *     chunk-ext-val  = token / quoted-string
+ * 
+ */ +@FunctionalInterface +@SdkInternalApi +public interface ChunkExtensionProvider { + Pair get(byte[] chunk); +} diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/ChunkHeaderProvider.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/ChunkHeaderProvider.java new file mode 100644 index 000000000000..5e108e3dad59 --- /dev/null +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/ChunkHeaderProvider.java @@ -0,0 +1,31 @@ +/* + * 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.http.auth.aws.crt.internal.chunkedencoding; + +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * A functional interface for defining a header of a chunk. + *

+ * The header usually depends on the chunk-data itself (hex-size), but is not required to. + * In RFC-7230, the chunk-header is specifically + * the {@code chunk-size}, but this interface can give us greater flexibility. + */ +@FunctionalInterface +@SdkInternalApi +public interface ChunkHeaderProvider { + byte[] get(byte[] chunk); +} diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/ChunkInputStream.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/ChunkInputStream.java new file mode 100644 index 000000000000..8597b686c5ee --- /dev/null +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/ChunkInputStream.java @@ -0,0 +1,45 @@ +/* + * 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.http.auth.aws.crt.internal.chunkedencoding; + +import java.io.IOException; +import java.io.InputStream; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.auth.aws.crt.internal.io.SdkLengthAwareInputStream; + +/** + * A wrapped stream to represent a "chunk" of data + */ +@SdkInternalApi +public final class ChunkInputStream extends SdkLengthAwareInputStream { + + public ChunkInputStream(InputStream inputStream, long length) { + super(inputStream, length); + } + + @Override + public void close() throws IOException { + // Drain this chunk on close, so the stream is left at the end of the chunk. + long remaining = remaining(); + if (remaining > 0) { + if (skip(remaining) < remaining) { + throw new IOException("Unable to drain stream for chunk. The underlying stream did not allow skipping the " + + "whole chunk."); + } + } + super.close(); + } +} diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/ChunkedEncodedInputStream.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/ChunkedEncodedInputStream.java new file mode 100644 index 000000000000..aebae53e9049 --- /dev/null +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/ChunkedEncodedInputStream.java @@ -0,0 +1,313 @@ +/* + * 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.http.auth.aws.crt.internal.chunkedencoding; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.Pair; +import software.amazon.awssdk.utils.Validate; + + +/** + * An implementation of chunk-transfer encoding, but by wrapping an {@link InputStream}. + * This implementation supports chunk-headers, chunk-extensions, and trailers. + *

+ * Per RFC-7230, a chunk-transfer encoded message + * is defined as: + *

+ *     chunked-body   = *chunk
+ *                      last-chunk
+ *                      trailer-part
+ *                      CRLF
+ *     chunk          = chunk-size [ chunk-ext ] CRLF
+ *                      chunk-data CRLF
+ *     chunk-size     = 1*HEXDIG
+ *     last-chunk     = 1*("0") [ chunk-ext ] CRLF
+ *     chunk-data     = 1*OCTET ; a sequence of chunk-size octets
+ * 
+ */ +@SdkInternalApi +public final class ChunkedEncodedInputStream extends InputStream { + private static final Logger LOG = Logger.loggerFor(ChunkedEncodedInputStream.class); + private static final byte[] CRLF = {'\r', '\n'}; + private static final byte[] END = {}; + + private final InputStream inputStream; + private final int chunkSize; + + private final ChunkHeaderProvider header; + private final List extensions = new ArrayList<>(); + private final List trailers = new ArrayList<>(); + + private Chunk currentChunk; + private boolean isFinished = false; + + private ChunkedEncodedInputStream(BuilderImpl builder) { + this.inputStream = Validate.notNull(builder.inputStream, "Input-Stream cannot be null!"); + this.chunkSize = Validate.isPositive(builder.chunkSize, "Chunk-size must be greater than 0!"); + this.header = Validate.notNull(builder.header, "Header cannot be null!"); + this.extensions.addAll(Validate.notNull(builder.extensions, "Extensions cannot be null!")); + this.trailers.addAll(Validate.notNull(builder.trailers, "Trailers cannot be null!")); + } + + @Override + public int read() throws IOException { + if (currentChunk == null || !currentChunk.hasRemaining() && !isFinished) { + currentChunk = getChunk(inputStream); + } + + return currentChunk.stream().read(); + } + + /** + * Get an encoded chunk from the input-stream, or the final chunk if we've reached the end of the input-stream. + */ + private Chunk getChunk(InputStream stream) throws IOException { + LOG.debug(() -> "Reading next chunk."); + if (currentChunk != null) { + currentChunk.close(); + } + // we *have* to read from the backing stream in order to figure out if it's the end or not + // TODO: We can likely optimize this by not copying the entire chunk of data into memory + byte[] chunkData = new byte[chunkSize]; + int read = read(stream, chunkData, chunkSize); + + if (read > 0) { + // set the current chunk to the newly written chunk + return getNextChunk(Arrays.copyOf(chunkData, read)); + } + + LOG.debug(() -> "End of backing stream reached. Reading final chunk."); + isFinished = true; + // set the current chunk to the written final chunk + return getFinalChunk(); + } + + /** + * Read from an input-stream, up to a max number of bytes, storing them in a byte-array. + * The actual number of bytes can be less than the max in the event that we reach the end of the stream. + *

+ * This method is necessary because we cannot assume the backing stream uses the default implementation of + * {@code read(byte b[], int off, int len)} + */ + private int read(InputStream inputStream, byte[] buf, int maxBytesToRead) throws IOException { + int read; + int offset = 0; + do { + read = inputStream.read(buf, offset, maxBytesToRead - offset); + assert read != 0; + if (read > 0) { + offset += read; + } + } while (read > 0 && offset < maxBytesToRead); + + return offset; + } + + /** + * Create a chunk from a byte-array, which includes the header, the extensions, and the chunk data. + * The input array should be correctly sized, i.e. the number of bytes should equal its length. + */ + private Chunk getNextChunk(byte[] data) throws IOException { + ByteArrayOutputStream chunkStream = new ByteArrayOutputStream(); + writeChunk(data, chunkStream); + chunkStream.write(CRLF); + byte[] newChunkData = chunkStream.toByteArray(); + + return Chunk.create(new ByteArrayInputStream(newChunkData), newChunkData.length); + } + + /** + * Create the final chunk, which includes the header, the extensions, the chunk (if applicable), and the trailer + */ + private Chunk getFinalChunk() throws IOException { + ByteArrayOutputStream chunkStream = new ByteArrayOutputStream(); + writeChunk(END, chunkStream); + writeTrailers(chunkStream); + chunkStream.write(CRLF); + byte[] newChunkData = chunkStream.toByteArray(); + + return Chunk.create(new ByteArrayInputStream(newChunkData), newChunkData.length); + } + + private void writeChunk(byte[] chunk, ByteArrayOutputStream outputStream) throws IOException { + writeHeader(chunk, outputStream); + writeExtensions(chunk, outputStream); + outputStream.write(CRLF); + outputStream.write(chunk); + } + + private void writeHeader(byte[] chunk, ByteArrayOutputStream outputStream) throws IOException { + byte[] hdr = header.get(chunk); + outputStream.write(hdr); + } + + private void writeExtensions(byte[] chunk, ByteArrayOutputStream outputStream) throws IOException { + for (ChunkExtensionProvider chunkExtensionProvider : extensions) { + Pair ext = chunkExtensionProvider.get(chunk); + outputStream.write((byte) ';'); + outputStream.write(ext.left()); + outputStream.write((byte) '='); + outputStream.write(ext.right()); + } + } + + private void writeTrailers(ByteArrayOutputStream outputStream) throws IOException { + for (TrailerProvider trailer : trailers) { + Pair> tlr = trailer.get(); + outputStream.write(tlr.left().getBytes(StandardCharsets.UTF_8)); + outputStream.write((byte) ':'); + outputStream.write(String.join(", ", tlr.right()).getBytes(StandardCharsets.UTF_8)); + outputStream.write(CRLF); + } + } + + @Override + public synchronized void mark(int readlimit) { + // TODO: Implement this, likely needed for retries + throw new UnsupportedOperationException(); + } + + @Override + public synchronized void reset() { + // TODO: Implement this, likely needed for retries + throw new UnsupportedOperationException(); + } + + public static Builder builder() { + return new BuilderImpl(); + } + + public interface Builder { + + /** + * Set the backing input stream. + */ + Builder inputStream(InputStream inputStream); + + /** + * Set the size of chunks from input stream. + * The actual size (in bytes) of an encoded chunk depends on the configuration. + */ + Builder chunkSize(int chunkSize); + + /** + * Set the header to be used when creating an encoded chunk. + * This header will be the first part of an encoded chunk. + */ + Builder header(ChunkHeaderProvider header); + + /** + * Set the chunk-extensions to be used when creating an encoded chunk. + * These extensions will immediately follow the header. + */ + Builder extensions(List extensions); + + /** + * Add a chunk-extension. + */ + Builder addExtension(ChunkExtensionProvider extension); + + /** + * Get the trailers currently set for the builder. + */ + List trailers(); + + /** + * Set the trailers to be used when creating the final chunk. + * These trailers will immediately follow the final encoded chunk. + */ + Builder trailers(List trailers); + + /** + * Add a trailer. + */ + Builder addTrailer(TrailerProvider trailer); + + ChunkedEncodedInputStream build(); + } + + private static class BuilderImpl implements Builder { + private InputStream inputStream; + private int chunkSize; + private ChunkHeaderProvider header = chunk -> Integer.toHexString(chunk.length).getBytes(StandardCharsets.UTF_8); + private final List extensions = new ArrayList<>(); + private final List trailers = new ArrayList<>(); + + @Override + public Builder inputStream(InputStream inputStream) { + this.inputStream = inputStream; + return this; + } + + @Override + public Builder chunkSize(int chunkSize) { + this.chunkSize = chunkSize; + return this; + } + + @Override + public Builder header(ChunkHeaderProvider header) { + this.header = header; + return this; + } + + @Override + public Builder extensions(List extensions) { + this.extensions.clear(); + extensions.forEach(this::addExtension); + return this; + } + + @Override + public Builder addExtension(ChunkExtensionProvider extension) { + this.extensions.add(Validate.notNull(extension, "ExtensionProvider cannot be null!")); + return this; + } + + @Override + public List trailers() { + return new ArrayList<>(trailers); + } + + @Override + public Builder trailers(List trailers) { + this.trailers.clear(); + trailers.forEach(this::addTrailer); + return this; + } + + @Override + public Builder addTrailer(TrailerProvider trailer) { + this.trailers.add(Validate.notNull(trailer, "TrailerProvider cannot be null!")); + return this; + } + + @Override + public ChunkedEncodedInputStream build() { + return new ChunkedEncodedInputStream(this); + } + } +} + diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/DefaultChunk.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/DefaultChunk.java new file mode 100644 index 000000000000..2bc0f503a773 --- /dev/null +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/DefaultChunk.java @@ -0,0 +1,48 @@ +/* + * 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.http.auth.aws.crt.internal.chunkedencoding; + +import static software.amazon.awssdk.utils.FunctionalUtils.invokeSafely; + +import software.amazon.awssdk.annotations.SdkInternalApi; + +/** + * An implementation of a chunk, backed by a {@link ChunkInputStream}. This allows it to have awareness of its length and + * determine the endedness of the chunk. + */ +@SdkInternalApi +final class DefaultChunk implements Chunk { + private final ChunkInputStream data; + + DefaultChunk(ChunkInputStream data) { + this.data = data; + } + + @Override + public boolean hasRemaining() { + return data.remaining() > 0; + } + + @Override + public ChunkInputStream stream() { + return data; + } + + @Override + public void close() { + invokeSafely(data::close); + } +} diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/TrailerProvider.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/TrailerProvider.java new file mode 100644 index 000000000000..64219c1a78fb --- /dev/null +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/chunkedencoding/TrailerProvider.java @@ -0,0 +1,38 @@ +/* + * 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.http.auth.aws.crt.internal.chunkedencoding; + +import java.util.List; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.utils.Pair; + +/** + * A functional interface for defining a trailer, where the trailer is a header pair. + *

+ * A trailer usually depends on the chunk-data itself (checksum, signature, etc.), but is not required to. + * Per RFC-7230, the chunked trailer + * section is defined as: + *

+ *     trailer-part   = *( header-field CRLF )
+ * 
+ * An implementation of this interface is specifically an element of the {@code trailer-part}. Therefore, all occurrences of + * {@code TrailerProvider}'s make up the {@code trailer-part}. + */ +@FunctionalInterface +@SdkInternalApi +public interface TrailerProvider { + Pair> get(); +} diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/io/SdkLengthAwareInputStream.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/io/SdkLengthAwareInputStream.java new file mode 100644 index 000000000000..e2454b9c8b5d --- /dev/null +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/io/SdkLengthAwareInputStream.java @@ -0,0 +1,108 @@ +/* + * 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.http.auth.aws.crt.internal.io; + +import static software.amazon.awssdk.utils.NumericUtils.saturatedCast; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.utils.Logger; +import software.amazon.awssdk.utils.Validate; + +/** + * An {@code InputStream} that is aware of its length. The main purpose of this class is to support truncating streams to a length + * that is shorter than the total length of the stream. + */ +@SdkInternalApi +public class SdkLengthAwareInputStream extends FilterInputStream { + private static final Logger LOG = Logger.loggerFor(SdkLengthAwareInputStream.class); + private long length; + private long remaining; + + public SdkLengthAwareInputStream(InputStream in, long length) { + super(in); + this.length = Validate.isNotNegative(length, "length"); + this.remaining = this.length; + } + + @Override + public int read() throws IOException { + if (!hasMoreBytes()) { + LOG.debug(() -> String.format("Specified InputStream length of %d has been reached. Returning EOF.", length)); + return -1; + } + + int read = super.read(); + if (read != -1) { + remaining--; + } + return read; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (!hasMoreBytes()) { + LOG.debug(() -> String.format("Specified InputStream length of %d has been reached. Returning EOF.", length)); + return -1; + } + + len = Math.min(len, saturatedCast(remaining)); + int read = super.read(b, off, len); + if (read > 0) { + remaining -= read; + } + + return read; + } + + @Override + public long skip(long requestedBytesToSkip) throws IOException { + requestedBytesToSkip = Math.min(requestedBytesToSkip, remaining); + long skippedActual = super.skip(requestedBytesToSkip); + remaining -= skippedActual; + return skippedActual; + } + + @Override + public int available() throws IOException { + int streamAvailable = super.available(); + return Math.min(streamAvailable, saturatedCast(remaining)); + } + + @Override + public void mark(int readlimit) { + super.mark(readlimit); + // mark() causes reset() to change the stream's position back to the current position. Therefore, when reset() is called, + // the new length of the stream will be equal to the current value of 'remaining'. + length = remaining; + } + + @Override + public void reset() throws IOException { + super.reset(); + remaining = length; + } + + public long remaining() { + return remaining; + } + + private boolean hasMoreBytes() { + return remaining > 0; + } +} diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/AwsChunkedV4aPayloadSigner.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/AwsChunkedV4aPayloadSigner.java new file mode 100644 index 000000000000..e2eb5f2707b8 --- /dev/null +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/AwsChunkedV4aPayloadSigner.java @@ -0,0 +1,131 @@ +/* + * 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.http.auth.aws.crt.internal.signer; + +import static software.amazon.awssdk.http.auth.aws.util.SignerConstant.STREAMING_ECDSA_SIGNED_PAYLOAD; +import static software.amazon.awssdk.http.auth.aws.util.SignerConstant.STREAMING_ECDSA_SIGNED_PAYLOAD_TRAILER; +import static software.amazon.awssdk.http.auth.aws.util.SignerConstant.STREAMING_UNSIGNED_PAYLOAD_TRAILER; +import static software.amazon.awssdk.http.auth.aws.util.SignerConstant.X_AMZ_DECODED_CONTENT_LENGTH; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.ContentStreamProvider; +import software.amazon.awssdk.http.Header; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.auth.aws.crt.internal.chunkedencoding.ChunkedEncodedInputStream; +import software.amazon.awssdk.http.auth.aws.crt.internal.chunkedencoding.TrailerProvider; +import software.amazon.awssdk.http.auth.aws.signer.CredentialScope; +import software.amazon.awssdk.utils.Pair; +import software.amazon.awssdk.utils.StringInputStream; + +/** + * A default implementation of a payload signer that is a no-op, since payloads are most commonly unsigned. + */ +@SdkInternalApi +public class AwsChunkedV4aPayloadSigner implements V4aPayloadSigner { + + private final CredentialScope credentialScope; + private final int chunkSize; + + public AwsChunkedV4aPayloadSigner(CredentialScope credentialScope, int chunkSize) { + this.credentialScope = credentialScope; + this.chunkSize = chunkSize; + } + + /** + * Move `Content-Length` to `x-amz-decoded-content-length` if not already present. If neither header is present, an exception + * is thrown. + */ + private static void moveContentLength(SdkHttpRequest.Builder request) { + if (!request.firstMatchingHeader(X_AMZ_DECODED_CONTENT_LENGTH).isPresent()) { + // if the decoded length isn't present, content-length must be there + String contentLength = request + .firstMatchingHeader(Header.CONTENT_LENGTH) + .orElseThrow(() -> new IllegalArgumentException(Header.CONTENT_LENGTH + " must be specified!")); + + request.putHeader(X_AMZ_DECODED_CONTENT_LENGTH, contentLength) + .removeHeader(Header.CONTENT_LENGTH); + } else { + // decoded header is already there, so remove content-length just to be sure it's gone + request.removeHeader(Header.CONTENT_LENGTH); + } + } + + @Override + public ContentStreamProvider sign(ContentStreamProvider payload, V4aContext v4aContext) { + SdkHttpRequest.Builder request = v4aContext.getSignedRequest(); + moveContentLength(request); + + InputStream inputStream = payload != null ? payload.newStream() : new StringInputStream(""); + ChunkedEncodedInputStream.Builder chunkedEncodedInputStreamBuilder = ChunkedEncodedInputStream + .builder() + .inputStream(inputStream) + .chunkSize(chunkSize) + .header(chunk -> Integer.toHexString(chunk.length).getBytes(StandardCharsets.UTF_8)); + + switch (v4aContext.getSigningConfig().getSignedBodyValue()) { + case STREAMING_ECDSA_SIGNED_PAYLOAD: { + RollingSigner rollingSigner = new RollingSigner(v4aContext.getSignature(), v4aContext.getSigningConfig()); + setupSigExt(chunkedEncodedInputStreamBuilder, rollingSigner); + break; + } + case STREAMING_UNSIGNED_PAYLOAD_TRAILER: + setupChecksumTrailer(chunkedEncodedInputStreamBuilder); + break; + case STREAMING_ECDSA_SIGNED_PAYLOAD_TRAILER: { + RollingSigner rollingSigner = new RollingSigner(v4aContext.getSignature(), v4aContext.getSigningConfig()); + setupSigExt(chunkedEncodedInputStreamBuilder, rollingSigner); + setupSigTrailer(chunkedEncodedInputStreamBuilder, rollingSigner); + setupChecksumTrailer(chunkedEncodedInputStreamBuilder); + break; + } + default: + throw new UnsupportedOperationException(); + } + + return chunkedEncodedInputStreamBuilder::build; + } + + private void setupSigExt(ChunkedEncodedInputStream.Builder builder, RollingSigner rollingSigner) { + builder.addExtension( + chunk -> Pair.of( + "chunk-signature".getBytes(StandardCharsets.UTF_8), + rollingSigner.sign(chunk) + ) + ); + } + + private void setupSigTrailer(ChunkedEncodedInputStream.Builder builder, RollingSigner rollingSigner) { + Map> trailers = + builder.trailers().stream().map(TrailerProvider::get).collect(Collectors.toMap(Pair::left, Pair::right)); + + builder.addTrailer( + () -> Pair.of( + "x-amz-trailer-signature", + Collections.singletonList(new String(rollingSigner.sign(trailers), StandardCharsets.UTF_8)) + ) + ); + } + + private void setupChecksumTrailer(ChunkedEncodedInputStream.Builder builder) { + // TODO: Set up checksumming of chunks and add as a trailer + } +} diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/RollingSigner.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/RollingSigner.java new file mode 100644 index 000000000000..88ab5d8f4dee --- /dev/null +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/RollingSigner.java @@ -0,0 +1,82 @@ +/* + * 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.http.auth.aws.crt.internal.signer; + +import java.io.ByteArrayInputStream; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import software.amazon.awssdk.annotations.SdkInternalApi; +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.HttpRequestBodyStream; +import software.amazon.awssdk.http.auth.aws.crt.internal.CrtInputStream; +import software.amazon.awssdk.utils.CompletableFutureUtils; + +/** + * A class which calculates a rolling signature of arbitrary data using HMAC-SHA256. Each time a signature is calculated, the + * prior calculation is incorporated, hence "rolling". + */ +@SdkInternalApi +public final class RollingSigner { + + private final byte[] seedSignature; + private final AwsSigningConfig signingConfig; + private byte[] previousSignature; + + public RollingSigner(byte[] seedSignature, AwsSigningConfig signingConfig) { + this.seedSignature = seedSignature.clone(); + this.previousSignature = seedSignature.clone(); + this.signingConfig = signingConfig; + } + + private static byte[] signChunk(byte[] chunkBody, byte[] previousSignature, AwsSigningConfig signingConfig) { + HttpRequestBodyStream crtBody = new CrtInputStream(() -> new ByteArrayInputStream(chunkBody)); + return CompletableFutureUtils.joinLikeSync(AwsSigner.signChunk(crtBody, previousSignature, signingConfig)); + } + + private static AwsSigningResult signTrailerHeaders(Map> headerMap, byte[] previousSignature, + AwsSigningConfig signingConfig) { + + List httpHeaderList = + headerMap.entrySet().stream().map(entry -> new HttpHeader( + entry.getKey(), String.join(",", entry.getValue()))).collect(Collectors.toList()); + + // All the config remains the same as signing config except the Signature Type. + AwsSigningConfig configCopy = signingConfig.clone(); + configCopy.setSignatureType(AwsSigningConfig.AwsSignatureType.HTTP_REQUEST_TRAILING_HEADERS); + + return CompletableFutureUtils.joinLikeSync(AwsSigner.sign(httpHeaderList, previousSignature, configCopy)); + } + + /** + * Using a template that incorporates the previous calculated signature, sign the string and return it. + */ + public byte[] sign(byte[] chunkBody) { + return signChunk(chunkBody, previousSignature, signingConfig); + } + + public byte[] sign(Map> headerMap) { + AwsSigningResult result = signTrailerHeaders(headerMap, previousSignature, signingConfig); + return result != null ? result.getSignature() : null; + } + + public void reset() { + previousSignature = seedSignature; + } +} diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/V4aContext.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/V4aContext.java index 50c27a2ce804..a2ff72a5f0a3 100644 --- a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/V4aContext.java +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/V4aContext.java @@ -16,6 +16,7 @@ package software.amazon.awssdk.http.auth.aws.crt.internal.signer; import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.crt.auth.signing.AwsSigningConfig; import software.amazon.awssdk.http.SdkHttpRequest; /** @@ -23,13 +24,25 @@ */ @SdkInternalApi public final class V4aContext { - private final SdkHttpRequest signedRequest; + private final SdkHttpRequest.Builder signedRequest; + private final byte[] signature; + private final AwsSigningConfig signingConfig; - public V4aContext(SdkHttpRequest signedRequest) { + public V4aContext(SdkHttpRequest.Builder signedRequest, byte[] signature, AwsSigningConfig signingConfig) { this.signedRequest = signedRequest; + this.signature = signature.clone(); + this.signingConfig = signingConfig; } - public SdkHttpRequest getSignedRequest() { + public SdkHttpRequest.Builder getSignedRequest() { return signedRequest; } + + public byte[] getSignature() { + return signature; + } + + public AwsSigningConfig getSigningConfig() { + return signingConfig; + } } diff --git a/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/V4aProperties.java b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/V4aProperties.java new file mode 100644 index 000000000000..d47f16b8f323 --- /dev/null +++ b/core/http-auth-aws-crt/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/V4aProperties.java @@ -0,0 +1,130 @@ +/* + * 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.http.auth.aws.crt.internal.signer; + +import java.time.Clock; +import software.amazon.awssdk.annotations.Immutable; +import software.amazon.awssdk.annotations.SdkInternalApi; +import software.amazon.awssdk.http.auth.aws.signer.CredentialScope; +import software.amazon.awssdk.http.auth.spi.SignRequest; +import software.amazon.awssdk.http.auth.spi.SignerProperty; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.awssdk.utils.Validate; + + +/** + * A class which contains "properties" relevant to SigV4a. These properties can be derived {@link SignerProperty}'s on a + * {@link SignRequest}. + */ +@SdkInternalApi +@Immutable +public final class V4aProperties { + private final AwsCredentialsIdentity credentials; + private final CredentialScope credentialScope; + private final Clock signingClock; + private final boolean doubleUrlEncode; + private final boolean normalizePath; + + + private V4aProperties(BuilderImpl builder) { + this.credentials = Validate.paramNotNull(builder.credentials, "Credentials"); + this.credentialScope = Validate.paramNotNull(builder.credentialScope, "CredentialScope"); + this.signingClock = Validate.paramNotNull(builder.signingClock, "SigningClock"); + this.doubleUrlEncode = Validate.getOrDefault(builder.doubleUrlEncode, () -> true); + this.normalizePath = Validate.getOrDefault(builder.normalizePath, () -> true); + } + + public AwsCredentialsIdentity getCredentials() { + return credentials; + } + + public CredentialScope getCredentialScope() { + return credentialScope; + } + + public Clock getSigningClock() { + return signingClock; + } + + public boolean shouldDoubleUrlEncode() { + return doubleUrlEncode; + } + + public boolean shouldNormalizePath() { + return normalizePath; + } + + public static Builder builder() { + return new BuilderImpl(); + } + + public interface Builder { + Builder credentials(AwsCredentialsIdentity credentials); + + Builder credentialScope(CredentialScope credentialScope); + + Builder signingClock(Clock signingClock); + + Builder doubleUrlEncode(Boolean doubleUrlEncode); + + Builder normalizePath(Boolean normalizePath); + + V4aProperties build(); + } + + private static class BuilderImpl implements Builder { + private AwsCredentialsIdentity credentials; + private CredentialScope credentialScope; + private Clock signingClock; + private Boolean doubleUrlEncode; + private Boolean normalizePath; + + @Override + public Builder credentials(AwsCredentialsIdentity credentials) { + this.credentials = Validate.paramNotNull(credentials, "Credentials"); + return this; + } + + @Override + public Builder credentialScope(CredentialScope credentialScope) { + this.credentialScope = Validate.paramNotNull(credentialScope, "CredentialScope"); + return this; + } + + @Override + public Builder signingClock(Clock signingClock) { + this.signingClock = signingClock; + return this; + } + + @Override + public Builder doubleUrlEncode(Boolean doubleUrlEncode) { + this.doubleUrlEncode = doubleUrlEncode; + return this; + } + + @Override + public Builder normalizePath(Boolean normalizePath) { + this.normalizePath = normalizePath; + return this; + } + + @Override + public V4aProperties build() { + return new V4aProperties(this); + } + } +} diff --git a/core/http-auth-aws-crt/src/test/java/software/amazon/awssdk/http/auth/aws/crt/DefaultAwsCrtV4aHttpSignerTest.java b/core/http-auth-aws-crt/src/test/java/software/amazon/awssdk/http/auth/aws/crt/DefaultAwsCrtV4aHttpSignerTest.java index 50ffa09d9a0b..1d732747eba9 100644 --- a/core/http-auth-aws-crt/src/test/java/software/amazon/awssdk/http/auth/aws/crt/DefaultAwsCrtV4aHttpSignerTest.java +++ b/core/http-auth-aws-crt/src/test/java/software/amazon/awssdk/http/auth/aws/crt/DefaultAwsCrtV4aHttpSignerTest.java @@ -20,14 +20,18 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static software.amazon.awssdk.http.auth.aws.AwsV4aHttpSigner.AUTH_LOCATION; import static software.amazon.awssdk.http.auth.aws.AwsV4aHttpSigner.AuthLocation; +import static software.amazon.awssdk.http.auth.aws.AwsV4aHttpSigner.CHUNK_ENCODING_ENABLED; import static software.amazon.awssdk.http.auth.aws.AwsV4aHttpSigner.EXPIRATION_DURATION; import static software.amazon.awssdk.http.auth.aws.AwsV4aHttpSigner.PAYLOAD_SIGNING_ENABLED; import static software.amazon.awssdk.http.auth.aws.crt.TestUtils.generateBasicRequest; import static software.amazon.awssdk.http.auth.aws.crt.internal.CrtUtils.toCredentials; import java.time.Duration; +import org.assertj.core.api.AssertionsForClassTypes; import org.junit.jupiter.api.Test; import software.amazon.awssdk.crt.auth.signing.AwsSigningConfig; +import software.amazon.awssdk.http.Header; +import software.amazon.awssdk.http.auth.aws.AwsV4HttpSigner; import software.amazon.awssdk.http.auth.spi.AsyncSignRequest; import software.amazon.awssdk.http.auth.spi.SyncSignRequest; import software.amazon.awssdk.http.auth.spi.SyncSignedRequest; @@ -193,4 +197,93 @@ public void sign_withAnonymousCredentials_shouldNotSign() { public void signAsync_throwsUnsupportedOperationException() { assertThrows(UnsupportedOperationException.class, () -> signer.signAsync((AsyncSignRequest) null)); } + + @Test + public void sign_WithChunkEncodingTrue_DelegatesToAwsChunkedPayloadSigner() { + SyncSignRequest request = generateBasicRequest( + AwsCredentialsIdentity.create("access", "secret"), + httpRequest -> httpRequest + .putHeader(Header.CONTENT_LENGTH, "20"), + signRequest -> signRequest + .putProperty(CHUNK_ENCODING_ENABLED, true) + ); + + SyncSignedRequest signedRequest = signer.sign(request); + + AssertionsForClassTypes.assertThat(signedRequest.request().firstMatchingHeader("x-amz-content-sha256")) + .hasValue(AwsSigningConfig.AwsSignedBodyValue.STREAMING_AWS4_ECDSA_P256_SHA256_PAYLOAD); + AssertionsForClassTypes.assertThat(signedRequest.request().firstMatchingHeader(Header.CONTENT_LENGTH)).isNotPresent(); + AssertionsForClassTypes.assertThat(signedRequest.request().firstMatchingHeader("x-amz-decoded-content-length")).hasValue("20"); + } + + @Test + public void sign_WithChunkEncodingTrueWithout_DelegatesToAwsChunkedPayloadSigner() { + SyncSignRequest request = generateBasicRequest( + AwsCredentialsIdentity.create("access", "secret"), + httpRequest -> httpRequest + .putHeader(Header.CONTENT_LENGTH, "20"), + signRequest -> signRequest + .putProperty(CHUNK_ENCODING_ENABLED, true) + ); + + SyncSignedRequest signedRequest = signer.sign(request); + + AssertionsForClassTypes.assertThat(signedRequest.request().firstMatchingHeader("x-amz-content-sha256")) + .hasValue(AwsSigningConfig.AwsSignedBodyValue.STREAMING_AWS4_ECDSA_P256_SHA256_PAYLOAD); + AssertionsForClassTypes.assertThat(signedRequest.request().firstMatchingHeader(Header.CONTENT_LENGTH)).isNotPresent(); + AssertionsForClassTypes.assertThat(signedRequest.request().firstMatchingHeader("x-amz-decoded-content-length")).hasValue("20"); + } + + @Test + public void sign_ChunkEncodingTrueAndTrailer_DelegatesToAwsChunkedPayloadSigner() { + SyncSignRequest request = generateBasicRequest( + AwsCredentialsIdentity.create("access", "secret"), + httpRequest -> httpRequest + .putHeader(Header.CONTENT_LENGTH, "20") + .putHeader("x-amz-trailer", "aTrailer"), + signRequest -> signRequest + .putProperty(CHUNK_ENCODING_ENABLED, true) + ); + + SyncSignedRequest signedRequest = signer.sign(request); + + AssertionsForClassTypes.assertThat(signedRequest.request().firstMatchingHeader("x-amz-content-sha256")) + .hasValue(AwsSigningConfig.AwsSignedBodyValue.STREAMING_AWS4_ECDSA_P256_SHA256_PAYLOAD_TRAILER); + AssertionsForClassTypes.assertThat(signedRequest.request().firstMatchingHeader(Header.CONTENT_LENGTH)).isNotPresent(); + AssertionsForClassTypes.assertThat(signedRequest.request().firstMatchingHeader("x-amz-decoded-content-length")).hasValue("20"); + } + + @Test + public void sign_WithPayloadSigningFalseAndChunkEncodingTrueAndTrailer_DelegatesToAwsChunkedPayloadSigner() { + SyncSignRequest request = generateBasicRequest( + AwsCredentialsIdentity.create("access", "secret"), + httpRequest -> httpRequest + .putHeader(Header.CONTENT_LENGTH, "20") + .putHeader("x-amz-trailer", "aTrailer"), + signRequest -> signRequest + .putProperty(AwsV4HttpSigner.PAYLOAD_SIGNING_ENABLED, false) + .putProperty(CHUNK_ENCODING_ENABLED, true) + ); + + SyncSignedRequest signedRequest = signer.sign(request); + + AssertionsForClassTypes.assertThat(signedRequest.request().firstMatchingHeader("x-amz-content-sha256")) + .hasValue(AwsSigningConfig.AwsSignedBodyValue.STREAMING_UNSIGNED_PAYLOAD_TRAILER); + AssertionsForClassTypes.assertThat(signedRequest.request().firstMatchingHeader(Header.CONTENT_LENGTH)).isNotPresent(); + AssertionsForClassTypes.assertThat(signedRequest.request().firstMatchingHeader("x-amz-decoded-content-length")).hasValue("20"); + } + + @Test + public void sign_WithPayloadSigningFalseAndChunkEncodingTrueWithoutTrailer_Throws() { + SyncSignRequest request = generateBasicRequest( + AwsCredentialsIdentity.create("access", "secret"), + httpRequest -> httpRequest + .putHeader(Header.CONTENT_LENGTH, "20"), + signRequest -> signRequest + .putProperty(AwsV4HttpSigner.PAYLOAD_SIGNING_ENABLED, false) + .putProperty(CHUNK_ENCODING_ENABLED, true) + ); + + assertThrows(UnsupportedOperationException.class, () -> signer.sign(request)); + } } diff --git a/core/http-auth-aws-crt/src/test/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/AwsChunkedV4aPayloadSignerTest.java b/core/http-auth-aws-crt/src/test/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/AwsChunkedV4aPayloadSignerTest.java new file mode 100644 index 000000000000..ac3607135290 --- /dev/null +++ b/core/http-auth-aws-crt/src/test/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/AwsChunkedV4aPayloadSignerTest.java @@ -0,0 +1,233 @@ +/* + * 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.http.auth.aws.crt.internal.signer; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static software.amazon.awssdk.http.auth.aws.crt.internal.CrtUtils.toCredentials; +import static software.amazon.awssdk.http.auth.aws.util.SignerConstant.STREAMING_ECDSA_SIGNED_PAYLOAD; +import static software.amazon.awssdk.http.auth.aws.util.SignerConstant.STREAMING_ECDSA_SIGNED_PAYLOAD_TRAILER; +import static software.amazon.awssdk.http.auth.aws.util.SignerConstant.STREAMING_UNSIGNED_PAYLOAD_TRAILER; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.crt.auth.signing.AwsSigningConfig; +import software.amazon.awssdk.http.ContentStreamProvider; +import software.amazon.awssdk.http.Header; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; +import software.amazon.awssdk.http.auth.aws.signer.CredentialScope; +import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; + +/** + * Test the delegation of signing to the correct implementations. + */ +public class AwsChunkedV4aPayloadSignerTest { + + int chunkSize = 4; + + CredentialScope credentialScope = new CredentialScope("us-east-1", "s3", Instant.EPOCH); + + byte[] data = "{\"TableName\": \"foo\"}".getBytes(); + + ContentStreamProvider payload = () -> new ByteArrayInputStream(data); + + SdkHttpRequest.Builder requestBuilder; + + AwsChunkedV4aPayloadSigner signer = new AwsChunkedV4aPayloadSigner(credentialScope, chunkSize); + + @BeforeEach + public void setUp() { + requestBuilder = SdkHttpRequest + .builder() + .method(SdkHttpMethod.POST) + .putHeader("Host", "demo.us-east-1.amazonaws.com") + .putHeader("x-amz-archive-description", "test test") + .putHeader(Header.CONTENT_LENGTH, Integer.toString(data.length)) + .encodedPath("/") + .uri(URI.create("http://demo.us-east-1.amazonaws.com")); + } + + @Test + public void sign_withSignedPayload_shouldChunkEncodeWithSigV4Ext() throws IOException { + AwsSigningConfig signingConfig = basicSigningConfig(); + signingConfig.setSignedBodyValue(STREAMING_ECDSA_SIGNED_PAYLOAD); + V4aContext v4aContext = new V4aContext( + requestBuilder, + "sig".getBytes(StandardCharsets.UTF_8), + signingConfig + ); + + ContentStreamProvider signedPayload = signer.sign(payload, v4aContext); + + assertThat(requestBuilder.firstMatchingHeader(Header.CONTENT_LENGTH)).isNotPresent(); + assertThat(requestBuilder.firstMatchingHeader("x-amz-decoded-content-length")).hasValue(Integer.toString(data.length)); + + byte[] tmp = new byte[2048]; + int actualBytes = readAll(signedPayload.newStream(), tmp); + + int expectedBytes = expectedByteCount(data, chunkSize); + assertEquals(expectedBytes, actualBytes); + } + + @Test + public void sign_withSignedPayloadAndTrailer_shouldChunkEncodeWithSigV4ExtAndSigV4Trailer() throws IOException { + // TODO: Update trailer here when flexible checksums is implemented + AwsSigningConfig signingConfig = basicSigningConfig(); + signingConfig.setSignedBodyValue(STREAMING_ECDSA_SIGNED_PAYLOAD_TRAILER); + V4aContext v4aContext = new V4aContext( + requestBuilder, + "sig".getBytes(StandardCharsets.UTF_8), + signingConfig + ); + + ContentStreamProvider signedPayload = signer.sign(payload, v4aContext); + + assertThat(requestBuilder.firstMatchingHeader(Header.CONTENT_LENGTH)).isNotPresent(); + assertThat(requestBuilder.firstMatchingHeader("x-amz-decoded-content-length")).hasValue(Integer.toString(data.length)); + + byte[] tmp = new byte[2048]; + int actualBytes = readAll(signedPayload.newStream(), tmp); + int expectedBytes = expectedByteCount(data, chunkSize); + // include trailer size + trailer signature size + expectedBytes += 26 + 144; + + assertEquals(expectedBytes, actualBytes); + } + + @Test + public void sign_withTrailer_shouldChunkEncodeWithTrailer() throws IOException { + // TODO: Update trailer here when flexible checksums is implemented + AwsSigningConfig signingConfig = basicSigningConfig(); + signingConfig.setSignedBodyValue(STREAMING_UNSIGNED_PAYLOAD_TRAILER); + V4aContext v4aContext = new V4aContext( + requestBuilder, + "sig".getBytes(StandardCharsets.UTF_8), + signingConfig + ); + + ContentStreamProvider signedPayload = signer.sign(payload, v4aContext); + + assertThat(requestBuilder.firstMatchingHeader(Header.CONTENT_LENGTH)).isNotPresent(); + assertThat(requestBuilder.firstMatchingHeader("x-amz-decoded-content-length")).hasValue(Integer.toString(data.length)); + + byte[] tmp = new byte[2048]; + int actualBytes = readAll(signedPayload.newStream(), tmp); + int expectedBytes = expectedByteCountUnsigned(data, chunkSize); + + assertEquals(expectedBytes, actualBytes); + } + + @Test + public void sign_withoutContentLength_throws() { + V4aContext v4aContext = new V4aContext( + requestBuilder, + "sig".getBytes(StandardCharsets.UTF_8), + null + ); + requestBuilder.removeHeader(Header.CONTENT_LENGTH); + + assertThrows(IllegalArgumentException.class, () -> signer.sign(payload, v4aContext)); + } + + private int readAll(InputStream src, byte[] dst) throws IOException { + int read = 0; + int offset = 0; + while (read >= 0) { + read = src.read(); + if (read >= 0) { + dst[offset] = (byte) read; + offset += 1; + } + } + return offset; + } + + private AwsSigningConfig basicSigningConfig() { + AwsSigningConfig signingConfig = new AwsSigningConfig(); + + signingConfig.setCredentials(toCredentials(AwsCredentialsIdentity.create("key", "secret"))); + signingConfig.setService("s3"); + signingConfig.setRegion("aws-global"); + signingConfig.setAlgorithm(AwsSigningConfig.AwsSigningAlgorithm.SIGV4_ASYMMETRIC); + signingConfig.setTime(Instant.now().toEpochMilli()); + signingConfig.setSignatureType(AwsSigningConfig.AwsSignatureType.HTTP_REQUEST_CHUNK); + + return signingConfig; + } + + private int expectedByteCount(byte[] data, int chunkSize) { + int size = data.length; + int ecdsaSignatureLength = 144; + int chunkHeaderLength = 17 + Integer.toHexString(chunkSize).length(); + int numChunks = size / chunkSize; + int expectedBytes = 0; + + // normal chunks + // x;chunk-signature=\r\n\r\n + expectedBytes += numChunks * (chunkHeaderLength + ecdsaSignatureLength + chunkSize + 4); + + // remaining chunk + // n;chunk-signature=\r\n\\r\n + int remainingBytes = size % chunkSize; + if (remainingBytes > 0) { + int remainingChunkHeaderLength = 17 + Integer.toHexString(remainingBytes).length(); + expectedBytes += remainingChunkHeaderLength + ecdsaSignatureLength + remainingBytes + 4; + } + + // final chunk + // 0;chunk-signature=\r\n\r\n + int finalBytes = 0; + int finalChunkHeaderLength = 17 + Integer.toHexString(finalBytes).length(); + expectedBytes += finalChunkHeaderLength + ecdsaSignatureLength + 4; + + return expectedBytes; + } + + private int expectedByteCountUnsigned(byte[] data, int chunkSize) { + int size = data.length; + int chunkHeaderLength = Integer.toHexString(chunkSize).length(); + int numChunks = size / chunkSize; + int expectedBytes = 0; + + // normal chunks + // x\r\n\r\n + expectedBytes += numChunks * (chunkHeaderLength + chunkSize + 4); + + // remaining chunk + // n\r\n\\r\n + int remainingBytes = size % chunkSize; + if (remainingBytes > 0) { + int remainingChunkHeaderLength = Integer.toHexString(remainingBytes).length(); + expectedBytes += remainingChunkHeaderLength + remainingBytes + 4; + } + + // final chunk + // 0\r\n\r\n + int finalBytes = 0; + int finalChunkHeaderLength = Integer.toHexString(finalBytes).length(); + expectedBytes += finalChunkHeaderLength + 4; + + return expectedBytes; + } +} diff --git a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/AwsV4aHttpSigner.java b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/AwsV4aHttpSigner.java index 43530f6c7dae..be8b363155b6 100644 --- a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/AwsV4aHttpSigner.java +++ b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/AwsV4aHttpSigner.java @@ -78,6 +78,13 @@ public interface AwsV4aHttpSigner extends HttpSigner { SignerProperty PAYLOAD_SIGNING_ENABLED = SignerProperty.create(Boolean.class, "PayloadSigningEnabled"); + /** + * Whether to indicate that a payload is chunk-encoded or not. This property defaults to false. This can be set true to + * enable the `aws-chunk` content-encoding + */ + SignerProperty CHUNK_ENCODING_ENABLED = + SignerProperty.create(Boolean.class, "ChunkEncodingEnabled"); + /** * Get a default implementation of a {@link AwsV4aHttpSigner} */ diff --git a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/SignerLoader.java b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/SignerLoader.java index 6b9f5022f14a..845c77b0e190 100644 --- a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/SignerLoader.java +++ b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/internal/signer/SignerLoader.java @@ -15,8 +15,8 @@ package software.amazon.awssdk.http.auth.aws.internal.signer; +import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import software.amazon.awssdk.annotations.SdkInternalApi; @@ -48,14 +48,14 @@ private static > T get(Class exp private static > T initializeV4aSigner(Class expectedClass, String fqcn) { try { Class signerClass = ClassLoaderHelper.loadClass(fqcn, false, (Class) null); - Method m = signerClass.getDeclaredMethod("create"); - Object result = m.invoke(null); + Constructor m = signerClass.getConstructor(); + Object result = m.newInstance(); return expectedClass.cast(result); } catch (ClassNotFoundException e) { throw new IllegalStateException("Cannot find the " + fqcn + " class. " + "To invoke a request that requires a SigV4a signer, such as region independent " + "signing, the 'aws-crt' core module must be on the class path.", e); - } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) { throw new IllegalStateException("Failed to create " + fqcn, e); } } diff --git a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/util/SignerConstant.java b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/util/SignerConstant.java index e7c5dbb5ec0f..3b09cd7c53ad 100644 --- a/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/util/SignerConstant.java +++ b/core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/util/SignerConstant.java @@ -59,6 +59,10 @@ public final class SignerConstant { public static final String STREAMING_UNSIGNED_PAYLOAD_TRAILER = "STREAMING-UNSIGNED-PAYLOAD-TRAILER"; + public static final String STREAMING_ECDSA_SIGNED_PAYLOAD = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD"; + + public static final String STREAMING_ECDSA_SIGNED_PAYLOAD_TRAILER = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER"; + public static final String STREAMING_SIGNED_PAYLOAD = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"; public static final String STREAMING_SIGNED_PAYLOAD_TRAILER = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER";