Page MenuHomePhabricator

D14978.diff
No OneTemporary

D14978.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -86,6 +86,8 @@
'PhutilAWSException' => 'future/aws/PhutilAWSException.php',
'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php',
'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php',
+ 'PhutilAWSv4Signature' => 'future/aws/PhutilAWSv4Signature.php',
+ 'PhutilAWSv4SignatureTestCase' => 'future/aws/__tests__/PhutilAWSv4SignatureTestCase.php',
'PhutilAggregateException' => 'error/PhutilAggregateException.php',
'PhutilAllCapsEnglishLocale' => 'internationalization/locales/PhutilAllCapsEnglishLocale.php',
'PhutilAmazonAuthAdapter' => 'auth/PhutilAmazonAuthAdapter.php',
@@ -602,6 +604,8 @@
'PhutilAWSException' => 'Exception',
'PhutilAWSFuture' => 'FutureProxy',
'PhutilAWSS3Future' => 'PhutilAWSFuture',
+ 'PhutilAWSv4Signature' => 'Phobject',
+ 'PhutilAWSv4SignatureTestCase' => 'PhutilTestCase',
'PhutilAggregateException' => 'Exception',
'PhutilAllCapsEnglishLocale' => 'PhutilLocale',
'PhutilAmazonAuthAdapter' => 'PhutilOAuthAuthAdapter',
diff --git a/src/future/aws/PhutilAWSv4Signature.php b/src/future/aws/PhutilAWSv4Signature.php
new file mode 100644
--- /dev/null
+++ b/src/future/aws/PhutilAWSv4Signature.php
@@ -0,0 +1,256 @@
+<?php
+
+final class PhutilAWSv4Signature extends Phobject {
+
+ private $accessKey;
+ private $secretKey;
+ private $signingKey;
+
+ private $date;
+
+ private $region;
+ private $service;
+
+ public function setAccessKey($access_key) {
+ $this->accessKey = $access_key;
+ return $this;
+ }
+
+ public function setSecretKey(PhutilOpaqueEnvelope $secret_key) {
+ $this->secretKey = $secret_key;
+ return $this;
+ }
+
+ public function setDate($date) {
+ $this->date = $date;
+ return $this;
+ }
+
+ public function getDate() {
+ if ($this->date === null) {
+ $this->date = date('c');
+ }
+ return $this->date;
+ }
+
+ public function setRegion($region) {
+ $this->region = $region;
+ return $this;
+ }
+
+ public function getRegion() {
+ return $this->region;
+ }
+
+ public function setService($service) {
+ $this->service = $service;
+ return $this;
+ }
+
+ public function getService() {
+ return $this->service;
+ }
+
+ public function setSigningKey($signing_key) {
+ $this->signingKey = $signing_key;
+ return $this;
+ }
+
+ public function getSigningKey() {
+ if ($this->signingKey === null) {
+ $this->signingKey = $this->computeSigningKey();
+ }
+
+ return $this->signingKey;
+ }
+
+ private function getAlgorithm() {
+ return 'AWS4-HMAC-SHA256';
+ }
+
+ private function getHost(HTTPSFuture $future) {
+ $uri = new PhutilURI($future->getURI());
+ return $uri->getDomain();
+ }
+
+ private function getPath(HTTPSFuture $future) {
+ $uri = new PhutilURI($future->getURI());
+ return $uri->getPath();
+ }
+
+ public function signRequest(HTTPSFuture $future) {
+ $body_signature = $this->getBodySignature($future);
+
+ $future->addHeader('X-Amz-Content-sha256', $body_signature);
+ $future->addHeader('X-Amz-Date', $this->getDate());
+
+ $request_signature = $this->getCanonicalRequestSignature(
+ $future,
+ $body_signature);
+
+ $string_to_sign = $this->getStringToSign($request_signature);
+
+ $signing_key = $this->getSigningKey();
+
+ $signature = hash_hmac('sha256', $string_to_sign, $signing_key);
+
+ $algorithm = $this->getAlgorithm();
+ $credential = $this->getCredential();
+ $signed_headers = $this->getSignedHeaderList($future);
+
+ $authorization =
+ $algorithm.' '.
+ 'Credential='.$credential.','.
+ 'SignedHeaders='.$signed_headers.','.
+ 'Signature='.$signature;
+
+ $future->addHeader('Authorization', $authorization);
+
+ return $future;
+ }
+
+ private function getBodySignature(HTTPSFuture $future) {
+ $http_body = $future->getData();
+
+ if (is_array($http_body)) {
+ $http_body = '';
+ }
+
+ return hash('sha256', $http_body);
+ }
+
+ private function getCanonicalRequestSignature(
+ HTTPSFuture $future,
+ $body_signature) {
+
+ $http_method = $future->getMethod();
+
+ $path = $this->getPath($future);
+ $path = rawurlencode($path);
+ $path = str_replace('%2F', '/', $path);
+
+ $canonical_parameters = $this->getCanonicalParameterList($future);
+ $canonical_headers = $this->getCanonicalHeaderList($future);
+ $signed_headers = $this->getSignedHeaderList($future);
+
+ $canonical_request =
+ $http_method."\n".
+ $path."\n".
+ $canonical_parameters."\n".
+ $canonical_headers."\n".
+ "\n".
+ $signed_headers."\n".
+ $body_signature;
+
+ return hash('sha256', $canonical_request);
+ }
+
+ private function getStringToSign($request_signature) {
+ $algorithm = $this->getAlgorithm();
+ $date = $this->getDate();
+ $scope_parts = $this->getScopeParts();
+ $scope = implode('/', $scope_parts);
+
+ $string_to_sign =
+ $algorithm."\n".
+ $date."\n".
+ $scope."\n".
+ $request_signature;
+
+ return $string_to_sign;
+ }
+
+ private function getScopeParts() {
+ return array(
+ substr($this->getDate(), 0, 8),
+ $this->getRegion(),
+ $this->getService(),
+ 'aws4_request',
+ );
+ }
+
+ private function computeSigningKey() {
+ $secret_key = $this->secretKey;
+ if (!$secret_key) {
+ throw new Exception(
+ pht(
+ 'You must either provide a signing key with setSigningKey(), or '.
+ 'provide a secret key with setSecretKey().'));
+ }
+
+ // NOTE: This part of the algorithm uses the raw binary hashes, and the
+ // result is not human-readable.
+ $raw_hash = true;
+
+ $signing_key = 'AWS4'.$secret_key->openEnvelope();
+
+ $scope_parts = $this->getScopeParts();
+ foreach ($scope_parts as $scope_part) {
+ $signing_key = hash_hmac('sha256', $scope_part, $signing_key, $raw_hash);
+ }
+
+ return $signing_key;
+ }
+
+ private function getCanonicalHeaderList(HTTPSFuture $future) {
+ $headers = $this->getCanonicalHeaderMap($future);
+
+ $canonical_headers = array();
+ foreach ($headers as $header => $header_value) {
+ $canonical_headers[] = $header.':'.trim($header_value);
+ }
+
+ return implode("\n", $canonical_headers);
+ }
+
+ private function getCanonicalHeaderMap(HTTPSFuture $future) {
+ $headers = $future->getHeaders();
+ $headers[] = array(
+ 'Host',
+ $this->getHost($future),
+ );
+
+ $header_map = array();
+ foreach ($headers as $header) {
+ list($key, $value) = $header;
+ $key = phutil_utf8_strtolower($key);
+ $header_map[$key] = $value;
+ }
+
+ ksort($header_map);
+
+ return $header_map;
+ }
+
+ private function getSignedHeaderList(HTTPSFuture $future) {
+ $headers = $this->getCanonicalHeaderMap($future);
+ return implode(';', array_keys($headers));
+ }
+
+ private function getCanonicalParameterList(HTTPSFuture $future) {
+ $uri = new PhutilURI($future->getURI());
+ $params = $uri->getQueryParams();
+
+ ksort($params);
+ $canonical_parameters = array();
+ foreach ($params as $key => $value) {
+ $canonical_parameters[] = rawurlencode($key).'='.rawurlencode($value);
+ }
+ $canonical_parameters = implode('&', $canonical_parameters);
+
+ return $canonical_parameters;
+ }
+
+ private function getCredential() {
+ $access_key = $this->accessKey;
+ if (!strlen($access_key)) {
+ throw new PhutilInvalidStateException('setAccessKey');
+ }
+
+ $parts = $this->getScopeParts();
+ array_unshift($parts, $access_key);
+
+ return implode('/', $parts);
+ }
+
+}
diff --git a/src/future/aws/__tests__/PhutilAWSv4SignatureTestCase.php b/src/future/aws/__tests__/PhutilAWSv4SignatureTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/future/aws/__tests__/PhutilAWSv4SignatureTestCase.php
@@ -0,0 +1,159 @@
+<?php
+
+final class PhutilAWSv4SignatureTestCase extends PhutilTestCase {
+
+
+ public function testAWSv4SignaturesS3GetObject() {
+ $access_key = 'AKIAIOSFODNN7EXAMPLE';
+ $secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
+ $date = '20130524T000000Z';
+ $region = 'us-east-1';
+ $service = 's3';
+ $uri = 'https://examplebucket.s3.amazonaws.com/test.txt';
+ $method = 'GET';
+
+ $future = id(new HTTPSFuture($uri))
+ ->setMethod($method)
+ ->addHeader('Range', 'bytes=0-9');
+
+ $signature = id(new PhutilAWSv4Signature())
+ ->setAccessKey($access_key)
+ ->setSecretKey(new PhutilOpaqueEnvelope($secret_key))
+ ->setDate($date)
+ ->setRegion($region)
+ ->setService($service);
+
+ $signature->signRequest($future);
+
+ $expect = <<<EOSIGNATURE
+AWS4-HMAC-SHA256
+
+Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,
+SignedHeaders=host;range;x-amz-content-sha256;x-amz-date,
+Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41
+EOSIGNATURE;
+
+ $this->assertSignature($expect, $future);
+ }
+
+
+ public function testAWSv4SignaturesS3PutObject() {
+ $access_key = 'AKIAIOSFODNN7EXAMPLE';
+ $secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
+ $date = '20130524T000000Z';
+ $region = 'us-east-1';
+ $service = 's3';
+ $uri = 'https://examplebucket.s3.amazonaws.com/test$file.text';
+ $method = 'PUT';
+ $body = 'Welcome to Amazon S3.';
+
+ $future = id(new HTTPSFuture($uri, $body))
+ ->setMethod($method)
+ ->addHeader('X-Amz-Storage-Class', 'REDUCED_REDUNDANCY')
+ ->addHeader('Date', 'Fri, 24 May 2013 00:00:00 GMT');
+
+ $signature = id(new PhutilAWSv4Signature())
+ ->setAccessKey($access_key)
+ ->setSecretKey(new PhutilOpaqueEnvelope($secret_key))
+ ->setDate($date)
+ ->setRegion($region)
+ ->setService($service);
+
+ $signature->signRequest($future);
+
+ $expect = <<<EOSIGNATURE
+AWS4-HMAC-SHA256
+
+Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,
+SignedHeaders=date;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class,
+Signature=98ad721746da40c64f1a55b78f14c238d841ea1380cd77a1b5971af0ece108bd
+EOSIGNATURE;
+
+ $this->assertSignature($expect, $future);
+ }
+
+
+ public function testAWSv4SignaturesS3GetBucketLifecycle() {
+ $access_key = 'AKIAIOSFODNN7EXAMPLE';
+ $secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
+ $date = '20130524T000000Z';
+ $region = 'us-east-1';
+ $service = 's3';
+ $uri = 'https://examplebucket.s3.amazonaws.com/?lifecycle';
+ $method = 'GET';
+
+ $future = id(new HTTPSFuture($uri))
+ ->setMethod($method);
+
+ $signature = id(new PhutilAWSv4Signature())
+ ->setAccessKey($access_key)
+ ->setSecretKey(new PhutilOpaqueEnvelope($secret_key))
+ ->setDate($date)
+ ->setRegion($region)
+ ->setService($service);
+
+ $signature->signRequest($future);
+
+ $expect = <<<EOSIGNATURE
+AWS4-HMAC-SHA256
+
+Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,
+SignedHeaders=host;x-amz-content-sha256;x-amz-date,
+Signature=fea454ca298b7da1c68078a5d1bdbfbbe0d65c699e0f91ac7a200a0136783543
+EOSIGNATURE;
+
+ $this->assertSignature($expect, $future);
+ }
+
+
+ public function testAWSv4SignaturesS3GetBucket() {
+ $access_key = 'AKIAIOSFODNN7EXAMPLE';
+ $secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
+ $date = '20130524T000000Z';
+ $region = 'us-east-1';
+ $service = 's3';
+ $uri = 'https://examplebucket.s3.amazonaws.com/?max-keys=2&prefix=J';
+ $method = 'GET';
+
+ $future = id(new HTTPSFuture($uri))
+ ->setMethod($method);
+
+ $signature = id(new PhutilAWSv4Signature())
+ ->setAccessKey($access_key)
+ ->setSecretKey(new PhutilOpaqueEnvelope($secret_key))
+ ->setDate($date)
+ ->setRegion($region)
+ ->setService($service);
+
+ $signature->signRequest($future);
+
+ $expect = <<<EOSIGNATURE
+AWS4-HMAC-SHA256
+
+Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,
+SignedHeaders=host;x-amz-content-sha256;x-amz-date,
+Signature=34b48302e7b5fa45bde8084f4b7868a86f0a534bc59db6670ed5711ef69dc6f7
+EOSIGNATURE;
+
+ $this->assertSignature($expect, $future);
+ }
+
+
+ private function assertSignature($expect, HTTPSFuture $signed) {
+ $authorization = null;
+ foreach ($signed->getHeaders() as $header) {
+ list($key, $value) = $header;
+ if (phutil_utf8_strtolower($key) === 'authorization') {
+ $authorization = $value;
+ break;
+ }
+ }
+
+ $expect = str_replace("\n\n", ' ', $expect);
+ $expect = str_replace("\n", '', $expect);
+
+ $this->assertEqual($expect, $authorization);
+ }
+
+
+}

File Metadata

Mime Type
text/plain
Expires
Tue, Jun 18, 4:12 AM (1 w, 1 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6298843
Default Alt Text
D14978.diff (12 KB)

Event Timeline