Kez/python/TUTORIAL.md
Jason Tudisco d0e96c17fb 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>
2026-06-05 22:57:52 -06:00

19 KiB
Raw Blame History

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, 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 and Node implementations — a claim signed in any of the three verifies in the other two. The repo-root crosstest.sh proves it across 84 scenarios.

For the full protocol spec, see ../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).

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:

.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:

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:

.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):

.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 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:

# 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.

.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).

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 and Node 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:

.venv/bin/python kez_cli.py claim dns tud.ink --nsec nsec1FAKE...

4. Verify it

.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 and Node CLIs. You can sign in any and verify in any other:

# 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).

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.

.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):

.venv/bin/python kez_cli.py sigchain show --primary nostr:npub1tkf...

Exporting

.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:

# 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:

.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.

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:

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/ for the full surface.


8. Quick reference card

# 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 " 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

That's the whole tutorial. Welcome to KEZ.