Skip to content

Commit 04459bb

Browse files
committed
Normalize URLs during signing (except for S3).
During signing when generating the canonical request, request paths are supposed to be normalized to remove . and .. path components. We were not doing this before, making it possible to construct requests that fail signature validation.
1 parent 639e57c commit 04459bb

File tree

13 files changed

+302
-105
lines changed

13 files changed

+302
-105
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Fixed an issue that could result in signature mismatch exceptions when requests included . or .."
6+
}

core/auth/src/main/java/software/amazon/awssdk/auth/signer/AwsSignerExecutionAttribute.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ public final class AwsSignerExecutionAttribute extends SdkExecutionAttribute {
5858
*/
5959
public static final ExecutionAttribute<Boolean> SIGNER_DOUBLE_URL_ENCODE = new ExecutionAttribute<>("DoubleUrlEncode");
6060

61+
/**
62+
* The key to specify whether to normalize the resource path during signing.
63+
*/
64+
public static final ExecutionAttribute<Boolean> SIGNER_NORMALIZE_PATH =
65+
new ExecutionAttribute<>("NormalizePath");
66+
6167
/**
6268
* The key to specify the expiration time when pre-signing aws requests.
6369
*/

core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/AbstractAws4Signer.java

Lines changed: 125 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@
2424
import java.time.Instant;
2525
import java.util.ArrayList;
2626
import java.util.Arrays;
27+
import java.util.Collections;
2728
import java.util.Comparator;
2829
import java.util.List;
30+
import java.util.SortedMap;
31+
import java.util.TreeMap;
2932
import software.amazon.awssdk.annotations.SdkInternalApi;
3033
import software.amazon.awssdk.auth.credentials.AwsCredentials;
3134
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
@@ -41,6 +44,7 @@
4144
import software.amazon.awssdk.core.internal.util.HttpChecksumUtils;
4245
import software.amazon.awssdk.core.signer.Presigner;
4346
import software.amazon.awssdk.http.SdkHttpFullRequest;
47+
import software.amazon.awssdk.http.SdkHttpRequest;
4448
import software.amazon.awssdk.utils.BinaryUtils;
4549
import software.amazon.awssdk.utils.Logger;
4650
import software.amazon.awssdk.utils.Pair;
@@ -81,6 +85,7 @@ protected SdkHttpFullRequest.Builder doSign(SdkHttpFullRequest request,
8185
ContentChecksum contentChecksum) {
8286

8387
SdkHttpFullRequest.Builder mutableRequest = request.toBuilder();
88+
8489
AwsCredentials sanitizedCredentials = sanitizeCredentials(signingParams.awsCredentials());
8590
if (sanitizedCredentials instanceof AwsSessionCredentials) {
8691
addSessionCredentials(mutableRequest, (AwsSessionCredentials) sanitizedCredentials);
@@ -97,9 +102,11 @@ protected SdkHttpFullRequest.Builder doSign(SdkHttpFullRequest request,
97102
putChecksumHeader(signingParams.checksumParams(), contentChecksum.contentFlexibleChecksum(),
98103
mutableRequest, contentChecksum.contentHash());
99104

100-
CanonicalRequest canonicalRequest = createCanonicalRequest(mutableRequest,
105+
CanonicalRequest canonicalRequest = createCanonicalRequest(request,
106+
mutableRequest,
101107
contentChecksum.contentHash(),
102-
signingParams.doubleUrlEncode());
108+
signingParams.doubleUrlEncode(),
109+
signingParams.normalizePath());
103110

104111
String canonicalRequestString = canonicalRequest.string();
105112
String stringToSign = createStringToSign(canonicalRequestString, requestParams);
@@ -138,8 +145,11 @@ protected SdkHttpFullRequest.Builder doPresign(SdkHttpFullRequest request,
138145
// Add the important parameters for v4 signing
139146
String contentSha256 = calculateContentHashPresign(mutableRequest, signingParams);
140147

141-
CanonicalRequest canonicalRequest = createCanonicalRequest(mutableRequest, contentSha256,
142-
signingParams.doubleUrlEncode());
148+
CanonicalRequest canonicalRequest = createCanonicalRequest(request,
149+
mutableRequest,
150+
contentSha256,
151+
signingParams.doubleUrlEncode(),
152+
signingParams.normalizePath());
143153

144154
addPreSignInformationToRequest(mutableRequest, canonicalRequest, sanitizedCredentials,
145155
requestParams, expirationInSeconds);
@@ -237,10 +247,12 @@ protected final byte[] deriveSigningKey(AwsCredentials credentials, Instant sign
237247
* .amazon.com/general/latest/gr/sigv4-create-canonical-request.html to
238248
* generate the canonical request.
239249
*/
240-
private CanonicalRequest createCanonicalRequest(SdkHttpFullRequest.Builder request,
250+
private CanonicalRequest createCanonicalRequest(SdkHttpFullRequest request,
251+
SdkHttpFullRequest.Builder requestBuilder,
241252
String contentSha256,
242-
boolean doubleUrlEncode) {
243-
return new CanonicalRequest(request, contentSha256, doubleUrlEncode);
253+
boolean doubleUrlEncode,
254+
boolean normalizePath) {
255+
return new CanonicalRequest(request, requestBuilder, contentSha256, doubleUrlEncode, normalizePath);
244256
}
245257

246258
/**
@@ -251,6 +263,8 @@ private CanonicalRequest createCanonicalRequest(SdkHttpFullRequest.Builder reque
251263
private String createStringToSign(String canonicalRequest,
252264
Aws4SignerRequestParams requestParams) {
253265

266+
LOG.debug(() -> "AWS4 Canonical Request: " + canonicalRequest);
267+
254268
String requestHash = BinaryUtils.toHex(hash(canonicalRequest));
255269

256270
String stringToSign = requestParams.getSigningAlgorithm() +
@@ -330,7 +344,7 @@ private void addPreSignInformationToRequest(SdkHttpFullRequest.Builder mutableRe
330344
* @param ch the character to be tested
331345
* @return true if the character is white space, false otherwise.
332346
*/
333-
private boolean isWhiteSpace(final char ch) {
347+
private static boolean isWhiteSpace(char ch) {
334348
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\u000b' || ch == '\r' || ch == '\f';
335349
}
336350

@@ -402,8 +416,15 @@ protected <B extends Aws4SignerParams.Builder> B extractSignerParams(B paramsBui
402416
.signingRegion(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION))
403417
.timeOffset(executionAttributes.getAttribute(AwsSignerExecutionAttribute.TIME_OFFSET));
404418

405-
if (executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE) != null) {
406-
paramsBuilder.doubleUrlEncode(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE));
419+
Boolean doubleUrlEncode = executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE);
420+
if (doubleUrlEncode != null) {
421+
paramsBuilder.doubleUrlEncode(doubleUrlEncode);
422+
}
423+
424+
Boolean normalizePath =
425+
executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_NORMALIZE_PATH);
426+
if (normalizePath != null) {
427+
paramsBuilder.normalizePath(normalizePath);
407428
}
408429
ChecksumSpecs checksumSpecs = executionAttributes.getAttribute(RESOLVED_CHECKSUM_SPECS);
409430
if (checksumSpecs != null && checksumSpecs.algorithm() != null) {
@@ -453,30 +474,41 @@ private SdkChecksum createSdkChecksumFromParams(T signingParams, SdkHttpFullRequ
453474
return null;
454475
}
455476

456-
private class CanonicalRequest {
457-
private final SdkHttpFullRequest.Builder request;
477+
static final class CanonicalRequest {
478+
private final SdkHttpFullRequest request;
479+
private final SdkHttpFullRequest.Builder requestBuilder;
458480
private final String contentSha256;
459481
private final boolean doubleUrlEncode;
482+
private final boolean normalizePath;
460483

461484
private String canonicalRequestString;
462485
private StringBuilder signedHeaderStringBuilder;
463486
private List<Pair<String, List<String>>> canonicalHeaders;
464487
private String signedHeaderString;
465488

466-
private CanonicalRequest(SdkHttpFullRequest.Builder request, String contentSha256, boolean doubleUrlEncode) {
489+
CanonicalRequest(SdkHttpFullRequest request,
490+
SdkHttpFullRequest.Builder requestBuilder,
491+
String contentSha256,
492+
boolean doubleUrlEncode,
493+
boolean normalizePath) {
467494
this.request = request;
495+
this.requestBuilder = requestBuilder;
468496
this.contentSha256 = contentSha256;
469497
this.doubleUrlEncode = doubleUrlEncode;
498+
this.normalizePath = normalizePath;
470499
}
471500

472501
public String string() {
473502
if (canonicalRequestString == null) {
474503
StringBuilder canonicalRequest = new StringBuilder(512);
475-
canonicalRequest.append(request.method().toString())
504+
canonicalRequest.append(requestBuilder.method().toString())
476505
.append(SignerConstant.LINE_SEPARATOR);
477-
addCanonicalizedResourcePath(canonicalRequest, request.encodedPath(), doubleUrlEncode);
506+
addCanonicalizedResourcePath(canonicalRequest,
507+
request,
508+
doubleUrlEncode,
509+
normalizePath);
478510
canonicalRequest.append(SignerConstant.LINE_SEPARATOR);
479-
addCanonicalizedQueryString(canonicalRequest, request);
511+
addCanonicalizedQueryString(canonicalRequest, requestBuilder);
480512
canonicalRequest.append(SignerConstant.LINE_SEPARATOR);
481513
addCanonicalizedHeaderString(canonicalRequest, canonicalHeaders());
482514
canonicalRequest.append(SignerConstant.LINE_SEPARATOR)
@@ -488,6 +520,82 @@ public String string() {
488520
return canonicalRequestString;
489521
}
490522

523+
private void addCanonicalizedResourcePath(StringBuilder result,
524+
SdkHttpRequest request,
525+
boolean urlEncode,
526+
boolean normalizePath) {
527+
String path = normalizePath ? request.getUri().normalize().getRawPath()
528+
: request.encodedPath();
529+
530+
if (StringUtils.isEmpty(path)) {
531+
result.append("/");
532+
return;
533+
}
534+
535+
if (urlEncode) {
536+
path = SdkHttpUtils.urlEncodeIgnoreSlashes(path);
537+
}
538+
539+
if (!path.startsWith("/")) {
540+
result.append("/");
541+
}
542+
result.append(path);
543+
544+
// Normalization can leave a trailing slash at the end of the resource path,
545+
// even if the input path doesn't end with one. Example input: /foo/bar/.
546+
// Remove the trailing slash if the input path doesn't end with one.
547+
boolean trimTrailingSlash = normalizePath &&
548+
path.length() > 1 &&
549+
!request.encodedPath().endsWith("/") &&
550+
result.charAt(result.length() - 1) == '/';
551+
if (trimTrailingSlash) {
552+
result.setLength(result.length() - 1);
553+
}
554+
}
555+
556+
/**
557+
* Examines the specified query string parameters and returns a
558+
* canonicalized form.
559+
* <p>
560+
* The canonicalized query string is formed by first sorting all the query
561+
* string parameters, then URI encoding both the key and value and then
562+
* joining them, in order, separating key value pairs with an '&amp;'.
563+
*
564+
* @return A canonicalized form for the specified query string parameters.
565+
*/
566+
private void addCanonicalizedQueryString(StringBuilder result, SdkHttpRequest.Builder httpRequest) {
567+
568+
SortedMap<String, List<String>> sorted = new TreeMap<>();
569+
570+
/**
571+
* Signing protocol expects the param values also to be sorted after url
572+
* encoding in addition to sorted parameter names.
573+
*/
574+
httpRequest.forEachRawQueryParameter((key, values) -> {
575+
if (StringUtils.isEmpty(key)) {
576+
// Do not sign empty keys.
577+
return;
578+
}
579+
580+
String encodedParamName = SdkHttpUtils.urlEncode(key);
581+
582+
List<String> encodedValues = new ArrayList<>(values.size());
583+
for (String value : values) {
584+
String encodedValue = SdkHttpUtils.urlEncode(value);
585+
586+
// Null values should be treated as empty for the purposes of signing, not missing.
587+
// For example "?foo=" instead of "?foo".
588+
String signatureFormattedEncodedValue = encodedValue == null ? "" : encodedValue;
589+
590+
encodedValues.add(signatureFormattedEncodedValue);
591+
}
592+
Collections.sort(encodedValues);
593+
sorted.put(encodedParamName, encodedValues);
594+
});
595+
596+
SdkHttpUtils.flattenQueryParameters(result, sorted);
597+
}
598+
491599
public StringBuilder signedHeaderStringBuilder() {
492600
if (signedHeaderStringBuilder == null) {
493601
signedHeaderStringBuilder = new StringBuilder();
@@ -505,7 +613,7 @@ public String signedHeaderString() {
505613

506614
private List<Pair<String, List<String>>> canonicalHeaders() {
507615
if (canonicalHeaders == null) {
508-
canonicalHeaders = canonicalizeSigningHeaders(request);
616+
canonicalHeaders = canonicalizeSigningHeaders(requestBuilder);
509617
}
510618
return canonicalHeaders;
511619
}

core/auth/src/main/java/software/amazon/awssdk/auth/signer/internal/AbstractAwsSigner.java

Lines changed: 0 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,6 @@
2121
import java.security.DigestInputStream;
2222
import java.security.MessageDigest;
2323
import java.security.NoSuchAlgorithmException;
24-
import java.util.ArrayList;
25-
import java.util.Collections;
26-
import java.util.List;
27-
import java.util.SortedMap;
28-
import java.util.TreeMap;
2924
import javax.crypto.Mac;
3025
import javax.crypto.spec.SecretKeySpec;
3126
import software.amazon.awssdk.annotations.SdkInternalApi;
@@ -38,10 +33,8 @@
3833
import software.amazon.awssdk.core.signer.Signer;
3934
import software.amazon.awssdk.http.ContentStreamProvider;
4035
import software.amazon.awssdk.http.SdkHttpFullRequest;
41-
import software.amazon.awssdk.http.SdkHttpRequest;
4236
import software.amazon.awssdk.utils.BinaryUtils;
4337
import software.amazon.awssdk.utils.StringUtils;
44-
import software.amazon.awssdk.utils.http.SdkHttpUtils;
4538

4639
/**
4740
* Abstract base class for AWS signing protocol implementations. Provides
@@ -219,49 +212,6 @@ byte[] hash(byte[] data) throws SdkClientException {
219212
return hash(data, null);
220213
}
221214

222-
/**
223-
* Examines the specified query string parameters and returns a
224-
* canonicalized form.
225-
* <p>
226-
* The canonicalized query string is formed by first sorting all the query
227-
* string parameters, then URI encoding both the key and value and then
228-
* joining them, in order, separating key value pairs with an '&amp;'.
229-
*
230-
* @return A canonicalized form for the specified query string parameters.
231-
*/
232-
protected void addCanonicalizedQueryString(StringBuilder result, SdkHttpRequest.Builder httpRequest) {
233-
234-
SortedMap<String, List<String>> sorted = new TreeMap<>();
235-
236-
/**
237-
* Signing protocol expects the param values also to be sorted after url
238-
* encoding in addition to sorted parameter names.
239-
*/
240-
httpRequest.forEachRawQueryParameter((key, values) -> {
241-
if (StringUtils.isEmpty(key)) {
242-
// Do not sign empty keys.
243-
return;
244-
}
245-
246-
String encodedParamName = SdkHttpUtils.urlEncode(key);
247-
248-
List<String> encodedValues = new ArrayList<>(values.size());
249-
for (String value : values) {
250-
String encodedValue = SdkHttpUtils.urlEncode(value);
251-
252-
// Null values should be treated as empty for the purposes of signing, not missing.
253-
// For example "?foo=" instead of "?foo".
254-
String signatureFormattedEncodedValue = encodedValue == null ? "" : encodedValue;
255-
256-
encodedValues.add(signatureFormattedEncodedValue);
257-
}
258-
Collections.sort(encodedValues);
259-
sorted.put(encodedParamName, encodedValues);
260-
});
261-
262-
SdkHttpUtils.flattenQueryParameters(result, sorted);
263-
}
264-
265215
protected InputStream getBinaryRequestPayloadStream(ContentStreamProvider streamProvider) {
266216
try {
267217
if (streamProvider == null) {
@@ -278,31 +228,6 @@ protected InputStream getBinaryRequestPayloadStream(ContentStreamProvider stream
278228
}
279229
}
280230

281-
protected void addCanonicalizedResourcePath(StringBuilder result, String resourcePath, boolean urlEncode) {
282-
if (StringUtils.isEmpty(resourcePath)) {
283-
result.append("/");
284-
} else {
285-
String value = urlEncode ? SdkHttpUtils.urlEncodeIgnoreSlashes(resourcePath) : resourcePath;
286-
if (value.startsWith("/")) {
287-
result.append(value);
288-
} else {
289-
result.append("/").append(value);
290-
}
291-
}
292-
}
293-
294-
protected String getCanonicalizedEndpoint(SdkHttpFullRequest request) {
295-
String endpointForStringToSign = StringUtils.lowerCase(request.host());
296-
297-
// Omit the port from the endpoint if we're using the default port for the protocol. Some HTTP clients (ie. Apache) don't
298-
// allow you to specify it in the request, so we're standardizing around not including it. See SdkHttpRequest#port().
299-
if (!SdkHttpUtils.isUsingStandardPort(request.protocol(), request.port())) {
300-
endpointForStringToSign += ":" + request.port();
301-
}
302-
303-
return endpointForStringToSign;
304-
}
305-
306231
/**
307232
* Loads the individual access key ID and secret key from the specified credentials, trimming any extra whitespace from the
308233
* credentials.

0 commit comments

Comments
 (0)