Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use a more explicit signature verification #1078

Merged
merged 19 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Improve Interactions moderation
* Compatibility with Akismet
* Comment type mapping for `Like` and `Announce`
* Signature verification for API endpoints

### Fixed

Expand Down
6 changes: 3 additions & 3 deletions includes/handler/class-delete.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public static function init() {

// Defer signature verification for `Delete` requests.
\add_filter(
'activitypub_defer_signature_verification',
array( self::class, 'defer_signature_verification' ),
'activitypub_defer_verify_signature',
array( self::class, 'defer_verify_signature' ),
10,
2
);
Expand Down Expand Up @@ -183,7 +183,7 @@ public static function maybe_delete_interaction( $activity ) {
*
* @return bool Whether to defer signature verification.
*/
public static function defer_signature_verification( $defer, $request ) {
public static function defer_verify_signature( $defer, $request ) {
$json = $request->get_json_params();

if ( isset( $json['type'] ) && 'Delete' === $json['type'] ) {
Expand Down
2 changes: 1 addition & 1 deletion includes/rest/class-actors.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public static function register_routes() {
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( self::class, 'get' ),
'permission_callback' => '__return_true',
'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ),
),
)
);
Expand Down
2 changes: 1 addition & 1 deletion includes/rest/class-followers.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static function register_routes() {
'methods' => WP_REST_Server::READABLE,
'callback' => array( self::class, 'get' ),
'args' => self::request_parameters(),
'permission_callback' => '__return_true',
'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ),
),
)
);
Expand Down
2 changes: 1 addition & 1 deletion includes/rest/class-following.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static function register_routes() {
'methods' => \WP_REST_Server::READABLE,
'callback' => array( self::class, 'get' ),
'args' => self::request_parameters(),
'permission_callback' => '__return_true',
'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ),
),
)
);
Expand Down
6 changes: 3 additions & 3 deletions includes/rest/class-inbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public static function register_routes() {
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( self::class, 'shared_inbox_post' ),
'args' => self::shared_inbox_post_parameters(),
'permission_callback' => '__return_true',
'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ),
),
)
);
Expand All @@ -58,13 +58,13 @@ public static function register_routes() {
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( self::class, 'user_inbox_post' ),
'args' => self::user_inbox_post_parameters(),
'permission_callback' => '__return_true',
'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ),
),
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( self::class, 'user_inbox_get' ),
'args' => self::user_inbox_get_parameters(),
'permission_callback' => '__return_true',
'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ),
),
)
);
Expand Down
2 changes: 1 addition & 1 deletion includes/rest/class-outbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public static function register_routes() {
'methods' => WP_REST_Server::READABLE,
'callback' => array( self::class, 'user_outbox_get' ),
'args' => self::request_parameters(),
'permission_callback' => '__return_true',
'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ),
),
)
);
Expand Down
50 changes: 17 additions & 33 deletions includes/rest/class-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ public static function init() {
* Add sever hooks.
*/
public static function add_hooks() {
\add_filter( 'rest_request_before_callbacks', array( self::class, 'validate_activitypub_requests' ), 9, 3 );
\add_filter( 'rest_request_before_callbacks', array( self::class, 'authorize_activitypub_requests' ), 10, 3 );
\add_filter( 'rest_request_before_callbacks', array( self::class, 'validate_requests' ), 9, 3 );
\add_filter( 'rest_request_parameter_order', array( self::class, 'request_parameter_order' ), 10, 2 );
}

Expand Down Expand Up @@ -74,39 +73,24 @@ public static function application_actor() {
}

/**
* Callback function to authorize each api requests
* Callback function to authorize an api request.
*
* @see WP_REST_Request
* The function is meant to be used as a permission callback for the rest api.
*
* It verifies the signature of POST, PUT, PATCH and DELETE requests and also the GET requests in secure mode.
* You can use the filter 'activitypub_defer_verify_signature' to defer the signature verification.
* The HEAD request is always bypassed.
pfefferle marked this conversation as resolved.
Show resolved Hide resolved
*
* @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
* @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
*
* @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.
* @param \WP_REST_Request $request The request object.
*
* @return mixed|WP_Error The response, error, or modified response.
* @return bool|\WP_Error True if the request is authorized, WP_Error if not.
*/
public static function authorize_activitypub_requests( $response, $handler, $request ) {
public static function verify_signature( $request ) {
if ( 'HEAD' === $request->get_method() ) {
return $response;
}

if ( \is_wp_error( $response ) ) {
return $response;
}

$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' ) ||
\str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'nodeinfo' ) ||
\str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'application' )
) {
return $response;
return true;
}

/**
Expand All @@ -120,29 +104,29 @@ public static function authorize_activitypub_requests( $response, $handler, $req
*
* @return bool Whether to defer signature verification.
*/
$defer = \apply_filters( 'activitypub_defer_signature_verification', false, $request );
$defer = \apply_filters( 'activitypub_defer_verify_signature', false, $request );

if ( $defer ) {
return $response;
return true;
}

if (
// POST-Requests are always signed.
// POST-Requests have to be always signed.
pfefferle marked this conversation as resolved.
Show resolved Hide resolved
'GET' !== $request->get_method() ||
// GET-Requests only require a signature in secure mode.
( 'GET' === $request->get_method() && use_authorized_fetch() )
) {
$verified_request = Signature::verify_http_signature( $request );
if ( \is_wp_error( $verified_request ) ) {
return new WP_Error(
'activitypub_signature_verification',
'activitypub_verify_signature',
$verified_request->get_error_message(),
array( 'status' => 401 )
);
}
}

return $response;
return true;
}

/**
Expand All @@ -155,7 +139,7 @@ public static function authorize_activitypub_requests( $response, $handler, $req
*
* @return mixed|WP_Error The response, error, or modified response.
*/
public static function validate_activitypub_requests( $response, $handler, $request ) {
public static function validate_requests( $response, $handler, $request ) {
if ( 'HEAD' === $request->get_method() ) {
return $response;
}
Expand Down
1 change: 1 addition & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ For reasons of data protection, it is not possible to see the followers of other
* Improved: Interactions moderation
* Improved: Compatibility with Akismet
* Improved: Comment type mapping for `Like` and `Announce`
* Improved: Signature verification for API endpoints
* Fixed: Empty `url` attributes in the Reply block no longer cause PHP warnings

= 4.4.0 =
Expand Down
22 changes: 11 additions & 11 deletions tests/includes/rest/class-test-inbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ public function set_up() {
*/
public function tear_down() {
\delete_option( 'permalink_structure' );
\add_filter( 'activitypub_defer_signature_verification', '__return_false' );
\add_filter( 'activitypub_defer_verify_signature', '__return_false' );
}

/**
* Test the inbox signature issue.
*/
public function test_inbox_signature_issue() {
\add_filter( 'activitypub_defer_signature_verification', '__return_false' );
\add_filter( 'activitypub_defer_verify_signature', '__return_false' );

$json = array(
'id' => 'https://remote.example/@id',
Expand All @@ -51,14 +51,14 @@ public function test_inbox_signature_issue() {
$response = \rest_do_request( $request );

$this->assertEquals( 401, $response->get_status() );
$this->assertEquals( 'activitypub_signature_verification', $response->get_data()['code'] );
$this->assertEquals( 'activitypub_verify_signature', $response->get_data()['code'] );
}

/**
* Test missing attribute.
*/
public function test_missing_attribute() {
\add_filter( 'activitypub_defer_signature_verification', '__return_true' );
\add_filter( 'activitypub_defer_verify_signature', '__return_true' );

$json = array(
'id' => 'https://remote.example/@id',
Expand All @@ -81,7 +81,7 @@ public function test_missing_attribute() {
* Test follow request.
*/
public function test_follow_request() {
\add_filter( 'activitypub_defer_signature_verification', '__return_true' );
\add_filter( 'activitypub_defer_verify_signature', '__return_true' );

$json = array(
'id' => 'https://remote.example/@id',
Expand All @@ -103,7 +103,7 @@ public function test_follow_request() {
* Test follow request global inbox.
*/
public function test_follow_request_global_inbox() {
\add_filter( 'activitypub_defer_signature_verification', '__return_true' );
\add_filter( 'activitypub_defer_verify_signature', '__return_true' );

$json = array(
'id' => 'https://remote.example/@id',
Expand All @@ -125,7 +125,7 @@ public function test_follow_request_global_inbox() {
* Test create request with a remote actor.
*/
public function test_create_request() {
\add_filter( 'activitypub_defer_signature_verification', '__return_true' );
\add_filter( 'activitypub_defer_verify_signature', '__return_true' );

// Invalid request, because of an invalid object.
$json = array(
Expand Down Expand Up @@ -165,7 +165,7 @@ public function test_create_request() {
* Test create request global inbox.
*/
public function test_create_request_global_inbox() {
\add_filter( 'activitypub_defer_signature_verification', '__return_true' );
\add_filter( 'activitypub_defer_verify_signature', '__return_true' );

// Invalid request, because of an invalid object.
$json = array(
Expand Down Expand Up @@ -205,7 +205,7 @@ public function test_create_request_global_inbox() {
* Test update request.
*/
public function test_update_request() {
\add_filter( 'activitypub_defer_signature_verification', '__return_true' );
\add_filter( 'activitypub_defer_verify_signature', '__return_true' );

$json = array(
'id' => 'https://remote.example/@id',
Expand Down Expand Up @@ -233,7 +233,7 @@ public function test_update_request() {
* Test like request.
*/
public function test_like_request() {
\add_filter( 'activitypub_defer_signature_verification', '__return_true' );
\add_filter( 'activitypub_defer_verify_signature', '__return_true' );

$json = array(
'id' => 'https://remote.example/@id',
Expand All @@ -255,7 +255,7 @@ public function test_like_request() {
* Test announce request.
*/
public function test_announce_request() {
\add_filter( 'activitypub_defer_signature_verification', '__return_true' );
\add_filter( 'activitypub_defer_verify_signature', '__return_true' );

$json = array(
'id' => 'https://remote.example/@id',
Expand Down
Loading