# Tutorial — your first KEZ identity, end to end 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). - ✅ 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. 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 ```sh git clone https://git.ptud.biz/DukeInc/Kez.git cd Kez/rust cargo build --release cargo install --path crates/kez-cli # puts `kez` in ~/.cargo/bin ``` Verify: ```sh kez --help ``` You should see subcommands `identity`, `claim`, `verify`, and `sigchain`. > **Don't want to install globally?** Replace every `kez` below with > `cargo run -p kez-cli --` (from the `rust/` directory). Slower to > start each time, but no install side effects. > **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 can confirm KEZ accepts your key without signing anything yet: ```sh kez identity new --key-type nostr # only if you want a NEW key # vs. # (no command needed to "register" an existing nsec — just pass it # directly with --nsec on the first claim you sign) ``` ### Option B: generate a fresh Ed25519 primary If you'd rather start clean, generate a new Ed25519 key with a BIP-39 recovery phrase you can write down on paper: ```sh kez identity new --key-type ed25519 # 24-word phrase (default) kez identity new --key-type ed25519 --mnemonic-words 12 # 12-word phrase ``` 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…" ``` You now have **two equivalent backups** — the hex seed *and* the 24-word BIP-39 phrase. Either restores the same identity. Most people back up the phrase (easier to write down, easier to verify by hand). > **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 that 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. kez identity mnemonic kez identity mnemonic --words 12 # Recover the Ed25519 key from a phrase. Word count auto-detected. kez 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 kez claim create github:tudisco \ --nsec nsec1FAKE... \ --format markdown \ --out github-tudisco.kez.md ``` That writes a file like: ```markdown # KEZ Proof This account publishes a signed KEZ identity claim. - Primary: `nostr:npub1tkf…` - Subject: `github:tudisco` - Created: `2026-05-27T19:21:46Z` ```kez { "kez": "claim", "payload": { ... }, "signature": { "alg": "ed25519-sha512-jcs" / "nostr-schnorr-bip340-jcs", "key": "nostr:npub1tkf…", "sig": "abc123…" } } ``` ``` ### 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 This is where KEZ does its job: you put the signed claim in a place that only *that specific account* could have put it. Anyone who can fetch that place can then verify it themselves. Pick the section that matches the subject system you claimed. ### GitHub You signed `github:tudisco`. Publish the markdown block to either: **A public gist named `kez.md`** — easiest. 1. Go to . 2. New gist → filename `kez.md` → paste the contents of `github-tudisco.kez.md`. 3. Click **Create public gist**. **Or your profile README** — fancier but you only get one. 1. Make a repo named the same as your username (e.g. `tudisco/tudisco`). GitHub treats it as your profile README. 2. Add the markdown block to `README.md`. 3. Push. KEZ's GitHub verifier checks public gists first, then the profile README. ### DNS — your own domain You signed `dns:tud.ink`. The CLI generates a ready-to-paste zone-file line for you: ```sh kez claim dns tud.ink --nsec nsec1FAKE... ``` Output (abbreviated): ``` _kez.tud.ink. 3600 IN TXT "kez:z1:KLUv_WAsACUHAD……" "…" ``` Add that TXT record at `_kez.` in your DNS provider's console (Cloudflare, Route 53, Gandi, Porkbun — wherever you registered the domain). Most providers will accept the whole compact string in one field and split it for you; the multi-chunk form above is the safe one for providers that don't. Wait a minute or two for propagation, then you can verify it. ### Nostr — your own npub You signed `nostr:npub1...`. Three places work (verifiers check all of them): - **Profile `about` field** (kind-0 event) — easiest, one-time. Edit your nostr profile and paste the markdown block into your bio. - **A normal post** (kind-1) containing the markdown block — quickest if you're already active. - **A NIP-78 kind-30078 event** with `d` tag = `kez` — cleanest for tooling, but most clients don't expose it. ### Bluesky Post the markdown block (or just the compact `kez:z1:…` string) as a public post on the account you claimed. The verifier scans your recent posts. ### Mastodon / ActivityPub You signed `ap:@user@instance`. Add the markdown block to your profile **metadata** field (most instances expose 4 of them), or post it as a pinned toot. The verifier resolves via WebFinger → actor JSON → checks those fields. ### Your own website You signed `web:https://example.com`. Upload the JSON form to `https://example.com/.well-known/kez.json`: ```sh kez claim create web:https://example.com --nsec nsec1FAKE... > kez.json scp kez.json youruser@example.com:/var/www/.well-known/kez.json ``` Make sure it's publicly fetchable (no auth gate). --- ## 4. Verify it This is the moment of truth. Pretend you're a stranger checking that the claim is real: ```sh kez verify id github:tudisco ``` Output: ``` Primary: nostr:npub1tkf... Verified identities: - github:tudisco Status: valid Confidence: strong ``` Same shape for any channel: ```sh kez verify id dns:tud.ink kez verify id nostr:npub1tkf... kez verify id bluesky:tudisco.bsky.social kez verify id ap:@tudisco@mastodon.social kez verify id web:https://tud.ink ``` The verifier: 1. Figured out which channel from the prefix. 2. Fetched the proof from where you published it (gist, TXT, etc.). 3. Decoded the envelope. 4. Verified the cryptographic signature against the key inside. **No KEZ server was involved.** Each side of the conversation independently proves the claim — that's the whole point. ### If verification fails A few common ones: - **`not_found`** — the 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. The claim's `subject` must equal the identifier you're verifying. - **`invalid_signature`** — the 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`. --- ## 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. - You can later **revoke** a claim (e.g., you lost access to that github account) without invalidating the others. - Old events stay verifiable; the chain head is the current truth. Chains live at `~/.kez/sigchains/.jsonl`. The CLI creates the directory on first use; you don't manage it manually. Add the github claim you already signed: ```sh kez sigchain add github:tudisco --nsec nsec1FAKE... ``` Add a DNS claim too: ```sh kez sigchain add dns:tud.ink --nsec nsec1FAKE... ``` You can optionally include a `--proof-url` pointing to where you published this claim's proof (your gist URL, etc.). Verifiers can use it to skip discovery. Inspect what you've got: ```sh kez sigchain show --nsec nsec1FAKE... ``` Output: ``` Primary: nostr:npub1tkf... Path: /home/you/.kez/sigchains/nostr_npub1tkf….jsonl Length: 2 events Head: sha256:9c3a… Events: 1. add github:tudisco proof_url=https://gist.github.com/tudisco/abc 2. add dns:tud.ink ``` Read-only view of a published chain (no secret needed): ```sh kez sigchain show --primary nostr:npub1tkf... ``` This is what other people will do to inspect your identity graph. ### Revoking If you ever lose control of an account (your github gets hacked, you sell a domain), revoke that subject: ```sh kez sigchain revoke github:tudisco --nsec nsec1FAKE... ``` That appends a revoke event. Subsequent verifications treat that subject as "no longer claimed" by your primary, even if the old proof is still out there. --- ## 6. Publish your sigchain Now make your chain discoverable so anyone with your primary can walk it. Options, in rough order of how much infra they need: ### To a kez-sig-server (zero setup) If you have access to a [`kez-sig-server`](../rust-sig-server/) (one runs at `https://sig.kez.lat`): ```sh kez sigchain publish --nsec nsec1FAKE... \ --server https://sig.kez.lat ``` Each event is POSTed to the server, which exposes them at predictable URLs. Cheap, fast, but you're trusting that server to stay up. Mitigate by also publishing to one of the channels below. ### To your own website (self-sovereign) Export the chain bundle and host it yourself: ```sh kez sigchain publish --nsec nsec1FAKE... \ --web --out kez-sigchain.jsonl ``` Then upload `kez-sigchain.jsonl` to `https:///.well-known/kez-sigchain.jsonl`. Verifiers fetch it directly. Hardest to censor; you own it. ### To DNS ```sh kez sigchain publish --nsec nsec1FAKE... --dns tud.ink ``` Prints a TXT record at `_kez-chain.` containing the compressed chain. Add it to your zone. Works for short chains; for long chains, prefer `--web` (TXT records are size-limited). ### To nostr ```sh kez sigchain publish --nsec nsec1FAKE... \ --nostr wss://relay.damus.io ``` Publishes the compact bundle as a kind-30078 event on that relay. Any nostr client / verifier subscribed can find it. ### Pick more than one `publish` accepts any combination of these flags — you can mirror to all four in one shot: ```sh kez sigchain publish --nsec nsec1FAKE... \ --server https://sig.kez.lat \ --web --out kez-sigchain.jsonl \ --dns tud.ink \ --nostr wss://relay.damus.io ``` Redundancy is good. If one channel goes down, the others still serve your identity graph. ### Export-only (no publish) If you want to see the bundle without publishing: ```sh kez sigchain export --nsec nsec1FAKE... --format compact > my-chain.txt kez sigchain export --nsec nsec1FAKE... --format jsonl > my-chain.jsonl ``` --- ## 7. Verifying someone else You've done the publishing side. Here's the receiving side — how to verify someone *else's* identity: ```sh # Start from any identifier they've published a proof for. kez verify id github:linus # Or walk their chain from any known endpoint: kez sigchain show --primary nostr:npub1abc... ``` If you have the chain bundle on disk: ```sh kez 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. --- ## 8. Quick reference card ```sh # Generate a fresh primary kez identity new kez identity new --key-type ed25519 # Sign a claim kez claim create --nsec # nostr key kez claim create --ed25519-seed # ed25519 key kez claim create --nsec --format markdown --out file.md kez claim create --nsec --format compact # one-liner kez claim dns --nsec # zone-file output # Verify kez verify id # live channel fetch kez verify file # local file # Sigchain kez sigchain add --nsec [--proof-url ] kez sigchain revoke --nsec kez sigchain show --nsec # your own kez sigchain show --primary # someone else's kez sigchain export --nsec --format jsonl|compact [--out file] kez sigchain publish --nsec \ [--server ] [--web --out ] [--dns ] [--nostr ] ``` --- ## 9. Common confusions **"Do I need a sigchain to use KEZ?"** No. A single signed claim, published, works on its own. The sigchain is for when you have several claims and want them discoverable together (and revocable). **"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 specifically. **"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 then someone else copies it and publishes 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 final "this primary is succeeded by " event (planned for the spec; not yet enforced by the CLI in v0.1). --- ## 10. Where to go next - The web client at — same protocol, no CLI. Useful for showing non-technical friends. - [`../SPEC.md`](../SPEC.md) — the formal protocol, if you want to know exactly what every byte means. - [`../rust-sig-server/`](../rust-sig-server/) — run your own sig-server, federate with others. - The channel plugin trait in [`crates/kez-channels/src/lib.rs`](crates/kez-channels/src/lib.rs) — ~40 lines, add a new channel in an afternoon. That's the whole tutorial. Welcome to KEZ.