Skip to content

Commit 43b5b43

Browse files
authored
Fix Content-Length requirement, header updates in payload signer, http fallback to signed (#4492)
* Fix header updates in payload signer, http fallback to signed * Update payload-signer to pre-calculate and add Content-Length * Add null test * Fix off-by-one error
1 parent 0b9f12e commit 43b5b43

File tree

14 files changed

+471
-105
lines changed

14 files changed

+471
-105
lines changed

core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/AwsChunkedV4aPayloadSigner.java

+135-31
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@
2525

2626
import java.io.InputStream;
2727
import java.nio.charset.StandardCharsets;
28+
import java.util.ArrayList;
2829
import java.util.Collections;
2930
import java.util.List;
3031
import software.amazon.awssdk.annotations.SdkInternalApi;
3132
import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm;
3233
import software.amazon.awssdk.http.ContentStreamProvider;
34+
import software.amazon.awssdk.http.Header;
3335
import software.amazon.awssdk.http.SdkHttpRequest;
3436
import software.amazon.awssdk.http.auth.aws.internal.signer.CredentialScope;
3537
import software.amazon.awssdk.http.auth.aws.internal.signer.checksums.SdkChecksum;
@@ -52,6 +54,7 @@ public final class AwsChunkedV4aPayloadSigner implements V4aPayloadSigner {
5254
private final CredentialScope credentialScope;
5355
private final int chunkSize;
5456
private final ChecksumAlgorithm checksumAlgorithm;
57+
private final List<Pair<String, List<String>>> preExistingTrailers = new ArrayList<>();
5558

5659
private AwsChunkedV4aPayloadSigner(Builder builder) {
5760
this.credentialScope = Validate.paramNotNull(builder.credentialScope, "CredentialScope");
@@ -65,16 +68,14 @@ public static Builder builder() {
6568

6669
@Override
6770
public ContentStreamProvider sign(ContentStreamProvider payload, V4aContext v4aContext) {
68-
SdkHttpRequest.Builder request = v4aContext.getSignedRequest();
69-
moveContentLength(request);
70-
7171
InputStream inputStream = payload != null ? payload.newStream() : new StringInputStream("");
7272
ChunkedEncodedInputStream.Builder chunkedEncodedInputStreamBuilder = ChunkedEncodedInputStream
7373
.builder()
7474
.inputStream(inputStream)
7575
.chunkSize(chunkSize)
7676
.header(chunk -> Integer.toHexString(chunk.length).getBytes(StandardCharsets.UTF_8));
77-
setupPreExistingTrailers(chunkedEncodedInputStreamBuilder, request);
77+
78+
preExistingTrailers.forEach(trailer -> chunkedEncodedInputStreamBuilder.addTrailer(() -> trailer));
7879

7980
switch (v4aContext.getSigningConfig().getSignedBodyValue()) {
8081
case STREAMING_ECDSA_SIGNED_PAYLOAD: {
@@ -83,12 +84,12 @@ public ContentStreamProvider sign(ContentStreamProvider payload, V4aContext v4aC
8384
break;
8485
}
8586
case STREAMING_UNSIGNED_PAYLOAD_TRAILER:
86-
setupChecksumTrailerIfNeeded(chunkedEncodedInputStreamBuilder, request);
87+
setupChecksumTrailerIfNeeded(chunkedEncodedInputStreamBuilder);
8788
break;
8889
case STREAMING_ECDSA_SIGNED_PAYLOAD_TRAILER: {
8990
RollingSigner rollingSigner = new RollingSigner(v4aContext.getSignature(), v4aContext.getSigningConfig());
9091
chunkedEncodedInputStreamBuilder.addExtension(new SigV4aChunkExtensionProvider(rollingSigner, credentialScope));
91-
setupChecksumTrailerIfNeeded(chunkedEncodedInputStreamBuilder, request);
92+
setupChecksumTrailerIfNeeded(chunkedEncodedInputStreamBuilder);
9293
chunkedEncodedInputStreamBuilder.addTrailer(
9394
new SigV4aTrailerProvider(chunkedEncodedInputStreamBuilder.trailers(), rollingSigner, credentialScope)
9495
);
@@ -101,49 +102,152 @@ public ContentStreamProvider sign(ContentStreamProvider payload, V4aContext v4aC
101102
return new ResettableContentStreamProvider(chunkedEncodedInputStreamBuilder::build);
102103
}
103104

104-
/**
105-
* Add the checksum as a chunk-trailer and add it to the request's trailer header.
106-
* <p>
107-
* The checksum-algorithm MUST be set if this is called, otherwise it will throw.
108-
*/
109-
private void setupChecksumTrailerIfNeeded(ChunkedEncodedInputStream.Builder builder, SdkHttpRequest.Builder request) {
110-
if (checksumAlgorithm == null) {
111-
return;
105+
@Override
106+
public void beforeSigning(SdkHttpRequest.Builder request, ContentStreamProvider payload, String checksum) {
107+
long encodedContentLength = 0;
108+
long contentLength = moveContentLength(request, payload != null ? payload.newStream() : new StringInputStream(""));
109+
setupPreExistingTrailers(request);
110+
111+
// pre-existing trailers
112+
encodedContentLength += calculateExistingTrailersLength();
113+
114+
switch (checksum) {
115+
case STREAMING_ECDSA_SIGNED_PAYLOAD: {
116+
long extensionsLength = 161; // ;chunk-signature:<sigv4a-ecsda hex signature, 144 bytes>
117+
encodedContentLength += calculateChunksLength(contentLength, extensionsLength);
118+
break;
119+
}
120+
case STREAMING_UNSIGNED_PAYLOAD_TRAILER:
121+
if (checksumAlgorithm != null) {
122+
encodedContentLength += calculateChecksumTrailerLength(checksumHeaderName(checksumAlgorithm));
123+
}
124+
encodedContentLength += calculateChunksLength(contentLength, 0);
125+
break;
126+
case STREAMING_ECDSA_SIGNED_PAYLOAD_TRAILER: {
127+
long extensionsLength = 161; // ;chunk-signature:<sigv4a-ecsda hex signature, 144 bytes>
128+
encodedContentLength += calculateChunksLength(contentLength, extensionsLength);
129+
if (checksumAlgorithm != null) {
130+
encodedContentLength += calculateChecksumTrailerLength(checksumHeaderName(checksumAlgorithm));
131+
}
132+
encodedContentLength += 170; // x-amz-trailer-signature:<sigv4a-ecsda hex signature, 144 bytes>\r\n
133+
break;
134+
}
135+
default:
136+
throw new UnsupportedOperationException();
112137
}
113-
SdkChecksum sdkChecksum = fromChecksumAlgorithm(checksumAlgorithm);
114-
ChecksumInputStream checksumInputStream = new ChecksumInputStream(
115-
builder.inputStream(),
116-
Collections.singleton(sdkChecksum)
117-
);
118-
String checksumHeaderName = checksumHeaderName(checksumAlgorithm);
119138

120-
TrailerProvider checksumTrailer = new ChecksumTrailerProvider(sdkChecksum, checksumHeaderName);
139+
// terminating \r\n
140+
encodedContentLength += 2;
121141

122-
request.appendHeader(X_AMZ_TRAILER, checksumHeaderName);
123-
builder.inputStream(checksumInputStream).addTrailer(checksumTrailer);
142+
if (checksumAlgorithm != null) {
143+
String checksumHeaderName = checksumHeaderName(checksumAlgorithm);
144+
request.appendHeader(X_AMZ_TRAILER, checksumHeaderName);
145+
}
146+
request.putHeader(Header.CONTENT_LENGTH, Long.toString(encodedContentLength));
124147
}
125148

126149
/**
127-
* Create chunk-trailers for each pre-existing trailer given in the request.
150+
* Set up a map of pre-existing trailer (headers) for the given request to be used when chunk-encoding the payload.
128151
* <p>
129152
* However, we need to validate that these are valid trailers. Since aws-chunked encoding adds the checksum as a trailer, it
130153
* isn't part of the request headers, but other trailers MUST be present in the request-headers.
131154
*/
132-
private void setupPreExistingTrailers(ChunkedEncodedInputStream.Builder builder, SdkHttpRequest.Builder request) {
133-
List<String> trailerHeaders = request.matchingHeaders(X_AMZ_TRAILER);
134-
135-
for (String header : trailerHeaders) {
155+
private void setupPreExistingTrailers(SdkHttpRequest.Builder request) {
156+
for (String header : request.matchingHeaders(X_AMZ_TRAILER)) {
136157
List<String> values = request.matchingHeaders(header);
137158
if (values.isEmpty()) {
138159
throw new IllegalArgumentException(header + " must be present in the request headers to be a valid trailer.");
139160
}
140-
141-
// Add the trailer to the aws-chunked stream-builder, and remove it from the request headers
142-
builder.addTrailer(() -> Pair.of(header, values));
161+
preExistingTrailers.add(Pair.of(header, values));
143162
request.removeHeader(header);
144163
}
145164
}
146165

166+
private long calculateChunksLength(long contentLength, long extensionsLength) {
167+
long lengthInBytes = 0;
168+
long chunkHeaderLength = Integer.toHexString(chunkSize).length();
169+
long numChunks = contentLength / chunkSize;
170+
171+
// normal chunks
172+
// x<metadata>\r\n<data>\r\n
173+
lengthInBytes += numChunks * (chunkHeaderLength + extensionsLength + 2 + chunkSize + 2);
174+
175+
// remaining chunk
176+
// x<metadata>\r\n<data>\r\n
177+
long remainingBytes = contentLength % chunkSize;
178+
if (remainingBytes > 0) {
179+
long remainingChunkHeaderLength = Long.toHexString(remainingBytes).length();
180+
lengthInBytes += remainingChunkHeaderLength + extensionsLength + 2 + remainingBytes + 2;
181+
}
182+
183+
// final chunk
184+
// 0<metadata>\r\n
185+
lengthInBytes += 1 + extensionsLength + 2;
186+
187+
return lengthInBytes;
188+
}
189+
190+
private long calculateExistingTrailersLength() {
191+
long lengthInBytes = 0;
192+
193+
for (Pair<String, List<String>> trailer : preExistingTrailers) {
194+
// size of trailer
195+
lengthInBytes += calculateTrailerLength(trailer);
196+
}
197+
198+
return lengthInBytes;
199+
}
200+
201+
private long calculateTrailerLength(Pair<String, List<String>> trailer) {
202+
// size of trailer-header and colon
203+
long lengthInBytes = trailer.left().length() + 1;
204+
205+
// size of trailer-values
206+
for (String value : trailer.right()) {
207+
lengthInBytes += value.length();
208+
}
209+
210+
// size of commas between trailer-values, 1 less comma than # of values
211+
lengthInBytes += trailer.right().size() - 1;
212+
213+
// terminating \r\n
214+
return lengthInBytes + 2;
215+
}
216+
217+
private long calculateChecksumTrailerLength(String checksumHeaderName) {
218+
// size of checksum trailer-header and colon
219+
long lengthInBytes = checksumHeaderName.length() + 1;
220+
221+
// get the base checksum for the algorithm
222+
SdkChecksum sdkChecksum = fromChecksumAlgorithm(checksumAlgorithm);
223+
// size of checksum value as hex-string
224+
lengthInBytes += sdkChecksum.getChecksum().length();
225+
226+
// terminating \r\n
227+
return lengthInBytes + 2;
228+
}
229+
230+
/**
231+
* Add the checksum as a trailer to the chunk-encoded stream.
232+
* <p>
233+
* If the checksum-algorithm is not present, then nothing is done.
234+
*/
235+
private void setupChecksumTrailerIfNeeded(ChunkedEncodedInputStream.Builder builder) {
236+
if (checksumAlgorithm == null) {
237+
return;
238+
}
239+
String checksumHeaderName = checksumHeaderName(checksumAlgorithm);
240+
SdkChecksum sdkChecksum = fromChecksumAlgorithm(checksumAlgorithm);
241+
ChecksumInputStream checksumInputStream = new ChecksumInputStream(
242+
builder.inputStream(),
243+
Collections.singleton(sdkChecksum)
244+
);
245+
246+
TrailerProvider checksumTrailer = new ChecksumTrailerProvider(sdkChecksum, checksumHeaderName);
247+
248+
builder.inputStream(checksumInputStream).addTrailer(checksumTrailer);
249+
}
250+
147251
static final class Builder {
148252
private CredentialScope credentialScope;
149253
private Integer chunkSize;

core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/DefaultAwsCrtV4aHttpSigner.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,11 @@ private static SignedRequest doSign(SignRequest<? extends AwsCredentialsIdentity
191191
.build();
192192
}
193193

194-
SdkHttpRequest sanitizedRequest = sanitizeRequest(request.request());
194+
SdkHttpRequest.Builder requestBuilder = request.request().toBuilder();
195+
196+
payloadSigner.beforeSigning(requestBuilder, request.payload().orElse(null), signingConfig.getSignedBodyValue());
197+
198+
SdkHttpRequest sanitizedRequest = sanitizeRequest(requestBuilder.build());
195199

196200
HttpRequest crtRequest = toRequest(sanitizedRequest, request.payload().orElse(null));
197201

core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/RollingSigner.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@ public RollingSigner(byte[] seedSignature, AwsSigningConfig signingConfig) {
4646
}
4747

4848
private static byte[] signChunk(byte[] chunkBody, byte[] previousSignature, AwsSigningConfig signingConfig) {
49+
// All the config remains the same as signing config except the Signature Type.
50+
AwsSigningConfig configCopy = signingConfig.clone();
51+
configCopy.setSignatureType(AwsSigningConfig.AwsSignatureType.HTTP_REQUEST_CHUNK);
52+
4953
HttpRequestBodyStream crtBody = new CrtInputStream(() -> new ByteArrayInputStream(chunkBody));
50-
return CompletableFutureUtils.joinLikeSync(AwsSigner.signChunk(crtBody, previousSignature, signingConfig));
54+
return CompletableFutureUtils.joinLikeSync(AwsSigner.signChunk(crtBody, previousSignature, configCopy));
5155
}
5256

5357
private static AwsSigningResult signTrailerHeaders(Map<String, List<String>> headerMap, byte[] previousSignature,

core/http-auth-aws/src/main/java/software/amazon/awssdk/http/auth/aws/crt/internal/signer/V4aPayloadSigner.java

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import software.amazon.awssdk.annotations.SdkInternalApi;
1919
import software.amazon.awssdk.http.ContentStreamProvider;
20+
import software.amazon.awssdk.http.SdkHttpRequest;
2021

2122
/**
2223
* An interface for defining how to sign a payload via SigV4a.
@@ -34,4 +35,10 @@ static V4aPayloadSigner create() {
3435
* Given a payload and v4a-context, sign the payload via the SigV4a process.
3536
*/
3637
ContentStreamProvider sign(ContentStreamProvider payload, V4aContext v4Context);
38+
39+
/**
40+
* Modify a request before it is signed, such as changing headers or query-parameters.
41+
*/
42+
default void beforeSigning(SdkHttpRequest.Builder request, ContentStreamProvider payload, String checksum) {
43+
}
3744
}

0 commit comments

Comments
 (0)