Skip to content

did:webvh and did:web: key rotation, and signing with your own domain - #25

Open
harlantwood wants to merge 2 commits into
masterfrom
claude/did-methods-bsuvj4
Open

harlantwood wants to merge 2 commits into
masterfrom
claude/did-methods-bsuvj4

Conversation

@harlantwood

Copy link
Copy Markdown
Member

TL;DR

This PR adds roadmap Phase 5 "key rotation" and Phase 3 "did:web so an organization can sign with its own domain". A key can now sign as a did:webvh (DIF Ratified v1.0) and rotate keys, with pre-rotation. Credentials signed before a rotation stay valid. A key can also sign as a did:web.

The core verifies did:webvh logs purely, from their bytes. It passes the DIF did:webvh test suite. The CLI fetches DID documents over HTTPS, caches them, and supports --offline. JavaScript hosts get the same verification through verifyWith.

Core (trustgraph-core, still pure)

  • did: a DID document model following DID 1.1 and Controlled Identifiers 1.0 (Multikey + publicKeyMultibase, assertionMethod references or embedded methods, relative #ids).
    • DidDocument::for_did_key builds a did:key document locally.
    • sign_atom_as(atom, keypair, doc, created) and verify_atom_with(credential, doc) sign and verify any issuer against a resolved document. They work for did:key too, with byte-identical output to credential::sign_atom.
  • did::web: maps a DID to its HTTPS URL for did:web and did:webvh. Covers ports (%3A), path segments, percent-encoding, punycode for international domain names, and rejects IP addresses and bad labels. Tested against the spec examples, including jp納豆.例.jp.
  • did::webvh: our own did:webvh v1.0 implementation.
    • Verification (verify_log) checks:
      • the SCID and the entry-hash chain
      • version numbers, and strictly increasing UTC times with a future-time limit when now is given
      • eddsa-jcs-2022 proofs by authorized update keys, using the spec's active-key rules
      • pre-rotation commitments (nextKeyHashes), which must be set explicitly while active
      • strict parameter validation
      • the SCID in state.id
      • portability (alsoKnownAs must name the previous DID)
      • deactivation
      • witness thresholds against did-witness.json
    • Resolution: by versionId, versionNumber or versionTime, with the implicit #files and #whois services added.
    • Historical resolution: DidLog::document_at(proof.created) returns the document in force when a proof was made.
    • Building logs: create, append, create_identity, rotate_identity.
    • Why our own code: didwebvh-rs 0.8.0 depends on tokio, getrandom, async-trait and the affinidi crates unconditionally. That would break core purity and grow the WebAssembly build.
  • api: did_document_url, resolve_did_key, resolve_did_webvh(did, log, options) and verify_with(credential, resolved). resolved is either a DID document or {didLog, didWitness?}.
  • credential.rs edits are minimal for the v1-formats merge: three constants and hash_data become pub(crate), and nothing else changes. The proof checks in did/proof.rs copy those in credential::verify (about 30 lines). Fold them together after v1-formats lands.
  • error.rs: new Error::InvalidDid variant.

Bindings

  • WebAssembly and Node export didDocumentUrl, resolveDidKey, resolveDidWebvh and verifyWith. Types are in bindings/trustgraph.d.ts.
  • tests/js/smoke.mjs covers them, including resolving a DIF vector.
  • WebAssembly size: 540 → 655 KiB (265 KiB gzipped). The budget is 1 MiB.

CLI

  • trust did resolve DID [--log FILE [--witness FILE]] [--version-id|--version-number|--version-time] prints a DID resolution result. --log verifies a log you were given and caches it, so later verification works offline.
  • trust did webvh create --domain HOST[:PORT] [--path P] [--prerotate] [--portable] [--key NAME] [-o did.jsonl] makes key NAME sign as a did:webvh, and writes the log to publish.
  • trust did webvh rotate [--key NAME] adds a version with a new key. With pre-rotation, it uses the committed next key and commits a fresh one. The old key is retired to dids/NAME.retired-N.key.json.
  • trust did web create … and trust did show [NAME].
  • trust atom --sign, trust sign and trust lens use the key's did:webvh or did:web identity when it has one. The proof's verificationMethod is that key's id in the document (did:webvh:…#key-N).
  • trust verify and trust add resolve did:web and did:webvh issuers:
    • blocking ureq 3.4 with rustls (HTTPS only, 20 s timeout, 8 MiB limit)
    • an on-disk cache in cache/did/, kept for the did:webvh ttl or 1 h for did:web, and re-verified every time it is read
    • global --offline / TRUST_OFFLINE uses the cache only, never the network.
  • TRUST_DID_TEST_ORIGIN is a hidden, loopback-only http:// origin override, used by the tests.

Docs

  • New doc/identity.md: which DID method to use when, how creating and rotating work, what verification checks, the JavaScript usage, how to host on GitHub Pages (including the .nojekyll trap for /.well-known/), and the files in the trust home.
  • README (commands table, bullets), architecture API table, core README, and one roadmap "shipped" row.

Verified

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets --locked on stable, and with RUSTFLAGS=-D warnings on +1.99 (workspace and -p trustgraph-core)
  • cargo clippy --target wasm32-unknown-unknown -p trustgraph-core -p trustgraph-wasm
  • RUSTDOCFLAGS=-D warnings cargo doc --workspace --no-deps --locked
  • scripts/check-core-purity.sh
  • cargo test --workspace --locked on stable and on 1.85 (MSRV)
  • The DIF didwebvh-test-suite vectors (commit 7be2491, as bundled with didwebvh-rs 0.8.0): 12 scenarios, compared on the full didDocument and on metadata. This includes pre-rotation, witnesses, portable moves, deactivation and services.
  • A 120-entry didwebvh-rs log with witnesses. Tamper and truncation tests on every vector.
  • CLI end-to-end tests against an in-process HTTP server (no external network): create, sign, publish, verify, cache, offline, rotate with pre-rotation (old signatures stay valid), tampered log, wrong did.json, did:web.
  • pnpm run build:node && pnpm run test:node, scripts/build-wasm-package.sh, the smoke test on wasm-node and wasm-web, and pnpm run typecheck.

Owner decisions / notes

  • Two upstream vectors are rejected on purpose, matching didwebvh-rs and the spec text:
    • witness-update: an entry lowers its own witness threshold and carries only one proof.
    • A didwebvh-ts log: it chains entry hashes to {SCID} instead of the previous versionId.
    • Tests pin both rejections. They are worth reporting upstream.
  • Historical resolution lets someone holding a rotated-out key backdate a signature. This is documented, and pre-rotation narrows the window. A stricter "latest key only" mode could be added if wanted.
  • New DID documents use the did/v1 + multikey/v1 contexts, as the did:webvh vectors do. DID 1.1 is still a Candidate Recommendation.

Not in this PR

  • Creating DIDs with witnesses or watchers, moving a portable DID and deactivating from the CLI. The core append supports all of these, and verification already handles them.
  • Generating a parallel did:web from a did:webvh.
  • Signing as a did:webvh from JavaScript. JavaScript can verify and resolve only.
  • Full IDNA2008: names are lowercased and punycoded, but not Unicode-normalized.
  • Resolving did:plc.
  • Merging the duplicated proof checks into credential.rs after v1-formats lands.
  • If the HTTPS-feeds PR also adds ureq, deduplicate the workspace entry (ureq = "3.4").

🤖 Generated with Claude Code

https://claude.ai/code/session_01UdKneRqUnsbv1t7NyvcT68


Generated by Claude Code

claude added 2 commits October 8, 2026 15:26
Core (pure, no I/O):
- did: DID document model (DID 1.1 / Controlled Identifiers 1.0,
  Multikey + publicKeyMultibase, assertionMethod), did:key documents,
  sign_atom_as / verify_atom_with against a resolved document.
- did::web: DID-to-HTTPS mapping for did:web and did:webvh (ports,
  paths, punycode).
- did::webvh: own implementation of DIF did:webvh v1.0 log verification
  (SCID, entry-hash chain, update-key proofs, pre-rotation, portability,
  deactivation, witnesses), creation, append and key rotation, historical
  resolution by proof time. Passes the DIF didwebvh-test-suite vectors and
  a 120-entry didwebvh-rs log; didwebvh-rs was not used because it pulls
  tokio and getrandom.
- api: didDocumentUrl, resolveDidKey, resolveDidWebvh, verifyWith, exposed
  in WebAssembly, Node and the TypeScript types.

CLI:
- trust did resolve | show | web create | webvh create | webvh rotate.
- HTTPS resolver (ureq 3 + rustls) with an on-disk cache; trust verify and
  trust add resolve did:web / did:webvh issuers; global --offline.
- Signing with a key that has a did:web/webvh identity uses that DID and
  its verification method.

Docs: doc/identity.md (which method when, GitHub Pages hosting).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UdKneRqUnsbv1t7NyvcT68
…bsuvj4

# Conflicts:
#	README.md
#	doc/plan/README.md
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