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