Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15371158
D14978.id.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
D14978.id.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Thu, Mar 13, 11:23 AM (5 d, 19 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7618349
Default Alt Text
D14978.id.diff (12 KB)
Attached To
Mode
D14978: Implement AWS v4 signature API
Attached
Detach File
Event Timeline
Log In to Comment