PageRenderTime 21ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Signature/SignatureV4.php

https://gitlab.com/github-cloud-corp/aws-sdk-php
PHP | 339 lines | 255 code | 46 blank | 38 comment | 17 complexity | e6f5d2362ac1c028d2440f5bfff65fbb MD5 | raw file
  1. <?php
  2. namespace Aws\Signature;
  3. use Aws\Credentials\CredentialsInterface;
  4. use Aws\Exception\CouldNotCreateChecksumException;
  5. use GuzzleHttp\Psr7;
  6. use Psr\Http\Message\RequestInterface;
  7. /**
  8. * Signature Version 4
  9. * @link http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
  10. */
  11. class SignatureV4 implements SignatureInterface
  12. {
  13. use SignatureTrait;
  14. const ISO8601_BASIC = 'Ymd\THis\Z';
  15. /** @var string */
  16. private $service;
  17. /** @var string */
  18. private $region;
  19. /**
  20. * @param string $service Service name to use when signing
  21. * @param string $region Region name to use when signing
  22. */
  23. public function __construct($service, $region)
  24. {
  25. $this->service = $service;
  26. $this->region = $region;
  27. }
  28. public function signRequest(
  29. RequestInterface $request,
  30. CredentialsInterface $credentials
  31. ) {
  32. $ldt = gmdate(self::ISO8601_BASIC);
  33. $sdt = substr($ldt, 0, 8);
  34. $parsed = $this->parseRequest($request);
  35. $parsed['headers']['X-Amz-Date'] = [$ldt];
  36. if ($token = $credentials->getSecurityToken()) {
  37. $parsed['headers']['X-Amz-Security-Token'] = [$token];
  38. }
  39. $cs = $this->createScope($sdt, $this->region, $this->service);
  40. $payload = $this->getPayload($request);
  41. $context = $this->createContext($parsed, $payload);
  42. $toSign = $this->createStringToSign($ldt, $cs, $context['creq']);
  43. $signingKey = $this->getSigningKey(
  44. $sdt,
  45. $this->region,
  46. $this->service,
  47. $credentials->getSecretKey()
  48. );
  49. $signature = hash_hmac('sha256', $toSign, $signingKey);
  50. $parsed['headers']['Authorization'] = [
  51. "AWS4-HMAC-SHA256 "
  52. . "Credential={$credentials->getAccessKeyId()}/{$cs}, "
  53. . "SignedHeaders={$context['headers']}, Signature={$signature}"
  54. ];
  55. return $this->buildRequest($parsed);
  56. }
  57. public function presign(
  58. RequestInterface $request,
  59. CredentialsInterface $credentials,
  60. $expires
  61. ) {
  62. $parsed = $this->createPresignedRequest($request, $credentials);
  63. $payload = $this->getPresignedPayload($request);
  64. $httpDate = gmdate(self::ISO8601_BASIC, time());
  65. $shortDate = substr($httpDate, 0, 8);
  66. $scope = $this->createScope($shortDate, $this->region, $this->service);
  67. $credential = $credentials->getAccessKeyId() . '/' . $scope;
  68. $parsed['query']['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256';
  69. $parsed['query']['X-Amz-Credential'] = $credential;
  70. $parsed['query']['X-Amz-Date'] = gmdate('Ymd\THis\Z', time());
  71. $parsed['query']['X-Amz-SignedHeaders'] = 'host';
  72. $parsed['query']['X-Amz-Expires'] = $this->convertExpires($expires);
  73. $context = $this->createContext($parsed, $payload);
  74. $stringToSign = $this->createStringToSign($httpDate, $scope, $context['creq']);
  75. $key = $this->getSigningKey(
  76. $shortDate,
  77. $this->region,
  78. $this->service,
  79. $credentials->getSecretKey()
  80. );
  81. $parsed['query']['X-Amz-Signature'] = hash_hmac('sha256', $stringToSign, $key);
  82. return $this->buildRequest($parsed);
  83. }
  84. /**
  85. * Converts a POST request to a GET request by moving POST fields into the
  86. * query string.
  87. *
  88. * Useful for pre-signing query protocol requests.
  89. *
  90. * @param RequestInterface $request Request to clone
  91. *
  92. * @return RequestInterface
  93. * @throws \InvalidArgumentException if the method is not POST
  94. */
  95. public static function convertPostToGet(RequestInterface $request)
  96. {
  97. if ($request->getMethod() !== 'POST') {
  98. throw new \InvalidArgumentException('Expected a POST request but '
  99. . 'received a ' . $request->getMethod() . ' request.');
  100. }
  101. $sr = $request->withMethod('GET')
  102. ->withBody(Psr7\stream_for(''))
  103. ->withoutHeader('Content-Type')
  104. ->withoutHeader('Content-Length');
  105. // Move POST fields to the query if they are present
  106. if ($request->getHeaderLine('Content-Type') === 'application/x-www-form-urlencoded') {
  107. $body = (string) $request->getBody();
  108. $sr = $sr->withUri($sr->getUri()->withQuery($body));
  109. }
  110. return $sr;
  111. }
  112. protected function getPayload(RequestInterface $request)
  113. {
  114. // Calculate the request signature payload
  115. if ($request->hasHeader('X-Amz-Content-Sha256')) {
  116. // Handle streaming operations (e.g. Glacier.UploadArchive)
  117. return $request->getHeaderLine('X-Amz-Content-Sha256');
  118. }
  119. if (!$request->getBody()->isSeekable()) {
  120. throw new CouldNotCreateChecksumException('sha256');
  121. }
  122. try {
  123. return Psr7\hash($request->getBody(), 'sha256');
  124. } catch (\Exception $e) {
  125. throw new CouldNotCreateChecksumException('sha256', $e);
  126. }
  127. }
  128. protected function getPresignedPayload(RequestInterface $request)
  129. {
  130. return $this->getPayload($request);
  131. }
  132. protected function createCanonicalizedPath($path)
  133. {
  134. $doubleEncoded = rawurlencode(ltrim($path, '/'));
  135. return '/' . str_replace('%2F', '/', $doubleEncoded);
  136. }
  137. private function createStringToSign($longDate, $credentialScope, $creq)
  138. {
  139. $hash = hash('sha256', $creq);
  140. return "AWS4-HMAC-SHA256\n{$longDate}\n{$credentialScope}\n{$hash}";
  141. }
  142. private function createPresignedRequest(
  143. RequestInterface $request,
  144. CredentialsInterface $credentials
  145. ) {
  146. $parsedRequest = $this->parseRequest($request);
  147. // Make sure to handle temporary credentials
  148. if ($token = $credentials->getSecurityToken()) {
  149. $parsedRequest['headers']['X-Amz-Security-Token'] = [$token];
  150. }
  151. return $this->moveHeadersToQuery($parsedRequest);
  152. }
  153. /**
  154. * @param array $parsedRequest
  155. * @param string $payload Hash of the request payload
  156. * @return array Returns an array of context information
  157. */
  158. private function createContext(array $parsedRequest, $payload)
  159. {
  160. // The following headers are not signed because signing these headers
  161. // would potentially cause a signature mismatch when sending a request
  162. // through a proxy or if modified at the HTTP client level.
  163. static $blacklist = [
  164. 'cache-control' => true,
  165. 'content-type' => true,
  166. 'content-length' => true,
  167. 'expect' => true,
  168. 'max-forwards' => true,
  169. 'pragma' => true,
  170. 'range' => true,
  171. 'te' => true,
  172. 'if-match' => true,
  173. 'if-none-match' => true,
  174. 'if-modified-since' => true,
  175. 'if-unmodified-since' => true,
  176. 'if-range' => true,
  177. 'accept' => true,
  178. 'authorization' => true,
  179. 'proxy-authorization' => true,
  180. 'from' => true,
  181. 'referer' => true,
  182. 'user-agent' => true
  183. ];
  184. // Normalize the path as required by SigV4
  185. $canon = $parsedRequest['method'] . "\n"
  186. . $this->createCanonicalizedPath($parsedRequest['path']) . "\n"
  187. . $this->getCanonicalizedQuery($parsedRequest['query']) . "\n";
  188. // Case-insensitively aggregate all of the headers.
  189. $aggregate = [];
  190. foreach ($parsedRequest['headers'] as $key => $values) {
  191. $key = strtolower($key);
  192. if (!isset($blacklist[$key])) {
  193. foreach ($values as $v) {
  194. $aggregate[$key][] = $v;
  195. }
  196. }
  197. }
  198. ksort($aggregate);
  199. $canonHeaders = [];
  200. foreach ($aggregate as $k => $v) {
  201. if (count($v) > 0) {
  202. sort($v);
  203. }
  204. $canonHeaders[] = $k . ':' . preg_replace('/\s+/', ' ', implode(',', $v));
  205. }
  206. $signedHeadersString = implode(';', array_keys($aggregate));
  207. $canon .= implode("\n", $canonHeaders) . "\n\n"
  208. . $signedHeadersString . "\n"
  209. . $payload;
  210. return ['creq' => $canon, 'headers' => $signedHeadersString];
  211. }
  212. private function getCanonicalizedQuery(array $query)
  213. {
  214. unset($query['X-Amz-Signature']);
  215. if (!$query) {
  216. return '';
  217. }
  218. $qs = '';
  219. ksort($query);
  220. foreach ($query as $k => $v) {
  221. if (!is_array($v)) {
  222. $qs .= rawurlencode($k) . '=' . rawurlencode($v) . '&';
  223. } else {
  224. sort($v);
  225. foreach ($v as $value) {
  226. $qs .= rawurlencode($k) . '=' . rawurlencode($value) . '&';
  227. }
  228. }
  229. }
  230. return substr($qs, 0, -1);
  231. }
  232. private function convertExpires($expires)
  233. {
  234. if ($expires instanceof \DateTime) {
  235. $expires = $expires->getTimestamp();
  236. } elseif (!is_numeric($expires)) {
  237. $expires = strtotime($expires);
  238. }
  239. $duration = $expires - time();
  240. // Ensure that the duration of the signature is not longer than a week
  241. if ($duration > 604800) {
  242. throw new \InvalidArgumentException('The expiration date of a '
  243. . 'signature version 4 presigned URL must be less than one '
  244. . 'week');
  245. }
  246. return $duration;
  247. }
  248. private function moveHeadersToQuery(array $parsedRequest)
  249. {
  250. foreach ($parsedRequest['headers'] as $name => $header) {
  251. $lname = strtolower($name);
  252. if (substr($lname, 0, 5) == 'x-amz') {
  253. $parsedRequest['query'][$name] = $header;
  254. }
  255. if ($lname !== 'host') {
  256. unset($parsedRequest['headers'][$name]);
  257. }
  258. }
  259. return $parsedRequest;
  260. }
  261. private function parseRequest(RequestInterface $request)
  262. {
  263. // Clean up any previously set headers.
  264. /** @var RequestInterface $request */
  265. $request = $request
  266. ->withoutHeader('X-Amz-Date')
  267. ->withoutHeader('Date')
  268. ->withoutHeader('Authorization');
  269. $uri = $request->getUri();
  270. return [
  271. 'method' => $request->getMethod(),
  272. 'path' => $uri->getPath(),
  273. 'query' => Psr7\parse_query($uri->getQuery()),
  274. 'uri' => $uri,
  275. 'headers' => $request->getHeaders(),
  276. 'body' => $request->getBody(),
  277. 'version' => $request->getProtocolVersion()
  278. ];
  279. }
  280. private function buildRequest(array $req)
  281. {
  282. if ($req['query']) {
  283. $req['uri'] = $req['uri']->withQuery(Psr7\build_query($req['query']));
  284. }
  285. return new Psr7\Request(
  286. $req['method'],
  287. $req['uri'],
  288. $req['headers'],
  289. $req['body'],
  290. $req['version']
  291. );
  292. }
  293. }