From 038bf25b2e186b36e2f4292ec6ec8f0508fb87b4 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sat, 15 Apr 2023 23:57:08 -0600 Subject: [PATCH 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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 + } }