Signature Verification with phpseclib3
This commit is contained in:
parent
44c652eba8
commit
86c796090d
6 changed files with 144 additions and 2 deletions
|
@ -15,6 +15,8 @@
|
||||||
|
|
||||||
namespace Activitypub;
|
namespace Activitypub;
|
||||||
|
|
||||||
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize plugin
|
* Initialize plugin
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
"type": "wordpress-plugin",
|
"type": "wordpress-plugin",
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.6.0",
|
"php": ">=5.6.0",
|
||||||
"composer/installers": "~1.0"
|
"composer/installers": "~1.0",
|
||||||
|
"phpseclib/phpseclib": "~3.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5 || ^8",
|
"phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5 || ^8",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Activitypub;
|
namespace Activitypub;
|
||||||
|
|
||||||
|
use phpseclib3\Crypt\RSA;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActivityPub Signature Class
|
* ActivityPub Signature Class
|
||||||
*
|
*
|
||||||
|
@ -8,6 +10,29 @@ namespace Activitypub;
|
||||||
*/
|
*/
|
||||||
class Signature {
|
class Signature {
|
||||||
|
|
||||||
|
public const SIGNATURE_PATTERN = '/^
|
||||||
|
keyId="(?P<keyId>
|
||||||
|
(https?:\/\/[\w\-\.]+[\w]+)
|
||||||
|
(:[\d]+)?
|
||||||
|
([\w\-\.#\/@]+)
|
||||||
|
)",
|
||||||
|
(algorithm="(?P<algorithm>[\w\s-]+)",)?
|
||||||
|
(headers="(?P<headers>[\(\)\w\s-]+)",)?
|
||||||
|
signature="(?P<signature>[\w+\/]+={0,2})"
|
||||||
|
/x';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed keys when splitting signature
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $allowedKeys = [
|
||||||
|
'keyId',
|
||||||
|
'algorithm', // optional
|
||||||
|
'headers', // optional
|
||||||
|
'signature',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param int $user_id
|
* @param int $user_id
|
||||||
*
|
*
|
||||||
|
@ -109,6 +134,114 @@ class Signature {
|
||||||
|
|
||||||
public static function verify_signature( $headers, $signature ) {
|
public static function verify_signature( $headers, $signature ) {
|
||||||
|
|
||||||
|
// https://github.com/landrok/activitypub/blob/master/src/ActivityPhp/Server/Http/HttpSignature.php
|
||||||
|
$header_data = $request->get_headers();
|
||||||
|
$body = $request->get_body();
|
||||||
|
if ( !$header_data['signature'][0] ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Split it into its parts ( keyId, headers and signature )
|
||||||
|
$signature_parts = self::splitSignature( $header_data['signature'][0] );
|
||||||
|
if ( !count($signature_parts ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
extract( $signature_parts );
|
||||||
|
|
||||||
|
// Fetch the public key linked from keyId
|
||||||
|
$actor = \strip_fragment_from_url( $keyId );
|
||||||
|
$publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId );
|
||||||
|
|
||||||
|
if (! is_wp_error( $publicKeyPem ) ) {
|
||||||
|
$pkey = \openssl_pkey_get_details( \openssl_pkey_get_public( $publicKeyPem ) );
|
||||||
|
$digest_gen = 'SHA-256=' . \base64_encode( \hash( 'sha256', $body, true ) );
|
||||||
|
if ( $digest_gen !== $header_data['digest'][0] ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Create a comparison string from the plaintext headers we got
|
||||||
|
// in the same order as was given in the signature header,
|
||||||
|
$data_plain = self::getPlainText(
|
||||||
|
explode(' ', trim( $headers ) ),
|
||||||
|
$request
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify that string using the public key and the original
|
||||||
|
// signature.
|
||||||
|
$rsa = RSA::createKey()
|
||||||
|
->loadPublicKey( $pkey['key'])
|
||||||
|
->withHash('sha256');
|
||||||
|
|
||||||
|
$verified = $rsa->verify( $data_plain, \base64_decode( $signature ) );
|
||||||
|
|
||||||
|
if ( '1' === $verified ) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$activity = json_decode($body);
|
||||||
|
if ( $activity->type === 'Delete' ) {
|
||||||
|
// TODO eventually process ld signatures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split HTTP signature into its parts (keyId, headers and signature)
|
||||||
|
*/
|
||||||
|
public static function splitSignature( $signature ) {
|
||||||
|
|
||||||
|
$allowedKeys = [
|
||||||
|
'keyId',
|
||||||
|
'algorithm', // optional
|
||||||
|
'headers', // optional
|
||||||
|
'signature',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!preg_match(self::SIGNATURE_PATTERN, $signature, $matches)) {
|
||||||
|
\error_log('Signature pattern failed' . print_r( $signature, true ) );
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers are optional
|
||||||
|
if (!isset($matches['headers']) || $matches['headers'] == '') {
|
||||||
|
$matches['headers'] = 'date';
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_filter($matches, function($key) use ($allowedKeys) {
|
||||||
|
return !is_int($key) && in_array($key, $allowedKeys);
|
||||||
|
}, ARRAY_FILTER_USE_KEY );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plain text that has been originally signed
|
||||||
|
*
|
||||||
|
* @param array $headers HTTP header keys
|
||||||
|
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||||
|
*/
|
||||||
|
public static function getPlainText( $headers, $request ) {
|
||||||
|
|
||||||
|
$url_params = $request->get_url_params();
|
||||||
|
if ( isset( $url_params ) && isset( $url_params['user_id'] ) ) {
|
||||||
|
$url_params = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$strings = [];
|
||||||
|
$request_target = sprintf(
|
||||||
|
'%s %s%s',
|
||||||
|
strtolower($request->get_method()),
|
||||||
|
$request->get_route(),
|
||||||
|
$url_params
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($headers as $value) {
|
||||||
|
if ( $value == '(request-target)' ) {
|
||||||
|
$strings[] = "$value: " . $request_target;
|
||||||
|
} else {
|
||||||
|
$strings[] = "$value: " . $request->get_header($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode("\n", $strings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function generate_digest( $body ) {
|
public static function generate_digest( $body ) {
|
||||||
|
|
|
@ -183,6 +183,11 @@ function get_inbox_by_actor( $actor ) {
|
||||||
function get_publickey_by_actor( $actor, $key_id ) {
|
function get_publickey_by_actor( $actor, $key_id ) {
|
||||||
$metadata = \Activitypub\get_remote_metadata_by_actor( $actor );
|
$metadata = \Activitypub\get_remote_metadata_by_actor( $actor );
|
||||||
|
|
||||||
|
//Other Implementations may include an 'operations' query (Zap)
|
||||||
|
if ( isset( $metadata['publicKey']['id'] ) ) {
|
||||||
|
$metadata['publicKey']['id'] = strtok($metadata['publicKey']['id'], "?");
|
||||||
|
}
|
||||||
|
|
||||||
if ( \is_wp_error( $metadata ) ) {
|
if ( \is_wp_error( $metadata ) ) {
|
||||||
return $metadata;
|
return $metadata;
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,7 @@ class Inbox {
|
||||||
$headers = $request->get_headers();
|
$headers = $request->get_headers();
|
||||||
|
|
||||||
// verify signature
|
// verify signature
|
||||||
|
\Activitypub\Signature::verify_signature( $request );
|
||||||
//\Activitypub\Signature::verify_signature( $headers, $key );
|
//\Activitypub\Signature::verify_signature( $headers, $key );
|
||||||
|
|
||||||
return $served;
|
return $served;
|
||||||
|
|
|
@ -24,7 +24,7 @@ class Server extends \WP_REST_Server {
|
||||||
|
|
||||||
// check for content-sub-types like 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
// check for content-sub-types like 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||||
if ( \preg_match( '/application\/([a-zA-Z+_-]+\+)json/', $content_type['value'] ) ) {
|
if ( \preg_match( '/application\/([a-zA-Z+_-]+\+)json/', $content_type['value'] ) ) {
|
||||||
$request->set_header( 'Content-Type', 'application/json' );
|
// Signature Verification requires headers to be intact
|
||||||
}
|
}
|
||||||
|
|
||||||
// make request filterable
|
// make request filterable
|
||||||
|
|
Loading…
Reference in a new issue