Reworks the "Pick your primary key" → Option B block in both tutorials
into a proper "Recovery phrases" mini-chapter:
• Table comparing 24-word (256 bits, bijection) vs 12-word (128 bits,
one-way SHA-256 derivation).
• Decision guide — why someone would actually pick 12 over 24 (and
vice versa). Explicitly: "save the phrase, not just the seed" for
the 12-word case.
• Wallet-incompatibility callout — KEZ phrases don't produce the
same key as the same phrase in Ledger / MetaMask / Bitcoin
wallets. Explains the two deliberate reasons (no BIP-39 PBKDF2,
no BIP-32 derivation tree), and the inverse — KEZ phrases can't be
used to extract funds from a hardware-wallet recovery so a
malicious importer can't phish that direction either.
• Concrete backup advice — pencil on paper, numbered words, fireproof
storage, don't photograph it, don't cloud-sync it, don't split it,
don't permute it. Calls out which password-manager patterns are
OK vs not.
• "Working with phrases later" — clean examples of `identity mnemonic`
(no key derived) and `identity from-mnemonic` (recover an existing
key), with the note that the recovered output is byte-for-byte
identical to what `identity new` originally printed.
Same content in both the Rust and Node tutorials, command examples
adapted to each CLI invocation style.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the canonical wallet-style backup form (12 or 24 BIP-39 English
words) to both implementations. Wire-compatible — bit-identical seed
derivation across Rust and Node.
Semantics:
• 24 words ↔ 32 bytes of entropy ↔ Ed25519 seed (bijection).
Phrase ↔ seed round-trips exactly.
• 12 words → 16 bytes of entropy → seed via
SHA-256("kez-bip39-12-v1" || entropy). Deterministic but one-way;
you can't recover a 12-word phrase from a seed.
The 12-word case is KEZ-specific (not interoperable with hardware-
wallet BIP-32 derivations). The 24-word case is. Both use the BIP-39
English wordlist so users can paper-back-up alongside other wallets.
We deliberately do NOT use BIP-39's PBKDF2 to_seed(passphrase) — that
produces a 64-byte seed for BIP-32 hierarchical derivation, which is
the wrong primitive for KEZ's single-identity-per-phrase model.
Rust (kez-core):
• New mod mnemonic with MnemonicWords, generate_mnemonic,
seed_from_mnemonic, mnemonic_from_seed_24.
• Ed25519Secret::{from_mnemonic, generate_with_mnemonic}.
• Dep: bip39 v2.0 with the `rand` feature for OS-RNG generation.
• 9 unit tests, all green.
Rust (kez-cli):
• `identity new --key-type ed25519` now also prints a 24-word phrase
(default), with --mnemonic-words 12 to use 12 instead.
• `identity mnemonic [--words 12|24]` — print a fresh phrase only.
• `identity from-mnemonic "<phrase>"` — derive the key from a phrase.
• `--mnemonic <phrase>` is now accepted everywhere `--ed25519-seed
<hex>` was (claim create/dns, sigchain add/revoke/show/export/
publish), mutually exclusive with --ed25519-seed and --nsec via
clap conflicts_with_all.
Node (@kez/core):
• New mnemonic.ts with the parallel API:
generateMnemonic, seedFromMnemonic, mnemonicFromSeed24,
ed25519FromMnemonic, generateEd25519WithMnemonic.
• Dep: @scure/bip39 v2.x (note: import path is
"@scure/bip39/wordlists/english.js" with the .js suffix in v2).
• 8 vitest cases mirroring the Rust tests, all green.
Node (@kez/cli):
• Same CLI surface added: identity new --mnemonic-words 12|24,
identity mnemonic --words 12|24, identity from-mnemonic "<phrase>".
• --mnemonic flag accepted alongside --nsec / --ed25519-seed in the
flag parser, with mutex enforcement; loadSigner dispatches it.
Verified cross-implementation interop:
• Same 24-word phrase → identical Ed25519 pubkey in Rust and Node.
• Same 12-word phrase → identical pubkey (proves the SHA-256
domain-tagged derivation matches byte-for-byte).
• A claim signed in Rust with --mnemonic verifies in Node (Status:
valid).
Tests: 114 Rust + 99 Node total, zero regressions.
TUTORIAL.md updated in both rust/ and nodejs/ with the new section in
"Pick your primary key" plus a callout that --mnemonic can substitute
for --ed25519-seed throughout the rest of the tutorial.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Parallel of rust/TUTORIAL.md (d10dfb9), adapted for the Node.js
implementation. Same end-state for the reader: from "I have a nostr
nsec" to "I have a verified, published sigchain" in ~15 minutes.
Node-specific adaptations:
• Install (Node 22+ note for the built-in WebSocket the nostr
channel needs, npm 9+ workspaces, optional `npm link` for global
`kez` instead of `npm run cli --`).
• Every command uses `npm run cli --` to match the README's
existing convention; explicit "-- swallowed flags" callout.
• New section 8 "Programmatic use" — short snippet showing how to
sign + verify via @kez/core + @kez/channels for embedding in a
Node app. Cross-checked against the real exports
(newClaimPayload(subject, primary, date), signClaim(payload,
signer), await defaultRegistry(), registry.verify(...)).
• Cross-implementation interop callout: sign in Node, verify in
Rust (wire-compatible by design).
• Common-confusions FAQ gets one extra entry — "Is the Node version
slower than Rust?" (answer: I/O-bound on channels, both fine for
interactive use; Rust faster only for batch sigchain work).
• Troubleshooting adds "WebSocket is not defined → upgrade Node" for
the nostr channel.
README now points to TUTORIAL.md as the on-ramp, matching the Rust
README's structure.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
KEZ is a portable, decentralized identity graph: a person signs claims
linking their many accounts, publishes those claims in places only the
claimed account can publish to, and anyone can verify the connections
without trusting a central server.
Layout
------
- SPEC.md Language-agnostic protocol spec (v0.2)
- rust/ Rust implementation: kez-core, kez-channels, kez-cli
- nodejs/ TypeScript port at full parity
- rust-sig-server/ Optional axum + SQLite storage server for sigchains
- crosstest.sh Cross-implementation interop harness
Capabilities (both implementations, byte-compatible)
----------------------------------------------------
- Two primary-key algorithms: nostr/secp256k1 Schnorr (BIP-340) and
Ed25519 (RFC 8032). Identifiers: nostr:npub1... and ed25519:<hex>.
- JCS (RFC 8785) canonicalization for everything signed.
- Four proof encodings: JSON envelope, compact (kez:z1:<base64url(zstd(json))>),
Markdown fence, DNS TXT.
- Five channel plugins (no API keys, no auth needed for any of them):
dns: system resolver, _kez.<domain> TXT records
github: public gist scan + <user>/<user> profile README fallback
nostr: kind-30078 events from default relays
bluesky: public AppView author feed
ap: WebFinger + actor JSON (alias mastodon:)
- Identical CLI surface:
kez identity new [--key-type nostr|ed25519]
kez claim create <subject> (--nsec | --ed25519-seed) [--format ...] [--out ...]
kez claim dns <domain> (--nsec | --ed25519-seed)
kez verify file <path>
kez verify id <identifier>
kez sigchain add|revoke|show|export|publish
- Sigchains: append-only signed log per primary, hash-chained per spec §6,
stored locally at ~/.kez/sigchains/, exportable as JSONL or kez:zc1: bundle.
- Sigchain publish destinations: chain server, web (file dump), DNS (zone
record print), nostr (kind-30078 wrapping event).
kez-sig-server
--------------
Optional storage tier. Axum + SQLite, single binary, no external deps.
- No auth — the cryptography is the access control. The server validates
every signature, every seq, every prev hash before storing.
- REST API: POST /v1/sigchains/{scheme}/{id}/events (append signed event,
201 with new head hash or 4xx); GET /{scheme}/{id} (full chain as JSONL);
GET /head; GET /healthz.
- Designed for one central instance for now; the design doesn't preclude
running more later (clients gain a configurable list, verifiers
reconcile per spec §6.2).
- Channel-based publishing remains the always-available fallback if the
server is unavailable.
Tests
-----
- rust/ 99 tests
- rust-sig-server/ 10 integration tests (real HTTP, real SQLite)
- nodejs/ 91 tests (vitest)
- crosstest.sh 19 cross-impl scenarios — proves JCS bytes,
Schnorr + Ed25519 sigs, all four claim encodings,
and the sigchain JSONL bundle are byte-compatible
between Rust and Node in both directions.
What's not done yet
-------------------
- verify id consulting the sigchain for revocations (data path exists,
just not wired into the verifier output).
- rotate and add_device sigchain ops (types reserved).
- expires_at enforcement during claim verification.
- Typed VerificationStatus.status reflecting the five failure modes.
- Auth-required publishers (GitHub gist, Bluesky, ActivityPub).