Skip to content

feat(activitypub,vc): outbound FEP-8b32 object integrity proofs - #576

Merged
davidwkeith merged 3 commits into
mainfrom
claude/trusting-hawking-3eawag
Oct 28, 2026
Merged

davidwkeith merged 3 commits into
mainfrom
claude/trusting-hawking-3eawag

Conversation

@davidwkeith

@davidwkeith davidwkeith commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

Outbound FEP-8b32 object integrity proofs. This is item 4 from the SiliconBeest comparison, following double-knock and the circuit breaker in #574.

  • Config: new optional integrityKey, an Ed25519 private JWK (kty: "OKP", crv: "Ed25519", a 32-byte x and d), stored as a secret binding. A malformed key fails at startup. Without it, behaviour is unchanged.
  • Actor document: publishes the public half as a FEP-521a assertionMethod Multikey at <actor>#ed25519-key, and adds the multikey/v1 and data-integrity/v2 contexts.
  • Delivery: the actor DO adds a top-level eddsa-jcs-2022 DataIntegrityProof (proofPurpose: "assertionMethod") at send time, only when the activity's actor is this actor, it has no proof yet, and its id is not a fragment IRI (FEP-8b32 says those SHOULD NOT carry proofs). Forwarded and relayed activities go out unchanged. Each attempt, retries included, is signed afresh. The key is stored in the DO's SQLite kv table like privateKeyPem, and is cleared when it is removed from config.
  • Resilience: a key that passes the startup shape check but fails to import in the DO is logged once per key (activitypub.integrity.key_invalid), and deliveries continue without a proof rather than stalling the queue.
  • Reuse: the proof pipeline (JCS, SHA-256, Ed25519, Multikey) already existed in @dwk/vc. It is now also exported as the @dwk/vc/data-integrity subpath (the same pattern as @dwk/ldn/discovery).

New dependency @dwk/activitypub → @dwk/vc. There's no tracking issue for this edge. I chose it over copying ~400 lines of crypto (JCS, base58, Multikey, the proof pipeline) into activitypub. The subpath entry imports only datetime.ts, jcs.ts and multibase.ts, so none of vc's endpoint, D1 or did:web code ships with activitypub. I checked this with an esbuild bundle of @dwk/activitypub (using the cloudflare:workers alias from @dwk/server's bundle.mjs): it contains eddsa-jcs-2022 and no did:web: or status-list code. pnpm --filter @dwk/server bundle also still succeeds. The alternative is to extract a separate @dwk/data-integrity library; that would be a new package and needs its own issue.

Not in this PR: verifying proofs on inbound activities. Doing that properly means resolving assertionMethod and binding it to the activity's origin (FEP-fe34), and the payoff (accepting forwarded activities on the proof alone) changes the inbox's authentication model. Inbound authentication remains the HTTP signature.

Packages affected

@dwk/activitypub, @dwk/vc

Checklist

  • Read the relevant spec(s) under spec/packages/ and updated them if
    behaviour changed: spec/packages/activitypub.md S2S section. spec/packages/vc.md needs no change; the new subpath is documented in the vc README and CLAUDE.md.
  • Added/updated colocated tests (src/*.test.ts). In integrity.test.ts: key validation (shape, x and seed length), the Multikey matching the JWK x, a proof that verifies through @dwk/vc's verifyProof and fails after tampering, and no signing for another actor's, already-proofed, fragment-id or unparseable activities. In index.test.ts: assertionMethod on the actor doc (and absent without a key), startup rejection of a bad key, the DO proofing its own delivery but not a forwarded one, unsigned delivery when the stored key won't import, and signing stopping once the key is removed.
  • Ran the local CI gate: pnpm lint && pnpm format:check && pnpm typecheck && pnpm build && pnpm test (3304 passed)
  • Added a changeset (pnpm changeset) if this touches a publishable
    package (one each for activitypub and vc)
  • Updated catalog.json / conformance/status.json if this adds a new
    mountable worker or changes conformance status: not applicable, no new worker; pnpm catalog:check passes

🤖 Generated with Claude Code

https://claude.ai/code/session_015LRGjmHE99WESN5HBERxSj

Add an optional `integrityKey` (Ed25519 private JWK). The actor document
publishes it as a FEP-521a assertionMethod Multikey, and the DO adds an
eddsa-jcs-2022 DataIntegrityProof to each activity this actor authors at
delivery time; forwarded activities are left untouched. The proof
pipeline comes from @dwk/vc, now also exported as the
`@dwk/vc/data-integrity` subpath.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015LRGjmHE99WESN5HBERxSj

@davidwkeith davidwkeith left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Review

Overall this is a clean, well-scoped PR. It follows CONTRIBUTING.md: the title and commit use Conventional Commits with the right scope, the PR body keeps the template headings with a reason on the unchecked item, both packages have changesets, the spec and READMEs are updated, and tests are colocated. CI was still running when I reviewed, so the "3302 passed" claim isn't independently confirmed yet.

Should fix

  1. A bad stored key can wedge all deliveries (packages/activitypub/src/object.ts, #processDeliveries). await this.#integritySigner() runs outside the per-row try. assertIntegrityKey only checks the JWK's shape, not that d is a valid Ed25519 seed or matches x. If importSigner throws, say on a corrupt kv value, the alarm throws before any delivery is attempted. The actor then stops delivering everything, including forwarded activities that need no proof. Either validate by importing the key at startup (so "fails loudly" holds), or catch the import error here, log it through @dwk/log, and deliver unsigned. Add a test for whichever you pick.

  2. New workspace dependency (packages/activitypub/package.json). CONTRIBUTING.md's ground rules say to open an issue before adding dependencies or making architectural changes. @dwk/activitypub → @dwk/vc is a new inter-package edge. The PR doesn't reference an issue, and the description only cites #574 as context. Link the discussion, or note why the subpath export (which keeps the credential and did:web code out of the bundle) settles it. Also confirm pnpm --filter @dwk/server bundle still resolves the @dwk/vc/data-integrity subpath, since @dwk/server bundles activitypub.

Nits

  1. Per-package CLAUDE.md not updated. packages/vc/CLAUDE.md doesn't mention the new ./data-integrity subpath export. packages/activitypub/CLAUDE.md doesn't list FEP-8b32 or integrityKey among its key constraints. The root CLAUDE.md says each package's CLAUDE.md describes it, so add a line to each. The same applies to the @dwk/ldn/discovery precedent, which the root file calls out.

  2. Duplicated base64url logic. integrity.test.ts hand-rolls the bytes→base64url conversion to compare against jwk.x. Cleaner would be to assert decodeMultikey(...).rawPublicKey equals base64urlToBytes(jwk.x), exporting the helper if needed.

  3. Retry re-signs every attempt. signOwnActivity runs per retry, so created changes each time. That's harmless for verification, but the spec line "the proof is applied at send time, so it is never stored" could say so explicitly.

  4. Secret handling. The private JWK is now persisted in DO SQLite kv and re-sent on every internal request in the forwarded-config header, like privateKeyPem. That's consistent with the existing pattern, but the README's JSON.parse(env.AP_INTEGRITY_KEY) example should say to store it as a secret binding, not a plain var.

No blocking spec or composition-contract violations found. The package stays config-injected, nothing reads the global environment, and no bare fetch was added.


Generated by Claude Code

Deliver unsigned (logging activitypub.integrity.key_invalid once per
key) when the stored integrity key fails to import, instead of letting
the alarm throw and stall every delivery. Also check the seed length at
startup, reuse base64urlToBytes in tests, note per-attempt re-signing in
the spec, document the secret-binding requirement, and describe the new
subpath and FEP-8b32 constraints in the per-package CLAUDE.md files.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015LRGjmHE99WESN5HBERxSj
@davidwkeith
davidwkeith enabled auto-merge (squash) October 28, 2026 22:48
… typecheck

Both packages path-map @dwk/activitypub to its source, so they must also
map the new `@dwk/vc/data-integrity` subpath it imports; CI typechecks
before building, when vc's dist does not exist yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015LRGjmHE99WESN5HBERxSj
@davidwkeith
davidwkeith merged commit e92aa5b into main Oct 28, 2026
9 checks passed
@davidwkeith
davidwkeith deleted the claude/trusting-hawking-3eawag branch October 28, 2026 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants