You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SignerV4.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /*
  2. * Copyright (C) 2015, Matthias Sohn <matthias.sohn@sap.com>
  3. * Copyright (C) 2015, Sasa Zivkov <sasa.zivkov@sap.com> and others
  4. *
  5. * This program and the accompanying materials are made available under the
  6. * terms of the Eclipse Distribution License v. 1.0 which is available at
  7. * https://www.eclipse.org/org/documents/edl-v10.php.
  8. *
  9. * SPDX-License-Identifier: BSD-3-Clause
  10. */
  11. package org.eclipse.jgit.lfs.server.s3;
  12. import static java.nio.charset.StandardCharsets.UTF_8;
  13. import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION;
  14. import java.io.UnsupportedEncodingException;
  15. import java.net.URL;
  16. import java.net.URLEncoder;
  17. import java.security.MessageDigest;
  18. import java.text.MessageFormat;
  19. import java.text.SimpleDateFormat;
  20. import java.util.ArrayList;
  21. import java.util.Collections;
  22. import java.util.Date;
  23. import java.util.Iterator;
  24. import java.util.List;
  25. import java.util.Locale;
  26. import java.util.Map;
  27. import java.util.SimpleTimeZone;
  28. import java.util.SortedMap;
  29. import java.util.TreeMap;
  30. import javax.crypto.Mac;
  31. import javax.crypto.spec.SecretKeySpec;
  32. import org.eclipse.jgit.lfs.lib.Constants;
  33. import org.eclipse.jgit.lfs.server.internal.LfsServerText;
  34. /**
  35. * Signing support for Amazon AWS signing V4
  36. * <p>
  37. * See
  38. * http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
  39. */
  40. class SignerV4 {
  41. static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD"; //$NON-NLS-1$
  42. private static final String ALGORITHM = "HMAC-SHA256"; //$NON-NLS-1$
  43. private static final String DATE_STRING_FORMAT = "yyyyMMdd"; //$NON-NLS-1$
  44. private static final String HEX = "0123456789abcdef"; //$NON-NLS-1$
  45. private static final String HMACSHA256 = "HmacSHA256"; //$NON-NLS-1$
  46. private static final String ISO8601_BASIC_FORMAT = "yyyyMMdd'T'HHmmss'Z'"; //$NON-NLS-1$
  47. private static final String S3 = "s3"; //$NON-NLS-1$
  48. private static final String SCHEME = "AWS4"; //$NON-NLS-1$
  49. private static final String TERMINATOR = "aws4_request"; //$NON-NLS-1$
  50. private static final String UTC = "UTC"; //$NON-NLS-1$
  51. private static final String X_AMZ_ALGORITHM = "X-Amz-Algorithm"; //$NON-NLS-1$
  52. private static final String X_AMZ_CREDENTIAL = "X-Amz-Credential"; //$NON-NLS-1$
  53. private static final String X_AMZ_DATE = "X-Amz-Date"; //$NON-NLS-1$
  54. private static final String X_AMZ_SIGNATURE = "X-Amz-Signature"; //$NON-NLS-1$
  55. private static final String X_AMZ_SIGNED_HEADERS = "X-Amz-SignedHeaders"; //$NON-NLS-1$
  56. static final String X_AMZ_CONTENT_SHA256 = "x-amz-content-sha256"; //$NON-NLS-1$
  57. static final String X_AMZ_EXPIRES = "X-Amz-Expires"; //$NON-NLS-1$
  58. static final String X_AMZ_STORAGE_CLASS = "x-amz-storage-class"; //$NON-NLS-1$
  59. /**
  60. * Create an AWSV4 authorization for a request, suitable for embedding in
  61. * query parameters.
  62. *
  63. * @param bucketConfig
  64. * configuration of S3 storage bucket this request should be
  65. * signed for
  66. * @param url
  67. * HTTP request URL
  68. * @param httpMethod
  69. * HTTP method
  70. * @param headers
  71. * The HTTP request headers; 'Host' and 'X-Amz-Date' will be
  72. * added to this set.
  73. * @param queryParameters
  74. * Any query parameters that will be added to the endpoint. The
  75. * parameters should be specified in canonical format.
  76. * @param bodyHash
  77. * Pre-computed SHA256 hash of the request body content; this
  78. * value should also be set as the header 'X-Amz-Content-SHA256'
  79. * for non-streaming uploads.
  80. * @return The computed authorization string for the request. This value
  81. * needs to be set as the header 'Authorization' on the subsequent
  82. * HTTP request.
  83. */
  84. static String createAuthorizationQuery(S3Config bucketConfig, URL url,
  85. String httpMethod, Map<String, String> headers,
  86. Map<String, String> queryParameters, String bodyHash) {
  87. addHostHeader(url, headers);
  88. queryParameters.put(X_AMZ_ALGORITHM, SCHEME + "-" + ALGORITHM); //$NON-NLS-1$
  89. Date now = new Date();
  90. String dateStamp = dateStamp(now);
  91. String scope = scope(bucketConfig.getRegion(), dateStamp);
  92. queryParameters.put(X_AMZ_CREDENTIAL,
  93. bucketConfig.getAccessKey() + "/" + scope); //$NON-NLS-1$
  94. String dateTimeStampISO8601 = dateTimeStampISO8601(now);
  95. queryParameters.put(X_AMZ_DATE, dateTimeStampISO8601);
  96. String canonicalizedHeaderNames = canonicalizeHeaderNames(headers);
  97. queryParameters.put(X_AMZ_SIGNED_HEADERS, canonicalizedHeaderNames);
  98. String canonicalizedQueryParameters = canonicalizeQueryString(
  99. queryParameters);
  100. String canonicalizedHeaders = canonicalizeHeaderString(headers);
  101. String canonicalRequest = canonicalRequest(url, httpMethod,
  102. canonicalizedQueryParameters, canonicalizedHeaderNames,
  103. canonicalizedHeaders, bodyHash);
  104. byte[] signature = createSignature(bucketConfig, dateTimeStampISO8601,
  105. dateStamp, scope, canonicalRequest);
  106. queryParameters.put(X_AMZ_SIGNATURE, toHex(signature));
  107. return formatAuthorizationQuery(queryParameters);
  108. }
  109. private static String formatAuthorizationQuery(
  110. Map<String, String> queryParameters) {
  111. StringBuilder s = new StringBuilder();
  112. for (String key : queryParameters.keySet()) {
  113. appendQuery(s, key, queryParameters.get(key));
  114. }
  115. return s.toString();
  116. }
  117. private static void appendQuery(StringBuilder s, String key,
  118. String value) {
  119. if (s.length() != 0) {
  120. s.append("&"); //$NON-NLS-1$
  121. }
  122. s.append(key).append("=").append(value); //$NON-NLS-1$
  123. }
  124. /**
  125. * Sign headers for given bucket, url and HTTP method and add signature in
  126. * Authorization header.
  127. *
  128. * @param bucketConfig
  129. * configuration of S3 storage bucket this request should be
  130. * signed for
  131. * @param url
  132. * HTTP request URL
  133. * @param httpMethod
  134. * HTTP method
  135. * @param headers
  136. * HTTP headers to sign
  137. * @param bodyHash
  138. * Pre-computed SHA256 hash of the request body content; this
  139. * value should also be set as the header 'X-Amz-Content-SHA256'
  140. * for non-streaming uploads.
  141. * @return HTTP headers signd by an Authorization header added to the
  142. * headers
  143. */
  144. static Map<String, String> createHeaderAuthorization(
  145. S3Config bucketConfig, URL url, String httpMethod,
  146. Map<String, String> headers, String bodyHash) {
  147. addHostHeader(url, headers);
  148. Date now = new Date();
  149. String dateTimeStamp = dateTimeStampISO8601(now);
  150. headers.put(X_AMZ_DATE, dateTimeStamp);
  151. String canonicalizedHeaderNames = canonicalizeHeaderNames(headers);
  152. String canonicalizedHeaders = canonicalizeHeaderString(headers);
  153. String canonicalRequest = canonicalRequest(url, httpMethod, "", //$NON-NLS-1$
  154. canonicalizedHeaderNames, canonicalizedHeaders, bodyHash);
  155. String dateStamp = dateStamp(now);
  156. String scope = scope(bucketConfig.getRegion(), dateStamp);
  157. byte[] signature = createSignature(bucketConfig, dateTimeStamp,
  158. dateStamp, scope, canonicalRequest);
  159. headers.put(HDR_AUTHORIZATION, formatAuthorizationHeader(bucketConfig,
  160. canonicalizedHeaderNames, scope, signature)); // $NON-NLS-1$
  161. return headers;
  162. }
  163. private static String formatAuthorizationHeader(
  164. S3Config bucketConfig, String canonicalizedHeaderNames,
  165. String scope, byte[] signature) {
  166. StringBuilder s = new StringBuilder();
  167. s.append(SCHEME).append("-").append(ALGORITHM).append(" "); //$NON-NLS-1$ //$NON-NLS-2$
  168. s.append("Credential=").append(bucketConfig.getAccessKey()).append("/") //$NON-NLS-1$//$NON-NLS-2$
  169. .append(scope).append(","); //$NON-NLS-1$
  170. s.append("SignedHeaders=").append(canonicalizedHeaderNames).append(","); //$NON-NLS-1$ //$NON-NLS-2$
  171. s.append("Signature=").append(toHex(signature)); //$NON-NLS-1$
  172. return s.toString();
  173. }
  174. private static void addHostHeader(URL url,
  175. Map<String, String> headers) {
  176. StringBuilder hostHeader = new StringBuilder(url.getHost());
  177. int port = url.getPort();
  178. if (port > -1) {
  179. hostHeader.append(":").append(port); //$NON-NLS-1$
  180. }
  181. headers.put("Host", hostHeader.toString()); //$NON-NLS-1$
  182. }
  183. private static String canonicalizeHeaderNames(
  184. Map<String, String> headers) {
  185. List<String> sortedHeaders = new ArrayList<>();
  186. sortedHeaders.addAll(headers.keySet());
  187. Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);
  188. StringBuilder buffer = new StringBuilder();
  189. for (String header : sortedHeaders) {
  190. if (buffer.length() > 0)
  191. buffer.append(";"); //$NON-NLS-1$
  192. buffer.append(header.toLowerCase(Locale.ROOT));
  193. }
  194. return buffer.toString();
  195. }
  196. private static String canonicalizeHeaderString(
  197. Map<String, String> headers) {
  198. if (headers == null || headers.isEmpty()) {
  199. return ""; //$NON-NLS-1$
  200. }
  201. List<String> sortedHeaders = new ArrayList<>();
  202. sortedHeaders.addAll(headers.keySet());
  203. Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);
  204. StringBuilder buffer = new StringBuilder();
  205. for (String key : sortedHeaders) {
  206. buffer.append(
  207. key.toLowerCase(Locale.ROOT).replaceAll("\\s+", " ") + ":" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
  208. + headers.get(key).replaceAll("\\s+", " ")); //$NON-NLS-1$//$NON-NLS-2$
  209. buffer.append("\n"); //$NON-NLS-1$
  210. }
  211. return buffer.toString();
  212. }
  213. private static String dateStamp(Date now) {
  214. // TODO(ms) cache and reuse DateFormat instances
  215. SimpleDateFormat dateStampFormat = new SimpleDateFormat(
  216. DATE_STRING_FORMAT);
  217. dateStampFormat.setTimeZone(new SimpleTimeZone(0, UTC));
  218. String dateStamp = dateStampFormat.format(now);
  219. return dateStamp;
  220. }
  221. private static String dateTimeStampISO8601(Date now) {
  222. // TODO(ms) cache and reuse DateFormat instances
  223. SimpleDateFormat dateTimeFormat = new SimpleDateFormat(
  224. ISO8601_BASIC_FORMAT);
  225. dateTimeFormat.setTimeZone(new SimpleTimeZone(0, UTC));
  226. String dateTimeStamp = dateTimeFormat.format(now);
  227. return dateTimeStamp;
  228. }
  229. private static String scope(String region, String dateStamp) {
  230. String scope = String.format("%s/%s/%s/%s", dateStamp, region, S3, //$NON-NLS-1$
  231. TERMINATOR);
  232. return scope;
  233. }
  234. private static String canonicalizeQueryString(
  235. Map<String, String> parameters) {
  236. if (parameters == null || parameters.isEmpty()) {
  237. return ""; //$NON-NLS-1$
  238. }
  239. SortedMap<String, String> sorted = new TreeMap<>();
  240. Iterator<Map.Entry<String, String>> pairs = parameters.entrySet()
  241. .iterator();
  242. while (pairs.hasNext()) {
  243. Map.Entry<String, String> pair = pairs.next();
  244. String key = pair.getKey();
  245. String value = pair.getValue();
  246. sorted.put(urlEncode(key, false), urlEncode(value, false));
  247. }
  248. StringBuilder builder = new StringBuilder();
  249. pairs = sorted.entrySet().iterator();
  250. while (pairs.hasNext()) {
  251. Map.Entry<String, String> pair = pairs.next();
  252. builder.append(pair.getKey());
  253. builder.append("="); //$NON-NLS-1$
  254. builder.append(pair.getValue());
  255. if (pairs.hasNext()) {
  256. builder.append("&"); //$NON-NLS-1$
  257. }
  258. }
  259. return builder.toString();
  260. }
  261. private static String canonicalRequest(URL endpoint, String httpMethod,
  262. String queryParameters, String canonicalizedHeaderNames,
  263. String canonicalizedHeaders, String bodyHash) {
  264. return String.format("%s\n%s\n%s\n%s\n%s\n%s", //$NON-NLS-1$
  265. httpMethod, canonicalizeResourcePath(endpoint),
  266. queryParameters, canonicalizedHeaders, canonicalizedHeaderNames,
  267. bodyHash);
  268. }
  269. private static String canonicalizeResourcePath(URL endpoint) {
  270. if (endpoint == null) {
  271. return "/"; //$NON-NLS-1$
  272. }
  273. String path = endpoint.getPath();
  274. if (path == null || path.isEmpty()) {
  275. return "/"; //$NON-NLS-1$
  276. }
  277. String encodedPath = urlEncode(path, true);
  278. if (encodedPath.startsWith("/")) { //$NON-NLS-1$
  279. return encodedPath;
  280. }
  281. return "/" + encodedPath; //$NON-NLS-1$
  282. }
  283. private static byte[] hash(String s) {
  284. MessageDigest md = Constants.newMessageDigest();
  285. md.update(s.getBytes(UTF_8));
  286. return md.digest();
  287. }
  288. private static byte[] sign(String stringData, byte[] key) {
  289. try {
  290. byte[] data = stringData.getBytes(UTF_8);
  291. Mac mac = Mac.getInstance(HMACSHA256);
  292. mac.init(new SecretKeySpec(key, HMACSHA256));
  293. return mac.doFinal(data);
  294. } catch (Exception e) {
  295. throw new RuntimeException(MessageFormat.format(
  296. LfsServerText.get().failedToCalcSignature, e.getMessage()),
  297. e);
  298. }
  299. }
  300. private static String stringToSign(String scheme, String algorithm,
  301. String dateTime, String scope, String canonicalRequest) {
  302. return String.format("%s-%s\n%s\n%s\n%s", //$NON-NLS-1$
  303. scheme, algorithm, dateTime, scope,
  304. toHex(hash(canonicalRequest)));
  305. }
  306. private static String toHex(byte[] bytes) {
  307. StringBuilder builder = new StringBuilder(2 * bytes.length);
  308. for (byte b : bytes) {
  309. builder.append(HEX.charAt((b & 0xF0) >> 4));
  310. builder.append(HEX.charAt(b & 0xF));
  311. }
  312. return builder.toString();
  313. }
  314. private static String urlEncode(String url, boolean keepPathSlash) {
  315. String encoded;
  316. try {
  317. encoded = URLEncoder.encode(url, UTF_8.name());
  318. } catch (UnsupportedEncodingException e) {
  319. throw new RuntimeException(LfsServerText.get().unsupportedUtf8, e);
  320. }
  321. if (keepPathSlash) {
  322. encoded = encoded.replace("%2F", "/"); //$NON-NLS-1$ //$NON-NLS-2$
  323. }
  324. return encoded;
  325. }
  326. private static byte[] createSignature(S3Config bucketConfig,
  327. String dateTimeStamp, String dateStamp,
  328. String scope, String canonicalRequest) {
  329. String stringToSign = stringToSign(SCHEME, ALGORITHM, dateTimeStamp,
  330. scope, canonicalRequest);
  331. byte[] signature = (SCHEME + bucketConfig.getSecretKey())
  332. .getBytes(UTF_8);
  333. signature = sign(dateStamp, signature);
  334. signature = sign(bucketConfig.getRegion(), signature);
  335. signature = sign(S3, signature);
  336. signature = sign(TERMINATOR, signature);
  337. signature = sign(stringToSign, signature);
  338. return signature;
  339. }
  340. }