Skip to content

Commit adbd2ea

Browse files
Correctly handle multi-value headers in Aws4Signer (#2629)
* Correctly handle multi-value headers in Aws4Signer ## Motivation and Context The current AbstractAws4Signer does not correctly handle multi-value headers. When creating the canonical header string to sign, it was placing each value on a separate line, when the documentation requires that they be comma-separated (even if the actual headers are serialized on the wire as multiple header fields). https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html ## Description * Update AbstractAws4Signer to correctly join multiple header values with a comma. * Replace "appendCompactedString" with "trimAll", where "trimAll" is the function name as specified in the documentation. This includes adding String#trim() functionality. ## Result Canonical header string example before: ``` foo:bar foo:baz ``` after: ``` foo:bar,baz ``` ## Testing * Add new unit tests for multi-value headers * Add new unit tests for the "Trimall" function, borrowing the example from the docs
1 parent b30fa93 commit adbd2ea

File tree

3 files changed

+74
-20
lines changed

3 files changed

+74
-20
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": "",
4+
"type": "bugfix",
5+
"description": "Correctly handle multi-value headers in Aws4Signer"
6+
}

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

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.List;
2626
import java.util.Map;
2727
import java.util.TreeMap;
28+
import java.util.stream.Collectors;
2829
import software.amazon.awssdk.annotations.SdkInternalApi;
2930
import software.amazon.awssdk.auth.credentials.AwsCredentials;
3031
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
@@ -323,46 +324,51 @@ private String getCanonicalizedHeaderString(Map<String, List<String>> canonicali
323324
StringBuilder buffer = new StringBuilder();
324325

325326
canonicalizedHeaders.forEach((headerName, headerValues) -> {
326-
for (String headerValue : headerValues) {
327-
appendCompactedString(buffer, headerName);
328-
buffer.append(":");
329-
if (headerValue != null) {
330-
appendCompactedString(buffer, headerValue);
331-
}
332-
buffer.append("\n");
333-
}
327+
buffer.append(headerName);
328+
buffer.append(":");
329+
buffer.append(String.join(",", trimAll(headerValues)));
330+
buffer.append("\n");
334331
});
335332

336333
return buffer.toString();
337334
}
338335

339336
/**
340-
* This method appends a string to a string builder and collapses contiguous
341-
* white space is a single space.
342-
*
343-
* This is equivalent to:
344-
* destination.append(source.replaceAll("\\s+", " "))
337+
* "The Trimall function removes excess white space before and after values,
338+
* and converts sequential spaces to a single space."
339+
* <p>
340+
* https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
341+
* <p>
342+
* The collapse-whitespace logic is equivalent to:
343+
* <pre>
344+
* value.replaceAll("\\s+", " ")
345+
* </pre>
345346
* but does not create a Pattern object that needs to compile the match
346347
* string; it also prevents us from having to make a Matcher object as well.
347-
*
348348
*/
349-
private void appendCompactedString(final StringBuilder destination, final String source) {
349+
private String trimAll(String value) {
350350
boolean previousIsWhiteSpace = false;
351-
int length = source.length();
351+
StringBuilder sb = new StringBuilder(value.length());
352352

353-
for (int i = 0; i < length; i++) {
354-
char ch = source.charAt(i);
353+
for (int i = 0; i < value.length(); i++) {
354+
char ch = value.charAt(i);
355355
if (isWhiteSpace(ch)) {
356356
if (previousIsWhiteSpace) {
357357
continue;
358358
}
359-
destination.append(' ');
359+
sb.append(' ');
360360
previousIsWhiteSpace = true;
361361
} else {
362-
destination.append(ch);
362+
sb.append(ch);
363363
previousIsWhiteSpace = false;
364364
}
365365
}
366+
367+
return sb.toString().trim();
368+
}
369+
370+
private List<String> trimAll(List<String> values) {
371+
return values.stream().map(this::trimAll).collect(Collectors.toList());
366372
}
367373

368374
/**

core/auth/src/test/java/software/amazon/awssdk/auth/signer/Aws4SignerTest.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,48 @@ public void xAmznTraceId_NotSigned() throws Exception {
156156
"Signature=581d0042389009a28d461124138f1fe8eeb8daed87611d2a2b47fd3d68d81d73");
157157
}
158158

159+
/**
160+
* Multi-value headers should be comma separated.
161+
*/
162+
@Test
163+
public void canonicalizedHeaderString_multiValueHeaders_areCommaSeparated() throws Exception {
164+
AwsBasicCredentials credentials = AwsBasicCredentials.create("akid", "skid");
165+
SdkHttpFullRequest.Builder request = generateBasicRequest();
166+
request.appendHeader("foo","bar");
167+
request.appendHeader("foo","baz");
168+
169+
SdkHttpFullRequest actual = SignerTestUtils.signRequest(signer, request.build(), credentials, "demo", signingOverrideClock, "us-east-1");
170+
171+
// We cannot easily test the canonical header string value, but the below signature asserts that it contains:
172+
// foo:bar,baz
173+
assertThat(actual.firstMatchingHeader("Authorization"))
174+
.hasValue("AWS4-HMAC-SHA256 Credential=akid/19810216/us-east-1/demo/aws4_request, "
175+
+ "SignedHeaders=foo;host;x-amz-archive-description;x-amz-date, "
176+
+ "Signature=1253bc1751048ea299e688cbe07a2224292e5cc606a079cb40459ad987793c19");
177+
}
178+
179+
/**
180+
* Canonical headers should remove excess white space before and after values, and convert sequential spaces to a single
181+
* space.
182+
*/
183+
@Test
184+
public void canonicalizedHeaderString_valuesWithExtraWhitespace_areTrimmed() throws Exception {
185+
AwsBasicCredentials credentials = AwsBasicCredentials.create("akid", "skid");
186+
SdkHttpFullRequest.Builder request = generateBasicRequest();
187+
request.putHeader("My-header1"," a b c ");
188+
request.putHeader("My-Header2"," \"a b c\" ");
189+
190+
SdkHttpFullRequest actual = SignerTestUtils.signRequest(signer, request.build(), credentials, "demo", signingOverrideClock, "us-east-1");
191+
192+
// We cannot easily test the canonical header string value, but the below signature asserts that it contains:
193+
// my-header1:a b c
194+
// my-header2:"a b c"
195+
assertThat(actual.firstMatchingHeader("Authorization"))
196+
.hasValue("AWS4-HMAC-SHA256 Credential=akid/19810216/us-east-1/demo/aws4_request, "
197+
+ "SignedHeaders=host;my-header1;my-header2;x-amz-archive-description;x-amz-date, "
198+
+ "Signature=6d3520e3397e7aba593d8ebd8361fc4405e90aed71bc4c7a09dcacb6f72460b9");
199+
}
200+
159201
private SdkHttpFullRequest.Builder generateBasicRequest() {
160202
return SdkHttpFullRequest.builder()
161203
.contentStreamProvider(() -> new ByteArrayInputStream("{\"TableName\": \"foo\"}".getBytes()))

0 commit comments

Comments
 (0)