From 366e34069cadb9a969a13551f7aef770244650e2 Mon Sep 17 00:00:00 2001 From: Jeremy Dormitzer Date: Mon, 14 Jan 2019 11:12:47 -0500 Subject: [PATCH] [WIP] Begin implementing http sig signing/verification --- composer.json | 4 +- composer.lock | 336 ++++++++++++++++++++++++- src/Auth/AuthenticationService.php | 32 +++ src/Auth/HttpSignatureService.php | 116 +++++++++ src/Auth/HttpSignatureValidator.php | 32 +++ test/Auth/HttpSignatureServiceTest.php | 73 ++++++ 6 files changed, 591 insertions(+), 2 deletions(-) create mode 100644 src/Auth/AuthenticationService.php create mode 100644 src/Auth/HttpSignatureService.php create mode 100644 src/Auth/HttpSignatureValidator.php create mode 100644 test/Auth/HttpSignatureServiceTest.php diff --git a/composer.json b/composer.json index 5eb34d4..aac0d4e 100644 --- a/composer.json +++ b/composer.json @@ -24,8 +24,10 @@ "require": { "doctrine/orm": "^2.6", "friendica/json-ld": "^1.1", + "guzzlehttp/guzzle": "^6.3", "phpseclib/phpseclib": "^2.0", - "symfony/http-kernel": "^4.2" + "symfony/http-kernel": "^4.2", + "symfony/psr-http-message-bridge": "^1.1" }, "require-dev": { "phpunit/dbunit": "^4.0", diff --git a/composer.lock b/composer.lock index 0057cf5..610739c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0b28ccdd3b389a1e12672e87cfe8a643", + "content-hash": "7312404cd7d0385ed891364e07144079", "packages": [ { "name": "doctrine/annotations", @@ -911,6 +911,189 @@ ], "time": "2018-10-08T20:41:00+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "reference": "407b0cb880ace85c9b63c5f9551db498cb2d50ba", + "shasum": "" + }, + "require": { + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.4", + "php": ">=5.5" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.0" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.3-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2018-04-22T15:46:56+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "v1.3.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "shasum": "" + }, + "require": { + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "time": "2016-12-20T10:07:11+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.5.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "9f83dded91781a01c63574e387eaa769be769115" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/9f83dded91781a01c63574e387eaa769be769115", + "reference": "9f83dded91781a01c63574e387eaa769be769115", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Schultze", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "time": "2018-12-04T20:46:45+00:00" + }, { "name": "phpseclib/phpseclib", "version": "2.0.13", @@ -1003,6 +1186,56 @@ ], "time": "2018-12-16T17:45:25+00:00" }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, { "name": "psr/log", "version": "1.1.0", @@ -1050,6 +1283,46 @@ ], "time": "2018-11-20T15:27:04+00:00" }, + { + "name": "ralouphie/getallheaders", + "version": "2.0.5", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "reference": "5601c8a83fbba7ef674a7369456d12f1e0d0eafa", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "~3.7.0", + "satooshi/php-coveralls": ">=1.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "time": "2016-02-11T07:05:27+00:00" + }, { "name": "symfony/console", "version": "v4.2.1", @@ -1566,6 +1839,67 @@ "shim" ], "time": "2018-09-21T13:07:52+00:00" + }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "53c15a6a7918e6c2ab16ae370ea607fb40cab196" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/53c15a6a7918e6c2ab16ae370ea607fb40cab196", + "reference": "53c15a6a7918e6c2ab16ae370ea607fb40cab196", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0", + "psr/http-message": "^1.0", + "symfony/http-foundation": "^2.3.42 || ^3.4 || ^4.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3.4 || 4.0" + }, + "suggest": { + "psr/http-factory-implementation": "To use the PSR-17 factory", + "psr/http-message-implementation": "To use the HttpFoundation factory", + "zendframework/zend-diactoros": "To use the Zend Diactoros factory" + }, + "type": "symfony-bridge", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "http://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-7" + ], + "time": "2018-08-30T16:28:28+00:00" } ], "packages-dev": [ diff --git a/src/Auth/AuthenticationService.php b/src/Auth/AuthenticationService.php new file mode 100644 index 0000000..3e68677 --- /dev/null +++ b/src/Auth/AuthenticationService.php @@ -0,0 +1,32 @@ +authFunction = $authFunction; + } +} +?> diff --git a/src/Auth/HttpSignatureService.php b/src/Auth/HttpSignatureService.php new file mode 100644 index 0000000..402a93c --- /dev/null +++ b/src/Auth/HttpSignatureService.php @@ -0,0 +1,116 @@ +headers; + if ( $headers->has( 'signature' ) ) { + $params = $this->parseSignatureParams( $headers->get( 'signature' ) ); + } else if ( $headers->has( 'authorization' ) && + substr($headers->get( 'authorization' ), 0, 9) === 'Signature' ) { + $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 = $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; + } + + /** + * Returns the signing string from the request + * + * @param Request $request The request + * @param array $headers The headers to use to generate the signing string + * @return string The signing string + */ + private function getSigningString( Request $request, $headers ) + { + $signingComponents = array(); + foreach ( $headers as $header ) { + $component = "${header}: "; + if ( $header == '(request-target)' ) { + $method = strtolower( $request->method ); + $path = $request->getRequestUri(); + $component = $component . $method . ' ' . $path; + } else { + // TODO handle 'digest' specially here too + $values = $request->headers->get( $header, null, false ); + $component = $component . implode( ', ', $values ); + } + $signingComponents[] = $component; + } + return implode( '\n', $signingComponents ); + } + + /** + * Parses the signature params from the provided params string + * + * @param string $paramsStr The params represented as a string, + * e.g. 'keyId="theKey",algorithm="rsa-sha256"' + * @return array The params as an associative array + */ + private function parseSignatureParams( string $paramsStr ) + { + $params = array(); + $split = HeaderUtils::split( $paramsStr, ',= ' ); + foreach ( $split as $paramArr ) { + $paramName = $paramArr[0]; + $paramValue = $paramArr[1]; + if ( count( $paramValue ) === 1 ) { + $paramValue = $paramValue[0]; + } + $params[$paramName] = $paramValue; + } + return $params; + } +} +?> diff --git a/src/Auth/HttpSignatureValidator.php b/src/Auth/HttpSignatureValidator.php new file mode 100644 index 0000000..48fdeae --- /dev/null +++ b/src/Auth/HttpSignatureValidator.php @@ -0,0 +1,32 @@ + 'validateHttpSignature' + ); + } + + /** + * Check for a valid HTTP signature on the request. If the request has a valid signature, + * set the 'signed' and 'signedBy' keys on the request ('signedBy' is the id of the actor + * whose key signed the request) + */ + public function validateHttpSignature( GetResponseEvent $event ) + { + $request = $event->getRequest(); + } +} +?> diff --git a/test/Auth/HttpSignatureServiceTest.php b/test/Auth/HttpSignatureServiceTest.php new file mode 100644 index 0000000..3614485 --- /dev/null +++ b/test/Auth/HttpSignatureServiceTest.php @@ -0,0 +1,73 @@ +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 ); + } +} +?>