SignatureV4.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. <?php
  2. /**
  3. * Copyright 2010-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License").
  6. * You may not use this file except in compliance with the License.
  7. * A copy of the License is located at
  8. *
  9. * http://aws.amazon.com/apache2.0
  10. *
  11. * or in the "license" file accompanying this file. This file is distributed
  12. * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
  13. * express or implied. See the License for the specific language governing
  14. * permissions and limitations under the License.
  15. */
  16. namespace Aws\Common\Signature;
  17. use Aws\Common\Credentials\CredentialsInterface;
  18. use Aws\Common\Enum\DateFormat;
  19. use Aws\Common\HostNameUtils;
  20. use Guzzle\Http\Message\EntityEnclosingRequestInterface;
  21. use Guzzle\Http\Message\RequestInterface;
  22. use Guzzle\Http\Url;
  23. /**
  24. * Signature Version 4
  25. * @link http://docs.amazonwebservices.com/general/latest/gr/signature-version-4.html
  26. */
  27. class SignatureV4 extends AbstractSignature implements EndpointSignatureInterface
  28. {
  29. /**
  30. * @var string Cache of the default empty entity-body payload
  31. */
  32. const DEFAULT_PAYLOAD = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
  33. /**
  34. * @var string Explicitly set service name
  35. */
  36. protected $serviceName;
  37. /**
  38. * @var string Explicitly set region name
  39. */
  40. protected $regionName;
  41. /**
  42. * @var int Maximum number of hashes to cache
  43. */
  44. protected $maxCacheSize = 50;
  45. /**
  46. * @var array Cache of previously signed values
  47. */
  48. protected $hashCache = array();
  49. /**
  50. * @var int Size of the hash cache
  51. */
  52. protected $cacheSize = 0;
  53. /**
  54. * Set the service name instead of inferring it from a request URL
  55. *
  56. * @param string $service Name of the service used when signing
  57. *
  58. * @return self
  59. */
  60. public function setServiceName($service)
  61. {
  62. $this->serviceName = $service;
  63. return $this;
  64. }
  65. /**
  66. * Set the region name instead of inferring it from a request URL
  67. *
  68. * @param string $region Name of the region used when signing
  69. *
  70. * @return self
  71. */
  72. public function setRegionName($region)
  73. {
  74. $this->regionName = $region;
  75. return $this;
  76. }
  77. /**
  78. * Set the maximum number of computed hashes to cache
  79. *
  80. * @param int $maxCacheSize Maximum number of hashes to cache
  81. *
  82. * @return self
  83. */
  84. public function setMaxCacheSize($maxCacheSize)
  85. {
  86. $this->maxCacheSize = $maxCacheSize;
  87. return $this;
  88. }
  89. /**
  90. * {@inheritdoc}
  91. */
  92. public function signRequest(RequestInterface $request, CredentialsInterface $credentials)
  93. {
  94. // Refresh the cached timestamp
  95. $this->getTimestamp(true);
  96. $longDate = $this->getDateTime(DateFormat::ISO8601);
  97. $shortDate = $this->getDateTime(DateFormat::SHORT);
  98. // Remove any previously set Authorization headers so that
  99. // exponential backoff works correctly
  100. $request->removeHeader('Authorization');
  101. // Requires a x-amz-date header or Date
  102. if ($request->hasHeader('x-amz-date') || !$request->hasHeader('Date')) {
  103. $request->setHeader('x-amz-date', $longDate);
  104. } else {
  105. $request->setHeader('Date', $this->getDateTime(DateFormat::RFC1123));
  106. }
  107. // Add the security token if one is present
  108. if ($credentials->getSecurityToken()) {
  109. $request->setHeader('x-amz-security-token', $credentials->getSecurityToken());
  110. }
  111. // Parse the service and region or use one that is explicitly set
  112. $url = null;
  113. if (!$this->regionName || !$this->serviceName) {
  114. $url = Url::factory($request->getUrl());
  115. }
  116. if (!$region = $this->regionName) {
  117. $region = HostNameUtils::parseRegionName($url);
  118. }
  119. if (!$service = $this->serviceName) {
  120. $service = HostNameUtils::parseServiceName($url);
  121. }
  122. $credentialScope = "{$shortDate}/{$region}/{$service}/aws4_request";
  123. $signingContext = $this->createCanonicalRequest($request);
  124. $signingContext['string_to_sign'] = "AWS4-HMAC-SHA256\n{$longDate}\n{$credentialScope}\n"
  125. . hash('sha256', $signingContext['canonical_request']);
  126. // Calculate the signing key using a series of derived keys
  127. $signingKey = $this->getSigningKey($shortDate, $region, $service, $credentials->getSecretKey());
  128. $signature = hash_hmac('sha256', $signingContext['string_to_sign'], $signingKey);
  129. $request->setHeader('Authorization', "AWS4-HMAC-SHA256 "
  130. . "Credential={$credentials->getAccessKeyId()}/{$credentialScope}, "
  131. . "SignedHeaders={$signingContext['signed_headers']}, Signature={$signature}");
  132. // Add debug information to the request
  133. $request->getParams()->set('aws.signature', $signingContext);
  134. }
  135. /**
  136. * Create the canonical representation of a request
  137. *
  138. * @param RequestInterface $request Request to canonicalize
  139. *
  140. * @return array Returns an array of context information
  141. */
  142. private function createCanonicalRequest(RequestInterface $request)
  143. {
  144. // Normalize the path as required by SigV4 and ensure it's absolute
  145. $method = $request->getMethod();
  146. $canon = $method . "\n"
  147. . '/' . ltrim($request->getUrl(true)->normalizePath()->getPath(), '/') . "\n"
  148. . $this->getCanonicalizedQueryString($request) . "\n";
  149. // Create the canonical headers
  150. $headers = array();
  151. foreach ($request->getHeaders()->getAll() as $key => $values) {
  152. if ($key != 'User-Agent') {
  153. $key = strtolower($key);
  154. if (!isset($headers[$key])) {
  155. $headers[$key] = array();
  156. }
  157. foreach ($values as $value) {
  158. $headers[$key][] = preg_replace('/\s+/', ' ', trim($value));
  159. }
  160. }
  161. }
  162. // The headers must be sorted
  163. ksort($headers);
  164. // Continue to build the canonical request by adding headers
  165. foreach ($headers as $key => $values) {
  166. // Combine multi-value headers into a sorted comma separated list
  167. if (count($values) > 1) {
  168. sort($values);
  169. }
  170. $canon .= $key . ':' . implode(',', $values) . "\n";
  171. }
  172. // Create the signed headers
  173. $signedHeaders = implode(';', array_keys($headers));
  174. $canon .= "\n{$signedHeaders}\n";
  175. // Create the payload if this request has an entity body
  176. if ($request->hasHeader('x-amz-content-sha256')) {
  177. // Handle streaming operations (e.g. Glacier.UploadArchive)
  178. $canon .= $request->getHeader('x-amz-content-sha256');
  179. } elseif ($request instanceof EntityEnclosingRequestInterface) {
  180. $canon .= hash(
  181. 'sha256',
  182. $method == 'POST' && count($request->getPostFields())
  183. ? (string) $request->getPostFields() : (string) $request->getBody()
  184. );
  185. } else {
  186. $canon .= self::DEFAULT_PAYLOAD;
  187. }
  188. return array(
  189. 'canonical_request' => $canon,
  190. 'signed_headers' => $signedHeaders
  191. );
  192. }
  193. /**
  194. * Get a hash for a specific key and value. If the hash was previously
  195. * cached, return it
  196. *
  197. * @param string $shortDate Short date
  198. * @param string $region Region name
  199. * @param string $service Service name
  200. * @param string $secretKey Secret Access Key
  201. *
  202. * @return string
  203. */
  204. private function getSigningKey($shortDate, $region, $service, $secretKey)
  205. {
  206. $cacheKey = $shortDate . '_' . $region . '_' . $service . '_' . $secretKey;
  207. // Retrieve the hash form the cache or create it and add it to the cache
  208. if (!isset($this->hashCache[$cacheKey])) {
  209. // When the cache size reaches the max, then just clear the cache
  210. if (++$this->cacheSize > $this->maxCacheSize) {
  211. $this->hashCache = array();
  212. $this->cacheSize = 0;
  213. }
  214. $dateKey = hash_hmac('sha256', $shortDate, 'AWS4' . $secretKey, true);
  215. $regionKey = hash_hmac('sha256', $region, $dateKey, true);
  216. $serviceKey = hash_hmac('sha256', $service, $regionKey, true);
  217. $this->hashCache[$cacheKey] = hash_hmac('sha256', 'aws4_request', $serviceKey, true);
  218. }
  219. return $this->hashCache[$cacheKey];
  220. }
  221. }