Merge branch 'stable-5.13' into stable-6.0

* stable-5.13:
  Prepare 5.13.2-SNAPSHOT builds
  JGit v5.13.1.202206130422-r
  AmazonS3: Add support for AWS API signature version 4

Change-Id: Ibd663a1d874d1aac274abc3dd44354fd99f64c39
diff --git a/org.eclipse.jgit.test/tst-rsrc/jgit-s3-config.disabled.properties b/org.eclipse.jgit.test/tst-rsrc/jgit-s3-config.disabled.properties
index d540977..3f36282 100644
--- a/org.eclipse.jgit.test/tst-rsrc/jgit-s3-config.disabled.properties
+++ b/org.eclipse.jgit.test/tst-rsrc/jgit-s3-config.disabled.properties
@@ -40,6 +40,15 @@
 # * https://docs.aws.amazon.com/AmazonS3/latest/dev/manage-lifecycle-using-console.html
 #
 
+# AWS API signature version (defaults to 2)
+# aws.api.signature.version=4
+
+# AWS S3 Region Domain (defaults to s3.amazonaws.com)
+# domain: s3-us-east-2.amazonaws.com
+
+# AWS S3 Region (required if aws.api.signature.version=4, must match domain)
+# region: us-east-2
+
 # Test bucket name
 test.bucket=jgit.eclipse.org
 
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 ee97c26..56701cc 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 @@
 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
@@ -361,6 +363,7 @@
 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
@@ -458,6 +461,7 @@
 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}
@@ -739,6 +743,7 @@
 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 f7ebe4f..b8b1ae2 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 static JGitText get() {
 	/***/ 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;
@@ -389,6 +391,7 @@ public static JGitText get() {
 	/***/ public String inTheFuture;
 	/***/ public String invalidAdvertisementOf;
 	/***/ public String invalidAncestryLength;
+	/***/ public String invalidAwsApiSignatureVersion;
 	/***/ public String invalidBooleanValue;
 	/***/ public String invalidChannel;
 	/***/ public String invalidCommitParentNumber;
@@ -486,6 +489,7 @@ public static JGitText get() {
 	/***/ public String mismatchOffset;
 	/***/ public String mismatchCRC;
 	/***/ public String missingAccesskey;
+	/***/ public String missingAwsRegion;
 	/***/ public String missingConfigurationForKey;
 	/***/ public String missingCookieFile;
 	/***/ public String missingCRC;
@@ -767,6 +771,7 @@ public static JGitText get() {
 	/***/ 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 d56b5b3..81a70af 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 @@
 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 @@ private static MessageDigest newMD5() {
 		}
 	}
 
+	/** 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 @@ private static MessageDigest newMD5() {
 	/** 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 @@ interface Keys {
 		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 @@ interface Keys {
 	 * 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 @@ interface Keys {
 	 * # 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 @@ interface Keys {
 	 *            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 URLConnection get(String bucket, String key)
 			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 void delete(String bucket, String key)
 			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 void put(String bucket, String key, byte[] data)
 		}
 
 		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 @@ void putImpl(final String bucket, final String key,
 			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 @@ void putImpl(final String bucket, final String key,
 			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 @@ HttpURLConnection open(final String method, final String bucket,
 		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 @@ HttpURLConnection open(final String method, final String bucket,
 		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 @@ void authorize(HttpURLConnection c) throws IOException {
 		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 @@ void list() throws IOException {
 
 			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 0000000..6b3d397
--- /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 23a73fa..663a344 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 static void encode(StringBuilder urlstr, String key) {
 	}
 
 	/**
+	 * 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