PluginProbe ʕ •ᴥ•ʔ
Transferito: WP Migration / trunk
Transferito: WP Migration vtrunk
trunk 11.4.0 12.0.0 13.1.0 14.0.0 14.0.11 14.0.7 14.1.0 14.1.1 14.1.2 14.1.3 14.1.4
transferito / vendor / aws / aws-sdk-php / src / Signature / SignatureV4.php
transferito / vendor / aws / aws-sdk-php / src / Signature Last commit date
AnonymousSignature.php 11 months ago S3ExpressSignature.php 11 months ago S3SignatureV4.php 11 months ago SignatureInterface.php 11 months ago SignatureProvider.php 11 months ago SignatureTrait.php 11 months ago SignatureV4.php 11 months ago
SignatureV4.php
577 lines
1 <?php
2 namespace Aws\Signature;
3
4 use Aws\Credentials\CredentialsInterface;
5 use AWS\CRT\Auth\Signable;
6 use AWS\CRT\Auth\SignatureType;
7 use AWS\CRT\Auth\Signing;
8 use AWS\CRT\Auth\SigningAlgorithm;
9 use AWS\CRT\Auth\SigningConfigAWS;
10 use AWS\CRT\Auth\StaticCredentialsProvider;
11 use AWS\CRT\HTTP\Request;
12 use Aws\Exception\CommonRuntimeException;
13 use Aws\Exception\CouldNotCreateChecksumException;
14 use GuzzleHttp\Psr7;
15 use Psr\Http\Message\RequestInterface;
16
17 /**
18 * Signature Version 4
19 * @link http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
20 */
21 class SignatureV4 implements SignatureInterface
22 {
23 use SignatureTrait;
24 const ISO8601_BASIC = 'Ymd\THis\Z';
25 const UNSIGNED_PAYLOAD = 'UNSIGNED-PAYLOAD';
26 const AMZ_CONTENT_SHA256_HEADER = 'X-Amz-Content-Sha256';
27
28 /** @var string */
29 private $service;
30
31 /** @var string */
32 protected $region;
33
34 /** @var bool */
35 private $unsigned;
36
37 /** @var bool */
38 private $useV4a;
39
40 /**
41 * The following headers are not signed because signing these headers
42 * would potentially cause a signature mismatch when sending a request
43 * through a proxy or if modified at the HTTP client level.
44 *
45 * @return array
46 */
47 protected function getHeaderBlacklist()
48 {
49 return [
50 'cache-control' => true,
51 'content-type' => true,
52 'content-length' => true,
53 'expect' => true,
54 'max-forwards' => true,
55 'pragma' => true,
56 'range' => true,
57 'te' => true,
58 'if-match' => true,
59 'if-none-match' => true,
60 'if-modified-since' => true,
61 'if-unmodified-since' => true,
62 'if-range' => true,
63 'accept' => true,
64 'authorization' => true,
65 'proxy-authorization' => true,
66 'from' => true,
67 'referer' => true,
68 'user-agent' => true,
69 'X-Amz-User-Agent' => true,
70 'x-amzn-trace-id' => true,
71 'aws-sdk-invocation-id' => true,
72 'aws-sdk-retry' => true,
73 ];
74 }
75
76 /**
77 * @param string $service Service name to use when signing
78 * @param string $region Region name to use when signing
79 * @param array $options Array of configuration options used when signing
80 * - unsigned-body: Flag to make request have unsigned payload.
81 * Unsigned body is used primarily for streaming requests.
82 */
83 public function __construct($service, $region, array $options = [])
84 {
85 $this->service = $service;
86 $this->region = $region;
87 $this->unsigned = isset($options['unsigned-body']) ? $options['unsigned-body'] : false;
88 $this->useV4a = isset($options['use_v4a']) && $options['use_v4a'] === true;
89 }
90
91 /**
92 * {@inheritdoc}
93 */
94 public function signRequest(
95 RequestInterface $request,
96 CredentialsInterface $credentials,
97 $signingService = null
98 ) {
99 $ldt = gmdate(self::ISO8601_BASIC);
100 $sdt = substr($ldt, 0, 8);
101 $parsed = $this->parseRequest($request);
102 $parsed['headers']['X-Amz-Date'] = [$ldt];
103
104 if ($token = $credentials->getSecurityToken()) {
105 $parsed['headers']['X-Amz-Security-Token'] = [$token];
106 }
107 $service = isset($signingService) ? $signingService : $this->service;
108
109 if ($this->useV4a) {
110 return $this->signWithV4a($credentials, $request, $service);
111 }
112
113 $cs = $this->createScope($sdt, $this->region, $service);
114 $payload = $this->getPayload($request);
115
116 if ($payload == self::UNSIGNED_PAYLOAD) {
117 $parsed['headers'][self::AMZ_CONTENT_SHA256_HEADER] = [$payload];
118 }
119
120 $context = $this->createContext($parsed, $payload);
121 $toSign = $this->createStringToSign($ldt, $cs, $context['creq']);
122 $signingKey = $this->getSigningKey(
123 $sdt,
124 $this->region,
125 $service,
126 $credentials->getSecretKey()
127 );
128 $signature = hash_hmac('sha256', $toSign, $signingKey);
129 $parsed['headers']['Authorization'] = [
130 "AWS4-HMAC-SHA256 "
131 . "Credential={$credentials->getAccessKeyId()}/{$cs}, "
132 . "SignedHeaders={$context['headers']}, Signature={$signature}"
133 ];
134
135 return $this->buildRequest($parsed);
136 }
137
138 /**
139 * Get the headers that were used to pre-sign the request.
140 * Used for the X-Amz-SignedHeaders header.
141 *
142 * @param array $headers
143 * @return array
144 */
145 private function getPresignHeaders(array $headers)
146 {
147 $presignHeaders = [];
148 $blacklist = $this->getHeaderBlacklist();
149 foreach ($headers as $name => $value) {
150 $lName = strtolower($name);
151 if (!isset($blacklist[$lName])
152 && $name !== self::AMZ_CONTENT_SHA256_HEADER
153 ) {
154 $presignHeaders[] = $lName;
155 }
156 }
157 return $presignHeaders;
158 }
159
160 /**
161 * {@inheritdoc}
162 */
163 public function presign(
164 RequestInterface $request,
165 CredentialsInterface $credentials,
166 $expires,
167 array $options = []
168 ) {
169 $startTimestamp = isset($options['start_time'])
170 ? $this->convertToTimestamp($options['start_time'], null)
171 : time();
172 $expiresTimestamp = $this->convertToTimestamp($expires, $startTimestamp);
173
174 if ($this->useV4a) {
175 return $this->presignWithV4a(
176 $request,
177 $credentials,
178 $this->convertExpires($expiresTimestamp, $startTimestamp)
179 );
180 }
181
182 $parsed = $this->createPresignedRequest($request, $credentials);
183
184 $payload = $this->getPresignedPayload($request);
185 $httpDate = gmdate(self::ISO8601_BASIC, $startTimestamp);
186 $shortDate = substr($httpDate, 0, 8);
187 $scope = $this->createScope($shortDate, $this->region, $this->service);
188 $credential = $credentials->getAccessKeyId() . '/' . $scope;
189 if ($credentials->getSecurityToken()) {
190 unset($parsed['headers']['X-Amz-Security-Token']);
191 }
192 $parsed['query']['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256';
193 $parsed['query']['X-Amz-Credential'] = $credential;
194 $parsed['query']['X-Amz-Date'] = gmdate('Ymd\THis\Z', $startTimestamp);
195 $parsed['query']['X-Amz-SignedHeaders'] = implode(';', $this->getPresignHeaders($parsed['headers']));
196 $parsed['query']['X-Amz-Expires'] = $this->convertExpires($expiresTimestamp, $startTimestamp);
197 $context = $this->createContext($parsed, $payload);
198 $stringToSign = $this->createStringToSign($httpDate, $scope, $context['creq']);
199 $key = $this->getSigningKey(
200 $shortDate,
201 $this->region,
202 $this->service,
203 $credentials->getSecretKey()
204 );
205 $parsed['query']['X-Amz-Signature'] = hash_hmac('sha256', $stringToSign, $key);
206
207 return $this->buildRequest($parsed);
208 }
209
210 /**
211 * Converts a POST request to a GET request by moving POST fields into the
212 * query string.
213 *
214 * Useful for pre-signing query protocol requests.
215 *
216 * @param RequestInterface $request Request to clone
217 *
218 * @return RequestInterface
219 * @throws \InvalidArgumentException if the method is not POST
220 */
221 public static function convertPostToGet(RequestInterface $request, $additionalQueryParams = "")
222 {
223 if ($request->getMethod() !== 'POST') {
224 throw new \InvalidArgumentException('Expected a POST request but '
225 . 'received a ' . $request->getMethod() . ' request.');
226 }
227
228 $sr = $request->withMethod('GET')
229 ->withBody(Psr7\Utils::streamFor(''))
230 ->withoutHeader('Content-Type')
231 ->withoutHeader('Content-Length');
232
233 // Move POST fields to the query if they are present
234 if ($request->getHeaderLine('Content-Type') === 'application/x-www-form-urlencoded') {
235 $body = (string) $request->getBody() . $additionalQueryParams;
236 $sr = $sr->withUri($sr->getUri()->withQuery($body));
237 }
238
239 return $sr;
240 }
241
242 protected function getPayload(RequestInterface $request)
243 {
244 if ($this->unsigned && $request->getUri()->getScheme() == 'https') {
245 return self::UNSIGNED_PAYLOAD;
246 }
247 // Calculate the request signature payload
248 if ($request->hasHeader(self::AMZ_CONTENT_SHA256_HEADER)) {
249 // Handle streaming operations (e.g. Glacier.UploadArchive)
250 return $request->getHeaderLine(self::AMZ_CONTENT_SHA256_HEADER);
251 }
252
253 if (!$request->getBody()->isSeekable()) {
254 throw new CouldNotCreateChecksumException('sha256');
255 }
256
257 try {
258 return Psr7\Utils::hash($request->getBody(), 'sha256');
259 } catch (\Exception $e) {
260 throw new CouldNotCreateChecksumException('sha256', $e);
261 }
262 }
263
264 protected function getPresignedPayload(RequestInterface $request)
265 {
266 return $this->getPayload($request);
267 }
268
269 protected function createCanonicalizedPath($path)
270 {
271 $doubleEncoded = rawurlencode(ltrim($path, '/'));
272
273 return '/' . str_replace('%2F', '/', $doubleEncoded);
274 }
275
276 private function createStringToSign($longDate, $credentialScope, $creq)
277 {
278 $hash = hash('sha256', $creq);
279
280 return "AWS4-HMAC-SHA256\n{$longDate}\n{$credentialScope}\n{$hash}";
281 }
282
283 private function createPresignedRequest(
284 RequestInterface $request,
285 CredentialsInterface $credentials
286 ) {
287 $parsedRequest = $this->parseRequest($request);
288
289 // Make sure to handle temporary credentials
290 if ($token = $credentials->getSecurityToken()) {
291 $parsedRequest['headers']['X-Amz-Security-Token'] = [$token];
292 }
293
294 return $this->moveHeadersToQuery($parsedRequest);
295 }
296
297 /**
298 * @param array $parsedRequest
299 * @param string $payload Hash of the request payload
300 * @return array Returns an array of context information
301 */
302 private function createContext(array $parsedRequest, $payload)
303 {
304 $blacklist = $this->getHeaderBlacklist();
305
306 // Normalize the path as required by SigV4
307 $canon = $parsedRequest['method'] . "\n"
308 . $this->createCanonicalizedPath($parsedRequest['path']) . "\n"
309 . $this->getCanonicalizedQuery($parsedRequest['query']) . "\n";
310
311 // Case-insensitively aggregate all of the headers.
312 $aggregate = [];
313 foreach ($parsedRequest['headers'] as $key => $values) {
314 $key = strtolower($key);
315 if (!isset($blacklist[$key])) {
316 foreach ($values as $v) {
317 $aggregate[$key][] = $v;
318 }
319 }
320 }
321
322 ksort($aggregate);
323 $canonHeaders = [];
324 foreach ($aggregate as $k => $v) {
325 if (count($v) > 0) {
326 sort($v);
327 }
328 $canonHeaders[] = $k . ':' . preg_replace('/\s+/', ' ', implode(',', $v));
329 }
330
331 $signedHeadersString = implode(';', array_keys($aggregate));
332 $canon .= implode("\n", $canonHeaders) . "\n\n"
333 . $signedHeadersString . "\n"
334 . $payload;
335
336 return ['creq' => $canon, 'headers' => $signedHeadersString];
337 }
338
339 private function getCanonicalizedQuery(array $query)
340 {
341 unset($query['X-Amz-Signature']);
342
343 if (!$query) {
344 return '';
345 }
346
347 $qs = '';
348 ksort($query);
349 foreach ($query as $k => $v) {
350 if (!is_array($v)) {
351 $qs .= rawurlencode($k) . '=' . rawurlencode($v !== null ? $v : '') . '&';
352 } else {
353 sort($v);
354 foreach ($v as $value) {
355 $qs .= rawurlencode($k) . '=' . rawurlencode($value !== null ? $value : '') . '&';
356 }
357 }
358 }
359
360 return substr($qs, 0, -1);
361 }
362
363 private function convertToTimestamp($dateValue, $relativeTimeBase = null)
364 {
365 if ($dateValue instanceof \DateTimeInterface) {
366 $timestamp = $dateValue->getTimestamp();
367 } elseif (!is_numeric($dateValue)) {
368 $timestamp = strtotime($dateValue,
369 $relativeTimeBase === null ? time() : $relativeTimeBase
370 );
371 } else {
372 $timestamp = $dateValue;
373 }
374
375 return $timestamp;
376 }
377
378 private function convertExpires($expiresTimestamp, $startTimestamp)
379 {
380 $duration = $expiresTimestamp - $startTimestamp;
381
382 // Ensure that the duration of the signature is not longer than a week
383 if ($duration > 604800) {
384 throw new \InvalidArgumentException('The expiration date of a '
385 . 'signature version 4 presigned URL must be less than one '
386 . 'week');
387 }
388
389 return $duration;
390 }
391
392 private function moveHeadersToQuery(array $parsedRequest)
393 {
394 //x-amz-user-agent shouldn't be put in a query param
395 unset($parsedRequest['headers']['X-Amz-User-Agent']);
396
397 foreach ($parsedRequest['headers'] as $name => $header) {
398 $lname = strtolower($name);
399 if (substr($lname, 0, 5) == 'x-amz') {
400 $parsedRequest['query'][$name] = $header;
401 }
402 $blacklist = $this->getHeaderBlacklist();
403 if (isset($blacklist[$lname])
404 || $lname === strtolower(self::AMZ_CONTENT_SHA256_HEADER)
405 ) {
406 unset($parsedRequest['headers'][$name]);
407 }
408 }
409
410 return $parsedRequest;
411 }
412
413 private function parseRequest(RequestInterface $request)
414 {
415 // Clean up any previously set headers.
416 /** @var RequestInterface $request */
417 $request = $request
418 ->withoutHeader('X-Amz-Date')
419 ->withoutHeader('Date')
420 ->withoutHeader('Authorization');
421 $uri = $request->getUri();
422
423 return [
424 'method' => $request->getMethod(),
425 'path' => $uri->getPath(),
426 'query' => Psr7\Query::parse($uri->getQuery()),
427 'uri' => $uri,
428 'headers' => $request->getHeaders(),
429 'body' => $request->getBody(),
430 'version' => $request->getProtocolVersion()
431 ];
432 }
433
434 private function buildRequest(array $req)
435 {
436 if ($req['query']) {
437 $req['uri'] = $req['uri']->withQuery(Psr7\Query::build($req['query']));
438 }
439
440 return new Psr7\Request(
441 $req['method'],
442 $req['uri'],
443 $req['headers'],
444 $req['body'],
445 $req['version']
446 );
447 }
448
449 private function verifyCRTLoaded()
450 {
451 if (!extension_loaded('awscrt')) {
452 throw new CommonRuntimeException(
453 "AWS Common Runtime for PHP is required to use Signature V4A"
454 . ". Please install it using the instructions found at"
455 . " https://github.com/aws/aws-sdk-php/blob/master/CRT_INSTRUCTIONS.md"
456 );
457 }
458 }
459
460 private function createCRTStaticCredentialsProvider($credentials)
461 {
462 return new StaticCredentialsProvider([
463 'access_key_id' => $credentials->getAccessKeyId(),
464 'secret_access_key' => $credentials->getSecretKey(),
465 'session_token' => $credentials->getSecurityToken(),
466 ]);
467 }
468
469 private function removeIllegalV4aHeaders(&$request)
470 {
471 $illegalV4aHeaders = [
472 self::AMZ_CONTENT_SHA256_HEADER,
473 "aws-sdk-invocation-id",
474 "aws-sdk-retry",
475 'x-amz-region-set'
476 ];
477 $storedHeaders = [];
478
479 foreach ($illegalV4aHeaders as $header) {
480 if ($request->hasHeader($header)){
481 $storedHeaders[$header] = $request->getHeader($header);
482 $request = $request->withoutHeader($header);
483 }
484 }
485
486 return $storedHeaders;
487 }
488
489 private function CRTRequestFromGuzzleRequest($request)
490 {
491 return new Request(
492 $request->getMethod(),
493 (string) $request->getUri(),
494 [], //leave empty as the query is parsed from the uri object
495 array_map(function ($header) {return $header[0];}, $request->getHeaders())
496 );
497 }
498
499 /**
500 * @param CredentialsInterface $credentials
501 * @param RequestInterface $request
502 * @param $signingService
503 * @return RequestInterface
504 */
505 protected function signWithV4a(CredentialsInterface $credentials, RequestInterface $request, $signingService)
506 {
507 $this->verifyCRTLoaded();
508 $credentials_provider = $this->createCRTStaticCredentialsProvider($credentials);
509 $signingConfig = new SigningConfigAWS([
510 'algorithm' => SigningAlgorithm::SIGv4_ASYMMETRIC,
511 'signature_type' => SignatureType::HTTP_REQUEST_HEADERS,
512 'credentials_provider' => $credentials_provider,
513 'signed_body_value' => $this->getPayload($request),
514 'region' => "*",
515 'service' => $signingService,
516 'date' => time(),
517 ]);
518
519 $removedIllegalHeaders = $this->removeIllegalV4aHeaders($request);
520 $http_request = $this->CRTRequestFromGuzzleRequest($request);
521
522 Signing::signRequestAws(
523 Signable::fromHttpRequest($http_request),
524 $signingConfig, function ($signing_result, $error_code) use (&$http_request) {
525 $signing_result->applyToHttpRequest($http_request);
526 });
527 foreach ($removedIllegalHeaders as $header => $value) {
528 $request = $request->withHeader($header, $value);
529 }
530
531 $sigV4AHeaders = $http_request->headers();
532 foreach ($sigV4AHeaders->toArray() as $h => $v) {
533 $request = $request->withHeader($h, $v);
534 }
535
536 return $request;
537 }
538
539 protected function presignWithV4a(
540 RequestInterface $request,
541 CredentialsInterface $credentials,
542 $expires
543 )
544 {
545 $this->verifyCRTLoaded();
546 $credentials_provider = $this->createCRTStaticCredentialsProvider($credentials);
547 $signingConfig = new SigningConfigAWS([
548 'algorithm' => SigningAlgorithm::SIGv4_ASYMMETRIC,
549 'signature_type' => SignatureType::HTTP_REQUEST_QUERY_PARAMS,
550 'credentials_provider' => $credentials_provider,
551 'signed_body_value' => $this->getPresignedPayload($request),
552 'region' => "*",
553 'service' => $this->service,
554 'date' => time(),
555 'expiration_in_seconds' => $expires
556 ]);
557
558 $this->removeIllegalV4aHeaders($request);
559 foreach ($this->getHeaderBlacklist() as $headerName => $headerValue) {
560 if ($request->hasHeader($headerName)) {
561 $request = $request->withoutHeader($headerName);
562 }
563 }
564
565 $http_request = $this->CRTRequestFromGuzzleRequest($request);
566 Signing::signRequestAws(
567 Signable::fromHttpRequest($http_request),
568 $signingConfig, function ($signing_result, $error_code) use (&$http_request) {
569 $signing_result->applyToHttpRequest($http_request);
570 });
571
572 return $request->withUri(
573 new Psr7\Uri($http_request->pathAndQuery())
574 );
575 }
576 }
577