From d0e96c17fb685d42cac7ebbf59c8b003e3e4cbb2 Mon Sep 17 00:00:00 2001 From: Jason Tudisco Date: Fri, 5 Jun 2026 22:57:52 -0600 Subject: [PATCH] docs(python): add TUTORIAL.md mirroring rust/nodejs + link from READMEs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the parallel tutorial set across all three implementations. Python now has the same friendly step-by-step walkthrough that the Rust and Node sides have had since the original tutorial commits. Python tutorial content mirrors the others 1:1, adapted for the Python invocation style (.venv/bin/python kez_cli.py …), plus: • Programmatic section uses Python imports (NostrSecret.from_nsec, sign_claim, default_registry, etc.) instead of the TS imports from the Node tutorial. • Same "Recovery phrases" mini-chapter as rust/nodejs — both 12-word AND 24-word are explained, with the entropy table, picking guide, hardware-wallet-incompatibility callout, concrete backup advice ("pencil + paper, numbered words, fireproof, don't split, don't permute"), and "Working with phrases later" examples (`identity mnemonic`, `identity from-mnemonic`). • Notes that `sigchain publish` isn't in the Python CLI yet (only add/revoke/show/export) — match the actual current surface; the JSONL the Python CLI produces is byte-compatible with Rust/Node, so users can build the chain in Python and publish via either of the other CLIs in the meantime. • Troubleshooting includes ModuleNotFoundError: kez (a Python- specific footgun when running outside the venv). • Links to ../rust/TUTORIAL.md and ../nodejs/TUTORIAL.md as parallel references throughout. python/README.md now opens with the same "New to KEZ? Read TUTORIAL.md" callout as the rust and nodejs READMEs do. Root README's quick-start blocks for each implementation now reference BOTH the impl README (reference) AND the impl TUTORIAL (step-by-step, on-ramp) instead of just the README. Co-Authored-By: Claude Opus 4.7 --- README.md | 10 +- python/README.md | 7 + python/TUTORIAL.md | 526 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 540 insertions(+), 3 deletions(-) create mode 100644 python/TUTORIAL.md diff --git a/README.md b/README.md index 1e3fc69..9aec743 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,9 @@ cargo test # 99 tests cargo install --path crates/kez-cli # → `kez` on PATH kez verify id github:jason ``` -Full guide: [`rust/README.md`](rust/README.md). +Full guide: [`rust/README.md`](rust/README.md) (reference) · +[`rust/TUTORIAL.md`](rust/TUTORIAL.md) (step-by-step, recommended +for newcomers). ### Node.js ```sh @@ -70,7 +72,8 @@ npm install npm test # 91 tests npm run cli -- verify id github:jason ``` -Full guide: [`nodejs/README.md`](nodejs/README.md). +Full guide: [`nodejs/README.md`](nodejs/README.md) (reference) · +[`nodejs/TUTORIAL.md`](nodejs/TUTORIAL.md) (step-by-step). ### Python ```sh @@ -79,7 +82,8 @@ python3 -m venv .venv .venv/bin/pip install -r requirements.txt .venv/bin/python kez_cli.py identity new ``` -Full guide: [`python/README.md`](python/README.md). +Full guide: [`python/README.md`](python/README.md) (reference) · +[`python/TUTORIAL.md`](python/TUTORIAL.md) (step-by-step). ### Sigchain storage server (optional) ```sh diff --git a/python/README.md b/python/README.md index b2f6be0..e3b75bc 100644 --- a/python/README.md +++ b/python/README.md @@ -39,6 +39,13 @@ python/ ## Setup +> **New to KEZ?** Read [**`TUTORIAL.md`**](TUTORIAL.md) — a friendly +> step-by-step walkthrough that takes you from "I have a nostr `nsec`" +> to "I have a verified, published sigchain," including the BIP-39 +> recovery-phrase backup (12 or 24 words). It assumes nothing. +> +> This README is the reference; the tutorial is the on-ramp. + ```sh cd python python3 -m venv .venv diff --git a/python/TUTORIAL.md b/python/TUTORIAL.md new file mode 100644 index 0000000..43d2508 --- /dev/null +++ b/python/TUTORIAL.md @@ -0,0 +1,526 @@ +# Tutorial — your first KEZ identity, end to end (Python) + +This is a hands-on walkthrough. By the end you'll have: + +- ✅ A KEZ identity tied to a key you already trust (your existing nostr + `nsec`, or a brand-new Ed25519 key with a 12- or 24-word backup + phrase). +- ✅ A signed proof that *you* control a GitHub account (or DNS domain, or + nostr handle, etc.) — verifiable by anyone, no central server needed. +- ✅ A sigchain that ties multiple identities together, exported in a + portable format, and published where strangers can find it. +- ✅ The ability to verify other people's identities the same way. + +If you've used [Keybase](https://keybase.io), the mental model is the same. +The difference: KEZ has no required central authority. Your proofs live +wherever you publish them; the verifier just walks the links. + +This is the Python implementation. It is **wire-compatible** with the +[Rust](../rust/TUTORIAL.md) and [Node](../nodejs/TUTORIAL.md) +implementations — a claim signed in any of the three verifies in the +other two. The repo-root [`crosstest.sh`](../crosstest.sh) proves it +across 84 scenarios. + +For the full protocol spec, see [`../SPEC.md`](../SPEC.md). This document +is the friendly cousin. + +> **Time budget:** 10–15 minutes for the first claim. A bit more if you +> want to set up DNS or a sigchain publish. + +--- + +## 0. Install + +You'll need **Python 3.10+** and standard build tooling for the +`cryptography` + `zstandard` native deps (clang/gcc on macOS / Linux, +or pre-built wheels on most platforms). + +```sh +git clone https://git.ptud.biz/DukeInc/Kez.git +cd Kez/python +python3 -m venv .venv +.venv/bin/pip install -r requirements.txt +``` + +Verify the CLI works: + +```sh +.venv/bin/python kez_cli.py --help +``` + +You should see subcommands `identity`, `claim`, `verify`, and `sigchain`. + +> **Want a global `kez` command instead?** From inside `python/` run +> `.venv/bin/pip install -e .` once. After that, plain `kez claim +> create …` works (provided your shell has `.venv/bin` on `PATH`, or +> you activate the venv). Substitute `kez` for `.venv/bin/python +> kez_cli.py` in every example below. + +> **Optional but recommended:** `export GITHUB_TOKEN=ghp_...` in your +> shell before verifying github claims. Anonymous GitHub limits you to +> 60 requests/hour; with a token it's 5000/hour. Any read-only token +> works; KEZ never sends it anywhere but `api.github.com`. + +--- + +## 1. Pick your primary key + +Your **primary key** is the one private key the rest of your identity +hangs off of. It signs every claim you make. Two choices: + +### Option A: use your existing nostr key (recommended if you have one) + +If you already use nostr (Damus, Amethyst, primal, etc.), you already +have an `nsec1...` private key. Use it. KEZ understands nostr keys +natively as Schnorr/secp256k1. + +Export the `nsec` from your nostr client (every client has a way — +usually Settings → Keys → Show / Export). Keep it secret; treat it the +same as a wallet seed. + +> **Warning.** Pasting your `nsec` into a CLI is fine on a machine you +> trust. Don't do it on a shared box, and consider whether you want +> shell history to remember it (`unset HISTFILE` for the session, or +> prefix the command with a space if `HISTCONTROL=ignorespace`). + +You don't need any command to "register" an existing nsec — just pass +it with `--nsec` on the first claim you sign. + +### Option B: generate a fresh primary + +A new nostr keypair: + +```sh +.venv/bin/python kez_cli.py identity new +``` + +Or a new Ed25519 keypair, which comes with a BIP-39 phrase alongside +the hex seed (both are equivalent backups): + +```sh +.venv/bin/python kez_cli.py identity new --key-type ed25519 # 24-word +.venv/bin/python kez_cli.py identity new --key-type ed25519 --mnemonic-words 12 # 12-word +``` + +Output (24-word, the default): + +``` +Primary: ed25519:7a3b4c… +Public: 7a3b4c… +Secret: 9e3f51… (32-byte seed) +Mnemonic (24 words): "abandon ability able about above absent academy accident…" +``` + +> **Save the backup.** Seed *or* phrase — at least one. Lose them both +> and the identity is gone. There's no recovery flow. + +### Recovery phrases — what's actually going on + +A KEZ recovery phrase is a [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) +mnemonic — the same 2048-word English wordlist that Bitcoin, Ethereum, +and most hardware wallets use. The words encode random bits: + +| Phrase length | Random bits | Resulting Ed25519 seed | +|---|---|---| +| **24 words** | 256 bits of entropy | The 32-byte seed *is* those 256 bits (1:1). Phrase ↔ seed round-trips. | +| **12 words** | 128 bits of entropy | 16 bytes → 32-byte seed via `SHA-256("kez-bip39-12-v1" \|\| entropy)`. Phrase → seed only (one-way). | + +#### Picking 12 vs 24 + +- **Pick 24 words** when you want full round-trip-ability — i.e. you'd + like to be able to *recover the phrase from the hex seed* at any time + in the future. Anyone's 32-byte Ed25519 secret can be re-encoded into + the unique 24-word phrase that produced it. Bigger security margin + (256 bits of entropy vs 128). +- **Pick 12 words** when you want a shorter thing to write down on + paper or remember. 128 bits of entropy is still enormously beyond + brute-forcing. The trade-off: the path is *one-way only* — you can + always derive the seed from the phrase, but you cannot derive the + phrase from the seed. So if you only ever have the seed, you'll + never know what 12-word phrase produced it. **Save the phrase + itself**, not just the resulting seed. + +Either way the resulting Ed25519 identity is exactly the same shape; +peers can't tell which word count you used. The choice is purely about +your backup ergonomics. + +#### ⚠ Not compatible with hardware-wallet derivations + +A KEZ 12-word phrase **does not** produce the same Bitcoin or Ethereum +key as the same 12 words typed into a Ledger or MetaMask, and vice +versa. The reasons are deliberate: + +1. Other wallets feed the phrase through BIP-39's PBKDF2 to get a + 64-byte "seed", then run that through BIP-32 hierarchical + derivation at a coin-specific path. KEZ doesn't — it takes the + raw entropy and uses it directly (24-word case) or hashes it with + a domain tag (12-word case). +2. KEZ identities aren't part of a derivation tree. There's one + identity per phrase; there's no path component. + +That means: **don't paste your existing hardware-wallet recovery +phrase into KEZ** expecting to get a key you've already seen. It'll +produce a *new* KEZ identity uncorrelated with anything else. + +Conversely: a KEZ phrase you saved is *only* useful for KEZ. A +malicious wallet that says "import this phrase" can't extract your +existing Bitcoin / Ethereum funds from a KEZ phrase, because the +phrase wasn't derived through the same path. + +#### Backing up — concrete advice + +The phrase is the master key to your identity. Practical guidance: + +- **Write it on paper, with a pencil. Number each word (1–12 or 1–24) + so you can later verify the order.** A photograph or cloud document + is one breach away from compromise. +- **Store the paper somewhere fireproof.** Safe-deposit boxes, lockable + desk drawers, etched-stainless-steel cards if you're paranoid. +- **Never type the phrase into a website, chat app, or password + manager that auto-syncs.** Local-only password managers (KeePassXC, + 1Password locked vault) are OK; cloud-synced managers are a softer + target. +- **Don't split it across two locations "for safety".** Half a BIP-39 + phrase weakens the entropy more than it protects against loss. If you + need redundancy, make two complete paper copies in different physical + locations. +- **Don't be cute.** Don't permute the words "because they're easy to + remember in this order." The wordlist position matters; reorder and + you change the key (and the BIP-39 checksum will reject it on + restore anyway). + +### Working with phrases later + +You can generate a fresh phrase without producing a key, or recover +the key from a phrase you wrote down earlier: + +```sh +# Print a fresh 24-word phrase (or 12, with --words 12). No key derived. +.venv/bin/python kez_cli.py identity mnemonic +.venv/bin/python kez_cli.py identity mnemonic --words 12 + +# Recover the Ed25519 key from a phrase. Word count auto-detected. +.venv/bin/python kez_cli.py identity from-mnemonic "abandon ability able about +above absent academy accident account accuse achieve acid acoustic acquire +across act action actor actress actual adapt add addict address" +``` + +The recovered output is identical, byte-for-byte, to what was printed +when you first ran `identity new` — same `Primary:`, same `Public:`, +same `Secret:`. + +Throughout the rest of this tutorial you can substitute +`--mnemonic "your phrase here"` anywhere `--ed25519-seed ` appears. +Both are accepted on every command that takes a signing key. + +For the rest of this tutorial we'll use a nostr key for examples and +write the secret as `nsec1FAKE...` — substitute your real one. + +--- + +## 2. Sign your first claim + +A **claim** is just a signed sentence: *"the key I signed this with also +controls ``."* The subject is a `system:identifier` string — +`github:tudisco`, `dns:tud.ink`, `nostr:npub1…`, etc. + +Say you want to prove you control the GitHub username `tudisco`. + +```sh +.venv/bin/python kez_cli.py claim create github:tudisco \ + --nsec nsec1FAKE... \ + --format markdown \ + --out github-tudisco.kez.md +``` + +That writes a file containing the human-readable header plus a +```kez``` fence with the raw JSON envelope inside (same shape as in the +[Rust tutorial](../rust/TUTORIAL.md#2-sign-your-first-claim)). + +### Picking the right format + +Same claim, three packagings — same signature inside: + +| Format | When to use | Command | +|---|---|---| +| **markdown** | Anywhere you can paste rich text — gists, profile READMEs, social posts. Most human-readable. | `--format markdown` | +| **compact** | Tight places: DNS TXT records, QR codes, chat messages. One-liner that decompresses back to the full envelope. | `--format compact` | +| **json** | Self-hosted `.well-known/kez.json`, developer tooling, anything that wants the raw envelope. | (default — no flag needed) | + +If you skip `--out`, the proof prints to stdout — handy for piping. + +--- + +## 3. Publish the proof + +Same rules as the [Rust](../rust/TUTORIAL.md#3-publish-the-proof) and +[Node](../nodejs/TUTORIAL.md#3-publish-the-proof) tutorials — pick the +section that matches your subject system. GitHub gist or profile +README, DNS TXT at `_kez.`, nostr (profile bio / kind-1 post / +kind-30078), Bluesky post, ActivityPub profile field, your own +`/.well-known/kez.json`. + +The `dns:` shortcut prints a ready-to-paste zone file line: + +```sh +.venv/bin/python kez_cli.py claim dns tud.ink --nsec nsec1FAKE... +``` + +--- + +## 4. Verify it + +```sh +.venv/bin/python kez_cli.py verify id github:tudisco +``` + +Output: + +``` +Primary: nostr:npub1tkf... + +Verified identities: +- github:tudisco + +Status: valid +Confidence: strong +``` + +Works against any channel — `dns:`, `github:`, `nostr:`, `bluesky:`, +`ap:`, `mastodon:`. The verifier fetches the proof from where you +published it, decodes the envelope, and verifies the cryptographic +signature against the embedded public key. + +**No KEZ server was involved.** Each side proves the claim +independently — that's the whole point. + +### Cross-implementation verification + +Wire-compatible with the [Rust](../rust/TUTORIAL.md) and +[Node](../nodejs/TUTORIAL.md) CLIs. You can sign in any and verify in +any other: + +```sh +# Sign in Python… +.venv/bin/python kez_cli.py claim create github:tudisco \ + --mnemonic "your 12 or 24 words…" --out p.kez.json + +# …verify in Rust +cd ../rust && cargo run -p kez-cli -- verify file ../python/p.kez.json + +# …or verify in Node +cd ../nodejs && npm run cli -- verify file ../python/p.kez.json +``` + +Same bytes, same signature, all three implementations agree. The repo +root's `crosstest.sh` exercises this for every (signer, verifier, +format) combination. + +### If verification fails + +- **`not_found`** — proof isn't where the verifier looked. For + GitHub, check the gist is public and the filename contains `kez`. For + DNS, the TXT record is at `_kez.`, not `` itself; + give propagation a minute. +- **`subject_mismatch`** — you published a proof for one subject but + asked the verifier to check a different one. +- **`invalid_signature`** — proof was tampered with, or you re-signed + with a different key after publishing. Re-sign and re-publish. +- **GitHub `403 rate_limited`** — anonymous gets 60 req/hr; export + `GITHUB_TOKEN`. +- **`ModuleNotFoundError: kez`** — you ran a `python` not from the + venv. Use `.venv/bin/python kez_cli.py …` (the launcher inserts the + package dir into `sys.path`). + +--- + +## 5. Sigchain — link multiple identities together + +A **sigchain** is an append-only log of "this key controls X" events, +each signed by your primary. Once you have more than one claim, you +want a sigchain so verifiers can discover your full identity graph +from a single starting point, and so you can later **revoke** a claim +without invalidating the others. + +Chains live at `~/.kez/sigchains/.jsonl`. The CLI +creates the directory on first use. + +```sh +.venv/bin/python kez_cli.py sigchain add github:tudisco --nsec nsec1FAKE... +.venv/bin/python kez_cli.py sigchain add dns:tud.ink --nsec nsec1FAKE... +.venv/bin/python kez_cli.py sigchain show --nsec nsec1FAKE... +.venv/bin/python kez_cli.py sigchain revoke github:tudisco --nsec nsec1FAKE... +``` + +Read-only view of someone else's chain (no secret needed): + +```sh +.venv/bin/python kez_cli.py sigchain show --primary nostr:npub1tkf... +``` + +### Exporting + +```sh +.venv/bin/python kez_cli.py sigchain export --nsec nsec1FAKE... --format jsonl +.venv/bin/python kez_cli.py sigchain export --nsec nsec1FAKE... --format compact +``` + +> **Note.** The Python CLI currently supports `sigchain add`, +> `revoke`, `show`, and `export`. `sigchain publish` (the one that +> POSTs to a kez-sig-server or writes a `.well-known/` bundle) is +> available in the Rust and Node CLIs; porting it to Python is a +> short follow-up. Until then, you can `export` the chain and upload +> it manually, or use the Rust/Node CLI for publishing the chain +> built by Python (the on-disk JSONL is byte-compatible). + +--- + +## 6. Verifying someone else + +You've done the publishing side. Here's the receiving side — verify +someone *else's* identity: + +```sh +# Start from any identifier they've published a proof for: +.venv/bin/python kez_cli.py verify id github:linus + +# Walk their chain from any known endpoint: +.venv/bin/python kez_cli.py sigchain show --primary nostr:npub1abc... +``` + +If you have the chain bundle on disk: + +```sh +.venv/bin/python kez_cli.py verify file ./their-chain.jsonl +``` + +`verify id` is the friendly day-to-day verb. `sigchain show +--primary ` is what you'd reach for to see the whole graph at once. + +--- + +## 7. Programmatic use — embedding KEZ in a Python app + +You don't have to go through the CLI. The same logic is exported by the +`kez` package. + +```python +from kez.identity import Identity +from kez.keys import NostrSecret +from kez.envelope import sign_claim, new_claim_payload +from kez.encodings import to_markdown +from kez.channels import default_registry + +# Sign a claim +secret = NostrSecret.from_nsec("nsec1FAKE...") +subject = Identity.parse("github:tudisco") +payload = new_claim_payload(subject, secret.identity(), None) # None = now +claim = sign_claim(payload, secret) +print(to_markdown(claim)) + +# Verify a peer +registry = default_registry() +hit = registry.verify(Identity.parse("dns:tud.ink")) +print(hit.status) # "valid" +``` + +For mnemonic helpers: + +```python +from kez.mnemonic import ( + generate_mnemonic, + seed_from_mnemonic, + mnemonic_from_seed_24, + ed25519_from_mnemonic, + generate_ed25519_with_mnemonic, +) + +# Round-trip a 24-word phrase +secret, phrase = generate_ed25519_with_mnemonic(24) +assert ed25519_from_mnemonic(phrase).pubkey_hex() == secret.pubkey_hex() +``` + +The implementations themselves are short (a few hundred lines each); +the package modules are well-named and read as documentation. See +[`kez/`](kez/) for the full surface. + +--- + +## 8. Quick reference card + +```sh +# Generate a fresh primary +.venv/bin/python kez_cli.py identity new +.venv/bin/python kez_cli.py identity new --key-type ed25519 # 24-word phrase +.venv/bin/python kez_cli.py identity new --key-type ed25519 --mnemonic-words 12 # 12-word phrase +.venv/bin/python kez_cli.py identity mnemonic [--words 12|24] # phrase only +.venv/bin/python kez_cli.py identity from-mnemonic "" # recover key + +# Sign a claim +.venv/bin/python kez_cli.py claim create --nsec +.venv/bin/python kez_cli.py claim create --ed25519-seed +.venv/bin/python kez_cli.py claim create --mnemonic "" +.venv/bin/python kez_cli.py claim create --nsec --format markdown --out file.md +.venv/bin/python kez_cli.py claim create --nsec --format compact +.venv/bin/python kez_cli.py claim dns --nsec # zone-file output + +# Verify +.venv/bin/python kez_cli.py verify id # live channel fetch +.venv/bin/python kez_cli.py verify file # local file + +# Sigchain +.venv/bin/python kez_cli.py sigchain add --nsec [--proof-url ] +.venv/bin/python kez_cli.py sigchain revoke --nsec +.venv/bin/python kez_cli.py sigchain show --nsec # your own +.venv/bin/python kez_cli.py sigchain show --primary # someone else's +.venv/bin/python kez_cli.py sigchain export --nsec --format jsonl|compact [--out file] +``` + +--- + +## 9. Common confusions + +**"Do I need a sigchain to use KEZ?"** No. A single signed claim, +published, works on its own. + +**"Why two key types — nostr and ed25519?"** Different ecosystems use +different curves. Nostr is secp256k1/Schnorr; the rest of the world +mostly likes Ed25519. KEZ supports both natively so you can use the +key you already have rather than spinning up a new one for KEZ. + +**"Is my `nsec` sent to KEZ servers?"** No, never. The CLI uses it +locally to sign things. Only the *signed envelope* (public key + claim ++ signature) ever leaves your machine. + +**"What if I publish a proof and someone copies it as theirs?"** +They can copy the bytes, but the signature inside is over *your* +primary. Their primary won't match, so any verifier sees through it +immediately. + +**"What if my key is compromised?"** Append a `sigchain revoke +` for the affected subjects, and ideally rotate to a new +primary by signing a "this primary is succeeded by " event +(planned for the spec; not yet enforced). + +**"Why is the Python version slower than Rust on imports?"** The +`cryptography` C extension lazy-loads on first use, so the very first +`identity new` or `verify` after a fresh shell can take an extra ~100 +ms. Steady-state is comparable to Node; both are I/O-bound on the +channel HTTP calls for `verify id`. + +--- + +## 10. Where to go next + +- The web client at — same protocol, no CLI. +- [`../SPEC.md`](../SPEC.md) — the formal protocol. +- [`../rust/TUTORIAL.md`](../rust/TUTORIAL.md) and + [`../nodejs/TUTORIAL.md`](../nodejs/TUTORIAL.md) — same tutorial for + the other two implementations. +- [`../rust-sig-server/`](../rust-sig-server/) — run your own + sig-server. +- The channel module in [`kez/channels.py`](kez/channels.py) — add a + new channel in an afternoon (each channel implementation is ~30–80 + lines). + +That's the whole tutorial. Welcome to KEZ.