Jason Tudisco 0058d9b421 feat(rust,nodejs): BIP-39 mnemonic phrases for Ed25519 identities
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>
2026-06-05 17:41:01 -06:00

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: single kez package, virtualenv setup, crypto stack rationale (pure-Python BIP-340 Schnorr + cryptography for 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
114 Rust ↔ Node: JSON / compact / markdown / DNS claims, nostr + ed25519
1520 Rust ↔ Node sigchains: build in one, parse + show in the other; JSONL byte parity
2144 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: (alias mastodon:).
  • 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 id consulting 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 — every verify is still a single one-shot proof check.
  • rotate and add_device sigchain ops.
  • expires_at enforcement during claim verify.
  • Typed VerificationStatus.status reflecting 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.

Description
No description provided
Readme 1.7 MiB
Languages
TypeScript 38.9%
Rust 31.8%
Svelte 18.2%
Python 5.4%
Shell 2.8%
Other 2.9%