From 86c796090d3319ed5dc3a0bf7f76712719896302 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Mon, 28 Feb 2022 15:52:30 -0700 Subject: [PATCH 01/81] Signature Verification with phpseclib3 --- activitypub.php | 2 + composer.json | 3 +- includes/class-signature.php | 133 +++++++++++++++++++++++++++++++++ includes/functions.php | 5 ++ includes/rest/class-inbox.php | 1 + includes/rest/class-server.php | 2 +- 6 files changed, 144 insertions(+), 2 deletions(-) diff --git a/activitypub.php b/activitypub.php index 55d569a..1e69abc 100644 --- a/activitypub.php +++ b/activitypub.php @@ -15,6 +15,8 @@ namespace Activitypub; +require __DIR__ . '/vendor/autoload.php'; + /** * Initialize plugin */ diff --git a/composer.json b/composer.json index 76d9b4c..0e70dc7 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "type": "wordpress-plugin", "require": { "php": ">=5.6.0", - "composer/installers": "~1.0" + "composer/installers": "~1.0", + "phpseclib/phpseclib": "~3.0" }, "require-dev": { "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5 || ^8", diff --git a/includes/class-signature.php b/includes/class-signature.php index 5caf884..b89b87f 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -1,6 +1,8 @@ + (https?:\/\/[\w\-\.]+[\w]+) + (:[\d]+)? + ([\w\-\.#\/@]+) + )", + (algorithm="(?P[\w\s-]+)",)? + (headers="(?P[\(\)\w\s-]+)",)? + signature="(?P[\w+\/]+={0,2})" + /x'; + + /** + * Allowed keys when splitting signature + * + * @var array + */ + private $allowedKeys = [ + 'keyId', + 'algorithm', // optional + 'headers', // optional + 'signature', + ]; + /** * @param int $user_id * @@ -109,6 +134,114 @@ class 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 ) { diff --git a/includes/functions.php b/includes/functions.php index 3c33664..94bfaba 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -182,6 +182,11 @@ function get_inbox_by_actor( $actor ) { */ function get_publickey_by_actor( $actor, $key_id ) { $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 ) ) { return $metadata; diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 1ffe451..8c246d7 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -82,6 +82,7 @@ class Inbox { $headers = $request->get_headers(); // verify signature + \Activitypub\Signature::verify_signature( $request ); //\Activitypub\Signature::verify_signature( $headers, $key ); return $served; diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index ac89dca..685b315 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -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"' 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 From 99630a58bb8498ff0197456d860f9b9783d15ba7 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Mon, 28 Feb 2022 19:32:26 -0700 Subject: [PATCH 02/81] fixes --- includes/class-signature.php | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index b89b87f..a1eddfa 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -20,18 +20,6 @@ class Signature { (headers="(?P[\(\)\w\s-]+)",)? signature="(?P[\w+\/]+={0,2})" /x'; - - /** - * Allowed keys when splitting signature - * - * @var array - */ - private $allowedKeys = [ - 'keyId', - 'algorithm', // optional - 'headers', // optional - 'signature', - ]; /** * @param int $user_id @@ -132,7 +120,7 @@ class Signature { } } - public static function verify_signature( $headers, $signature ) { + public static function verify_signature( $request ) { // https://github.com/landrok/activitypub/blob/master/src/ActivityPhp/Server/Http/HttpSignature.php $header_data = $request->get_headers(); @@ -140,7 +128,8 @@ class Signature { if ( !$header_data['signature'][0] ) { return false; } - // Split it into its parts ( keyId, headers and signature ) + + // Split it into its parts ( keyId, headers and signature ) $signature_parts = self::splitSignature( $header_data['signature'][0] ); if ( !count($signature_parts ) ) { return false; @@ -157,6 +146,7 @@ class Signature { 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( @@ -177,12 +167,8 @@ class Signature { } else { return false; } - } else { - $activity = json_decode($body); - if ( $activity->type === 'Delete' ) { - // TODO eventually process ld signatures - } } + return true; } /** @@ -198,7 +184,6 @@ class Signature { ]; if (!preg_match(self::SIGNATURE_PATTERN, $signature, $matches)) { - \error_log('Signature pattern failed' . print_r( $signature, true ) ); return []; } From 1f6e1cf37ccd2e3030cc1c554db06055dafbcb96 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sat, 19 Mar 2022 20:19:59 -0600 Subject: [PATCH 03/81] add openss_verify method and openssl_error_string --- includes/class-signature.php | 46 ++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index a1eddfa..518baa4 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -129,21 +129,21 @@ class Signature { return false; } - // Split it into its parts ( keyId, headers and signature ) + // Split signature into its parts $signature_parts = self::splitSignature( $header_data['signature'][0] ); - if ( !count($signature_parts ) ) { + if ( !count( $signature_parts ) ) { return false; } - extract( $signature_parts ); + extract( $signature_parts );// $keyId, $algorithm, $headers, $signature // Fetch the public key linked from keyId $actor = \strip_fragment_from_url( $keyId ); - $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $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] ) { + if ( ! isset( $header_data['digest'][0] ) || ( $digest_gen !== $header_data['digest'][0] ) ) { return false; } @@ -154,21 +154,41 @@ class Signature { $request ); - // Verify that string using the public key and the original - // signature. + // 2 methods because neither works ¯\_(ツ)_/¯ + // phpseclib method $rsa = RSA::createKey() ->loadPublicKey( $pkey['key']) ->withHash('sha256'); - - $verified = $rsa->verify( $data_plain, \base64_decode( $signature ) ); - - if ( '1' === $verified ) { + $verified = $rsa->verify( $signing_headers, \base64_decode( $signature ) ); + if ( $verified > 0 ) { + \error_log( '$rsa->verify: //return true;' ); return true; } else { + while ( $ossl_error = openssl_error_string() ) { + \error_log( '$rsa->verify(): ' . $ossl_error ); + } + $activity = \json_decode( $body ); + \error_log( 'activity->type: ' . print_r( $activity->type, true ) ); return false; - } + } + + // openssl method + $verified = \openssl_verify( $signing_headers, + \base64_decode( \normalize_whitespace( $signature ) ), + $pkey['key'], + \OPENSSL_ALGO_SHA256 + ); + if ( $verified > 0 ) { + \error_log( 'openssl_verify: //return true;' ); + return true; + } else { + while ( $ossl_error = openssl_error_string() ) { + \error_log( 'openssl_error_string(): ' . $ossl_error ); + } + return false; + } } - return true; + return false; } /** From 16ae895312053b96904c22a956b1dee11eed058c Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 20 Mar 2022 20:57:01 -0600 Subject: [PATCH 04/81] fixes --- includes/class-signature.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 518baa4..625649f 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -140,8 +140,11 @@ class Signature { $actor = \strip_fragment_from_url( $keyId ); $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); - if (! is_wp_error( $publicKeyPem ) ) { + if ( !is_wp_error( $publicKeyPem ) ) { + // Probably overkill since we already have a seemingly weelformed PEM $pkey = \openssl_pkey_get_details( \openssl_pkey_get_public( $publicKeyPem ) ); + + // Verify Digest $digest_gen = 'SHA-256=' . \base64_encode( \hash( 'sha256', $body, true ) ); if ( ! isset( $header_data['digest'][0] ) || ( $digest_gen !== $header_data['digest'][0] ) ) { return false; @@ -149,7 +152,7 @@ class Signature { // 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( + $signing_headers = self::getPlainText( explode(' ', trim( $headers ) ), $request ); @@ -169,7 +172,7 @@ class Signature { } $activity = \json_decode( $body ); \error_log( 'activity->type: ' . print_r( $activity->type, true ) ); - return false; + //return false; } // openssl method @@ -185,7 +188,7 @@ class Signature { while ( $ossl_error = openssl_error_string() ) { \error_log( 'openssl_error_string(): ' . $ossl_error ); } - return false; + //return false; } } return false; From 63993b20b949173f70bc0631fd085eef1334fae6 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 20 Mar 2022 21:12:53 -0600 Subject: [PATCH 05/81] fix const --- includes/class-signature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 625649f..fa373e8 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -10,7 +10,7 @@ use phpseclib3\Crypt\RSA; */ class Signature { - public const SIGNATURE_PATTERN = '/^ + const SIGNATURE_PATTERN = '/^ keyId="(?P (https?:\/\/[\w\-\.]+[\w]+) (:[\d]+)? From 0c7cec3eba21dfc139dd80d2b214f3247df4a3c8 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sat, 1 Apr 2023 10:17:56 -0600 Subject: [PATCH 06/81] Fix signature parse verification --- includes/class-signature.php | 286 +++++++++++++++++++--------------- includes/rest/class-inbox.php | 11 +- 2 files changed, 172 insertions(+), 125 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 2f136b3..a4c45f4 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -1,7 +1,8 @@ - (https?:\/\/[\w\-\.]+[\w]+) - (:[\d]+)? - ([\w\-\.#\/@]+) - )", - (algorithm="(?P[\w\s-]+)",)? - (headers="(?P[\(\)\w\s-]+)",)? - signature="(?P[\w+\/]+={0,2})" - /x'; + /** + * How much leeway to provide on the date header in seconds. + * Not everybody uses NTP. + */ + const MAX_TIME_OFFSET = 10800; + + const DEFAULT_SIGNING_ALGORITHM = 'sha256'; /** * @param int $user_id @@ -120,136 +118,176 @@ class Signature { } } - public static function verify_signature( $request ) { + public static function verify_signature( $request = null ) { + $headers = $request->get_headers(); + $headers["(request-target)"][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); - // 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 signature into its parts - $signature_parts = self::splitSignature( $header_data['signature'][0] ); - if ( !count( $signature_parts ) ) { - return false; - } - extract( $signature_parts );// $keyId, $algorithm, $headers, $signature + if ( !$headers ) { + $headers = self::default_server_headers(); + } + if ( array_key_exists( 'signature', $headers ) ) { + $signature_block = self::parse_signature_header( $headers['signature'] ); + } elseif ( array_key_exists( 'authorization', $headers ) ) { + $signature_block = self::parse_signature_header( $headers['authorization'] ); + } - // 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 ) ) { - // Probably overkill since we already have a seemingly weelformed PEM - $pkey = \openssl_pkey_get_details( \openssl_pkey_get_public( $publicKeyPem ) ); + if ( !$signature_block ) { + return false; + } - // Verify Digest - $digest_gen = 'SHA-256=' . \base64_encode( \hash( 'sha256', $body, true ) ); - if ( ! isset( $header_data['digest'][0] ) || ( $digest_gen !== $header_data['digest'][0] ) ) { + $signed_headers = $signature_block['headers']; + if ( ! $signed_headers ) { + $signed_headers = ['date']; + } + + $signed_data = self::get_signed_data( $signed_headers, $signature_block, $headers ); + if ( ! $signed_data ) { + return false; + } + + $algorithm = self::get_signature_algorithm( $signature_block ); + if ( ! $algorithm ) { + return false; + } + + if ( in_array( 'digest', $signed_headers ) && isset( $body ) ) { + $digest = explode( '=', $headers['digest'], 2 ); + if ( $digest[0] === 'SHA-256' ) { + $hashalg = 'sha256'; + } + if ( $digest[0] === 'SHA-512' ) { + $hashalg = 'sha512'; + } + + // TODO Test + if ( base64_encode( hash( $hashalg, $body, true ) ) !== $digest[1] ) { return false; } + } - // Create a comparison string from the plaintext headers we got - // in the same order as was given in the signature header, - $signing_headers = self::getPlainText( - explode(' ', trim( $headers ) ), - $request - ); + $public_key = $key?? self::get_key( $signature_block['keyId'] ); - // 2 methods because neither works ¯\_(ツ)_/¯ - // phpseclib method - $rsa = RSA::createKey() - ->loadPublicKey( $pkey['key']) - ->withHash('sha256'); - $verified = $rsa->verify( $signing_headers, \base64_decode( $signature ) ); - if ( $verified > 0 ) { - \error_log( '$rsa->verify: //return true;' ); - return true; - } else { - while ( $ossl_error = openssl_error_string() ) { - \error_log( '$rsa->verify(): ' . $ossl_error ); - } - $activity = \json_decode( $body ); - \error_log( 'activity->type: ' . print_r( $activity->type, true ) ); - //return false; - } - - // openssl method - $verified = \openssl_verify( $signing_headers, - \base64_decode( \normalize_whitespace( $signature ) ), - $pkey['key'], - \OPENSSL_ALGO_SHA256 - ); - if ( $verified > 0 ) { - \error_log( 'openssl_verify: //return true;' ); - return true; - } else { - while ( $ossl_error = openssl_error_string() ) { - \error_log( 'openssl_error_string(): ' . $ossl_error ); - } - //return false; + return \openssl_verify( $signed_data,$signature_block['signature'], $public_key, $algorithm ) > 0; + + } + + public static function default_server_headers() { + $headers = array( + '(request-target)' => strtolower( $_SERVER['REQUEST_METHOD'] ) . ' ' . $_SERVER['REQUEST_URI'], + 'content-type' => $_SERVER['CONTENT_TYPE'], + 'content-length' => $_SERVER['CONTENT_LENGTH'], + ); + foreach ( $_SERVER as $k => $v ) { + if ( strpos( $k, 'HTTP_' ) === 0 ) { + $field = str_replace( '_', '-', strtolower( substr( $k, 5 ) ) ); + $headers[$field] = $v; } } + return $headers; + } + + public static function get_signature_algorithm( $signature_block ) { + switch ( $signature_block['algorithm'] ) { + case 'rsa-sha256': + return 'sha256'; + case 'rsa-sha-512': + return 'sha512'; + case 'hs2019': + return self::DEFAULT_SIGNING_ALGORITHM; + } return false; } - /** - * Split HTTP signature into its parts (keyId, headers and signature) - */ - public static function splitSignature( $signature ) { - - $allowedKeys = [ - 'keyId', - 'algorithm', // optional - 'headers', // optional - 'signature', - ]; + public static function parse_signature_header( $header ) { + $ret = []; + $matches = []; + $h_string = implode( ',', (array) $header[0] ); - if (!preg_match(self::SIGNATURE_PATTERN, $signature, $matches)) { - 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 = ''; + if ( preg_match( '/keyId="(.*?)"/ism', $h_string, $matches ) ) { + $ret['keyId'] = $matches[1]; + } + if ( preg_match( '/created=([0-9]*)/ism', $h_string, $matches ) ) { + $ret['(created)'] = $matches[1]; + } + if ( preg_match( '/expires=([0-9]*)/ism', $h_string, $matches ) ) { + $ret['(expires)'] = $matches[1]; + } + if ( preg_match( '/algorithm="(.*?)"/ism', $h_string, $matches ) ) { + $ret['algorithm'] = $matches[1]; + } + if ( preg_match( '/headers="(.*?)"/ism', $h_string, $matches ) ) { + $ret['headers'] = explode( ' ', $matches[1] ); + } + if ( preg_match( '/signature="(.*?)"/ism', $h_string, $matches ) ) { + $ret['signature'] = base64_decode( preg_replace( '/\s+/', '', $matches[1] ) ); } - $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); + if ( ( $ret['signature'] ) && ( $ret['algorithm'] ) && ( !$ret['headers'] ) ) { + $ret['headers'] = ['date']; + } + + return $ret; + } + + public static function get_key( $keyId ) { + // If there was no key passed to verify, it will find the keyId and call this + // function to fetch the public key from stored data or a network fetch. + $actor = \strip_fragment_from_url( $keyId ); + $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); + return rtrim( $publicKeyPem ); + } + + + public static function get_signed_data( $signed_headers, $signature_block, $headers ) { + + $signed_data = ''; + // This also verifies time-based values by returning false if any of these are out of range. + foreach ( $signed_headers as $header ) { + if ( array_key_exists($header, $headers ) ) { + if ( $header === 'host' ) { + if ( isset( $headers['x_original_host'] ) ) { + $signed_data .= 'host: ' . $headers['x_original_host'][0] . "\n"; + } else { + $signed_data .= $header . ': ' . $headers[$header][0] . "\n"; + } + } else { + $signed_data .= $header . ': ' . $headers[$header][0] . "\n"; + } + } + if ( $header === '(created)' ) { + if ( !empty( $signature_block['(created)'] ) && intval( $signature_block['(created)'] ) > time() ) { + // created in future + return false; + } + $signed_data .= '(created): ' . $signature_block['(created)'] . "\n"; + } + if ( $header === '(expires)' ) { + if ( !empty( $signature_block['(expires)'] ) && intval( $signature_block['(expires)'] ) < time() ) { + // expired in past + return false; + } + $signed_data .= '(expires): ' . $signature_block['(expires)'] . "\n"; + } + if ( $header === 'content-type' ) { + $signed_data .= $header . ': ' . $headers['content_type'][0] . "\n"; + } + if ( $header === 'date' ) { + // allow a bit of leeway for misconfigured clocks. + $d = new DateTime( $headers[$header][0] ); + $d->setTimeZone( new DateTimeZone('UTC') ); + + $dplus = time() + self::MAX_TIME_OFFSET; + $dminus = time() - self::MAX_TIME_OFFSET; + $c = wp_date( 'U' ); + + if ( $c > $dplus || $c < $dminus ) { + // time out of range + return false; + } } } - - return implode("\n", $strings); + // error_log( '$signed_data: ' . print_r( rtrim( $signed_data, "\n" ), true ) ); + return rtrim($signed_data, "\n"); } public static function generate_digest( $body ) { diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 2ad5ea2..c3ca058 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -84,7 +84,6 @@ class Inbox { // verify signature \Activitypub\Signature::verify_signature( $request ); - //\Activitypub\Signature::verify_signature( $headers, $key ); return $served; } @@ -340,6 +339,16 @@ class Inbox { }, ); + $params['validated'] = array( + 'sanitize_callback' => function( $param, $request, $key ) { + if ( \is_string( $param ) ) { + $param = array( $param ); + } + + return $param; + }, + ); + return $params; } From 504bbb99992879962b8bffec364f830b53c3a0d7 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sat, 1 Apr 2023 23:59:49 -0600 Subject: [PATCH 07/81] code style phpcs --- includes/class-signature.php | 104 +++++++++++++++++------------------ 1 file changed, 50 insertions(+), 54 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index a4c45f4..d10309f 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -1,8 +1,8 @@ get_headers(); - $headers["(request-target)"][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); + $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); - if ( !$headers ) { + if ( ! $headers ) { $headers = self::default_server_headers(); } if ( array_key_exists( 'signature', $headers ) ) { @@ -131,13 +130,13 @@ class Signature { $signature_block = self::parse_signature_header( $headers['authorization'] ); } - if ( !$signature_block ) { + if ( ! $signature_block ) { return false; } $signed_headers = $signature_block['headers']; if ( ! $signed_headers ) { - $signed_headers = ['date']; + $signed_headers = array( 'date' ); } $signed_data = self::get_signed_data( $signed_headers, $signature_block, $headers ); @@ -150,37 +149,36 @@ class Signature { return false; } - if ( in_array( 'digest', $signed_headers ) && isset( $body ) ) { + if ( \in_array( 'digest', $signed_headers, true ) && isset( $body ) ) { $digest = explode( '=', $headers['digest'], 2 ); - if ( $digest[0] === 'SHA-256' ) { + if ( 'SHA-256' === $digest[0] ) { $hashalg = 'sha256'; } - if ( $digest[0] === 'SHA-512' ) { + if ( 'SHA-512' === $digest[0] ) { $hashalg = 'sha512'; } - // TODO Test - if ( base64_encode( hash( $hashalg, $body, true ) ) !== $digest[1] ) { + if ( \base64_encode( \hash( $hashalg, $body, true ) ) !== $digest[1] ) { // phpcs:ignore return false; } } - $public_key = $key?? self::get_key( $signature_block['keyId'] ); + $public_key = isset( $key ) ? $key : self::get_key( $signature_block['keyId'] ); - return \openssl_verify( $signed_data,$signature_block['signature'], $public_key, $algorithm ) > 0; + return \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; } public static function default_server_headers() { - $headers = array( + $headers = array( '(request-target)' => strtolower( $_SERVER['REQUEST_METHOD'] ) . ' ' . $_SERVER['REQUEST_URI'], 'content-type' => $_SERVER['CONTENT_TYPE'], 'content-length' => $_SERVER['CONTENT_LENGTH'], ); foreach ( $_SERVER as $k => $v ) { - if ( strpos( $k, 'HTTP_' ) === 0 ) { - $field = str_replace( '_', '-', strtolower( substr( $k, 5 ) ) ); - $headers[$field] = $v; + if ( \strpos( $k, 'HTTP_' ) === 0 ) { + $field = \str_replace( '_', '-', \strtolower( \substr( $k, 5 ) ) ); + $headers[ $field ] = $v; } } return $headers; @@ -199,86 +197,85 @@ class Signature { } public static function parse_signature_header( $header ) { - $ret = []; - $matches = []; - $h_string = implode( ',', (array) $header[0] ); + $ret = array(); + $matches = array(); + $h_string = \implode( ',', (array) $header[0] ); - if ( preg_match( '/keyId="(.*?)"/ism', $h_string, $matches ) ) { + if ( \preg_match( '/keyId="(.*?)"/ism', $h_string, $matches ) ) { $ret['keyId'] = $matches[1]; } - if ( preg_match( '/created=([0-9]*)/ism', $h_string, $matches ) ) { + if ( \preg_match( '/created=([0-9]*)/ism', $h_string, $matches ) ) { $ret['(created)'] = $matches[1]; } - if ( preg_match( '/expires=([0-9]*)/ism', $h_string, $matches ) ) { + if ( \preg_match( '/expires=([0-9]*)/ism', $h_string, $matches ) ) { $ret['(expires)'] = $matches[1]; } - if ( preg_match( '/algorithm="(.*?)"/ism', $h_string, $matches ) ) { + if ( \preg_match( '/algorithm="(.*?)"/ism', $h_string, $matches ) ) { $ret['algorithm'] = $matches[1]; } - if ( preg_match( '/headers="(.*?)"/ism', $h_string, $matches ) ) { - $ret['headers'] = explode( ' ', $matches[1] ); + if ( \preg_match( '/headers="(.*?)"/ism', $h_string, $matches ) ) { + $ret['headers'] = \explode( ' ', $matches[1] ); } - if ( preg_match( '/signature="(.*?)"/ism', $h_string, $matches ) ) { - $ret['signature'] = base64_decode( preg_replace( '/\s+/', '', $matches[1] ) ); + if ( \preg_match( '/signature="(.*?)"/ism', $h_string, $matches ) ) { + $ret['signature'] = \base64_decode( preg_replace( '/\s+/', '', $matches[1] ) ); // phpcs:ignore } - if ( ( $ret['signature'] ) && ( $ret['algorithm'] ) && ( !$ret['headers'] ) ) { - $ret['headers'] = ['date']; + if ( ( $ret['signature'] ) && ( $ret['algorithm'] ) && ( ! $ret['headers'] ) ) { + $ret['headers'] = array( 'date' ); } return $ret; } - public static function get_key( $keyId ) { + public static function get_key( $keyId ) { // phpcs:ignore // If there was no key passed to verify, it will find the keyId and call this // function to fetch the public key from stored data or a network fetch. - $actor = \strip_fragment_from_url( $keyId ); - $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); - return rtrim( $publicKeyPem ); + $actor = \strip_fragment_from_url( $keyId ); // phpcs:ignore + $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); // phpcs:ignore + return \rtrim( $publicKeyPem ); // phpcs:ignore } public static function get_signed_data( $signed_headers, $signature_block, $headers ) { - $signed_data = ''; // This also verifies time-based values by returning false if any of these are out of range. foreach ( $signed_headers as $header ) { - if ( array_key_exists($header, $headers ) ) { - if ( $header === 'host' ) { + if ( \array_key_exists( $header, $headers ) ) { + if ( 'host' === $header ) { if ( isset( $headers['x_original_host'] ) ) { - $signed_data .= 'host: ' . $headers['x_original_host'][0] . "\n"; + $signed_data .= $header . ': ' . $headers['x_original_host'][0] . "\n"; } else { - $signed_data .= $header . ': ' . $headers[$header][0] . "\n"; + $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; } } else { - $signed_data .= $header . ': ' . $headers[$header][0] . "\n"; + $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; } } - if ( $header === '(created)' ) { - if ( !empty( $signature_block['(created)'] ) && intval( $signature_block['(created)'] ) > time() ) { + if ( '(created)' === $header ) { + if ( ! \empty( $signature_block['(created)'] ) && \intval( $signature_block['(created)'] ) > \time() ) { // created in future return false; } $signed_data .= '(created): ' . $signature_block['(created)'] . "\n"; } - if ( $header === '(expires)' ) { - if ( !empty( $signature_block['(expires)'] ) && intval( $signature_block['(expires)'] ) < time() ) { + if ( '(expires)' === $header ) { + if ( ! empty( $signature_block['(expires)'] ) && \intval( $signature_block['(expires)'] ) < \time() ) { // expired in past return false; } $signed_data .= '(expires): ' . $signature_block['(expires)'] . "\n"; } - if ( $header === 'content-type' ) { + if ( 'content-type' === $header ) { $signed_data .= $header . ': ' . $headers['content_type'][0] . "\n"; } - if ( $header === 'date' ) { + if ( 'date' === $header ) { // allow a bit of leeway for misconfigured clocks. - $d = new DateTime( $headers[$header][0] ); - $d->setTimeZone( new DateTimeZone('UTC') ); + $d = new DateTime( $headers[ $header ][0] ); + $d->setTimeZone( new DateTimeZone( 'UTC' ) ); + $c = $d->format( 'U' ); - $dplus = time() + self::MAX_TIME_OFFSET; - $dminus = time() - self::MAX_TIME_OFFSET; - $c = wp_date( 'U' ); + $dplus = time() + ( 3 * HOUR_IN_SECONDS ); + $dminus = time() - ( 3 * HOUR_IN_SECONDS ); if ( $c > $dplus || $c < $dminus ) { // time out of range @@ -286,8 +283,7 @@ class Signature { } } } - // error_log( '$signed_data: ' . print_r( rtrim( $signed_data, "\n" ), true ) ); - return rtrim($signed_data, "\n"); + return \rtrim( $signed_data, "\n" ); } public static function generate_digest( $body ) { From 9ec09c540716b7c2c3ab797f17d0d5fa4497c829 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 2 Apr 2023 00:12:02 -0600 Subject: [PATCH 08/81] remove unneeded dependencies --- activitypub.php | 2 -- composer.json | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/activitypub.php b/activitypub.php index f8633e8..7320f0c 100644 --- a/activitypub.php +++ b/activitypub.php @@ -15,8 +15,6 @@ namespace Activitypub; -require __DIR__ . '/vendor/autoload.php'; - /** * Initialize plugin */ diff --git a/composer.json b/composer.json index e9cb057..67bd2d3 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,7 @@ "type": "wordpress-plugin", "require": { "php": ">=5.6.0", - "composer/installers": "^1.0 || ^2.0", - "phpseclib/phpseclib": "~3.0" + "composer/installers": "^1.0 || ^2.0" }, "require-dev": { "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5 || ^8", From 90b45438b20202ce3f491712d2d7bf23b7b0322e Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 2 Apr 2023 00:30:17 -0600 Subject: [PATCH 09/81] cleanup --- includes/class-signature.php | 7 +------ includes/functions.php | 5 ----- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index d10309f..a25d939 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -11,11 +11,6 @@ use DateTimeZone; */ class Signature { - /** - * How much leeway to provide on the date header in seconds. - * Not everybody uses NTP. - */ - const DEFAULT_SIGNING_ALGORITHM = 'sha256'; /** @@ -252,7 +247,7 @@ class Signature { } } if ( '(created)' === $header ) { - if ( ! \empty( $signature_block['(created)'] ) && \intval( $signature_block['(created)'] ) > \time() ) { + if ( ! empty( $signature_block['(created)'] ) && \intval( $signature_block['(created)'] ) > \time() ) { // created in future return false; } diff --git a/includes/functions.php b/includes/functions.php index 9df6311..8f2e5d9 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -200,11 +200,6 @@ function get_inbox_by_actor( $actor ) { */ function get_publickey_by_actor( $actor, $key_id ) { $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 ) ) { return $metadata; From 2293c0b3d776e90a92dfa536a52f05fbff8b2068 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 2 Apr 2023 16:38:39 -0600 Subject: [PATCH 10/81] use verify_http_signature in validate_callback rename verify_signature --- includes/class-signature.php | 2 +- includes/rest/class-inbox.php | 31 +++++++++++++------------------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index a25d939..1ef447b 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -112,7 +112,7 @@ class Signature { } } - public static function verify_signature( $request = null ) { + public static function verify_http_signature( $request = null ) { $headers = $request->get_headers(); $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index c3ca058..0869533 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -74,17 +74,10 @@ class Inbox { return $served; } - $signature = $request->get_header( 'signature' ); - - if ( ! $signature ) { + if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { return $served; } - $headers = $request->get_headers(); - - // verify signature - \Activitypub\Signature::verify_signature( $request ); - return $served; } @@ -237,6 +230,12 @@ class Inbox { $params['id'] = array( 'required' => true, 'sanitize_callback' => 'esc_url_raw', + 'validate_callback' => function( $param, $request, $key ) { + if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { + return false; + } + return $param; + }, ); $params['actor'] = array( @@ -281,6 +280,12 @@ class Inbox { 'required' => true, 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', + 'validate_callback' => function( $param, $request, $key ) { + if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { + return false; + } + return $param; + }, ); $params['actor'] = array( @@ -339,16 +344,6 @@ class Inbox { }, ); - $params['validated'] = array( - 'sanitize_callback' => function( $param, $request, $key ) { - if ( \is_string( $param ) ) { - $param = array( $param ); - } - - return $param; - }, - ); - return $params; } From d6169f4bc3f111143d73773eb0ee4c3446d45da3 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 2 Apr 2023 20:38:10 -0600 Subject: [PATCH 11/81] Add content-length header if present in sig headers --- includes/class-signature.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/includes/class-signature.php b/includes/class-signature.php index 1ef447b..96845d4 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -263,6 +263,9 @@ class Signature { if ( 'content-type' === $header ) { $signed_data .= $header . ': ' . $headers['content_type'][0] . "\n"; } + if ( 'content-length' === $header ) { + $signed_data .= $header . ': ' . $headers['content_length'][0] . "\n"; + } if ( 'date' === $header ) { // allow a bit of leeway for misconfigured clocks. $d = new DateTime( $headers[ $header ][0] ); From 502bf8b5a6ef55d9a0bdb50bb85f75230bac567a Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Tue, 4 Apr 2023 19:58:08 -0600 Subject: [PATCH 12/81] Get actor from key with non-standard uri --- includes/class-signature.php | 2 +- includes/functions.php | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 96845d4..17a9a6f 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -225,7 +225,7 @@ class Signature { public static function get_key( $keyId ) { // phpcs:ignore // If there was no key passed to verify, it will find the keyId and call this // function to fetch the public key from stored data or a network fetch. - $actor = \strip_fragment_from_url( $keyId ); // phpcs:ignore + $actor = \Activitypub\get_actor_from_key( $keyId ); // phpcs:ignore $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); // phpcs:ignore return \rtrim( $publicKeyPem ); // phpcs:ignore } diff --git a/includes/functions.php b/includes/functions.php index 8f2e5d9..f731190 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -219,6 +219,15 @@ function get_publickey_by_actor( $actor, $key_id ) { return new \WP_Error( 'activitypub_no_public_key', \__( 'No "Public-Key" found', 'activitypub' ), $metadata ); } +function get_actor_from_key( $key_id ) { + $actor = \strip_fragment_from_url( $key_id ); + if ( $actor === $key_id ) { + // strip /main-key/ for GoToSocial. + $actor = \dirname( $key_id, 1 ); + } + return $actor; +} + function get_follower_inboxes( $user_id ) { $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); $inboxes = array(); From 9eb903ac15c9e813d3ccb6e4adf159c1bc8929f8 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Tue, 4 Apr 2023 20:33:00 -0600 Subject: [PATCH 13/81] phpcs compat --- includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/functions.php b/includes/functions.php index f731190..818f9f3 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -223,7 +223,7 @@ function get_actor_from_key( $key_id ) { $actor = \strip_fragment_from_url( $key_id ); if ( $actor === $key_id ) { // strip /main-key/ for GoToSocial. - $actor = \dirname( $key_id, 1 ); + $actor = \dirname( $key_id ); } return $actor; } From 3a0f62b092208ad87c15bc850b2de66335803e4f Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Tue, 4 Apr 2023 20:36:25 -0600 Subject: [PATCH 14/81] phpcs --- includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/functions.php b/includes/functions.php index 818f9f3..751e493 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -221,7 +221,7 @@ function get_publickey_by_actor( $actor, $key_id ) { function get_actor_from_key( $key_id ) { $actor = \strip_fragment_from_url( $key_id ); - if ( $actor === $key_id ) { + if ( $actor === $key_id ) { // strip /main-key/ for GoToSocial. $actor = \dirname( $key_id ); } From e1722cd4d393b2e60bf7abf8868345bc7a9d0f74 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Wed, 5 Apr 2023 13:25:39 -0600 Subject: [PATCH 15/81] Simplify signature_algorithm --- includes/class-signature.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 17a9a6f..3c28941 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -11,8 +11,6 @@ use DateTimeZone; */ class Signature { - const DEFAULT_SIGNING_ALGORITHM = 'sha256'; - /** * @param int $user_id * @@ -181,12 +179,10 @@ class Signature { public static function get_signature_algorithm( $signature_block ) { switch ( $signature_block['algorithm'] ) { - case 'rsa-sha256': - return 'sha256'; case 'rsa-sha-512': return 'sha512'; - case 'hs2019': - return self::DEFAULT_SIGNING_ALGORITHM; + default: + return 'sha256'; } return false; } @@ -223,8 +219,6 @@ class Signature { } public static function get_key( $keyId ) { // phpcs:ignore - // If there was no key passed to verify, it will find the keyId and call this - // function to fetch the public key from stored data or a network fetch. $actor = \Activitypub\get_actor_from_key( $keyId ); // phpcs:ignore $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); // phpcs:ignore return \rtrim( $publicKeyPem ); // phpcs:ignore From 30d78417d87c5e4765e7c89cac7ffb3ee4c4cccf Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 14 Apr 2023 23:53:43 -0600 Subject: [PATCH 16/81] Fixes key retrieval --- includes/class-signature.php | 15 ++++++--------- includes/rest/class-inbox.php | 10 +++++----- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 3c28941..84cf612 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -112,6 +112,7 @@ class Signature { public static function verify_http_signature( $request = null ) { $headers = $request->get_headers(); + $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : '' ; $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); if ( ! $headers ) { @@ -123,7 +124,7 @@ class Signature { $signature_block = self::parse_signature_header( $headers['authorization'] ); } - if ( ! $signature_block ) { + if ( ! isset( $signature_block ) || ! $signature_block ) { return false; } @@ -143,6 +144,9 @@ class Signature { } if ( \in_array( 'digest', $signed_headers, true ) && isset( $body ) ) { + if ( is_array( $headers['digest'] ) ) { + $headers['digest'] = $headers['digest'][0]; + } $digest = explode( '=', $headers['digest'], 2 ); if ( 'SHA-256' === $digest[0] ) { $hashalg = 'sha256'; @@ -156,7 +160,7 @@ class Signature { } } - $public_key = isset( $key ) ? $key : self::get_key( $signature_block['keyId'] ); + $public_key = \rtrim( \Activitypub\get_publickey_by_actor( $actor, $signature_block['keyId'] ) ); // phpcs:ignore return \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; @@ -218,13 +222,6 @@ class Signature { return $ret; } - public static function get_key( $keyId ) { // phpcs:ignore - $actor = \Activitypub\get_actor_from_key( $keyId ); // phpcs:ignore - $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); // phpcs:ignore - return \rtrim( $publicKeyPem ); // phpcs:ignore - } - - public static function get_signed_data( $signed_headers, $signature_block, $headers ) { $signed_data = ''; // This also verifies time-based values by returning false if any of these are out of range. diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 0869533..a93d064 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -74,10 +74,6 @@ class Inbox { return $served; } - if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { - return $served; - } - return $served; } @@ -230,9 +226,13 @@ class Inbox { $params['id'] = array( 'required' => true, 'sanitize_callback' => 'esc_url_raw', + ); + + $params['signature'] = array( + 'required' => true, 'validate_callback' => function( $param, $request, $key ) { if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { - return false; + return false; // returns http 400 rest_invalid_param } return $param; }, From 590c990e218b1bc60a77f8397bd7016c580bbf39 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 14 Apr 2023 23:59:04 -0600 Subject: [PATCH 17/81] phpcs --- includes/class-signature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 84cf612..c6036bd 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -112,7 +112,7 @@ class Signature { public static function verify_http_signature( $request = null ) { $headers = $request->get_headers(); - $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : '' ; + $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); if ( ! $headers ) { From 5faddba5114af24abf4764f0e82fbeab63d0cf3a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 08:51:25 +0200 Subject: [PATCH 18/81] this function should not work without $request --- includes/class-signature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index c6036bd..f0ca349 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -110,7 +110,7 @@ class Signature { } } - public static function verify_http_signature( $request = null ) { + public static function verify_http_signature( $request ) { $headers = $request->get_headers(); $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); From a8b963ec26102fc8ac5bbfbd479d5de470dd5956 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 08:51:38 +0200 Subject: [PATCH 19/81] some code cleanups --- includes/rest/class-inbox.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index c811605..4327587 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -1,7 +1,10 @@ header( 'Content-Type', 'application/activity+json' ); @@ -140,7 +143,7 @@ class Inbox { \do_action( 'activitypub_inbox', $data, $user_id, $type ); \do_action( "activitypub_inbox_{$type}", $data, $user_id ); - return new \WP_REST_Response( array(), 202 ); + return new WP_REST_Response( array(), 202 ); } /** @@ -179,7 +182,7 @@ class Inbox { \do_action( "activitypub_inbox_{$type}", $data, $user->ID ); } - return new \WP_REST_Response( array(), 202 ); + return new WP_REST_Response( array(), 202 ); } /** @@ -233,7 +236,7 @@ class Inbox { $params['signature'] = array( 'required' => true, 'validate_callback' => function( $param, $request, $key ) { - if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { + if ( ! Signature::verify_http_signature( $request ) ) { return false; // returns http 400 rest_invalid_param } return $param; @@ -283,7 +286,7 @@ class Inbox { 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', 'validate_callback' => function( $param, $request, $key ) { - if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { + if ( ! Signature::verify_http_signature( $request ) ) { return false; } return $param; @@ -357,7 +360,7 @@ class Inbox { */ public static function handle_follow( $object, $user_id ) { // save follower - \Activitypub\Peer\Followers::add_follower( $object['actor'], $user_id ); + Followers::add_follower( $object['actor'], $user_id ); // get inbox $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); @@ -382,7 +385,7 @@ class Inbox { */ public static function handle_unfollow( $object, $user_id ) { if ( isset( $object['object'] ) && isset( $object['object']['type'] ) && 'Follow' === $object['object']['type'] ) { - \Activitypub\Peer\Followers::remove_follower( $object['actor'], $user_id ); + Followers::remove_follower( $object['actor'], $user_id ); } } From 038bf25b2e186b36e2f4292ec6ec8f0508fb87b4 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sat, 15 Apr 2023 23:57:08 -0600 Subject: [PATCH 20/81] remove guessing function --- includes/functions.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 95ed09e..e00f9e0 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -219,16 +219,7 @@ function get_publickey_by_actor( $actor, $key_id ) { return new \WP_Error( 'activitypub_no_public_key', \__( 'No "Public-Key" found', 'activitypub' ), $metadata ); } -function get_actor_from_key( $key_id ) { - $actor = \strip_fragment_from_url( $key_id ); - if ( $actor === $key_id ) { - // strip /main-key/ for GoToSocial. - $actor = \dirname( $key_id ); - } - return $actor; -} - -function get_follower_inboxes( $user_id, $cc = array() ) { +function get_follower_inboxes( $user_id ) { $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); $followers = array_merge( $followers, $cc ); $followers = array_unique( $followers ); From bb21803b18464ea8f5863667409d45a3359e8e80 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 16 Apr 2023 13:46:44 -0600 Subject: [PATCH 21/81] Add Secure mode setting --- includes/class-admin.php | 9 +++++++++ templates/settings.php | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/includes/class-admin.php b/includes/class-admin.php index 220ed9b..dfe7deb 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -142,6 +142,15 @@ class Admin { 'default' => array( 'post', 'pages' ), ) ); + \register_setting( + 'activitypub', + 'activitypub_use_secure_mode', + array( + 'type' => 'boolean', + 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), + 'default' => 0, + ) + ); } public static function add_settings_help_tab() { diff --git a/templates/settings.php b/templates/settings.php index 2bfaed4..6429688 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -171,6 +171,21 @@ + + + + + + + +
+ + +

+ +

+
+ From 036ee3180b54c9f63c531811ebef86f7d99a9a78 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 21 Apr 2023 07:46:14 -0600 Subject: [PATCH 22/81] move signature verification to callback --- includes/rest/class-inbox.php | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 4327587..8a1f139 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -134,6 +134,11 @@ class Inbox { * @return WP_REST_Response */ public static function user_inbox_post( $request ) { + // SecureMode/Authorized fetch. + if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { + return new \WP_REST_Response( array(), 403 ); + } + $user_id = $request->get_param( 'user_id' ); $data = $request->get_params(); @@ -154,6 +159,11 @@ class Inbox { * @return WP_REST_Response */ public static function shared_inbox_post( $request ) { + // SecureMode/Authorized fetch. + if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { + return new \WP_REST_Response( array(), 403 ); + } + $data = $request->get_params(); $type = $request->get_param( 'type' ); $users = self::extract_recipients( $data ); @@ -233,16 +243,6 @@ class Inbox { 'sanitize_callback' => 'esc_url_raw', ); - $params['signature'] = array( - 'required' => true, - 'validate_callback' => function( $param, $request, $key ) { - if ( ! Signature::verify_http_signature( $request ) ) { - return false; // returns http 400 rest_invalid_param - } - return $param; - }, - ); - $params['actor'] = array( 'required' => true, 'sanitize_callback' => function( $param, $request, $key ) { @@ -285,12 +285,6 @@ class Inbox { 'required' => true, 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', - 'validate_callback' => function( $param, $request, $key ) { - if ( ! Signature::verify_http_signature( $request ) ) { - return false; - } - return $param; - }, ); $params['actor'] = array( From d23ff46073fecf307bba8c00ded5407048bc3674 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 21 Apr 2023 08:45:10 -0600 Subject: [PATCH 23/81] fix merge omission --- includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/functions.php b/includes/functions.php index e00f9e0..77508c5 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -219,7 +219,7 @@ function get_publickey_by_actor( $actor, $key_id ) { return new \WP_Error( 'activitypub_no_public_key', \__( 'No "Public-Key" found', 'activitypub' ), $metadata ); } -function get_follower_inboxes( $user_id ) { +function get_follower_inboxes( $user_id, $cc = array() ) { $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); $followers = array_merge( $followers, $cc ); $followers = array_unique( $followers ); From 1631f1c7dcc5cced91c4a3a87f53f28405ea9ae5 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 21 Apr 2023 09:18:24 -0600 Subject: [PATCH 24/81] fix rest api endpoint --- includes/class-signature.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index f0ca349..0b2184d 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -112,12 +112,14 @@ class Signature { public static function verify_http_signature( $request ) { $headers = $request->get_headers(); - $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; - $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); if ( ! $headers ) { - $headers = self::default_server_headers(); + return false; } + + $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; + $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /' . rest_get_url_prefix() . $request->get_route(); + if ( array_key_exists( 'signature', $headers ) ) { $signature_block = self::parse_signature_header( $headers['signature'] ); } elseif ( array_key_exists( 'authorization', $headers ) ) { From 7dbce74a96d2b149adbc53daf58c33ec7492b1e3 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 21 Apr 2023 09:36:17 -0600 Subject: [PATCH 25/81] ensure signature block has algorithm --- includes/class-signature.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 0b2184d..9261d99 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -184,11 +184,13 @@ class Signature { } public static function get_signature_algorithm( $signature_block ) { - switch ( $signature_block['algorithm'] ) { - case 'rsa-sha-512': - return 'sha512'; - default: - return 'sha256'; + if ( $signature_block['algorithm'] ) { + switch ( $signature_block['algorithm'] ) { + case 'rsa-sha-512': + return 'sha512'; //hs2019 https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12 + default: + return 'sha256'; + } } return false; } From f396c6da4ed5dc3eaea51201a3b0ee564a7f7aeb Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 21 Apr 2023 15:25:39 -0600 Subject: [PATCH 26/81] Optimize verification code and returns WP_Errors --- activitypub.php | 3 ++ includes/class-signature.php | 20 ++++++++----- includes/rest/class-inbox.php | 8 ----- includes/rest/class-server.php | 55 ++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 includes/rest/class-server.php diff --git a/activitypub.php b/activitypub.php index 35e6de8..c5275ba 100644 --- a/activitypub.php +++ b/activitypub.php @@ -57,6 +57,9 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/rest/class-following.php'; Rest\Following::init(); + require_once \dirname( __FILE__ ) . '/includes/rest/class-server.php'; + \Activitypub\Rest\Server::init(); + require_once \dirname( __FILE__ ) . '/includes/rest/class-webfinger.php'; Rest\Webfinger::init(); diff --git a/includes/class-signature.php b/includes/class-signature.php index 9261d99..68fb41e 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -114,7 +114,7 @@ class Signature { $headers = $request->get_headers(); if ( ! $headers ) { - return false; + return new \WP_Error( 'activitypub_signature', 'Request not signed', array( 'status' => 403 ) ); } $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; @@ -127,7 +127,7 @@ class Signature { } if ( ! isset( $signature_block ) || ! $signature_block ) { - return false; + return new \WP_Error( 'activitypub_signature', 'Incompatible request signature. keyId and signature are required', array( 'status' => 403 ) ); } $signed_headers = $signature_block['headers']; @@ -137,12 +137,12 @@ class Signature { $signed_data = self::get_signed_data( $signed_headers, $signature_block, $headers ); if ( ! $signed_data ) { - return false; + return new \WP_Error( 'activitypub_signature', 'Signed request date outside acceptable time window', array( 'status' => 403 ) ); } $algorithm = self::get_signature_algorithm( $signature_block ); if ( ! $algorithm ) { - return false; + return new \WP_Error( 'activitypub_signature', 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)', array( 'status' => 403 ) ); } if ( \in_array( 'digest', $signed_headers, true ) && isset( $body ) ) { @@ -158,13 +158,17 @@ class Signature { } if ( \base64_encode( \hash( $hashalg, $body, true ) ) !== $digest[1] ) { // phpcs:ignore - return false; + return new \WP_Error( 'activitypub_signature', 'Invalid Digest header', array( 'status' => 403 ) ); } } - $public_key = \rtrim( \Activitypub\get_publickey_by_actor( $actor, $signature_block['keyId'] ) ); // phpcs:ignore - - return \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; + $public_key = \Activitypub\get_publickey_by_actor( $actor, $signature_block['keyId'] ); // phpcs:ignore + if ( \is_wp_error( $public_key ) ) { + return $public_key; + } else { + $public_key = \rtrim( $public_key ); + } + return \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0 ?? new \WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); } diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 8a1f139..0257ba1 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -134,10 +134,6 @@ class Inbox { * @return WP_REST_Response */ public static function user_inbox_post( $request ) { - // SecureMode/Authorized fetch. - if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { - return new \WP_REST_Response( array(), 403 ); - } $user_id = $request->get_param( 'user_id' ); @@ -159,10 +155,6 @@ class Inbox { * @return WP_REST_Response */ public static function shared_inbox_post( $request ) { - // SecureMode/Authorized fetch. - if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { - return new \WP_REST_Response( array(), 403 ); - } $data = $request->get_params(); $type = $request->get_param( 'type' ); diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php new file mode 100644 index 0000000..790728a --- /dev/null +++ b/includes/rest/class-server.php @@ -0,0 +1,55 @@ +get_route(); + if ( str_starts_with( $maybe_activitypub, '/activitypub' ) ) { + if ( 'POST' === $request->get_method() ) { + $verified_request = Signature::verify_http_signature( $request ); + + if ( \is_wp_error( $verified_request ) ) { + return $verified_request; + } + } else { + // SecureMode/Authorized fetch. + $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); + + if ( $secure_mode ) { + if ( \is_wp_error( $verified_request ) ) { + return $verified_request; + } + } + } + } + } +} From 023ba25f38aa08447fdf9eed8a84cb721618bd9d Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 21 Apr 2023 15:27:02 -0600 Subject: [PATCH 27/81] PHPDoc --- includes/class-signature.php | 45 ++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 68fb41e..a889967 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -110,6 +110,13 @@ class Signature { } } + /** + * Verifies the http signatures + * + * @param WP_REQUEST | Array $request + * @return void + * @author Django Doucet + */ public static function verify_http_signature( $request ) { $headers = $request->get_headers(); @@ -172,21 +179,13 @@ class Signature { } - public static function default_server_headers() { - $headers = array( - '(request-target)' => strtolower( $_SERVER['REQUEST_METHOD'] ) . ' ' . $_SERVER['REQUEST_URI'], - 'content-type' => $_SERVER['CONTENT_TYPE'], - 'content-length' => $_SERVER['CONTENT_LENGTH'], - ); - foreach ( $_SERVER as $k => $v ) { - if ( \strpos( $k, 'HTTP_' ) === 0 ) { - $field = \str_replace( '_', '-', \strtolower( \substr( $k, 5 ) ) ); - $headers[ $field ] = $v; - } - } - return $headers; - } - + /** + * Gets the signature algorithm from the signature header + * + * @param array $signature_block + * @return string algorithm + * @author Django Doucet + */ public static function get_signature_algorithm( $signature_block ) { if ( $signature_block['algorithm'] ) { switch ( $signature_block['algorithm'] ) { @@ -199,6 +198,13 @@ class Signature { return false; } + /** + * Parses the Signature header + * + * @param array $header + * @return array signature parts + * @author Django Doucet + */ public static function parse_signature_header( $header ) { $ret = array(); $matches = array(); @@ -230,6 +236,15 @@ class Signature { return $ret; } + /** + * Gets the header data from the included pseudo headers + * + * @param array $signed_headers + * @param array $signature_block (pseudo-headers) + * @param array $headers (original http headers) + * @return signed headers for comparison + * @author Django Doucet + */ public static function get_signed_data( $signed_headers, $signature_block, $headers ) { $signed_data = ''; // This also verifies time-based values by returning false if any of these are out of range. From b641cb03f36eed34c86f3bdde3569fdb9378224b Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 21 Apr 2023 16:16:52 -0600 Subject: [PATCH 28/81] fix phpcs --- includes/class-signature.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index a889967..608bff3 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -175,7 +175,10 @@ class Signature { } else { $public_key = \rtrim( $public_key ); } - return \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0 ?? new \WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); + $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; + if ( ! $verified ) { + return new \WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); // phpcs:ignore null coalescing operator + } } From 857fae9db16392b1ff00959328bc555b7a648756 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 2 May 2023 09:50:08 +0200 Subject: [PATCH 29/81] serve_request is not needed any more this was only for handling the signing, so no more need for that --- includes/rest/class-inbox.php | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 4327587..29d0d16 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -19,7 +19,6 @@ class Inbox { */ public static function init() { \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); - \add_filter( 'rest_pre_serve_request', array( self::class, 'serve_request' ), 11, 4 ); \add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow' ), 10, 2 ); \add_action( 'activitypub_inbox_undo', array( self::class, 'handle_unfollow' ), 10, 2 ); //\add_action( 'activitypub_inbox_like', array( self::class, 'handle_reaction' ), 10, 2 ); @@ -64,24 +63,6 @@ class Inbox { ); } - /** - * Hooks into the REST API request to verify the signature. - * - * @param bool $served Whether the request has already been served. - * @param WP_HTTP_ResponseInterface $result Result to send to the client. Usually a WP_REST_Response. - * @param WP_REST_Request $request Request used to generate the response. - * @param WP_REST_Server $server Server instance. - * - * @return true - */ - public static function serve_request( $served, $result, $request, $server ) { - if ( '/activitypub' !== \substr( $request->get_route(), 0, 12 ) ) { - return $served; - } - - return $served; - } - /** * Renders the user-inbox * From 8aa3f53dbd6fb41dfee952fdd74263d911e6e7d4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 5 May 2023 10:22:01 +0200 Subject: [PATCH 30/81] no need to use Followers any more --- includes/rest/class-inbox.php | 1 - 1 file changed, 1 deletion(-) diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index ba1fc0a..217e816 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -4,7 +4,6 @@ namespace Activitypub\Rest; use WP_REST_Response; use Activitypub\Signature; use Activitypub\Model\Activity; -use \Activitypub\Peer\Followers; /** * ActivityPub Inbox REST-Class From 6b68f0763d6cd6966ba36c55d97697a13d912bfe Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Wed, 26 Apr 2023 15:54:09 -0600 Subject: [PATCH 31/81] hold off secure mode --- includes/class-admin.php | 18 +++++++++--------- includes/rest/class-server.php | 14 +++++++------- templates/settings.php | 6 +++--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 7a19e8e..4bc7b9c 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -144,15 +144,15 @@ class Admin { 'default' => array( 'post', 'pages' ), ) ); - \register_setting( - 'activitypub', - 'activitypub_use_secure_mode', - array( - 'type' => 'boolean', - 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), - 'default' => 0, - ) - ); + // \register_setting( + // 'activitypub', + // 'activitypub_use_secure_mode', + // array( + // 'type' => 'boolean', + // 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), + // 'default' => 0, + // ) + // ); } public static function schedule_migration() { diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 790728a..bf1cd5e 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -42,13 +42,13 @@ class Server { } } else { // SecureMode/Authorized fetch. - $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); - - if ( $secure_mode ) { - if ( \is_wp_error( $verified_request ) ) { - return $verified_request; - } - } + // $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); + // if ( $secure_mode ) { + // $verified_request = Signature::verify_http_signature( $request ); + // if ( \is_wp_error( $verified_request ) ) { + // return $verified_request; + // } + // } } } } diff --git a/templates/settings.php b/templates/settings.php index 6429688..28d7f32 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -171,7 +171,7 @@ - + From 27636b62d50c81d05105e343030daa01a0b18aa9 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 12:02:12 -0600 Subject: [PATCH 32/81] Add Service actor for signing get requests --- includes/class-http.php | 4 +-- includes/class-signature.php | 39 +++++++++++++++++--------- includes/functions.php | 16 ++--------- includes/rest/class-server.php | 50 +++++++++++++++++++++++++++++++++- 4 files changed, 80 insertions(+), 29 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index 798328b..e6a9a79 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -60,9 +60,9 @@ class Http { * * @return array|WP_Error The GET Response or an WP_ERROR */ - public static function get( $url, $user_id ) { + public static function get( $url ) { $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = Signature::generate_signature( $user_id, 'get', $url, $date ); + $signature = Signature::generate_signature( -1, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); diff --git a/includes/class-signature.php b/includes/class-signature.php index 608bff3..dcf3d96 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -35,14 +35,15 @@ class Signature { * @return mixed */ public static function get_private_key( $user_id, $force = false ) { - $key = \get_user_meta( $user_id, 'magic_sig_private_key' ); - - if ( $key && ! $force ) { - return $key[0]; + if ( $force ) { + self::generate_key_pair( $user_id ); } - self::generate_key_pair( $user_id ); - $key = \get_user_meta( $user_id, 'magic_sig_private_key' ); + if ( -1 === $user_id ) { + $key = \get_option('activitypub_magic_sig_private_key' ); + } else { + $key = \get_user_meta( $user_id, 'magic_sig_private_key' ); + } return $key[0]; } @@ -63,14 +64,22 @@ class Signature { $priv_key = null; \openssl_pkey_export( $key, $priv_key ); - - // private key - \update_user_meta( $user_id, 'magic_sig_private_key', $priv_key ); - $detail = \openssl_pkey_get_details( $key ); - // public key - \update_user_meta( $user_id, 'magic_sig_public_key', $detail['key'] ); + if ( -1 === $user_id ) { + // private key + \add_option('activitypub_magic_sig_private_key', $priv_key ); + + // public key + \add_option('activitypub_magic_sig_public_key', $detail['key'] ); + + } else { + // private key + \update_user_meta( $user_id, 'magic_sig_private_key', $priv_key ); + + // public key + \update_user_meta( $user_id, 'magic_sig_public_key', $detail['key'] ); + } } public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { @@ -101,7 +110,11 @@ class Signature { \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore - $key_id = \get_author_posts_url( $user_id ) . '#main-key'; + if ( -1 === $user_id ) { + $key_id = \get_rest_url( null, 'activitypub/1.0/service#main-key' ); + } else { + $key_id = \get_author_posts_url( $user_id ) . '#main-key'; + } if ( ! empty( $digest ) ) { return \sprintf( 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"', $key_id, $signature ); diff --git a/includes/functions.php b/includes/functions.php index bd04e6c..9743b44 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -36,8 +36,8 @@ function safe_remote_post( $url, $body, $user_id ) { return \Activitypub\Http::post( $url, $body, $user_id ); } -function safe_remote_get( $url, $user_id ) { - return \Activitypub\Http::get( $url, $user_id ); +function safe_remote_get( $url ) { + return \Activitypub\Http::get( $url ); } /** @@ -88,21 +88,11 @@ function get_remote_metadata_by_actor( $actor ) { return $metadata; } - $user = \get_users( - array( - 'number' => 1, - 'capability__in' => array( 'publish_posts' ), - 'fields' => 'ID', - ) - ); - - // we just need any user to generate a request signature - $user_id = \reset( $user ); $short_timeout = function() { return 3; }; add_filter( 'activitypub_remote_get_timeout', $short_timeout ); - $response = Http::get( $actor, $user_id ); + $response = Http::get( $actor ); remove_filter( 'activitypub_remote_get_timeout', $short_timeout ); if ( \is_wp_error( $response ) ) { \set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period. diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index bf1cd5e..5934bcf 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -1,6 +1,7 @@ \WP_REST_Server::READABLE, + 'callback' => array( self::class, 'service_actor' ), + 'permission_callback' => '__return_true', + ), + ) + ); + } + + /** + * Render Service actor profile + * + * @return WP_REST_Response + */ + public static function service_actor() { + $json = new \stdClass(); + + $json->{'@context'} = \Activitypub\get_context(); + $json->id = \get_rest_url( null, 'activitypub/1.0/service' ); + $json->type = 'Application'; + $json->preferredUsername = parse_url( get_site_url(), PHP_URL_HOST ); + $json->name = get_bloginfo( 'name' ); + $json->summary = "ActivityPub service actor"; + $json->manuallyApprovesFollowers = TRUE; + $json->icon = [ get_site_icon_url() ]; + $json->publicKey = (object) array( + 'id' => \get_rest_url( null, 'activitypub/1.0/service#main-key' ), + 'owner' => \get_rest_url( null, 'activitypub/1.0/service' ), + 'publicKeyPem' => Signature::get_public_key( -1 ), + ); + + $response = new WP_REST_Response( $json, 200 ); + + $response->header( 'Content-Type', 'application/activity+json' ); + + return $response; } /** From e827221ee6b13ea4839fe920843b1fce12ea0e9b Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 12:09:12 -0600 Subject: [PATCH 33/81] service actor as application actor --- includes/rest/class-server.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 5934bcf..6ee28d5 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -27,11 +27,11 @@ class Server { public static function register_routes() { \register_rest_route( 'activitypub/1.0', - '/service', + '/application', array( array( 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( self::class, 'service_actor' ), + 'callback' => array( self::class, 'application_actor' ), 'permission_callback' => '__return_true', ), ) @@ -39,24 +39,24 @@ class Server { } /** - * Render Service actor profile + * Render Application actor profile * * @return WP_REST_Response */ - public static function service_actor() { + public static function application_actor() { $json = new \stdClass(); $json->{'@context'} = \Activitypub\get_context(); - $json->id = \get_rest_url( null, 'activitypub/1.0/service' ); + $json->id = \get_rest_url( null, 'activitypub/1.0/application' ); $json->type = 'Application'; $json->preferredUsername = parse_url( get_site_url(), PHP_URL_HOST ); $json->name = get_bloginfo( 'name' ); - $json->summary = "ActivityPub service actor"; + $json->summary = "WordPress-ActivityPub application actor"; $json->manuallyApprovesFollowers = TRUE; $json->icon = [ get_site_icon_url() ]; $json->publicKey = (object) array( - 'id' => \get_rest_url( null, 'activitypub/1.0/service#main-key' ), - 'owner' => \get_rest_url( null, 'activitypub/1.0/service' ), + 'id' => \get_rest_url( null, 'activitypub/1.0/application#main-key' ), + 'owner' => \get_rest_url( null, 'activitypub/1.0/application' ), 'publicKeyPem' => Signature::get_public_key( -1 ), ); From 35496f5026d06ebbc34cd7ad971eb6b62101920d Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 12:52:24 -0600 Subject: [PATCH 34/81] get_public_key support application actor --- includes/class-signature.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index dcf3d96..f735fc3 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -17,14 +17,15 @@ class Signature { * @return mixed */ public static function get_public_key( $user_id, $force = false ) { - $key = \get_user_meta( $user_id, 'magic_sig_public_key' ); - - if ( $key && ! $force ) { - return $key[0]; + if ( $force ) { + self::generate_key_pair( $user_id ); } - self::generate_key_pair( $user_id ); - $key = \get_user_meta( $user_id, 'magic_sig_public_key' ); + if ( -1 === $user_id ) { + $key = array( \get_option('activitypub_magic_sig_public_key' ) ); + } else { + $key = \get_user_meta( $user_id, 'magic_sig_public_key' ); + } return $key[0]; } From c5ca061805257d558ac2e2cf00be4586b61e4de7 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 12:53:43 -0600 Subject: [PATCH 35/81] Add helper format_server_request --- includes/class-signature.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/includes/class-signature.php b/includes/class-signature.php index f735fc3..ae4ff61 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -319,4 +319,29 @@ class Signature { $digest = \base64_encode( \hash( 'sha256', $body, true ) ); // phpcs:ignore return "$digest"; } + + /** + * Formats the $_SERVER to resemble the WP_REST_REQUEST array, + * for use with verify_http_signature() + * + * @param array $_SERVER + * @return array $request + */ + public static function format_server_request( $server ) { + $request = array(); + foreach ( $server as $param_key => $param_val ) { + $req_param = strtolower( $param_key ); + if ( 'REQUEST_URI' === $req_param ) { + $request['headers']['route'][] = $param_val; + } else { + $header_key = str_replace( + 'http_', + '', + $req_param + ); + $request['headers'][ $header_key ][] = \wp_unslash( $param_val ); + } + } + return $request; + } } From 9d30f2c1dd899a1bade19f030381b39ff804561c Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 12:55:12 -0600 Subject: [PATCH 36/81] phpcbf --- includes/rest/class-server.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 6ee28d5..03e72cc 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -49,15 +49,15 @@ class Server { $json->{'@context'} = \Activitypub\get_context(); $json->id = \get_rest_url( null, 'activitypub/1.0/application' ); $json->type = 'Application'; - $json->preferredUsername = parse_url( get_site_url(), PHP_URL_HOST ); + $json->preferredUsername = parse_url( get_site_url(), PHP_URL_HOST ); // phpcs:ignore snake_case $json->name = get_bloginfo( 'name' ); - $json->summary = "WordPress-ActivityPub application actor"; - $json->manuallyApprovesFollowers = TRUE; - $json->icon = [ get_site_icon_url() ]; - $json->publicKey = (object) array( + $json->summary = 'WordPress-ActivityPub application actor'; + $json->manuallyApprovesFollowers = true; // phpcs:ignore snake_case + $json->icon = [ get_site_icon_url() ]; // phpcs:ignore short array syntax + $json->publicKey = (object) array( // phpcs:ignore snake_case 'id' => \get_rest_url( null, 'activitypub/1.0/application#main-key' ), 'owner' => \get_rest_url( null, 'activitypub/1.0/application' ), - 'publicKeyPem' => Signature::get_public_key( -1 ), + 'publicKeyPem' => Signature::get_public_key( -1 ), // phpcs:ignore snake_case ); $response = new WP_REST_Response( $json, 200 ); From 14f3c3985b843bba51e6bbd29a18a5468cb2af97 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 13:00:21 -0600 Subject: [PATCH 37/81] code style --- includes/class-signature.php | 45 +++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index ae4ff61..59d20c7 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -22,7 +22,7 @@ class Signature { } if ( -1 === $user_id ) { - $key = array( \get_option('activitypub_magic_sig_public_key' ) ); + $key = array( \get_option( 'activitypub_magic_sig_public_key' ) ); } else { $key = \get_user_meta( $user_id, 'magic_sig_public_key' ); } @@ -41,7 +41,7 @@ class Signature { } if ( -1 === $user_id ) { - $key = \get_option('activitypub_magic_sig_private_key' ); + $key = \get_option( 'activitypub_magic_sig_private_key' ); } else { $key = \get_user_meta( $user_id, 'magic_sig_private_key' ); } @@ -69,10 +69,10 @@ class Signature { if ( -1 === $user_id ) { // private key - \add_option('activitypub_magic_sig_private_key', $priv_key ); + \add_option( 'activitypub_magic_sig_private_key', $priv_key ); // public key - \add_option('activitypub_magic_sig_public_key', $detail['key'] ); + \add_option( 'activitypub_magic_sig_public_key', $detail['key'] ); } else { // private key @@ -127,19 +127,31 @@ class Signature { /** * Verifies the http signatures * - * @param WP_REQUEST | Array $request + * @param WP_REQUEST | Array $_SERVER * @return void * @author Django Doucet */ public static function verify_http_signature( $request ) { - $headers = $request->get_headers(); - - if ( ! $headers ) { - return new \WP_Error( 'activitypub_signature', 'Request not signed', array( 'status' => 403 ) ); + if ( is_object( $request ) ) { // REST Request object + $headers = $request->get_headers(); + error_log( 'verify: $request: ' . print_r( $request, true ) ); + $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; + $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); + error_log( 'request $headers: ' . print_r( $headers['(request-target)'], true ) ); + } else { + $request = self::format_server_request( $request ); + $headers = $request['headers']; // $_SERVER array + // error_log( print_r( $headers, true ) ); + $headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0]; + // $post = get_page_by_path( $headers['request_uri'], ARRAY_A ); + // $actor = post['post_author'] ?? ''; + $actor = ''; + error_log( 'request $headers: ' . print_r( $headers, true ) ); } - $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; - $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /' . rest_get_url_prefix() . $request->get_route(); + if ( ! isset( $headers['signature'] ) ) { + return new \WP_Error( 'activitypub_signature', 'Request not signed', array( 'status' => 403 ) ); + } if ( array_key_exists( 'signature', $headers ) ) { $signature_block = self::parse_signature_header( $headers['signature'] ); @@ -183,7 +195,8 @@ class Signature { } } - $public_key = \Activitypub\get_publickey_by_actor( $actor, $signature_block['keyId'] ); // phpcs:ignore + strtok( $signature_block['keyId'], '?'); + $public_key = \Activitypub\get_remote_metadata_by_actor( $signature_block['keyId'] ); // phpcs:ignore if ( \is_wp_error( $public_key ) ) { return $public_key; } else { @@ -191,9 +204,9 @@ class Signature { } $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; if ( ! $verified ) { - return new \WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); // phpcs:ignore null coalescing operator + return new \WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); } - + return $verified; } /** @@ -220,7 +233,7 @@ class Signature { * * @param array $header * @return array signature parts - * @author Django Doucet + * @author Django Doucet */ public static function parse_signature_header( $header ) { $ret = array(); @@ -258,7 +271,7 @@ class Signature { * * @param array $signed_headers * @param array $signature_block (pseudo-headers) - * @param array $headers (original http headers) + * @param array $headers (http headers) * @return signed headers for comparison * @author Django Doucet */ From 656a2b0f44009ef5d998c8b2ee89bfe5fc33e6da Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 13:22:47 -0600 Subject: [PATCH 38/81] remove unneeded filter --- includes/rest/class-inbox.php | 1 - 1 file changed, 1 deletion(-) diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 217e816..64e4a0c 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -18,7 +18,6 @@ class Inbox { */ public static function init() { \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); - \add_filter( 'rest_pre_serve_request', array( self::class, 'serve_request' ), 11, 4 ); \add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 2 ); } From 0b4bada2b6d46bdf5a901a3bab6986a70c5f2e91 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 13:24:59 -0600 Subject: [PATCH 39/81] enable secure mode --- includes/class-admin.php | 18 +++++++++--------- templates/settings.php | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 4bc7b9c..7a19e8e 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -144,15 +144,15 @@ class Admin { 'default' => array( 'post', 'pages' ), ) ); - // \register_setting( - // 'activitypub', - // 'activitypub_use_secure_mode', - // array( - // 'type' => 'boolean', - // 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), - // 'default' => 0, - // ) - // ); + \register_setting( + 'activitypub', + 'activitypub_use_secure_mode', + array( + 'type' => 'boolean', + 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), + 'default' => 0, + ) + ); } public static function schedule_migration() { diff --git a/templates/settings.php b/templates/settings.php index 28d7f32..6429688 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -171,7 +171,7 @@
- + From 6c95a23d105b1dbe70e475b6641391796fc631cf Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 13:45:38 -0600 Subject: [PATCH 40/81] phpcbf --- includes/rest/class-server.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 03e72cc..fb3e569 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -49,15 +49,15 @@ class Server { $json->{'@context'} = \Activitypub\get_context(); $json->id = \get_rest_url( null, 'activitypub/1.0/application' ); $json->type = 'Application'; - $json->preferredUsername = parse_url( get_site_url(), PHP_URL_HOST ); // phpcs:ignore snake_case + $json->preferredUsername = wp_parse_url( get_site_url(), PHP_URL_HOST ); // phpcs:ignore WordPress.NamingConventions $json->name = get_bloginfo( 'name' ); $json->summary = 'WordPress-ActivityPub application actor'; - $json->manuallyApprovesFollowers = true; // phpcs:ignore snake_case - $json->icon = [ get_site_icon_url() ]; // phpcs:ignore short array syntax - $json->publicKey = (object) array( // phpcs:ignore snake_case + $json->manuallyApprovesFollowers = true; // phpcs:ignore WordPress.NamingConventions + $json->icon = array( get_site_icon_url() ); // phpcs:ignore WordPress.NamingConventions short array syntax + $json->publicKey = (object) array( // phpcs:ignore WordPress.NamingConventions 'id' => \get_rest_url( null, 'activitypub/1.0/application#main-key' ), 'owner' => \get_rest_url( null, 'activitypub/1.0/application' ), - 'publicKeyPem' => Signature::get_public_key( -1 ), // phpcs:ignore snake_case + 'publicKeyPem' => Signature::get_public_key( -1 ), // phpcs:ignore WordPress.NamingConventions ); $response = new WP_REST_Response( $json, 200 ); From 9202c1973070f97c3fc028c75cddb6025ff68fe4 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 14:39:33 -0600 Subject: [PATCH 41/81] Add secure mode to REST get requests --- includes/class-signature.php | 3 +-- includes/rest/class-server.php | 14 +++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 59d20c7..47ab002 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -195,8 +195,7 @@ class Signature { } } - strtok( $signature_block['keyId'], '?'); - $public_key = \Activitypub\get_remote_metadata_by_actor( $signature_block['keyId'] ); // phpcs:ignore + $public_key = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $signature_block['keyId'] ), '?' ) ); // phpcs:ignore if ( \is_wp_error( $public_key ) ) { return $public_key; } else { diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index fb3e569..585b68b 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -90,13 +90,13 @@ class Server { } } else { // SecureMode/Authorized fetch. - // $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); - // if ( $secure_mode ) { - // $verified_request = Signature::verify_http_signature( $request ); - // if ( \is_wp_error( $verified_request ) ) { - // return $verified_request; - // } - // } + $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); + if ( $secure_mode ) { + $verified_request = Signature::verify_http_signature( $request ); + if ( \is_wp_error( $verified_request ) ) { + return $verified_request; + } + } } } } From 3d4ae84573a31f7ee863499129d6272b525085b2 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 14:40:30 -0600 Subject: [PATCH 42/81] Add secure mode to content negotiated requests --- includes/class-activitypub.php | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 16a1f22..c0695a9 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -1,6 +1,8 @@ Date: Fri, 5 May 2023 14:43:05 -0600 Subject: [PATCH 43/81] removing logging --- includes/class-signature.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 47ab002..7310e3a 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -134,19 +134,13 @@ class Signature { public static function verify_http_signature( $request ) { if ( is_object( $request ) ) { // REST Request object $headers = $request->get_headers(); - error_log( 'verify: $request: ' . print_r( $request, true ) ); $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); - error_log( 'request $headers: ' . print_r( $headers['(request-target)'], true ) ); } else { $request = self::format_server_request( $request ); $headers = $request['headers']; // $_SERVER array - // error_log( print_r( $headers, true ) ); $headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0]; - // $post = get_page_by_path( $headers['request_uri'], ARRAY_A ); - // $actor = post['post_author'] ?? ''; $actor = ''; - error_log( 'request $headers: ' . print_r( $headers, true ) ); } if ( ! isset( $headers['signature'] ) ) { From f79200ef27ac85875363ad28afac5bdc1e628227 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 23:44:15 -0600 Subject: [PATCH 44/81] make webfinger route available unsigned --- includes/rest/class-server.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 585b68b..0e8df43 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -79,22 +79,22 @@ class Server { * @return mixed|\WP_Error */ public static function authorize_activitypub_requests( $response, $handler, $request ) { - - $maybe_activitypub = $request->get_route(); - if ( str_starts_with( $maybe_activitypub, '/activitypub' ) ) { + $route = $request->get_route(); + if ( str_starts_with( $route, '/activitypub' ) ) { if ( 'POST' === $request->get_method() ) { $verified_request = Signature::verify_http_signature( $request ); - if ( \is_wp_error( $verified_request ) ) { return $verified_request; } } else { - // SecureMode/Authorized fetch. - $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); - if ( $secure_mode ) { - $verified_request = Signature::verify_http_signature( $request ); - if ( \is_wp_error( $verified_request ) ) { - return $verified_request; + if ( '/activitypub/1.0/webfinger' !== $route ) { + // SecureMode/Authorized fetch. + $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); + if ( $secure_mode ) { + $verified_request = Signature::verify_http_signature( $request ); + if ( \is_wp_error( $verified_request ) ) { + return $verified_request; + } } } } From 0d5c249eafc4f74d6c8eed59f86198316f4099a4 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 23:44:55 -0600 Subject: [PATCH 45/81] remove user_id variable from activitypub_safe_remote_get_response --- includes/class-http.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-http.php b/includes/class-http.php index e6a9a79..247d87e 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -86,7 +86,7 @@ class Http { $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); } - \do_action( 'activitypub_safe_remote_get_response', $response, $url, $user_id ); + \do_action( 'activitypub_safe_remote_get_response', $response, $url ); return $response; } From dc8e1e0f3e5fe4edf4bd65d89bd165f47a25888b Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 23:50:49 -0600 Subject: [PATCH 46/81] fix request-target route, remove $actor from verify_http_signature --- includes/class-signature.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 7310e3a..2a4ea53 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -134,13 +134,11 @@ class Signature { public static function verify_http_signature( $request ) { if ( is_object( $request ) ) { // REST Request object $headers = $request->get_headers(); - $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; - $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); + $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /' . rest_get_url_prefix() . $request->get_route(); } else { $request = self::format_server_request( $request ); $headers = $request['headers']; // $_SERVER array $headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0]; - $actor = ''; } if ( ! isset( $headers['signature'] ) ) { From afafdf1543f451a28b7ecc993bcc978edb199a89 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 23:54:29 -0600 Subject: [PATCH 47/81] Add get_remote_key method --- includes/class-signature.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 2a4ea53..05d2165 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -187,7 +187,7 @@ class Signature { } } - $public_key = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $signature_block['keyId'] ), '?' ) ); // phpcs:ignore + $public_key = self::get_remote_key( $signature_block['keyId'] ); if ( \is_wp_error( $public_key ) ) { return $public_key; } else { @@ -200,6 +200,24 @@ class Signature { return $verified; } + /** + * Get public key from key_id + * + * @param string $key_id + * @return string $publicKeyPem + * @author Django Doucet + */ + public static function get_remote_key( $key_id ) { // phpcs:ignore + $actor = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $key_id ), '?' ) ); // phpcs:ignore + if ( \is_wp_error( $actor ) ) { + return $actor; + } + if ( isset( $actor['publicKey']['publicKeyPem'] ) ) { + return \rtrim( $actor['publicKey']['publicKeyPem'] ); // phpcs:ignore + } + return null; + } + /** * Gets the signature algorithm from the signature header * From abedf014ae470062362878ee3e7b550a86f543ee Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 23:56:39 -0600 Subject: [PATCH 48/81] remove redundant --- includes/class-signature.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 05d2165..c426727 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -190,8 +190,6 @@ class Signature { $public_key = self::get_remote_key( $signature_block['keyId'] ); if ( \is_wp_error( $public_key ) ) { return $public_key; - } else { - $public_key = \rtrim( $public_key ); } $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; if ( ! $verified ) { From 378f5dacdc305a6f83755549277500cd7fc817b0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 11:32:26 +0200 Subject: [PATCH 49/81] fix issue with missing array --- includes/class-signature.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index c426727..a56a206 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -22,12 +22,12 @@ class Signature { } if ( -1 === $user_id ) { - $key = array( \get_option( 'activitypub_magic_sig_public_key' ) ); + $key = \get_option( 'activitypub_magic_sig_public_key' ); } else { - $key = \get_user_meta( $user_id, 'magic_sig_public_key' ); + $key = \get_user_meta( $user_id, 'magic_sig_public_key', true ); } - return $key[0]; + return $key; } /** @@ -43,10 +43,10 @@ class Signature { if ( -1 === $user_id ) { $key = \get_option( 'activitypub_magic_sig_private_key' ); } else { - $key = \get_user_meta( $user_id, 'magic_sig_private_key' ); + $key = \get_user_meta( $user_id, 'magic_sig_private_key', true ); } - return $key[0]; + return $key; } /** @@ -69,10 +69,10 @@ class Signature { if ( -1 === $user_id ) { // private key - \add_option( 'activitypub_magic_sig_private_key', $priv_key ); + \update_option( 'activitypub_magic_sig_private_key', $priv_key ); // public key - \add_option( 'activitypub_magic_sig_public_key', $detail['key'] ); + \update_option( 'activitypub_magic_sig_public_key', $detail['key'] ); } else { // private key From c42edfce6825a3c913ae332561d17c3de8c55edb Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 11:51:53 +0200 Subject: [PATCH 50/81] use WP_Error --- includes/class-signature.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index a56a206..1032438 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -1,6 +1,7 @@ 403 ) ); + return new WP_Error( 'activitypub_signature', 'Request not signed', array( 'status' => 403 ) ); } if ( array_key_exists( 'signature', $headers ) ) { @@ -152,7 +153,7 @@ class Signature { } if ( ! isset( $signature_block ) || ! $signature_block ) { - return new \WP_Error( 'activitypub_signature', 'Incompatible request signature. keyId and signature are required', array( 'status' => 403 ) ); + return new WP_Error( 'activitypub_signature', 'Incompatible request signature. keyId and signature are required', array( 'status' => 403 ) ); } $signed_headers = $signature_block['headers']; @@ -162,12 +163,12 @@ class Signature { $signed_data = self::get_signed_data( $signed_headers, $signature_block, $headers ); if ( ! $signed_data ) { - return new \WP_Error( 'activitypub_signature', 'Signed request date outside acceptable time window', array( 'status' => 403 ) ); + return new WP_Error( 'activitypub_signature', 'Signed request date outside acceptable time window', array( 'status' => 403 ) ); } $algorithm = self::get_signature_algorithm( $signature_block ); if ( ! $algorithm ) { - return new \WP_Error( 'activitypub_signature', 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)', array( 'status' => 403 ) ); + return new WP_Error( 'activitypub_signature', 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)', array( 'status' => 403 ) ); } if ( \in_array( 'digest', $signed_headers, true ) && isset( $body ) ) { @@ -183,7 +184,7 @@ class Signature { } if ( \base64_encode( \hash( $hashalg, $body, true ) ) !== $digest[1] ) { // phpcs:ignore - return new \WP_Error( 'activitypub_signature', 'Invalid Digest header', array( 'status' => 403 ) ); + return new WP_Error( 'activitypub_signature', 'Invalid Digest header', array( 'status' => 403 ) ); } } @@ -193,7 +194,7 @@ class Signature { } $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; if ( ! $verified ) { - return new \WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); + return new WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); } return $verified; } From 96953dfc7e033036941a2d423981dc506e3adda0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 11:57:43 +0200 Subject: [PATCH 51/81] fail early and always return $response as fallback --- includes/rest/class-server.php | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 0e8df43..8deac10 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -80,24 +80,28 @@ class Server { */ public static function authorize_activitypub_requests( $response, $handler, $request ) { $route = $request->get_route(); - if ( str_starts_with( $route, '/activitypub' ) ) { - if ( 'POST' === $request->get_method() ) { - $verified_request = Signature::verify_http_signature( $request ); - if ( \is_wp_error( $verified_request ) ) { - return $verified_request; - } - } else { - if ( '/activitypub/1.0/webfinger' !== $route ) { - // SecureMode/Authorized fetch. - $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); - if ( $secure_mode ) { - $verified_request = Signature::verify_http_signature( $request ); - if ( \is_wp_error( $verified_request ) ) { - return $verified_request; - } + if ( ! str_starts_with( $route, '/activitypub' ) ) { + return $response; + } + + if ( 'POST' === $request->get_method() ) { + $verified_request = Signature::verify_http_signature( $request ); + if ( \is_wp_error( $verified_request ) ) { + return $verified_request; + } + } else { + if ( '/activitypub/1.0/webfinger' !== $route ) { + // SecureMode/Authorized fetch. + $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); + if ( $secure_mode ) { + $verified_request = Signature::verify_http_signature( $request ); + if ( \is_wp_error( $verified_request ) ) { + return $verified_request; } } } } + + return $response; } } From 180e882c4af2803f471ecba42beb3b2d3ea00c70 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 12:12:05 +0200 Subject: [PATCH 52/81] generate key if not existent --- includes/class-signature.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/includes/class-signature.php b/includes/class-signature.php index 1032438..796cb2c 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -28,6 +28,10 @@ class Signature { $key = \get_user_meta( $user_id, 'magic_sig_public_key', true ); } + if ( ! $key ) { + return self::generate_key_pair( $user_id, true ); + } + return $key; } @@ -47,6 +51,10 @@ class Signature { $key = \get_user_meta( $user_id, 'magic_sig_private_key', true ); } + if ( ! $key ) { + return self::generate_key_pair( $user_id, true ); + } + return $key; } @@ -108,6 +116,8 @@ class Signature { $signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date"; } + var_dump($key); + $signature = null; \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore From c872cb69d08aab49f2ab30856fc3112814da7f2d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 12:13:35 +0200 Subject: [PATCH 53/81] remove var_dump :( --- includes/class-signature.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 796cb2c..16e33ea 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -116,8 +116,6 @@ class Signature { $signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date"; } - var_dump($key); - $signature = null; \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore From b88c5f606dabbe1cb43e31541e9509ad273fdc2a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 12:17:48 +0200 Subject: [PATCH 54/81] fixed copy/paste issue --- includes/class-signature.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 16e33ea..c958379 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -29,7 +29,7 @@ class Signature { } if ( ! $key ) { - return self::generate_key_pair( $user_id, true ); + return self::get_public_key( $user_id, true ); } return $key; @@ -52,7 +52,7 @@ class Signature { } if ( ! $key ) { - return self::generate_key_pair( $user_id, true ); + return self::get_private_key( $user_id, true ); } return $key; From ca8aff182350b57561f5868c583bd7d6bc326cd4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 12:25:25 +0200 Subject: [PATCH 55/81] cast to bool, to be sure that '0' is false --- includes/class-activitypub.php | 2 +- includes/rest/class-server.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index c0695a9..6e79fa6 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -78,7 +78,7 @@ class Activitypub { \in_array( 'application/ld+json', $accept, true ) || \in_array( 'application/json', $accept, true ) ) { - $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); + $secure_mode = (bool) \get_option( 'activitypub_use_secure_mode', '0' ); if ( $secure_mode ) { $verification = Signature::verify_http_signature( $_SERVER ); if ( \is_wp_error( $verification ) ) { diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 8deac10..712a3af 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -92,7 +92,7 @@ class Server { } else { if ( '/activitypub/1.0/webfinger' !== $route ) { // SecureMode/Authorized fetch. - $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); + $secure_mode = (bool) \get_option( 'activitypub_use_secure_mode', '0' ); if ( $secure_mode ) { $verified_request = Signature::verify_http_signature( $request ); if ( \is_wp_error( $verified_request ) ) { From fc1b89561e7f6e07a2e726db6d5f8d265bcac92e Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Wed, 10 May 2023 19:46:52 -0600 Subject: [PATCH 56/81] If WP_REST_Request set actor for get_remote_key() --- includes/class-signature.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index c958379..7c948f9 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -143,10 +143,12 @@ class Signature { public static function verify_http_signature( $request ) { if ( is_object( $request ) ) { // REST Request object $headers = $request->get_headers(); + $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /' . rest_get_url_prefix() . $request->get_route(); } else { $request = self::format_server_request( $request ); $headers = $request['headers']; // $_SERVER array + $actor = null; $headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0]; } @@ -196,7 +198,11 @@ class Signature { } } - $public_key = self::get_remote_key( $signature_block['keyId'] ); + if ( $actor ) { + $public_key = self::get_remote_key( $actor ); + } else { + $public_key = self::get_remote_key( $signature_block['keyId'] ); + } if ( \is_wp_error( $public_key ) ) { return $public_key; } From f196047901e606f07eb4bcbe3e1adb31b0c16cbf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 11 May 2023 11:02:06 +0200 Subject: [PATCH 57/81] remove casts after feedback from @akirk --- activitypub.php | 2 +- includes/class-activitypub.php | 3 +-- includes/rest/class-server.php | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/activitypub.php b/activitypub.php index da2eea2..049d7d7 100644 --- a/activitypub.php +++ b/activitypub.php @@ -75,7 +75,7 @@ function init() { Rest\Webfinger::init(); // load NodeInfo endpoints only if blog is public - if ( true === (bool) \get_option( 'blog_public', 1 ) ) { + if ( \get_option( 'blog_public', 1 ) ) { require_once \dirname( __FILE__ ) . '/includes/rest/class-nodeinfo.php'; Rest\NodeInfo::init(); } diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 6e79fa6..30cc7d0 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -78,8 +78,7 @@ class Activitypub { \in_array( 'application/ld+json', $accept, true ) || \in_array( 'application/json', $accept, true ) ) { - $secure_mode = (bool) \get_option( 'activitypub_use_secure_mode', '0' ); - if ( $secure_mode ) { + if ( \get_option( 'activitypub_use_secure_mode', '0' ) ) { $verification = Signature::verify_http_signature( $_SERVER ); if ( \is_wp_error( $verification ) ) { // fallback as template_loader can't return http headers diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 712a3af..a5219f3 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -92,8 +92,7 @@ class Server { } else { if ( '/activitypub/1.0/webfinger' !== $route ) { // SecureMode/Authorized fetch. - $secure_mode = (bool) \get_option( 'activitypub_use_secure_mode', '0' ); - if ( $secure_mode ) { + if ( \get_option( 'activitypub_use_secure_mode', '0' ) ) { $verified_request = Signature::verify_http_signature( $request ); if ( \is_wp_error( $verified_request ) ) { return $verified_request; From 7d5cfb3078bfd2b4775b4edc6e22e5733db15946 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 12 May 2023 10:17:36 +0200 Subject: [PATCH 58/81] phpdoc --- includes/class-signature.php | 47 ++++++++++++++++++++++++---------- includes/rest/class-server.php | 11 ++++---- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 7c948f9..f82806a 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -9,13 +9,17 @@ use DateTimeZone; * ActivityPub Signature Class * * @author Matthias Pfefferle + * @author Django Doucet */ class Signature { /** - * @param int $user_id + * Return the public key for a given user. * - * @return mixed + * @param int $user_id The WordPress User ID. + * @param bool $force Force the generation of a new key pair. + * + * @return mixed The public key. */ public static function get_public_key( $user_id, $force = false ) { if ( $force ) { @@ -36,9 +40,12 @@ class Signature { } /** - * @param int $user_id + * Return the private key for a given user. * - * @return mixed + * @param int $user_id The WordPress User ID. + * @param bool $force Force the generation of a new key pair. + * + * @return mixed The private key. */ public static function get_private_key( $user_id, $force = false ) { if ( $force ) { @@ -61,7 +68,9 @@ class Signature { /** * Generates the pair keys * - * @param int $user_id + * @param int $user_id The WordPress User ID. + * + * @return void */ public static function generate_key_pair( $user_id ) { $config = array( @@ -92,6 +101,17 @@ class Signature { } } + /** + * Generates the Signature for a HTTP Request + * + * @param int $user_id The WordPress User ID. + * @param string $http_method The HTTP method. + * @param string $url The URL to send the request to. + * @param string $date The date the request is sent. + * @param string $digest The digest of the request body. + * + * @return string The signature. + */ public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { $key = self::get_private_key( $user_id ); @@ -136,9 +156,9 @@ class Signature { /** * Verifies the http signatures * - * @param WP_REQUEST | Array $_SERVER - * @return void - * @author Django Doucet + * @param WP_REQUEST|array $request The request object or $_SERVER array. + * + * @return mixed A boolean or WP_Error. */ public static function verify_http_signature( $request ) { if ( is_object( $request ) ) { // REST Request object @@ -217,8 +237,8 @@ class Signature { * Get public key from key_id * * @param string $key_id + * * @return string $publicKeyPem - * @author Django Doucet */ public static function get_remote_key( $key_id ) { // phpcs:ignore $actor = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $key_id ), '?' ) ); // phpcs:ignore @@ -235,8 +255,8 @@ class Signature { * Gets the signature algorithm from the signature header * * @param array $signature_block + * * @return string algorithm - * @author Django Doucet */ public static function get_signature_algorithm( $signature_block ) { if ( $signature_block['algorithm'] ) { @@ -254,8 +274,8 @@ class Signature { * Parses the Signature header * * @param array $header + * * @return array signature parts - * @author Django Doucet */ public static function parse_signature_header( $header ) { $ret = array(); @@ -293,9 +313,9 @@ class Signature { * * @param array $signed_headers * @param array $signature_block (pseudo-headers) - * @param array $headers (http headers) + * @param array $headers (http headers) + * * @return signed headers for comparison - * @author Django Doucet */ public static function get_signed_data( $signed_headers, $signature_block, $headers ) { $signed_data = ''; @@ -360,6 +380,7 @@ class Signature { * for use with verify_http_signature() * * @param array $_SERVER + * * @return array $request */ public static function format_server_request( $server ) { diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index a5219f3..7e8f720 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -41,7 +41,7 @@ class Server { /** * Render Application actor profile * - * @return WP_REST_Response + * @return WP_REST_Response The JSON profile of the Application Actor. */ public static function application_actor() { $json = new \stdClass(); @@ -72,11 +72,12 @@ class Server { * * @see \WP_REST_Request * - * @param $response - * @param $handler - * @param \WP_REST_Request $request + * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. + * Usually a WP_REST_Response or WP_Error. + * @param array $handler Route handler used for the request. + * @param WP_REST_Request $request Request used to generate the response. * - * @return mixed|\WP_Error + * @return mixed|WP_Error The response, error, or modified response. */ public static function authorize_activitypub_requests( $response, $handler, $request ) { $route = $request->get_route(); From 7456d36834acf459f0fcdc489e457f9ff06491f5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 15 May 2023 10:48:34 +0200 Subject: [PATCH 59/81] use const instead of -1 --- activitypub.php | 1 + includes/class-http.php | 3 ++- includes/class-signature.php | 9 +++++---- includes/model/class-user.php | 23 +++++++++++++++++++++++ includes/rest/class-server.php | 3 ++- 5 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 includes/model/class-user.php diff --git a/activitypub.php b/activitypub.php index 049d7d7..4292684 100644 --- a/activitypub.php +++ b/activitypub.php @@ -41,6 +41,7 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/model/class-activity.php'; require_once \dirname( __FILE__ ) . '/includes/model/class-post.php'; + require_once \dirname( __FILE__ ) . '/includes/model/class-user.php'; require_once \dirname( __FILE__ ) . '/includes/model/class-follower.php'; require_once \dirname( __FILE__ ) . '/includes/class-migration.php'; diff --git a/includes/class-http.php b/includes/class-http.php index 247d87e..58cace9 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -2,6 +2,7 @@ namespace Activitypub; use WP_Error; +use Activitypub\Model\User; /** * ActivityPub HTTP Class @@ -62,7 +63,7 @@ class Http { */ public static function get( $url ) { $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = Signature::generate_signature( -1, 'get', $url, $date ); + $signature = Signature::generate_signature( User::APPLICATION_USER_ID, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); diff --git a/includes/class-signature.php b/includes/class-signature.php index f82806a..9472128 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -4,6 +4,7 @@ namespace Activitypub; use WP_Error; use DateTime; use DateTimeZone; +use Activitypub\Model\User; /** * ActivityPub Signature Class @@ -26,7 +27,7 @@ class Signature { self::generate_key_pair( $user_id ); } - if ( -1 === $user_id ) { + if ( User::APPLICATION_USER_ID === $user_id ) { $key = \get_option( 'activitypub_magic_sig_public_key' ); } else { $key = \get_user_meta( $user_id, 'magic_sig_public_key', true ); @@ -52,7 +53,7 @@ class Signature { self::generate_key_pair( $user_id ); } - if ( -1 === $user_id ) { + if ( User::APPLICATION_USER_ID === $user_id ) { $key = \get_option( 'activitypub_magic_sig_private_key' ); } else { $key = \get_user_meta( $user_id, 'magic_sig_private_key', true ); @@ -85,7 +86,7 @@ class Signature { \openssl_pkey_export( $key, $priv_key ); $detail = \openssl_pkey_get_details( $key ); - if ( -1 === $user_id ) { + if ( User::APPLICATION_USER_ID === $user_id ) { // private key \update_option( 'activitypub_magic_sig_private_key', $priv_key ); @@ -140,7 +141,7 @@ class Signature { \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore - if ( -1 === $user_id ) { + if ( User::APPLICATION_USER_ID === $user_id ) { $key_id = \get_rest_url( null, 'activitypub/1.0/service#main-key' ); } else { $key_id = \get_author_posts_url( $user_id ) . '#main-key'; diff --git a/includes/model/class-user.php b/includes/model/class-user.php new file mode 100644 index 0000000..5c62473 --- /dev/null +++ b/includes/model/class-user.php @@ -0,0 +1,23 @@ +publicKey = (object) array( // phpcs:ignore WordPress.NamingConventions 'id' => \get_rest_url( null, 'activitypub/1.0/application#main-key' ), 'owner' => \get_rest_url( null, 'activitypub/1.0/application' ), - 'publicKeyPem' => Signature::get_public_key( -1 ), // phpcs:ignore WordPress.NamingConventions + 'publicKeyPem' => Signature::get_public_key( User::APPLICATION_USER_ID ), // phpcs:ignore WordPress.NamingConventions ); $response = new WP_REST_Response( $json, 200 ); From 12724a3681b12ad8e369737fd458e44ade51f3be Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 14 May 2023 22:53:11 -0600 Subject: [PATCH 60/81] Switch secure_mode to a filter --- activitypub.php | 1 + includes/class-activitypub.php | 2 +- includes/class-admin.php | 18 +++++++++--------- includes/rest/class-server.php | 2 +- templates/settings.php | 6 +++--- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/activitypub.php b/activitypub.php index 4292684..378e999 100644 --- a/activitypub.php +++ b/activitypub.php @@ -25,6 +25,7 @@ function init() { \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=

)|(?<=
)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' ); \defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9_-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' ); \defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "

[ap_title]

\n\n[ap_content]\n\n

[ap_hashtags]

\n\n

[ap_shortlink]

" ); + \defined( 'ACTIVITYPUB_SECURE_MODE' ) || \define( 'ACTIVITYPUB_SECURE_MODE', apply_filters( 'activitypub_secure_mode', $value = false ) ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 30cc7d0..f7885cd 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -78,7 +78,7 @@ class Activitypub { \in_array( 'application/ld+json', $accept, true ) || \in_array( 'application/json', $accept, true ) ) { - if ( \get_option( 'activitypub_use_secure_mode', '0' ) ) { + if ( ACTIVITYPUB_SECURE_MODE ) { $verification = Signature::verify_http_signature( $_SERVER ); if ( \is_wp_error( $verification ) ) { // fallback as template_loader can't return http headers diff --git a/includes/class-admin.php b/includes/class-admin.php index 7a19e8e..4bc7b9c 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -144,15 +144,15 @@ class Admin { 'default' => array( 'post', 'pages' ), ) ); - \register_setting( - 'activitypub', - 'activitypub_use_secure_mode', - array( - 'type' => 'boolean', - 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), - 'default' => 0, - ) - ); + // \register_setting( + // 'activitypub', + // 'activitypub_use_secure_mode', + // array( + // 'type' => 'boolean', + // 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), + // 'default' => 0, + // ) + // ); } public static function schedule_migration() { diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index a2d837a..b5b55fd 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -94,7 +94,7 @@ class Server { } else { if ( '/activitypub/1.0/webfinger' !== $route ) { // SecureMode/Authorized fetch. - if ( \get_option( 'activitypub_use_secure_mode', '0' ) ) { + if ( ACTIVITYPUB_SECURE_MODE ) { $verified_request = Signature::verify_http_signature( $request ); if ( \is_wp_error( $verified_request ) ) { return $verified_request; diff --git a/templates/settings.php b/templates/settings.php index 6429688..1966c4f 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -171,7 +171,7 @@ - + From e79f2e89913c949f6a4d1c0eeb783c6fa0b937a6 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Tue, 16 May 2023 00:11:27 -0600 Subject: [PATCH 61/81] fix keyId url --- includes/class-signature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 9472128..0ef8bcb 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -142,7 +142,7 @@ class Signature { $signature = \base64_encode( $signature ); // phpcs:ignore if ( User::APPLICATION_USER_ID === $user_id ) { - $key_id = \get_rest_url( null, 'activitypub/1.0/service#main-key' ); + $key_id = \get_rest_url( null, 'activitypub/1.0/application#main-key' ); } else { $key_id = \get_author_posts_url( $user_id ) . '#main-key'; } From 49ee03f1f1637e50079152ca550cafaf58daa4d5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 16 May 2023 10:01:23 +0200 Subject: [PATCH 62/81] fix indents --- activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 84460df..691a318 100644 --- a/activitypub.php +++ b/activitypub.php @@ -25,7 +25,7 @@ function init() { \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=

)|(?<=
)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' ); \defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9_-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' ); \defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" ); - \defined( 'ACTIVITYPUB_SECURE_MODE' ) || \define( 'ACTIVITYPUB_SECURE_MODE', apply_filters( 'activitypub_secure_mode', $value = false ) ); + \defined( 'ACTIVITYPUB_SECURE_MODE' ) || \define( 'ACTIVITYPUB_SECURE_MODE', apply_filters( 'activitypub_secure_mode', $value = false ) ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); From 5e4c68ab66ea0bfd3fbb9a518c6ba67aaae7954a Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Wed, 17 May 2023 23:49:33 -0600 Subject: [PATCH 63/81] server init --- activitypub.php | 1 + 1 file changed, 1 insertion(+) diff --git a/activitypub.php b/activitypub.php index 2f74c2f..b3f6d7f 100644 --- a/activitypub.php +++ b/activitypub.php @@ -44,6 +44,7 @@ function init() { Rest\Following::init(); Rest\Nodeinfo::init(); Rest\Webfinger::init(); + Rest\Server::init(); Admin::init(); Hashtag::init(); From ed77ffce264515f74de56abd3b4c59f3560c9624 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Thu, 18 May 2023 00:03:11 -0600 Subject: [PATCH 64/81] update rest paths to namespace --- includes/rest/class-server.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index b5b55fd..c99bb34 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -4,6 +4,8 @@ namespace Activitypub\Rest; use WP_REST_Response; use Activitypub\Signature; use Activitypub\Model\User; +use function Activitypub\get_rest_url_by_path; + /** * ActivityPub Server REST-Class @@ -18,8 +20,8 @@ class Server { * Initialize the class, registering WordPress hooks */ public static function init() { - \add_filter( 'rest_request_before_callbacks', array( self::class, 'authorize_activitypub_requests' ), 10, 3 ); \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); + \add_filter( 'rest_request_before_callbacks', array( self::class, 'authorize_activitypub_requests' ), 10, 3 ); } /** @@ -27,7 +29,7 @@ class Server { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', + ACTIVITYPUB_REST_NAMESPACE, '/application', array( array( @@ -48,16 +50,16 @@ class Server { $json = new \stdClass(); $json->{'@context'} = \Activitypub\get_context(); - $json->id = \get_rest_url( null, 'activitypub/1.0/application' ); + $json->id = get_rest_url_by_path( 'application' ); $json->type = 'Application'; - $json->preferredUsername = wp_parse_url( get_site_url(), PHP_URL_HOST ); // phpcs:ignore WordPress.NamingConventions + $json->preferredUsername = str_replace( array( '.' ), '-' , wp_parse_url( get_site_url(), PHP_URL_HOST ) ); // phpcs:ignore WordPress.NamingConventions $json->name = get_bloginfo( 'name' ); $json->summary = 'WordPress-ActivityPub application actor'; $json->manuallyApprovesFollowers = true; // phpcs:ignore WordPress.NamingConventions $json->icon = array( get_site_icon_url() ); // phpcs:ignore WordPress.NamingConventions short array syntax $json->publicKey = (object) array( // phpcs:ignore WordPress.NamingConventions - 'id' => \get_rest_url( null, 'activitypub/1.0/application#main-key' ), - 'owner' => \get_rest_url( null, 'activitypub/1.0/application' ), + 'id' => get_rest_url_by_path( 'application#main-key' ), + 'owner' => get_rest_url_by_path( 'application' ), 'publicKeyPem' => Signature::get_public_key( User::APPLICATION_USER_ID ), // phpcs:ignore WordPress.NamingConventions ); @@ -92,7 +94,7 @@ class Server { return $verified_request; } } else { - if ( '/activitypub/1.0/webfinger' !== $route ) { + if ( get_rest_url_by_path( 'webfinger' ) !== $route ) { // SecureMode/Authorized fetch. if ( ACTIVITYPUB_SECURE_MODE ) { $verified_request = Signature::verify_http_signature( $request ); From f4aadc00fcc41b2c21d8b69b6902e5b5a899ece1 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Thu, 18 May 2023 00:10:03 -0600 Subject: [PATCH 65/81] phpcs --- includes/rest/class-server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index c99bb34..dfee380 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -52,7 +52,7 @@ class Server { $json->{'@context'} = \Activitypub\get_context(); $json->id = get_rest_url_by_path( 'application' ); $json->type = 'Application'; - $json->preferredUsername = str_replace( array( '.' ), '-' , wp_parse_url( get_site_url(), PHP_URL_HOST ) ); // phpcs:ignore WordPress.NamingConventions + $json->preferredUsername = str_replace( array( '.' ), '-', wp_parse_url( get_site_url(), PHP_URL_HOST ) ); // phpcs:ignore WordPress.NamingConventions $json->name = get_bloginfo( 'name' ); $json->summary = 'WordPress-ActivityPub application actor'; $json->manuallyApprovesFollowers = true; // phpcs:ignore WordPress.NamingConventions From e48ce0ebceedc1a84d02fe0bcc3ce031a1455ab9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 May 2023 17:16:19 +0200 Subject: [PATCH 66/81] I would remove the settings for now --- includes/class-admin.php | 9 --------- templates/settings.php | 15 --------------- 2 files changed, 24 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 4bc7b9c..fe44ed4 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -144,15 +144,6 @@ class Admin { 'default' => array( 'post', 'pages' ), ) ); - // \register_setting( - // 'activitypub', - // 'activitypub_use_secure_mode', - // array( - // 'type' => 'boolean', - // 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), - // 'default' => 0, - // ) - // ); } public static function schedule_migration() { diff --git a/templates/settings.php b/templates/settings.php index 1966c4f..2bfaed4 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -171,21 +171,6 @@

- - From e04ccdc961b62a5ad15cfda759f44e2bb74f0da9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 May 2023 18:06:39 +0200 Subject: [PATCH 67/81] fix missing namespace --- includes/rest/class-outbox.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index d6b0757..2138f71 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -8,6 +8,7 @@ use WP_REST_Response; use Activitypub\Model\Post; use Activitypub\Model\Activity; +use function Activitypub\get_context; use function Activitypub\get_rest_url_by_path; /** From 467a349b1618240e9b9efb6aa0a05ddeed351ff9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 May 2023 11:31:46 +0200 Subject: [PATCH 68/81] some small improvements --- includes/rest/class-server.php | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index dfee380..8962db8 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -1,9 +1,12 @@ {'@context'} = \Activitypub\get_context(); + $json->{'@context'} = get_context(); $json->id = get_rest_url_by_path( 'application' ); $json->type = 'Application'; $json->preferredUsername = str_replace( array( '.' ), '-', wp_parse_url( get_site_url(), PHP_URL_HOST ) ); // phpcs:ignore WordPress.NamingConventions $json->name = get_bloginfo( 'name' ); - $json->summary = 'WordPress-ActivityPub application actor'; + $json->summary = __( 'WordPress-ActivityPub application actor', 'activitypub' ); $json->manuallyApprovesFollowers = true; // phpcs:ignore WordPress.NamingConventions $json->icon = array( get_site_icon_url() ); // phpcs:ignore WordPress.NamingConventions short array syntax - $json->publicKey = (object) array( // phpcs:ignore WordPress.NamingConventions + $json->publicKey = array( // phpcs:ignore WordPress.NamingConventions 'id' => get_rest_url_by_path( 'application#main-key' ), 'owner' => get_rest_url_by_path( 'application' ), 'publicKeyPem' => Signature::get_public_key( User::APPLICATION_USER_ID ), // phpcs:ignore WordPress.NamingConventions @@ -73,7 +76,7 @@ class Server { /** * Callback function to authorize each api requests * - * @see \WP_REST_Request + * @see WP_REST_Request * * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. * Usually a WP_REST_Response or WP_Error. @@ -84,23 +87,25 @@ class Server { */ public static function authorize_activitypub_requests( $response, $handler, $request ) { $route = $request->get_route(); + if ( ! str_starts_with( $route, '/activitypub' ) ) { return $response; } + if ( get_rest_url_by_path( 'webfinger' ) !== $route ) { + return $response; + } + if ( 'POST' === $request->get_method() ) { $verified_request = Signature::verify_http_signature( $request ); if ( \is_wp_error( $verified_request ) ) { return $verified_request; } - } else { - if ( get_rest_url_by_path( 'webfinger' ) !== $route ) { - // SecureMode/Authorized fetch. - if ( ACTIVITYPUB_SECURE_MODE ) { - $verified_request = Signature::verify_http_signature( $request ); - if ( \is_wp_error( $verified_request ) ) { - return $verified_request; - } + } elseif ( 'GET' === $request->get_method() ) { + if ( ACTIVITYPUB_SECURE_MODE ) { + $verified_request = Signature::verify_http_signature( $request ); + if ( \is_wp_error( $verified_request ) ) { + return $verified_request; } } } From ec4e22f57084178f8f7ae804c4066debb6a6fe84 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 May 2023 13:34:14 +0200 Subject: [PATCH 69/81] fix routing checks --- includes/rest/class-server.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 8962db8..7e52772 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -88,11 +88,11 @@ class Server { public static function authorize_activitypub_requests( $response, $handler, $request ) { $route = $request->get_route(); - if ( ! str_starts_with( $route, '/activitypub' ) ) { - return $response; - } - - if ( get_rest_url_by_path( 'webfinger' ) !== $route ) { + if ( + ! str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) || + str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'webfinger' ) || + str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'nodeinfo' ) + ) { return $response; } From d2b7c287fcd07d7876af8db9a525b7625e0be61b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 May 2023 13:35:46 +0200 Subject: [PATCH 70/81] code doc --- includes/rest/class-server.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 7e52772..351284d 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -88,6 +88,7 @@ class Server { public static function authorize_activitypub_requests( $response, $handler, $request ) { $route = $request->get_route(); + // check if it is an activitypub request and exclude webfinger and nodeinfo endpoints if ( ! str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) || str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'webfinger' ) || @@ -96,12 +97,13 @@ class Server { return $response; } + // POST-Requets are always signed if ( 'POST' === $request->get_method() ) { $verified_request = Signature::verify_http_signature( $request ); if ( \is_wp_error( $verified_request ) ) { return $verified_request; } - } elseif ( 'GET' === $request->get_method() ) { + } elseif ( 'GET' === $request->get_method() ) { // GET-Requests are only signed in secure mode if ( ACTIVITYPUB_SECURE_MODE ) { $verified_request = Signature::verify_http_signature( $request ); if ( \is_wp_error( $verified_request ) ) { From 221c5778264457e050adc5d2208dab2d6ae85db5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 25 May 2023 14:03:30 +0200 Subject: [PATCH 71/81] Fix federation with pixelfed! --- includes/class-signature.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/includes/class-signature.php b/includes/class-signature.php index 0ef8bcb..5ab47cc 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -353,6 +353,12 @@ class Signature { if ( 'content-length' === $header ) { $signed_data .= $header . ': ' . $headers['content_length'][0] . "\n"; } + if ( 'user-agent' === $header ) { + $signed_data .= $header . ': ' . $headers['user_agent'][0] . "\n"; + } + if ( 'accept' === $header ) { + $signed_data .= $header . ': ' . $headers['accept'][0] . "\n"; + } if ( 'date' === $header ) { // allow a bit of leeway for misconfigured clocks. $d = new DateTime( $headers[ $header ][0] ); From 273493e7689e349bf0779b53d77d2d515a39cd12 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 26 May 2023 12:40:46 -0600 Subject: [PATCH 72/81] update header parsing in get_signed_data() --- includes/class-signature.php | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 5ab47cc..64467b7 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -322,42 +322,31 @@ class Signature { $signed_data = ''; // This also verifies time-based values by returning false if any of these are out of range. foreach ( $signed_headers as $header ) { - if ( \array_key_exists( $header, $headers ) ) { - if ( 'host' === $header ) { - if ( isset( $headers['x_original_host'] ) ) { - $signed_data .= $header . ': ' . $headers['x_original_host'][0] . "\n"; - } else { - $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; - } - } else { - $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; + if ( 'host' === $header ) { + if ( isset( $headers['x_original_host'] ) ) { + $signed_data .= $header . ': ' . $headers['x_original_host'][0] . "\n"; + continue; } } + if ( '(request-target)' === $header ) { + $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; + continue; + } + if ( str_contains( $header, '-' ) ) { + $signed_data .= $header . ': ' . $headers[ str_replace( '-', '_', $header ) ][0] . "\n"; + continue; + } if ( '(created)' === $header ) { if ( ! empty( $signature_block['(created)'] ) && \intval( $signature_block['(created)'] ) > \time() ) { // created in future return false; } - $signed_data .= '(created): ' . $signature_block['(created)'] . "\n"; } if ( '(expires)' === $header ) { if ( ! empty( $signature_block['(expires)'] ) && \intval( $signature_block['(expires)'] ) < \time() ) { // expired in past return false; } - $signed_data .= '(expires): ' . $signature_block['(expires)'] . "\n"; - } - if ( 'content-type' === $header ) { - $signed_data .= $header . ': ' . $headers['content_type'][0] . "\n"; - } - if ( 'content-length' === $header ) { - $signed_data .= $header . ': ' . $headers['content_length'][0] . "\n"; - } - if ( 'user-agent' === $header ) { - $signed_data .= $header . ': ' . $headers['user_agent'][0] . "\n"; - } - if ( 'accept' === $header ) { - $signed_data .= $header . ': ' . $headers['accept'][0] . "\n"; } if ( 'date' === $header ) { // allow a bit of leeway for misconfigured clocks. @@ -373,6 +362,7 @@ class Signature { return false; } } + $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; } return \rtrim( $signed_data, "\n" ); } From 285925ea08740c27c0d68ab713fb3ac72f1865a8 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Wed, 31 May 2023 06:35:58 -0600 Subject: [PATCH 73/81] test_activity_signature --- ...typub-rest-post-signature-verification.php | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test-class-activitypub-rest-post-signature-verification.php diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php new file mode 100644 index 0000000..f972871 --- /dev/null +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -0,0 +1,57 @@ + 1, + 'post_content' => 'hello world', + ) + ); + $remote_actor = \get_author_posts_url( 2 ); + $activitypub_post = new \Activitypub\Model\Post( $post ); + $activitypub_activity = new Activitypub\Model\Activity( 'Create' ); + $activitypub_activity->from_post( $activitypub_post ); + $activitypub_activity->add_cc( $remote_actor ); + $activity = $activitypub_activity->to_json(); + + // generate_digest & generate_signature + $digest = Activitypub\Signature::generate_digest( $activity ); + $date = gmdate( 'D, d M Y H:i:s T' ); + $signature = Activitypub\Signature::generate_signature( 1, 'POST', $remote_actor, $date, $digest ); + + // Signed headers + $url_parts = wp_parse_url( $remote_actor ); + $route = $url_parts['path'] . '?' . $url_parts['query']; + $host = $url_parts['host']; + + $headers = array( + 'digest' => [ "SHA-256=$digest" ], + 'signature' => [ $signature ], + 'date' => [ $date ], + 'host' => [ $host ], + '(request-target)' => [ 'POST ' . $route ] + ); + + // Start verification + // parse_signature_header, get_signed_data, get_public_key + $signature_block = Activitypub\Signature::parse_signature_header( $headers['signature'] ); + $signed_headers = $signature_block['headers']; + $signed_data = Activitypub\Signature::get_signed_data( $signed_headers, $signature_block, $headers ); + $public_key = Activitypub\Signature::get_public_key( 1 ); + + // signature_verification + $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, 'rsa-sha256' ) > 0; + $this->assertTRUE( $verified ); + + remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); + } + + +} From 73cd19ec207a48c16390b083607f692471715338 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Wed, 31 May 2023 23:23:40 -0600 Subject: [PATCH 74/81] added test and pre_get_remote_key filter --- includes/class-signature.php | 5 ++ ...typub-rest-post-signature-verification.php | 87 ++++++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 64467b7..2c5be60 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -242,6 +242,11 @@ class Signature { * @return string $publicKeyPem */ public static function get_remote_key( $key_id ) { // phpcs:ignore + $pre = apply_filters( 'pre_get_remote_key', false, $key_id ); + if ( $pre ) { + return $pre; + } + $actor = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $key_id ), '?' ) ); // phpcs:ignore if ( \is_wp_error( $actor ) ) { return $actor; diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index f972871..a0c5e08 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -1,6 +1,35 @@ server = $wp_rest_server; + + do_action( 'rest_api_init' ); + + } + + /** + * Tear down after test ends + */ + public function tearDown() : void { + remove_filter( 'pre_get_remote_key', array( get_called_class(), 'pre_get_remote_key' ) ); + parent::tearDown(); + + global $wp_rest_server; + $wp_rest_server = null; + + } public function test_activity_signature() { @@ -53,5 +82,61 @@ class Test_Activitypub_Rest_Post_Signature_Verification extends WP_UnitTestCase remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } + public function test_rest_activity_signature() { + + $pre_http_request = new MockAction(); + // $pre_get_remote_key = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + add_filter( 'pre_get_remote_key', array( get_called_class(), 'pre_get_remote_key' ), 10, 2 ); + + // Activity Object + $post = \wp_insert_post( + array( + 'post_author' => 1, + 'post_content' => 'hello world', + ) + ); + $remote_actor = \get_author_posts_url( 2 ); + $remote_actor_inbox = \get_rest_url( null, 'activitypub/1.0/inbox' ); + $activitypub_post = new \Activitypub\Model\Post( $post ); + $activitypub_activity = new Activitypub\Model\Activity( 'Create' ); + $activitypub_activity->from_post( $activitypub_post ); + $activitypub_activity->add_cc( $remote_actor_inbox ); + $activity = $activitypub_activity->to_json(); + + // generate_digest & generate_signature + $digest = Activitypub\Signature::generate_digest( $activity ); + $date = gmdate( 'D, d M Y H:i:s T' ); + $signature = Activitypub\Signature::generate_signature( 1, 'POST', $remote_actor, $date, $digest ); + + // Signed headers + $url_parts = wp_parse_url( $remote_actor ); + $route = add_query_arg( $url_parts['query'], $url_parts['path'] ); + $host = $url_parts['host']; + + $request = new WP_REST_Request( 'POST', ACTIVITYPUB_REST_NAMESPACE . '/inbox' ); + $request->set_header( 'content-type', 'application/activity+json' ); + $request->set_header( 'digest', "SHA-256=$digest" ); + $request->set_header( 'signature', $signature ); + $request->set_header( 'date', $date ); + $request->set_header( 'host', $host ); + $request->set_body( $activity ); + + // Start verification + $verified = \Activitypub\Signature::verify_http_signature( $request ); + // $this->assertTRUE( $verified ); + + remove_filter( 'pre_get_remote_key', array( get_called_class(), 'pre_get_remote_key' ) ); + remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); + } + + public static function pre_get_remote_key( $pre, $key_id ) { + $query = wp_parse_url( $key_id, PHP_URL_QUERY ); + parse_str( $query, $output ); + if ( is_int( $output['author'] ) ) { + return ActivityPub\Signature::get_public_key( int( $output['author'] ) ); + } + return $pre; + } } From 727aaf1c4578861fe171784d9cea9ff7647afcd8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 08:05:19 +0200 Subject: [PATCH 75/81] add signature regex test --- tests/test-class-activitypub-activity.php | 2 +- ...est-class-activitypub-rest-post-signature-verification.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test-class-activitypub-activity.php b/tests/test-class-activitypub-activity.php index af0ed55..d689677 100644 --- a/tests/test-class-activitypub-activity.php +++ b/tests/test-class-activitypub-activity.php @@ -22,7 +22,7 @@ class Test_Activitypub_Activity extends WP_UnitTestCase { $activitypub_activity = new \Activitypub\Model\Activity( 'Create' ); $activitypub_activity->from_post( $activitypub_post ); - $this->assertContains( \Activitypub\get_rest_url_by_path( 'users/1/followers' ), $activitypub_activity->get_to() ); + $this->assertContains( \Activitypub\get_rest_url_by_path( 'users/1/followers' ), $activitypub_activity->get_to() ); $this->assertContains( 'https://example.com/alex', $activitypub_activity->get_cc() ); remove_all_filters( 'activitypub_extract_mentions' ); diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index f972871..e9335fa 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -26,6 +26,8 @@ class Test_Activitypub_Rest_Post_Signature_Verification extends WP_UnitTestCase $date = gmdate( 'D, d M Y H:i:s T' ); $signature = Activitypub\Signature::generate_signature( 1, 'POST', $remote_actor, $date, $digest ); + $this->assertRegExp( '/keyId="http:\/\/example\.org\/\?author=1#main-key",algorithm="rsa-sha256",headers="\(request-target\) host date digest",signature="[^"]*"/', $signature ); + // Signed headers $url_parts = wp_parse_url( $remote_actor ); $route = $url_parts['path'] . '?' . $url_parts['query']; @@ -48,7 +50,7 @@ class Test_Activitypub_Rest_Post_Signature_Verification extends WP_UnitTestCase // signature_verification $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, 'rsa-sha256' ) > 0; - $this->assertTRUE( $verified ); + $this->assertTrue( $verified ); remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } From 96881b940afdadd2088cea7f134ffcb6b513a7fc Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 09:49:40 +0200 Subject: [PATCH 76/81] some refactorings and fixed the tests --- includes/class-http.php | 2 +- includes/class-signature.php | 15 ++- ...typub-rest-post-signature-verification.php | 96 ++++++------------- 3 files changed, 39 insertions(+), 74 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index 58cace9..58551a4 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -34,7 +34,7 @@ class Http { 'headers' => array( 'Accept' => 'application/activity+json', 'Content-Type' => 'application/activity+json', - 'Digest' => "SHA-256=$digest", + 'Digest' => $digest, 'Signature' => $signature, 'Date' => $date, ), diff --git a/includes/class-signature.php b/includes/class-signature.php index 2c5be60..e06a226 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -131,8 +131,10 @@ class Signature { $path .= '?' . $url_parts['query']; } + $http_method = \strtolower( $http_method ); + if ( ! empty( $digest ) ) { - $signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date\ndigest: SHA-256=$digest"; + $signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date\ndigest: $digest"; } else { $signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date"; } @@ -165,7 +167,7 @@ class Signature { if ( is_object( $request ) ) { // REST Request object $headers = $request->get_headers(); $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; - $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /' . rest_get_url_prefix() . $request->get_route(); + $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' ' . $request->get_route(); } else { $request = self::format_server_request( $request ); $headers = $request['headers']; // $_SERVER array @@ -227,7 +229,9 @@ class Signature { if ( \is_wp_error( $public_key ) ) { return $public_key; } + $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; + if ( ! $verified ) { return new WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); } @@ -242,11 +246,6 @@ class Signature { * @return string $publicKeyPem */ public static function get_remote_key( $key_id ) { // phpcs:ignore - $pre = apply_filters( 'pre_get_remote_key', false, $key_id ); - if ( $pre ) { - return $pre; - } - $actor = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $key_id ), '?' ) ); // phpcs:ignore if ( \is_wp_error( $actor ) ) { return $actor; @@ -374,7 +373,7 @@ class Signature { public static function generate_digest( $body ) { $digest = \base64_encode( \hash( 'sha256', $body, true ) ); // phpcs:ignore - return "$digest"; + return "SHA-256=$digest"; } /** diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index e87b1fd..f0acd34 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -1,41 +1,7 @@ server = $wp_rest_server; - - do_action( 'rest_api_init' ); - - } - - /** - * Tear down after test ends - */ - public function tearDown() : void { - remove_filter( 'pre_get_remote_key', array( get_called_class(), 'pre_get_remote_key' ) ); - parent::tearDown(); - - global $wp_rest_server; - $wp_rest_server = null; - - } - public function test_activity_signature() { - - $pre_http_request = new MockAction(); - add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - // Activity for generate_digest $post = \wp_insert_post( array( @@ -63,11 +29,11 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { $host = $url_parts['host']; $headers = array( - 'digest' => [ "SHA-256=$digest" ], - 'signature' => [ $signature ], - 'date' => [ $date ], - 'host' => [ $host ], - '(request-target)' => [ 'POST ' . $route ] + 'digest' => array( $digest ), + 'signature' => array( $signature ), + 'date' => array( $date ), + 'host' => array( $host ), + '(request-target)' => array( 'post ' . $route ), ); // Start verification @@ -75,21 +41,32 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { $signature_block = Activitypub\Signature::parse_signature_header( $headers['signature'] ); $signed_headers = $signature_block['headers']; $signed_data = Activitypub\Signature::get_signed_data( $signed_headers, $signature_block, $headers ); + $public_key = Activitypub\Signature::get_public_key( 1 ); // signature_verification $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, 'rsa-sha256' ) > 0; $this->assertTrue( $verified ); - - remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } public function test_rest_activity_signature() { - - $pre_http_request = new MockAction(); - // $pre_get_remote_key = new MockAction(); - add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - add_filter( 'pre_get_remote_key', array( get_called_class(), 'pre_get_remote_key' ), 10, 2 ); + add_filter( + 'pre_get_remote_metadata_by_actor', + function( $json, $actor ) { + // return ActivityPub Profile with signature + return array( + 'id' => $actor, + 'type' => 'Person', + 'publicKey' => array( + 'id' => $actor . '#main-key', + 'owner' => $actor, + 'publicKeyPem' => \Activitypub\Signature::get_public_key( 1 ), + ), + ); + }, + 10, + 2 + ); // Activity Object $post = \wp_insert_post( @@ -99,7 +76,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { ) ); $remote_actor = \get_author_posts_url( 2 ); - $remote_actor_inbox = \get_rest_url( null, 'activitypub/1.0/inbox' ); + $remote_actor_inbox = Activitypub\get_rest_url_by_path( '/inbox' ); $activitypub_post = new \Activitypub\Model\Post( $post ); $activitypub_activity = new Activitypub\Model\Activity( 'Create' ); $activitypub_activity->from_post( $activitypub_post ); @@ -109,16 +86,16 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { // generate_digest & generate_signature $digest = Activitypub\Signature::generate_digest( $activity ); $date = gmdate( 'D, d M Y H:i:s T' ); - $signature = Activitypub\Signature::generate_signature( 1, 'POST', $remote_actor, $date, $digest ); + $signature = Activitypub\Signature::generate_signature( 1, 'POST', $remote_actor_inbox, $date, $digest ); // Signed headers - $url_parts = wp_parse_url( $remote_actor ); - $route = add_query_arg( $url_parts['query'], $url_parts['path'] ); + $url_parts = wp_parse_url( $remote_actor_inbox ); + $route = $url_parts['path'] . '?' . $url_parts['query']; $host = $url_parts['host']; - $request = new WP_REST_Request( 'POST', ACTIVITYPUB_REST_NAMESPACE . '/inbox' ); + $request = new WP_REST_Request( 'POST', $route ); $request->set_header( 'content-type', 'application/activity+json' ); - $request->set_header( 'digest', "SHA-256=$digest" ); + $request->set_header( 'digest', $digest ); $request->set_header( 'signature', $signature ); $request->set_header( 'date', $date ); $request->set_header( 'host', $host ); @@ -126,19 +103,8 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { // Start verification $verified = \Activitypub\Signature::verify_http_signature( $request ); - // $this->assertTRUE( $verified ); + $this->assertTrue( $verified ); - remove_filter( 'pre_get_remote_key', array( get_called_class(), 'pre_get_remote_key' ) ); - remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); + remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_key' ), 10, 2 ); } - - public static function pre_get_remote_key( $pre, $key_id ) { - $query = wp_parse_url( $key_id, PHP_URL_QUERY ); - parse_str( $query, $output ); - if ( is_int( $output['author'] ) ) { - return ActivityPub\Signature::get_public_key( int( $output['author'] ) ); - } - return $pre; - } - } From c1bf6691c1b2686c0ea30dbd715db5ef42b188fd Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 10:13:49 +0200 Subject: [PATCH 77/81] fix route issues --- includes/class-signature.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index e06a226..b4c9374 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -165,9 +165,15 @@ class Signature { */ public static function verify_http_signature( $request ) { if ( is_object( $request ) ) { // REST Request object + // check if route starts with "index.php" + if ( str_starts_with( $request->get_route(), 'index.php' ) ) { + $route = $request->get_route(); + } else { + $route = rest_get_url_prefix() . '/' . ltrim( $request->get_route(), '/' ); + } $headers = $request->get_headers(); $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; - $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' ' . $request->get_route(); + $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' ' . $route; } else { $request = self::format_server_request( $request ); $headers = $request['headers']; // $_SERVER array From 9118e506234f3923e38f1e5a3d88098fd43bf06b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 10:25:18 +0200 Subject: [PATCH 78/81] fix signature verification path --- includes/class-signature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index b4c9374..0b5f681 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -169,7 +169,7 @@ class Signature { if ( str_starts_with( $request->get_route(), 'index.php' ) ) { $route = $request->get_route(); } else { - $route = rest_get_url_prefix() . '/' . ltrim( $request->get_route(), '/' ); + $route = '/' . rest_get_url_prefix() . '/' . ltrim( $request->get_route(), '/' ); } $headers = $request->get_headers(); $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; From b834666edad2641e5a1b932ce699d3061b34c506 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 10:44:05 +0200 Subject: [PATCH 79/81] add missing slash --- includes/class-signature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 0b5f681..06ea766 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -166,7 +166,7 @@ class Signature { public static function verify_http_signature( $request ) { if ( is_object( $request ) ) { // REST Request object // check if route starts with "index.php" - if ( str_starts_with( $request->get_route(), 'index.php' ) ) { + if ( str_starts_with( $request->get_route(), '/index.php' ) ) { $route = $request->get_route(); } else { $route = '/' . rest_get_url_prefix() . '/' . ltrim( $request->get_route(), '/' ); From 00dd5d2c52ab7198cdc7e5271e1c09d44f385b0b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 11:05:47 +0200 Subject: [PATCH 80/81] some phpdoc --- includes/class-signature.php | 45 +++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 06ea766..a3293d6 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -247,9 +247,9 @@ class Signature { /** * Get public key from key_id * - * @param string $key_id + * @param string $key_id The URL to the public key. * - * @return string $publicKeyPem + * @return string The public key. */ public static function get_remote_key( $key_id ) { // phpcs:ignore $actor = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $key_id ), '?' ) ); // phpcs:ignore @@ -267,7 +267,7 @@ class Signature { * * @param array $signature_block * - * @return string algorithm + * @return string The signature algorithm. */ public static function get_signature_algorithm( $signature_block ) { if ( $signature_block['algorithm'] ) { @@ -284,39 +284,39 @@ class Signature { /** * Parses the Signature header * - * @param array $header + * @param array $header The signature header. * * @return array signature parts */ public static function parse_signature_header( $header ) { - $ret = array(); - $matches = array(); - $h_string = \implode( ',', (array) $header[0] ); + $parsed_header = array(); + $matches = array(); + $h_string = \implode( ',', (array) $header[0] ); if ( \preg_match( '/keyId="(.*?)"/ism', $h_string, $matches ) ) { - $ret['keyId'] = $matches[1]; + $parsed_header['keyId'] = $matches[1]; } if ( \preg_match( '/created=([0-9]*)/ism', $h_string, $matches ) ) { - $ret['(created)'] = $matches[1]; + $parsed_header['(created)'] = $matches[1]; } if ( \preg_match( '/expires=([0-9]*)/ism', $h_string, $matches ) ) { - $ret['(expires)'] = $matches[1]; + $parsed_header['(expires)'] = $matches[1]; } if ( \preg_match( '/algorithm="(.*?)"/ism', $h_string, $matches ) ) { - $ret['algorithm'] = $matches[1]; + $parsed_header['algorithm'] = $matches[1]; } if ( \preg_match( '/headers="(.*?)"/ism', $h_string, $matches ) ) { - $ret['headers'] = \explode( ' ', $matches[1] ); + $parsed_header['headers'] = \explode( ' ', $matches[1] ); } if ( \preg_match( '/signature="(.*?)"/ism', $h_string, $matches ) ) { - $ret['signature'] = \base64_decode( preg_replace( '/\s+/', '', $matches[1] ) ); // phpcs:ignore + $parsed_header['signature'] = \base64_decode( preg_replace( '/\s+/', '', $matches[1] ) ); // phpcs:ignore } - if ( ( $ret['signature'] ) && ( $ret['algorithm'] ) && ( ! $ret['headers'] ) ) { - $ret['headers'] = array( 'date' ); + if ( ( $parsed_header['signature'] ) && ( $parsed_header['algorithm'] ) && ( ! $parsed_header['headers'] ) ) { + $parsed_header['headers'] = array( 'date' ); } - return $ret; + return $parsed_header; } /** @@ -326,7 +326,7 @@ class Signature { * @param array $signature_block (pseudo-headers) * @param array $headers (http headers) * - * @return signed headers for comparison + * @return string signed headers for comparison */ public static function get_signed_data( $signed_headers, $signature_block, $headers ) { $signed_data = ''; @@ -377,6 +377,13 @@ class Signature { return \rtrim( $signed_data, "\n" ); } + /** + * Generates the digest for a HTTP Request + * + * @param string $body The body of the request. + * + * @return string The digest. + */ public static function generate_digest( $body ) { $digest = \base64_encode( \hash( 'sha256', $body, true ) ); // phpcs:ignore return "SHA-256=$digest"; @@ -386,9 +393,9 @@ class Signature { * Formats the $_SERVER to resemble the WP_REST_REQUEST array, * for use with verify_http_signature() * - * @param array $_SERVER + * @param array $_SERVER The $_SERVER array. * - * @return array $request + * @return array $request The formatted request array. */ public static function format_server_request( $server ) { $request = array(); From 00e56ca112bda422e9e646ad7871b11228b5d615 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 11:17:08 +0200 Subject: [PATCH 81/81] always use is_activitypub_request to check if it is an AP request --- includes/class-activitypub.php | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 3b16213..e99bcab 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -88,27 +88,7 @@ class Activitypub { $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php'; } - global $wp_query; - - if ( isset( $wp_query->query_vars['activitypub'] ) ) { - return $json_template; - } - - if ( ! isset( $_SERVER['HTTP_ACCEPT'] ) ) { - return $template; - } - - $accept_header = $_SERVER['HTTP_ACCEPT']; - // Accept header as an array. - $accept = \explode( ',', \trim( $accept_header ) ); - if ( - \stristr( $accept_header, 'application/activity+json' ) || - \stristr( $accept_header, 'application/ld+json' ) || - \in_array( 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', $accept, true ) || - \in_array( 'application/activity+json', $accept, true ) || - \in_array( 'application/ld+json', $accept, true ) || - \in_array( 'application/json', $accept, true ) - ) { + if ( is_activitypub_request() ) { if ( ACTIVITYPUB_SECURE_MODE ) { $verification = Signature::verify_http_signature( $_SERVER ); if ( \is_wp_error( $verification ) ) {