Skip to content

Commit 5ac3968

Browse files
committed
Support payload signing in V4a signer
1 parent 5cbd30a commit 5ac3968

File tree

13 files changed

+932
-78
lines changed

13 files changed

+932
-78
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Add support for signing async payloads in the default `AwsV4aHttpSigner`."
6+
}

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

Lines changed: 143 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,19 @@
2020
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.STREAMING_ECDSA_SIGNED_PAYLOAD;
2121
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.STREAMING_ECDSA_SIGNED_PAYLOAD_TRAILER;
2222
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.STREAMING_UNSIGNED_PAYLOAD_TRAILER;
23+
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.X_AMZ_DECODED_CONTENT_LENGTH;
2324
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.X_AMZ_TRAILER;
25+
import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerUtils.computeAndMoveContentLength;
2426

2527
import java.io.InputStream;
28+
import java.nio.ByteBuffer;
2629
import java.nio.charset.StandardCharsets;
2730
import java.util.ArrayList;
2831
import java.util.Collections;
2932
import java.util.List;
33+
import java.util.Optional;
34+
import java.util.concurrent.CompletableFuture;
35+
import org.reactivestreams.Publisher;
3036
import software.amazon.awssdk.annotations.SdkInternalApi;
3137
import software.amazon.awssdk.checksums.SdkChecksum;
3238
import software.amazon.awssdk.checksums.spi.ChecksumAlgorithm;
@@ -35,8 +41,12 @@
3541
import software.amazon.awssdk.http.SdkHttpRequest;
3642
import software.amazon.awssdk.http.auth.aws.internal.signer.CredentialScope;
3743
import software.amazon.awssdk.http.auth.aws.internal.signer.NoOpPayloadChecksumStore;
44+
import software.amazon.awssdk.http.auth.aws.internal.signer.chunkedencoding.AsyncChunkEncodedPayload;
3845
import software.amazon.awssdk.http.auth.aws.internal.signer.chunkedencoding.ChecksumTrailerProvider;
3946
import software.amazon.awssdk.http.auth.aws.internal.signer.chunkedencoding.ChunkedEncodedInputStream;
47+
import software.amazon.awssdk.http.auth.aws.internal.signer.chunkedencoding.ChunkedEncodedPayload;
48+
import software.amazon.awssdk.http.auth.aws.internal.signer.chunkedencoding.ChunkedEncodedPublisher;
49+
import software.amazon.awssdk.http.auth.aws.internal.signer.chunkedencoding.SyncChunkEncodedPayload;
4050
import software.amazon.awssdk.http.auth.aws.internal.signer.chunkedencoding.TrailerProvider;
4151
import software.amazon.awssdk.http.auth.aws.internal.signer.io.ChecksumInputStream;
4252
import software.amazon.awssdk.http.auth.aws.internal.signer.io.ResettableContentStreamProvider;
@@ -83,39 +93,73 @@ public ContentStreamProvider sign(ContentStreamProvider payload, V4aRequestSigni
8393
.chunkSize(chunkSize)
8494
.header(chunk -> Integer.toHexString(chunk.remaining()).getBytes(StandardCharsets.UTF_8));
8595

86-
preExistingTrailers.forEach(trailer -> chunkedEncodedInputStreamBuilder.addTrailer(() -> trailer));
96+
SyncChunkEncodedPayload chunkedPayload = new SyncChunkEncodedPayload(chunkedEncodedInputStreamBuilder);
97+
98+
signCommon(chunkedPayload, requestSigningResult);
99+
100+
return new ResettableContentStreamProvider(chunkedEncodedInputStreamBuilder::build);
101+
}
102+
103+
/**
104+
* Given a payload and result of request signing, sign the payload via the SigV4 process.
105+
*/
106+
@Override
107+
public Publisher<ByteBuffer> signAsync(Publisher<ByteBuffer> payload, V4aRequestSigningResult requestSigningResult) {
108+
ChunkedEncodedPublisher.Builder chunkedStreamBuilder = ChunkedEncodedPublisher.builder()
109+
.publisher(payload)
110+
.chunkSize(chunkSize)
111+
.addEmptyTrailingChunk(true);
112+
AsyncChunkEncodedPayload chunkedPayload = new AsyncChunkEncodedPayload(chunkedStreamBuilder);
113+
114+
signCommon(chunkedPayload, requestSigningResult);
115+
116+
return chunkedStreamBuilder.build();
117+
}
118+
119+
private ChunkedEncodedPayload signCommon(ChunkedEncodedPayload payload, V4aRequestSigningResult requestSigningResult) {
120+
SdkHttpRequest.Builder request = requestSigningResult.getSignedRequest();
121+
122+
payload.decodedContentLength(request.firstMatchingHeader(X_AMZ_DECODED_CONTENT_LENGTH)
123+
.map(Long::parseLong)
124+
.orElseThrow(() -> {
125+
String msg = String.format("Expected header '%s' to be present",
126+
X_AMZ_DECODED_CONTENT_LENGTH);
127+
return new RuntimeException(msg);
128+
}));
129+
130+
preExistingTrailers.forEach(trailer -> payload.addTrailer(() -> trailer));
87131

88132
switch (requestSigningResult.getSigningConfig().getSignedBodyValue()) {
89133
case STREAMING_ECDSA_SIGNED_PAYLOAD: {
90134
RollingSigner rollingSigner = new RollingSigner(requestSigningResult.getSignature(),
91135
requestSigningResult.getSigningConfig());
92-
chunkedEncodedInputStreamBuilder.addExtension(new SigV4aChunkExtensionProvider(rollingSigner, credentialScope));
136+
payload.addExtension(new SigV4aChunkExtensionProvider(rollingSigner, credentialScope));
93137
break;
94138
}
95139
case STREAMING_UNSIGNED_PAYLOAD_TRAILER:
96-
setupChecksumTrailerIfNeeded(chunkedEncodedInputStreamBuilder);
140+
setupChecksumTrailerIfNeeded(payload);
97141
break;
98142
case STREAMING_ECDSA_SIGNED_PAYLOAD_TRAILER: {
99143
RollingSigner rollingSigner = new RollingSigner(requestSigningResult.getSignature(),
100144
requestSigningResult.getSigningConfig());
101-
chunkedEncodedInputStreamBuilder.addExtension(new SigV4aChunkExtensionProvider(rollingSigner, credentialScope));
102-
setupChecksumTrailerIfNeeded(chunkedEncodedInputStreamBuilder);
103-
chunkedEncodedInputStreamBuilder.addTrailer(
104-
new SigV4aTrailerProvider(chunkedEncodedInputStreamBuilder.trailers(), rollingSigner, credentialScope)
145+
payload.addExtension(new SigV4aChunkExtensionProvider(rollingSigner, credentialScope));
146+
setupChecksumTrailerIfNeeded(payload);
147+
payload.addTrailer(
148+
new SigV4aTrailerProvider(payload.trailers(), rollingSigner, credentialScope)
105149
);
106150
break;
107151
}
108152
default:
109153
throw new UnsupportedOperationException();
110154
}
111155

112-
return new ResettableContentStreamProvider(chunkedEncodedInputStreamBuilder::build);
156+
return payload;
113157
}
114158

115159
@Override
116160
public void beforeSigning(SdkHttpRequest.Builder request, ContentStreamProvider payload, String checksum) {
117161
long encodedContentLength = 0;
118-
long contentLength = SignerUtils.computeAndMoveContentLength(request, payload);
162+
long contentLength = computeAndMoveContentLength(request, payload);
119163
setupPreExistingTrailers(request);
120164

121165
// pre-existing trailers
@@ -157,6 +201,72 @@ public void beforeSigning(SdkHttpRequest.Builder request, ContentStreamProvider
157201
// CRT-signed request doesn't expect 'aws-chunked' Content-Encoding, so we don't add it
158202
}
159203

204+
@Override
205+
public CompletableFuture<Pair<SdkHttpRequest.Builder, Optional<Publisher<ByteBuffer>>>> beforeSigningAsync(
206+
SdkHttpRequest.Builder request, Publisher<ByteBuffer> payload, String checksum) {
207+
208+
return SignerUtils.moveContentLength(request, payload)
209+
.thenApply(p -> {
210+
SdkHttpRequest.Builder requestBuilder = p.left();
211+
setupPreExistingTrailers(requestBuilder);
212+
213+
long decodedContentLength =
214+
requestBuilder.firstMatchingHeader(X_AMZ_DECODED_CONTENT_LENGTH)
215+
.map(Long::parseLong)
216+
// should not happen, this header is added by
217+
// moveContentLength
218+
.orElseThrow(() -> new RuntimeException(
219+
X_AMZ_DECODED_CONTENT_LENGTH + " header not present"));
220+
221+
long encodedContentLength = calculateEncodedContentLength(request, decodedContentLength, checksum);
222+
223+
if (checksumAlgorithm != null) {
224+
String checksumHeaderName = checksumHeaderName(checksumAlgorithm);
225+
request.appendHeader(X_AMZ_TRAILER, checksumHeaderName);
226+
}
227+
request.putHeader(Header.CONTENT_LENGTH, Long.toString(encodedContentLength));
228+
229+
return Pair.of(requestBuilder, p.right());
230+
});
231+
}
232+
233+
private long calculateEncodedContentLength(SdkHttpRequest.Builder requestBuilder, long decodedContentLength,
234+
String checksum) {
235+
long encodedContentLength = 0;
236+
237+
encodedContentLength += calculateExistingTrailersLength();
238+
239+
switch (checksum) {
240+
case STREAMING_ECDSA_SIGNED_PAYLOAD: {
241+
long extensionsLength = 161; // ;chunk-signature:<sigv4a-ecsda hex signature, 144 bytes>
242+
encodedContentLength += calculateChunksLength(decodedContentLength, extensionsLength);
243+
break;
244+
}
245+
case STREAMING_UNSIGNED_PAYLOAD_TRAILER:
246+
if (checksumAlgorithm != null) {
247+
encodedContentLength += calculateChecksumTrailerLength(checksumHeaderName(checksumAlgorithm));
248+
}
249+
encodedContentLength += calculateChunksLength(decodedContentLength, 0);
250+
break;
251+
case STREAMING_ECDSA_SIGNED_PAYLOAD_TRAILER: {
252+
long extensionsLength = 161; // ;chunk-signature:<sigv4a-ecsda hex signature, 144 bytes>
253+
encodedContentLength += calculateChunksLength(decodedContentLength, extensionsLength);
254+
if (checksumAlgorithm != null) {
255+
encodedContentLength += calculateChecksumTrailerLength(checksumHeaderName(checksumAlgorithm));
256+
}
257+
encodedContentLength += 170; // x-amz-trailer-signature:<sigv4a-ecsda hex signature, 144 bytes>\r\n
258+
break;
259+
}
260+
default:
261+
throw new UnsupportedOperationException();
262+
}
263+
264+
// terminating \r\n
265+
encodedContentLength += 2;
266+
267+
return encodedContentLength;
268+
}
269+
160270
/**
161271
* Set up a map of pre-existing trailer (headers) for the given request to be used when chunk-encoding the payload.
162272
* <p>
@@ -270,6 +380,30 @@ private void setupChecksumTrailerIfNeeded(ChunkedEncodedInputStream.Builder buil
270380
builder.inputStream(checksumInputStream).addTrailer(checksumTrailer);
271381
}
272382

383+
private void setupChecksumTrailerIfNeeded(ChunkedEncodedPayload payload) {
384+
if (checksumAlgorithm == null) {
385+
return;
386+
}
387+
String checksumHeaderName = checksumHeaderName(checksumAlgorithm);
388+
389+
String cachedChecksum = getCachedChecksum();
390+
391+
if (cachedChecksum != null) {
392+
LOG.debug(() -> String.format("Cached payload checksum available for algorithm %s: %s. Using cached value",
393+
checksumAlgorithm.algorithmId(), checksumHeaderName));
394+
payload.addTrailer(() -> Pair.of(checksumHeaderName, Collections.singletonList(cachedChecksum)));
395+
return;
396+
}
397+
398+
SdkChecksum sdkChecksum = fromChecksumAlgorithm(checksumAlgorithm);
399+
payload.checksumPayload(sdkChecksum);
400+
401+
TrailerProvider checksumTrailer =
402+
new ChecksumTrailerProvider(sdkChecksum, checksumHeaderName, checksumAlgorithm, payloadChecksumStore);
403+
404+
payload.addTrailer(checksumTrailer);
405+
}
406+
273407
private String getCachedChecksum() {
274408
byte[] checksumBytes = payloadChecksumStore.getChecksumValue(checksumAlgorithm);
275409
if (checksumBytes != null) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.http.auth.aws.crt.internal.signer;
17+
18+
import java.nio.ByteBuffer;
19+
import org.reactivestreams.Publisher;
20+
import software.amazon.awssdk.annotations.SdkInternalApi;
21+
import software.amazon.awssdk.crt.http.HttpRequestBodyStream;
22+
import software.amazon.awssdk.utils.async.ByteBufferStoringSubscriber;
23+
import software.amazon.awssdk.utils.async.ByteBufferStoringSubscriber.TransferResult;
24+
25+
@SdkInternalApi
26+
public final class CrtRequestBodyAdapter implements HttpRequestBodyStream {
27+
private static final int BUFFER_SIZE = 4 * 1024 * 1024; // 4 MB
28+
private final Publisher<ByteBuffer> requestPublisher;
29+
private final long contentLength;
30+
private ByteBufferStoringSubscriber requestBodySubscriber;
31+
32+
public CrtRequestBodyAdapter(Publisher<ByteBuffer> requestPublisher, long contentLength) {
33+
this.requestPublisher = requestPublisher;
34+
this.contentLength = contentLength;
35+
this.requestBodySubscriber = new ByteBufferStoringSubscriber(BUFFER_SIZE);
36+
}
37+
38+
@Override
39+
public boolean sendRequestBody(ByteBuffer bodyBytesOut) {
40+
return requestBodySubscriber.transferTo(bodyBytesOut) == TransferResult.END_OF_STREAM;
41+
}
42+
43+
@Override
44+
public boolean resetPosition() {
45+
requestBodySubscriber = new ByteBufferStoringSubscriber(BUFFER_SIZE);
46+
requestPublisher.subscribe(requestBodySubscriber);
47+
return true;
48+
}
49+
50+
@Override
51+
public long getLength() {
52+
return contentLength;
53+
}
54+
}

0 commit comments

Comments
 (0)