docs(python): add TUTORIAL.md mirroring rust/nodejs + link from READMEs

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 <noreply@anthropic.com>
This commit is contained in:
Jason Tudisco 2026-06-05 22:57:52 -06:00
parent aeba28d9e5
commit d0e96c17fb
3 changed files with 540 additions and 3 deletions

View File

@ -61,7 +61,9 @@ cargo test # 99 tests
cargo install --path crates/kez-cli # → `kez` on PATH cargo install --path crates/kez-cli # → `kez` on PATH
kez verify id github:jason 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 ### Node.js
```sh ```sh
@ -70,7 +72,8 @@ npm install
npm test # 91 tests npm test # 91 tests
npm run cli -- verify id github:jason 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 ### Python
```sh ```sh
@ -79,7 +82,8 @@ python3 -m venv .venv
.venv/bin/pip install -r requirements.txt .venv/bin/pip install -r requirements.txt
.venv/bin/python kez_cli.py identity new .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) ### Sigchain storage server (optional)
```sh ```sh

View File

@ -39,6 +39,13 @@ python/
## Setup ## 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 ```sh
cd python cd python
python3 -m venv .venv python3 -m venv .venv

526
python/TUTORIAL.md Normal file
View File

@ -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:** 1015 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 (112 or 124)
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 <hex>` 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 `<subject>`."* 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.<domain>`, 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.<domain>`, not `<domain>` 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/<safe-primary>.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 <id>` 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 "<phrase>" # recover key
# Sign a claim
.venv/bin/python kez_cli.py claim create <subject> --nsec <nsec>
.venv/bin/python kez_cli.py claim create <subject> --ed25519-seed <hex-seed>
.venv/bin/python kez_cli.py claim create <subject> --mnemonic "<phrase>"
.venv/bin/python kez_cli.py claim create <subject> --nsec <nsec> --format markdown --out file.md
.venv/bin/python kez_cli.py claim create <subject> --nsec <nsec> --format compact
.venv/bin/python kez_cli.py claim dns <domain> --nsec <nsec> # zone-file output
# Verify
.venv/bin/python kez_cli.py verify id <subject> # live channel fetch
.venv/bin/python kez_cli.py verify file <path> # local file
# Sigchain
.venv/bin/python kez_cli.py sigchain add <subject> --nsec <nsec> [--proof-url <url>]
.venv/bin/python kez_cli.py sigchain revoke <subject> --nsec <nsec>
.venv/bin/python kez_cli.py sigchain show --nsec <nsec> # your own
.venv/bin/python kez_cli.py sigchain show --primary <id> # someone else's
.venv/bin/python kez_cli.py sigchain export --nsec <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
<subject>` for the affected subjects, and ideally rotate to a new
primary by signing a "this primary is succeeded by <new>" 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 <https://kez.lat> — 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 ~3080
lines).
That's the whole tutorial. Welcome to KEZ.