This commit is contained in:
Fred 2021-10-16 06:55:42 +08:00
commit cefeda7908
22 changed files with 993 additions and 0 deletions

20
.php_cs Normal file
View File

@ -0,0 +1,20 @@
<?php
$finder = Symfony\CS\Finder\DefaultFinder::create()
->in(__DIR__ . "/src");
return Symfony\CS\Config\Config::create()
->level(\Symfony\CS\FixerInterface::PSR2_LEVEL)
->fixers([
'unused_use',
'remove_lines_between_uses',
'remove_leading_slash_use',
'ordered_use',
'short_array_syntax',
'whitespacy_lines',
'ternary_spaces',
'standardize_not_equal',
'spaces_cast',
'extra_empty_lines',
])
->finder($finder);

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014-2015 99designs
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

35
composer.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "99designs/http-signatures",
"description": "Sign and verify HTTP messages",
"keywords": ["http", "https", "signing", "signed", "signature", "hmac"],
"license": "MIT",
"authors": [
{
"name": "Paul Annesley",
"email": "paul@99designs.com"
}
],
"autoload": {
"psr-4": {
"HttpSignatures\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"HttpSignatures\\Tests\\": "tests/"
}
},
"require": {
"php": ">=5.5",
"paragonie/random_compat": "^1.0|^2.0",
"psr/http-message": "^1.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^1.11",
"guzzlehttp/psr7": "^1.2",
"phpunit/phpunit": "~4.8",
"symfony/http-foundation": "~2.8|~3.0",
"symfony/psr-http-message-bridge": "^1.0",
"zendframework/zend-diactoros": "^1.1"
}
}

26
src/Algorithm.php Normal file
View File

@ -0,0 +1,26 @@
<?php
namespace HttpSignatures;
abstract class Algorithm
{
/**
* @param string $name
* @return HmacAlgorithm
* @throws Exception
*/
public static function create($name)
{
switch ($name) {
case 'hmac-sha1':
return new HmacAlgorithm('sha1');
break;
case 'hmac-sha256':
return new HmacAlgorithm('sha256');
break;
default:
throw new Exception("No algorithm named '$name'");
break;
}
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace HttpSignatures;
interface AlgorithmInterface
{
/**
* @return string
*/
public function name();
/**
* @param string $key
* @param string $data
* @return string
*/
public function sign($key, $data);
}

127
src/Context.php Normal file
View File

@ -0,0 +1,127 @@
<?php
namespace HttpSignatures;
class Context
{
/** @var array */
private $headers;
/** @var KeyStoreInterface */
private $keyStore;
/** @var array */
private $keys;
/** @var string */
private $signingKeyId;
/**
* @param array $args
*
* @throws Exception
*/
public function __construct($args)
{
if (isset($args['keys']) && isset($args['keyStore'])) {
throw new Exception(__CLASS__.' accepts keys or keyStore but not both');
} elseif (isset($args['keys'])) {
// array of keyId => keySecret
$this->keys = $args['keys'];
} elseif (isset($args['keyStore'])) {
$this->setKeyStore($args['keyStore']);
}
// algorithm for signing; not necessary for verifying.
if (isset($args['algorithm'])) {
$this->algorithmName = $args['algorithm'];
}
// headers list for signing; not necessary for verifying.
if (isset($args['headers'])) {
$this->headers = $args['headers'];
}
// signingKeyId specifies the key used for signing messages.
if (isset($args['signingKeyId'])) {
$this->signingKeyId = $args['signingKeyId'];
} elseif (isset($args['keys']) && count($args['keys']) === 1) {
list($this->signingKeyId) = array_keys($args['keys']); // first key
}
}
/**
* @return Signer
*
* @throws Exception
*/
public function signer()
{
return new Signer(
$this->signingKey(),
$this->algorithm(),
$this->headerList()
);
}
/**
* @return Verifier
*/
public function verifier()
{
return new Verifier($this->keyStore());
}
/**
* @return Key
*
* @throws Exception
* @throws KeyStoreException
*/
private function signingKey()
{
if (isset($this->signingKeyId)) {
return $this->keyStore()->fetch($this->signingKeyId);
} else {
throw new Exception('no implicit or specified signing key');
}
}
/**
* @return HmacAlgorithm
*
* @throws Exception
*/
private function algorithm()
{
return Algorithm::create($this->algorithmName);
}
/**
* @return HeaderList
*/
private function headerList()
{
return new HeaderList($this->headers);
}
/**
* @return KeyStore
*/
private function keyStore()
{
if (empty($this->keyStore)) {
$this->keyStore = new KeyStore($this->keys);
}
return $this->keyStore;
}
/**
* @param KeyStoreInterface $keyStore
*/
private function setKeyStore(KeyStoreInterface $keyStore)
{
$this->keyStore = $keyStore;
}
}

7
src/Exception.php Normal file
View File

@ -0,0 +1,7 @@
<?php
namespace HttpSignatures;
class Exception extends \Exception
{
}

48
src/HeaderList.php Normal file
View File

@ -0,0 +1,48 @@
<?php
namespace HttpSignatures;
class HeaderList
{
/** @var array */
public $names;
/**
* @param array $names
*/
public function __construct(array $names)
{
$this->names = array_map(
[$this, 'normalize'],
$names
);
}
/**
* @param $string
*
* @return HeaderList
*/
public static function fromString($string)
{
return new static(explode(' ', $string));
}
/**
* @return string
*/
public function string()
{
return implode(' ', $this->names);
}
/**
* @param $name
*
* @return string
*/
private function normalize($name)
{
return strtolower($name);
}
}

36
src/HmacAlgorithm.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace HttpSignatures;
class HmacAlgorithm implements AlgorithmInterface
{
/** @var string */
private $digestName;
/**
* @param string $digestName
*/
public function __construct($digestName)
{
$this->digestName = $digestName;
}
/**
* @return string
*/
public function name()
{
return sprintf('hmac-%s', $this->digestName);
}
/**
* @param string $key
* @param string $data
*
* @return string
*/
public function sign($key, $data)
{
return hash_hmac($this->digestName, $data, $key, true);
}
}

22
src/Key.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace HttpSignatures;
class Key
{
/** @var string */
public $id;
/** @var string */
public $secret;
/**
* @param string $id
* @param string $secret
*/
public function __construct($id, $secret)
{
$this->id = $id;
$this->secret = $secret;
}
}

36
src/KeyStore.php Normal file
View File

@ -0,0 +1,36 @@
<?php
namespace HttpSignatures;
class KeyStore implements KeyStoreInterface
{
/** @var Key[] */
private $keys;
/**
* @param array $keys
*/
public function __construct($keys)
{
$this->keys = [];
foreach ($keys as $id => $secret) {
$this->keys[$id] = new Key($id, $secret);
}
}
/**
* @param string $keyId
*
* @return Key
*
* @throws KeyStoreException
*/
public function fetch($keyId)
{
if (isset($this->keys[$keyId])) {
return $this->keys[$keyId];
} else {
throw new KeyStoreException("Key '$keyId' not found");
}
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace HttpSignatures;
class KeyStoreException extends Exception
{
}

15
src/KeyStoreInterface.php Normal file
View File

@ -0,0 +1,15 @@
<?php
namespace HttpSignatures;
interface KeyStoreInterface
{
/**
* return the secret for the specified $keyId.
*
* @param string $keyId
*
* @return Key
*/
public function fetch($keyId);
}

38
src/Signature.php Normal file
View File

@ -0,0 +1,38 @@
<?php
namespace HttpSignatures;
use Psr\Http\Message\RequestInterface;
class Signature
{
/** @var Key */
private $key;
/** @var HmacAlgorithm */
private $algorithm;
/** @var SigningString */
private $signingString;
/**
* @param RequestInterface $message
* @param Key $key
* @param AlgorithmInterface $algorithm
* @param HeaderList $headerList
*/
public function __construct($message, Key $key, AlgorithmInterface $algorithm, HeaderList $headerList)
{
$this->key = $key;
$this->algorithm = $algorithm;
$this->signingString = new SigningString($headerList, $message);
}
public function string()
{
return $this->algorithm->sign(
$this->key->secret,
$this->signingString->string()
);
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace HttpSignatures;
class SignatureParameters
{
/**
* @param Key $key
* @param AlgorithmInterface $algorithm
* @param HeaderList $headerList
* @param Signature $signature
*/
public function __construct($key, $algorithm, $headerList, $signature)
{
$this->key = $key;
$this->algorithm = $algorithm;
$this->headerList = $headerList;
$this->signature = $signature;
}
/**
* @return string
*/
public function string()
{
return implode(',', $this->parameterComponents());
}
/**
* @return array
*/
private function parameterComponents()
{
return [
sprintf('keyId="%s"', $this->key->id),
sprintf('algorithm="%s"', $this->algorithm->name()),
sprintf('headers="%s"', $this->headerList->string()),
sprintf('signature="%s"', $this->signatureBase64()),
];
}
/**
* @return string
*/
private function signatureBase64()
{
return base64_encode($this->signature->string());
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace HttpSignatures;
class SignatureParametersParser
{
/** @var string */
private $input;
/**
* @param string $input
*/
public function __construct($input)
{
$this->input = $input;
}
/**
* @return array
*/
public function parse()
{
$result = $this->pairsToAssociative(
$this->arrayOfPairs()
);
$this->validate($result);
return $result;
}
/**
* @param array $pairs
*
* @return array
*/
private function pairsToAssociative($pairs)
{
$result = [];
foreach ($pairs as $pair) {
$result[$pair[0]] = $pair[1];
}
return $result;
}
/**
* @return array
*/
private function arrayOfPairs()
{
return array_map(
[$this, 'pair'],
$this->segments()
);
}
/**
* @return array
*/
private function segments()
{
return explode(',', $this->input);
}
/**
* @param $segment
*
* @return array
*
* @throws SignatureParseException
*/
private function pair($segment)
{
$segmentPattern = '/\A(keyId|algorithm|headers|signature)="(.*)"\z/';
$matches = [];
$result = preg_match($segmentPattern, $segment, $matches);
if ($result !== 1) {
throw new SignatureParseException("Signature parameters segment '$segment' invalid");
}
array_shift($matches);
return $matches;
}
/**
* @param $result
*
* @throws SignatureParseException
*/
private function validate($result)
{
$this->validateAllKeysArePresent($result);
}
/**
* @param $result
*
* @throws SignatureParseException
*/
private function validateAllKeysArePresent($result)
{
// Regexp in pair() ensures no unwanted keys exist.
// Ensure that all wanted keys exist.
$wanted = ['keyId', 'algorithm', 'headers', 'signature'];
$missing = array_diff($wanted, array_keys($result));
if (!empty($missing)) {
$csv = implode(', ', $missing);
throw new SignatureParseException("Missing keys $csv");
}
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace HttpSignatures;
class SignatureParseException extends Exception
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace HttpSignatures;
class SignedHeaderNotPresentException extends Exception
{
}

69
src/Signer.php Normal file
View File

@ -0,0 +1,69 @@
<?php
namespace HttpSignatures;
use Psr\Http\Message\RequestInterface;
class Signer
{
/** @var Key */
private $key;
/** @var HmacAlgorithm */
private $algorithm;
/** @var HeaderList */
private $headerList;
/**
* @param Key $key
* @param HmacAlgorithm $algorithm
* @param HeaderList $headerList
*/
public function __construct($key, $algorithm, $headerList)
{
$this->key = $key;
$this->algorithm = $algorithm;
$this->headerList = $headerList;
}
/**
* @param RequestInterface $message
* @return RequestInterface
*/
public function sign($message)
{
$signatureParameters = $this->signatureParameters($message);
$message = $message->withAddedHeader("Signature", $signatureParameters->string());
$message = $message->withAddedHeader("Authorization", "Signature " . $signatureParameters->string());
return $message;
}
/**
* @param RequestInterface $message
* @return SignatureParameters
*/
private function signatureParameters($message)
{
return new SignatureParameters(
$this->key,
$this->algorithm,
$this->headerList,
$this->signature($message)
);
}
/**
* @param RequestInterface $message
* @return Signature
*/
private function signature($message)
{
return new Signature(
$message,
$this->key,
$this->algorithm,
$this->headerList
);
}
}

84
src/SigningString.php Normal file
View File

@ -0,0 +1,84 @@
<?php
namespace HttpSignatures;
use Psr\Http\Message\RequestInterface;
class SigningString
{
/** @var HeaderList */
private $headerList;
/** @var RequestInterface */
private $message;
/**
* @param HeaderList $headerList
* @param RequestInterface $message
*/
public function __construct(HeaderList $headerList, $message)
{
$this->headerList = $headerList;
$this->message = $message;
}
/**
* @return string
*/
public function string()
{
return implode("\n", $this->lines());
}
/**
* @return array
*/
private function lines()
{
return array_map(
[$this, 'line'],
$this->headerList->names
);
}
/**
* @param string $name
* @return string
* @throws SignedHeaderNotPresentException
*/
private function line($name)
{
if ($name == '(request-target)') {
return $this->requestTargetLine();
} else {
return sprintf('%s: %s', $name, $this->headerValue($name));
}
}
/**
* @param string $name
* @return string
* @throws SignedHeaderNotPresentException
*/
private function headerValue($name)
{
if ($this->message->hasHeader($name)) {
$header = $this->message->getHeader($name);
return end($header);
} else {
throw new SignedHeaderNotPresentException("Header '$name' not in message");
}
}
/**
* @return string
*/
private function requestTargetLine()
{
return sprintf(
'(request-target): %s %s',
strtolower($this->message->getMethod()),
$this->message->getRequestTarget()
);
}
}

181
src/Verification.php Normal file
View File

@ -0,0 +1,181 @@
<?php
namespace HttpSignatures;
use Psr\Http\Message\RequestInterface;
class Verification
{
/** @var RequestInterface */
private $message;
/** @var KeyStoreInterface */
private $keyStore;
/** @var array */
private $_parameters;
/**
* @param RequestInterface $message
* @param KeyStoreInterface $keyStore
*/
public function __construct($message, KeyStoreInterface $keyStore)
{
$this->message = $message;
$this->keyStore = $keyStore;
}
/**
* @return bool
*/
public function isValid()
{
return $this->hasSignatureHeader() && $this->signatureMatches();
}
/**
* @return bool
*/
private function signatureMatches()
{
try {
$random = random_bytes(32);
return hash_hmac('sha256', $this->expectedSignatureBase64(), $random, true) === hash_hmac('sha256', $this->providedSignatureBase64(), $random, true);
} catch (SignatureParseException $e) {
return false;
} catch (KeyStoreException $e) {
return false;
} catch (SignedHeaderNotPresentException $e) {
return false;
}
}
/**
* @return string
*/
private function expectedSignatureBase64()
{
return base64_encode($this->expectedSignature()->string());
}
/**
* @return Signature
*/
private function expectedSignature()
{
return new Signature(
$this->message,
$this->key(),
$this->algorithm(),
$this->headerList()
);
}
/**
* @return string
*
* @throws Exception
*/
private function providedSignatureBase64()
{
return $this->parameter('signature');
}
/**
* @return Key
*
* @throws Exception
*/
private function key()
{
return $this->keyStore->fetch($this->parameter('keyId'));
}
/**
* @return HmacAlgorithm
*
* @throws Exception
*/
private function algorithm()
{
return Algorithm::create($this->parameter('algorithm'));
}
/**
* @return HeaderList
*
* @throws Exception
*/
private function headerList()
{
return HeaderList::fromString($this->parameter('headers'));
}
/**
* @param string $name
*
* @return string
*
* @throws Exception
*/
private function parameter($name)
{
$parameters = $this->parameters();
if (!isset($parameters[$name])) {
throw new Exception("Signature parameters does not contain '$name'");
}
return $parameters[$name];
}
/**
* @return array
*
* @throws Exception
*/
private function parameters()
{
if (!isset($this->_parameters)) {
$parser = new SignatureParametersParser($this->signatureHeader());
$this->_parameters = $parser->parse();
}
return $this->_parameters;
}
/**
* @return bool
*/
private function hasSignatureHeader()
{
return $this->message->hasHeader('Signature') || $this->message->hasHeader('Authorization');
}
/**
* @return string
*
* @throws Exception
*/
private function signatureHeader()
{
if ($signature = $this->fetchHeader('Signature')) {
return $signature;
} elseif ($authorization = $this->fetchHeader('Authorization')) {
return substr($authorization, strlen('Signature '));
} else {
throw new Exception('HTTP message has no Signature or Authorization header');
}
}
/**
* @param $name
*
* @return string|null
*/
private function fetchHeader($name)
{
// grab the most recently set header.
$header = $this->message->getHeader($name);
return end($header);
}
}

30
src/Verifier.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace HttpSignatures;
use Psr\Http\Message\RequestInterface;
class Verifier
{
/** @var KeyStoreInterface */
private $keyStore;
/**
* @param KeyStoreInterface $keyStore
*/
public function __construct(KeyStoreInterface $keyStore)
{
$this->keyStore = $keyStore;
}
/**
* @param RequestInterface $message
* @return bool
*/
public function isValid($message)
{
$verification = new Verification($message, $this->keyStore);
return $verification->isValid();
}
}