Skip to content

Document signing companion HTTP requests - #3556

Merged
pfefferle merged 10 commits into
Automattic:trunkfrom
Jiwoon-Kim:docs/http-request-signing-extension
Jul 29, 2026
Merged

Document signing companion HTTP requests#3556
pfefferle merged 10 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

This comment was marked as outdated.

@Jiwoon-Kim

This comment was marked as outdated.

@Jiwoon-Kim

This comment was marked as outdated.

@pfefferle

Copy link
Copy Markdown
Member

I know I mentioned the FEDERATION.md, but I think (because the FEDERATION.md is about generics) we should have a dedicated doc in the docs folder instead. What do you think?

@Jiwoon-Kim

Copy link
Copy Markdown
Contributor Author

Yes, agreed. I moved the companion signing/delivery material out of FEDERATION.md into docs/external-activity-delivery.md, and added it to the docs index. FEDERATION.md is generic again.

Separately, I am currently implementing Question support in the Axismundi companion stack: structured Question/options storage, ActivityStreams Question projection, and a read-only poll view. That work is intentionally outside this documentation-only PR.

@Jiwoon-Kim

This comment was marked as off-topic.

@Jiwoon-Kim

This comment was marked as outdated.

@Jiwoon-Kim

This comment was marked as off-topic.

@pfefferle pfefferle left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for moving this out of FEDERATION.md. Two structural notes and one correction to the prose, suggested inline where GitHub lets me.

Placement. docs/developer-docs.md already exists for exactly this audience. It opens by saying it is for developers building a complementary plugin and documents the available hooks, so a companion signing seam belongs there as a section rather than as a new top-level file. docs/readme.md currently indexes only the user-facing FAQ and How-To sections and does not link developer-docs.md at all, so a one-item "Extension points" heading there ends up promoting this single seam above the general developer documentation.

Filename. "External Activity delivery" reads as though the plugin delivers something. The doc correctly says the companion owns recipient selection, the queue and retries, so what the plugin actually contributes is the signature. Under a "Signing Outbound Requests" heading that mismatch goes away.

One correction worth making. The bullet about not persisting key material understates what happens. key_id and private_key deliberately stay in the request arguments after signing, because Signature::maybe_double_knock() is hooked on http_response and needs them to re-sign in the draft format and retry when an RFC 9421 request comes back 4xx (see includes/class-signature.php:214). They are therefore readable by any other http_request_args or http_response callback on the site, and by anything that logs request arguments. A companion cannot prevent that, so the doc should say so plainly instead of implying that careful handling on the caller's side is enough.

Here is the section to add to docs/developer-docs.md, with - [Signing Outbound Requests](#signing-outbound-requests) added to its Table of Contents:

## Signing Outbound Requests

A companion plugin that constructs and delivers its own Activities can reuse the plugin's HTTP-signing implementation, without adopting the Outbox model. Pass the sender's public key identifier and private key as the `key_id` and `private_key` request arguments, and the plugin's `http_request_args` filter adds the signature headers before WordPress sends the request:

```php
$response = wp_safe_remote_post(
	$recipient_inbox,
	array(
		'body'        => wp_json_encode( $activity ),
		'headers'     => array( 'Content-Type' => 'application/activity+json' ),
		'data_format' => 'body',
		'key_id'      => $sender_key_id,
		'private_key' => $sender_private_key,
	)
);
```

The plugin chooses the signature format itself, based on the site's RFC 9421 setting and whether the recipient is known to support it. When an RFC 9421 request is answered with a 4xx, the plugin re-signs it in the older draft format and retries once. Callers do not select the format and should not depend on which one a given request uses.

Both arguments stay in the request arguments after signing, because that retry needs them again once the response comes back. They are therefore visible to any other callback on `http_request_args` and `http_response`, and to anything on the site that logs request arguments. Resolve key material only for the duration of the send, and treat the request arguments as readable by other plugins.

The companion remains responsible for:

- validating recipient URLs;
- selecting recipients;
- owning its delivery queue and retry policy; and
- resolving private key material only while sending, without persisting it in transport rows or logs.

Comment thread docs/readme.md Outdated
Comment thread docs/readme.md
Comment thread docs/external-activity-delivery.md Outdated
@Jiwoon-Kim

Jiwoon-Kim commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the structural and security review. I have moved the signing material into docs/developer-docs.md, removed the separate document and README promotion, and documented the RFC 9421 4xx fallback plus the fact that key_id and private_key remain visible in request arguments for the retry path.

Companion-stack update: the current prereleases are ActivityPub Bridge 0.0.23, Actors 0.0.57, Object Projections 0.0.59, Note 0.0.32, and Emoji 0.1.3. They remain prereleases.

The boundary documented here is now the one the companion uses in practice: it owns Activity construction, recipient validation/selection, its delivery table, queue, and retry policy; the stock plugin remains responsible for HTTP signing and verified Inbox parsing. The companion does not add a parallel Outbox or signature implementation.

Since the prior update, the stack has added URI-keyed remote Actor/Object caches and public cache-only views; lifecycle-backed Note and Article projection; plaintext inline mentions that are resolved into ActivityStreams Mention tags without rewriting the authored body; and FEP-9098 local/remote custom-emoji handling. The latter authoring and presentation work is deliberately companion-side, not a request for another upstream transport API.

The remaining work is mostly interoperability and product work: broader public-instance coverage for the newer authoring representations, profile-editing UX, and continued reconciliation of cached remote representations. I will report concrete peer behavior separately rather than treating a local implementation as interoperability proof.

@pfefferle pfefferle self-assigned this Jul 29, 2026
@pfefferle pfefferle added the Skip Changelog Disables the "Changelog Updated" action for PRs where changelog entries are not necessary. label Jul 29, 2026
@pfefferle
pfefferle merged commit 814614a into Automattic:trunk Jul 29, 2026
5 checks passed
@Jiwoon-Kim

Copy link
Copy Markdown
Contributor Author
image image

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

Labels

Docs OSS Citizen Skip Changelog Disables the "Changelog Updated" action for PRs where changelog entries are not necessary.

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