1
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
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
114
115
116 mutableRequest.putRawQueryParameter(SignerConstant.X_AMZ_SECURITY_TOKEN,
117 ((AwsSessionCredentials) sanitizedCredentials).sessionToken());
118 }
119
120
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
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
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
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
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
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
260 private byte[] computeSignature(String stringToSign, byte[] signingKey) {
261 return sign(stringToSign.getBytes(Charset.forName("UTF-8")), signingKey,
262 SigningAlgorithm.HmacSHA256);
263 }
264
265
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
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
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
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
385
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
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
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