Skip to content

Document signing companion HTTP requests#3556

Open
Jiwoon-Kim wants to merge 3 commits into
Automattic:trunkfrom
Jiwoon-Kim:docs/http-request-signing-extension
Open

Document signing companion HTTP requests#3556
Jiwoon-Kim wants to merge 3 commits into
Automattic:trunkfrom
Jiwoon-Kim:docs/http-request-signing-extension

Conversation

@Jiwoon-Kim

@Jiwoon-Kim Jiwoon-Kim commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What

Document the existing companion-plugin signing surface in FEDERATION.md.

A caller can pass key_id and private_key to wp_safe_remote_post() and reuse the plugin's existing http_request_args signature implementation without adopting the official Outbox model.

The new section also makes the ownership boundary explicit: the caller owns recipient validation, recipient selection, queueing, and retries, and should resolve private key material only while sending.

Why

This follows the maintainer guidance in #3548. The capability already exists and works with stock 9.0.2, but it was not documented as an extension point. A small documentation contract is preferable to adding a parallel delivery API.

Resolution of #3533

No new domain-persistence or delivery API is required. The inbound half already composes from the existing post-verification controller actions (activitypub_inbox and activitypub_inbox_shared), behavior-level handler registration through activitypub_register_handlers, and conditional activitypub_skip_inbox_storage claiming. A companion can therefore consume verified traffic and suppress duplicate default domain persistence without replacing signature verification.

For outbound traffic, the companion owns recipient selection, its private spool, and retry policy. This PR documents the remaining supported boundary: reusing the official http_request_args signer with transient key_id and private_key request arguments. Together these existing seams satisfy the inbound and outbound requirements described in #3533 while keeping the official plugin authoritative for protocol verification and HTTP signing.

Validation

  • Markdown diff check passes.
  • A companion implementation using this exact request shape was tested against both stock 9.0.2 and the former integration fork; the official signer produced the outbound HTTP signature in both configurations.

Changelog

  • Automatically create a changelog entry from the details below.
Changelog Entry Details

Significance

  • Patch
  • Minor
  • Major

Type

  • Added - for new features
  • Changed - for changes in existing functionality
  • Deprecated - for soon-to-be removed features
  • Removed - for now removed features
  • Fixed - for any bug fixes
  • Security - in case of vulnerabilities

Message

Document how companion plugins can reuse the existing outbound HTTP signing extension point.

Closes #3533

@matticbot

Copy link
Copy Markdown
Contributor

👋 Thanks for your contribution!

I see you have provided all the required changelog information in your PR description. To complete the process, you'll need to create a changelog file in your branch.

Please create a file named .github/changelog/3556-from-description with this content:

Significance: patch
Type: changed

Document how companion plugins can reuse the existing outbound HTTP signing extension point.

You can do this by either:

  1. Running composer changelog:add in your local repository, or
  2. Creating the file manually with the content above

Once you've committed and pushed the file to your PR branch, the checks will pass automatically.

@Jiwoon-Kim

Copy link
Copy Markdown
Contributor Author

Thanks again for pointing us back toward the existing composition and signing surfaces. I have now released Axismundi ActivityPub Bridge 0.0.16 Alpha, which works with stock ActivityPub 9.0.2 and no longer depends on the integration fork.

The transport spool was also changed from the provisional private CPT to a dedicated delivery table rather than simply copying ap_outbox into another CPT. There were two concrete reasons:

  1. A CPT split the transport state between post_status and post meta, recreating ambiguous ownership of the state machine.
  2. add_post_meta( ..., true ) is not a database-level worker claim because wp_postmeta has no unique (post_id, meta_key) constraint. Two workers can pass its check-then-insert window.

The released Bridge instead uses one site-prefixed delivery table with a unique Activity URI hash, one status field, a conditional UPDATE compare-and-swap claim, claim heartbeats, bounded retries, and no persisted private key material. Legacy experimental ap_outbox rows and provisional Bridge CPT rows are migrated non-destructively and linked to the new jobs. The Bridge suite passes 74 assertions, and the stock-plugin composition/signing subset passes 52 assertions.

Current companion releases:

At this point the companion stack includes FEP-b2b8 Article projection, an FEP-1311-based standalone media rendition application, and Like/Undo plus Announce/Undo. FEP-044f quote authorization work is in progress. Once that layer is complete, I plan to run the real Article Create and end-to-end interaction scenarios against public Mastodon/Misskey instances and report the observed interoperability results.

@Jiwoon-Kim

Jiwoon-Kim commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up interoperability update:

The FEP-044f consent and authorization path is now implemented in the Axismundi companion stack. This includes per-Article canQuote policy projection, URI-keyed and idempotent QuoteRequest Accept/Reject decisions, dereferenceable QuoteAuthorization objects with active/revoked 200/410 behavior, privacy-minimal revocation Deletes, and quote-relation indexing that recognizes both FEP-044f and Misskey-compatible quote fields without treating a compatibility alias as consent evidence.

I cannot yet honestly mark the complete Mastodon quote round trip as passed. During one deployment the Actor document was briefly cached without its publicKey. mastodon.social cached that recent keyless Actor and currently rejects the generated Follow/Quote Accept activities with 401 Public key not found for key ...#main-key, even though the canonical Actor document now advertises that exact key again.

Bridge 0.0.20 closes that deployment race: inbox, endpoints, and publicKey are now advertised as one atomic bundle, the WebFinger ActivityStreams self link is withheld while the signing key cannot be projected, and queued delivery is held rather than discarded during that readiness window. The exact peer key-discovery 401 now uses a bounded 5m -> 1h -> 24h -> 48h recovery schedule before dead-lettering; generic authentication failures remain terminal. I will repeat the final QuoteAuthorization test after the remote Actor cache becomes stale.

As an independent control, the same Actor identity, key material, stock ActivityPub signing hook, and Bridge delivery table successfully completed Follow/Accept and Announce/repost interoperability against uri.life. That instance does not expose quote authoring, so it could not exercise FEP-044f, but it confirms that the stock signing/composition path works outside the mastodon.social cache condition.

Current relevant releases:

I will post the final mastodon.social QuoteRequest -> Accept -> QuoteAuthorization result once that cache no longer masks the corrected Actor representation.

@Jiwoon-Kim

Copy link
Copy Markdown
Contributor Author

Final interoperability update:

The outbound FEP-044f Quote path is now complete in the Axismundi companion stack, including the held Create lifecycle for remote consent:

  • a local Note quoting a remote object emits a QuoteRequest with the finalized Note inline;
  • the Note remains non-public and no Create is emitted while approval is pending;
  • a verified Accept plus QuoteAuthorization opens the first Create, with quoteAuthorization attached;
  • self-quotes emit immediately without fabricated authorization, while local-other and remote targets use the authorization path;
  • a terminal Reject remains immutable, and an explicit target removal/re-addition creates a new QuoteRequest generation for a retry.

The projection emits the FEP-044f quote member and the Misskey compatibility aliases, while keeping policy declaration separate from approval evidence. Existing unset content remains fail-closed; only newly authored objects receive the public/anyone default.

I have now verified the completed flow against both public implementations:

The generic Bridge delivery path is also covered end to end for outbound QuoteRequest, automatic Accept, and revocation Delete. The full quote path no longer requires Quote-specific transport code.

Current releases:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Supported S2S transport extension points without default domain persistence

3 participants