Skip to content

Commit 632fea8

Browse files
author
Simon Krueger
committed
Update getCanonicalizedHeaderString to follow SigV4 spec
- Update to handle leading and trailing whitespace in header values - Update to handle multiple header values See Step 4 of https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html Without this change a request to S3 with multiple header values or a header value with leading and trailing spaces (e.g., ``` Header1: Cat\n Header1: Dog\n Header2: "a b c" \n ``` ) that is signed by the AwsS3V4Signer will receive a 403 Forbidden response with an Error Code of SignatureDoesNotMatch from S3.
1 parent 35eace7 commit 632fea8

File tree

3 files changed

+132
-18
lines changed

3 files changed

+132
-18
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"category": "AWS SDK for Java v2",
3+
"contributor": "skrueger",
4+
"type": "bugfix",
5+
"description": "Fix bug in SigV4 canonical request creation to handle multiple header values and leading & trailing whitespace in header values correctly."
6+
}

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

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ private void addPreSignInformationToRequest(SdkHttpFullRequest.Builder mutableRe
304304
mutableRequest.putRawQueryParameter(SignerConstant.X_AMZ_CREDENTIAL, signingCredentials);
305305
}
306306

307-
private Map<String, List<String>> canonicalizeSigningHeaders(Map<String, List<String>> headers) {
307+
static Map<String, List<String>> canonicalizeSigningHeaders(Map<String, List<String>> headers) {
308308
Map<String, List<String>> result = new TreeMap<>();
309309

310310
for (Map.Entry<String, List<String>> header : headers.entrySet()) {
@@ -319,48 +319,73 @@ private Map<String, List<String>> canonicalizeSigningHeaders(Map<String, List<St
319319
return result;
320320
}
321321

322-
private String getCanonicalizedHeaderString(Map<String, List<String>> canonicalizedHeaders) {
322+
/**
323+
* Step 4 of the AWS Signature version 4 calculation.
324+
* Refer to
325+
* https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
326+
*/
327+
static String getCanonicalizedHeaderString(Map<String, List<String>> canonicalizedHeaders) {
323328
StringBuilder buffer = new StringBuilder();
324329

325330
canonicalizedHeaders.forEach((headerName, headerValues) -> {
326-
for (String headerValue : headerValues) {
331+
if (headerValues.size() > 0) {
327332
appendCompactedString(buffer, headerName);
328-
buffer.append(":");
329-
if (headerValue != null) {
330-
appendCompactedString(buffer, headerValue);
333+
buffer.append(':');
334+
boolean headerValueAppended = false;
335+
// Append a comma-separated list of values for that header.
336+
// Do not sort the values in headers that have multiple values.
337+
for (int i = 0; i < headerValues.size(); i++) {
338+
String headerValue = headerValues.get(i);
339+
if (headerValue != null) {
340+
if (headerValueAppended) {
341+
buffer.append(',');
342+
}
343+
appendCompactedString(buffer, headerValue);
344+
headerValueAppended = true;
345+
}
331346
}
332-
buffer.append("\n");
347+
348+
buffer.append('\n');
333349
}
334350
});
335351

336352
return buffer.toString();
337353
}
338354

339355
/**
340-
* This method appends a string to a string builder and collapses contiguous
341-
* white space is a single space.
356+
* This method appends a modified version of the source string to the destination string builder.
357+
* The source string's leading and trailing whitespace is removed and
358+
* and all contiguous white space not at the beginning or end is collapsed into a single space.
359+
*
360+
* e.g.,
361+
* Given a source String of " a b c ",
362+
* "a b c" is appended to the destination StringBuilder.
342363
*
343364
* This is equivalent to:
344-
* destination.append(source.replaceAll("\\s+", " "))
365+
* <code>
366+
* destination.append(source.replaceAll("(^\\s*|\\s*$)", "").replaceAll("\\s+", " "))
367+
* </code>
345368
* but does not create a Pattern object that needs to compile the match
346369
* string; it also prevents us from having to make a Matcher object as well.
347-
*
348370
*/
349-
private void appendCompactedString(final StringBuilder destination, final String source) {
371+
static void appendCompactedString(final StringBuilder destination, final String source) {
372+
boolean appendedNonWhitespaceChar = false;
350373
boolean previousIsWhiteSpace = false;
351374
int length = source.length();
352375

353376
for (int i = 0; i < length; i++) {
354377
char ch = source.charAt(i);
355378
if (isWhiteSpace(ch)) {
356-
if (previousIsWhiteSpace) {
357-
continue;
379+
if (appendedNonWhitespaceChar) {
380+
previousIsWhiteSpace = true;
358381
}
359-
destination.append(' ');
360-
previousIsWhiteSpace = true;
361382
} else {
383+
if (previousIsWhiteSpace) {
384+
destination.append(' ');
385+
previousIsWhiteSpace = false;
386+
}
362387
destination.append(ch);
363-
previousIsWhiteSpace = false;
388+
appendedNonWhitespaceChar = true;
364389
}
365390
}
366391
}
@@ -373,7 +398,7 @@ private void appendCompactedString(final StringBuilder destination, final String
373398
* @param ch the character to be tested
374399
* @return true if the character is white space, false otherwise.
375400
*/
376-
private boolean isWhiteSpace(final char ch) {
401+
private static boolean isWhiteSpace(final char ch) {
377402
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\u000b' || ch == '\r' || ch == '\f';
378403
}
379404

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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.auth.signer.internal;
17+
18+
import org.junit.Test;
19+
20+
import java.util.*;
21+
22+
import static org.junit.Assert.assertEquals;
23+
24+
public class AbstractAws4SignerTest {
25+
26+
@Test
27+
public void testAppendCompactedString() {
28+
StringBuilder destination = new StringBuilder();
29+
AbstractAws4Signer.appendCompactedString(destination, " a b c ");
30+
String expected = "a b c";
31+
assertEquals(expected, destination.toString());
32+
}
33+
34+
@Test
35+
public void testGetCanonicalizedHeaderString_SpecExample() {
36+
Map<String, List<String>> headers = new LinkedHashMap<>();
37+
headers.put("Host", Arrays.asList("iam.amazonaws.com"));
38+
headers.put("Content-Type", Arrays.asList("application/x-www-form-urlencoded; charset=utf-8"));
39+
headers.put("My-header1", Arrays.asList(" a b c "));
40+
headers.put("X-Amz-Date", Arrays.asList("20150830T123600Z"));
41+
headers.put("My-Header2", Arrays.asList(" \"a b c\" "));
42+
43+
Map<String, List<String>> canonicalizeSigningHeaders =
44+
AbstractAws4Signer.canonicalizeSigningHeaders(headers);
45+
46+
String actual = AbstractAws4Signer.getCanonicalizedHeaderString(canonicalizeSigningHeaders);
47+
48+
String expected = String.join("\n",
49+
"content-type:application/x-www-form-urlencoded; charset=utf-8",
50+
"host:iam.amazonaws.com",
51+
"my-header1:a b c",
52+
"my-header2:\"a b c\"",
53+
"x-amz-date:20150830T123600Z",
54+
"");
55+
56+
assertEquals(expected, actual);
57+
}
58+
59+
@Test
60+
public void testGetCanonicalizedHeaderString_MultipleHeaderValuess() {
61+
Map<String, List<String>> headers = new LinkedHashMap<>();
62+
headers.put("Host", Arrays.asList("iam.amazonaws.com"));
63+
headers.put("Content-Type", Arrays.asList("application/x-www-form-urlencoded; charset=utf-8"));
64+
headers.put("My-header1", Arrays.asList(" a b c "));
65+
headers.put("X-Amz-Date", Arrays.asList("20150830T123600Z"));
66+
headers.put("My-Header1", Arrays.asList(" \"a b c\" "));
67+
68+
Map<String, List<String>> canonicalizeSigningHeaders =
69+
AbstractAws4Signer.canonicalizeSigningHeaders(headers);
70+
71+
String actual = AbstractAws4Signer.getCanonicalizedHeaderString(canonicalizeSigningHeaders);
72+
73+
String expected = String.join("\n",
74+
"content-type:application/x-www-form-urlencoded; charset=utf-8",
75+
"host:iam.amazonaws.com",
76+
"my-header1:a b c,\"a b c\"",
77+
"x-amz-date:20150830T123600Z",
78+
"");
79+
80+
assertEquals(expected, actual);
81+
}
82+
83+
}

0 commit comments

Comments
 (0)