Fix http signatures signing of psr7 requests

This commit is contained in:
Jeremy Dormitzer 2019-01-17 14:47:30 -05:00
parent 634db57684
commit 4c5b667175
4 changed files with 152 additions and 15 deletions

View File

@ -28,7 +28,8 @@
"phpseclib/phpseclib": "^2.0", "phpseclib/phpseclib": "^2.0",
"psr/http-message": "^1.0", "psr/http-message": "^1.0",
"symfony/http-kernel": "^4.2", "symfony/http-kernel": "^4.2",
"symfony/psr-http-message-bridge": "^1.1" "symfony/psr-http-message-bridge": "^1.1",
"zendframework/zend-diactoros": "^1.8"
}, },
"require-dev": { "require-dev": {
"phpunit/dbunit": "^4.0", "phpunit/dbunit": "^4.0",

118
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "fa1affc2b91bce0c5e025b09069a9fc6", "content-hash": "4fdd0e94241c59ddd2c9763881167989",
"packages": [ "packages": [
{ {
"name": "doctrine/annotations", "name": "doctrine/annotations",
@ -1900,6 +1900,70 @@
"psr-7" "psr-7"
], ],
"time": "2018-08-30T16:28:28+00:00" "time": "2018-08-30T16:28:28+00:00"
},
{
"name": "zendframework/zend-diactoros",
"version": "1.8.6",
"source": {
"type": "git",
"url": "https://github.com/zendframework/zend-diactoros.git",
"reference": "20da13beba0dde8fb648be3cc19765732790f46e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/20da13beba0dde8fb648be3cc19765732790f46e",
"reference": "20da13beba0dde8fb648be3cc19765732790f46e",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0",
"psr/http-message": "^1.0"
},
"provide": {
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"ext-dom": "*",
"ext-libxml": "*",
"php-http/psr7-integration-tests": "dev-master",
"phpunit/phpunit": "^5.7.16 || ^6.0.8 || ^7.2.7",
"zendframework/zend-coding-standard": "~1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.8.x-dev",
"dev-develop": "1.9.x-dev",
"dev-release-2.0": "2.0.x-dev"
}
},
"autoload": {
"files": [
"src/functions/create_uploaded_file.php",
"src/functions/marshal_headers_from_sapi.php",
"src/functions/marshal_method_from_sapi.php",
"src/functions/marshal_protocol_version_from_sapi.php",
"src/functions/marshal_uri_from_sapi.php",
"src/functions/normalize_server.php",
"src/functions/normalize_uploaded_files.php",
"src/functions/parse_cookie_header.php"
],
"psr-4": {
"Zend\\Diactoros\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"description": "PSR HTTP Message implementations",
"homepage": "https://github.com/zendframework/zend-diactoros",
"keywords": [
"http",
"psr",
"psr-7"
],
"time": "2018-09-05T19:29:37+00:00"
} }
], ],
"packages-dev": [ "packages-dev": [
@ -2657,6 +2721,58 @@
], ],
"time": "2018-12-12T07:20:32+00:00" "time": "2018-12-12T07:20:32+00:00"
}, },
{
"name": "psr/http-factory",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
"reference": "378bfe27931ecc54ff824a20d6f6bfc303bbd04c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-factory/zipball/378bfe27931ecc54ff824a20d6f6bfc303bbd04c",
"reference": "378bfe27931ecc54ff824a20d6f6bfc303bbd04c",
"shasum": ""
},
"require": {
"php": ">=7.0.0",
"psr/http-message": "^1.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 interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
"message",
"psr",
"psr-17",
"psr-7",
"request",
"response"
],
"time": "2018-07-30T21:54:04+00:00"
},
{ {
"name": "sebastian/code-unit-reverse-lookup", "name": "sebastian/code-unit-reverse-lookup",
"version": "1.0.1", "version": "1.0.1",

View File

@ -5,7 +5,7 @@ use DateTime;
use ActivityPub\Utils\DateTimeProvider; use ActivityPub\Utils\DateTimeProvider;
use ActivityPub\Utils\SimpleDateTimeProvider; use ActivityPub\Utils\SimpleDateTimeProvider;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory; use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\HeaderUtils; use Symfony\Component\HttpFoundation\HeaderUtils;
@ -29,9 +29,9 @@ class HttpSignatureService
private $dateTimeProvider; private $dateTimeProvider;
/** /**
* @var HttpFoundationFactory * @var DiactorosFactory
*/ */
private $httpFoundationFactory; private $psr7Factory;
/** /**
* Constructs a new HttpSignatureService * Constructs a new HttpSignatureService
@ -45,7 +45,7 @@ class HttpSignatureService
$dateTimeProvider = new SimpleDateTimeProvider(); $dateTimeProvider = new SimpleDateTimeProvider();
} }
$this->dateTimeProvider = $dateTimeProvider; $this->dateTimeProvider = $dateTimeProvider;
$this->httpFoundationFactory = new HttpFoundationFactory(); $this->psr7Factory = new DiactorosFactory();
} }
/** /**
@ -58,10 +58,9 @@ class HttpSignatureService
* (default ['(request-target)', 'host', 'date']) * (default ['(request-target)', 'host', 'date'])
* @return string The Signature header value * @return string The Signature header value
*/ */
public function sign( RequestInterface $psrRequest, string $privateKey, public function sign( RequestInterface $request, string $privateKey,
string $keyId, $headers = self::DEFAULT_HEADERS ) string $keyId, $headers = self::DEFAULT_HEADERS )
{ {
$request = $this->httpFoundationFactory->createRequest( $psrRequest );
$headers = array_map( 'strtolower', $headers ); $headers = array_map( 'strtolower', $headers );
$signingString = $this->getSigningString( $request, $headers ); $signingString = $this->getSigningString( $request, $headers );
$keypair = RsaKeypair::fromPrivateKey( $privateKey ); $keypair = RsaKeypair::fromPrivateKey( $privateKey );
@ -111,7 +110,8 @@ class HttpSignatureService
$targetHeaders = $params['headers']; $targetHeaders = $params['headers'];
} }
$signingString = $this->getSigningString( $request, $targetHeaders ); $psrRequest = $this->psr7Factory->createRequest( $request );
$signingString = $this->getSigningString( $psrRequest, $targetHeaders );
$signature = base64_decode( $params['signature'] ); $signature = base64_decode( $params['signature'] );
// TODO handle different algorithms here, checking the 'algorithm' param and the key headers // TODO handle different algorithms here, checking the 'algorithm' param and the key headers
$keypair = RsaKeypair::fromPublicKey( $publicKey ); $keypair = RsaKeypair::fromPublicKey( $publicKey );
@ -125,18 +125,22 @@ class HttpSignatureService
* @param array $headers The headers to use to generate the signing string * @param array $headers The headers to use to generate the signing string
* @return string The signing string * @return string The signing string
*/ */
private function getSigningString( Request $request, $headers ) private function getSigningString( RequestInterface $request, $headers )
{ {
$signingComponents = array(); $signingComponents = array();
foreach ( $headers as $header ) { foreach ( $headers as $header ) {
$component = "${header}: "; $component = "${header}: ";
if ( $header == '(request-target)' ) { if ( $header == '(request-target)' ) {
$method = strtolower( $request->getMethod()); $method = strtolower( $request->getMethod());
$path = $request->getRequestUri(); $path = $request->getUri()->getPath();
$query = $request->getUri()->getQuery();
if ( ! empty( $query ) ) {
$path = "$path?$query";
}
$component = $component . $method . ' ' . $path; $component = $component . $method . ' ' . $path;
} else { } else {
// TODO handle 'digest' specially here too // TODO handle 'digest' specially here too
$values = $request->headers->get( $header, null, false ); $values = $request->getHeader( $header );
$component = $component . implode( ', ', $values ); $component = $component . implode( ', ', $values );
} }
$signingComponents[] = $component; $signingComponents[] = $component;

View File

@ -4,6 +4,7 @@ namespace ActivityPub\Test\Crypto;
use DateTime; use DateTime;
use ActivityPub\Crypto\HttpSignatureService; use ActivityPub\Crypto\HttpSignatureService;
use ActivityPub\Test\TestUtils\TestDateTimeProvider; use ActivityPub\Test\TestUtils\TestDateTimeProvider;
use GuzzleHttp\Psr7\Request as PsrRequest;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -44,7 +45,7 @@ G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
$this->httpSignatureService = new HttpSignatureService( $dateTimeProvider ); $this->httpSignatureService = new HttpSignatureService( $dateTimeProvider );
} }
private static function getRequest() private static function getSymfonyRequest()
{ {
$request = Request::create( $request = Request::create(
'https://example.com/foo?param=value&pet=dog', 'https://example.com/foo?param=value&pet=dog',
@ -65,6 +66,21 @@ G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
return $request; return $request;
} }
private static function getPsrRequest()
{
$headers = array(
'Host' => 'example.com',
'Content-Type' => 'application/json',
'Digest' => 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=',
'Content-Length' => 18,
'Date' => 'Sun, 05 Jan 2014 21:31:40 GMT'
);
$body = '{"hello": "world"}';
return new PsrRequest(
'POST', 'https://example.com/foo?param=value&pet=dog', $headers, $body
);
}
public function testItVerifies() public function testItVerifies()
{ {
$testCases = array( $testCases = array(
@ -164,7 +180,7 @@ G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
) ); ) );
$this->httpSignatureService = new HttpSignatureService( $dateTimeProvider ); $this->httpSignatureService = new HttpSignatureService( $dateTimeProvider );
} }
$request = self::getRequest(); $request = self::getSymfonyRequest();
foreach ( $testCase['headers'] as $header => $value ) { foreach ( $testCase['headers'] as $header => $value ) {
$request->headers->set( $header, $value ); $request->headers->set( $header, $value );
} }
@ -198,7 +214,7 @@ G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
), ),
); );
foreach ( $testCases as $testCase ) { foreach ( $testCases as $testCase ) {
$request = self::getRequest(); $request = self::getPsrRequest();
if ( array_key_exists( 'headers', $testCase ) ) { if ( array_key_exists( 'headers', $testCase ) ) {
$actual = $this->httpSignatureService->sign( $actual = $this->httpSignatureService->sign(
$request, self::PRIVATE_KEY, $testCase['keyId'], $testCase['headers'] $request, self::PRIVATE_KEY, $testCase['keyId'], $testCase['headers']