SignerV4.java

  1. /*
  2.  * Copyright (C) 2015, Matthias Sohn <matthias.sohn@sap.com>
  3.  * Copyright (C) 2015, Sasa Zivkov <sasa.zivkov@sap.com>
  4.  * and other copyright owners as documented in the project's IP log.
  5.  *
  6.  * This program and the accompanying materials are made available
  7.  * under the terms of the Eclipse Distribution License v1.0 which
  8.  * accompanies this distribution, is reproduced below, and is
  9.  * available at http://www.eclipse.org/org/documents/edl-v10.php
  10.  *
  11.  * All rights reserved.
  12.  *
  13.  * Redistribution and use in source and binary forms, with or
  14.  * without modification, are permitted provided that the following
  15.  * conditions are met:
  16.  *
  17.  * - Redistributions of source code must retain the above copyright
  18.  *   notice, this list of conditions and the following disclaimer.
  19.  *
  20.  * - Redistributions in binary form must reproduce the above
  21.  *   copyright notice, this list of conditions and the following
  22.  *   disclaimer in the documentation and/or other materials provided
  23.  *   with the distribution.
  24.  *
  25.  * - Neither the name of the Eclipse Foundation, Inc. nor the
  26.  *   names of its contributors may be used to endorse or promote
  27.  *   products derived from this software without specific prior
  28.  *   written permission.
  29.  *
  30.  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  31.  * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  32.  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  33.  * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  34.  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  35.  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  36.  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  37.  * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  38.  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  39.  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  40.  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  41.  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  42.  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  43.  */
  44. package org.eclipse.jgit.lfs.server.s3;

  45. import static java.nio.charset.StandardCharsets.UTF_8;
  46. import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION;

  47. import java.io.UnsupportedEncodingException;
  48. import java.net.URL;
  49. import java.net.URLEncoder;
  50. import java.security.MessageDigest;
  51. import java.text.MessageFormat;
  52. import java.text.SimpleDateFormat;
  53. import java.util.ArrayList;
  54. import java.util.Collections;
  55. import java.util.Date;
  56. import java.util.Iterator;
  57. import java.util.List;
  58. import java.util.Locale;
  59. import java.util.Map;
  60. import java.util.SimpleTimeZone;
  61. import java.util.SortedMap;
  62. import java.util.TreeMap;

  63. import javax.crypto.Mac;
  64. import javax.crypto.spec.SecretKeySpec;

  65. import org.eclipse.jgit.lfs.lib.Constants;
  66. import org.eclipse.jgit.lfs.server.internal.LfsServerText;

  67. /**
  68.  * Signing support for Amazon AWS signing V4
  69.  * <p>
  70.  * See
  71.  * http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
  72.  */
  73. class SignerV4 {
  74.     static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD"; //$NON-NLS-1$

  75.     private static final String ALGORITHM = "HMAC-SHA256"; //$NON-NLS-1$
  76.     private static final String DATE_STRING_FORMAT = "yyyyMMdd"; //$NON-NLS-1$
  77.     private static final String HEX = "0123456789abcdef"; //$NON-NLS-1$
  78.     private static final String HMACSHA256 = "HmacSHA256"; //$NON-NLS-1$
  79.     private static final String ISO8601_BASIC_FORMAT = "yyyyMMdd'T'HHmmss'Z'"; //$NON-NLS-1$
  80.     private static final String S3 = "s3"; //$NON-NLS-1$
  81.     private static final String SCHEME = "AWS4"; //$NON-NLS-1$
  82.     private static final String TERMINATOR = "aws4_request"; //$NON-NLS-1$
  83.     private static final String UTC = "UTC"; //$NON-NLS-1$
  84.     private static final String X_AMZ_ALGORITHM = "X-Amz-Algorithm"; //$NON-NLS-1$
  85.     private static final String X_AMZ_CREDENTIAL = "X-Amz-Credential"; //$NON-NLS-1$
  86.     private static final String X_AMZ_DATE = "X-Amz-Date"; //$NON-NLS-1$
  87.     private static final String X_AMZ_SIGNATURE = "X-Amz-Signature"; //$NON-NLS-1$
  88.     private static final String X_AMZ_SIGNED_HEADERS = "X-Amz-SignedHeaders"; //$NON-NLS-1$

  89.     static final String X_AMZ_CONTENT_SHA256 = "x-amz-content-sha256"; //$NON-NLS-1$
  90.     static final String X_AMZ_EXPIRES = "X-Amz-Expires"; //$NON-NLS-1$
  91.     static final String X_AMZ_STORAGE_CLASS = "x-amz-storage-class"; //$NON-NLS-1$

  92.     /**
  93.      * Create an AWSV4 authorization for a request, suitable for embedding in
  94.      * query parameters.
  95.      *
  96.      * @param bucketConfig
  97.      *            configuration of S3 storage bucket this request should be
  98.      *            signed for
  99.      * @param url
  100.      *            HTTP request URL
  101.      * @param httpMethod
  102.      *            HTTP method
  103.      * @param headers
  104.      *            The HTTP request headers; 'Host' and 'X-Amz-Date' will be
  105.      *            added to this set.
  106.      * @param queryParameters
  107.      *            Any query parameters that will be added to the endpoint. The
  108.      *            parameters should be specified in canonical format.
  109.      * @param bodyHash
  110.      *            Pre-computed SHA256 hash of the request body content; this
  111.      *            value should also be set as the header 'X-Amz-Content-SHA256'
  112.      *            for non-streaming uploads.
  113.      * @return The computed authorization string for the request. This value
  114.      *         needs to be set as the header 'Authorization' on the subsequent
  115.      *         HTTP request.
  116.      */
  117.     static String createAuthorizationQuery(S3Config bucketConfig, URL url,
  118.             String httpMethod, Map<String, String> headers,
  119.             Map<String, String> queryParameters, String bodyHash) {
  120.         addHostHeader(url, headers);

  121.         queryParameters.put(X_AMZ_ALGORITHM, SCHEME + "-" + ALGORITHM); //$NON-NLS-1$

  122.         Date now = new Date();
  123.         String dateStamp = dateStamp(now);
  124.         String scope = scope(bucketConfig.getRegion(), dateStamp);
  125.         queryParameters.put(X_AMZ_CREDENTIAL,
  126.                 bucketConfig.getAccessKey() + "/" + scope); //$NON-NLS-1$

  127.         String dateTimeStampISO8601 = dateTimeStampISO8601(now);
  128.         queryParameters.put(X_AMZ_DATE, dateTimeStampISO8601);

  129.         String canonicalizedHeaderNames = canonicalizeHeaderNames(headers);
  130.         queryParameters.put(X_AMZ_SIGNED_HEADERS, canonicalizedHeaderNames);

  131.         String canonicalizedQueryParameters = canonicalizeQueryString(
  132.                 queryParameters);
  133.         String canonicalizedHeaders = canonicalizeHeaderString(headers);
  134.         String canonicalRequest = canonicalRequest(url, httpMethod,
  135.                 canonicalizedQueryParameters, canonicalizedHeaderNames,
  136.                 canonicalizedHeaders, bodyHash);
  137.         byte[] signature = createSignature(bucketConfig, dateTimeStampISO8601,
  138.                 dateStamp, scope, canonicalRequest);
  139.         queryParameters.put(X_AMZ_SIGNATURE, toHex(signature));

  140.         return formatAuthorizationQuery(queryParameters);
  141.     }

  142.     private static String formatAuthorizationQuery(
  143.             Map<String, String> queryParameters) {
  144.         StringBuilder s = new StringBuilder();
  145.         for (String key : queryParameters.keySet()) {
  146.             appendQuery(s, key, queryParameters.get(key));
  147.         }
  148.         return s.toString();
  149.     }

  150.     private static void appendQuery(StringBuilder s, String key,
  151.             String value) {
  152.         if (s.length() != 0) {
  153.             s.append("&"); //$NON-NLS-1$
  154.         }
  155.         s.append(key).append("=").append(value); //$NON-NLS-1$
  156.     }

  157.     /**
  158.      * Sign headers for given bucket, url and HTTP method and add signature in
  159.      * Authorization header.
  160.      *
  161.      * @param bucketConfig
  162.      *            configuration of S3 storage bucket this request should be
  163.      *            signed for
  164.      * @param url
  165.      *            HTTP request URL
  166.      * @param httpMethod
  167.      *            HTTP method
  168.      * @param headers
  169.      *            HTTP headers to sign
  170.      * @param bodyHash
  171.      *            Pre-computed SHA256 hash of the request body content; this
  172.      *            value should also be set as the header 'X-Amz-Content-SHA256'
  173.      *            for non-streaming uploads.
  174.      * @return HTTP headers signd by an Authorization header added to the
  175.      *         headers
  176.      */
  177.     static Map<String, String> createHeaderAuthorization(
  178.             S3Config bucketConfig, URL url, String httpMethod,
  179.             Map<String, String> headers, String bodyHash) {
  180.         addHostHeader(url, headers);

  181.         Date now = new Date();
  182.         String dateTimeStamp = dateTimeStampISO8601(now);
  183.         headers.put(X_AMZ_DATE, dateTimeStamp);

  184.         String canonicalizedHeaderNames = canonicalizeHeaderNames(headers);
  185.         String canonicalizedHeaders = canonicalizeHeaderString(headers);
  186.         String canonicalRequest = canonicalRequest(url, httpMethod, "", //$NON-NLS-1$
  187.                 canonicalizedHeaderNames, canonicalizedHeaders, bodyHash);
  188.         String dateStamp = dateStamp(now);
  189.         String scope = scope(bucketConfig.getRegion(), dateStamp);

  190.         byte[] signature = createSignature(bucketConfig, dateTimeStamp,
  191.                 dateStamp, scope, canonicalRequest);

  192.         headers.put(HDR_AUTHORIZATION, formatAuthorizationHeader(bucketConfig,
  193.                 canonicalizedHeaderNames, scope, signature)); // $NON-NLS-1$

  194.         return headers;
  195.     }

  196.     private static String formatAuthorizationHeader(
  197.             S3Config bucketConfig, String canonicalizedHeaderNames,
  198.             String scope, byte[] signature) {
  199.         StringBuilder s = new StringBuilder();
  200.         s.append(SCHEME).append("-").append(ALGORITHM).append(" "); //$NON-NLS-1$ //$NON-NLS-2$
  201.         s.append("Credential=").append(bucketConfig.getAccessKey()).append("/") //$NON-NLS-1$//$NON-NLS-2$
  202.                 .append(scope).append(","); //$NON-NLS-1$
  203.         s.append("SignedHeaders=").append(canonicalizedHeaderNames).append(","); //$NON-NLS-1$ //$NON-NLS-2$
  204.         s.append("Signature=").append(toHex(signature)); //$NON-NLS-1$
  205.         return s.toString();
  206.     }

  207.     private static void addHostHeader(URL url,
  208.             Map<String, String> headers) {
  209.         StringBuilder hostHeader = new StringBuilder(url.getHost());
  210.         int port = url.getPort();
  211.         if (port > -1) {
  212.             hostHeader.append(":").append(port); //$NON-NLS-1$
  213.         }
  214.         headers.put("Host", hostHeader.toString()); //$NON-NLS-1$
  215.     }

  216.     private static String canonicalizeHeaderNames(
  217.             Map<String, String> headers) {
  218.         List<String> sortedHeaders = new ArrayList<>();
  219.         sortedHeaders.addAll(headers.keySet());
  220.         Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);

  221.         StringBuilder buffer = new StringBuilder();
  222.         for (String header : sortedHeaders) {
  223.             if (buffer.length() > 0)
  224.                 buffer.append(";"); //$NON-NLS-1$
  225.             buffer.append(header.toLowerCase(Locale.ROOT));
  226.         }

  227.         return buffer.toString();
  228.     }

  229.     private static String canonicalizeHeaderString(
  230.             Map<String, String> headers) {
  231.         if (headers == null || headers.isEmpty()) {
  232.             return ""; //$NON-NLS-1$
  233.         }

  234.         List<String> sortedHeaders = new ArrayList<>();
  235.         sortedHeaders.addAll(headers.keySet());
  236.         Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);

  237.         StringBuilder buffer = new StringBuilder();
  238.         for (String key : sortedHeaders) {
  239.             buffer.append(
  240.                     key.toLowerCase(Locale.ROOT).replaceAll("\\s+", " ") + ":" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
  241.                     + headers.get(key).replaceAll("\\s+", " ")); //$NON-NLS-1$//$NON-NLS-2$
  242.             buffer.append("\n"); //$NON-NLS-1$
  243.         }

  244.         return buffer.toString();
  245.     }

  246.     private static String dateStamp(Date now) {
  247.         // TODO(ms) cache and reuse DateFormat instances
  248.         SimpleDateFormat dateStampFormat = new SimpleDateFormat(
  249.                 DATE_STRING_FORMAT);
  250.         dateStampFormat.setTimeZone(new SimpleTimeZone(0, UTC));
  251.         String dateStamp = dateStampFormat.format(now);
  252.         return dateStamp;
  253.     }

  254.     private static String dateTimeStampISO8601(Date now) {
  255.         // TODO(ms) cache and reuse DateFormat instances
  256.         SimpleDateFormat dateTimeFormat = new SimpleDateFormat(
  257.                 ISO8601_BASIC_FORMAT);
  258.         dateTimeFormat.setTimeZone(new SimpleTimeZone(0, UTC));
  259.         String dateTimeStamp = dateTimeFormat.format(now);
  260.         return dateTimeStamp;
  261.     }

  262.     private static String scope(String region, String dateStamp) {
  263.         String scope = String.format("%s/%s/%s/%s", dateStamp, region, S3, //$NON-NLS-1$
  264.                 TERMINATOR);
  265.         return scope;
  266.     }

  267.     private static String canonicalizeQueryString(
  268.             Map<String, String> parameters) {
  269.         if (parameters == null || parameters.isEmpty()) {
  270.             return ""; //$NON-NLS-1$
  271.         }

  272.         SortedMap<String, String> sorted = new TreeMap<>();

  273.         Iterator<Map.Entry<String, String>> pairs = parameters.entrySet()
  274.                 .iterator();
  275.         while (pairs.hasNext()) {
  276.             Map.Entry<String, String> pair = pairs.next();
  277.             String key = pair.getKey();
  278.             String value = pair.getValue();
  279.             sorted.put(urlEncode(key, false), urlEncode(value, false));
  280.         }

  281.         StringBuilder builder = new StringBuilder();
  282.         pairs = sorted.entrySet().iterator();
  283.         while (pairs.hasNext()) {
  284.             Map.Entry<String, String> pair = pairs.next();
  285.             builder.append(pair.getKey());
  286.             builder.append("="); //$NON-NLS-1$
  287.             builder.append(pair.getValue());
  288.             if (pairs.hasNext()) {
  289.                 builder.append("&"); //$NON-NLS-1$
  290.             }
  291.         }

  292.         return builder.toString();
  293.     }

  294.     private static String canonicalRequest(URL endpoint, String httpMethod,
  295.             String queryParameters, String canonicalizedHeaderNames,
  296.             String canonicalizedHeaders, String bodyHash) {
  297.         return String.format("%s\n%s\n%s\n%s\n%s\n%s", //$NON-NLS-1$
  298.                 httpMethod, canonicalizeResourcePath(endpoint),
  299.                 queryParameters, canonicalizedHeaders, canonicalizedHeaderNames,
  300.                 bodyHash);
  301.     }

  302.     private static String canonicalizeResourcePath(URL endpoint) {
  303.         if (endpoint == null) {
  304.             return "/"; //$NON-NLS-1$
  305.         }
  306.         String path = endpoint.getPath();
  307.         if (path == null || path.isEmpty()) {
  308.             return "/"; //$NON-NLS-1$
  309.         }

  310.         String encodedPath = urlEncode(path, true);
  311.         if (encodedPath.startsWith("/")) { //$NON-NLS-1$
  312.             return encodedPath;
  313.         } else {
  314.             return "/" + encodedPath; //$NON-NLS-1$
  315.         }
  316.     }

  317.     private static byte[] hash(String s) {
  318.         MessageDigest md = Constants.newMessageDigest();
  319.         md.update(s.getBytes(UTF_8));
  320.         return md.digest();
  321.     }

  322.     private static byte[] sign(String stringData, byte[] key) {
  323.         try {
  324.             byte[] data = stringData.getBytes(UTF_8);
  325.             Mac mac = Mac.getInstance(HMACSHA256);
  326.             mac.init(new SecretKeySpec(key, HMACSHA256));
  327.             return mac.doFinal(data);
  328.         } catch (Exception e) {
  329.             throw new RuntimeException(MessageFormat.format(
  330.                     LfsServerText.get().failedToCalcSignature, e.getMessage()),
  331.                     e);
  332.         }
  333.     }

  334.     private static String stringToSign(String scheme, String algorithm,
  335.             String dateTime, String scope, String canonicalRequest) {
  336.         return String.format("%s-%s\n%s\n%s\n%s", //$NON-NLS-1$
  337.                 scheme, algorithm, dateTime, scope,
  338.                 toHex(hash(canonicalRequest)));
  339.     }

  340.     private static String toHex(byte[] bytes) {
  341.         StringBuilder builder = new StringBuilder(2 * bytes.length);
  342.         for (byte b : bytes) {
  343.             builder.append(HEX.charAt((b & 0xF0) >> 4));
  344.             builder.append(HEX.charAt(b & 0xF));
  345.         }
  346.         return builder.toString();
  347.     }

  348.     private static String urlEncode(String url, boolean keepPathSlash) {
  349.         String encoded;
  350.         try {
  351.             encoded = URLEncoder.encode(url, UTF_8.name());
  352.         } catch (UnsupportedEncodingException e) {
  353.             throw new RuntimeException(LfsServerText.get().unsupportedUtf8, e);
  354.         }
  355.         if (keepPathSlash) {
  356.             encoded = encoded.replace("%2F", "/"); //$NON-NLS-1$ //$NON-NLS-2$
  357.         }
  358.         return encoded;
  359.     }

  360.     private static byte[] createSignature(S3Config bucketConfig,
  361.             String dateTimeStamp, String dateStamp,
  362.             String scope, String canonicalRequest) {
  363.         String stringToSign = stringToSign(SCHEME, ALGORITHM, dateTimeStamp,
  364.                 scope, canonicalRequest);

  365.         byte[] signature = (SCHEME + bucketConfig.getSecretKey())
  366.                 .getBytes(UTF_8);
  367.         signature = sign(dateStamp, signature);
  368.         signature = sign(bucketConfig.getRegion(), signature);
  369.         signature = sign(S3, signature);
  370.         signature = sign(TERMINATOR, signature);
  371.         signature = sign(stringToSign, signature);
  372.         return signature;
  373.     }
  374. }