diff --git a/src/Context.php b/src/Context.php index 5dd73e9..e7d84ee 100644 --- a/src/Context.php +++ b/src/Context.php @@ -21,7 +21,7 @@ class Context * * @throws Exception */ - public function __construct($args) + public function __construct($args = null) { if (isset($args['keys']) && isset($args['keyStore'])) { throw new Exception(__CLASS__.' accepts keys or keyStore but not both'); @@ -104,6 +104,9 @@ private function headerList() private function keyStore() { if (empty($this->keyStore)) { + if (is_null($this->keys)) { + return null; + } $this->keyStore = new KeyStore($this->keys); } @@ -122,4 +125,16 @@ private function algorithm() { return $this->algorithm; } + + public function addKeys($keys) + { + foreach ($keys as $keyId => $keyValue) { + $newKey = new Key($keyId, $keyValue); + $newKeys[$keyId] = $keyValue; + } + if (empty($this->keyStore)) { + $this->keyStore = new KeyStore(); + } + $this->keyStore = $this->keyStore->withKeys($newKeys); + } } diff --git a/src/KeyStore.php b/src/KeyStore.php index bedff36..68ebb6b 100644 --- a/src/KeyStore.php +++ b/src/KeyStore.php @@ -10,11 +10,13 @@ class KeyStore implements KeyStoreInterface /** * @param array $keys */ - public function __construct($keys) + public function __construct($keys = null) { $this->keys = []; - foreach ($keys as $id => $key) { - $this->keys[$id] = new Key($id, $key); + if (!empty($keys)) { + foreach ($keys as $id => $key) { + $this->keys[$id] = new Key($id, $key); + } } } @@ -33,4 +35,18 @@ public function fetch($keyId) throw new KeyStoreException("Key '$keyId' not found"); } } + + public function withKeys($keys) + { + foreach ($keys as $keyId => $value) { + $this->keys[$keyId] = new Key($keyId, $value); + } + + return $this; + } + + public function getCount() + { + return sizeof($this->keys); + } } diff --git a/src/SignatureParametersParser.php b/src/SignatureParametersParser.php index 13280d2..c43d3bb 100644 --- a/src/SignatureParametersParser.php +++ b/src/SignatureParametersParser.php @@ -91,7 +91,7 @@ private function pair($segment) */ private function validate($result) { - $this->validateAllKeysArePresent($result); + $this->validateRequiredKeysArePresent($result); } /** @@ -99,7 +99,7 @@ private function validate($result) * * @throws SignatureParseException */ - private function validateAllKeysArePresent($result) + private function validateRequiredKeysArePresent($result) { // Regexp in pair() ensures no unwanted keys exist. // Ensure that all mandatory keys exist. diff --git a/src/Verification.php b/src/Verification.php index 70cbe52..844f20e 100644 --- a/src/Verification.php +++ b/src/Verification.php @@ -22,7 +22,7 @@ class Verification * @param RequestInterface $message * @param KeyStoreInterface $keyStore */ - public function __construct($message, KeyStoreInterface $keyStore, $header) + public function __construct($message, KeyStoreInterface $keyStore = null, $header, $headers = []) { $this->message = $message; $this->keyStore = $keyStore; diff --git a/src/Verifier.php b/src/Verifier.php index 4180ac2..76e7b5c 100644 --- a/src/Verifier.php +++ b/src/Verifier.php @@ -17,9 +17,12 @@ class Verifier /** * @param KeyStoreInterface $keyStore */ - public function __construct(KeyStoreInterface $keyStore) + public function __construct(KeyStoreInterface $keyStore = null, $minimumHeaders = []) { + // if ( $keyStore ) { $this->keyStore = $keyStore; + // }; + $this->minimumHeaders = $minimumHeaders; $this->status = []; } @@ -30,6 +33,11 @@ public function __construct(KeyStoreInterface $keyStore) */ public function isSigned($message) { + if (is_null($this->keyStore)) { + $this->status[] = 'No keys provided, cannot verify'; + + return false; + } $this->status = []; try { $verification = new Verification($message, $this->keyStore, 'Signature'); @@ -83,6 +91,11 @@ public function isSigned($message) */ public function isAuthorized($message) { + if (is_null($this->keyStore)) { + $this->status[] = 'No keys provided, cannot verify'; + + return false; + } $this->status = []; try { $verification = new Verification($message, $this->keyStore, 'Authorization'); @@ -184,4 +197,40 @@ public function getStatus() { return $this->status; } + + public function getSignatureParameters($message) + { + $signatureLine = $message->getHeader('Signature')[0]; + $signatureParametersParser = new SignatureParametersParser( + $signatureLine + ); + + return $signatureParametersParser->parse(); + } + + public function getSignatureKeyId($message) + { + return $this->getSignatureParameters($message)['keyId']; + } + + public function getSignatureHeaders($message, $parameter) + { + $parameters = $this->getSignatureParameters($message); + if (!isset($parameters['headers'])) { + return ['date']; + } + $headers = explode(' ', $parameters['headers']); + + return $headers; + } + + public function setMinimumHeaders(array $minimumHeaders) + { + $this->minimumHeaders = $minimumHeaders; + } + + public function getMinimumHeaders() + { + return $this->minimumHeaders; + } } diff --git a/tests/KeyStoreTest.php b/tests/KeyStoreTest.php index 4adf6b7..3c2130a 100644 --- a/tests/KeyStoreTest.php +++ b/tests/KeyStoreTest.php @@ -7,6 +7,35 @@ class KeyStoreTest extends TestCase { + public function testKeyStore() + { + $ks = new KeyStore(['testkey' => 'abc123']); + $this->assertEquals( + 'abc123', + $ks->fetch('testkey')->getSigningKey() + ); + $this->assertEquals( + 'secret', + $ks->fetch('testkey')->getType() + ); + $ks = $ks->withKeys([ + 'testkey2' => 'def456', + 'testkey3' => 'foo-bar', + ]); + $this->assertEquals( + 3, + $ks->getCount() + ); + $this->assertEquals( + 'abc123', + $ks->fetch('testkey')->getSigningKey() + ); + $this->assertEquals( + 'secret', + $ks->fetch('testkey2')->getType() + ); + } + public function testFetchFail() { $ks = new KeyStore(['id' => 'secret']); diff --git a/tests/VerifierContextTest.php b/tests/VerifierContextTest.php new file mode 100644 index 0000000..e3adb41 --- /dev/null +++ b/tests/VerifierContextTest.php @@ -0,0 +1,189 @@ +setUpVerifier(); + $this->setUpSignedMessage(); + $this->setUpAuthorizedMessage(); + $this->setUpSignedAndAuthorizedMessage(); + } + + private function setUpVerifier() + { + // $keyStore = new KeyStore(['pda' => 'secret']); + $this->verifierCompleteContext = new Context([ + 'keys' => ['pda' => 'secret'], + 'headers' => ['(request-target)', 'date'], + ]); + $this->verifierEmptyContext = new Context([ + ]); + } + + private function setUpSignedMessage() + { + $signatureLine = sprintf( + 'keyId="%s",algorithm="%s",headers="%s",signature="%s"', + 'pda', + 'hmac-sha256', + '(request-target) date', + 'cS2VvndvReuTLy52Ggi4j6UaDqGm9hMb4z0xJZ6adqU=' + ); + + $this->signedMessage = new Request('GET', '/path?query=123', [ + 'Date' => self::DATE, + 'Signature' => $signatureLine, + 'Authorization' => 'Bearer abc123', + ]); + + $signatureLineNoHeaders = sprintf( + 'keyId="%s",algorithm="%s",signature="%s"', + 'pda', + 'hmac-sha256', + 'cS2VvndvReuTLy52Ggi4j6UaDqGm9hMb4z0xJZ6adqU=' + ); + + $this->signedMessageNoHeaders = new Request('GET', '/path?query=123', [ + 'Date' => self::DATE, + 'Signature' => $signatureLineNoHeaders, + 'Authorization' => 'Bearer abc123', + ]); + } + + private function setUpAuthorizedMessage() + { + $authorizationHeader = sprintf( + 'Signature keyId="%s",algorithm="%s",headers="%s",signature="%s"', + 'pda', + 'hmac-sha256', + '(request-target) date', + 'cS2VvndvReuTLy52Ggi4j6UaDqGm9hMb4z0xJZ6adqU=' + ); + + $this->authorizedMessage = new Request('GET', '/path?query=123', [ + 'Date' => self::DATE, + 'Authorization' => $authorizationHeader, + 'Signature' => 'My Lawyer signed this', + ]); + } + + private function setUpSignedAndAuthorizedMessage() + { + $authorizationHeader = sprintf( + 'Signature keyId="%s",algorithm="%s",headers="%s",signature="%s"', + 'pda', + 'hmac-sha256', + '(request-target) date', + 'cS2VvndvReuTLy52Ggi4j6UaDqGm9hMb4z0xJZ6adqU=' + ); + $signatureHeader = sprintf( + 'keyId="%s",algorithm="%s",headers="%s",signature="%s"', + 'pda', + 'hmac-sha256', + '(request-target) date', + 'cS2VvndvReuTLy52Ggi4j6UaDqGm9hMb4z0xJZ6adqU=' + ); + + $this->signedAndAuthorizedMessage = new Request('GET', '/path?query=123', [ + 'Date' => self::DATE, + 'Authorization' => $authorizationHeader, + 'Signature' => $signatureHeader, + ]); + } + + public function testVerifySignedMessage() + { + $verifier = $this->verifierCompleteContext->verifier(); + $this->assertTrue($verifier->isSigned($this->signedMessage)); + $this->assertEquals( + "Signed with SigningString 'KHJlcXVlc3QtdGFyZ2V0KTogZ2V0IC9wYXRoP3F1ZXJ5PTEyMwpkYXRlOiBGcmksIDAxIEF1ZyAyMDE0IDEzOjQ0OjMyIC0wNzAw'", + $verifier->getStatus()[0] + ); + $verifier->isSigned($this->signedMessage); + $this->assertEquals( + 1, + sizeof($verifier->getStatus()) + ); + } + + public function testVerifyAuthorizedMessage() + { + $verifier = $this->verifierCompleteContext->verifier(); + $this->assertTrue($verifier->isAuthorized($this->authorizedMessage)); + $this->assertEquals( + "Authorized with SigningString 'KHJlcXVlc3QtdGFyZ2V0KTogZ2V0IC9wYXRoP3F1ZXJ5PTEyMwpkYXRlOiBGcmksIDAxIEF1ZyAyMDE0IDEzOjQ0OjMyIC0wNzAw'", + $verifier->getStatus()[0] + ); + $verifier->isAuthorized($this->authorizedMessage); + $this->assertEquals( + 1, + sizeof($verifier->getStatus()) + ); + } + + public function testProvidedParameters() + { + $verifier = $this->verifierCompleteContext->verifier(); + $this->assertEquals( + ['(request-target)', 'date'], + $verifier->getSignatureHeaders($this->signedMessage, 'headers') + ); + $verifier = $this->verifierCompleteContext->verifier(); + $this->assertEquals( + ['date'], + $verifier->getSignatureHeaders($this->signedMessageNoHeaders, 'headers') + ); + } + + public function testVerifyInjectKey() + { + $dummyKey = ['abc' => 'notsosecret']; + $usefulKey = ['pda' => 'secret']; + $context = new Context(['keys' => $dummyKey]); + $verifier = $context->verifier(); + $this->assertFalse($verifier->isSigned($this->signedMessage)); + $this->assertEquals( + "Cannot locate key for supplied keyId 'pda'", + $verifier->getStatus()[0] + ); + $requiredKeyId = $verifier->getSignatureKeyId($this->signedMessage); + $this->assertEquals( + 'pda', + $requiredKeyId + ); + $context->addKeys($usefulKey); + $verifier = $context->verifier(); + $this->assertTrue($verifier->isSigned($this->signedMessage)); + } +}