[WIP] Begin implementing http sig signing/verification

This commit is contained in:
Jeremy Dormitzer 2019-01-14 11:12:47 -05:00
parent b344c7ae8a
commit 366e34069c
6 changed files with 591 additions and 2 deletions

View File

@ -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",

336
composer.lock generated
View File

@ -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": [

View File

@ -0,0 +1,32 @@
<?php
namespace ActivityPub\Auth;
/**
* The AuthenticationService class answers the question, "is this request authenticated
* to act on behalf of this Actor?"
*
* It delegates most of the work to a passed-in Callable to allow library clients to
* plug in their own authentication methods.
*/
class AuthenticationService
{
/**
* The Callable that is called to determine if a request is authorized for an Actor
*
* @var Callable
*
*/
private $authFunction;
/**
* Constructs a new AuthenticationService
*
* @param Callable $authFunction A Callable that should accept
*
*/
public function __construct( Callable $authFunction )
{
$this->authFunction = $authFunction;
}
}
?>

View File

@ -0,0 +1,116 @@
<?php
namespace ActivityPub\Auth;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\HeaderUtils;
/**
* The HttpSignatureService provides methods to generate and verify HTTP signatures
*/
class HttpSignatureService
{
// TODO handle the Digest header better, both on generating and verifying
const DEFAULT_HEADERS = array(
'(request-target)',
'host',
'date',
);
public function sign( Request $request, string $privateKey, $headers = self::DEFAULT_HEADERS )
{
// To generate a signature for a request:
// 1. put together the signing string from the headers list
// 2. generate an RSA-sha256 signature of the signing string using the private key
// 3. return the signature base64-encoded
}
/**
* Verifies the HTTP signature of $request
*
* @param Request $request The request to verify
* @param string $publicKey The public key to use to verify the request
* @return bool True if the signature is valid, false if it is missing or invalid
*/
public function verify( Request $request, string $publicKey )
{
// To verify a signature:
// 1. Re-create the signing string from the request and the headers
// 2. verify that the signature is signed correctly using the public key and the signing string
// The signature can either be in the Authentication header or the Signature header.
// If it's in the Authentication header, the params will be prefixed with the string "Signature",
// e.g. Authentication: Signature keyId="key-1",algorithm="rsa-sha256",headers="(request-target) host date",signature="thesig"
// as opposed to the Signature header, which just has the params as its value:
// Signature: keyId="key-1",algorithm="rsa-sha256",headers="(request-target) host date",signature="thesig"
$params = array();
$headers = $request->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;
}
}
?>

View File

@ -0,0 +1,32 @@
<?php
namespace ActivityPub\Auth;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* The HttpSignatureValidator is a subscriber to the kernel.request event
* that validates HTTP signatures if present.
*
*/
class HttpSignatureValidator implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => '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();
}
}
?>

View File

@ -0,0 +1,73 @@
<?php
// tests:
// - signature for request that specifies a header but is missing that header
// - signature for request with malformed Signature header
namespace ActivityPub\Test\Auth;
use ActivityPub\Auth\HttpSignatureService;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
class HttpSignatureServiceTest extends TestCase
{
const PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3
6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6
Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw
oYi+1hqp1fIekaxsyQIDAQAB
-----END PUBLIC KEY-----";
const PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF
NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F
UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB
AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA
QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK
kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg
f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u
412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc
mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7
kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA
gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW
G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA==
-----END RSA PRIVATE KEY-----";
private $httpSignatureService;
public function setUp()
{
$this->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 );
}
}
?>