4 Commits

Author SHA1 Message Date
aeba28d9e5 docs(rust,nodejs): expand TUTORIAL.md recovery-phrase section
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>
2026-06-05 22:53:59 -06:00
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
Jason Tudisco
b1f8b3a5fb docs(nodejs): add TUTORIAL.md — Node.js mirror of the Rust tutorial
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>
2026-05-30 00:19:18 -06:00
Tudisco
d0db6f00f1 Initial implementation of KEZ — protocol, two impls, and storage server
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).
2026-05-24 14:41:00 -06:00