Skip to content

Commit

Permalink
Add option for v4 of request signing
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-zahariev committed Jul 10, 2020
1 parent 63e36e6 commit bee2c15
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 24 deletions.
52 changes: 40 additions & 12 deletions src/SimpleEmailService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -571,7 +599,7 @@ public function __triggerError($functionname, $error)

/**
* Set SES Request
*
*
* @param SimpleEmailServiceRequest $ses_request description
* @return SimpleEmailService $this
*/
Expand All @@ -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
*/
Expand Down
128 changes: 116 additions & 12 deletions src/SimpleEmailServiceRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
));
}
}

0 comments on commit bee2c15

Please sign in to comment.