From c01dc18d0435c5b74c16532f981d3bbb0c2f3903 Mon Sep 17 00:00:00 2001 From: Daniel Supernault Date: Sun, 18 Nov 2018 20:33:10 -0700 Subject: [PATCH] Add HTTPSignatures --- app/Util/HTTPSignatures/Algorithm.php | 34 +++ .../HTTPSignatures/AlgorithmException.php | 7 + .../HTTPSignatures/AlgorithmInterface.php | 19 ++ app/Util/HTTPSignatures/Context.php | 119 ++++++++ app/Util/HTTPSignatures/Exception.php | 7 + .../HTTPSignatures/GuzzleHttpSignatures.php | 41 +++ app/Util/HTTPSignatures/HeaderList.php | 48 ++++ app/Util/HTTPSignatures/HmacAlgorithm.php | 36 +++ app/Util/HTTPSignatures/Key.php | 260 ++++++++++++++++++ app/Util/HTTPSignatures/KeyException.php | 7 + app/Util/HTTPSignatures/KeyStore.php | 36 +++ app/Util/HTTPSignatures/KeyStoreException.php | 7 + app/Util/HTTPSignatures/KeyStoreInterface.php | 15 + app/Util/HTTPSignatures/RsaAlgorithm.php | 64 +++++ app/Util/HTTPSignatures/Signature.php | 38 +++ .../HTTPSignatures/SignatureParameters.php | 49 ++++ .../SignatureParametersParser.php | 111 ++++++++ .../SignatureParseException.php | 7 + .../SignedHeaderNotPresentException.php | 7 + app/Util/HTTPSignatures/Signer.php | 104 +++++++ app/Util/HTTPSignatures/SigningString.php | 89 ++++++ app/Util/HTTPSignatures/Verification.php | 202 ++++++++++++++ app/Util/HTTPSignatures/Verifier.php | 31 +++ 23 files changed, 1338 insertions(+) create mode 100755 app/Util/HTTPSignatures/Algorithm.php create mode 100755 app/Util/HTTPSignatures/AlgorithmException.php create mode 100755 app/Util/HTTPSignatures/AlgorithmInterface.php create mode 100755 app/Util/HTTPSignatures/Context.php create mode 100755 app/Util/HTTPSignatures/Exception.php create mode 100644 app/Util/HTTPSignatures/GuzzleHttpSignatures.php create mode 100755 app/Util/HTTPSignatures/HeaderList.php create mode 100755 app/Util/HTTPSignatures/HmacAlgorithm.php create mode 100755 app/Util/HTTPSignatures/Key.php create mode 100755 app/Util/HTTPSignatures/KeyException.php create mode 100755 app/Util/HTTPSignatures/KeyStore.php create mode 100755 app/Util/HTTPSignatures/KeyStoreException.php create mode 100755 app/Util/HTTPSignatures/KeyStoreInterface.php create mode 100755 app/Util/HTTPSignatures/RsaAlgorithm.php create mode 100755 app/Util/HTTPSignatures/Signature.php create mode 100755 app/Util/HTTPSignatures/SignatureParameters.php create mode 100755 app/Util/HTTPSignatures/SignatureParametersParser.php create mode 100755 app/Util/HTTPSignatures/SignatureParseException.php create mode 100755 app/Util/HTTPSignatures/SignedHeaderNotPresentException.php create mode 100755 app/Util/HTTPSignatures/Signer.php create mode 100755 app/Util/HTTPSignatures/SigningString.php create mode 100755 app/Util/HTTPSignatures/Verification.php create mode 100755 app/Util/HTTPSignatures/Verifier.php diff --git a/app/Util/HTTPSignatures/Algorithm.php b/app/Util/HTTPSignatures/Algorithm.php new file mode 100755 index 00000000..0ba7188b --- /dev/null +++ b/app/Util/HTTPSignatures/Algorithm.php @@ -0,0 +1,34 @@ + 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; + } +} diff --git a/app/Util/HTTPSignatures/Exception.php b/app/Util/HTTPSignatures/Exception.php new file mode 100755 index 00000000..c41918dc --- /dev/null +++ b/app/Util/HTTPSignatures/Exception.php @@ -0,0 +1,7 @@ +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); + }; + }; + } +} \ No newline at end of file diff --git a/app/Util/HTTPSignatures/HeaderList.php b/app/Util/HTTPSignatures/HeaderList.php new file mode 100755 index 00000000..7dad4c13 --- /dev/null +++ b/app/Util/HTTPSignatures/HeaderList.php @@ -0,0 +1,48 @@ +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); + } +} diff --git a/app/Util/HTTPSignatures/HmacAlgorithm.php b/app/Util/HTTPSignatures/HmacAlgorithm.php new file mode 100755 index 00000000..a83f7b77 --- /dev/null +++ b/app/Util/HTTPSignatures/HmacAlgorithm.php @@ -0,0 +1,36 @@ +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); + } +} diff --git a/app/Util/HTTPSignatures/Key.php b/app/Util/HTTPSignatures/Key.php new file mode 100755 index 00000000..6d576c73 --- /dev/null +++ b/app/Util/HTTPSignatures/Key.php @@ -0,0 +1,260 @@ +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); + } + } +} diff --git a/app/Util/HTTPSignatures/KeyException.php b/app/Util/HTTPSignatures/KeyException.php new file mode 100755 index 00000000..0b56d1dd --- /dev/null +++ b/app/Util/HTTPSignatures/KeyException.php @@ -0,0 +1,7 @@ +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"); + } + } +} diff --git a/app/Util/HTTPSignatures/KeyStoreException.php b/app/Util/HTTPSignatures/KeyStoreException.php new file mode 100755 index 00000000..9e982394 --- /dev/null +++ b/app/Util/HTTPSignatures/KeyStoreException.php @@ -0,0 +1,7 @@ +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'); + } + } +} diff --git a/app/Util/HTTPSignatures/Signature.php b/app/Util/HTTPSignatures/Signature.php new file mode 100755 index 00000000..5397d150 --- /dev/null +++ b/app/Util/HTTPSignatures/Signature.php @@ -0,0 +1,38 @@ +key = $key; + $this->algorithm = $algorithm; + $this->signingString = new SigningString($headerList, $message); + } + + public function string() + { + return $this->algorithm->sign( + $this->key->getSigningKey(), + $this->signingString->string() + ); + } +} diff --git a/app/Util/HTTPSignatures/SignatureParameters.php b/app/Util/HTTPSignatures/SignatureParameters.php new file mode 100755 index 00000000..41387c72 --- /dev/null +++ b/app/Util/HTTPSignatures/SignatureParameters.php @@ -0,0 +1,49 @@ +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()); + } +} diff --git a/app/Util/HTTPSignatures/SignatureParametersParser.php b/app/Util/HTTPSignatures/SignatureParametersParser.php new file mode 100755 index 00000000..b7ea25cb --- /dev/null +++ b/app/Util/HTTPSignatures/SignatureParametersParser.php @@ -0,0 +1,111 @@ +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"); + } + } +} diff --git a/app/Util/HTTPSignatures/SignatureParseException.php b/app/Util/HTTPSignatures/SignatureParseException.php new file mode 100755 index 00000000..c32dab50 --- /dev/null +++ b/app/Util/HTTPSignatures/SignatureParseException.php @@ -0,0 +1,7 @@ +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 + ); + } +} diff --git a/app/Util/HTTPSignatures/SigningString.php b/app/Util/HTTPSignatures/SigningString.php new file mode 100755 index 00000000..9a453852 --- /dev/null +++ b/app/Util/HTTPSignatures/SigningString.php @@ -0,0 +1,89 @@ +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() + ); + } +} diff --git a/app/Util/HTTPSignatures/Verification.php b/app/Util/HTTPSignatures/Verification.php new file mode 100755 index 00000000..85ece7cf --- /dev/null +++ b/app/Util/HTTPSignatures/Verification.php @@ -0,0 +1,202 @@ +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); + } +} diff --git a/app/Util/HTTPSignatures/Verifier.php b/app/Util/HTTPSignatures/Verifier.php new file mode 100755 index 00000000..ccf260ee --- /dev/null +++ b/app/Util/HTTPSignatures/Verifier.php @@ -0,0 +1,31 @@ +keyStore = $keyStore; + } + + /** + * @param RequestInterface $message + * + * @return bool + */ + public function isValid($message) + { + $verification = new Verification($message, $this->keyStore); + + return $verification->isValid(); + } +}