Skip to content
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
df1c13e
Add FAPI (Fediverse Auxiliary Service Provider) support
pfefferle Oct 10, 2025
803d3ba
Implement FASP registration and capability management
pfefferle Oct 10, 2025
f806c0a
Update REST route base and endpoint path
pfefferle Oct 10, 2025
d6420e7
Fix typo in registration endpoint URL
pfefferle Oct 10, 2025
bce4469
Fix REST route to include rest_base in path
pfefferle Oct 10, 2025
b6a9fff
Refactor FASP registration and admin classes
pfefferle Oct 15, 2025
35cde33
Use Activitypub signature verification for provider info
pfefferle Oct 15, 2025
af77faa
Refactor FASP to use Application RSA keypair for signing
pfefferle Oct 15, 2025
b2bda2b
Refactor FASP controller to use signature helper
pfefferle Oct 15, 2025
b068326
Change signature base and params methods to private
pfefferle Oct 15, 2025
c27b06a
Move FASP registrations admin UI to settings tab
pfefferle Oct 29, 2025
a46b3de
Refactor FASP capability auth to use signature verification
pfefferle Oct 29, 2025
ab3cafa
Add E2E tests for FASP controller REST API
pfefferle Oct 29, 2025
c1ec77c
Enforce FASP public key fingerprint and key matching
pfefferle Oct 29, 2025
8f80146
Update docs/fasp-registration.md
pfefferle Oct 29, 2025
743d5af
Update docs/fasp-registration.md
pfefferle Oct 29, 2025
61ded0a
Update docs/fasp-registration.md
pfefferle Oct 29, 2025
a210f0b
Refactor FASP registration handling and admin actions
pfefferle Oct 30, 2025
f177a56
Update includes/class-fasp.php
pfefferle Oct 30, 2025
9ba9050
Add sanitize_callback to REST API registration args
pfefferle Oct 30, 2025
e060736
Address PR review feedback for FASP implementation.
pfefferle Jan 20, 2026
59c0b8f
Simplify FASP keyId handling to match spec.
pfefferle Jan 20, 2026
396e265
Integrate FASP signature handling with existing system.
pfefferle Jan 20, 2026
a6fcefe
Add tests for Ed25519 signature verification and FASP integration.
pfefferle Jan 20, 2026
49961cf
Use multi-line comment syntax.
pfefferle Jan 20, 2026
8cd6713
Add FASP to supported federation protocols.
pfefferle Jan 20, 2026
4418739
Use Ed25519 for FASP signatures as required by spec.
pfefferle Jan 20, 2026
a1a5081
Remove FASP documentation files.
pfefferle Jan 20, 2026
666b78b
Add changelog entry.
pfefferle Jan 20, 2026
dbce571
Fix security issues and address Copilot feedback.
pfefferle Jan 20, 2026
24d109d
Fix docblock and Ed25519 alg parameter validation.
pfefferle Jan 20, 2026
d1c3b2d
Fix PHPCS issues in test file.
pfefferle Jan 20, 2026
c31a2da
Fix E2E tests to use valid Ed25519 public keys.
pfefferle Jan 21, 2026
2fa7b9a
Make FASP feature opt-in and improve admin UI.
pfefferle Jan 21, 2026
f0ef6d9
Enable FASP feature flag in tests.
pfefferle Jan 21, 2026
2a49821
Refactor FASP registration storage and improve feedback
pfefferle Jan 27, 2026
921b19e
Merge branch 'trunk' into fapi
pfefferle Jan 27, 2026
04f22cf
Refactor FASP capability management and add HTTPS validation
pfefferle Jan 27, 2026
ea1e4d5
Refactor FASP admin notices to use WordPress settings API
pfefferle Jan 27, 2026
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
4 changes: 4 additions & 0 deletions .github/changelog/add-fasp-support
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add support for auxiliary fediverse services like moderation tools and search providers.
1 change: 1 addition & 0 deletions FEDERATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The WordPress plugin largely follows ActivityPub's server-to-server specificatio
- [HTTP Signatures](https://swicg.github.io/activitypub-http-signature/)
- [NodeInfo](https://nodeinfo.diaspora.software/)
- [Interaction Policy](https://docs.gotosocial.org/en/latest/federation/interaction_policy/)
- [FASP](https://github.com/mastodon/fediverse_auxiliary_service_provider_specifications/) (Fediverse Auxiliary Service Provider)

## Supported FEPs

Expand Down
2 changes: 2 additions & 0 deletions activitypub.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function rest_init() {
( new Rest\Application_Controller() )->register_routes();
( new Rest\Collections_Controller() )->register_routes();
( new Rest\Comments_Controller() )->register_routes();
( new Rest\Fasp_Controller() )->register_routes();
( new Rest\Followers_Controller() )->register_routes();
( new Rest\Following_Controller() )->register_routes();
( new Rest\Inbox_Controller() )->register_routes();
Expand Down Expand Up @@ -75,6 +76,7 @@ function plugin_init() {
\add_action( 'init', array( __NAMESPACE__ . '\Comment', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Dispatcher', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Embed', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Fasp', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Link', 'init' ) );
Expand Down
348 changes: 348 additions & 0 deletions includes/class-fasp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
<?php
/**
* FASP integration class file.
*
* @package Activitypub
*/

namespace Activitypub;

/**
* FASP integration class.
*
* Handles FASP-related integrations that aren't part of the REST API,
* and provides registration management functionality.
*/
class Fasp {

/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'activitypub_pre_get_public_key', array( __CLASS__, 'get_public_key_for_server_id' ), 10, 2 );
}

/**
* Provide public key for FASP serverId lookups.
*
* This filter integrates FASP signature verification with the existing
* ActivityPub signature system. When a signature's keyId matches a
* registered FASP's serverId, we return the stored public key.
*
* FASP uses Ed25519 keys, so we return an array with type information
* that the signature verification system can use.
*
* @param resource|string|array|\WP_Error|null $public_key The current public key (null to continue lookup).
* @param string $key_id The key ID from the signature.
* @return resource|string|array|\WP_Error|null The public key or null to continue default lookup.
*/
public static function get_public_key_for_server_id( $public_key, $key_id ) {
// If another filter already provided a key, don't override.
if ( null !== $public_key ) {
return $public_key;
}

// Try to find a FASP registration matching this serverId.
$registration = self::get_registration_by_server_id( $key_id );

if ( ! $registration ) {
return null; // Not a FASP serverId, continue with default lookup.
}

// Check if FASP is approved.
if ( 'approved' !== $registration['status'] ) {
return new \WP_Error(
'fasp_not_approved',
'FASP registration is not approved',
array( 'status' => 403 )
);
}

// Return the stored public key.
if ( empty( $registration['fasp_public_key'] ) ) {
return new \WP_Error(
'fasp_no_public_key',
'FASP registration does not have a public key',
array( 'status' => 401 )
);
}

/*
* FASP uses Ed25519 keys stored as base64.
* Decode and return as Ed25519 key array for signature verification.
*/
$raw_key = base64_decode( $registration['fasp_public_key'] ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode

if ( false === $raw_key ) {
return new \WP_Error(
'fasp_invalid_key',
'FASP public key is not valid base64',
array( 'status' => 401 )
);
}

return array(
'type' => 'ed25519',
'key' => $raw_key,
);
}

/**
* Get registration by server ID.
*
* @param string $server_id The server ID from the FASP.
* @return array|null Registration data or null if not found.
*/
public static function get_registration_by_server_id( $server_id ) {
$registrations = self::get_registrations_store();

foreach ( $registrations as $registration ) {
if ( isset( $registration['server_id'] ) && $registration['server_id'] === $server_id ) {
return $registration;
}
}

return null;
}


/**
* Get all pending registration requests.
*
* @return array Array of registration requests.
*/
public static function get_pending_registrations() {
$registrations = self::get_registrations_store();
$pending = array();

foreach ( $registrations as $registration ) {
if ( 'pending' === $registration['status'] ) {
$pending[] = $registration;
}
}

// Sort by requested_at DESC.
usort(
$pending,
function ( $a, $b ) {
return strcmp( $b['requested_at'], $a['requested_at'] );
}
);

return $pending;
}

/**
* Get all approved registrations.
*
* @return array Array of approved registrations.
*/
public static function get_approved_registrations() {
$registrations = self::get_registrations_store();
$approved = array();

foreach ( $registrations as $registration ) {
if ( 'approved' === $registration['status'] ) {
$approved[] = $registration;
}
}

// Sort by approved_at DESC.
usort(
$approved,
function ( $a, $b ) {
return ( $b['approved_at'] ?? '' ) <=> ( $a['approved_at'] ?? '' );
}
);

return $approved;
}

/**
* Approve a registration request.
*
* @param string $fasp_id FASP ID.
* @param int $user_id User ID who approved.
* @return bool True on success, false on failure.
*/
public static function approve_registration( $fasp_id, $user_id ) {
$registrations = self::get_registrations_store();

if ( ! isset( $registrations[ $fasp_id ] ) ) {
return false;
}

$registrations[ $fasp_id ]['status'] = 'approved';
$registrations[ $fasp_id ]['approved_at'] = current_time( 'mysql', true );
$registrations[ $fasp_id ]['approved_by'] = $user_id;

return update_option( 'activitypub_fasp_registrations', $registrations, false );
}

/**
* Reject a registration request.
*
* @param string $fasp_id FASP ID.
* @param int $user_id User ID who rejected.
* @return bool True on success, false on failure.
*/
public static function reject_registration( $fasp_id, $user_id ) {
$registrations = self::get_registrations_store();

if ( ! isset( $registrations[ $fasp_id ] ) ) {
return false;
}

$registrations[ $fasp_id ]['status'] = 'rejected';
$registrations[ $fasp_id ]['approved_at'] = current_time( 'mysql', true );
$registrations[ $fasp_id ]['approved_by'] = $user_id;

return update_option( 'activitypub_fasp_registrations', $registrations, false );
}

/**
* Get registration by FASP ID.
*
* @param string $fasp_id FASP ID.
* @return array|null Registration data or null if not found.
*/
public static function get_registration( $fasp_id ) {
$registrations = self::get_registrations_store();

return isset( $registrations[ $fasp_id ] ) ? $registrations[ $fasp_id ] : null;
}

/**
* Delete a registration request.
*
* @param string $fasp_id FASP ID.
* @return bool True on success, false on failure.
*/
public static function delete_registration( $fasp_id ) {
$registrations = self::get_registrations_store();

if ( ! isset( $registrations[ $fasp_id ] ) ) {
return false;
}

unset( $registrations[ $fasp_id ] );

return update_option( 'activitypub_fasp_registrations', $registrations, false );
}

/**
* Generate public key fingerprint.
*
* @param string $public_key Base64 encoded public key.
* @return string SHA-256 fingerprint.
*/
public static function get_public_key_fingerprint( $public_key ) {
$decoded_key = base64_decode( $public_key ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
$hash = hash( 'sha256', $decoded_key, true );
return base64_encode( $hash ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
}

/**
* Get enabled capabilities for a FASP.
*
* @param string $fasp_id FASP ID.
* @return array Array of enabled capabilities.
*/
public static function get_enabled_capabilities( $fasp_id ) {
$capabilities = self::get_capabilities_store();
$enabled = array();

foreach ( $capabilities as $capability ) {
if ( $capability['fasp_id'] === $fasp_id && $capability['enabled'] ) {
$enabled[] = $capability;
}
}

return $enabled;
}

/**
* Check if a FASP has a specific capability enabled.
*
* @param string $fasp_id FASP ID.
* @param string $identifier Capability identifier.
* @param int $version Capability version.
* @return bool True if capability is enabled, false otherwise.
*/
public static function is_capability_enabled( $fasp_id, $identifier, $version ) {
$capabilities = self::get_capabilities_store();
$capability_key = $fasp_id . '_' . $identifier . '_v' . $version;

return isset( $capabilities[ $capability_key ] ) && $capabilities[ $capability_key ]['enabled'];
}

/**
* Retrieve registrations, ensuring the option exists, is non-autoloaded, and sanitized.
*
* @return array
*/
private static function get_registrations_store() {
$registrations = get_option( 'activitypub_fasp_registrations', null );

if ( null === $registrations ) {
add_option( 'activitypub_fasp_registrations', array(), '', 'no' );
return array();
}

if ( ! is_array( $registrations ) ) {
$registrations = array();
}

return self::sanitize_registration_records( $registrations );
}

/**
* Remove sensitive data from stored registrations.
*
* @param array $registrations Registration data.
* @return array Sanitized registrations.
*/
private static function sanitize_registration_records( array $registrations ) {
$modified = false;

foreach ( $registrations as $fasp_id => $registration ) {
if ( isset( $registration['server_private_key'] ) ) {
unset( $registration['server_private_key'] );
$registrations[ $fasp_id ] = $registration;
$modified = true;
}

if ( isset( $registration['fasp_public_key'] ) && empty( $registration['fasp_public_key_fingerprint'] ) ) {
$registration['fasp_public_key_fingerprint'] = self::get_public_key_fingerprint( $registration['fasp_public_key'] );
$registrations[ $fasp_id ] = $registration;
$modified = true;
}
}

if ( $modified ) {
update_option( 'activitypub_fasp_registrations', $registrations, false );
}

return $registrations;
}

/**
* Retrieve capabilities store ensuring the option exists and is non-autoloaded.
*
* @return array
*/
private static function get_capabilities_store() {
$capabilities = get_option( 'activitypub_fasp_capabilities', null );

if ( null === $capabilities ) {
add_option( 'activitypub_fasp_capabilities', array(), '', 'no' );
return array();
}

if ( ! is_array( $capabilities ) ) {
return array();
}

return $capabilities;
}
}
Loading
Loading