Completes the three-way BIP-39 mnemonic surface (Rust + Node landed in
0058d9b) and pins down byte-for-byte agreement with crosstest scenarios.
Python (mirrors rust/crates/kez-core/src/mnemonic.rs + nodejs's mnemonic.ts):
• python/kez/mnemonic.py — generate_mnemonic, seed_from_mnemonic,
mnemonic_from_seed_24, ed25519_from_mnemonic,
generate_ed25519_with_mnemonic. Same 24-word-bijection / 12-word-
SHA-256-domain-tagged semantics. Uses Trezor's `mnemonic` library
(v0.21) for the BIP-39 wordlist + entropy parsing; deliberately does
NOT use BIP-39's PBKDF2 to_seed function.
• python/kez/keys.py — Ed25519Secret.from_mnemonic() +
generate_with_mnemonic() classmethods; signer_from_flags widened to
accept --mnemonic.
• python/kez/cli.py — identity new --mnemonic-words, identity
mnemonic [--words], identity from-mnemonic; --mnemonic flag on
claim create/dns and sigchain add/revoke/show/export. Output format
matches Rust + Node verbatim so the crosstest harness can grep
Primary/Public/Secret/Mnemonic lines.
• python/tests/test_mnemonic.py — 19 tests covering all three
canonical vectors (exact-match Secret + Public hex), round-trip,
determinism, whitespace tolerance, bad-checksum, bad-word-count,
the literal domain-tag bytes, and the 12-vs-24 entropy-overlap
non-collision case.
Note: --mnemonic is NOT added to `sigchain publish` because that
subcommand doesn't exist in the Python CLI yet (rust + node only). When
the publish surface is ported, --mnemonic should follow it the same way.
Ground truth — python/MNEMONIC-TEST-VECTORS.md:
V1: 24-word zero-entropy phrase ("abandon… art")
seed = 0000…0000
pubkey = 3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29
V2: 12-word zero-entropy phrase ("abandon… about")
seed = 09451c0f06588db78205e32a793536e15ae263c8f9ee6d14f5c6fd82b8bd20da
pubkey = 9403c32e0d3b4ce51105c0bcac09a0d73be0cca98a6bf7b3cd434651be866d70
V3: 12-word "legal winner thank year wave sausage worth useful legal winner thank yellow"
seed = 9df434a2bd5dc767ee949d8ab95ca09c4ebbb88cefc3d0b1523f6b2a744ca824
pubkey = cc99d06b15ccb83a5ca43f25dd3d27f50638c1c6fbe3a822352da3e07156ce03
The domain tag for the 12-word derivation is exactly the 15 ASCII
bytes of "kez-bip39-12-v1", documented in the spec doc.
crosstest.sh — new "BIP-39 mnemonic interop" section:
• Vector match: each impl × each vector × Public hex == expected (9
scenarios). Catches any silent derivation drift.
• Cross-impl claim signing via --mnemonic: every signer ↔ verifier
pair (rust↔node, rust↔py, node↔py), every format (json/compact/
markdown). 6 pairings × 3 formats = 18 scenarios.
• Bijection sanity: the 24-word phrase printed by `identity from-
mnemonic` round-trips to itself byte-for-byte (rust + node).
• Python-involving scenarios auto-skip if `python/.venv/bin/python
kez_cli.py identity from-mnemonic` returns non-zero, so the harness
stays runnable on machines where Python isn't set up.
Verified end-to-end: `bash crosstest.sh` reports
"All 84 scenarios passed."
Test totals across implementations:
Rust: 114 (9 mnemonic-specific in kez-core)
Node: 99 (8 mnemonic-specific in @kez/core)
Python: 19 (mnemonic only; was no test suite before)
Crosstest: 84 scenarios end-to-end
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
KEZ
KEZ is a portable, decentralized identity graph. It lets a person say:
"These accounts, keys, domains, and identities are all me."
…without depending on any central authority. Every connection is proven by a cryptographic signature against a key the user already controls (a nostr key, an Ed25519 key, etc.), and the proofs are published in places only the claimed account itself can publish to (their gist, their DNS, their nostr relay event). Anyone can verify the graph without trusting a server.
Repository layout
.
├── SPEC.md ← The protocol. Language-agnostic, normative.
├── rust/ ← Rust implementation (kez-core, kez-channels, kez-cli)
├── nodejs/ ← TypeScript/Node implementation (same shape, same CLI)
├── python/ ← Python implementation (same shape, same CLI)
├── rust-sig-server/ ← Optional HTTP store for sigchains (axum + SQLite)
├── crosstest.sh ← Interop test: artifacts move between implementations
└── README.md ← (this file)
Three parallel implementations. Wire-compatible: a claim signed in Rust verifies in Node and Python and vice versa, in every direction. The cross-test harness proves it.
A separate rust-sig-server/ crate provides an optional
HTTP storage tier for sigchains — useful when a user doesn't want to set up
DNS/hosting/nostr, but never required; the protocol stays decentralized.
Documentation
Start here:
SPEC.md— the language-agnostic protocol spec (v0.2). Normative for every implementation.rust/README.md— Rust implementation guide: crate layout (kez-core/kez-channels/kez-cli), full CLI reference, channel plugin model, library examples, and the gap list.nodejs/README.md— Node/TypeScript port: same shape as Rust, npm workspaces layout, crypto stack rationale, CLI reference.python/README.md— Python port: singlekezpackage, virtualenv setup, crypto stack rationale (pure-Python BIP-340 Schnorr +cryptographyfor Ed25519), CLI reference.rust-sig-server/README.md— the optional storage server: API reference, no-auth design + threat model, deployment recipes (bare-metal, Docker, PaaS), and how channel-based publishing remains the fallback if the server is down.
Quick start
Rust
cd rust
cargo build
cargo test # 99 tests
cargo install --path crates/kez-cli # → `kez` on PATH
kez verify id github:jason
Full guide: rust/README.md.
Node.js
cd nodejs
npm install
npm test # 91 tests
npm run cli -- verify id github:jason
Full guide: nodejs/README.md.
Python
cd python
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python kez_cli.py identity new
Full guide: python/README.md.
Sigchain storage server (optional)
cd rust-sig-server
cargo build --release
./target/release/kez-sig-server # listens on :7878
Full guide: rust-sig-server/README.md.
Cross-testing
./crosstest.sh
Runs 55 scenarios that swap implementations at the artifact boundary:
| # | Scenarios |
|---|---|
| 1–14 | Rust ↔ Node: JSON / compact / markdown / DNS claims, nostr + ed25519 |
| 15–20 | Rust ↔ Node sigchains: build in one, parse + show in the other; JSONL byte parity |
| 21–44 | Python ↔ Rust and Python ↔ Node claims: every format × key type, both directions |
| — | Python ↔ both peers DNS zone form, both directions |
| — | Python ↔ both peers sigchains: build/show both ways, JSONL byte parity, ed25519 |
If all 55 pass: JCS canonicalization, both signature suites (BIP-340 Schnorr
and Ed25519), the compact kez:z1: zstd+base64url encoding, the Markdown
fence, the DNS TXT shape, and the sigchain JSONL bundle format are all
byte-compatible across all three implementations.
Pass -v for verbose output (echoes intermediate commands and proofs).
What ships in v0.2
- Five channel plugins in each implementation:
dns:,github:,nostr:,bluesky:,ap:(aliasmastodon:). - Four wire encodings: JSON, compact, Markdown fence, DNS TXT.
- Two primary-key algorithms: nostr/secp256k1 Schnorr (BIP-340) and Ed25519 (RFC 8032).
- JCS (RFC 8785) canonicalization for everything signed.
- No API keys required for any channel.
What's not done yet
Tracked in rust/README.md and the
spec:
verify idconsulting the sigchain. Sigchain types, CLI commands (kez sigchain add/revoke/show/export/publish), and the storage server all exist. But proof verification doesn't yet fetch the chain to check for revocations — everyverifyis still a single one-shot proof check.rotateandadd_devicesigchain ops.expires_atenforcement during claim verify.- Typed
VerificationStatus.statusreflecting the five failure modes (valid/revoked/expired/unreachable/fork). - Auth-required publishers (GitHub gist, Bluesky, ActivityPub).
License
Dual-licensed under MIT or Apache-2.0.