Repository navigation
feat(activitypub,vc): outbound FEP-8b32 object integrity proofs - #576
Conversation
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
left a comment
There was a problem hiding this comment.
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
-
A bad stored key can wedge all deliveries (
packages/activitypub/src/object.ts,#processDeliveries).await this.#integritySigner()runs outside the per-rowtry.assertIntegrityKeyonly checks the JWK's shape, not thatdis a valid Ed25519 seed or matchesx. IfimportSignerthrows, say on a corruptkvvalue, 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. -
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/vcis 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 anddid:webcode out of the bundle) settles it. Also confirmpnpm --filter @dwk/server bundlestill resolves the@dwk/vc/data-integritysubpath, since@dwk/serverbundles activitypub.
Nits
-
Per-package
CLAUDE.mdnot updated.packages/vc/CLAUDE.mddoesn't mention the new./data-integritysubpath export.packages/activitypub/CLAUDE.mddoesn't list FEP-8b32 orintegrityKeyamong 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/discoveryprecedent, which the root file calls out. -
Duplicated base64url logic.
integrity.test.tshand-rolls the bytes→base64url conversion to compare againstjwk.x. Cleaner would be to assertdecodeMultikey(...).rawPublicKeyequalsbase64urlToBytes(jwk.x), exporting the helper if needed. -
Retry re-signs every attempt.
signOwnActivityruns per retry, socreatedchanges 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. -
Secret handling. The private JWK is now persisted in DO SQLite
kvand re-sent on every internal request in the forwarded-config header, likeprivateKeyPem. That's consistent with the existing pattern, but the README'sJSON.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
… 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
Summary
Outbound FEP-8b32 object integrity proofs. This is item 4 from the SiliconBeest comparison, following double-knock and the circuit breaker in #574.
integrityKey, an Ed25519 private JWK (kty: "OKP",crv: "Ed25519", a 32-bytexandd), stored as a secret binding. A malformed key fails at startup. Without it, behaviour is unchanged.assertionMethodMultikeyat<actor>#ed25519-key, and adds themultikey/v1anddata-integrity/v2contexts.eddsa-jcs-2022DataIntegrityProof(proofPurpose: "assertionMethod") at send time, only when the activity'sactoris this actor, it has noproofyet, and itsidis 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 SQLitekvtable likeprivateKeyPem, and is cleared when it is removed from config.activitypub.integrity.key_invalid), and deliveries continue without a proof rather than stalling the queue.@dwk/vc. It is now also exported as the@dwk/vc/data-integritysubpath (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 onlydatetime.ts,jcs.tsandmultibase.ts, so none of vc's endpoint, D1 ordid:webcode ships with activitypub. I checked this with an esbuild bundle of@dwk/activitypub(using thecloudflare:workersalias from@dwk/server'sbundle.mjs): it containseddsa-jcs-2022and nodid:web:or status-list code.pnpm --filter @dwk/server bundlealso still succeeds. The alternative is to extract a separate@dwk/data-integritylibrary; 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
assertionMethodand 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
spec/packages/and updated them ifbehaviour changed:
spec/packages/activitypub.mdS2S section.spec/packages/vc.mdneeds no change; the new subpath is documented in the vc README and CLAUDE.md.src/*.test.ts). Inintegrity.test.ts: key validation (shape,xand seed length), the Multikey matching the JWKx, a proof that verifies through@dwk/vc'sverifyProofand fails after tampering, and no signing for another actor's, already-proofed, fragment-id or unparseable activities. Inindex.test.ts:assertionMethodon 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.pnpm lint && pnpm format:check && pnpm typecheck && pnpm build && pnpm test(3304 passed)pnpm changeset) if this touches a publishablepackage (one each for activitypub and vc)
catalog.json/conformance/status.jsonif this adds a newmountable worker or changes conformance status: not applicable, no new worker;
pnpm catalog:checkpasses🤖 Generated with Claude Code
https://claude.ai/code/session_015LRGjmHE99WESN5HBERxSj