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-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/includes/class-signature.php b/includes/class-signature.php index f0ca349..608bff3 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -110,14 +110,23 @@ 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(); - $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 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 : ''; + $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 ) ) { @@ -125,7 +134,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']; @@ -135,12 +144,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 ) ) { @@ -156,41 +165,49 @@ 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 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; - } + $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 $headers; + $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 + } + } + /** + * 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 ) { - 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; } + /** + * 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(); @@ -222,6 +239,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. diff --git a/includes/functions.php b/includes/functions.php index 95ed09e..77508c5 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -219,15 +219,6 @@ 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() ) { $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); $followers = array_merge( $followers, $cc ); diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 29d0d16..cfc7677 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -115,6 +115,7 @@ class Inbox { * @return WP_REST_Response */ public static function user_inbox_post( $request ) { + $user_id = $request->get_param( 'user_id' ); $data = $request->get_params(); @@ -135,6 +136,7 @@ class Inbox { * @return WP_REST_Response */ public static function shared_inbox_post( $request ) { + $data = $request->get_params(); $type = $request->get_param( 'type' ); $users = self::extract_recipients( $data ); @@ -214,16 +216,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 ) { @@ -266,12 +258,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( 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; + } + } + } + } + } +} 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 @@ + + + + + + + +
+ + +

+ +

+
+