• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 Google LLC
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *    * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *    * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *
15  *    * Neither the name of Google LLC nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31 
32 package com.google.auth.oauth2;
33 
34 import static com.google.common.base.Preconditions.checkNotNull;
35 import static java.nio.charset.StandardCharsets.UTF_8;
36 
37 import com.google.auth.ServiceAccountSigner.SigningException;
38 import com.google.common.base.Joiner;
39 import com.google.common.base.Splitter;
40 import com.google.common.io.BaseEncoding;
41 import com.google.errorprone.annotations.CanIgnoreReturnValue;
42 import java.net.URI;
43 import java.security.InvalidKeyException;
44 import java.security.MessageDigest;
45 import java.security.NoSuchAlgorithmException;
46 import java.text.ParseException;
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.HashMap;
50 import java.util.List;
51 import java.util.Locale;
52 import java.util.Map;
53 import javax.annotation.Nullable;
54 import javax.crypto.Mac;
55 import javax.crypto.spec.SecretKeySpec;
56 
57 /**
58  * Internal utility that signs AWS API requests based on the AWS Signature Version 4 signing
59  * process.
60  *
61  * @see <a href="https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html">AWS
62  *     Signature V4</a>
63  */
64 class AwsRequestSigner {
65 
66   // AWS Signature Version 4 signing algorithm identifier.
67   private static final String HASHING_ALGORITHM = "AWS4-HMAC-SHA256";
68 
69   // The termination string for the AWS credential scope value as defined in
70   // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
71   private static final String AWS_REQUEST_TYPE = "aws4_request";
72 
73   private final AwsSecurityCredentials awsSecurityCredentials;
74   private final Map<String, String> additionalHeaders;
75   private final String httpMethod;
76   private final String region;
77   private final String requestPayload;
78   private final URI uri;
79   private final AwsDates dates;
80 
81   /**
82    * Internal constructor.
83    *
84    * @param awsSecurityCredentials AWS security credentials
85    * @param httpMethod the HTTP request method
86    * @param url the request URL
87    * @param region the targeted region
88    * @param requestPayload the request payload
89    * @param additionalHeaders a map of additional HTTP headers to be included with the signed
90    *     request
91    */
AwsRequestSigner( AwsSecurityCredentials awsSecurityCredentials, String httpMethod, String url, String region, @Nullable String requestPayload, @Nullable Map<String, String> additionalHeaders, @Nullable AwsDates awsDates)92   private AwsRequestSigner(
93       AwsSecurityCredentials awsSecurityCredentials,
94       String httpMethod,
95       String url,
96       String region,
97       @Nullable String requestPayload,
98       @Nullable Map<String, String> additionalHeaders,
99       @Nullable AwsDates awsDates) {
100     this.awsSecurityCredentials = checkNotNull(awsSecurityCredentials);
101     this.httpMethod = checkNotNull(httpMethod);
102     this.uri = URI.create(url).normalize();
103     this.region = checkNotNull(region);
104     this.requestPayload = requestPayload == null ? "" : requestPayload;
105     this.additionalHeaders =
106         (additionalHeaders != null)
107             ? new HashMap<>(additionalHeaders)
108             : new HashMap<String, String>();
109     this.dates = awsDates == null ? AwsDates.generateXAmzDate() : awsDates;
110   }
111 
112   /**
113    * Signs the specified AWS API request.
114    *
115    * @return the {@link AwsRequestSignature}
116    */
sign()117   AwsRequestSignature sign() {
118     // Retrieve the service name. For example: iam.amazonaws.com host => iam service.
119     String serviceName = Splitter.on(".").split(uri.getHost()).iterator().next();
120 
121     Map<String, String> canonicalHeaders = getCanonicalHeaders(dates.getOriginalDate());
122     // Headers must be sorted.
123     List<String> sortedHeaderNames = new ArrayList<>();
124     for (String headerName : canonicalHeaders.keySet()) {
125       sortedHeaderNames.add(headerName.toLowerCase(Locale.US));
126     }
127     Collections.sort(sortedHeaderNames);
128 
129     String canonicalRequestHash = createCanonicalRequestHash(canonicalHeaders, sortedHeaderNames);
130     String credentialScope =
131         dates.getFormattedDate() + "/" + region + "/" + serviceName + "/" + AWS_REQUEST_TYPE;
132     String stringToSign =
133         createStringToSign(canonicalRequestHash, dates.getXAmzDate(), credentialScope);
134     String signature =
135         calculateAwsV4Signature(
136             serviceName,
137             awsSecurityCredentials.getSecretAccessKey(),
138             dates.getFormattedDate(),
139             region,
140             stringToSign);
141 
142     String authorizationHeader =
143         generateAuthorizationHeader(
144             sortedHeaderNames, awsSecurityCredentials.getAccessKeyId(), credentialScope, signature);
145 
146     return new AwsRequestSignature.Builder()
147         .setSignature(signature)
148         .setCanonicalHeaders(canonicalHeaders)
149         .setHttpMethod(httpMethod)
150         .setSecurityCredentials(awsSecurityCredentials)
151         .setCredentialScope(credentialScope)
152         .setUrl(uri.toString())
153         .setDate(dates.getOriginalDate())
154         .setRegion(region)
155         .setAuthorizationHeader(authorizationHeader)
156         .build();
157   }
158 
159   /** Task 1: Create a canonical request for Signature Version 4. */
createCanonicalRequestHash( Map<String, String> headers, List<String> sortedHeaderNames)160   private String createCanonicalRequestHash(
161       Map<String, String> headers, List<String> sortedHeaderNames) {
162     // Append the HTTP request method.
163     StringBuilder canonicalRequest = new StringBuilder(httpMethod).append("\n");
164 
165     // Append the path.
166     String urlPath = uri.getRawPath().isEmpty() ? "/" : uri.getRawPath();
167     canonicalRequest.append(urlPath).append("\n");
168 
169     // Append the canonical query string.
170     String actionQueryString = uri.getRawQuery() != null ? uri.getRawQuery() : "";
171     canonicalRequest.append(actionQueryString).append("\n");
172 
173     // Append the canonical headers.
174     StringBuilder canonicalHeaders = new StringBuilder();
175     for (String headerName : sortedHeaderNames) {
176       canonicalHeaders.append(headerName).append(":").append(headers.get(headerName)).append("\n");
177     }
178     canonicalRequest.append(canonicalHeaders).append("\n");
179 
180     // Append the signed headers.
181     canonicalRequest.append(Joiner.on(';').join(sortedHeaderNames)).append("\n");
182 
183     // Append the hashed request payload.
184     canonicalRequest.append(getHexEncodedSha256Hash(requestPayload.getBytes(UTF_8)));
185 
186     // Return the hashed canonical request.
187     return getHexEncodedSha256Hash(canonicalRequest.toString().getBytes(UTF_8));
188   }
189 
190   /** Task 2: Create a string to sign for Signature Version 4. */
createStringToSign( String canonicalRequestHash, String xAmzDate, String credentialScope)191   private String createStringToSign(
192       String canonicalRequestHash, String xAmzDate, String credentialScope) {
193     return HASHING_ALGORITHM
194         + "\n"
195         + xAmzDate
196         + "\n"
197         + credentialScope
198         + "\n"
199         + canonicalRequestHash;
200   }
201 
202   /**
203    * Task 3: Calculate the signature for AWS Signature Version 4.
204    *
205    * @param date the date used in the hashing process in YYYYMMDD format
206    */
calculateAwsV4Signature( String serviceName, String secret, String date, String region, String stringToSign)207   private String calculateAwsV4Signature(
208       String serviceName, String secret, String date, String region, String stringToSign) {
209     byte[] kDate = sign(("AWS4" + secret).getBytes(UTF_8), date.getBytes(UTF_8));
210     byte[] kRegion = sign(kDate, region.getBytes(UTF_8));
211     byte[] kService = sign(kRegion, serviceName.getBytes(UTF_8));
212     byte[] kSigning = sign(kService, AWS_REQUEST_TYPE.getBytes(UTF_8));
213     return BaseEncoding.base16().lowerCase().encode(sign(kSigning, stringToSign.getBytes(UTF_8)));
214   }
215 
216   /** Task 4: Format the signature to be added to the HTTP request. */
generateAuthorizationHeader( List<String> sortedHeaderNames, String accessKeyId, String credentialScope, String signature)217   private String generateAuthorizationHeader(
218       List<String> sortedHeaderNames,
219       String accessKeyId,
220       String credentialScope,
221       String signature) {
222     return String.format(
223         "%s Credential=%s/%s, SignedHeaders=%s, Signature=%s",
224         HASHING_ALGORITHM,
225         accessKeyId,
226         credentialScope,
227         Joiner.on(';').join(sortedHeaderNames),
228         signature);
229   }
230 
getCanonicalHeaders(String defaultDate)231   private Map<String, String> getCanonicalHeaders(String defaultDate) {
232     Map<String, String> headers = new HashMap<>();
233     headers.put("host", uri.getHost());
234 
235     // Only add the date if it hasn't been specified through the "date" header.
236     if (!additionalHeaders.containsKey("date")) {
237       headers.put("x-amz-date", defaultDate);
238     }
239 
240     if (awsSecurityCredentials.getSessionToken() != null
241         && !awsSecurityCredentials.getSessionToken().isEmpty()) {
242       headers.put("x-amz-security-token", awsSecurityCredentials.getSessionToken());
243     }
244 
245     // Add all additional headers.
246     for (String key : additionalHeaders.keySet()) {
247       // Header keys need to be lowercase.
248       headers.put(key.toLowerCase(Locale.US), additionalHeaders.get(key));
249     }
250     return headers;
251   }
252 
sign(byte[] key, byte[] value)253   private static byte[] sign(byte[] key, byte[] value) {
254     try {
255       String algorithm = "HmacSHA256";
256       Mac mac = Mac.getInstance(algorithm);
257       mac.init(new SecretKeySpec(key, algorithm));
258       return mac.doFinal(value);
259     } catch (NoSuchAlgorithmException e) {
260       // Will not occur as HmacSHA256 is supported. We may allow other algorithms in the future.
261       throw new RuntimeException("HmacSHA256 must be supported by the JVM.", e);
262     } catch (InvalidKeyException e) {
263       throw new SigningException("Invalid key used when calculating the AWS V4 Signature", e);
264     }
265   }
266 
getHexEncodedSha256Hash(byte[] bytes)267   private static String getHexEncodedSha256Hash(byte[] bytes) {
268     try {
269       MessageDigest digest = MessageDigest.getInstance("SHA-256");
270       return BaseEncoding.base16().lowerCase().encode(digest.digest(bytes));
271     } catch (NoSuchAlgorithmException e) {
272       throw new RuntimeException("Failed to compute SHA-256 hash.", e);
273     }
274   }
275 
newBuilder( AwsSecurityCredentials awsSecurityCredentials, String httpMethod, String url, String region)276   static Builder newBuilder(
277       AwsSecurityCredentials awsSecurityCredentials, String httpMethod, String url, String region) {
278     return new Builder(awsSecurityCredentials, httpMethod, url, region);
279   }
280 
281   static class Builder {
282 
283     private final AwsSecurityCredentials awsSecurityCredentials;
284     private final String httpMethod;
285     private final String url;
286     private final String region;
287 
288     @Nullable private String requestPayload;
289     @Nullable private Map<String, String> additionalHeaders;
290     @Nullable private AwsDates dates;
291 
Builder( AwsSecurityCredentials awsSecurityCredentials, String httpMethod, String url, String region)292     private Builder(
293         AwsSecurityCredentials awsSecurityCredentials,
294         String httpMethod,
295         String url,
296         String region) {
297       this.awsSecurityCredentials = awsSecurityCredentials;
298       this.httpMethod = httpMethod;
299       this.url = url;
300       this.region = region;
301     }
302 
303     @CanIgnoreReturnValue
setRequestPayload(String requestPayload)304     Builder setRequestPayload(String requestPayload) {
305       this.requestPayload = requestPayload;
306       return this;
307     }
308 
309     @CanIgnoreReturnValue
setAdditionalHeaders(Map<String, String> additionalHeaders)310     Builder setAdditionalHeaders(Map<String, String> additionalHeaders) {
311       if (additionalHeaders.containsKey("date") && additionalHeaders.containsKey("x-amz-date")) {
312         throw new IllegalArgumentException("One of {date, x-amz-date} can be specified, not both.");
313       }
314       try {
315         if (additionalHeaders.containsKey("date")) {
316           this.dates = AwsDates.fromDateHeader(additionalHeaders.get("date"));
317         }
318         if (additionalHeaders.containsKey("x-amz-date")) {
319           this.dates = AwsDates.fromXAmzDate(additionalHeaders.get("x-amz-date"));
320         }
321       } catch (ParseException e) {
322         throw new IllegalArgumentException("The provided date header value is invalid.", e);
323       }
324 
325       this.additionalHeaders = additionalHeaders;
326       return this;
327     }
328 
build()329     AwsRequestSigner build() {
330       return new AwsRequestSigner(
331           awsSecurityCredentials,
332           httpMethod,
333           url,
334           region,
335           requestPayload,
336           additionalHeaders,
337           dates);
338     }
339   }
340 }
341