summaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit
diff options
context:
space:
mode:
authorMatthias Sohn <matthias.sohn@sap.com>2022-06-15 17:39:52 +0200
committerMatthias Sohn <matthias.sohn@sap.com>2022-06-15 17:39:52 +0200
commit58f5302e1d8c1c7a4111c141bea0533c41129a74 (patch)
tree80b4c1cb1b30f9c3124c6e73b644c13d74cb8fec /org.eclipse.jgit
parenta9f535e104e66180942a4cfdcb6c8e686b8cb951 (diff)
parentd0bc2b544a71e4150bfb31d0f0c9348ac076499b (diff)
downloadjgit-58f5302e1d8c1c7a4111c141bea0533c41129a74.tar.gz
jgit-58f5302e1d8c1c7a4111c141bea0533c41129a74.zip
Merge branch 'stable-6.1' into stable-6.2
* stable-6.1: Prepare 5.13.2-SNAPSHOT builds JGit v5.13.1.202206130422-r AmazonS3: Add support for AWS API signature version 4 Change-Id: Id4965aacd4e2ea1e8575a2c1bd4845729db6049a
Diffstat (limited to 'org.eclipse.jgit')
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties5
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java5
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java92
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java313
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java27
5 files changed, 429 insertions, 13 deletions
diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
index f0bb6c6c99..66adad5151 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -209,12 +209,14 @@ couldNotGetAdvertisedRef=Remote {0} did not advertise Ref for branch {1}. This R
couldNotGetRepoStatistics=Could not get repository statistics
couldNotFindTabInLine=Could not find tab in line {0}. Tab is the mandatory separator for the Netscape Cookie File Format.
couldNotFindSixTabsInLine=Could not find 6 tabs but only {0} in line '{1}'. 7 tab separated columns per line are mandatory for the Netscape Cookie File Format.
+couldNotHashByteArrayWithSha256=Could not hash byte array with SHA-256 algorithm.
couldNotLockHEAD=Could not lock HEAD
couldNotPersistCookies=Could not persist received cookies in file ''{0}''
couldNotReadCookieFile=Could not read cookie file ''{0}''
couldNotReadIndexInOneGo=Could not read index in one go, only {0} out of {1} read
couldNotReadObjectWhileParsingCommit=Could not read an object while parsing commit {0}
couldNotRewindToUpstreamCommit=Could not rewind to upstream commit
+couldNotSignStringWithKey=Could not sign string with key.
couldNotURLEncodeToUTF8=Could not URL encode to UTF-8
countingObjects=Counting objects
corruptPack=Pack file {0} is corrupt, removing it from pack list
@@ -366,6 +368,7 @@ interruptedWriting=Interrupted writing {0}
inTheFuture=in the future
invalidAdvertisementOf=invalid advertisement of {0}
invalidAncestryLength=Invalid ancestry length
+invalidAwsApiSignatureVersion=Invalid aws.api.signature.version: {0}
invalidBooleanValue=Invalid boolean value: {0}.{1}={2}
invalidChannel=Invalid channel {0}
invalidCommitParentNumber=Invalid commit parent number
@@ -467,6 +470,7 @@ minutesAgo={0} minutes ago
mismatchOffset=mismatch offset for object {0}
mismatchCRC=mismatch CRC for object {0}
missingAccesskey=Missing accesskey.
+missingAwsRegion=Missing region (e.g. us-west-2).
missingConfigurationForKey=No value for key {0} found in configuration
missingCookieFile=Configured http.cookieFile ''{0}'' is missing
missingCRC=missing CRC for object {0}
@@ -753,6 +757,7 @@ unableToWrite=Unable to write {0}
unableToSignCommitNoSecretKey=Unable to sign commit. Signing key not available.
unauthorized=Unauthorized
unencodeableFile=Unencodable file: {0}
+unexpectedAwsApiSignatureVersion=Unexpected AWS API Signature Version: {0}
unexpectedCompareResult=Unexpected metadata comparison result: {0}
unexpectedEndOfConfigFile=Unexpected end of config file
unexpectedEndOfInput=Unexpected end of input
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
index 17e359de49..efdb8e42e3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -238,12 +238,14 @@ public class JGitText extends TranslationBundle {
/***/ public String couldNotFindSixTabsInLine;
/***/ public String couldNotGetAdvertisedRef;
/***/ public String couldNotGetRepoStatistics;
+ /***/ public String couldNotHashByteArrayWithSha256;
/***/ public String couldNotLockHEAD;
/***/ public String couldNotPersistCookies;
/***/ public String couldNotReadCookieFile;
/***/ public String couldNotReadIndexInOneGo;
/***/ public String couldNotReadObjectWhileParsingCommit;
/***/ public String couldNotRewindToUpstreamCommit;
+ /***/ public String couldNotSignStringWithKey;
/***/ public String couldNotURLEncodeToUTF8;
/***/ public String countingObjects;
/***/ public String createBranchFailedUnknownReason;
@@ -394,6 +396,7 @@ public class JGitText extends TranslationBundle {
/***/ public String inTheFuture;
/***/ public String invalidAdvertisementOf;
/***/ public String invalidAncestryLength;
+ /***/ public String invalidAwsApiSignatureVersion;
/***/ public String invalidBooleanValue;
/***/ public String invalidChannel;
/***/ public String invalidCommitParentNumber;
@@ -495,6 +498,7 @@ public class JGitText extends TranslationBundle {
/***/ public String mismatchOffset;
/***/ public String mismatchCRC;
/***/ public String missingAccesskey;
+ /***/ public String missingAwsRegion;
/***/ public String missingConfigurationForKey;
/***/ public String missingCookieFile;
/***/ public String missingCRC;
@@ -781,6 +785,7 @@ public class JGitText extends TranslationBundle {
/***/ public String unableToSignCommitNoSecretKey;
/***/ public String unauthorized;
/***/ public String unencodeableFile;
+ /***/ public String unexpectedAwsApiSignatureVersion;
/***/ public String unexpectedCompareResult;
/***/ public String unexpectedEndOfConfigFile;
/***/ public String unexpectedEndOfInput;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java
index d56b5b320d..81a70af2d2 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AmazonS3.java
@@ -86,6 +86,12 @@ import org.xml.sax.helpers.DefaultHandler;
public class AmazonS3 {
private static final Set<String> SIGNED_HEADERS;
+ private static final String AWS_API_V2 = "2"; //$NON-NLS-1$
+
+ private static final String AWS_API_V4 = "4"; //$NON-NLS-1$
+
+ private static final String AWS_S3_SERVICE_NAME = "s3"; //$NON-NLS-1$
+
private static final String HMAC = "HmacSHA1"; //$NON-NLS-1$
private static final String X_AMZ_ACL = "x-amz-acl"; //$NON-NLS-1$
@@ -135,11 +141,17 @@ public class AmazonS3 {
}
}
+ /** AWS API Signature Version. */
+ private final String awsApiSignatureVersion;
+
/** AWSAccessKeyId, public string that identifies the user's account. */
private final String publicKey;
/** Decoded form of the private AWSSecretAccessKey, to sign requests. */
- private final SecretKeySpec privateKey;
+ private final SecretKeySpec secretKeySpec;
+
+ /** AWSSecretAccessKey, private string used to access a user's account. */
+ private final char[] secretKey; // store as char[] for security
/** Our HTTP proxy support, in case we are behind a firewall. */
private final ProxySelector proxySelector;
@@ -159,8 +171,12 @@ public class AmazonS3 {
/** S3 Bucket Domain. */
private final String domain;
+ /** S3 Region. */
+ private final String region;
+
/** Property names used in amazon connection configuration file. */
interface Keys {
+ String AWS_API_SIGNATURE_VERSION = "aws.api.signature.version"; //$NON-NLS-1$
String ACCESS_KEY = "accesskey"; //$NON-NLS-1$
String SECRET_KEY = "secretkey"; //$NON-NLS-1$
String PASSWORD = "password"; //$NON-NLS-1$
@@ -168,6 +184,7 @@ public class AmazonS3 {
String CRYPTO_VER = "crypto.version"; //$NON-NLS-1$
String ACL = "acl"; //$NON-NLS-1$
String DOMAIN = "domain"; //$NON-NLS-1$
+ String REGION = "region"; //$NON-NLS-1$
String HTTP_RETRY = "httpclient.retry-max"; //$NON-NLS-1$
String TMP_DIR = "tmpdir"; //$NON-NLS-1$
}
@@ -180,6 +197,12 @@ public class AmazonS3 {
* For example:
*
* <pre>
+ * # AWS API signature version, must be one of:
+ * # 2 - deprecated (not supported in all AWS regions)
+ * # 4 - latest (supported in all AWS regions)
+ * # Defaults to 2.
+ * aws.api.signature.version: 4
+ *
* # AWS Access and Secret Keys (required)
* accesskey: &lt;YourAWSAccessKey&gt;
* secretkey: &lt;YourAWSSecretKey&gt;
@@ -192,6 +215,9 @@ public class AmazonS3 {
* # AWS S3 Region Domain (defaults to s3.amazonaws.com)
* domain: s3.amazonaws.com
*
+ * # AWS S3 Region (required if aws.api.signature.version = 4)
+ * region: us-west-2
+ *
* # Number of times to retry after internal error from S3.
* httpclient.retry-max: 3
*
@@ -204,16 +230,34 @@ public class AmazonS3 {
* connection properties.
*/
public AmazonS3(final Properties props) {
+ awsApiSignatureVersion = props
+ .getProperty(Keys.AWS_API_SIGNATURE_VERSION, AWS_API_V2);
+ if (awsApiSignatureVersion.equals(AWS_API_V4)) {
+ region = props.getProperty(Keys.REGION);
+ if (region == null) {
+ throw new IllegalArgumentException(
+ JGitText.get().missingAwsRegion);
+ }
+ } else if (awsApiSignatureVersion.equals(AWS_API_V2)) {
+ region = null;
+ } else {
+ throw new IllegalArgumentException(MessageFormat.format(
+ JGitText.get().invalidAwsApiSignatureVersion,
+ awsApiSignatureVersion));
+ }
+
domain = props.getProperty(Keys.DOMAIN, "s3.amazonaws.com"); //$NON-NLS-1$
publicKey = props.getProperty(Keys.ACCESS_KEY);
if (publicKey == null)
throw new IllegalArgumentException(JGitText.get().missingAccesskey);
- final String secret = props.getProperty(Keys.SECRET_KEY);
- if (secret == null)
+ final String secretKeyStr = props.getProperty(Keys.SECRET_KEY);
+ if (secretKeyStr == null) {
throw new IllegalArgumentException(JGitText.get().missingSecretkey);
- privateKey = new SecretKeySpec(Constants.encodeASCII(secret), HMAC);
+ }
+ secretKeySpec = new SecretKeySpec(Constants.encodeASCII(secretKeyStr), HMAC);
+ secretKey = secretKeyStr.toCharArray();
final String pacl = props.getProperty(Keys.ACL, "PRIVATE"); //$NON-NLS-1$
if (StringUtils.equalsIgnoreCase("PRIVATE", pacl)) //$NON-NLS-1$
@@ -258,7 +302,7 @@ public class AmazonS3 {
throws IOException {
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
final HttpURLConnection c = open("GET", bucket, key); //$NON-NLS-1$
- authorize(c);
+ authorize(c, Collections.emptyMap(), 0, null);
switch (HttpSupport.response(c)) {
case HttpURLConnection.HTTP_OK:
encryption.validate(c, X_AMZ_META);
@@ -339,7 +383,7 @@ public class AmazonS3 {
throws IOException {
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
final HttpURLConnection c = open("DELETE", bucket, key); //$NON-NLS-1$
- authorize(c);
+ authorize(c, Collections.emptyMap(), 0, null);
switch (HttpSupport.response(c)) {
case HttpURLConnection.HTTP_NO_CONTENT:
return;
@@ -385,13 +429,16 @@ public class AmazonS3 {
}
final String md5str = Base64.encodeBytes(newMD5().digest(data));
+ final String bodyHash = awsApiSignatureVersion.equals(AWS_API_V4)
+ ? AwsRequestSignerV4.calculateBodyHash(data)
+ : null;
final String lenstr = String.valueOf(data.length);
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$
c.setRequestProperty("Content-Length", lenstr); //$NON-NLS-1$
c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$
c.setRequestProperty(X_AMZ_ACL, acl);
- authorize(c);
+ authorize(c, Collections.emptyMap(), data.length, bodyHash);
c.setDoOutput(true);
c.setFixedLengthStreamingMode(data.length);
try (OutputStream os = c.getOutputStream()) {
@@ -466,6 +513,9 @@ public class AmazonS3 {
monitorTask = MessageFormat.format(JGitText.get().progressMonUploading, key);
final String md5str = Base64.encodeBytes(csum);
+ final String bodyHash = awsApiSignatureVersion.equals(AWS_API_V4)
+ ? AwsRequestSignerV4.calculateBodyHash(buf.toByteArray())
+ : null;
final long len = buf.length();
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$
@@ -473,7 +523,7 @@ public class AmazonS3 {
c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$
c.setRequestProperty(X_AMZ_ACL, acl);
encryption.request(c, X_AMZ_META);
- authorize(c);
+ authorize(c, Collections.emptyMap(), len, bodyHash);
c.setDoOutput(true);
monitor.beginTask(monitorTask, (int) (len / 1024));
try (OutputStream os = c.getOutputStream()) {
@@ -545,8 +595,13 @@ public class AmazonS3 {
urlstr.append('.');
urlstr.append(domain);
urlstr.append('/');
- if (key.length() > 0)
- HttpSupport.encode(urlstr, key);
+ if (key.length() > 0) {
+ if (awsApiSignatureVersion.equals(AWS_API_V2)) {
+ HttpSupport.encode(urlstr, key);
+ } else if (awsApiSignatureVersion.equals(AWS_API_V4)) {
+ urlstr.append(key);
+ }
+ }
if (!args.isEmpty()) {
final Iterator<Map.Entry<String, String>> i;
@@ -573,7 +628,18 @@ public class AmazonS3 {
return c;
}
- void authorize(HttpURLConnection c) throws IOException {
+ void authorize(HttpURLConnection httpURLConnection,
+ Map<String, String> queryParams, long contentLength,
+ final String bodyHash) throws IOException {
+ if (awsApiSignatureVersion.equals(AWS_API_V2)) {
+ authorizeV2(httpURLConnection);
+ } else if (awsApiSignatureVersion.equals(AWS_API_V4)) {
+ AwsRequestSignerV4.sign(httpURLConnection, queryParams, contentLength, bodyHash, AWS_S3_SERVICE_NAME,
+ region, publicKey, secretKey);
+ }
+ }
+
+ void authorizeV2(HttpURLConnection c) throws IOException {
final Map<String, List<String>> reqHdr = c.getRequestProperties();
final SortedMap<String, String> sigHdr = new TreeMap<>();
for (Map.Entry<String, List<String>> entry : reqHdr.entrySet()) {
@@ -610,7 +676,7 @@ public class AmazonS3 {
final String sec;
try {
final Mac m = Mac.getInstance(HMAC);
- m.init(privateKey);
+ m.init(secretKeySpec);
sec = Base64.encodeBytes(m.doFinal(s.toString().getBytes(UTF_8)));
} catch (NoSuchAlgorithmException e) {
throw new IOException(MessageFormat.format(JGitText.get().noHMACsupport, HMAC, e.getMessage()));
@@ -674,7 +740,7 @@ public class AmazonS3 {
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
final HttpURLConnection c = open("GET", bucket, "", args); //$NON-NLS-1$ //$NON-NLS-2$
- authorize(c);
+ authorize(c, args, 0, null);
switch (HttpSupport.response(c)) {
case HttpURLConnection.HTTP_OK:
truncated = false;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java
new file mode 100644
index 0000000000..6b3d39721a
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/AwsRequestSignerV4.java
@@ -0,0 +1,313 @@
+/*
+ * 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
+ */
+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);
+ }
+ }
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
index 23a73faf8c..663a3449e1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/util/HttpSupport.java
@@ -186,6 +186,33 @@ public class HttpSupport {
}
/**
+ * Translates the provided URL into application/x-www-form-urlencoded
+ * format.
+ *
+ * @param url
+ * The URL to translate.
+ * @param keepPathSlash
+ * Whether or not to keep "/" in the URL (i.e. don't translate
+ * them to "%2F").
+ *
+ * @return The translated URL.
+ * @since 5.13
+ */
+ public static String urlEncode(String url, boolean keepPathSlash) {
+ String encoded;
+ try {
+ encoded = URLEncoder.encode(url, UTF_8.name());
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(JGitText.get().couldNotURLEncodeToUTF8,
+ e);
+ }
+ if (keepPathSlash) {
+ encoded = encoded.replace("%2F", "/"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ return encoded;
+ }
+
+ /**
* Get the HTTP response code from the request.
* <p>
* Roughly the same as <code>c.getResponseCode()</code> but the