• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.http.auth.aws.internal.signer.util;
17 
18 import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.X_AMZ_CONTENT_SHA256;
19 import static software.amazon.awssdk.http.auth.aws.internal.signer.util.SignerConstant.X_AMZ_DECODED_CONTENT_LENGTH;
20 
21 import java.io.ByteArrayInputStream;
22 import java.io.InputStream;
23 import java.nio.ByteBuffer;
24 import java.nio.charset.StandardCharsets;
25 import java.security.MessageDigest;
26 import java.time.Instant;
27 import java.time.ZoneId;
28 import java.time.format.DateTimeFormatter;
29 import java.util.Optional;
30 import javax.crypto.Mac;
31 import javax.crypto.spec.SecretKeySpec;
32 import software.amazon.awssdk.annotations.SdkInternalApi;
33 import software.amazon.awssdk.http.ContentStreamProvider;
34 import software.amazon.awssdk.http.Header;
35 import software.amazon.awssdk.http.SdkHttpRequest;
36 import software.amazon.awssdk.http.auth.aws.internal.signer.CredentialScope;
37 import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity;
38 import software.amazon.awssdk.utils.BinaryUtils;
39 import software.amazon.awssdk.utils.Logger;
40 import software.amazon.awssdk.utils.http.SdkHttpUtils;
41 
42 /**
43  * Utility methods to be used by various AWS Signer implementations. This class is protected and subject to change.
44  */
45 @SdkInternalApi
46 public final class SignerUtils {
47 
48     private static final Logger LOG = Logger.loggerFor(SignerUtils.class);
49 
50     private static final FifoCache<SignerKey> SIGNER_CACHE =
51         new FifoCache<>(300);
52 
53     private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter
54         .ofPattern("yyyyMMdd").withZone(ZoneId.of("UTC"));
55 
56     private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter
57         .ofPattern("yyyyMMdd'T'HHmmss'Z'").withZone(ZoneId.of("UTC"));
58 
SignerUtils()59     private SignerUtils() {
60     }
61 
62     /**
63      * Returns a string representation of the given datetime in yyyyMMdd format. The date returned is in the UTC zone.
64      * <p>
65      * For example, given an Instant with millis-value of 1416863450581, this method returns "20141124"
66      */
formatDate(Instant instant)67     public static String formatDate(Instant instant) {
68         return DATE_FORMATTER.format(instant);
69     }
70 
71     /**
72      * Returns a string representation of the given datetime in yyyyMMdd'T'HHmmss'Z' format. The date returned is in the UTC
73      * zone.
74      * <p>
75      * For example, given an Instant with millis-value of 1416863450581, this method returns "20141124T211050Z"
76      */
formatDateTime(Instant instant)77     public static String formatDateTime(Instant instant) {
78         return TIME_FORMATTER.format(instant);
79     }
80 
81     /**
82      * Create a hash of the canonical request string
83      * <p>
84      * Step 2 of the AWS Signature version 4 calculation. Refer to
85      * https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html#create-canonical-request-hash.
86      */
hashCanonicalRequest(String canonicalRequestString)87     public static String hashCanonicalRequest(String canonicalRequestString) {
88         return BinaryUtils.toHex(
89             hash(canonicalRequestString)
90         );
91     }
92 
93     /**
94      * Get the signing key based on the given credentials and a credential-scope
95      */
deriveSigningKey(AwsCredentialsIdentity credentials, CredentialScope credentialScope)96     public static byte[] deriveSigningKey(AwsCredentialsIdentity credentials, CredentialScope credentialScope) {
97         String cacheKey = createSigningCacheKeyName(credentials, credentialScope.getRegion(), credentialScope.getService());
98         SignerKey signerKey = SIGNER_CACHE.get(cacheKey);
99 
100         if (signerKey != null && signerKey.isValidForDate(credentialScope.getInstant())) {
101             return signerKey.getSigningKey();
102         }
103 
104         LOG.trace(() -> "Generating a new signing key as the signing key not available in the cache for the date: " +
105                         credentialScope.getInstant().toEpochMilli());
106         byte[] signingKey = newSigningKey(credentials,
107                                           credentialScope.getDate(),
108                                           credentialScope.getRegion(),
109                                           credentialScope.getService());
110         SIGNER_CACHE.add(cacheKey, new SignerKey(credentialScope.getInstant(), signingKey));
111         return signingKey;
112     }
113 
createSigningCacheKeyName(AwsCredentialsIdentity credentials, String regionName, String serviceName)114     private static String createSigningCacheKeyName(AwsCredentialsIdentity credentials,
115                                                     String regionName,
116                                                     String serviceName) {
117         return credentials.secretAccessKey() + "-" + regionName + "-" + serviceName;
118     }
119 
newSigningKey(AwsCredentialsIdentity credentials, String dateStamp, String regionName, String serviceName)120     private static byte[] newSigningKey(AwsCredentialsIdentity credentials,
121                                         String dateStamp, String regionName, String serviceName) {
122         byte[] kSecret = ("AWS4" + credentials.secretAccessKey())
123             .getBytes(StandardCharsets.UTF_8);
124         byte[] kDate = sign(dateStamp, kSecret);
125         byte[] kRegion = sign(regionName, kDate);
126         byte[] kService = sign(serviceName, kRegion);
127         return sign(SignerConstant.AWS4_TERMINATOR, kService);
128     }
129 
130     /**
131      * Sign given data using a key.
132      */
sign(String stringData, byte[] key)133     public static byte[] sign(String stringData, byte[] key) {
134         try {
135             byte[] data = stringData.getBytes(StandardCharsets.UTF_8);
136             return sign(data, key, SigningAlgorithm.HMAC_SHA256);
137         } catch (Exception e) {
138             throw new RuntimeException("Unable to calculate a request signature: ", e);
139         }
140     }
141 
142     /**
143      * Sign given data using a key and a specific algorithm
144      */
sign(byte[] data, byte[] key, SigningAlgorithm algorithm)145     public static byte[] sign(byte[] data, byte[] key, SigningAlgorithm algorithm) {
146         try {
147             Mac mac = algorithm.getMac();
148             mac.init(new SecretKeySpec(key, algorithm.toString()));
149             return mac.doFinal(data);
150         } catch (Exception e) {
151             throw new RuntimeException("Unable to calculate a request signature: ", e);
152         }
153     }
154 
155     /**
156      * Compute the signature of a string using a signing key.
157      * <p>
158      * Step 4 of the AWS Signature version 4 calculation. Refer to
159      * https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html#calculate-signature.
160      */
computeSignature(String stringToSign, byte[] signingKey)161     public static byte[] computeSignature(String stringToSign, byte[] signingKey) {
162         return sign(stringToSign.getBytes(StandardCharsets.UTF_8), signingKey,
163                     SigningAlgorithm.HMAC_SHA256);
164     }
165 
166     /**
167      * Add the host header based on parameters of a request
168      */
addHostHeader(SdkHttpRequest.Builder requestBuilder)169     public static void addHostHeader(SdkHttpRequest.Builder requestBuilder) {
170         // AWS4 requires that we sign the Host header, so we
171         // have to have it in the request by the time we sign.
172 
173         String host = requestBuilder.host();
174         if (!SdkHttpUtils.isUsingStandardPort(requestBuilder.protocol(), requestBuilder.port())) {
175             StringBuilder hostHeaderBuilder = new StringBuilder(host);
176             hostHeaderBuilder.append(":").append(requestBuilder.port());
177             requestBuilder.putHeader(SignerConstant.HOST, hostHeaderBuilder.toString());
178         } else {
179             requestBuilder.putHeader(SignerConstant.HOST, host);
180         }
181     }
182 
183     /**
184      * Add a date header using a datetime string
185      */
addDateHeader(SdkHttpRequest.Builder requestBuilder, String dateTime)186     public static void addDateHeader(SdkHttpRequest.Builder requestBuilder, String dateTime) {
187         requestBuilder.putHeader(SignerConstant.X_AMZ_DATE, dateTime);
188     }
189 
190     /**
191      * Move `Content-Length` to `x-amz-decoded-content-length` if not already present. If `Content-Length` is not present, then
192      * the payload is read in its entirety to calculate the length.
193      */
moveContentLength(SdkHttpRequest.Builder request, InputStream payload)194     public static long moveContentLength(SdkHttpRequest.Builder request, InputStream payload) {
195         Optional<String> decodedContentLength = request.firstMatchingHeader(X_AMZ_DECODED_CONTENT_LENGTH);
196         if (!decodedContentLength.isPresent()) {
197             // if the decoded length isn't present, content-length must be there
198             String contentLength = request.firstMatchingHeader(Header.CONTENT_LENGTH).orElseGet(
199                 () -> String.valueOf(readAll(payload))
200             );
201 
202             request.putHeader(X_AMZ_DECODED_CONTENT_LENGTH, contentLength)
203                    .removeHeader(Header.CONTENT_LENGTH);
204             return Long.parseLong(contentLength);
205         }
206 
207         // decoded header is already there, so remove content-length just to be sure it's gone
208         request.removeHeader(Header.CONTENT_LENGTH);
209         return Long.parseLong(decodedContentLength.get());
210     }
211 
getMessageDigestInstance()212     private static MessageDigest getMessageDigestInstance() {
213         return DigestAlgorithm.SHA256.getDigest();
214     }
215 
getBinaryRequestPayloadStream(ContentStreamProvider streamProvider)216     public static InputStream getBinaryRequestPayloadStream(ContentStreamProvider streamProvider) {
217         try {
218             if (streamProvider == null) {
219                 return new ByteArrayInputStream(new byte[0]);
220             }
221             return streamProvider.newStream();
222         } catch (Exception e) {
223             throw new RuntimeException("Unable to read request payload to sign request: ", e);
224         }
225     }
226 
hash(InputStream input)227     public static byte[] hash(InputStream input) {
228         try {
229             MessageDigest md = getMessageDigestInstance();
230             byte[] buf = new byte[4096];
231             int read = 0;
232             while (read >= 0) {
233                 read = input.read(buf);
234                 md.update(buf, 0, read);
235             }
236             return md.digest();
237         } catch (Exception e) {
238             throw new RuntimeException("Unable to compute hash while signing request: ", e);
239         }
240     }
241 
hash(ByteBuffer input)242     public static byte[] hash(ByteBuffer input) {
243         try {
244             MessageDigest md = getMessageDigestInstance();
245             md.update(input);
246             return md.digest();
247         } catch (Exception e) {
248             throw new RuntimeException("Unable to compute hash while signing request: ", e);
249         }
250     }
251 
hash(byte[] data)252     public static byte[] hash(byte[] data) {
253         try {
254             MessageDigest md = getMessageDigestInstance();
255             md.update(data);
256             return md.digest();
257         } catch (Exception e) {
258             throw new RuntimeException("Unable to compute hash while signing request: ", e);
259         }
260     }
261 
hash(String text)262     public static byte[] hash(String text) {
263         return hash(text.getBytes(StandardCharsets.UTF_8));
264     }
265 
266     /**
267      * Consume entire stream and return the number of bytes - the stream will NOT be reset upon completion, so if it needs to
268      * be read again, the caller MUST reset the stream.
269      */
readAll(InputStream inputStream)270     private static int readAll(InputStream inputStream) {
271         try {
272             byte[] buffer = new byte[4096];
273             int read = 0;
274             int offset = 0;
275             while (read >= 0) {
276                 read = inputStream.read(buffer);
277                 if (read >= 0) {
278                     offset += read;
279                 }
280             }
281             return offset;
282         } catch (Exception e) {
283             throw new RuntimeException("Could not finish reading stream: ", e);
284         }
285     }
286 
getContentHash(SdkHttpRequest.Builder requestBuilder)287     public static String getContentHash(SdkHttpRequest.Builder requestBuilder) {
288         return requestBuilder.firstMatchingHeader(X_AMZ_CONTENT_SHA256).orElseThrow(
289             () -> new IllegalArgumentException("Content hash must be present in the '" + X_AMZ_CONTENT_SHA256 + "' header!")
290         );
291     }
292 }
293