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;
17 
18 import static org.assertj.core.api.Assertions.assertThat;
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertNull;
21 import static org.mockito.Mockito.when;
22 
23 import java.io.ByteArrayInputStream;
24 import java.net.URI;
25 import java.nio.charset.StandardCharsets;
26 import java.text.SimpleDateFormat;
27 import java.time.Clock;
28 import java.util.Calendar;
29 import java.util.Date;
30 import java.util.GregorianCalendar;
31 import java.util.SimpleTimeZone;
32 import java.util.TimeZone;
33 import org.junit.Before;
34 import org.junit.Test;
35 import org.junit.runner.RunWith;
36 import org.mockito.Mock;
37 import org.mockito.junit.MockitoJUnitRunner;
38 import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider;
39 import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
40 import software.amazon.awssdk.auth.credentials.AwsCredentials;
41 import software.amazon.awssdk.auth.signer.internal.Aws4SignerUtils;
42 import software.amazon.awssdk.auth.signer.internal.SignerConstant;
43 import software.amazon.awssdk.auth.signer.params.SignerChecksumParams;
44 import software.amazon.awssdk.auth.signer.internal.SignerTestUtils;
45 import software.amazon.awssdk.core.checksums.Algorithm;
46 import software.amazon.awssdk.http.SdkHttpFullRequest;
47 import software.amazon.awssdk.http.SdkHttpMethod;
48 
49 /**
50  * Unit tests for the {@link Aws4Signer}.
51  */
52 @RunWith(MockitoJUnitRunner.class)
53 public class Aws4SignerTest {
54 
55     private static final String AWS_4_HMAC_SHA_256_AUTHORIZATION = "AWS4-HMAC-SHA256 Credential=access/19810216/us-east-1/demo/aws4_request, ";
56     private static final String SIGNER_HEADER_WITH_CHECKSUMS_IN_HEADER = "SignedHeaders=host;x-amz-archive-description;x-amz-date;x-amzn-header-crc, ";
57     private static final String SIGNER_HEADER_WITH_CHECKSUMS_IN_TRAILER = "SignedHeaders=host;x-amz-archive-description;x-amz-date;x-amz-trailer, ";
58 
59     private Aws4Signer signer = Aws4Signer.create();
60 
61     @Mock
62     private Clock signingOverrideClock;
63 
64     SdkHttpFullRequest.Builder request;
65 
66     AwsBasicCredentials credentials;
67 
68     @Before
setupCase()69     public void setupCase() {
70         mockClock();
71         credentials = AwsBasicCredentials.create("access", "secret");
72         request = SdkHttpFullRequest.builder()
73                 .contentStreamProvider(() -> new ByteArrayInputStream("abc".getBytes()))
74                 .method(SdkHttpMethod.POST)
75                 .putHeader("Host", "demo.us-east-1.amazonaws.com")
76                 .putHeader("x-amz-archive-description", "test  test")
77                 .encodedPath("/")
78                 .uri(URI.create("http://demo.us-east-1.amazonaws.com"));
79     }
80 
81     @Test
testSigning()82     public void testSigning() throws Exception {
83         final String expectedAuthorizationHeaderWithoutSha256Header =
84             AWS_4_HMAC_SHA_256_AUTHORIZATION +
85             "SignedHeaders=host;x-amz-archive-description;x-amz-date, " +
86             "Signature=77fe7c02927966018667f21d1dc3dfad9057e58401cbb9ed64f1b7868288e35a";
87 
88         final String expectedAuthorizationHeaderWithSha256Header =
89             AWS_4_HMAC_SHA_256_AUTHORIZATION +
90             "SignedHeaders=host;x-amz-archive-description;x-amz-date;x-amz-sha256, " +
91             "Signature=e73e20539446307a5dc71252dbd5b97e861f1d1267456abda3ebd8d57e519951";
92 
93 
94         AwsBasicCredentials credentials = AwsBasicCredentials.create("access", "secret");
95         // Test request without 'x-amz-sha256' header
96         SdkHttpFullRequest.Builder request = generateBasicRequest();
97 
98         SdkHttpFullRequest signed = SignerTestUtils.signRequest(signer, request.build(), credentials,
99                                                                 "demo", signingOverrideClock, "us-east-1");
100         assertThat(signed.firstMatchingHeader("Authorization"))
101                 .hasValue(expectedAuthorizationHeaderWithoutSha256Header);
102 
103 
104         // Test request with 'x-amz-sha256' header
105         request = generateBasicRequest();
106         request.putHeader("x-amz-sha256", "required");
107 
108         signed = SignerTestUtils.signRequest(signer, request.build(), credentials, "demo", signingOverrideClock, "us-east-1");
109         assertThat(signed.firstMatchingHeader("Authorization")).hasValue(expectedAuthorizationHeaderWithSha256Header);
110     }
111 
112     @Test
queryParamsWithNullValuesAreStillSignedWithTrailingEquals()113     public void queryParamsWithNullValuesAreStillSignedWithTrailingEquals() throws Exception {
114         final String expectedAuthorizationHeaderWithoutSha256Header =
115             AWS_4_HMAC_SHA_256_AUTHORIZATION  +
116             "SignedHeaders=host;x-amz-archive-description;x-amz-date, " +
117             "Signature=c45a3ff1f028e83017f3812c06b4440f0b3240264258f6e18cd683b816990ba4";
118 
119         AwsBasicCredentials credentials = AwsBasicCredentials.create("access", "secret");
120         // Test request without 'x-amz-sha256' header
121         SdkHttpFullRequest.Builder request = generateBasicRequest().putRawQueryParameter("Foo", (String) null);
122 
123         SdkHttpFullRequest signed = SignerTestUtils.signRequest(signer, request.build(), credentials,
124                                                                 "demo", signingOverrideClock, "us-east-1");
125         assertThat(signed.firstMatchingHeader("Authorization")).hasValue(expectedAuthorizationHeaderWithoutSha256Header);
126     }
127 
128     @Test
testPresigning()129     public void testPresigning() throws Exception {
130         final String expectedAmzSignature = "bf7ae1c2f266d347e290a2aee7b126d38b8a695149d003b9fab2ed1eb6d6ebda";
131         final String expectedAmzCredentials = "access/19810216/us-east-1/demo/aws4_request";
132         final String expectedAmzHeader = "19810216T063000Z";
133         final String expectedAmzExpires = "604800";
134 
135         AwsBasicCredentials credentials = AwsBasicCredentials.create("access", "secret");
136         // Test request without 'x-amz-sha256' header
137 
138         SdkHttpFullRequest request = generateBasicRequest().build();
139 
140         SdkHttpFullRequest signed = SignerTestUtils.presignRequest(signer, request, credentials, null, "demo",
141                                                                    signingOverrideClock, "us-east-1");
142         assertEquals(expectedAmzSignature, signed.rawQueryParameters().get("X-Amz-Signature").get(0));
143         assertEquals(expectedAmzCredentials, signed.rawQueryParameters().get("X-Amz-Credential").get(0));
144         assertEquals(expectedAmzHeader, signed.rawQueryParameters().get("X-Amz-Date").get(0));
145         assertEquals(expectedAmzExpires, signed.rawQueryParameters().get("X-Amz-Expires").get(0));
146     }
147 
148     /**
149      * Tests that if passed anonymous credentials, signer will not generate a signature.
150      */
151     @Test
testAnonymous()152     public void testAnonymous() throws Exception {
153         AwsCredentials credentials = AnonymousCredentialsProvider.create().resolveCredentials();
154         SdkHttpFullRequest request = generateBasicRequest().build();
155 
156         SignerTestUtils.signRequest(signer, request, credentials, "demo", signingOverrideClock, "us-east-1");
157 
158         assertNull(request.headers().get("Authorization"));
159     }
160 
161     /**
162      * x-amzn-trace-id should not be signed as it may be mutated by proxies or load balancers.
163      */
164     @Test
xAmznTraceId_NotSigned()165     public void xAmznTraceId_NotSigned() throws Exception {
166         AwsBasicCredentials credentials = AwsBasicCredentials.create("akid", "skid");
167         SdkHttpFullRequest.Builder request = generateBasicRequest();
168         request.putHeader("X-Amzn-Trace-Id", " Root=1-584b150a-708479cb060007ffbf3ee1da;Parent=36d3dbcfd150aac9;Sampled=1");
169 
170         SdkHttpFullRequest actual = SignerTestUtils.signRequest(signer, request.build(), credentials, "demo", signingOverrideClock, "us-east-1");
171 
172         assertThat(actual.firstMatchingHeader("Authorization"))
173                 .hasValue("AWS4-HMAC-SHA256 Credential=akid/19810216/us-east-1/demo/aws4_request, " +
174                           "SignedHeaders=host;x-amz-archive-description;x-amz-date, " +
175                           "Signature=581d0042389009a28d461124138f1fe8eeb8daed87611d2a2b47fd3d68d81d73");
176     }
177 
178     /**
179      * Multi-value headers should be comma separated.
180      */
181     @Test
canonicalizedHeaderString_multiValueHeaders_areCommaSeparated()182     public void canonicalizedHeaderString_multiValueHeaders_areCommaSeparated() throws Exception {
183         AwsBasicCredentials credentials = AwsBasicCredentials.create("akid", "skid");
184         SdkHttpFullRequest.Builder request = generateBasicRequest();
185         request.appendHeader("foo","bar");
186         request.appendHeader("foo","baz");
187 
188         SdkHttpFullRequest actual = SignerTestUtils.signRequest(signer, request.build(), credentials, "demo", signingOverrideClock, "us-east-1");
189 
190         // We cannot easily test the canonical header string value, but the below signature asserts that it contains:
191         // foo:bar,baz
192         assertThat(actual.firstMatchingHeader("Authorization"))
193             .hasValue("AWS4-HMAC-SHA256 Credential=akid/19810216/us-east-1/demo/aws4_request, "
194                       + "SignedHeaders=foo;host;x-amz-archive-description;x-amz-date, "
195                       + "Signature=1253bc1751048ea299e688cbe07a2224292e5cc606a079cb40459ad987793c19");
196     }
197 
198     /**
199      * Canonical headers should remove excess white space before and after values, and convert sequential spaces to a single
200      * space.
201      */
202     @Test
canonicalizedHeaderString_valuesWithExtraWhitespace_areTrimmed()203     public void canonicalizedHeaderString_valuesWithExtraWhitespace_areTrimmed() throws Exception {
204         AwsBasicCredentials credentials = AwsBasicCredentials.create("akid", "skid");
205         SdkHttpFullRequest.Builder request = generateBasicRequest();
206         request.putHeader("My-header1","    a   b   c  ");
207         request.putHeader("My-Header2","    \"a   b   c\"  ");
208 
209         SdkHttpFullRequest actual = SignerTestUtils.signRequest(signer, request.build(), credentials, "demo", signingOverrideClock, "us-east-1");
210 
211         // We cannot easily test the canonical header string value, but the below signature asserts that it contains:
212         // my-header1:a b c
213         // my-header2:"a b c"
214         assertThat(actual.firstMatchingHeader("Authorization"))
215             .hasValue("AWS4-HMAC-SHA256 Credential=akid/19810216/us-east-1/demo/aws4_request, "
216                       + "SignedHeaders=host;my-header1;my-header2;x-amz-archive-description;x-amz-date, "
217                       + "Signature=6d3520e3397e7aba593d8ebd8361fc4405e90aed71bc4c7a09dcacb6f72460b9");
218     }
219 
220     /**
221      * Query strings with empty keys should not be included in the canonical string.
222      */
223     @Test
canonicalizedQueryString_keyWithEmptyNames_doNotGetSigned()224     public void canonicalizedQueryString_keyWithEmptyNames_doNotGetSigned() throws Exception {
225         AwsBasicCredentials credentials = AwsBasicCredentials.create("akid", "skid");
226         SdkHttpFullRequest.Builder request = generateBasicRequest();
227         request.putRawQueryParameter("", (String) null);
228 
229         SdkHttpFullRequest actual = SignerTestUtils.signRequest(signer, request.build(), credentials, "demo", signingOverrideClock, "us-east-1");
230 
231         assertThat(actual.firstMatchingHeader("Authorization"))
232             .hasValue("AWS4-HMAC-SHA256 Credential=akid/19810216/us-east-1/demo/aws4_request, "
233                       + "SignedHeaders=host;x-amz-archive-description;x-amz-date, "
234                       + "Signature=581d0042389009a28d461124138f1fe8eeb8daed87611d2a2b47fd3d68d81d73");
235     }
236 
generateBasicRequest()237     private SdkHttpFullRequest.Builder generateBasicRequest() {
238         return SdkHttpFullRequest.builder()
239                                  .contentStreamProvider(() -> new ByteArrayInputStream("{\"TableName\": \"foo\"}".getBytes()))
240                                  .method(SdkHttpMethod.POST)
241                                  .putHeader("Host", "demo.us-east-1.amazonaws.com")
242                                  .putHeader("x-amz-archive-description", "test  test")
243                                  .encodedPath("/")
244                                  .uri(URI.create("http://demo.us-east-1.amazonaws.com"));
245     }
246 
mockClock()247     private void mockClock() {
248         Calendar c = new GregorianCalendar();
249         c.set(1981, 1, 16, 6, 30, 0);
250         c.setTimeZone(TimeZone.getTimeZone("UTC"));
251 
252         when(signingOverrideClock.millis()).thenReturn(c.getTimeInMillis());
253     }
254 
getOldTimeStamp(Date date)255     private String getOldTimeStamp(Date date) {
256         final SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
257         dateTimeFormat.setTimeZone(new SimpleTimeZone(0, "UTC"));
258         return dateTimeFormat.format(date);
259     }
260 
261     @Test
getTimeStamp()262     public void getTimeStamp() {
263         Date now = new Date();
264         String timeStamp = Aws4SignerUtils.formatTimestamp(now.getTime());
265         String old = getOldTimeStamp(now);
266         assertEquals(old, timeStamp);
267     }
268 
getOldDateStamp(Date date)269     private String getOldDateStamp(Date date) {
270         final SimpleDateFormat dateStampFormat = new SimpleDateFormat("yyyyMMdd");
271         dateStampFormat.setTimeZone(new SimpleTimeZone(0, "UTC"));
272         return dateStampFormat.format(date);
273     }
274 
275     @Test
getDateStamp()276     public void getDateStamp() {
277         Date now = new Date();
278         String dateStamp = Aws4SignerUtils.formatDateStamp(now.getTime());
279         String old = getOldDateStamp(now);
280         assertEquals(old, dateStamp);
281     }
282     @Test
signing_with_Crc32Checksum_WithOut_x_amz_sha25_header()283     public void signing_with_Crc32Checksum_WithOut_x_amz_sha25_header() throws Exception {
284         //Note here x_amz_sha25_header is not present in SignedHeaders
285         String expectedAuthorization = AWS_4_HMAC_SHA_256_AUTHORIZATION + SIGNER_HEADER_WITH_CHECKSUMS_IN_HEADER
286                                        + "Signature=c1804802dc623d1689e7d0a7f9f5caee3588cc8d3df4495425129dbd52965d1f";
287 
288         final SignerChecksumParams signerChecksumParams = SignerChecksumParams.builder()
289                                                                               .algorithm(Algorithm.CRC32)
290                                                                               .checksumHeaderName("x-amzn-header-crc")
291                                                                               .isStreamingRequest(false)
292                 .build();
293         SdkHttpFullRequest signed = SignerTestUtils.signRequest(signer, request.contentStreamProvider(
294                 () -> new ByteArrayInputStream("{\"TableName\": \"foo\"}".getBytes(StandardCharsets.UTF_8))
295                 ).build(), credentials,
296                 "demo", signingOverrideClock, "us-east-1", signerChecksumParams);
297         assertThat(signed.firstMatchingHeader("x-amzn-header-crc").get()).contains("oL+a/g==");
298         assertThat(signed.firstMatchingHeader(SignerConstant.X_AMZ_CONTENT_SHA256)).isNotPresent();
299         assertThat(signed.firstMatchingHeader("Authorization")).hasValue(expectedAuthorization);
300     }
301 
302     @Test
signing_with_Crc32Checksum_with_streaming_input_request()303     public void signing_with_Crc32Checksum_with_streaming_input_request() throws Exception {
304         //Note here x_amz_sha25_header is not present in SignedHeaders
305         String expectedAuthorization = AWS_4_HMAC_SHA_256_AUTHORIZATION + SIGNER_HEADER_WITH_CHECKSUMS_IN_HEADER
306                                        + "Signature=c1804802dc623d1689e7d0a7f9f5caee3588cc8d3df4495425129dbd52965d1f";
307         final SignerChecksumParams signerChecksumParams = SignerChecksumParams.builder()
308                                                                               .algorithm(Algorithm.CRC32)
309                                                                               .checksumHeaderName("x-amzn-header-crc")
310                                                                               .isStreamingRequest(true)
311                                                                               .build();
312         SdkHttpFullRequest signed = SignerTestUtils.signRequest(signer, request.contentStreamProvider(
313                                                                     () -> new ByteArrayInputStream("{\"TableName\": \"foo\"}".getBytes(StandardCharsets.UTF_8))
314                                                                 ).build(), credentials,
315                                                                 "demo", signingOverrideClock, "us-east-1", signerChecksumParams);
316         assertThat(signed.firstMatchingHeader("x-amzn-header-crc").get()).contains("oL+a/g==");
317         assertThat(signed.firstMatchingHeader(SignerConstant.X_AMZ_CONTENT_SHA256)).isNotPresent();
318         assertThat(signed.firstMatchingHeader("Authorization")).hasValue(expectedAuthorization);
319     }
320 
321 
322     @Test
signing_with_Crc32Checksum_with_x_amz_sha25_header_preset()323     public void signing_with_Crc32Checksum_with_x_amz_sha25_header_preset() throws Exception {
324         //Note here x_amz_sha25_header is  present in SignedHeaders, we make sure checksum is calculated even in this case.
325         String expectedAuthorization = AWS_4_HMAC_SHA_256_AUTHORIZATION
326             + "SignedHeaders=host;x-amz-archive-description;x-amz-content-sha256;x-amz-date;x-amzn-header-crc, "
327             + "Signature=bc931232666f226854cdd9c9962dc03d791cf4024f5ca032fab996c1d15e4a5d";
328         final SignerChecksumParams signerChecksumParams = SignerChecksumParams.builder()
329                                                                               .algorithm(Algorithm.CRC32)
330                                                                               .checksumHeaderName("x-amzn-header-crc")
331                                                                               .isStreamingRequest(true).build();
332         request = generateBasicRequest();
333         // presetting of the header
334         request.putHeader(SignerConstant.X_AMZ_CONTENT_SHA256, "required");
335         SdkHttpFullRequest signed = SignerTestUtils.signRequest(signer, request.build(), credentials,
336                 "demo", signingOverrideClock, "us-east-1", signerChecksumParams);
337         assertThat(signed.firstMatchingHeader("Authorization")).hasValue(expectedAuthorization);
338         assertThat(signed.firstMatchingHeader("x-amzn-header-crc").get()).contains("oL+a/g==");
339         assertThat(signed.firstMatchingHeader(SignerConstant.X_AMZ_CONTENT_SHA256)).isPresent();
340     }
341 
342     @Test
signing_with_NoHttpChecksum_As_No_impact_on_Signature()343     public void signing_with_NoHttpChecksum_As_No_impact_on_Signature() throws Exception {
344         //Note here x_amz_sha25_header is not present in SignedHeaders
345         String expectedAuthorization =
346             AWS_4_HMAC_SHA_256_AUTHORIZATION +
347             "SignedHeaders=host;x-amz-archive-description;x-amz-date, " +
348             "Signature=77fe7c02927966018667f21d1dc3dfad9057e58401cbb9ed64f1b7868288e35a";
349         SdkHttpFullRequest signed = SignerTestUtils.signRequest(signer, request.contentStreamProvider(
350                 () -> new ByteArrayInputStream("{\"TableName\": \"foo\"}".getBytes(StandardCharsets.UTF_8))
351                 ).build(), credentials,
352                 "demo", signingOverrideClock, "us-east-1", null);
353         assertThat(signed.firstMatchingHeader("Authorization")).hasValue(expectedAuthorization);
354         assertThat(signed.firstMatchingHeader("x-amzn-header-crc")).isNotPresent();
355     }
356 
357     @Test
signing_with_Crc32Checksum_with_header_already_present()358     public void signing_with_Crc32Checksum_with_header_already_present() throws Exception {
359 
360         String expectedAuthorization = AWS_4_HMAC_SHA_256_AUTHORIZATION + SIGNER_HEADER_WITH_CHECKSUMS_IN_HEADER
361                                        + "Signature=f6fad563460f2ac50fe2ab5f5f5d77a787e357897ac6e9bb116ff12d30f45589";
362 
363         final SignerChecksumParams signerChecksumParams = SignerChecksumParams.builder()
364                                                                               .algorithm(Algorithm.CRC32)
365                                                                               .checksumHeaderName("x-amzn-header-crc")
366                                                                               .isStreamingRequest(false)
367                                                                               .build();
368         SdkHttpFullRequest signed = SignerTestUtils.signRequest(signer, request.contentStreamProvider(
369                                                                     () -> new ByteArrayInputStream("{\"TableName\": \"foo\"}".getBytes(StandardCharsets.UTF_8))
370                                                                 )
371                                                        .appendHeader("x-amzn-header-crc", "preCalculatedChecksum")
372                                                                                .build(), credentials,
373                                                                 "demo", signingOverrideClock, "us-east-1", signerChecksumParams);
374         assertThat(signed.firstMatchingHeader("x-amzn-header-crc")).hasValue("preCalculatedChecksum");
375         assertThat(signed.firstMatchingHeader(SignerConstant.X_AMZ_CONTENT_SHA256)).isNotPresent();
376         assertThat(signed.firstMatchingHeader("Authorization")).hasValue(expectedAuthorization);
377     }
378 
379     @Test
signing_with_Crc32Checksum_with__trailer_header_already_present()380     public void signing_with_Crc32Checksum_with__trailer_header_already_present() throws Exception {
381         String expectedAuthorization = AWS_4_HMAC_SHA_256_AUTHORIZATION + SIGNER_HEADER_WITH_CHECKSUMS_IN_TRAILER
382                                        + "Signature=3436c4bc175d31e87a591802e64756cebf2d1c6c2054d26ca3dc91bdd3de303e";
383 
384         final SignerChecksumParams signerChecksumParams = SignerChecksumParams.builder()
385                                                                               .algorithm(Algorithm.CRC32)
386                                                                               .checksumHeaderName("x-amzn-header-crc")
387                                                                               .isStreamingRequest(false)
388                                                                               .build();
389         SdkHttpFullRequest signed = SignerTestUtils.signRequest(
390             signer, request.contentStreamProvider(() -> new ByteArrayInputStream(("{\"TableName"
391                                                                                   + "\": "
392                                                                                   + "\"foo\"}").getBytes(StandardCharsets.UTF_8)))
393                            .appendHeader("x-amz-trailer", "x-amzn-header-crc")
394                            .build(), credentials,
395             "demo", signingOverrideClock, "us-east-1", signerChecksumParams);
396         assertThat(signed.firstMatchingHeader("x-amzn-header-crc")).isNotPresent();
397         assertThat(signed.firstMatchingHeader("x-amz-trailer")).contains("x-amzn-header-crc");
398         assertThat(signed.firstMatchingHeader(SignerConstant.X_AMZ_CONTENT_SHA256)).isNotPresent();
399         assertThat(signed.firstMatchingHeader("Authorization")).hasValue(expectedAuthorization);
400     }
401 }