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 }