feat: DIDComm Messaging v2 (Phases 0–7): TS + Python parity, transport, protocols, auto-discovery - #633
Merged
Conversation
Research and a phased plan for adding standards-based DIDComm v2 messaging to Archon: the secp256k1-vs-key-agreement-curve constraint, the existing reuse surface (JWE, sign/verify, service endpoints, Dmail), build-vs-buy decision, and a Phase 0-7 implementation plan. Linked from the docs index. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-contained, throwaway spike validating the DIDComm design doc's build-vs-buy and curve decisions. Packs/unpacks DIDComm v2 envelopes between two Archon-shaped DIDs via the didcomm-node library and the DIDResolver/SecretsResolver adapter interfaces later phases will back with the gatekeeper and wallet. All three round-trips pass: anoncrypt (XC20P/ECDH-ES over X25519), authcrypt (A256CBC-HS512/ECDH-1PU over X25519), and authcrypt+sign with a secp256k1 key (ES256K) — confirming Archon's existing keys can sign DIDComm as-is and only a new X25519 key-agreement key is required. Not wired into the monorepo (root workspaces is packages/* only); runs standalone via `npm ci && node spike.mjs` in spike/didcomm-phase0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
macterra
force-pushed
the
docs/didcomm-design
branch
from
June 21, 2026 01:07
a261a62 to
cb75497
Compare
Adds the standards-curve key-agreement key that DIDComm v2 encryption
requires (secp256k1 cannot do key agreement; it keeps signing).
- cipher: generateX25519Jwk(seed) + OKP JWK types. Derives a deterministic
X25519 keypair from 32 bytes of seed material (@noble/curves, already a
dependency).
- keymaster: fetchDidCommKeyPair derives the key on a dedicated HD branch
(m/44'/0'/{account}'/1/0) so it never collides with the signing key at
change=0 and survives backup/recovery with no extra stored material.
publishDidComm writes it into the DID document as a keyAgreement
verification method (+ optional DIDCommMessaging service); unpublishDidComm
removes it. Wired through KeymasterInterface, KeymasterClient, and the
keymaster API (/didcomm/publish).
- gatekeeper: DidCidDocument gains keyAgreement[] and allows OKP publicKeyJwk;
signature verification now asserts the signing key is secp256k1 (EC).
Tests: tests/keymaster/didcomm.test.ts (derivation determinism, distinct
from signing key, publish/unpublish, idempotent, DIDCommMessaging service)
and client coverage. Verified resolved DID docs carry a valid X25519
keyAgreement key. Phase 0 spike's lib round-trip already confirmed
didcomm-node accepts exactly this key/doc shape.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In-browser self-custody wallets are the primary use case, so pack/unpack must run client-side with local keys. Nothing in DIDComm requires WASM — every primitive is available pure-JS via @noble — so building the envelope crypto by extending the cipher package (which already runs in browser + node with zero WASM) beats shipping the SICPA WASM library to two browser bundlers (Vite + Webpack), with async init, bundle weight, and an MV3 wasm-unsafe-eval CSP allowance. - Decision A: build, not buy. cipher owns envelope crypto (ECDH-1PU, A256KW, A256CBC-HS512 + JWM/JWS/JWE framing) on raw JWKs; keymaster orchestrates resolve+derive. didcomm-node retained only as a dev/test interop oracle. - Architecture/diagram updated to the cipher<->keymaster split. - Phase 2 re-planned: 2a (cipher crypto, interop-tested vs didcomm-node) + 2b (keymaster packDidComm/unpackDidComm). Phases 0 and 1 marked done. - Risk reframed from "WASM dependency" to "we own the envelope crypto" (mitigated by two-way interop tests). Spike README annotated with the revision. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the DIDComm v2 encrypted (JWE) and signed (JWS) envelopes in pure JS, so they run identically in node and the browser (no WASM). New module packages/cipher/src/didcomm.ts (exported as @didcid/cipher/didcomm): - anoncrypt: ECDH-ES + A256KW, XC20P - authcrypt: ECDH-1PU + A256KW, A256CBC-HS512 — with the content-encryption tag mixed into the Concat-KDF as length-prefixed SuppPrivInfo (the 1PU+KW requirement; ECDH-ES omits it) - A256KW (RFC 3394) on AES-ECB; A256CBC-HS512 (AES-CBC + HMAC-SHA-512) - JWS sign/verify with ES256K (Archon's secp256k1 keys) - message-level packDidCommMessage/unpackDidCommMessage (sign-then-encrypt) and getEnvelopeInfo for skid resolution - header construction (alg/enc/apu/apv/skid/epk) matches didcomm-rust concat-kdf gains an optional SuppPrivInfo arg (backward compatible — the ES path passes nothing). Pure functions over raw JWKs; keymaster (Phase 2b) will resolve DIDs to keys and orchestrate. Validated: - tests/cipher/didcomm.test.ts — 22 unit tests (RFC 3394 KW vector, all enc round-trips, header shapes, tamper rejection, JWS, sign-then-encrypt) - spike/didcomm-phase0/interop.mjs — round-trips against didcomm-node BOTH directions for anoncrypt/authcrypt/sign-then-encrypt (6/6). didcomm-node is a dev/test oracle only, never a runtime dependency. - full cipher build (esm+cjs) + 48/48 cipher tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the pure-JS cipher envelope crypto into keymaster so encrypted DIDComm
messages flow between real did:cid identities, with live gatekeeper resolution
and wallet-derived keys (retiring the spike's mock-doc caveat).
- packDidComm(message, to, { sign, anoncrypt, encryption, name }): resolves each
recipient DID's X25519 keyAgreement key, sets from/to, derives the sender's
X25519 key for authcrypt (default) and the secp256k1 key for an optional
ES256K signature, then calls cipher.packDidCommMessage.
- unpackDidComm(packed, { name }): inspects the envelope (getEnvelopeInfo),
checks the message is addressed to this identity, resolves the skid (authcrypt)
and nested JWS signer DID, derives the recipient key, and decrypts/verifies.
- Verification-method lookup matches by DID-fragment so it handles both the
relative (#key-1) and absolute (did#key-agreement-1) ids in did:cid docs.
- Wired through KeymasterInterface, KeymasterClient, and the keymaster API
(/didcomm/pack, /didcomm/unpack). Because the crypto is pure-JS in cipher,
this works unchanged in the browser-shared Keymaster core.
Tests: tests/keymaster/didcomm.test.ts gains 5 end-to-end cases between two
real identities (authcrypt, anoncrypt, sign-then-encrypt, plus the
not-published and not-a-recipient error paths) — 12/12; client nock coverage
added (313/313); keymaster builds and the service typechecks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phases 0-2 are done (spike, X25519 keyAgreement keys, pure-JS envelope crypto, keymaster pack/unpack). Reframes Phase 3: because self-custody wallets hold keys client-side, the inbound side can only store-and-forward encrypted envelopes (a mailbox), not unpack on the agent's behalf — which converges with the Phase 6 mediator. Records the open transport decisions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enables DIDComm with non-Archon agents (other DID methods), not just did:cid. cipher (@didcid/cipher/didcomm): - didKeyToX25519: resolves did:key to its X25519 key-agreement key — z6LS (X25519 direct) and z6Mk (Ed25519, with the spec-correct Ed25519->X25519 derivation, verified against the W3C did:key vector). - normalizeX25519PublicKey: accepts publicKeyJwk (OKP) or publicKeyMultibase, so foreign DID docs (e.g. from a universal resolver) normalize to an X25519 JWK. - x25519JwkToDidKey: encode an X25519 key as a did:key. keymaster: - resolveDidForDidComm: resolves did:key locally; routes everything else through the gatekeeper (which has a universal-resolver fallback for did:web etc.). - pack/unpack use it for recipient and sender (skid) keys; resolveKeyAgreement now normalizes multibase key material. Validated: - cipher unit tests incl the W3C did:key known-answer vector (53/53 cipher). - keymaster e2e: Archon did:cid <-> did:key both directions (14/14). - spike interop: our pack to a did:key recipient is unpacked by the reference didcomm-node library. Caveats (additive, later): foreign signing interop needs EdDSA verify (we verify ES256K only); P-256 key agreement for ecosystems that use it; and a Universal Resolver driver so others can resolve did:cid. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Standard HTTP transport so self-custody recipients can receive DIDComm over a
published DIDCommMessaging endpoint (store-and-forward, since the relay can't
unpack on the key-holder's behalf).
New services/didcomm/server (mirrors the keymaster server):
- POST /api/v1/messages — store an encrypted envelope by recipient DID (parsed
from the JWE recipient kids); inbound is open (envelopes are encrypted).
- GET /api/v1/challenge + POST /api/v1/messages/fetch — a recipient proves DID
control with a single-use signed challenge (ES256K over the nonce, verified by
resolving the DID and checking its key), then retrieves its queue.
- POST /api/v1/messages/remove — ack/remove fetched messages.
- Express-free core (store.ts MailboxStore + MemoryMailboxStore w/ TTL,
mailbox.ts recipient-parse + challenge verify) for unit-testability; in-memory
store now, swappable for redis/mongo behind MailboxStore.
keymaster:
- sendDidComm(message, to, options): pack (Phase 2/3a) -> resolve each
recipient's DIDCommMessaging endpoint -> POST the envelope.
- receiveDidComm({ name, endpoint }): challenge -> sign -> fetch -> unpack -> ack.
- Wired through KeymasterInterface, KeymasterClient, and the keymaster API
(/didcomm/send, /didcomm/receive). Uses global fetch (browser + node).
Tests: tests/didcomm/mailbox.test.ts (store/parse/auth, 8) and
tests/didcomm/e2e.test.ts (two identities exchange authcrypt + signed messages
through the live relay over HTTP, ack, forged-fetch rejected, 3); keymaster
client nock coverage (317). Service builds (tsc) with its own deps.
Remaining for transport: Dockerfile + docker-compose wiring, redis/mongo store,
and 3c Forward/routing for mediated recipients.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds docker/Dockerfile.didcomm (mirrors Dockerfile.keymaster-ts: build the @didcid/* workspace packages, then the didcomm server) and an opt-in compose fragment docker/compose/didcomm.yml (enable with COMPOSE_PROFILES=didcomm), included from docker-compose.yml. The relay depends only on the gatekeeper (in-memory store, no redis/mongo yet) and health-checks GET /health. `docker compose config` validates the merged configuration. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Makes the MailboxStore interface async (prerequisite for network-backed stores) and adds a redis backend alongside the in-memory one, selected by ARCHON_DIDCOMM_DB (memory|redis). Redis uses native key expiry (EX/PX) for message and challenge TTLs; an inbox SET per recipient with lazy pruning of expired ids; GETDEL for single-use challenges. - store.ts: MailboxStore now returns Promises; MemoryMailboxStore updated; RedisMailboxStore (ioredis) added with create()/connect()/disconnect(). - didcomm-api.ts: route handlers await the store. - index.ts/config.js: select store by ARCHON_DIDCOMM_DB; ARCHON_REDIS_URL. - compose: ARCHON_DIDCOMM_DB + ARCHON_REDIS_URL env, depends_on redis. Validated: 13/13 didcomm tests incl. the RedisMailboxStore against a live redis, and the relay e2e still green after the async refactor. Service builds with ioredis; `docker compose config` valid. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the routing/2.0 Forward protocol crypto in cipher so messages can be
delivered to recipients behind a mediator:
- wrapForward(inner, next, routingKey): builds a routing/2.0/forward JWM
(body.next = recipient, attachments[0].data.json = inner envelope) and
anoncrypts it to the mediator's key.
- parseForward(plaintext): extracts { next, forwardedMessage } from a decrypted
Forward, for a mediator to relay.
Format matches didcomm-rust; interop-validated against didcomm-node both ways
(our wrapForward read by the lib; the lib's wrap_in_forward parsed by us with
the inner envelope still decrypting for the final recipient). Cipher unit tests
+ spike interop oracle updated (9 interop checks, all green; 29 cipher tests).
Remaining 3c integration (separate): DID-doc routingKeys (serviceEndpoint object
form + publishDidComm), sendDidComm wrapping when routingKeys are present, and an
Archon mediator that unpacks a Forward and relays to next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…diator
Completes Forward/routing end-to-end:
- DID-doc serviceEndpoint now allows the DIDComm object form
{ uri, accept, routingKeys }; publishDidComm(endpoint, name, routingKeys)
advertises a mediator. (lightning-mediator narrows its string endpoint
accordingly.)
- resolveDidCommEndpoint returns { uri, routingKeys }; sendDidComm wraps the
packed message in a Forward to the recipient's mediator routing key when one
is advertised, and delivers to the mediator endpoint.
- mediateDidComm: an Archon identity acting as a mediator fetches Forward
messages from its mailbox, unpacks them, and relays the inner envelope to the
final recipient (stored under `next`). Wired through interface/client/API
(/didcomm/mediate).
Validated: an e2e where Alice -> mediator -> Bob is delivered via the Forward
protocol through the live relay (14 didcomm tests); keymaster + service build,
360 keymaster/cipher tests, 319 client tests, lightning mediator typechecks.
Phase 3 (cross-method resolution + transport + routing) is now complete.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix a port collision: the didcomm relay used 4228, which react-wallet already owns. Moved it to 4236 (config default + compose). - Front the relay through Drawbridge (the public gateway, with Tor): add a generic proxyRequest, mount `/didcomm` -> config.didcommURL (ARCHON_DIDCOMM_URL, default http://didcomm:4236), and capture application/didcomm-encrypted+json as text so envelopes survive proxying. So the published DIDCommMessaging endpoint is `<drawbridge public host>/didcomm` (or .onion), not the relay exposed directly. Drawbridge + didcomm services typecheck/build; `docker compose config` valid; 14 didcomm tests still green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds packages/keymaster/src/didcomm-protocols.ts (re-exported from @didcid/keymaster) with spec-correct builders for the core DIDComm v2 application protocols, which compose with sendDidComm/receiveDidComm: - Trust Ping 2.0: trustPing / trustPingResponse (thid-correlated) - Basic Message 2.0: basicMessage(content) - Discover Features 2.0: discoverFeaturesQuery / discoverFeaturesDisclose - Out-of-Band 2.0: outOfBandInvitation + encode/decodeOutOfBandInvitation (the _oob=base64url(json) URL form) Type URIs and body shapes verified against the DIDComm messaging spec. Tests: tests/keymaster/didcomm-protocols.test.ts (builder shapes vs spec) and a relay e2e where Alice sends a Basic Message + a Trust Ping and Bob returns a thid-correlated ping-response (5 e2e / 4 unit, all green). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rewrote a chained ternary in POST /messages as an if/else block to satisfy the indent rule (build-and-test lint step). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt-Proof 3.0) Adds Issue-Credential 3.0 and Present-Proof 3.0 message builders to didcomm-protocols.ts, carrying an Archon verifiable credential/presentation as a DIDComm attachment (data.json). They map onto Archon's existing VC machinery (bindCredential/addProof to issue, verifyProof to check) and compose with sendDidComm/receiveDidComm: - issueCredentialMessage(vc), offerCredential, requestCredential - requestPresentation, presentationMessage(vp), proposePresentation - attachedJson(message) to extract the VC/VP from an attachment - spec type URIs + attachment format identifiers Tests: builder-shape units + an e2e where Alice issues a signed VC to Bob over DIDComm (Bob verifies the issuer proof), then Carol requests a presentation, Bob presents a VP wrapping the VC, and Carol verifies both holder and issuer signatures. Note: cross-agent VC interop is bounded by the credential format — Archon VCs use EcdsaSecp256k1Signature2019; a standard attachment format for Aries/AnonCreds is a follow-on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes Phase 6 (Forward messages already landed in 3c). Adds the Coordinate-Mediation 2.0 enrollment handshake so the mediator relationship is negotiated rather than hard-coded: - didcomm-protocols.ts: builders for mediate-request / mediate-grant (routing_did) / mediate-deny / keylist-update (+response) / keylist-query / keylist, with the exact spec type URIs and bodies. - keymaster: resolveRoutingKey accepts a routing key as a full kid OR a bare DID (the grant's routing_did form); sendDidComm uses it when wrapping Forwards. Tests: builder-shape units + an e2e where Bob requests mediation, the mediator grants its routing_did and acks a keylist-update, Bob re-publishes advertising it, and Alice then routes to Bob through the mediator (17 protocol/e2e tests green; lint + build clean). Note: the mediator still relays any Forward it can unpack; gating on the registered keylist is a follow-on refinement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The RedisMailboxStore integration tests were gated opt-OUT
(`ARCHON_SKIP_REDIS ? describe.skip : describe`), so they ran by default.
The unit-test CI job has no redis service and doesn't set ARCHON_SKIP_REDIS,
so the suite connected `new Redis('redis://localhost:6379')` to nothing;
ioredis then reconnected forever, leaking a timer/socket handle. Under
`jest --runInBand` with no `--forceExit`, that open handle kept the event
loop alive and jest never exited — the unit-test workflow hung indefinitely
(observed across the last few commits; runs sat until the runner timeout).
Flip to opt-IN: the live-redis suite runs only when ARCHON_REDIS_URL is set
(pointing at a reachable redis) and is skipped otherwise. Verified locally:
mailbox.test.ts now skips the redis block and jest exits 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the DIDComm v2 surface (REST routes already shipped in Phase 3a/3b) through the two remaining client tiers, completing the build. CLI (packages/keymaster/src/cli.ts): publish-didcomm / unpublish-didcomm / pack-didcomm / unpack-didcomm / send-didcomm / receive-didcomm / mediate-didcomm. pack/send read the plaintext message from a JSON file and a comma-separated recipient list, with --sign / --anoncrypt / --encryption / --name flags. Python SDK (python/keymaster_sdk): publish_didcomm / unpublish_didcomm / pack_didcomm / unpack_didcomm / send_didcomm / receive_didcomm / mediate_didcomm, mirroring KeymasterClient (same endpoints/bodies), exported from the package. Tests: a mocked contract test asserts endpoint + body + return parity for all seven SDK functions, and a live test does a real authcrypt pack->unpack round-trip between two identities. The contract-unit file is now also run by the python-sdk-tests workflow (it was previously not executed by any CI). Docs: design-doc Phase 7 marked done with the CLI/SDK surface; status table shows Phases 0-7 all complete. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The mcp-server CLI-parity test (tests/mcp-server/parity.test.ts) requires one
MCP tool per Keymaster CLI command. Phase 7's new didcomm CLI commands had no
MCP counterparts, so parity failed (7 unmapped commands). Add the matching
tools — archon_{publish,unpublish,pack,unpack,send,receive,mediate}_didcomm —
each delegating to the corresponding KeymasterClient method. publish/unpublish/
send/receive/mediate are marked mutates (DID-doc/mailbox/network side effects);
pack/unpack are read-only.
Parity + full mcp-server suite green (24 tests); root build + lint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Implements end-to-end DIDComm Messaging v2 support across Archon’s crypto (cipher), orchestration (keymaster), transport (new relay service), and client surfaces (REST/Swagger, JS client, Python SDK, MCP tools), with extensive unit + e2e + interop validation.
Changes:
- Add pure-JS DIDComm v2 envelope crypto (anoncrypt/authcrypt + optional ES256K JWS) plus did:key/X25519 normalization and Forward routing helpers.
- Extend Keymaster with DIDComm keyAgreement publishing, pack/unpack, send/receive, and mediator relay; expose via REST + client SDKs + MCP tools.
- Introduce a DIDComm mailbox relay service (memory/redis stores), Docker/Compose wiring, Drawbridge reverse proxy, and broad test coverage.
Reviewed changes
Copilot reviewed 48 out of 50 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/keymaster/didcomm.test.ts | Keymaster DIDComm key derivation/publish + pack/unpack e2e tests |
| tests/keymaster/didcomm-protocols.test.ts | Unit tests for DIDComm protocol builders (ping/basic/oob/cred/mediation) |
| tests/keymaster/client.test.ts | Adds KeymasterClient contract tests for DIDComm REST endpoints |
| tests/didcomm/mailbox.test.ts | Unit + opt-in live-redis tests for mailbox store + auth helpers |
| tests/didcomm/e2e.test.ts | Full HTTP relay e2e flows (send/receive/forward/mediation/forgery rejection) |
| tests/cipher/didcomm.test.ts | Unit tests for DIDComm crypto primitives, did:key, Forward routing |
| spike/didcomm-phase0/spike.mjs | Phase-0 reference spike using didcomm-node (dev/test oracle) |
| spike/didcomm-phase0/README.md | Spike documentation + rationale/interop instructions |
| spike/didcomm-phase0/package.json | Spike package definition (didcomm-node dependency) |
| spike/didcomm-phase0/package-lock.json | Lockfile for spike package |
| spike/didcomm-phase0/interop.mjs | Interop oracle (ours↔didcomm-node) regression runner |
| spike/didcomm-phase0/.gitignore | Ignore node_modules for spike |
| services/mediators/lightning/src/lightning-mediator.ts | Accept serviceEndpoint object form by normalizing to a URL string |
| services/keymaster/server/src/keymaster-api.ts | REST + Swagger routes for DIDComm publish/pack/unpack/send/receive/mediate |
| services/drawbridge/server/src/drawbridge-api.ts | Add generic proxy helper + public /didcomm reverse proxy + body parsing |
| services/drawbridge/server/src/config.ts | Add didcommURL config knob |
| services/didcomm/server/tsconfig.json | TS build config for the new relay service |
| services/didcomm/server/src/store.ts | Mailbox store interface + memory and redis implementations |
| services/didcomm/server/src/mailbox.ts | Core relay logic helpers (recipient extraction, challenge signature verify) |
| services/didcomm/server/src/index.ts | Relay service entrypoint wiring Gatekeeper resolver + store + app |
| services/didcomm/server/src/didcomm-api.ts | Relay HTTP API (store, challenge, fetch/remove) |
| services/didcomm/server/src/config.js | Relay environment configuration |
| services/didcomm/server/package.json | Relay package definition + deps |
| python/keymaster_sdk/tests/test_keymaster_sdk.py | Live DIDComm pack→unpack round-trip test |
| python/keymaster_sdk/tests/test_keymaster_sdk_contract_unit.py | Contract-unit parity tests for DIDComm SDK wrappers |
| python/keymaster_sdk/src/keymaster_sdk/keymaster_sdk.py | Python SDK DIDComm wrapper functions |
| python/keymaster_sdk/src/keymaster_sdk/init.py | Export DIDComm SDK functions |
| packages/mcp-server/src/tools.ts | Add MCP tool definitions for DIDComm commands |
| packages/keymaster/src/types.ts | Add DIDComm types + KeymasterInterface methods |
| packages/keymaster/src/keymaster.ts | Implement DIDComm key derivation, publish, pack/unpack, send/receive, mediate |
| packages/keymaster/src/keymaster-client.ts | Add DIDComm methods to KeymasterClient (REST calls) |
| packages/keymaster/src/index.ts | Re-export DIDComm protocol builders |
| packages/keymaster/src/didcomm-protocols.ts | Add DIDComm protocol message builder utilities |
| packages/keymaster/src/cli.ts | Add DIDComm CLI commands |
| packages/gatekeeper/src/types.ts | Extend DID document types for keyAgreement + DIDComm serviceEndpoint object form |
| packages/gatekeeper/src/gatekeeper.ts | Ensure signature verification uses secp256k1 (kty check) |
| packages/cipher/src/types.ts | Add OKP/X25519 JWK types + cipher API for X25519 keygen |
| packages/cipher/src/didcomm.ts | Implement DIDComm v2 envelope crypto + Forward routing + did:key helpers |
| packages/cipher/src/concat-kdf.ts | Add SuppPrivInfo support for ECDH-1PU KDF |
| packages/cipher/src/cipher-base.ts | Implement deterministic X25519 JWK generation |
| packages/cipher/rollup.cjs.config.js | Add didcomm entry to CJS bundling |
| packages/cipher/package.json | Export ./didcomm entrypoint |
| docs/index.md | Link to DIDComm design doc |
| docs/didcomm-design.md | DIDComm design/decisions/phases documentation |
| docker/Dockerfile.didcomm | Docker image build for relay service |
| docker/compose/drawbridge.yml | Wire drawbridge env var to reach didcomm service |
| docker/compose/didcomm.yml | Compose profile for relay service + healthcheck |
| docker-compose.yml | Include didcomm compose fragment |
| .github/workflows/python-sdk-tests.yml | Run both Python SDK test suites in CI |
Files not reviewed (2)
- services/didcomm/server/package-lock.json: Generated file
- spike/didcomm-phase0/package-lock.json: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Completes the deferred Python work: a full pure-Python port of the DIDComm v2 stack in the standalone python/keymaster library, interoperable byte-for-byte with the TypeScript stack and the didcomm/didcomm-node reference library. - didcomm_crypto.py: envelope crypto mirroring @didcid/cipher's didcomm.ts — X25519, ECDH-ES/1PU+A256KW (the content-encryption tag mixed into Concat-KDF as length-prefixed SuppPrivInfo), A256CBC-HS512 / XC20P / A256GCM, ES256K JWS, RFC 3394 key wrap, Forward (routing/2.0), and did:key resolution/normalization. Built on `cryptography` + `coincurve`, plus PyNaCl for XChaCha20-Poly1305 and the Ed25519->X25519 did:key conversion. - didcomm_protocols.py: trust-ping / basic-message / discover-features / out-of-band / issue-credential 3.0 / present-proof 3.0 / coordinate-mediation 2.0 message builders. - core.py (Keymaster): fetch_didcomm_key_pair, publish_didcomm, unpublish_didcomm, pack_didcomm, unpack_didcomm, send_didcomm, receive_didcomm, mediate_didcomm + resolution/routing helpers, mirroring keymaster.ts. - cli.py + scripts/archon-cli.js: the 7 DIDComm commands, bringing all three CLIs into parity with packages/keymaster/src/cli.ts (AGENTS.md CLI-parity rule). Tests (tests/test_didcomm.py): Python self round-trips (anon/auth/gcm/signed/ forward), JS-produced envelope vectors decrypted/verified in Python (JS->PY interop, no Node needed at test time), did:key round-trip + the Ed25519 W3C vector, and protocol-builder shape checks. 16 new tests; full library suite (160) and keymaster_service (21) green; pynacl added to deps; lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…5519 inline The Dependency Review gate (fail-on-severity: moderate) flagged the new pynacl@1.5.0 for GHSA-mrfv-m5wm-5w6w (libsodium incomplete input validation), which has no patched PyNaCl release. Rather than weaken the security gate, remove the dependency: - XChaCha20-Poly1305 is now built from a small HChaCha20 subkey derivation plus `cryptography`'s IETF ChaCha20Poly1305 (the standard XChaCha construction). - Ed25519->X25519 (for did:key z6Mk… key agreement) uses the standard Edwards y -> Montgomery u birational map. Both are gated by existing test vectors: the committed JS-produced XC20P envelope decrypts in Python (proving the HChaCha20 subkey matches libsodium/@noble byte-for-byte), and the Ed25519 W3C did:key vector still resolves. 16 didcomm tests + full library suite green; no pynacl in deps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves the valid Copilot review comments on the DIDComm work: - packDidComm (keymaster.ts + Python core.py): spread the caller's message first, then force the protocol-controlled headers (id/typ/to) and delete any caller-supplied `from`, setting `from` only for authcrypt. Previously a caller could override `typ` (producing a non-compliant envelope) or smuggle a `from` into an anoncrypt envelope, defeating sender anonymity. - unpublishDidComm (keymaster.ts + Python core.py): delete `verificationMethod` when the filtered list is empty, mirroring the `service` handling (defensive symmetry; an identity always retains its signing key in practice). - Swagger for POST /didcomm/publish: document the `routingKeys` field the handler already accepts, so the spec matches the implementation. (The 4th review comment — a "broken" compose healthcheck — was a false positive: the backslashes are intentional YAML escaped line-breaks; the folded `node -e` script is valid JS, verified with `node --check`.) JS DIDComm e2e + unit (21) and Python didcomm (16) green; keymaster build + lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The DIDComm relay + Drawbridge reverse-proxy work introduced env vars that were never added to sample.env. Add them and list the `didcomm` compose profile: - ARCHON_DIDCOMM_PORT (4236), ARCHON_DIDCOMM_HOST_BIND (127.0.0.1), ARCHON_DIDCOMM_DB (memory|redis), and commented ARCHON_DIDCOMM_UPLOAD_LIMIT / ARCHON_DIDCOMM_MESSAGE_TTL_MS / ARCHON_REDIS_URL. - ARCHON_DIDCOMM_URL (Drawbridge -> relay proxy) documented in the Drawbridge block. - Added `didcomm` to the available-profiles comment. Also corrected the didcomm compose header comment (the redis backend exists; it no longer says "no redis dependency yet"). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the new DIDComm relay (mailbox) service under docs/services/, in the same language-agnostic spec style as the other services: - docs/services/didcomm/README.md — responsibilities, HTTP API (/health, /messages, /challenge, /messages/fetch|remove), signed-challenge auth, mailbox routing by JWE recipient kids, the MailboxStore contract + memory/redis backends (incl. the redis key schema), lifecycle + env vars, deployment (didcomm profile, Drawbridge /didcomm proxy, Tor/NAT pull model), and conformance requirements. - docs/services/README.md — new "Messaging" section linking the relay spec. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
publish-didcomm previously required an explicit endpoint, unlike publish-lightning which learns its public host automatically. Close that gap. Drawbridge owns the public /didcomm proxy mount, so it's the right place to advertise the endpoint: - Wire up the (previously unused) ARCHON_DRAWBRIDGE_PUBLIC_HOST in Drawbridge config and add a public GET /api/v1/didcomm-endpoint returning `<publicHost>/didcomm` (or null), bypassing L402. - DrawbridgeClient.getDidCommEndpoint() (JS) + GatekeeperClient.get_didcomm_endpoint() (Python) query it; added to DrawbridgeInterface. - publishDidComm / publish_didcomm: when no endpoint is given, auto-discover via the gateway (mirrors publishLightning's publicHost fallback). A plain Gatekeeper exposes nothing, so it falls back to today's key-agreement-only publish; an explicit endpoint still overrides (standalone relay, other proxy, onion, etc.). - CLI help (all three) + sample.env + the relay service spec updated. Tests: JS keymaster test stubs a gateway endpoint and asserts publishDidComm() writes the auto-discovered DIDCommMessaging service (15 didcomm tests green); Python didcomm suite (16) green; root build + drawbridge typecheck + lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror publishLightning fully: when ARCHON_DRAWBRIDGE_PUBLIC_HOST is unset, GET /api/v1/didcomm-endpoint now falls back to the Tor onion fronting Drawbridge — `http://<onion>:<drawbridgePort>/didcomm` — read from the shared hidden-service hostname file (the same /data/tor/hostname the lightning-mediator uses). So a Tor-only node gets an onion DIDComm endpoint automatically, just like its Lightning endpoint. - config.ts: add torHostnameFile (default /data/tor/hostname). - drawbridge-api.ts: resolveDidCommEndpoint() prefers the explicit public host, else reads the onion; cached on first success (null until the hostname file exists, so it retries). - drawbridge.yml: mount the shared tor-hostname volume read-only at /data/tor. - Docs/sample.env note the onion fallback. Drawbridge typecheck + lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 21, 2026
This was referenced Aug 19, 2026
Open
macterra
added a commit
that referenced
this pull request
Aug 22, 2026
…too (#921) The Python service registered none of the DIDComm routes -- `grep -c didcomm app.py` was 0 -- so all eleven SDK didcomm methods fell through to the catch-all 404 while working fine against the JS service. The `keymaster` library has implemented the operations since #633; only this routing layer was missing, and nothing checked. Closes #920. Two neighbours were missing for the same reason, so this closes them at the same time: GET /capabilities and POST|DELETE /addresses/publish. That brings the two route surfaces to exact parity in both directions, which means the new guard needs no allowlist to rot. The guard compares the JS routers against app.py -- paths, methods, and public-vs-protected, read from the mount order around createRequireAdminKey rather than a hardcoded list. It resolves each createXRouter to its file by looking for the export, not by mangling the name into a filename: the first draft guessed keymaster-did-comm-router.ts, silently dropped the router it was written to check, and passed. Mutation-tested by renaming a route. That guard also found a live bug: POST /polls/ballot/send was declared after POST /polls/{poll}/send, and FastAPI matches in declaration order, so a ballot send arrived as send_poll(poll="ballot"). Verified against unmodified main by resolving the route through the real app. Both it and the new /addresses/publish now sit ahead of the templates that would swallow them, with a test for that ordering class. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements DIDComm Messaging v2 for Archon end-to-end — design doc plus a working, interop-validated stack reachable from every client tier in both the TypeScript and Python ecosystems. Every envelope is round-tripped against the
didcomm-nodereference library, kept as a dev/test oracle only — not a runtime dependency (nodidcomm-nodein any shipped manifest); the runtime crypto is pure-JS (and pure-Python).See docs/didcomm-design.md for the full design and docs/services/didcomm/README.md for the relay service spec.
What's implemented (Phases 0–7, all complete)
spike/didcomm-phase0+ interop oracle; validated decisions (build-not-buy, X25519)keyAgreementkeys indid:ciddocs (publishDidComm/unpublishDidComm)cipher(ECDH-ES & ECDH-1PU + A256KW, A256CBC-HS512, XC20P, A256GCM, ES256K JWS); keymasterpackDidComm/unpackDidCommdid:key(Ed25519/X25519) + universal-resolver fallback; multibase normalization — Archondid:cid↔did:keyboth directionsservices/didcomm/serverrelay (signed-challenge fetch auth; in-memory/redis stores; Docker + opt-indidcommcompose profile, port 4236); keymastersendDidComm/receiveDidComm; Drawbridge/didcommreverse proxy for the public endpointwrapForward/parseForward; DID-docroutingKeys;sendDidCommmediator wrapping;mediateDidComm(Archon-as-mediator)_oobURL encode/decode)bindCredential/addProof/verifyProof)mediate-request/grantwithrouting_did,keylist-update); Forward landed in 3cReachable from every tier
The DIDComm verbs (
publish/unpublish/pack/unpack/send/receive/mediate) are exposed consistently across:core
Keymaster→ REST API (+ Swagger) →KeymasterClient→ CLI → MCP server, and independently in Python (SDK + standalone library).packages/keymaster/src/cli.ts,scripts/archon-cli.js, and the PythonkeymasterCLI each gainpublish-/unpublish-/pack-/unpack-/send-/receive-/mediate-didcomm.archon_{publish,unpublish,pack,unpack,send,receive,mediate}_didcomm(a parity test enforces one tool per CLI command).python/keymaster_sdk):publish_didcomm…mediate_didcomm, mirroringKeymasterClient(REST).python/keymaster): a full pure-Python port — envelope crypto (didcomm_crypto.py: X25519, ECDH-ES/1PU+A256KW with the tag-in-Concat-KDF, A256CBC-HS512/XC20P/A256GCM, ES256K JWS, did:key, Forward), protocol builders, and the Keymaster methods/CLI. Usescryptography+coincurve; no new native deps — XChaCha20-Poly1305 and the Ed25519→X25519 did:key map are implemented inline (a moderate libsodium/PyNaCl advisory made the dependency not worth it). Validated byte-for-byte against the TS stack in both directions (committed JS-produced envelope vectors decrypt in Python; Python round-trips decrypt indidcomm-node).Endpoint auto-discovery
publishDidCommwith no explicit endpoint now auto-discovers it from the gateway (GET /api/v1/didcomm-endpoint), the waypublishLightninglearns its public host:ARCHON_DRAWBRIDGE_PUBLIC_HOST→<host>/didcomm,http://<onion>:<port>/didcomm(read from the shared hidden-service hostname),Wires up the previously-unused
ARCHON_DRAWBRIDGE_PUBLIC_HOSTin Drawbridge and mounts the sharedtor-hostnamevolume into it.Key design decision
Build, not buy (decided after confirming in-browser self-custody is the primary use case): nothing in DIDComm requires WASM, so the envelope crypto is pure-JS in
cipher— identical in node and the browser, no WASM bundler/CSP friction. Correctness is held by interop tests againstdidcomm-node; the Python port holds the same contract via committed cross-language vectors.Tests
pack→unpackround-trip against the docker stack.unit-test, both Python suites, browser/explorer/webapp builds, both Docker image builds, lockfile + dependency review.Review feedback addressed
packDidCommbuilds the envelope with protocol-controlledid/typ/toforced after the caller's message andfromset only for authcrypt (a caller can no longer overridetypor smuggle afrominto an anoncrypt envelope) — fixed in both languages.unpublishDidCommdeletes an emptyverificationMethodarray (symmetry withservice).POST /didcomm/publishdocuments theroutingKeysfield.node -escript passesnode --check.)Test-infra fix included
Made the live-redis mailbox suite opt-in (
ARCHON_REDIS_URLset) instead of opt-out — it was running by default against a non-existent redis in theunit-testCI, leaving ioredis reconnecting forever, a leaked handle that hungjest --runInBand(no--forceExit). Also wired the previously-unrun SDK contract-unit test intopython-sdk-tests.yml.Docs / config
sample.envdocuments the relay env vars (ARCHON_DIDCOMM_*), thedidcommprofile, and theARCHON_DRAWBRIDGE_PUBLIC_HOST/ onion auto-discovery behaviour.Optional follow-ons (not blocking)
EdDSA signature verify (foreign Ed25519 signers), P-256 key agreement, a
did:cidUniversal Resolver driver so non-Archon agents can resolvedid:cid, a mongo mailbox backend, and a standard credential attachment format for cross-agent VC interop (Archon VCs useEcdsaSecp256k1Signature2019).Note:
spike/is a throwaway/reference tree intended to be removed before release.🤖 Generated with Claude Code