Document signing companion HTTP requests - #3556
Conversation
|
👋 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 You can do this by either:
Once you've committed and pushed the file to your PR branch, the checks will pass automatically. |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
|
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? |
|
Yes, agreed. I moved the companion signing/delivery material out of Separately, I am currently implementing |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as off-topic.
This comment was marked as off-topic.
pfefferle
left a comment
There was a problem hiding this comment.
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.|
Thanks for the structural and security review. I have moved the signing material into 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 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. |


What
Document the existing companion-plugin signing surface in
FEDERATION.md.A caller can pass
key_idandprivate_keytowp_safe_remote_post()and reuse the plugin's existinghttp_request_argssignature 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_inboxandactivitypub_inbox_shared), behavior-level handler registration throughactivitypub_register_handlers, and conditionalactivitypub_skip_inbox_storageclaiming. 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_argssigner with transientkey_idandprivate_keyrequest 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
Changelog
Changelog Entry Details
Significance
Type
Message
Document how companion plugins can reuse the existing outbound HTTP signing extension point.
Closes #3533