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 static software.amazon.awssdk.utils.StringUtils.lowerCase;
19
20 import java.io.InputStream;
21 import java.nio.charset.Charset;
22 import java.time.Instant;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.TreeMap;
28 import software.amazon.awssdk.annotations.SdkInternalApi;
29 import software.amazon.awssdk.auth.credentials.AwsCredentials;
30 import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
31 import software.amazon.awssdk.auth.signer.Aws4Signer;
32 import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute;
33 import software.amazon.awssdk.auth.signer.params.Aws4PresignerParams;
34 import software.amazon.awssdk.auth.signer.params.Aws4SignerParams;
35 import software.amazon.awssdk.core.exception.SdkClientException;
36 import software.amazon.awssdk.core.interceptor.ExecutionAttributes;
37 import software.amazon.awssdk.core.signer.Presigner;
38 import software.amazon.awssdk.http.SdkHttpFullRequest;
39 import software.amazon.awssdk.utils.BinaryUtils;
40 import software.amazon.awssdk.utils.Logger;
41 import software.amazon.awssdk.utils.http.SdkHttpUtils;
42
43 /**
44  * Abstract base class for the AWS SigV4 signer implementations.
45  * @param <T> Type of the signing params class that is used for signing the request
46  * @param <U> Type of the signing params class that is used for pre signing the request
47  */

48 @SdkInternalApi
49 public abstract class AbstractAws4Signer<T extends Aws4SignerParams, U extends Aws4PresignerParams>
50     extends AbstractAwsSigner implements Presigner {
51
52     public static final String EMPTY_STRING_SHA256_HEX = BinaryUtils.toHex(hash(""));
53
54     private static final Logger LOG = Logger.loggerFor(Aws4Signer.class);
55     private static final int SIGNER_CACHE_MAX_SIZE = 300;
56     private static final FifoCache<SignerKey> SIGNER_CACHE =
57         new FifoCache<>(SIGNER_CACHE_MAX_SIZE);
58     private static final List<String> LIST_OF_HEADERS_TO_IGNORE_IN_LOWER_CASE =
59         Arrays.asList("connection""x-amzn-trace-id""user-agent""expect");
60
61     protected SdkHttpFullRequest.Builder doSign(SdkHttpFullRequest request,
62                                                 Aws4SignerRequestParams requestParams,
63                                                 T signingParams) {
64
65         SdkHttpFullRequest.Builder mutableRequest = request.toBuilder();
66         AwsCredentials sanitizedCredentials = sanitizeCredentials(signingParams.awsCredentials());
67         if (sanitizedCredentials instanceof AwsSessionCredentials) {
68             addSessionCredentials(mutableRequest, (AwsSessionCredentials) sanitizedCredentials);
69         }
70
71         addHostHeader(mutableRequest);
72         addDateHeader(mutableRequest, requestParams.getFormattedRequestSigningDateTime());
73
74         String contentSha256 = calculateContentHash(mutableRequest, signingParams);
75         mutableRequest.firstMatchingHeader(SignerConstant.X_AMZ_CONTENT_SHA256)
76                       .filter(h -> h.equals("required"))
77                       .ifPresent(h -> mutableRequest.putHeader(SignerConstant.X_AMZ_CONTENT_SHA256, contentSha256));
78
79         Map<String, List<String>> canonicalHeaders = canonicalizeSigningHeaders(mutableRequest.headers());
80         String signedHeadersString = getSignedHeadersString(canonicalHeaders);
81
82         String canonicalRequest = createCanonicalRequest(mutableRequest,
83                                                          canonicalHeaders,
84                                                          signedHeadersString,
85                                                          contentSha256,
86                                                          signingParams.doubleUrlEncode());
87
88         String stringToSign = createStringToSign(canonicalRequest, requestParams);
89
90         byte[] signingKey = deriveSigningKey(sanitizedCredentials, requestParams);
91
92         byte[] signature = computeSignature(stringToSign, signingKey);
93
94         mutableRequest.putHeader(SignerConstant.AUTHORIZATION,
95                                  buildAuthorizationHeader(signature, sanitizedCredentials, requestParams, signedHeadersString));
96
97         processRequestPayload(mutableRequest, signature, signingKey, requestParams, signingParams);
98
99         return mutableRequest;
100     }
101
102     protected SdkHttpFullRequest.Builder doPresign(SdkHttpFullRequest request,
103                                                    Aws4SignerRequestParams requestParams,
104                                                    U signingParams) {
105
106         SdkHttpFullRequest.Builder mutableRequest = request.toBuilder();
107
108         long expirationInSeconds = getSignatureDurationInSeconds(requestParams, signingParams);
109         addHostHeader(mutableRequest);
110
111         AwsCredentials sanitizedCredentials = sanitizeCredentials(signingParams.awsCredentials());
112         if (sanitizedCredentials instanceof AwsSessionCredentials) {
113             // For SigV4 pre-signing URL, we need to add "X-Amz-Security-Token"
114             // as a query string parameter, before constructing the canonical
115             // request.
116             mutableRequest.putRawQueryParameter(SignerConstant.X_AMZ_SECURITY_TOKEN,
117                                                 ((AwsSessionCredentials) sanitizedCredentials).sessionToken());
118         }
119
120         // Add the important parameters for v4 signing
121         Map<String, List<String>> canonicalizedHeaders = canonicalizeSigningHeaders(mutableRequest.headers());
122         String signedHeadersString = getSignedHeadersString(canonicalizedHeaders);
123
124         addPreSignInformationToRequest(mutableRequest, signedHeadersString, sanitizedCredentials,
125                                        requestParams, expirationInSeconds);
126
127         String contentSha256 = calculateContentHashPresign(mutableRequest, signingParams);
128
129         String canonicalRequest = createCanonicalRequest(mutableRequest, canonicalizedHeaders, signedHeadersString,
130                                                          contentSha256, signingParams.doubleUrlEncode());
131
132         String stringToSign = createStringToSign(canonicalRequest, requestParams);
133
134         byte[] signingKey = deriveSigningKey(sanitizedCredentials, requestParams);
135
136         byte[] signature = computeSignature(stringToSign, signingKey);
137
138         mutableRequest.putRawQueryParameter(SignerConstant.X_AMZ_SIGNATURE, BinaryUtils.toHex(signature));
139
140         return mutableRequest;
141     }
142
143     @Override
144     protected void addSessionCredentials(SdkHttpFullRequest.Builder mutableRequest,
145                                          AwsSessionCredentials credentials) {
146         mutableRequest.putHeader(SignerConstant.X_AMZ_SECURITY_TOKEN, credentials.sessionToken());
147     }
148
149     /**
150      * Calculate the hash of the request's payload. Subclass could override this
151      * method to provide different values for "x-amz-content-sha256" header or
152      * do any other necessary set-ups on the request headers. (e.g. aws-chunked
153      * uses a pre-defined header value, and needs to change some headers
154      * relating to content-encoding and content-length.)
155      */

156     protected String calculateContentHash(SdkHttpFullRequest.Builder mutableRequest, T signerParams) {
157         InputStream payloadStream = getBinaryRequestPayloadStream(mutableRequest.contentStreamProvider());
158         return BinaryUtils.toHex(hash(payloadStream));
159     }
160
161     protected abstract void processRequestPayload(SdkHttpFullRequest.Builder mutableRequest,
162                                                   byte[] signature,
163                                                   byte[] signingKey,
164                                                   Aws4SignerRequestParams signerRequestParams,
165                                                   T signerParams);
166
167     protected abstract String calculateContentHashPresign(SdkHttpFullRequest.Builder mutableRequest, U signerParams);
168
169     /**
170      * Step 3 of the AWS Signature version 4 calculation. It involves deriving
171      * the signing key and computing the signature. Refer to
172      * http://docs.aws.amazon
173      * .com/general/latest/gr/sigv4-calculate-signature.html
174      */

175     protected final byte[] deriveSigningKey(AwsCredentials credentials, Aws4SignerRequestParams signerRequestParams) {
176         return deriveSigningKey(credentials,
177                 Instant.ofEpochMilli(signerRequestParams.getRequestSigningDateTimeMilli()),
178                 signerRequestParams.getRegionName(),
179                 signerRequestParams.getServiceSigningName());
180     }
181
182     protected final byte[] deriveSigningKey(AwsCredentials credentials, Instant signingInstant, String region, String service) {
183         String cacheKey = createSigningCacheKeyName(credentials, region, service);
184         SignerKey signerKey = SIGNER_CACHE.get(cacheKey);
185
186         if (signerKey != null && signerKey.isValidForDate(signingInstant)) {
187             return signerKey.getSigningKey();
188         }
189
190         LOG.trace(() -> "Generating a new signing key as the signing key not available in the cache for the date: " +
191                 signingInstant.toEpochMilli());
192         byte[] signingKey = newSigningKey(credentials,
193                 Aws4SignerUtils.formatDateStamp(signingInstant),
194                 region,
195                 service);
196         SIGNER_CACHE.add(cacheKey, new SignerKey(signingInstant, signingKey));
197         return signingKey;
198     }
199
200     /**
201      * Step 1 of the AWS Signature version 4 calculation. Refer to
202      * http://docs.aws
203      * .amazon.com/general/latest/gr/sigv4-create-canonical-request.html to
204      * generate the canonical request.
205      */

206     private String createCanonicalRequest(SdkHttpFullRequest.Builder request,
207                                           Map<String, List<String>> canonicalHeaders,
208                                           String signedHeadersString,
209                                           String contentSha256,
210                                           boolean doubleUrlEncode) {
211         String canonicalRequest = request.method().toString() +
212                                   SignerConstant.LINE_SEPARATOR +
213                                   // This would optionally double url-encode the resource path
214                                   getCanonicalizedResourcePath(request.encodedPath(), doubleUrlEncode) +
215                                   SignerConstant.LINE_SEPARATOR +
216                                   getCanonicalizedQueryString(request.rawQueryParameters()) +
217                                   SignerConstant.LINE_SEPARATOR +
218                                   getCanonicalizedHeaderString(canonicalHeaders) +
219                                   SignerConstant.LINE_SEPARATOR +
220                                   signedHeadersString +
221                                   SignerConstant.LINE_SEPARATOR +
222                                   contentSha256;
223
224         LOG.trace(() -> "AWS4 Canonical Request: " + canonicalRequest);
225         return canonicalRequest;
226     }
227
228     /**
229      * Step 2 of the AWS Signature version 4 calculation. Refer to
230      * http://docs.aws
231      * .amazon.com/general/latest/gr/sigv4-create-string-to-sign.html.
232      */

233     private String createStringToSign(String canonicalRequest,
234                                       Aws4SignerRequestParams requestParams) {
235
236         String stringToSign = requestParams.getSigningAlgorithm() +
237                                     SignerConstant.LINE_SEPARATOR +
238                                     requestParams.getFormattedRequestSigningDateTime() +
239                                     SignerConstant.LINE_SEPARATOR +
240                                     requestParams.getScope() +
241                                     SignerConstant.LINE_SEPARATOR +
242                                     BinaryUtils.toHex(hash(canonicalRequest));
243
244         LOG.debug(() -> "AWS4 String to sign: " + stringToSign);
245         return stringToSign;
246     }
247
248     private String createSigningCacheKeyName(AwsCredentials credentials,
249                                              String regionName,
250                                              String serviceName) {
251         return credentials.secretAccessKey() + "-" + regionName + "-" + serviceName;
252     }
253
254     /**
255      * Step 3 of the AWS Signature version 4 calculation. It involves deriving
256      * the signing key and computing the signature. Refer to
257      * http://docs.aws.amazon
258      * .com/general/latest/gr/sigv4-calculate-signature.html
259      */

260     private byte[] computeSignature(String stringToSign, byte[] signingKey) {
261         return sign(stringToSign.getBytes(Charset.forName("UTF-8")), signingKey,
262                     SigningAlgorithm.HmacSHA256);
263     }
264
265     /**
266      * Creates the authorization header to be included in the request.
267      */

268     private String buildAuthorizationHeader(byte[] signature,
269                                             AwsCredentials credentials,
270                                             Aws4SignerRequestParams signerParams,
271                                             String signedHeadersString) {
272
273         String signingCredentials = credentials.accessKeyId() + "/" + signerParams.getScope();
274         String credential = "Credential=" + signingCredentials;
275         String signerHeaders = "SignedHeaders=" + signedHeadersString;
276         String signatureHeader = "Signature=" + BinaryUtils.toHex(signature);
277
278         return SignerConstant.AWS4_SIGNING_ALGORITHM + " " + credential + ", " + signerHeaders + ", " + signatureHeader;
279     }
280
281     /**
282      * Includes all the signing headers as request parameters for pre-signing.
283      */

284     private void addPreSignInformationToRequest(SdkHttpFullRequest.Builder mutableRequest,
285                                                 String signedHeadersString,
286                                                 AwsCredentials sanitizedCredentials,
287                                                 Aws4SignerRequestParams signerParams,
288                                                 long expirationInSeconds) {
289
290         String signingCredentials = sanitizedCredentials.accessKeyId() + "/" + signerParams.getScope();
291
292         mutableRequest.putRawQueryParameter(SignerConstant.X_AMZ_ALGORITHM, SignerConstant.AWS4_SIGNING_ALGORITHM);
293         mutableRequest.putRawQueryParameter(SignerConstant.X_AMZ_DATE, signerParams.getFormattedRequestSigningDateTime());
294         mutableRequest.putRawQueryParameter(SignerConstant.X_AMZ_SIGNED_HEADER, signedHeadersString);
295         mutableRequest.putRawQueryParameter(SignerConstant.X_AMZ_EXPIRES, Long.toString(expirationInSeconds));
296         mutableRequest.putRawQueryParameter(SignerConstant.X_AMZ_CREDENTIAL, signingCredentials);
297     }
298
299     private Map<String, List<String>> canonicalizeSigningHeaders(Map<String, List<String>> headers) {
300         Map<String, List<String>> result = new TreeMap<>();
301
302         for (Map.Entry<String, List<String>> header : headers.entrySet()) {
303             String lowerCaseHeader = lowerCase(header.getKey());
304             if (LIST_OF_HEADERS_TO_IGNORE_IN_LOWER_CASE.contains(lowerCaseHeader)) {
305                 continue;
306             }
307
308             result.computeIfAbsent(lowerCaseHeader, x -> new ArrayList<>()).addAll(header.getValue());
309         }
310
311         return result;
312     }
313
314     private String getCanonicalizedHeaderString(Map<String, List<String>> canonicalizedHeaders) {
315         StringBuilder buffer = new StringBuilder();
316
317         canonicalizedHeaders.forEach((headerName, headerValues) -> {
318             for (String headerValue : headerValues) {
319                 appendCompactedString(buffer, headerName);
320                 buffer.append(":");
321                 if (headerValue != null) {
322                     appendCompactedString(buffer, headerValue);
323                 }
324                 buffer.append("\n");
325             }
326         });
327
328         return buffer.toString();
329     }
330
331     /**
332      * This method appends a string to a string builder and collapses contiguous
333      * white space is a single space.
334      *
335      * This is equivalent to:
336      *      destination.append(source.replaceAll("\\s+"" "))
337      * but does not create a Pattern object that needs to compile the match
338      * string; it also prevents us from having to make a Matcher object as well.
339      *
340      */

341     private void appendCompactedString(final StringBuilder destination, final String source) {
342         boolean previousIsWhiteSpace = false;
343         int length = source.length();
344
345         for (int i = 0; i < length; i++) {
346             char ch = source.charAt(i);
347             if (isWhiteSpace(ch)) {
348                 if (previousIsWhiteSpace) {
349                     continue;
350                 }
351                 destination.append(' ');
352                 previousIsWhiteSpace = true;
353             } else {
354                 destination.append(ch);
355                 previousIsWhiteSpace = false;
356             }
357         }
358     }
359
360     /**
361      * Tests a char to see if is it whitespace.
362      * This method considers the same characters to be white
363      * space as the Pattern class does when matching \s
364      *
365      * @param ch the character to be tested
366      * @return true if the character is white  space, false otherwise.
367      */

368     private boolean isWhiteSpace(final char ch) {
369         return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\u000b' || ch == '\r' || ch == '\f';
370     }
371
372     private String getSignedHeadersString(Map<String, List<String>> canonicalizedHeaders) {
373         StringBuilder buffer = new StringBuilder();
374         for (String header : canonicalizedHeaders.keySet()) {
375             if (buffer.length() > 0) {
376                 buffer.append(";");
377             }
378             buffer.append(header);
379         }
380         return buffer.toString();
381     }
382
383     private void addHostHeader(SdkHttpFullRequest.Builder mutableRequest) {
384         // AWS4 requires that we sign the Host header so we
385         // have to have it in the request by the time we sign.
386
387         StringBuilder hostHeaderBuilder = new StringBuilder(mutableRequest.host());
388         if (!SdkHttpUtils.isUsingStandardPort(mutableRequest.protocol(), mutableRequest.port())) {
389             hostHeaderBuilder.append(":").append(mutableRequest.port());
390         }
391
392         mutableRequest.putHeader(SignerConstant.HOST, hostHeaderBuilder.toString());
393     }
394
395     private void addDateHeader(SdkHttpFullRequest.Builder mutableRequest, String dateTime) {
396         mutableRequest.putHeader(SignerConstant.X_AMZ_DATE, dateTime);
397     }
398
399     /**
400      * Generates an expiration time for the presigned url. If user has specified
401      * an expiration time, check if it is in the given limit.
402      */

403     private long getSignatureDurationInSeconds(Aws4SignerRequestParams requestParams,
404                                                U signingParams) {
405
406         long expirationInSeconds = signingParams.expirationTime()
407                                                 .map(t -> t.getEpochSecond() -
408                                                           (requestParams.getRequestSigningDateTimeMilli() / 1000))
409                                                 .orElse(SignerConstant.PRESIGN_URL_MAX_EXPIRATION_SECONDS);
410
411         if (expirationInSeconds > SignerConstant.PRESIGN_URL_MAX_EXPIRATION_SECONDS) {
412             throw SdkClientException.builder()
413                                     .message("Requests that are pre-signed by SigV4 algorithm are valid for at most 7" +
414                                              " days. The expiration date set on the current request [" +
415                                              Aws4SignerUtils.formatTimestamp(expirationInSeconds * 1000L) + "] +" +
416                                             " has exceeded this limit.")
417                                     .build();
418         }
419         return expirationInSeconds;
420     }
421
422     /**
423      * Generates a new signing key from the given parameters and returns it.
424      */

425     private byte[] newSigningKey(AwsCredentials credentials,
426                                  String dateStamp, String regionName, String serviceName) {
427         byte[] kSecret = ("AWS4" + credentials.secretAccessKey())
428             .getBytes(Charset.forName("UTF-8"));
429         byte[] kDate = sign(dateStamp, kSecret, SigningAlgorithm.HmacSHA256);
430         byte[] kRegion = sign(regionName, kDate, SigningAlgorithm.HmacSHA256);
431         byte[] kService = sign(serviceName, kRegion,
432                                SigningAlgorithm.HmacSHA256);
433         return sign(SignerConstant.AWS4_TERMINATOR, kService, SigningAlgorithm.HmacSHA256);
434     }
435
436     protected <B extends Aws4PresignerParams.Builder> B extractPresignerParams(B builder,
437                                                                                ExecutionAttributes executionAttributes) {
438         builder = extractSignerParams(builder, executionAttributes);
439         builder.expirationTime(executionAttributes.getAttribute(AwsSignerExecutionAttribute.PRESIGNER_EXPIRATION));
440
441         return builder;
442     }
443
444     protected <B extends Aws4SignerParams.Builder> B extractSignerParams(B paramsBuilder,
445                                                                          ExecutionAttributes executionAttributes) {
446         paramsBuilder.awsCredentials(executionAttributes.getAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS))
447                      .signingName(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SERVICE_SIGNING_NAME))
448                      .signingRegion(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION))
449                      .timeOffset(executionAttributes.getAttribute(AwsSignerExecutionAttribute.TIME_OFFSET));
450
451         if (executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE) != null) {
452             paramsBuilder.doubleUrlEncode(executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNER_DOUBLE_URL_ENCODE));
453         }
454
455         return paramsBuilder;
456     }
457 }
458