diff --git a/src/Auth/HttpSignatureService.php b/src/Crypto/HttpSignatureService.php similarity index 56% rename from src/Auth/HttpSignatureService.php rename to src/Crypto/HttpSignatureService.php index 6976c90..e751127 100644 --- a/src/Auth/HttpSignatureService.php +++ b/src/Crypto/HttpSignatureService.php @@ -1,6 +1,9 @@ dateTimeProvider = $dateTimeProvider; + } + + /** + * Generates a signature given the request and private key + * + * @param Request $request The request to be signed + * @param string $privateKey The private key to use to sign the request + * @param string $keyId The id of the signing key + * @param array $headers The headers to use in the signature + * (default ['(request-target)', 'host', 'date']) + * @return string The Signature header value + */ + public function sign( Request $request, string $privateKey, string $keyId, + $headers = self::DEFAULT_HEADERS ) + { + $headers = array_map( 'strtolower', $headers ); + $signingString = $this->getSigningString( $request, $headers ); + $keypair = RsaKeypair::fromPrivateKey( $privateKey ); + $signature = base64_encode( $keypair->sign( $signingString, 'rsa256' ) ); + $headersStr = implode( ' ', $headers ); + return "keyId=\"$keyId\"," . + "algorithm=\"rsa-sha256\"," . + "headers=\"$headersStr\"," . + "signature=\"$signature\""; } /** @@ -33,9 +73,18 @@ class HttpSignatureService */ public function verify( Request $request, string $publicKey ) { - // TODO fail verification if date is > 300 seconds ago to prevent replay attacks $params = array(); $headers = $request->headers; + + if ( ! $headers->has( 'date' ) ) { + return false; + } + $now = $this->dateTimeProvider->getTime( 'http-signature.verify' ); + $then = DateTime::createFromFormat( DateTime::RFC2822, $headers->get( 'date' ) ); + if ( abs( $now->getTimestamp() - $then->getTimestamp() ) > self::REPLAY_THRESHOLD ) { + return false; + } + if ( $headers->has( 'signature' ) ) { $params = $this->parseSignatureParams( $headers->get( 'signature' ) ); } else if ( $headers->has( 'authorization' ) && @@ -43,19 +92,21 @@ class HttpSignatureService $paramsStr = substr( $headers->get( 'authorization' ), 10 ); $params = $this->parseSignatureParams( $paramsStr ); } + if ( count( $params ) === 0 ) { return false; } + $targetHeaders = array( 'date' ); if ( array_key_exists( 'headers', $params ) ) { $targetHeaders = $params['headers']; } + $signingString = $this->getSigningString( $request, $targetHeaders ); $signature = base64_decode( $params['signature'] ); // TODO handle different algorithms here, checking the 'algorithm' param and the key headers - return openssl_verify( - $signingString, $signature, $publicKey, OPENSSL_ALGO_SHA256 - ) === 1; + $keypair = RsaKeypair::fromPublicKey( $publicKey ); + return $keypair->verify($signingString, $signature, 'sha256'); } /** @@ -71,7 +122,7 @@ class HttpSignatureService foreach ( $headers as $header ) { $component = "${header}: "; if ( $header == '(request-target)' ) { - $method = strtolower( $request->method ); + $method = strtolower( $request->getMethod()); $path = $request->getRequestUri(); $component = $component . $method . ' ' . $path; } else { @@ -81,7 +132,7 @@ class HttpSignatureService } $signingComponents[] = $component; } - return implode( '\n', $signingComponents ); + return implode( "\n", $signingComponents ); } /** @@ -94,12 +145,12 @@ class HttpSignatureService private function parseSignatureParams( string $paramsStr ) { $params = array(); - $split = HeaderUtils::split( $paramsStr, ',= ' ); + $split = HeaderUtils::split( $paramsStr, ',=' ); foreach ( $split as $paramArr ) { - $paramName = $paramArr[0][0]; + $paramName = $paramArr[0]; $paramValue = $paramArr[1]; - if ( count( $paramValue ) === 1 ) { - $paramValue = $paramValue[0]; + if ( $paramName == 'headers' ) { + $paramValue = explode(' ', $paramValue); } $params[$paramName] = $paramValue; } diff --git a/src/Crypto/RsaKeypair.php b/src/Crypto/RsaKeypair.php index d3375f0..dfe6767 100644 --- a/src/Crypto/RsaKeypair.php +++ b/src/Crypto/RsaKeypair.php @@ -22,7 +22,7 @@ class RsaKeypair */ private $privateKey; - private function __construct( string $publicKey, string $privateKey ) + public function __construct( string $publicKey, string $privateKey ) { $this->publicKey = $publicKey; $this->privateKey = $privateKey; @@ -53,9 +53,11 @@ class RsaKeypair * * Throws a BadMethodCallException if this RsaKeypair does not have a private key. * @param string $data The data to sign + * @param string $hash The hash algorithm to use. One of: + * 'md2', 'md5', 'sha1', 'sha256', 'sha384', 'sha512'. Default: 'sha256' * @return string The signature */ - public function sign( $data ) + public function sign( $data, $hash = 'sha256' ) { if ( empty( $this->privateKey ) ) { throw new BadMethodCallException( @@ -64,6 +66,7 @@ class RsaKeypair } $rsa = new RSA(); $rsa->setHash( 'sha256' ); + $rsa->setSignatureMode(RSA::SIGNATURE_PKCS1); $rsa->loadKey( $this->privateKey ); return $rsa->sign( $data ); } @@ -73,12 +76,15 @@ class RsaKeypair * * @param string $data The data * @param string $signature The signature + * @param string $hash The hash algorithm to use. One of: + * 'md2', 'md5', 'sha1', 'sha256', 'sha384', 'sha512'. Default: 'sha256' * @return bool */ - public function verify( $data, $signature ) + public function verify( $data, $signature, $hash = 'sha256' ) { $rsa = new RSA(); - $rsa->setHash( 'sha256' ); + $rsa->setHash( $hash ); + $rsa->setSignatureMode(RSA::SIGNATURE_PKCS1); $rsa->loadKey( $this->publicKey ); return $rsa->verify( $data, $signature ); } @@ -108,5 +114,19 @@ class RsaKeypair { return new RsaKeypair( $publicKey, '' ); } + + /** + * Generates an RsaKeypair with the given private key + * + * The generated RsaKeypair will be able to sign data but + * not verify signatures, since it won't have a public key. + * + * @param string $privateKey The private key + * @return RsaKeypair + */ + public function fromPrivateKey( string $privateKey) + { + return new RsaKeypair( '', $privateKey ); + } } ?> diff --git a/test/Auth/HttpSignatureServiceTest.php b/test/Auth/HttpSignatureServiceTest.php deleted file mode 100644 index 3614485..0000000 --- a/test/Auth/HttpSignatureServiceTest.php +++ /dev/null @@ -1,73 +0,0 @@ -httpSignatureService = new HttpSignatureService(); - } - - private static function getRequest() - { - $request = Request::create( - 'https://example.com/foo', - Request::METHOD_POST, - array( 'param' => 'value', 'pet' => 'dog' ), - array(), - array(), - array(), - '{"hello": "world"}' - ); - $request->headers->set( 'host', 'example.com' ); - $request->headers->set( 'content-type', 'application/json' ); - $request->headers->set( - 'digest', 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=' - ); - $request->headers->set( 'content-length', 18 ); - $request->headers->set( 'date', 'Sun, 05 Jan 2014 21:31:40 GMT' ); - return $request; - } - - public function testItVerifies() - { - $request = self::getRequest(); - $authHeader = 'Signature keyId="Test",algorithm="rsa-sha256",signature="SjWJWbWN7i0wzBvtPl8rbASWz5xQW6mcJmn+ibttBqtifLN7Sazz6m79cNfwwb8DMJ5cou1s7uEGKKCs+FLEEaDV5lp7q25WqS+lavg7T8hc0GppauB6hbgEKTwblDHYGEtbGmtdHgVCk9SuS13F0hZ8FD0k/5OxEPXe5WozsbM="'; - $request->headers->set( 'authorization', $authHeader ); - $verified = $this->httpSignatureService->verify( $request, self::PUBLIC_KEY ); - $this->assertTrue( $verified ); - } -} -?> diff --git a/test/Crypto/HttpSignatureServiceTest.php b/test/Crypto/HttpSignatureServiceTest.php new file mode 100644 index 0000000..653314e --- /dev/null +++ b/test/Crypto/HttpSignatureServiceTest.php @@ -0,0 +1,217 @@ + DateTime::createFromFormat( + DateTime::RFC2822, 'Sun, 05 Jan 2014 21:31:40 GMT' + ), + ) ); + $this->httpSignatureService = new HttpSignatureService( $dateTimeProvider ); + } + + private static function getRequest() + { + $request = Request::create( + 'https://example.com/foo?param=value&pet=dog', + Request::METHOD_POST, + array(), + array(), + array(), + array(), + '{"hello": "world"}' + ); + $request->headers->set( 'host', 'example.com' ); + $request->headers->set( 'content-type', 'application/json' ); + $request->headers->set( + 'digest', 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=' + ); + $request->headers->set( 'content-length', 18 ); + $request->headers->set( 'date', 'Sun, 05 Jan 2014 21:31:40 GMT' ); + return $request; + } + + public function testItVerifies() + { + $testCases = array( + array( + 'id' => 'defaultTest', + 'headers' => array( + 'Authorization' => 'Signature keyId="Test",algorithm="rsa-sha256",signature="SjWJWbWN7i0wzBvtPl8rbASWz5xQW6mcJmn+ibttBqtifLN7Sazz6m79cNfwwb8DMJ5cou1s7uEGKKCs+FLEEaDV5lp7q25WqS+lavg7T8hc0GppauB6hbgEKTwblDHYGEtbGmtdHgVCk9SuS13F0hZ8FD0k/5OxEPXe5WozsbM="', + ), + 'expectedResult' => true, + ), + array( + 'id' => 'basicTest', + 'headers' => array( + 'Authorization' => 'Signature keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date", signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="', + ), + 'expectedResult' => true, + ), + array( + 'id' => 'allHeadersTest', + 'headers' => array( + 'Authorization' => 'Signature keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="vSdrb+dS3EceC9bcwHSo4MlyKS59iFIrhgYkz8+oVLEEzmYZZvRs8rgOp+63LEM3v+MFHB32NfpB2bEKBIvB1q52LaEUHFv120V01IL+TAD48XaERZFukWgHoBTLMhYS2Gb51gWxpeIq8knRmPnYePbF5MOkR0Zkly4zKH7s1dE="', + ), + 'expectedResult' => true, + ), + array( + 'id' => 'defaultTestSigHeader', + 'headers' => array( + 'Signature' => 'keyId="Test",algorithm="rsa-sha256",signature="SjWJWbWN7i0wzBvtPl8rbASWz5xQW6mcJmn+ibttBqtifLN7Sazz6m79cNfwwb8DMJ5cou1s7uEGKKCs+FLEEaDV5lp7q25WqS+lavg7T8hc0GppauB6hbgEKTwblDHYGEtbGmtdHgVCk9SuS13F0hZ8FD0k/5OxEPXe5WozsbM="', + ), + 'expectedResult' => true, + ), + array( + 'id' => 'basicTestSigHeader', + 'headers' => array( + 'Signature' => 'keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date", signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="', + ), + 'expectedResult' => true, + ), + array( + 'id' => 'allHeadersTestSigHeader', + 'headers' => array( + 'Signature' => 'keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="vSdrb+dS3EceC9bcwHSo4MlyKS59iFIrhgYkz8+oVLEEzmYZZvRs8rgOp+63LEM3v+MFHB32NfpB2bEKBIvB1q52LaEUHFv120V01IL+TAD48XaERZFukWgHoBTLMhYS2Gb51gWxpeIq8knRmPnYePbF5MOkR0Zkly4zKH7s1dE="', + ), + 'expectedResult' => true, + ), + array( + 'id' => 'noHeaders', + 'headers' => array(), + 'expectedResult' => false, + ), + array( + 'id' => 'headerMissing', + 'headers' => array( + 'Authorization' => 'Signature keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length x-foo-header",signature="vSdrb+dS3EceC9bcwHSo4MlyKS59iFIrhgYkz8+oVLEEzmYZZvRs8rgOp+63LEM3v+MFHB32NfpB2bEKBIvB1q52LaEUHFv120V01IL+TAD48XaERZFukWgHoBTLMhYS2Gb51gWxpeIq8knRmPnYePbF5MOkR0Zkly4zKH7s1dE="', + ), + 'expectedResult' => false, + ), + array( + 'id' => 'malformedHeader', + 'headers' => array( + 'Authorization' => 'not a real auth header', + ), + 'expectedResult' => false, + ), + array( + 'id' => 'partlyMalformedHeader', + 'headers' => array( + 'Authorization' => 'Signature keyId="Test",algorithm="rsa-sha256",headers-malformed="(request-target) host date content-type digest content-length",signature="vSdrb+dS3EceC9bcwHSo4MlyKS59iFIrhgYkz8+oVLEEzmYZZvRs8rgOp+63LEM3v+MFHB32NfpB2bEKBIvB1q52LaEUHFv120V01IL+TAD48XaERZFukWgHoBTLMhYS2Gb51gWxpeIq8knRmPnYePbF5MOkR0Zkly4zKH7s1dE="', + ), + 'expectedResult' => false, + ), + array( + 'id' => 'dateTooFarInPast', + 'headers' => array( + 'Authorization' => 'Signature keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date", signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="', + ), + 'expectedResult' => false, + 'currentDatetime' => DateTime::createFromFormat( + DateTime::RFC2822, 'Sun, 05 Jan 2014 21:36:41 GMT' + ), + ), + array( + 'id' => 'dateTooFarInFuture', + 'headers' => array( + 'Authorization' => 'Signature keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date", signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="', + ), + 'expectedResult' => false, + 'currentDatetime' => DateTime::createFromFormat( + DateTime::RFC2822, 'Sun, 05 Jan 2014 21:26:39 GMT' + ), + ), + ); + foreach ( $testCases as $testCase ) { + if ( array_key_exists( 'currentDatetime', $testCase ) ) { + $dateTimeProvider = new TestDateTimeProvider( array( + 'http-signature.verify' => $testCase['currentDatetime'], + ) ); + $this->httpSignatureService = new HttpSignatureService( $dateTimeProvider ); + } + $request = self::getRequest(); + foreach ( $testCase['headers'] as $header => $value ) { + $request->headers->set( $header, $value ); + } + $actual = $this->httpSignatureService->verify( $request, self::PUBLIC_KEY ); + $this->assertEquals( + $testCase['expectedResult'], $actual, "Error on test $testCase[id]" + ); + } + } + + public function testItSigns() + { + $testCases = array( + array( + 'id' => 'basicTest', + 'keyId' => 'Test', + 'expected' => 'keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date",signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="', + ), + array( + 'id' => 'allHeadersTest', + 'keyId' => 'Test', + 'headers' => array( + '(request-target)', + 'host', + 'date', + 'content-type', + 'digest', + 'content-length', + ), + 'expected' => 'keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length",signature="vSdrb+dS3EceC9bcwHSo4MlyKS59iFIrhgYkz8+oVLEEzmYZZvRs8rgOp+63LEM3v+MFHB32NfpB2bEKBIvB1q52LaEUHFv120V01IL+TAD48XaERZFukWgHoBTLMhYS2Gb51gWxpeIq8knRmPnYePbF5MOkR0Zkly4zKH7s1dE="', + ), + ); + foreach ( $testCases as $testCase ) { + $request = self::getRequest(); + if ( array_key_exists( 'headers', $testCase ) ) { + $actual = $this->httpSignatureService->sign( + $request, self::PRIVATE_KEY, $testCase['keyId'], $testCase['headers'] + ); + } else { + $actual= $this->httpSignatureService->sign( + $request, self::PRIVATE_KEY, $testCase['keyId'] + ); + } + $this->assertEquals( + $testCase['expected'], $actual, "Error on test $testCase[id]" + ); + } + } +} +?> diff --git a/test/Objects/ObjectsServiceTest.php b/test/Objects/ObjectsServiceTest.php index 6fcd78c..fb7d344 100644 --- a/test/Objects/ObjectsServiceTest.php +++ b/test/Objects/ObjectsServiceTest.php @@ -39,7 +39,7 @@ class ObjectsServiceTest extends SQLiteTestCase ); $this->entityManager = EntityManager::create( $dbParams, $dbConfig ); $this->dateTimeProvider = new TestDateTimeProvider( - new DateTime( "12:00" ), new DateTime( "12:01" ) + array( 'create' => new DateTime( "12:00" ), 'update' => new DateTime( "12:01" ) ) ); $this->objectsService = new ObjectsService( $this->entityManager, $this->dateTimeProvider diff --git a/test/TestUtils/TestDateTimeProvider.php b/test/TestUtils/TestDateTimeProvider.php index b1e6558..96138a2 100644 --- a/test/TestUtils/TestDateTimeProvider.php +++ b/test/TestUtils/TestDateTimeProvider.php @@ -9,21 +9,20 @@ use ActivityPub\Utils\DateTimeProvider; */ class TestDateTimeProvider implements DateTimeProvider { - protected $createTime; - protected $updateTime; + protected $context; - public function __construct( DateTime $createTime, DateTime $updateTime ) + /** + * @param array $context An array mapping context strings to DateTime instances + */ + public function __construct( $context ) { - $this->createTime = $createTime; - $this->updateTime = $updateTime; + $this->context = $context; } public function getTime( $context = '' ) { - if ( $context === 'create' ) { - return $this->createTime; - } else if ( $context === 'update' ) { - return $this->updateTime; + if ( array_key_exists( $context, $this->context )) { + return $this->context[$context]; } else { return new DateTime( 'now' ); }