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 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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. }
  314. return "/" + encodedPath; //$NON-NLS-1$
  315. }
  316. private static byte[] hash(String s) {
  317. MessageDigest md = Constants.newMessageDigest();
  318. md.update(s.getBytes(UTF_8));
  319. return md.digest();
  320. }
  321. private static byte[] sign(String stringData, byte[] key) {
  322. try {
  323. byte[] data = stringData.getBytes(UTF_8);
  324. Mac mac = Mac.getInstance(HMACSHA256);
  325. mac.init(new SecretKeySpec(key, HMACSHA256));
  326. return mac.doFinal(data);
  327. } catch (Exception e) {
  328. throw new RuntimeException(MessageFormat.format(
  329. LfsServerText.get().failedToCalcSignature, e.getMessage()),
  330. e);
  331. }
  332. }
  333. private static String stringToSign(String scheme, String algorithm,
  334. String dateTime, String scope, String canonicalRequest) {
  335. return String.format("%s-%s\n%s\n%s\n%s", //$NON-NLS-1$
  336. scheme, algorithm, dateTime, scope,
  337. toHex(hash(canonicalRequest)));
  338. }
  339. private static String toHex(byte[] bytes) {
  340. StringBuilder builder = new StringBuilder(2 * bytes.length);
  341. for (byte b : bytes) {
  342. builder.append(HEX.charAt((b & 0xF0) >> 4));
  343. builder.append(HEX.charAt(b & 0xF));
  344. }
  345. return builder.toString();
  346. }
  347. private static String urlEncode(String url, boolean keepPathSlash) {
  348. String encoded;
  349. try {
  350. encoded = URLEncoder.encode(url, UTF_8.name());
  351. } catch (UnsupportedEncodingException e) {
  352. throw new RuntimeException(LfsServerText.get().unsupportedUtf8, e);
  353. }
  354. if (keepPathSlash) {
  355. encoded = encoded.replace("%2F", "/"); //$NON-NLS-1$ //$NON-NLS-2$
  356. }
  357. return encoded;
  358. }
  359. private static byte[] createSignature(S3Config bucketConfig,
  360. String dateTimeStamp, String dateStamp,
  361. String scope, String canonicalRequest) {
  362. String stringToSign = stringToSign(SCHEME, ALGORITHM, dateTimeStamp,
  363. scope, canonicalRequest);
  364. byte[] signature = (SCHEME + bucketConfig.getSecretKey())
  365. .getBytes(UTF_8);
  366. signature = sign(dateStamp, signature);
  367. signature = sign(bucketConfig.getRegion(), signature);
  368. signature = sign(S3, signature);
  369. signature = sign(TERMINATOR, signature);
  370. signature = sign(stringToSign, signature);
  371. return signature;
  372. }
  373. }