AwsRequestSignerV4.java
- /*
- * Copyright (C) 2022, Workday Inc.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
- package org.eclipse.jgit.transport;
- import java.net.HttpURLConnection;
- import java.net.URL;
- import java.nio.charset.StandardCharsets;
- import java.security.MessageDigest;
- import java.time.Instant;
- import java.time.OffsetDateTime;
- import java.time.ZoneOffset;
- import java.time.format.DateTimeFormatter;
- import java.util.HashMap;
- import java.util.Map;
- import java.util.stream.Collectors;
- import javax.crypto.Mac;
- import javax.crypto.spec.SecretKeySpec;
- import org.eclipse.jgit.internal.JGitText;
- import org.eclipse.jgit.util.Hex;
- import org.eclipse.jgit.util.HttpSupport;
- /**
- * Utility class for signing requests to AWS service endpoints using the V4
- * signing protocol.
- *
- * Reference implementation: <a href=
- * "https://docs.aws.amazon.com/AmazonS3/latest/API/samples/AWSS3SigV4JavaSamples.zip">AWSS3SigV4JavaSamples.zip</a>
- *
- * @see <a href=
- * "https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html">AWS
- * Signature Version 4</a>
- *
- * @since 5.13.1
- */
- public final class AwsRequestSignerV4 {
- /** AWS version 4 signing algorithm (for authorization header). **/
- private static final String ALGORITHM = "HMAC-SHA256"; //$NON-NLS-1$
- /** Java Message Authentication Code (MAC) algorithm name. **/
- private static final String MAC_ALGORITHM = "HmacSHA256"; //$NON-NLS-1$
- /** AWS version 4 signing scheme. **/
- private static final String SCHEME = "AWS4"; //$NON-NLS-1$
- /** AWS version 4 terminator string. **/
- private static final String TERMINATOR = "aws4_request"; //$NON-NLS-1$
- /** SHA-256 hash of an empty request body. **/
- private static final String EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; //$NON-NLS-1$
- /** Date format for the 'x-amz-date' header. **/
- private static final DateTimeFormatter AMZ_DATE_FORMAT = DateTimeFormatter
- .ofPattern("yyyyMMdd'T'HHmmss'Z'"); //$NON-NLS-1$
- /** Date format for the string-to-sign's scope. **/
- private static final DateTimeFormatter SCOPE_DATE_FORMAT = DateTimeFormatter
- .ofPattern("yyyyMMdd"); //$NON-NLS-1$
- private AwsRequestSignerV4() {
- // Don't instantiate utility class
- }
- /**
- * Sign the provided request with an AWS4 signature as the 'Authorization'
- * header.
- *
- * @param httpURLConnection
- * The request to sign.
- * @param queryParameters
- * The query parameters being sent in the request.
- * @param contentLength
- * The content length of the data being sent in the request
- * @param bodyHash
- * Hex-encoded SHA-256 hash of the data being sent in the request
- * @param serviceName
- * The signing name of the AWS service (e.g. "s3").
- * @param regionName
- * The name of the AWS region that will handle the request (e.g.
- * "us-east-1").
- * @param awsAccessKey
- * The user's AWS Access Key.
- * @param awsSecretKey
- * The user's AWS Secret Key.
- */
- public static void sign(HttpURLConnection httpURLConnection,
- Map<String, String> queryParameters, long contentLength,
- String bodyHash, String serviceName, String regionName,
- String awsAccessKey, char[] awsSecretKey) {
- // get request headers
- Map<String, String> headers = new HashMap<>();
- httpURLConnection.getRequestProperties()
- .forEach((headerName, headerValues) -> headers.put(headerName,
- String.join(",", headerValues))); //$NON-NLS-1$
- // add required content headers
- if (contentLength > 0) {
- headers.put(HttpSupport.HDR_CONTENT_LENGTH,
- String.valueOf(contentLength));
- } else {
- bodyHash = EMPTY_BODY_SHA256;
- }
- headers.put("x-amz-content-sha256", bodyHash); //$NON-NLS-1$
- // add the 'x-amz-date' header
- OffsetDateTime now = Instant.now().atOffset(ZoneOffset.UTC);
- String amzDate = now.format(AMZ_DATE_FORMAT);
- headers.put("x-amz-date", amzDate); //$NON-NLS-1$
- // add the 'host' header
- URL endpointUrl = httpURLConnection.getURL();
- int port = endpointUrl.getPort();
- String hostHeader = (port > -1)
- ? endpointUrl.getHost().concat(":" + port) //$NON-NLS-1$
- : endpointUrl.getHost();
- headers.put("Host", hostHeader); //$NON-NLS-1$
- // construct the canonicalized request
- String canonicalizedHeaderNames = getCanonicalizeHeaderNames(headers);
- String canonicalizedHeaders = getCanonicalizedHeaderString(headers);
- String canonicalizedQueryParameters = getCanonicalizedQueryString(
- queryParameters);
- String httpMethod = httpURLConnection.getRequestMethod();
- String canonicalRequest = httpMethod + '\n'
- + getCanonicalizedResourcePath(endpointUrl) + '\n'
- + canonicalizedQueryParameters + '\n' + canonicalizedHeaders
- + '\n' + canonicalizedHeaderNames + '\n' + bodyHash;
- // construct the string-to-sign
- String scopeDate = now.format(SCOPE_DATE_FORMAT);
- String scope = scopeDate + '/' + regionName + '/' + serviceName + '/'
- + TERMINATOR;
- String stringToSign = SCHEME + '-' + ALGORITHM + '\n' + amzDate + '\n'
- + scope + '\n' + Hex.toHexString(hash(
- canonicalRequest.getBytes(StandardCharsets.UTF_8)));
- // compute the signing key
- byte[] secretKey = (SCHEME + new String(awsSecretKey)).getBytes();
- byte[] dateKey = signStringWithKey(scopeDate, secretKey);
- byte[] regionKey = signStringWithKey(regionName, dateKey);
- byte[] serviceKey = signStringWithKey(serviceName, regionKey);
- byte[] signingKey = signStringWithKey(TERMINATOR, serviceKey);
- byte[] signature = signStringWithKey(stringToSign, signingKey);
- // construct the authorization header
- String credentialsAuthorizationHeader = "Credential=" + awsAccessKey //$NON-NLS-1$
- + '/' + scope;
- String signedHeadersAuthorizationHeader = "SignedHeaders=" //$NON-NLS-1$
- + canonicalizedHeaderNames;
- String signatureAuthorizationHeader = "Signature=" //$NON-NLS-1$
- + Hex.toHexString(signature);
- String authorizationHeader = SCHEME + '-' + ALGORITHM + ' '
- + credentialsAuthorizationHeader + ", " //$NON-NLS-1$
- + signedHeadersAuthorizationHeader + ", " //$NON-NLS-1$
- + signatureAuthorizationHeader;
- // Copy back the updated request headers
- headers.forEach(httpURLConnection::setRequestProperty);
- // Add the 'authorization' header
- httpURLConnection.setRequestProperty(HttpSupport.HDR_AUTHORIZATION,
- authorizationHeader);
- }
- /**
- * Calculates the hex-encoded SHA-256 hash of the provided byte array.
- *
- * @param data
- * Byte array to hash
- *
- * @return Hex-encoded SHA-256 hash of the provided byte array.
- */
- public static String calculateBodyHash(final byte[] data) {
- return (data == null || data.length < 1) ? EMPTY_BODY_SHA256
- : Hex.toHexString(hash(data));
- }
- /**
- * Construct a string listing all request headers in sorted case-insensitive
- * order, separated by a ';'.
- *
- * @param headers
- * Map containing all request headers.
- *
- * @return String that lists all request headers in sorted case-insensitive
- * order, separated by a ';'.
- */
- private static String getCanonicalizeHeaderNames(
- Map<String, String> headers) {
- return headers.keySet().stream().map(String::toLowerCase).sorted()
- .collect(Collectors.joining(";")); //$NON-NLS-1$
- }
- /**
- * Constructs the canonical header string for a request.
- *
- * @param headers
- * Map containing all request headers.
- *
- * @return The canonical headers with values for the request.
- */
- private static String getCanonicalizedHeaderString(
- Map<String, String> headers) {
- if (headers == null || headers.isEmpty()) {
- return ""; //$NON-NLS-1$
- }
- StringBuilder sb = new StringBuilder();
- headers.keySet().stream().sorted(String.CASE_INSENSITIVE_ORDER)
- .forEach(key -> {
- String header = key.toLowerCase().replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$
- String value = headers.get(key).replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$
- sb.append(header).append(':').append(value).append('\n');
- });
- return sb.toString();
- }
- /**
- * Constructs the canonicalized resource path for an AWS service endpoint.
- *
- * @param url
- * The AWS service endpoint URL, including the path to any
- * resource.
- *
- * @return The canonicalized resource path for the AWS service endpoint.
- */
- private static String getCanonicalizedResourcePath(URL url) {
- if (url == null) {
- return "/"; //$NON-NLS-1$
- }
- String path = url.getPath();
- if (path == null || path.isEmpty()) {
- return "/"; //$NON-NLS-1$
- }
- String encodedPath = HttpSupport.urlEncode(path, true);
- if (encodedPath.startsWith("/")) { //$NON-NLS-1$
- return encodedPath;
- }
- return "/".concat(encodedPath); //$NON-NLS-1$
- }
- /**
- * Constructs the canonicalized query string for a request.
- *
- * @param queryParameters
- * The query parameters in the request.
- *
- * @return The canonicalized query string for the request.
- */
- public static String getCanonicalizedQueryString(
- Map<String, String> queryParameters) {
- if (queryParameters == null || queryParameters.isEmpty()) {
- return ""; //$NON-NLS-1$
- }
- return queryParameters
- .keySet().stream().sorted().map(
- key -> HttpSupport.urlEncode(key, false) + '='
- + HttpSupport.urlEncode(
- queryParameters.get(key), false))
- .collect(Collectors.joining("&")); //$NON-NLS-1$
- }
- /**
- * Hashes the provided byte array using the SHA-256 algorithm.
- *
- * @param data
- * The byte array to hash.
- *
- * @return Hashed string contents of the provided byte array.
- */
- public static byte[] hash(byte[] data) {
- try {
- MessageDigest md = MessageDigest.getInstance("SHA-256"); //$NON-NLS-1$
- md.update(data);
- return md.digest();
- } catch (Exception e) {
- throw new RuntimeException(
- JGitText.get().couldNotHashByteArrayWithSha256, e);
- }
- }
- /**
- * Signs the provided string data using the specified key.
- *
- * @param stringToSign
- * The string data to sign.
- * @param key
- * The key material of the secret key.
- *
- * @return Signed string data.
- */
- private static byte[] signStringWithKey(String stringToSign, byte[] key) {
- try {
- byte[] data = stringToSign.getBytes(StandardCharsets.UTF_8);
- Mac mac = Mac.getInstance(MAC_ALGORITHM);
- mac.init(new SecretKeySpec(key, MAC_ALGORITHM));
- return mac.doFinal(data);
- } catch (Exception e) {
- throw new RuntimeException(JGitText.get().couldNotSignStringWithKey,
- e);
- }
- }
- }