浏览代码

Add HTTPSignatures

Daniel Supernault 6 年之前
父节点
当前提交
c01dc18d04

+ 34 - 0
app/Util/HTTPSignatures/Algorithm.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Util\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;
+        case 'rsa-sha1':
+            return new RsaAlgorithm('sha1');
+            break;
+        case 'rsa-sha256':
+            return new RsaAlgorithm('sha256');
+            break;
+        default:
+            throw new AlgorithmException("No algorithm named '$name'");
+            break;
+        }
+    }
+}

+ 7 - 0
app/Util/HTTPSignatures/AlgorithmException.php

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

+ 19 - 0
app/Util/HTTPSignatures/AlgorithmInterface.php

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

+ 119 - 0
app/Util/HTTPSignatures/Context.php

@@ -0,0 +1,119 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class Context
+{
+    /** @var array */
+    private $headers;
+
+    /** @var KeyStoreInterface */
+    private $keyStore;
+
+    /** @var array */
+    private $keys;
+
+    /** @var string */
+    private $signingKeyId;
+
+    /** @var AlgorithmInterface */
+    private $algorithm;
+
+    /**
+     * @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->algorithm = Algorithm::create($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']) && 1 === count($args['keys'])) {
+            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 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 - 0
app/Util/HTTPSignatures/Exception.php

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

+ 41 - 0
app/Util/HTTPSignatures/GuzzleHttpSignatures.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\Psr7\Request;
+use App\Util\HttpSignatures\Context;
+
+class GuzzleHttpSignatures
+{
+    /**
+     * @param Context $context
+     * @return HandlerStack
+     */
+    public static function defaultHandlerFromContext(Context $context)
+    {
+        $stack = HandlerStack::create();
+        $stack->push(self::middlewareFromContext($context));
+
+        return $stack;
+    }
+
+    /**
+     * @param Context $context
+     * @return \Closure
+     */
+    public static function middlewareFromContext(Context $context)
+    {
+        return function (callable $handler) use ($context)
+        {
+            return function (
+                Request $request,
+                array $options
+            ) use ($handler, $context)
+            {
+                $request = $context->signer()->sign($request);
+                return $handler($request, $options);
+            };
+        };
+    }
+}

+ 48 - 0
app/Util/HTTPSignatures/HeaderList.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace App\Util\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 - 0
app/Util/HTTPSignatures/HmacAlgorithm.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Util\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($secret, $data)
+    {
+        return hash_hmac($this->digestName, $data, $secret, true);
+    }
+}

+ 260 - 0
app/Util/HTTPSignatures/Key.php

@@ -0,0 +1,260 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class Key
+{
+    /** @var string */
+    private $id;
+
+    /** @var string */
+    private $secret;
+
+    /** @var resource */
+    private $certificate;
+
+    /** @var resource */
+    private $publicKey;
+
+    /** @var resource */
+    private $privateKey;
+
+    /** @var string */
+    private $type;
+
+    /**
+     * @param string       $id
+     * @param string|array $secret
+     */
+    public function __construct($id, $item)
+    {
+        $this->id = $id;
+        if (Key::hasX509Certificate($item) || Key::hasPublicKey($item)) {
+            $publicKey = Key::getPublicKey($item);
+        } else {
+            $publicKey = null;
+        }
+        if (Key::hasPrivateKey($item)) {
+            $privateKey = Key::getPrivateKey($item);
+        } else {
+            $privateKey = null;
+        }
+        if (($publicKey || $privateKey)) {
+            $this->type = 'asymmetric';
+            if ($publicKey && $privateKey) {
+                $publicKeyPEM = openssl_pkey_get_details($publicKey)['key'];
+                $privateKeyPublicPEM = openssl_pkey_get_details($privateKey)['key'];
+                if ($privateKeyPublicPEM != $publicKeyPEM) {
+                    throw new KeyException('Supplied Certificate and Key are not related');
+                }
+            }
+            $this->privateKey = $privateKey;
+            $this->publicKey = $publicKey;
+            $this->secret = null;
+        } else {
+            $this->type = 'secret';
+            $this->secret = $item;
+            $this->publicKey = null;
+            $this->privateKey = null;
+        }
+    }
+
+    /**
+     * Retrieves private key resource from a input string or
+     * array of strings.
+     *
+     * @param string|array $object PEM-format Private Key or file path to same
+     *
+     * @return resource|false
+     */
+    public static function getPrivateKey($object)
+    {
+        if (is_array($object)) {
+            foreach ($object as $candidateKey) {
+                $privateKey = Key::getPrivateKey($candidateKey);
+                if ($privateKey) {
+                    return $privateKey;
+                }
+            }
+        } else {
+            // OpenSSL libraries don't have detection methods, so try..catch
+            try {
+                $privateKey = openssl_get_privatekey($object);
+
+                return $privateKey;
+            } catch (\Exception $e) {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Retrieves public key resource from a input string or
+     * array of strings.
+     *
+     * @param string|array $object PEM-format Public Key or file path to same
+     *
+     * @return resource|false
+     */
+    public static function getPublicKey($object)
+    {
+        if (is_array($object)) {
+            // If we implement key rotation in future, this should add to a collection
+            foreach ($object as $candidateKey) {
+                $publicKey = Key::getPublicKey($candidateKey);
+                if ($publicKey) {
+                    return $publicKey;
+                }
+            }
+        } else {
+            // OpenSSL libraries don't have detection methods, so try..catch
+            try {
+                $publicKey = openssl_get_publickey($object);
+
+                return $publicKey;
+            } catch (\Exception $e) {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * Signing HTTP Messages 'keyId' field.
+     *
+     * @return string
+     *
+     * @throws KeyException
+     */
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    /**
+     * Retrieve Verifying Key - Public Key for Asymmetric/PKI, or shared secret for HMAC.
+     *
+     * @return string Shared Secret or PEM-format Public Key
+     *
+     * @throws KeyException
+     */
+    public function getVerifyingKey()
+    {
+        switch ($this->type) {
+        case 'asymmetric':
+            if ($this->publicKey) {
+                return openssl_pkey_get_details($this->publicKey)['key'];
+            } else {
+                return null;
+            }
+            break;
+        case 'secret':
+            return $this->secret;
+        default:
+            throw new KeyException("Unknown key type $this->type");
+        }
+    }
+
+    /**
+     * Retrieve Signing Key - Private Key for Asymmetric/PKI, or shared secret for HMAC.
+     *
+     * @return string Shared Secret or PEM-format Private Key
+     *
+     * @throws KeyException
+     */
+    public function getSigningKey()
+    {
+        switch ($this->type) {
+        case 'asymmetric':
+            if ($this->privateKey) {
+                openssl_pkey_export($this->privateKey, $pem);
+
+                return $pem;
+            } else {
+                return null;
+            }
+            break;
+        case 'secret':
+            return $this->secret;
+        default:
+            throw new KeyException("Unknown key type $this->type");
+        }
+    }
+
+    /**
+     * @return string 'secret' for HMAC or 'asymmetric'
+     */
+    public function getType()
+    {
+        return $this->type;
+    }
+
+    /**
+     * Test if $object is, points to or contains, X.509 PEM-format certificate.
+     *
+     * @param string|array $object PEM Format X.509 Certificate or file path to one
+     *
+     * @return bool
+     */
+    public static function hasX509Certificate($object)
+    {
+        if (is_array($object)) {
+            foreach ($object as $candidateCertificate) {
+                $result = Key::hasX509Certificate($candidateCertificate);
+                if ($result) {
+                    return $result;
+                }
+            }
+        } else {
+            // OpenSSL libraries don't have detection methods, so try..catch
+            try {
+                openssl_x509_export($object, $null);
+
+                return true;
+            } catch (\Exception $e) {
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Test if $object is, points to or contains, PEM-format Public Key.
+     *
+     * @param string|array $object PEM-format Public Key or file path to one
+     *
+     * @return bool
+     */
+    public static function hasPublicKey($object)
+    {
+        if (is_array($object)) {
+            foreach ($object as $candidatePublicKey) {
+                $result = Key::hasPublicKey($candidatePublicKey);
+                if ($result) {
+                    return $result;
+                }
+            }
+        } else {
+            return false == !openssl_pkey_get_public($object);
+        }
+    }
+
+    /**
+     * Test if $object is, points to or contains, PEM-format Private Key.
+     *
+     * @param string|array $object PEM-format Private Key or file path to one
+     *
+     * @return bool
+     */
+    public static function hasPrivateKey($object)
+    {
+        if (is_array($object)) {
+            foreach ($object as $candidatePrivateKey) {
+                $result = Key::hasPrivateKey($candidatePrivateKey);
+                if ($result) {
+                    return $result;
+                }
+            }
+        } else {
+            return  false != openssl_pkey_get_private($object);
+        }
+    }
+}

+ 7 - 0
app/Util/HTTPSignatures/KeyException.php

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

+ 36 - 0
app/Util/HTTPSignatures/KeyStore.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class KeyStore implements KeyStoreInterface
+{
+    /** @var Key[] */
+    private $keys;
+
+    /**
+     * @param array $keys
+     */
+    public function __construct($keys)
+    {
+        $this->keys = [];
+        foreach ($keys as $id => $key) {
+            $this->keys[$id] = new Key($id, $key);
+        }
+    }
+
+    /**
+     * @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");
+        }
+    }
+}

+ 7 - 0
app/Util/HTTPSignatures/KeyStoreException.php

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

+ 15 - 0
app/Util/HTTPSignatures/KeyStoreInterface.php

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

+ 64 - 0
app/Util/HTTPSignatures/RsaAlgorithm.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+class RsaAlgorithm implements AlgorithmInterface
+{
+    /** @var string */
+    private $digestName;
+
+    /**
+     * @param string $digestName
+     */
+    public function __construct($digestName)
+    {
+        $this->digestName = $digestName;
+    }
+
+    /**
+     * @return string
+     */
+    public function name()
+    {
+        return sprintf('rsa-%s', $this->digestName);
+    }
+
+    /**
+     * @param string $key
+     * @param string $data
+     *
+     * @return string
+     *
+     * @throws \HttpSignatures\AlgorithmException
+     */
+    public function sign($signingKey, $data)
+    {
+        $algo = $this->getRsaHashAlgo($this->digestName);
+        if (!openssl_get_privatekey($signingKey)) {
+            throw new AlgorithmException("OpenSSL doesn't understand the supplied key (not valid or not found)");
+        }
+        $signature = '';
+        openssl_sign($data, $signature, $signingKey, $algo);
+
+        return $signature;
+    }
+
+    public function verify($message, $signature, $verifyingKey)
+    {
+        $algo = $this->getRsaHashAlgo($this->digestName);
+
+        return openssl_verify($message, base64_decode($signature), $verifyingKey, $algo);
+    }
+
+    private function getRsaHashAlgo($digestName)
+    {
+        switch ($digestName) {
+        case 'sha256':
+            return OPENSSL_ALGO_SHA256;
+        case 'sha1':
+            return OPENSSL_ALGO_SHA1;
+        default:
+            throw new HttpSignatures\AlgorithmException($digestName.' is not a supported hash format');
+      }
+    }
+}

+ 38 - 0
app/Util/HTTPSignatures/Signature.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace App\Util\HttpSignatures;
+
+use Psr\Http\Message\RequestInterface;
+
+class Signature
+{
+    /** @var Key */
+    private $key;
+
+    /** @var AlgorithmInterface */
+    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->getSigningKey(),
+            $this->signingString->string()
+          );
+    }
+}

+ 49 - 0
app/Util/HTTPSignatures/SignatureParameters.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Util\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->getId()),
+            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());
+    }
+}

+ 111 - 0
app/Util/HTTPSignatures/SignatureParametersParser.php

@@ -0,0 +1,111 @@
+<?php
+
+namespace App\Util\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 (1 !== $result) {
+            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");
+        }
+    }
+}

+ 7 - 0
app/Util/HTTPSignatures/SignatureParseException.php

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

+ 7 - 0
app/Util/HTTPSignatures/SignedHeaderNotPresentException.php

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

+ 104 - 0
app/Util/HTTPSignatures/Signer.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace App\Util\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 RequestInterface
+     */
+    public function signWithDigest($message)
+    {
+        $message = $this->addDigest($message);
+
+        return $this->sign($message);
+    }
+
+    /**
+     * @param RequestInterface $message
+     *
+     * @return RequestInterface
+     */
+    private function addDigest($message)
+    {
+        if (!array_search('digest', $this->headerList->names)) {
+            $this->headerList->names[] = 'digest';
+        }
+        $message = $message->withoutHeader('Digest')
+            ->withHeader(
+                'Digest',
+                'SHA-256='.base64_encode(hash('sha256', $message->getBody(), true))
+            );
+
+        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
+        );
+    }
+}

+ 89 - 0
app/Util/HTTPSignatures/SigningString.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace App\Util\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 ('(request-target)' == $name) {
+            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()
+        );
+    }
+}

+ 202 - 0
app/Util/HTTPSignatures/Verification.php

@@ -0,0 +1,202 @@
+<?php
+
+namespace App\Util\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 {
+            $key = $this->key();
+            switch ($key->getType()) {
+                case 'secret':
+                  $random = random_bytes(32);
+                  $expectedResult = hash_hmac(
+                      'sha256', $this->expectedSignatureBase64(),
+                      $random,
+                      true
+                  );
+                  $providedResult = hash_hmac(
+                      'sha256', $this->providedSignatureBase64(),
+                      $random,
+                      true
+                  );
+
+                  return $expectedResult === $providedResult;
+                case 'asymmetric':
+                    $signedString = new SigningString(
+                        $this->headerList(),
+                        $this->message
+                    );
+                    $hashAlgo = explode('-', $this->parameter('algorithm'))[1];
+                    $algorithm = new RsaAlgorithm($hashAlgo);
+                    $result = $algorithm->verify(
+                        $signedString->string(),
+                        $this->parameter('signature'),
+                        $key->getVerifyingKey());
+
+                    return $result;
+                default:
+                    throw new Exception("Unknown key type '".$key->getType()."', cannot verify");
+            }
+        } 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
+     */
+    private function providedSignatureBase64()
+    {
+        return $this->parameter('signature');
+    }
+
+    /**
+     * @return Key
+     */
+    private function key()
+    {
+        return $this->keyStore->fetch($this->parameter('keyId'));
+    }
+
+    /**
+     * @return HmacAlgorithm
+     */
+    private function algorithm()
+    {
+        return Algorithm::create($this->parameter('algorithm'));
+    }
+
+    /**
+     * @return HeaderList
+     */
+    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
+     */
+    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);
+    }
+}

+ 31 - 0
app/Util/HTTPSignatures/Verifier.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace App\Util\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();
+    }
+}