Kez/python
Jason Tudisco b0cc1a74a0 feat(python,crosstest): mirror BIP-39 mnemonic to Python + add interop scenarios
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>
2026-06-05 17:50:34 -06:00
..

KEZ — Python Implementation

KEZ is a portable, decentralized identity graph. It lets one person say:

"These accounts, keys, domains, and identities are all me."

…without depending on any central authority. Every connection is proven by a signature against a key the user already controls. The protocol is specified in ../SPEC.md; this directory is the Python implementation of that spec.

It is wire-compatible with the Rust and Node implementations: a claim signed here verifies there and vice versa, in every direction. The repo-root crosstest.sh proves it.


What's in this directory

python/
├── pyproject.toml          Package metadata + entry point (`kez`)
├── requirements.txt        Runtime deps (cryptography, zstandard)
├── kez_cli.py              Standalone launcher (used by ../crosstest.sh)
└── kez/
    ├── jcs.py              RFC 8785 JSON canonicalization
    ├── bech32.py           Bech32 (nsec/npub) encode/decode
    ├── schnorr.py          Pure-Python BIP-340 Schnorr over secp256k1
    ├── identity.py         `system:identifier` parsing + normalization
    ├── keys.py             NostrSecret / Ed25519Secret signers + verification
    ├── envelope.py         Envelope, claim & sigchain-event payloads, sign/verify
    ├── encodings.py        JSON / compact (kez:z1:) / markdown / DNS / JSONL bundle
    ├── sigchain.py         Append-only signed sigchain + on-disk storage
    ├── channels.py         parse_proof across all four wire encodings
    └── cli.py              The `kez` command-line interface

Setup

cd python
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

Then run the CLI either through the launcher or the installed entry point:

.venv/bin/python kez_cli.py identity new
# or, after `.venv/bin/pip install -e .`:
.venv/bin/kez identity new

Crypto stack

Concern Choice Why
JCS (RFC 8785) hand-rolled (jcs.py) KEZ payloads are strings/ints/objects only; a tiny dependency-free canonicalizer guarantees byte-identical output
secp256k1 Schnorr (BIP-340) pure-Python reference (schnorr.py) the native coincurve/secp256k1 bindings fail to build on recent CPython; signing fixed-size digests is fast enough for a CLI. Signs with zero aux-rand to match Rust/Node exactly
Ed25519 (RFC 8032) cryptography well-maintained, ships wheels
zstd zstandard level 3, matching the other impls; decompressobj handles frames without a content-size header
Bech32 hand-rolled (bech32.py) the BIP-173 reference is small and avoids a dependency

All signing is deterministic, so the same claim signs identically every time.


CLI reference

kez identity new [--key-type nostr|ed25519]

kez claim create <subject> (--nsec <nsec> | --ed25519-seed <hex>)
                 [--format json|compact|markdown] [--out <path>]
kez claim dns <domain>     (--nsec <nsec> | --ed25519-seed <hex>)

kez verify file <path>

kez sigchain add    <subject> (--nsec | --ed25519-seed) [--proof-url <url>]
kez sigchain revoke <subject> (--nsec | --ed25519-seed)
kez sigchain show   [--primary <id> | --nsec | --ed25519-seed]
kez sigchain export [--primary <id> | --nsec | --ed25519-seed]
                    [--format jsonl|compact] [--out <path>]

Sigchain state lives in ~/.kez/sigchains/<primary-with-colons-as-underscores>.jsonl — the same paths the Rust and Node CLIs use, so chains built by one are readable by the others.


What's not done yet

Matching the gap list in ../rust/README.md, the Python CLI implements claim, verify file, and sigchain add/revoke/show/export. Not yet ported: verify id channel resolution (network fetch), sigchain publish, and the rotate/add_device ops.

License

Dual-licensed under MIT or Apache-2.0.