Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
}