| /* |
| * 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); |
| } |
| } |
| |
| } |