From bee2c15ba68999f5fbe13ad5d32a7528b03b60b0 Mon Sep 17 00:00:00 2001 From: Daniel Zahariev Date: Fri, 10 Jul 2020 12:04:04 +0300 Subject: [PATCH] Add option for v4 of request signing --- src/SimpleEmailService.php | 52 +++++++++--- src/SimpleEmailServiceRequest.php | 128 +++++++++++++++++++++++++++--- 2 files changed, 156 insertions(+), 24 deletions(-) diff --git a/src/SimpleEmailService.php b/src/SimpleEmailService.php index 8af44d6..f36bd69 100644 --- a/src/SimpleEmailService.php +++ b/src/SimpleEmailService.php @@ -57,6 +57,9 @@ class SimpleEmailService const AWS_US_WEST_2 = 'email.us-west-2.amazonaws.com'; const AWS_EU_WEST1 = 'email.eu-west-1.amazonaws.com'; + const REQUEST_SIGNATURE_V3 = 'v3'; + const REQUEST_SIGNATURE_V4 = 'v4'; + /** * AWS SES Target host of region */ @@ -98,23 +101,48 @@ class SimpleEmailService */ protected $__verifyPeer = true; - /** - * Constructor - * - * @param string $accessKey Access key - * @param string $secretKey Secret key - * @param string $host Amazon Host through which to send the emails - * @param boolean $trigger_errors Trigger PHP errors when AWS SES API returns an error - * @return void - */ - public function __construct($accessKey = null, $secretKey = null, $host = self::AWS_US_EAST_1, $trigger_errors = true) { + /** + * @var string HTTP Request signature version + */ + protected $__requestSignatureVersion; + + /** + * Constructor + * + * @param string $accessKey Access key + * @param string $secretKey Secret key + * @param string $host Amazon Host through which to send the emails + * @param boolean $trigger_errors Trigger PHP errors when AWS SES API returns an error + * @param string $requestSignatureVersion Version of the request signature + */ + public function __construct($accessKey = null, $secretKey = null, $host = self::AWS_US_EAST_1, $trigger_errors = true, $requestSignatureVersion = self::REQUEST_SIGNATURE_V3) { if ($accessKey !== null && $secretKey !== null) { $this->setAuth($accessKey, $secretKey); } $this->__host = $host; $this->__trigger_errors = $trigger_errors; + $this->__requestSignatureVersion = $requestSignatureVersion; } + /** + * Set the request signature version + * + * @param string $requestSignatureVersion + * @return SimpleEmailService $this + */ + public function setRequestSignatureVersion($requestSignatureVersion) { + $this->__requestSignatureVersion = $requestSignatureVersion; + + return $this; + } + + /** + * @return string + */ + public function getRequestSignatureVersion() { + return $this->__requestSignatureVersion; + } + /** * Set AWS access key and secret key * @@ -571,7 +599,7 @@ public function __triggerError($functionname, $error) /** * Set SES Request - * + * * @param SimpleEmailServiceRequest $ses_request description * @return SimpleEmailService $this */ @@ -587,7 +615,7 @@ public function setRequestHandler(SimpleEmailServiceRequest $ses_request = null) /** * Get SES Request - * + * * @param string $verb HTTP Verb: GET, POST, DELETE * @return SimpleEmailServiceRequest SES Request */ diff --git a/src/SimpleEmailServiceRequest.php b/src/SimpleEmailServiceRequest.php index 4828edf..430bdb7 100644 --- a/src/SimpleEmailServiceRequest.php +++ b/src/SimpleEmailServiceRequest.php @@ -78,7 +78,7 @@ public function setParameter($key, $value, $replace = true) { } /** - * Get the params for the reques + * Get the params for the request * * @return array $params */ @@ -145,17 +145,9 @@ protected function getCurlHandler() { */ public function getResponse() { - // must be in format 'Sun, 06 Nov 1994 08:49:37 GMT' - $date = gmdate('D, d M Y H:i:s e'); - $query = implode('&', $this->getParametersEncoded()); - $auth = 'AWS3-HTTPS AWSAccessKeyId='.$this->ses->getAccessKey(); - $auth .= ',Algorithm=HmacSHA256,Signature='.$this->__getSignature($date); - $url = 'https://'.$this->ses->getHost().'/'; - - $headers = array(); - $headers[] = 'Date: ' . $date; - $headers[] = 'Host: ' . $this->ses->getHost(); - $headers[] = 'X-Amzn-Authorization: ' . $auth; + $url = 'https://'.$this->ses->getHost().'/'; + $query = implode('&', $this->getParametersEncoded()); + $headers = $this->getHeaders($query); $curl_handler = $this->getCurlHandler(); curl_setopt($curl_handler, CURLOPT_CUSTOMREQUEST, $this->verb); @@ -218,6 +210,34 @@ public function getResponse() { return $response; } + /** + * Get request headers + * @param string $query + * @return array + */ + protected function getHeaders($query) { + $headers = array(); + + if ($this->ses->getRequestSignatureVersion() == SimpleEmailService::REQUEST_SIGNATURE_V4) { + $date = (new DateTime('now', new DateTimeZone('UTC')))->format('Ymd\THis\Z'); + $headers[] = 'X-Amz-Date: ' . $date; + $headers[] = 'Host: ' . $this->ses->getHost(); + $headers[] = 'Authorization: ' . $this->__getAuthHeaderV4($date, $query); + + } else { + // must be in format 'Sun, 06 Nov 1994 08:49:37 GMT' + $date = gmdate('D, d M Y H:i:s e'); + $auth = 'AWS3-HTTPS AWSAccessKeyId='.$this->ses->getAccessKey(); + $auth .= ',Algorithm=HmacSHA256,Signature='.$this->__getSignature($date); + + $headers[] = 'Date: ' . $date; + $headers[] = 'Host: ' . $this->ses->getHost(); + $headers[] = 'X-Amzn-Authorization: ' . $auth; + } + + return $headers; + } + /** * Destroy any leftover handlers */ @@ -266,4 +286,88 @@ private function __customUrlEncode($var) { private function __getSignature($string) { return base64_encode(hash_hmac('sha256', $string, $this->ses->getSecretKey(), true)); } + + /** + * @param string $key + * @param string $dateStamp + * @param string $regionName + * @param string $serviceName + * @param string $algo + * @return string + */ + private function __getSigningKey($key, $dateStamp, $regionName, $serviceName, $algo) { + $kDate = hash_hmac($algo, $dateStamp, 'AWS4' . $key, true); + $kRegion = hash_hmac($algo, $regionName, $kDate, true); + $kService = hash_hmac($algo, $serviceName, $kRegion, true); + + return hash_hmac($algo,'aws4_request', $kService, true); + } + + /** + * Implementation of AWS Signature Version 4 + * @see https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html + * @param string $amz_datetime + * @param string $query + * @return string + */ + private function __getAuthHeaderV4($amz_datetime, $query) { + $amz_date = substr($amz_datetime, 0, 8); + $algo = 'sha256'; + $aws_algo = 'AWS4-HMAC-' . strtoupper($algo); + + $host_parts = explode('.', $this->ses->getHost()); + $service = $host_parts[0]; + $region = $host_parts[1]; + + $canonical_uri = '/'; + if($this->verb === 'POST') { + $canonical_querystring = ''; + $payload_data = $query; + } else { + $canonical_querystring = $query; + $payload_data = ''; + } + + // ************* TASK 1: CREATE A CANONICAL REQUEST ************* + $canonical_headers_list = [ + 'host:' . $this->ses->getHost(), + 'x-amz-date:' . $amz_datetime + ]; + + $canonical_headers = implode("\n", $canonical_headers_list) . "\n"; + $signed_headers = 'host;x-amz-date'; + $payload_hash = hash($algo, $payload_data, false); + + $canonical_request = implode("\n", array( + $this->verb, + $canonical_uri, + $canonical_querystring, + $canonical_headers, + $signed_headers, + $payload_hash + )); + + // ************* TASK 2: CREATE THE STRING TO SIGN************* + $credential_scope = $amz_date. '/' . $region . '/' . $service . '/' . 'aws4_request'; + $string_to_sign = implode("\n", array( + $aws_algo, + $amz_datetime, + $credential_scope, + hash($algo, $canonical_request, false) + )); + + // ************* TASK 3: CALCULATE THE SIGNATURE ************* + // Create the signing key using the function defined above. + $signing_key = $this->__getSigningKey($this->ses->getSecretKey(), $amz_date, $region, $service, $algo); + + // Sign the string_to_sign using the signing_key + $signature = hash_hmac($algo, $string_to_sign, $signing_key, false); + + // ************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST ************* + return $aws_algo . ' ' . implode(', ', array( + 'Credential=' . $this->ses->getAccessKey() . '/' . $credential_scope, + 'SignedHeaders=' . $signed_headers , + 'Signature=' . $signature + )); + } }