Skip to content

Sign portable compound activities with one proof - #1073

Merged
dahlia merged 1 commit into
fedify-dev:mainfrom
dahlia:fep-ef61/prevent-compound-producers
Sep 26, 2026
Merged

dahlia merged 1 commit into
fedify-dev:mainfrom
dahlia:fep-ef61/prevent-compound-producers

Conversation

@dahlia

@dahlia dahlia commented Sep 26, 2026

Copy link
Copy Markdown
Member

Closes #1045.

Fedify's inbox accepts exactly one direct proof per map in a document that contains FEP-ef61 portable objects (#1041). The signer created one proof per Ed25519 key, so an actor with several keys produced activities that Fedify itself rejected.

Key selection

When the outgoing JSON contains a map identified by an ap: or ap+ef61: URI, Fedify now creates at most one proof. With several Ed25519 keys, a portable activity is signed by the one key whose ID is a DID URL for the activity's DID. FEP-ef61 accepts no proof from any other key.

Fedify throws a TypeError before delivery or enqueueing if no key matches, several keys match, or a non-portable activity embeds portable objects. In that last case, key order cannot tell which key the application meant, so Fedify does not fall back to the first one; callers pass explicit sender keys or pre-sign with signObject(). An activity that already has a proof is left alone, even when sent by actor identifier, which used to append more proofs.

Keys are matched by the ID the proof will name, not by ActorKeyPair.keyId. Dispatched keys sign as …#multikey-N, which is never a DID URL, so a portable activity with several dispatched Ed25519 keys always fails.

The three signing sites in packages/fedify/src/federation/middleware.ts now share one helper in packages/fedify/src/federation/outgoing-proof.ts. It serializes the activity only when there are two or more candidate keys, so the single-key path skips that serialization.

Final guard

Before delivery or enqueueing, Fedify also rejects a document with portable objects if any map carries a proof set, such as an object signed twice with signObject(). The fanout path runs this check before enqueueing, so the error reaches the caller instead of the worker.

Both JSON scans run without the inbox's traversal limits. Reusing the bounded inbox scan would let a limit hit hide a later proof array, and would make large ordinary activities look compound. Documents without portable objects are signed exactly as before, proof sets included.

Tests

The new tests cover immediate delivery, the outbox queue, forced fanout, the fanout worker, and the key pairs dispatcher. One delivers a Fedify-produced portable activity to a Fedify inbox and expects 202 Accepted. With both checks disabled, nine tests fail, including that one. The tests pass on Deno, Node.js, and Bun.

Not covered

That end-to-end test uses Ed25519 keys only. With an RSA key, Fedify attaches a Linked Data Signature, and the inbox's compound check hashes the signature property along with the document, so the activity is rejected. FEP-8b32 says the Linked Data Signature must be removed before verifying proofs. That inbox bug is outside #1045 and needs a separate PR.

Fedify's activity signer created one Object Integrity Proof for each
Ed25519 key, so a sender with several Ed25519 keys emitted a proof set.
The map-local compound-proof profile that Fedify's inbox applies to
documents containing FEP-ef61 portable maps accepts exactly one direct
proof per map, so Fedify rejected such activities from Fedify itself.

The three places that sign outgoing activities now share one helper
with an explicit rule for documents that contain portable maps:

- An activity that already carries a proof is kept as is, including
  when it is sent by actor identifier.
- A single Ed25519 key signs the activity, as before.
- With several Ed25519 keys, a portable activity is signed only by the
  key whose ID is a DID URL for the activity's DID, the only proof
  FEP-ef61 accepts.  If no key or several keys qualify, or a
  non-portable activity embeds portable maps, sendActivity() rejects
  with an actionable TypeError before delivering or enqueueing.

A final check also refuses to deliver or enqueue a document with
portable maps in which any map carries a proof set, for example after
signObject() has been called twice.  The producer-side scans are
unbounded, so neither check depends on the inbox's traversal limits.
Activities without portable maps are signed exactly as before.

Fixes fedify-dev#1045
fedify-dev#288
fedify-dev#1041

Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Codex:gpt-6-astra
@dahlia dahlia added this to the Fedify 2.4 milestone Sep 26, 2026
@dahlia dahlia self-assigned this Sep 26, 2026
@dahlia dahlia added component/federation Federation object related component/signatures OIP or HTTP/LD Signatures related activitypub/interop Interoperability issues labels Sep 26, 2026
@netlify

netlify Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for fedify-json-schema canceled.

Name Link
🔨 Latest commit 8931c9d
🔍 Latest deploy log https://app.netlify.com/projects/fedify-json-schema/deploys/6ab7ef34a5fb250007fa0224

@coderabbitai

coderabbitai Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 95d9808c-ff12-4705-8567-f96566de6298

📥 Commits

Reviewing files that changed from the base of the PR and between fcf88bf and 8931c9d.

📒 Files selected for processing (8)
  • CHANGES.md
  • changes.d/fedify/compound-proof-key-selection.md
  • docs/manual/send.md
  • packages/fedify/src/federation/middleware.ts
  • packages/fedify/src/federation/outgoing-proof.test.ts
  • packages/fedify/src/federation/outgoing-proof.ts
  • packages/fedify/src/sig/compound-proof-producer.test.ts
  • packages/fedify/src/sig/compound-proof.ts

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

Outgoing activity signing now selects a single eligible Ed25519 key for compound portable activities and rejects unsupported proof shapes. The send paths validate serialized activities before delivery or fanout. The manual and changelog describe the signing and rejection rules.

Changes

Compound portable activity signing

Layer / File(s) Summary
Portable object and proof-shape checks
packages/fedify/src/sig/compound-proof.ts, packages/fedify/src/sig/compound-proof-producer.test.ts
Added traversal helpers to detect portable objects and locate unsupported proof values. Tests cover traversal exclusions, repeated references, and JSON Pointer paths.
Outgoing proof selection
packages/fedify/src/federation/outgoing-proof.ts, packages/fedify/src/federation/outgoing-proof.test.ts, docs/manual/send.md, changes.d/fedify/compound-proof-key-selection.md, CHANGES.md
Added signing rules for portable and ordinary activities. Tests cover key selection, existing proofs, and proof-set rejection. The manual and changelog describe these rules.
Delivery and fanout integration
packages/fedify/src/federation/middleware.ts, packages/fedify/src/federation/outgoing-proof.test.ts
Send paths use the signing helper and validate serialized activities before delivery or fanout. Tests cover queued delivery, actor-dispatched keys, and inbox receipt.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant ContextImpl
  participant signOutgoingActivity
  participant assertSupportedCompoundProofShape
  participant Fanout
  ContextImpl->>signOutgoingActivity: Sign activity with candidate keys
  signOutgoingActivity-->>ContextImpl: Return signed activity and proof state
  ContextImpl->>assertSupportedCompoundProofShape: Validate serialized activity
  ContextImpl->>Fanout: Enqueue validated activity JSON-LD
Loading

Merge Risk: ⚪ Minimal · up to 8931c

No actionable merge-blocking issue remains in the supplied evidence; proceed with normal validation.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 8931c

The new rules reduce ambiguous signing of portable activities, but they affect every outgoing delivery mode. The available evidence does not fully establish how receiving inboxes handle every final signed payload.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The affected boundary is outgoing activity signing for recipients selected by the application. Immediate delivery, fanout, and ordinary outbox delivery consume the resulting proof-bearing document; the evidence does not establish a new source of signing keys or broader authority.

Trust Boundaries and Controls

  • observed — The multi-key branch checks DID origin before signing, while pre-existing proofs are preserved and subjected to a separate shape check. Neither preservation nor that check establishes cryptographic validity; proof trust remains distinct from outgoing shape validation.

Resilience and Maintainability Implications

  • observed — Compound-proof shape rejection occurs before fanout enqueueing or direct delivery. An RSA Linked Data signature can subsequently be attached to the document in the delivery path, so the shape check does not inspect the final serialized payload.

Hardening Proposals

  • proposed — Check portable DID/key identity for the single-key route as well as the multi-key route, and verify receiver behavior against the final payload when an RSA signature is also present. These checks would clarify existing caller and receiver assumptions; they are not established PR regressions.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.84% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 5 files. (3 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: portable compound activities use one proof.
Description check ✅ Passed The description directly explains the signing changes, key-selection rules, rejection cases, tests, and scope.
Linked Issues check ✅ Passed Issue #1045 has coding requirements. The new signOutgoingActivity keeps one-key signing direct, selects exactly one Ed25519 key whose DID URL matches a portable activity, and raises actionable `Type…
Out of Scope Changes check ✅ Passed The changes stay within Issue #1045. The signing helper, compound-proof detection, middleware integration, tests, and documentation all support portable-map proof selection or rejection. No unrelated …
Full details: Docstring Coverage

Explanation

Docstring coverage is 36.84% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 5 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@dahlia

dahlia commented Sep 26, 2026

Copy link
Copy Markdown
Member Author

@codex review

@codecov

codecov Bot commented Sep 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.50495% with 1 line in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
packages/fedify/src/federation/middleware.ts 97.67% 0 Missing and 1 partial ⚠️
Files with missing lines Coverage Δ
packages/fedify/src/federation/outgoing-proof.ts 100.00% <100.00%> (ø)
packages/fedify/src/sig/compound-proof.ts 90.52% <100.00%> (+1.60%) ⬆️
packages/fedify/src/federation/middleware.ts 84.06% <97.67%> (+<0.01%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dahlia
dahlia merged commit e6b3de3 into fedify-dev:main Sep 26, 2026
25 checks passed
@dahlia
dahlia deleted the fep-ef61/prevent-compound-producers branch September 26, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

activitypub/interop Interoperability issues component/federation Federation object related component/signatures OIP or HTTP/LD Signatures related

Development

Successfully merging this pull request may close these issues.

Prevent compound producers from emitting unsupported proof arrays

1 participant