src/Signer/Signer.php
<?php
/**
* Copyright (c) 2011-2014 Amazon Web Services, Inc.
* Copyright (c) 2014 Ryan Parman.
*
* Based on a stripped-down version of the AWS Signature v4 implementation,
* built into the AWS SDK for PHP 3.0. Original authors:
*
* @author Michael Dowling <https://github.com/mtdowling>
* @author Jeremy Lindblom <https://github.com/jeremeamia>
*
* http://opensource.org/licenses/Apache2.0
*/
namespace Skyzyx\Signer;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerTrait;
use Psr\Log\NullLogger;
/**
* The Signer class is designed for those who are signing data on behalf of a public-private keypair.
*
* In principle, the "client party" has public key (i.e., `client_id`) has a matching private key
* (i.e., `client_secret`) that can be verified by both the signer, as well as the client, but
* by nobody else as we don't want to make forgeries possible.
*
* The "signing party" has a simple an identifier which acts as an additional piece of entropy in the
* algorithm, and can help differentiate between multiple signing parties if the client party does
* something like try to use the same public-private keypair independently of a signing party
* (as is common with GPG signing).
*
* For example, in the original AWS implementation, the "self key" for AWS was "AWS4".
*/
class Signer implements SignerInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
/**************************************************************************/
// PROPERTIES
/** @var string */
private $self_key;
/** @var string */
private $client_id;
/** @var string */
private $client_secret;
/** @var string */
private $hash_algo = '';
/**************************************************************************/
// PUBLIC METHODS
/**
* Constructs a new instance of this class.
*
* @param string $self_key A string which identifies the signing party and adds additional entropy.
* @param string $client_id A string which is the public portion of the keypair identifying the client party.
* The pairing of the public and private portions of the keypair should only be known
* to the client party and the signing party.
* @param string $client_secret A string which is the private portion of the keypair identifying the client party.
* The pairing of the public and private portions of the keypair should only be known
* to the client party and the signing party.
* @param string $hash_algo The hash algorithm to use for signing. Run `hash_algos()` to see what's supported.
* The default value is `sha512`.
*
* @see http://php.net/hash_algos
*/
public function __construct($self_key, $client_id, $client_secret, $hash_algo = 'sha512')
{
$this->self_key = $self_key;
$this->client_id = $client_id;
$this->client_secret = $client_secret;
$this->hash_algo = $hash_algo;
$this->logger = new NullLogger();
}
/**
* {@inheritdoc}
*/
public function getSelfKey()
{
/** @var string */
return $this->self_key;
}
/**
* {@inheritdoc}
*/
public function getClientId()
{
/** @var string */
return $this->client_id;
}
/**
* {@inheritdoc}
*/
public function getClientSecret()
{
/** @var string */
return $this->client_secret;
}
/**
* {@inheritdoc}
*/
public function sign(array $payload)
{
$scope = $this->createScope($this->getSelfKey(), $this->getClientId());
$context = $this->createContext($payload);
$s2s = $this->createStringToSign($this->getSelfKey(), $this->getClientId(), $scope, $context);
$signing_key = $this->getSigningSalt($this->getSelfKey(), $this->getClientId(), $this->getClientSecret());
$signature = hash_hmac($this->hash_algo, $s2s, $signing_key);
/** @var string */
return $signature;
}
/**************************************************************************/
// PRIVATE METHODS
/**
* Creates the string-to-sign based on a variety of factors.
*
* @param string $self_key A string which identifies the signing party and adds additional entropy.
* @param string $client_id A string which is the public portion of the keypair identifying the client party.
* @param string $scope The results of a call to the `createScope()` method.
* @param string $context The results of a call to the `createContext()` method.
* @return string The final string to be signed.
*/
private function createStringToSign($self_key, $client_id, $scope, $context)
{
$s2s = sprintf(
"SIGNER-HMAC-%s\n%s\n%s\n%s\n%s",
strtoupper($this->hash_algo),
$self_key,
$client_id,
hash($this->hash_algo, $scope),
hash($this->hash_algo, $context)
);
$this->logger->debug(__FUNCTION__, [
'string_to_sign' => $s2s,
]);
/** @var string */
return $s2s;
}
/**
* An array of key-value pairs representing the data that you want to sign.
* All values must be `scalar`.
*
* @param array $payload The data that you want to sign.
* @return string A canonical string representation of the data to sign.
*/
private function createContext(array $payload)
{
$canonical_payload = [];
foreach ($payload as $k => $v) {
$k = strtolower($k);
$v = strtolower($v);
$canonical_payload[$k] = sprintf('%s=%s', $k, $v);
}
ksort($canonical_payload);
$signed_headers_string = implode(';', array_keys($canonical_payload));
$canonical_context = implode("\n", $canonical_payload) . "\n\n" . $signed_headers_string;
$this->logger->debug(__FUNCTION__, [
'payload' => $payload,
'canonical_payload' => $canonical_payload,
'signed_headers_string' => $signed_headers_string,
'canonical_context' => $canonical_context,
]);
/** @var string */
return $canonical_context;
}
/**
* Gets the salt value that should be used for signing.
*
* @param string $self_key A string which identifies the signing party and adds additional entropy.
* @param string $client_id A string which is the public portion of the keypair identifying the client party.
* @param string $client_secret A string which is the private portion of the keypair identifying the client party.
* @return string The signing salt.
*/
private function getSigningSalt($self_key, $client_id, $client_secret)
{
$self_key_sign = hash_hmac($this->hash_algo, $self_key, $client_secret, true);
$client_id_sign = hash_hmac($this->hash_algo, $client_id, $self_key_sign, true);
$salt = hash_hmac($this->hash_algo, 'signer', $client_id_sign, true);
$this->logger->debug(__FUNCTION__, [
'input' => [
'self_key' => $self_key,
'client_id' => $client_id,
'client_secret' => $client_secret,
],
'output' => [
'self_key_sign' => $self_key_sign,
'client_id_sign' => $client_id_sign,
'salt' => $salt,
],
]);
/** @var string */
return $salt;
}
/**
* Creates the "scope" in which the signature is valid.
*
* @param string $self_key A string which identifies the signing party and adds additional entropy.
* @param string $client_id A string which is the public portion of the keypair identifying the client party.
* @return string The string which represents the scope in which the signature is valid.
*/
private function createScope($self_key, $client_id)
{
$scope = sprintf(
'%s/%s/signer',
$self_key,
$client_id
);
$this->logger->debug(__FUNCTION__, [
'scope' => $scope,
]);
/** @var string */
return $scope;
}
}