فهرست منبع

Add HTTPSignature tests

Daniel Supernault 6 سال پیش
والد
کامیت
75fa22ea0f

+ 20 - 0
tests/Unit/HttpSignatures/HeaderListTest.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace Tests\Unit\HttpSignatures;
+
+use App\Util\HttpSignatures\HeaderList;
+
+class HeaderListTest extends \PHPUnit\Framework\TestCase
+{
+    public function testToString()
+    {
+        $hl = new HeaderList(['(request-target)', 'Date']);
+        $this->assertEquals('(request-target) date', $hl->string());
+    }
+
+    public function testFromStringRoundTripNormalized()
+    {
+        $hl = HeaderList::fromString('(request-target) Accept');
+        $this->assertEquals('(request-target) accept', $hl->string());
+    }
+}

+ 190 - 0
tests/Unit/HttpSignatures/HmacContextTest.php

@@ -0,0 +1,190 @@
+<?php
+
+namespace Tests\Unit\HttpSignatures;
+
+use GuzzleHttp\Psr7\Request;
+use App\Util\HttpSignatures\Context;
+
+class HmacContextTest extends \PHPUnit\Framework\TestCase
+{
+    private $context;
+
+    public function setUp()
+    {
+        $this->noDigestContext = new Context([
+            'keys' => ['pda' => 'secret'],
+            'algorithm' => 'hmac-sha256',
+            'headers' => ['(request-target)', 'date'],
+        ]);
+        $this->withDigestContext = new Context([
+            'keys' => ['pda' => 'secret'],
+            'algorithm' => 'hmac-sha256',
+            'headers' => ['(request-target)', 'date', 'digest'],
+        ]);
+    }
+
+    public function testSignerNoDigestAction()
+    {
+        $message = new Request('GET', '/path?query=123', ['date' => 'today', 'accept' => 'llamas']);
+        $message = $this->noDigestContext->signer()->sign($message);
+
+        $expectedString = implode(',', [
+            'keyId="pda"',
+            'algorithm="hmac-sha256"',
+            'headers="(request-target) date"',
+            'signature="SFlytCGpsqb/9qYaKCQklGDvwgmrwfIERFnwt+yqPJw="',
+        ]);
+
+        $this->assertEquals(
+            $expectedString,
+            $message->getHeader('Signature')[0]
+        );
+
+        $this->assertEquals(
+            'Signature '.$expectedString,
+            $message->getHeader('Authorization')[0]
+        );
+    }
+
+    public function testSignerAddDigestToHeadersList()
+    {
+        $message = new Request(
+            'POST', '/path/to/things?query=123',
+            ['date' => 'today', 'accept' => 'llamas'],
+            'Thing to POST');
+        $message = $this->noDigestContext->signer()->signWithDigest($message);
+
+        $expectedString = implode(',', [
+            'keyId="pda"',
+            'algorithm="hmac-sha256"',
+            'headers="(request-target) date digest"',
+            'signature="HH6R3OJmJbKUFqqL0tGVIIb7xi1WbbSh/HBXHUtLkUs="', ]);
+        $expectedDigestHeader =
+          'SHA-256=rEcNhYZoBKiR29D30w1JcgArNlF8rXIXf5MnIL/4kcc=';
+
+        $this->assertEquals(
+            $expectedString,
+            $message->getHeader('Signature')[0]
+        );
+
+        $this->assertEquals(
+            $expectedDigestHeader,
+            $message->getHeader('Digest')[0]
+        );
+
+        $this->assertEquals(
+            'Signature '.$expectedString,
+            $message->getHeader('Authorization')[0]
+        );
+    }
+
+    public function testSignerReplaceDigest()
+    {
+        $message = new Request(
+            'PUT', '/things/thething?query=123',
+              ['date' => 'today',
+              'accept' => 'llamas',
+              'Digest' => 'SHA-256=E/P+4y4x6EySO9qNAjCtQKxVwE1xKsNI/k+cjK+vtLU=', ],
+            'Thing to PUT at /things/thething please...');
+        $message = $this->noDigestContext->signer()->signWithDigest($message);
+
+        $expectedString = implode(',', [
+            'keyId="pda"',
+            'algorithm="hmac-sha256"',
+            'headers="(request-target) date digest"',
+            'signature="Hyatt1lSR/4XLI9Gcx8XOEKiG8LVktH7Lfr+0tmhwRU="', ]);
+        $expectedDigestHeader =
+          'SHA-256=mulOx+77mQU1EbPET50SCGA4P/4bYxVCJA1pTwJsaMw=';
+
+        $this->assertEquals(
+            $expectedString,
+            $message->getHeader('Signature')[0]
+        );
+
+        $this->assertEquals(
+            $expectedDigestHeader,
+            $message->getHeader('Digest')[0]
+        );
+
+        $this->assertEquals(
+            'Signature '.$expectedString,
+            $message->getHeader('Authorization')[0]
+        );
+    }
+
+    public function testSignerNewDigestIsInHeaderList()
+    {
+        $message = new Request(
+            'POST', '/path?query=123',
+              ['date' => 'today',
+              'accept' => 'llamas', ],
+            'Stuff that belongs in /path');
+        $message = $this->withDigestContext->signer()->signWithDigest($message);
+
+        $expectedString = implode(',', [
+            'keyId="pda"',
+            'algorithm="hmac-sha256"',
+            'headers="(request-target) date digest"',
+            'signature="p8gQHs59X2WzQLUecfmxm1YO0OBTCNKldRZZBQsepfk="', ]);
+        $expectedDigestHeader =
+          'SHA-256=jnSMEfBSum4Rh2k6/IVFyvLuQLmGYwMAGBS9WybyDqQ=';
+
+        $this->assertEquals(
+            $expectedString,
+            $message->getHeader('Signature')[0]
+        );
+
+        $this->assertEquals(
+            $expectedDigestHeader,
+            $message->getHeader('Digest')[0]
+        );
+
+        $this->assertEquals(
+            'Signature '.$expectedString,
+            $message->getHeader('Authorization')[0]
+        );
+    }
+
+    public function testSignerNewDigestWithoutBody()
+    {
+        $message = new Request(
+            'GET', '/path?query=123',
+              ['date' => 'today',
+              'accept' => 'llamas', ]);
+        $message = $this->withDigestContext->signer()->signWithDigest($message);
+
+        $expectedString = implode(',', [
+            'keyId="pda"',
+            'algorithm="hmac-sha256"',
+            'headers="(request-target) date digest"',
+            'signature="7iFqqryI6I9opV/Zp3eEg6PDY1tKw/3GqioOM7ACHHA="', ]);
+        $zeroLengthStringDigest =
+          'SHA-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=';
+
+        $this->assertEquals(
+            $expectedString,
+            $message->getHeader('Signature')[0]
+        );
+
+        $this->assertEquals(
+            $zeroLengthStringDigest,
+            $message->getHeader('Digest')[0]
+        );
+
+        $this->assertEquals(
+            'Signature '.$expectedString,
+            $message->getHeader('Authorization')[0]
+        );
+    }
+
+    public function testVerifier()
+    {
+        $message = $this->noDigestContext->signer()->sign(new Request('GET', '/path?query=123', [
+            'Signature' => 'keyId="pda",algorithm="hmac-sha1",headers="date",signature="x"',
+            'Date' => 'x',
+        ]));
+
+        // assert it works without errors; correctness of results tested elsewhere.
+        $this->assertTrue(is_bool($this->noDigestContext->verifier()->isValid($message)));
+    }
+}

+ 16 - 0
tests/Unit/HttpSignatures/KeyStoreHmacTest.php

@@ -0,0 +1,16 @@
+<?php
+
+namespace Tests\Unit\HttpSignatures;
+
+use App\Util\HttpSignatures\KeyStore;
+
+class KeyStoreHmacTest extends \PHPUnit\Framework\TestCase
+{
+    public function testFetchHmacSuccess()
+    {
+        $ks = new KeyStore(['hmacsecret' => 'ThisIsASecretKey']);
+        $key = $ks->fetch('hmacsecret');
+        $this->assertEquals(['hmacsecret', 'ThisIsASecretKey', 'ThisIsASecretKey', 'secret'], [
+          $key->getId(), $key->getVerifyingKey(), $key->getSigningKey(), $key->getType(), ]);
+    }
+}

+ 114 - 0
tests/Unit/HttpSignatures/KeyStoreRsaTest.php

@@ -0,0 +1,114 @@
+<?php
+
+namespace Tests\Unit\HttpSignatures;
+
+use App\Util\HttpSignatures\KeyStore;
+use App\Util\HttpSignatures\Key;
+use Tests\Unit\HttpSignatures\TestKeys;
+
+class KeyStoreRsaTest extends \PHPUnit\Framework\TestCase
+{
+    public function setUp()
+    {
+        openssl_pkey_export(
+            openssl_pkey_get_private(TestKeys::rsaPrivateKey),
+            $this->testRsaPrivateKeyPEM
+        );
+        $this->testRsaPublicKeyPEM = openssl_pkey_get_details(
+            openssl_get_publickey(TestKeys::rsaPublicKey)
+        )['key'];
+        $this->testRsaCert = TestKeys::rsaCert;
+    }
+
+    public function testParseX509inObject()
+    {
+        $keySpec = ['rsaCert' => [TestKeys::rsaCert]];
+        $this->assertTrue(Key::hasX509Certificate($keySpec));
+
+        $ks = new KeyStore($keySpec);
+        $publicKey = $ks->fetch('rsaCert')->getVerifyingKey();
+        $this->assertEquals('asymmetric', $ks->fetch('rsaCert')->getType());
+        $this->assertEquals(TestKeys::rsaPublicKey, $publicKey);
+    }
+
+    public function testParseRsaPublicKeyinObject()
+    {
+        $keySpec = ['rsaPubKey' => [TestKeys::rsaPublicKey]];
+        $this->assertTrue(Key::hasPublicKey($keySpec));
+
+        $ks = new KeyStore($keySpec);
+        $publicKey = $ks->fetch('rsaPubKey')->getVerifyingKey();
+        $this->assertEquals('asymmetric', $ks->fetch('rsaPubKey')->getType());
+        $this->assertEquals(TestKeys::rsaPublicKey, $publicKey);
+    }
+
+    public function testParsePrivateKeyinObject()
+    {
+        $keySpec = ['rsaPrivKey' => [TestKeys::rsaPrivateKey]];
+        $this->assertTrue(Key::hasPrivateKey($keySpec));
+
+        $ks = new KeyStore($keySpec);
+        $publicKey = $ks->fetch('rsaPrivKey')->getSigningKey();
+        $this->assertEquals('asymmetric', $ks->fetch('rsaPrivKey')->getType());
+        $this->assertEquals($this->testRsaPrivateKeyPEM, $publicKey);
+    }
+
+    public function testFetchRsaSigningKeySuccess()
+    {
+        $ks = new KeyStore(['rsakey' => TestKeys::rsaPrivateKey]);
+        $key = $ks->fetch('rsakey');
+        openssl_pkey_export($key->getSigningKey(), $keyStoreSigningKey);
+        $this->assertEquals(['rsakey', $this->testRsaPrivateKeyPEM, null, 'asymmetric'], [
+          $key->getId(), $keyStoreSigningKey, $key->getVerifyingKey(), $key->getType(), ]);
+    }
+
+    public function testFetchRsaVerifyingKeyFromCertificateSuccess()
+    {
+        $ks = new KeyStore(['rsacert' => TestKeys::rsaCert]);
+        $key = $ks->fetch('rsacert');
+        $keyStoreVerifyingKey = $key->getVerifyingKey();
+        $this->assertEquals(['rsacert', null, $this->testRsaPublicKeyPEM, 'asymmetric'], [
+          $key->getId(), $key->getSigningKey(), $keyStoreVerifyingKey, $key->getType(), ]);
+    }
+
+    public function testFetchRsaVerifyingKeyFromPublicKeySuccess()
+    {
+        $ks = new KeyStore(['rsapubkey' => TestKeys::rsaPublicKey]);
+        $key = $ks->fetch('rsapubkey');
+        $keyStoreVerifyingKey = $key->getVerifyingKey();
+        $this->assertEquals(['rsapubkey', null, $this->testRsaPublicKeyPEM, 'asymmetric'], [
+          $key->getId(), $key->getSigningKey(), $keyStoreVerifyingKey, $key->getType(), ]);
+    }
+
+    public function testFetchRsaBothSuccess()
+    {
+        $ks = new KeyStore(['rsaboth' => [TestKeys::rsaCert, TestKeys::rsaPrivateKey]]);
+        $key = $ks->fetch('rsaboth');
+        $keyStoreVerifyingKey = $key->getVerifyingKey();
+        $keyStoreSigningKey = $key->getSigningKey();
+        $this->assertEquals(['rsaboth', $this->testRsaPrivateKeyPEM, $this->testRsaPublicKeyPEM, 'asymmetric'], [
+          $key->getId(), $keyStoreSigningKey, $keyStoreVerifyingKey, $key->getType(), ]);
+    }
+
+    public function testFetchRsaBothSuccessSwitched()
+    {
+        $ks = new KeyStore(['rsabothswitch' => [TestKeys::rsaPrivateKey, TestKeys::rsaCert]]);
+        $key = $ks->fetch('rsabothswitch');
+        $keyStoreVerifyingKey = $key->getVerifyingKey();
+        $keyStoreSigningKey = $key->getSigningKey();
+        $this->assertEquals(['rsabothswitch', $this->testRsaPrivateKeyPEM, $this->testRsaPublicKeyPEM, 'asymmetric'], [
+          $key->getId(), $keyStoreSigningKey, $keyStoreVerifyingKey, $key->getType(), ]);
+    }
+
+    /**
+     * @expectedException \App\Util\HttpSignatures\KeyException
+     */
+    public function testRsaMismatch()
+    {
+        $privateKey = openssl_pkey_new([
+          'private_key_type' => 'OPENSSL_KEYTYPE_RSA',
+          'private_key_bits' => 1024, ]
+        );
+        $ks = new Key('badpki', [TestKeys::rsaCert, $privateKey]);
+    }
+}

+ 17 - 0
tests/Unit/HttpSignatures/KeyStoreTest.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace Tests\Unit\HttpSignatures;
+
+use App\Util\HttpSignatures\KeyStore;
+
+class KeyStoreTest extends \PHPUnit\Framework\TestCase
+{
+    /**
+     * @expectedException App\Util\HttpSignatures\Exception
+     */
+    public function testFetchFail()
+    {
+        $ks = new KeyStore(['id' => 'secret']);
+        $key = $ks->fetch('nope');
+    }
+}

+ 84 - 0
tests/Unit/HttpSignatures/RsaContextTest.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace Tests\Unit\HttpSignatures;
+
+use GuzzleHttp\Psr7\Request;
+use App\Util\HttpSignatures\Context;
+use Tests\Unit\HttpSignatures\TestKeys;
+
+class RsaContextTest extends \PHPUnit\Framework\TestCase
+{
+    private $context;
+
+    public function setUp()
+    {
+        $this->sha1context = new Context([
+            'keys' => ['rsa1' => TestKeys::rsaPrivateKey],
+            'algorithm' => 'rsa-sha1',
+            'headers' => ['(request-target)', 'date'],
+        ]);
+        $this->sha256context = new Context([
+            'keys' => ['rsa1' => TestKeys::rsaPrivateKey],
+            'algorithm' => 'rsa-sha256',
+            'headers' => ['(request-target)', 'date'],
+        ]);
+    }
+
+    public function testSha1Signer()
+    {
+        $message = new Request('GET', '/path?query=123', ['date' => 'today', 'accept' => 'llamas']);
+
+        $message = $this->sha1context->signer()->sign($message);
+        $expectedSha1String = implode(',', [
+            'keyId="rsa1"',
+            'algorithm="rsa-sha1"',
+            'headers="(request-target) date"',
+            'signature="YIR3DteE3Jmz1VAnUMTgjTn3vTKfQuZl1CJhMBvGOZpnzwKeYBXA'.
+              'H108FojnbSeVG/AXq9pcrA6AFK0peg0aueqxpaFlo+4L/q5XzJ+QoryY3dlSr'.
+              'xwVnE5s5M19xmFm/6YkZR/KPeANCsG4SPL82Um/PCEMU0tmKd6sSx+IIzAYbX'.
+              'G/VrFMDeQAdXqpU1EhgxopKEAapN8rChb49+1JfR/RxlSKiLukJJ6auurm2zM'.
+              'n2D40fR1d2umA5LAO7vRt2iQwVbtwiFkVlRqkMvGftCNZByu8jJ6StI5H7Efu'.
+              'ANSHAZXKXWNH8yxpBUW/QCHCZjPd0ugM0QJJIc7i8JbGlA=="',
+        ]);
+
+        $this->assertEquals(
+            $expectedSha1String,
+            $message->getHeader('Signature')[0]
+        );
+    }
+
+    public function testSha256Signer()
+    {
+        $message = new Request('GET', '/path?query=123', ['date' => 'today', 'accept' => 'llamas']);
+
+        $message = $this->sha256context->signer()->sign($message);
+        $expectedSha256String = implode(',', [
+            'keyId="rsa1"',
+            'algorithm="rsa-sha256"',
+            'headers="(request-target) date"',
+            'signature="WGIegQCC3GEwxbkuXtq67CAqeDhkwblxAH2uoDx5kfWurhLRA5WB'.
+            'FDA/aktsZAjuUoimG1w4CGxSecziER1ez44PBlHP2fCW4ArLgnQgcjkdN2cOf/g'.
+            'j0OVL8s2usG4o4tud/+jjF3nxTxLl3HC+erBKsJakwXbw9kt4Cr028BToVfNXsW'.
+            'oMFpv0IjcgBH2V41AVlX/mYBMMJAihBCIcpgAcGrrxmG2gkfvSn09wtTttkGHft'.
+            'PIp3VpB53zbemlJS9Yw3tmmHr6cvWSXqQy/bTsEOoQJ2REfn5eiyzsJu3GiOpiI'.
+            'LK67i/WH9moltJtlfV57TV72cgYtjWa6yqhtFg=="',
+        ]);
+
+        $this->assertEquals(
+            $expectedSha256String,
+            $message->getHeader('Signature')[0]
+        );
+    }
+
+    /**
+     * @expectedException     App\Util\HttpSignatures\AlgorithmException
+     */
+    public function testRsaBadalgorithm()
+    {
+        $sha224context = new Context([
+              'keys' => ['rsa1' => TestKeys::rsaPrivateKey],
+              'algorithm' => 'rsa-sha224',
+              'headers' => ['(request-target)', 'date'],
+          ]);
+    }
+}

+ 156 - 0
tests/Unit/HttpSignatures/RsaVerifierTest.php

@@ -0,0 +1,156 @@
+<?php
+
+namespace Tests\Unit\HttpSignatures;
+
+use GuzzleHttp\Psr7\Request;
+use App\Util\HttpSignatures\KeyStore;
+use App\Util\HttpSignatures\Verifier;
+use Tests\Unit\HttpSignatures\TestKeys;
+
+class VerifierRsaTest extends \PHPUnit\Framework\TestCase
+{
+    const DATE = 'Fri, 01 Aug 2014 13:44:32 -0700';
+    const DATE_DIFFERENT = 'Fri, 01 Aug 2014 13:44:33 -0700';
+
+    /**
+     * @var Verifier
+     */
+    private $verifier;
+
+    /**
+     * @var Request
+     */
+    private $message;
+
+    public function setUp()
+    {
+        $this->setUpRsaVerifier();
+
+        $sha1SignatureHeader =
+        'keyId="rsa1",algorithm="rsa-sha1",headers="(request-target) date",'.
+        'signature="YIR3DteE3Jmz1VAnUMTgjTn3vTKfQuZl1CJhMBvGOZpnzwKeYBXAH10'.
+        '8FojnbSeVG/AXq9pcrA6AFK0peg0aueqxpaFlo+4L/q5XzJ+QoryY3dlSrxwVnE5s5'.
+        'M19xmFm/6YkZR/KPeANCsG4SPL82Um/PCEMU0tmKd6sSx+IIzAYbXG/VrFMDeQAdXq'.
+        'pU1EhgxopKEAapN8rChb49+1JfR/RxlSKiLukJJ6auurm2zMn2D40fR1d2umA5LAO7'.
+        'vRt2iQwVbtwiFkVlRqkMvGftCNZByu8jJ6StI5H7EfuANSHAZXKXWNH8yxpBUW/QCH'.
+        'CZjPd0ugM0QJJIc7i8JbGlA=="';
+
+        $this->sha1Message = new Request('GET', '/path?query=123', [
+            'Date' => 'today',
+            'Signature' => $sha1SignatureHeader,
+        ]);
+
+        $sha256SignatureHeader =
+        'keyId="rsa1",algorithm="rsa-sha256",headers="(request-target) date",'.
+        'signature="WGIegQCC3GEwxbkuXtq67CAqeDhkwblxAH2uoDx5kfWurhLRA5WBFDA/a'.
+        'ktsZAjuUoimG1w4CGxSecziER1ez44PBlHP2fCW4ArLgnQgcjkdN2cOf/gj0OVL8s2us'.
+        'G4o4tud/+jjF3nxTxLl3HC+erBKsJakwXbw9kt4Cr028BToVfNXsWoMFpv0IjcgBH2V4'.
+        '1AVlX/mYBMMJAihBCIcpgAcGrrxmG2gkfvSn09wtTttkGHftPIp3VpB53zbemlJS9Yw3'.
+        'tmmHr6cvWSXqQy/bTsEOoQJ2REfn5eiyzsJu3GiOpiILK67i/WH9moltJtlfV57TV72c'.
+        'gYtjWa6yqhtFg=="';
+
+        $this->sha256Message = new Request('GET', '/path?query=123', [
+            'Date' => 'today',
+            'Signature' => $sha256SignatureHeader,
+        ]);
+    }
+
+    private function setUpRsaVerifier()
+    {
+        $keyStore = new KeyStore(['rsa1' => TestKeys::rsaPublicKey]);
+        $this->verifier = new Verifier($keyStore);
+    }
+
+    public function testVerifyValidRsaMessage()
+    {
+        $this->assertTrue($this->verifier->isValid($this->sha1Message));
+        $this->assertTrue($this->verifier->isValid($this->sha256Message));
+    }
+
+    public function testVerifyValidRsaMessageAuthorizationHeader()
+    {
+        $message = $this->sha1Message->withHeader(
+          'Authorization',
+          "Signature {$this->sha1Message->getHeader('Signature')[0]}");
+        $message = $this->sha1Message->withoutHeader('Signature');
+
+        $this->assertTrue($this->verifier->isValid($this->sha1Message));
+
+        $message = $this->sha256Message->withHeader(
+          'Authorization',
+          "Signature {$this->sha256Message->getHeader('Signature')[0]}");
+        $message = $this->sha256Message->withoutHeader('Signature');
+
+        $this->assertTrue($this->verifier->isValid($this->sha256Message));
+    }
+
+    public function testRejectTamperedRsaRequestMethod()
+    {
+        $message = $this->sha1Message->withMethod('POST');
+        $this->assertFalse($this->verifier->isValid($message));
+        $message = $this->sha256Message->withMethod('POST');
+        $this->assertFalse($this->verifier->isValid($message));
+    }
+
+    public function testRejectTamperedRsaDate()
+    {
+        $message = $this->sha1Message->withHeader('Date', self::DATE_DIFFERENT);
+        $this->assertFalse($this->verifier->isValid($message));
+        $message = $this->sha256Message->withHeader('Date', self::DATE_DIFFERENT);
+        $this->assertFalse($this->verifier->isValid($message));
+    }
+
+    public function testRejectTamperedRsaSignature()
+    {
+        $message = $this->sha1Message->withHeader(
+            'Signature',
+            preg_replace('/signature="/', 'signature="x', $this->sha1Message->getHeader('Signature')[0])
+        );
+        $this->assertFalse($this->verifier->isValid($message));
+        $message = $this->sha256Message->withHeader(
+            'Signature',
+            preg_replace('/signature="/', 'signature="x', $this->sha256Message->getHeader('Signature')[0])
+        );
+        $this->assertFalse($this->verifier->isValid($message));
+    }
+
+    public function testRejectRsaMessageWithoutSignatureHeader()
+    {
+        $message = $this->sha1Message->withoutHeader('Signature');
+        $this->assertFalse($this->verifier->isValid($message));
+        $message = $this->sha256Message->withoutHeader('Signature');
+        $this->assertFalse($this->verifier->isValid($message));
+    }
+
+    public function testRejectRsaMessageWithGarbageSignatureHeader()
+    {
+        $message = $this->sha1Message->withHeader('Signature', 'not="a",valid="signature"');
+        $this->assertFalse($this->verifier->isValid($message));
+        $message = $this->sha256Message->withHeader('Signature', 'not="a",valid="signature"');
+        $this->assertFalse($this->verifier->isValid($message));
+    }
+
+    public function testRejectRsaMessageWithPartialSignatureHeader()
+    {
+        $message = $this->sha1Message->withHeader('Signature', 'keyId="aa",algorithm="bb"');
+        $this->assertFalse($this->verifier->isValid($message));
+        $message = $this->sha256Message->withHeader('Signature', 'keyId="aa",algorithm="bb"');
+        $this->assertFalse($this->verifier->isValid($message));
+    }
+
+    public function testRejectsRsaMessageWithUnknownKeyId()
+    {
+        $keyStore = new KeyStore(['nope' => 'secret']);
+        $verifier = new Verifier($keyStore);
+        $this->assertFalse($verifier->isValid($this->sha1Message));
+        $this->assertFalse($verifier->isValid($this->sha256Message));
+    }
+
+    public function testRejectsRsaMessageMissingSignedHeaders()
+    {
+        $message = $this->sha1Message->withoutHeader('Date');
+        $this->assertFalse($this->verifier->isValid($message));
+        $message = $this->sha256Message->withoutHeader('Date');
+        $this->assertFalse($this->verifier->isValid($message));
+    }
+}

+ 44 - 0
tests/Unit/HttpSignatures/SignatureParametersParserTest.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Tests\Unit\HttpSignatures;
+
+use App\Util\HttpSignatures\SignatureParametersParser;
+
+class SignatureParametersParserTest extends \PHPUnit\Framework\TestCase
+{
+    public function testParseReturnsExpectedAssociativeArray()
+    {
+        $parser = new SignatureParametersParser(
+            'keyId="example",algorithm="hmac-sha1",headers="(request-target) date",signature="b64"'
+        );
+        $this->assertEquals(
+            [
+                'keyId' => 'example',
+                'algorithm' => 'hmac-sha1',
+                'headers' => '(request-target) date',
+                'signature' => 'b64',
+            ],
+            $parser->parse()
+        );
+    }
+
+    /**
+     * @expectedException App\Util\HttpSignatures\SignatureParseException
+     */
+    public function testParseThrowsTypedException()
+    {
+        $parser = new SignatureParametersParser('nope');
+        $parser->parse();
+    }
+
+    /**
+     * @expectedException App\Util\HttpSignatures\SignatureParseException
+     */
+    public function testParseExceptionForMissingComponents()
+    {
+        $parser = new SignatureParametersParser(
+            'keyId="example",algorithm="hmac-sha1",headers="(request-target) date"'
+        );
+        $parser->parse();
+    }
+}

+ 58 - 0
tests/Unit/HttpSignatures/SignatureParametersTest.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace Tests\Unit\HttpSignatures;
+
+use App\Util\HttpSignatures\HeaderList;
+use App\Util\HttpSignatures\HmacAlgorithm;
+use App\Util\HttpSignatures\RsaAlgorithm;
+use App\Util\HttpSignatures\Key;
+use App\Util\HttpSignatures\SignatureParameters;
+
+class SignatureParametersTest extends \PHPUnit\Framework\TestCase
+{
+    public function testHmacToString()
+    {
+        $key = new Key('pda', 'secret');
+        $algorithm = new HmacAlgorithm('sha256');
+        $headerList = new HeaderList(['(request-target)', 'date']);
+
+        $signature = $this->getMockBuilder('HttpSignatures\Signature')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $signature
+            ->expects($this->any())
+            ->method('string')
+            ->will($this->returnValue('thesignature'));
+
+        $sp = new SignatureParameters($key, $algorithm, $headerList, $signature);
+
+        $this->assertEquals(
+            'keyId="pda",algorithm="hmac-sha256",headers="(request-target) date",signature="dGhlc2lnbmF0dXJl"',
+            $sp->string()
+        );
+    }
+
+    public function testRsaToString()
+    {
+        $key = new Key('pda', TestKeys::rsaPrivateKey);
+        $algorithm = new RsaAlgorithm('sha256');
+        $headerList = new HeaderList(['(request-target)', 'date']);
+
+        $signature = $this->getMockBuilder('HttpSignatures\Signature')
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $signature
+            ->expects($this->any())
+            ->method('string')
+            ->will($this->returnValue('thesignature'));
+
+        $sp = new SignatureParameters($key, $algorithm, $headerList, $signature);
+
+        $this->assertEquals(
+            'keyId="pda",algorithm="rsa-sha256",headers="(request-target) date",signature="dGhlc2lnbmF0dXJl"',
+            $sp->string()
+        );
+    }
+}

+ 93 - 0
tests/Unit/HttpSignatures/SigningStringTest.php

@@ -0,0 +1,93 @@
+<?php
+
+namespace Tests\Unit\HttpSignatures;
+
+use GuzzleHttp\Psr7\Request;
+use App\Util\HttpSignatures\HeaderList;
+use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
+use App\Util\HttpSignatures\SigningString;
+use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
+
+class SigningStringTest extends \PHPUnit\Framework\TestCase
+{
+    public function testWithoutQueryString()
+    {
+        $headerList = new HeaderList(['(request-target)']);
+        $ss = new SigningString($headerList, $this->message('/path'));
+
+        $this->assertEquals(
+            '(request-target): get /path',
+            $ss->string()
+        );
+    }
+
+    public function testSigningStringWithOrderedQueryParameters()
+    {
+        $headerList = new HeaderList(['(request-target)', 'date']);
+        $ss = new SigningString($headerList, $this->message('/path?a=antelope&z=zebra'));
+
+        $this->assertEquals(
+            "(request-target): get /path?a=antelope&z=zebra\ndate: Mon, 28 Jul 2014 15:39:13 -0700",
+            $ss->string()
+        );
+    }
+
+    public function testSigningStringWithUnorderedQueryParameters()
+    {
+        $headerList = new HeaderList(['(request-target)', 'date']);
+        $ss = new SigningString($headerList, $this->message('/path?z=zebra&a=antelope'));
+
+        $this->assertEquals(
+            "(request-target): get /path?z=zebra&a=antelope\ndate: Mon, 28 Jul 2014 15:39:13 -0700",
+            $ss->string()
+        );
+    }
+
+    public function testSigningStringWithOrderedQueryParametersSymfonyRequest()
+    {
+        $headerList = new HeaderList(['(request-target)', 'date']);
+        $ss = new SigningString($headerList, $this->symfonyMessage('/path?a=antelope&z=zebra'));
+
+        $this->assertEquals(
+            "(request-target): get /path?a=antelope&z=zebra\ndate: Mon, 28 Jul 2014 15:39:13 -0700",
+            $ss->string()
+        );
+    }
+
+    public function testSigningStringWithUnorderedQueryParametersSymfonyRequest()
+    {
+        $headerList = new HeaderList(['(request-target)', 'date']);
+        $ss = new SigningString($headerList, $this->symfonyMessage('/path?z=zebra&a=antelope'));
+
+        $this->assertEquals(
+            "(request-target): get /path?z=zebra&a=antelope\ndate: Mon, 28 Jul 2014 15:39:13 -0700",
+            $ss->string()
+        );
+    }
+
+    /**
+     * @expectedException App\Util\HttpSignatures\Exception
+     */
+    public function testSigningStringErrorForMissingHeader()
+    {
+        $headerList = new HeaderList(['nope']);
+        $ss = new SigningString($headerList, $this->message('/'));
+        $ss->string();
+    }
+
+    private function message($path)
+    {
+        return new Request('GET', $path, ['date' => 'Mon, 28 Jul 2014 15:39:13 -0700']);
+    }
+
+    private function symfonyMessage($path)
+    {
+        $symfonyRequest = SymfonyRequest::create($path, 'GET');
+        $symfonyRequest->headers->replace(['date' => 'Mon, 28 Jul 2014 15:39:13 -0700']);
+
+        $psr7Factory = new DiactorosFactory();
+        $psrRequest = $psr7Factory->createRequest($symfonyRequest)->withRequestTarget($symfonyRequest->getRequestUri());
+
+        return $psrRequest;
+    }
+}

+ 64 - 0
tests/Unit/HttpSignatures/TestKeys.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace Tests\Unit\HttpSignatures;
+
+abstract class TestKeys
+{
+    const rsaPrivateKey =
+'-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAxyfPtnW/CakdLpZVh90sR+hpRwiBBdS/za6wal0pFddl2xcL
+sHY/IA1u348xOc4LEY2D0807o1IXSEaqrt9eUkAh+yLGKgDCWLGzW026HBMgojhn
+Y702pmRI2CGqP0xtzrynCSmSMvhdKM9IMc7DGf7Wdo4Y0En0KPat+IyCZpbf7jUX
+cGL/tjxMKS3fXNgthGWVDU2TBSQRFM+fgeH9egODd4DfpzfaUoUDcoB/CZ1FEa4+
+wNfXR5CZ4WjIaeVbCQ09maup07J+KaGe8SQ9LjcUuityyuNBjy5nvQkbmDfAh082
+2lZQYR62FM5EAlNptThODg6q4wkIQ+spLv1dSwIDAQABAoIBAQCXYQyCvVd7qV80
+JTNYNWbONbuoMa+Y1hEA77LK9osfPf3/HbJV7FupKmzHY5lgPdyt9+pnWQ3m46Qs
+3QIqMEEKthLeSJ1mGfOf5VrWoOtBIczhYYw9BPsAWSQBnP1CZf7lcQJqdX3aXmy5
+c22F5oroPIuZzALSeBQt+utcDLml7dr4D2TbFkmh3ocGGK1SDUATGBVQG+MAVGFQ
+J5BYLFPUp8QC2o1672/aqNkx1XO08vIoANtWz5Rjf2hCRrdw1liAN30nnZ+l9h3x
+mbof0sUQMPxxmZ/u+HGPShkN+Y/DLfIyjHlpXI127WBwFLr5P/iL6+DDbsn4Iav3
+L57kSxhhAoGBAO6v/ojuZFE0h3mKHy+SdWggcRqe8Re2C5QTODTy7hRwdUv791lk
+jFnONqlcn51H/hDQwP8nnG8HFxJYl4IoCCdcWfeFlQ82oerIWmsX6H4BFBfF7I9Y
+3Muo1BWj4vif/mwwsL0NOAn5XCO/Gi1nF7YZBd/or+X/x4XLSWKnBISRAoGBANWZ
+xzhpUltGPfKWgfE+ETbZC++Nqsd1MHAdZOhOaeBY615DdmfVv7ryaTWJ1kRbRmq0
+9eKmopEYqfCwyfKZ/8+2dudhndnnEqmiJPu2WFyEiR9sb8NMPcOOjKPhUMgP0ZNx
+Ynz/oTvBOylvU2MfC0hpLghq50JeEJSiGxzE9kIbAoGAejcpeMnAGghwmd4Ma9pt
+PXznDP93aXGwagiRTiNZnqOam+aPV3lxmAZL3NptbCZRxCBvwfZxVjRmLuGn6mA/
+FJBoDKKcmWaa79HY4l8ij2pT9HxGzXttyuZOeiopbK7XomQoCxU6rXi+IhuW9sqD
+zJzxch39+yHF8w8NK3Njj9ECgYEAj8ZXu5fhEIECV5SJWKmvipykFRXleyZdeUm/
+z0Jgr9sKasO8In5U9PAQczIZYJ+TkWXHEE2bpVDVqqZE+KBB+T1XYb1qM+7+t+Hl
+ROzjIzsu1VD3FZzvAf+kmPajmlZTegxa/8pNa9xQBz7hARo3TQFHM/FJQnnwbSuE
+VmQZYjsCgYA9ADxvlgGQmo3uHup6u54S7MgwvzIK7WiXKkuoI5rp0B0mwTr3loVt
+3r3tZBH4+z17fVhmoQ4a4kYT8ixn0XpaL0LOv8s02b36XCNlrfPlafOwhHfOHmlz
+zQnzviLiUOgXyD8FwZlYx+hTM09CYPcdJWSPl6JVF7uxm2fX/HdS3w==
+-----END RSA PRIVATE KEY-----';
+    const rsaCert =
+'-----BEGIN CERTIFICATE-----
+MIICmjCCAYICCQDIxrpvPCnqRjANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDAR0
+ZXN0MB4XDTE4MDgxODEyMjMzMloXDTI4MDgxNTEyMjMzMlowDzENMAsGA1UEAwwE
+dGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMcnz7Z1vwmpHS6W
+VYfdLEfoaUcIgQXUv82usGpdKRXXZdsXC7B2PyANbt+PMTnOCxGNg9PNO6NSF0hG
+qq7fXlJAIfsixioAwlixs1tNuhwTIKI4Z2O9NqZkSNghqj9Mbc68pwkpkjL4XSjP
+SDHOwxn+1naOGNBJ9Cj2rfiMgmaW3+41F3Bi/7Y8TCkt31zYLYRllQ1NkwUkERTP
+n4Hh/XoDg3eA36c32lKFA3KAfwmdRRGuPsDX10eQmeFoyGnlWwkNPZmrqdOyfimh
+nvEkPS43FLorcsrjQY8uZ70JG5g3wIdPNtpWUGEethTORAJTabU4Tg4OquMJCEPr
+KS79XUsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAgnvP8UDFND0G/R5ptKeaAUvF
+xmDWnyBOv8/GWb8i9zesBfgjmjoKfjIgbYS/z0ZqMHApuv6td6NovsCVgpfEpLAv
+zxtljLtOWEeQ/25bespBiTiOVp1w8BzEZ2IhNX6M0LxXQkUXgeyOC2wnH6SH9rTW
+USM0aZhhDcdOZ4q+OkpAN6uux3r0QNJLdU8vInBGoyE3s+7MjEun30HQy24HSgEA
+p/Ee+dkqU2Jp7wr5omMzurGrEwre0KjNLbrDvcb/0u8r7RA5sghHiE7MUe8acGqR
+GyMYMn7AX97SD2yxYgwt7i/v65wkAC5oxXA2Yg1TTJZrLD6obGv+wELnePhKgw==
+-----END CERTIFICATE-----
+';
+    const rsaPublicKey =
+'-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxyfPtnW/CakdLpZVh90s
+R+hpRwiBBdS/za6wal0pFddl2xcLsHY/IA1u348xOc4LEY2D0807o1IXSEaqrt9e
+UkAh+yLGKgDCWLGzW026HBMgojhnY702pmRI2CGqP0xtzrynCSmSMvhdKM9IMc7D
+Gf7Wdo4Y0En0KPat+IyCZpbf7jUXcGL/tjxMKS3fXNgthGWVDU2TBSQRFM+fgeH9
+egODd4DfpzfaUoUDcoB/CZ1FEa4+wNfXR5CZ4WjIaeVbCQ09maup07J+KaGe8SQ9
+LjcUuityyuNBjy5nvQkbmDfAh0822lZQYR62FM5EAlNptThODg6q4wkIQ+spLv1d
+SwIDAQAB
+-----END PUBLIC KEY-----
+';
+}

+ 116 - 0
tests/Unit/HttpSignatures/VerifierHmacTest.php

@@ -0,0 +1,116 @@
+<?php
+
+namespace Tests\Unit\HttpSignatures;
+
+use GuzzleHttp\Psr7\Request;
+use App\Util\HttpSignatures\KeyStore;
+use App\Util\HttpSignatures\Verifier;
+
+class VerifierHmacTest extends \PHPUnit\Framework\TestCase
+{
+    const DATE = 'Fri, 01 Aug 2014 13:44:32 -0700';
+    const DATE_DIFFERENT = 'Fri, 01 Aug 2014 13:44:33 -0700';
+
+    /**
+     * @var Verifier
+     */
+    private $verifier;
+
+    /**
+     * @var Request
+     */
+    private $message;
+
+    public function setUp()
+    {
+        $this->setUpHmacVerifier();
+        $this->setUpValidHmacMessage();
+    }
+
+    private function setUpHmacVerifier()
+    {
+        $keyStore = new KeyStore(['secret1' => 'secret']);
+        $this->verifier = new Verifier($keyStore);
+    }
+
+    private function setUpValidHmacMessage()
+    {
+        $signatureHeader = sprintf(
+            'keyId="%s",algorithm="%s",headers="%s",signature="%s"',
+            'secret1',
+            'hmac-sha256',
+            '(request-target) date',
+            'cS2VvndvReuTLy52Ggi4j6UaDqGm9hMb4z0xJZ6adqU='
+        );
+
+        $this->message = new Request('GET', '/path?query=123', [
+            'Date' => self::DATE,
+            'Signature' => $signatureHeader,
+        ]);
+    }
+
+    public function testVerifyValidHmacMessage()
+    {
+        $this->assertTrue($this->verifier->isValid($this->message));
+    }
+
+    public function testVerifyValidHmacMessageAuthorizationHeader()
+    {
+        $message = $this->message->withHeader('Authorization', "Signature {$this->message->getHeader('Signature')[0]}");
+        $message = $message->withoutHeader('Signature');
+
+        $this->assertTrue($this->verifier->isValid($this->message));
+    }
+
+    public function testRejectTamperedHmacRequestMethod()
+    {
+        $message = $this->message->withMethod('POST');
+        $this->assertFalse($this->verifier->isValid($message));
+    }
+
+    public function testRejectTamperedHmacDate()
+    {
+        $message = $this->message->withHeader('Date', self::DATE_DIFFERENT);
+        $this->assertFalse($this->verifier->isValid($message));
+    }
+
+    public function testRejectTamperedHmacSignature()
+    {
+        $message = $this->message->withHeader(
+            'Signature',
+            preg_replace('/signature="/', 'signature="x', $this->message->getHeader('Signature')[0])
+        );
+        $this->assertFalse($this->verifier->isValid($message));
+    }
+
+    public function testRejectHmacMessageWithoutSignatureHeader()
+    {
+        $message = $this->message->withoutHeader('Signature');
+        $this->assertFalse($this->verifier->isValid($message));
+    }
+
+    public function testRejectHmacMessageWithGarbageSignatureHeader()
+    {
+        $message = $this->message->withHeader('Signature', 'not="a",valid="signature"');
+        $this->assertFalse($this->verifier->isValid($message));
+    }
+
+    public function testRejectHmacMessageWithPartialSignatureHeader()
+    {
+        $message = $this->message->withHeader('Signature', 'keyId="aa",algorithm="bb"');
+        $this->assertFalse($this->verifier->isValid($message));
+    }
+
+    public function testRejectsHmacMessageWithUnknownKeyId()
+    {
+        $keyStore = new KeyStore(['nope' => 'secret']);
+        $verifier = new Verifier($keyStore);
+        $this->assertFalse($verifier->isValid($this->message));
+    }
+
+    public function testRejectsHmacMessageMissingSignedHeaders()
+    {
+        $message = $this->message->withoutHeader('Date');
+        $this->assertFalse($this->verifier->isValid($message));
+    }
+}