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 extends AwsCredentialsIdentity> 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 extends AwsCredentialsIdentity> 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 extends AwsCredentialsIdentity> 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 extends AwsCredentialsIdentity> 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 extends AwsCredentialsIdentity> 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";