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