The existing README is a solid reference but assumes you already know
what KEZ is and what each subcommand does. Add a parallel TUTORIAL.md
that takes a complete newcomer from "I have a nostr nsec" to "I have
a published, verified sigchain" in ~15 minutes.
Sections (~500 lines):
0. Install (incl. cargo-run alternative + GITHUB_TOKEN tip)
1. Pick your primary key — use your existing nsec (recommended) OR
generate a fresh ed25519. Concrete warnings about nsec handling.
2. Sign your first claim — full markdown/compact/json walkthrough
with a real github:tudisco example.
3. Publish the proof — separate concrete how-tos per channel:
github (gist + profile README), DNS (zone-file output), nostr
(3 places it can live), bluesky, ActivityPub, your own website.
4. Verify it — `kez verify id` + a full "if verification fails"
troubleshooting block (not_found, subject_mismatch, bad sig,
github rate limit).
5. Sigchain basics — when you actually need one, add/show/revoke,
where chain files live on disk.
6. Publish your sigchain — server, web (.well-known), DNS,
nostr (kind-30078), and how to combine destinations.
7. Verify someone else — the reverse direction (verify id, walk
a chain by --primary, verify a chain bundle from disk).
8. Quick-reference command card.
9. Common confusions FAQ — sigchain optional? two key types?
nsec leakage? proof copying? key rotation?
10. Where to go next — kez.lat, SPEC.md, sig-server, channel plugin
trait.
All commands cross-checked against crates/kez-cli/src/main.rs (every
flag and output format quoted in the tutorial actually exists in the
binary).
README now points to TUTORIAL.md as the on-ramp; the existing reference
content stays put.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
16 KiB
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, 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. 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
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:
kez --help
You should see subcommands identity, claim, verify, and sigchain.
Don't want to install globally? Replace every
kezbelow withcargo run -p kez-cli --(from therust/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 butapi.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
nsecinto 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 HISTFILEfor the session, or prefix the command with a space ifHISTCONTROL=ignorespace).
You can confirm KEZ accepts your key without signing anything yet:
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:
kez identity new --key-type ed25519
Output:
Primary: ed25519:7a3b4c…
Public: 7a3b4c… (hex)
Secret: 9e3f51… (hex — 64 chars, KEEP SECRET)
Save the secret. It's the only thing that can sign as this identity. There's no recovery flow — lose it and the identity is gone. Write it down offline, or paste it into a password manager. From here on this tutorial assumes you stored it.
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.
kez claim create github:tudisco \
--nsec nsec1FAKE... \
--format markdown \
--out github-tudisco.kez.md
That writes a file like:
# 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 <https://gist.github.com/>.
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…<chunk 1>…"
"<chunk 2>…"
Add that TXT record at _kez.<your-domain> 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
aboutfield (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
dtag =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:
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:
kez verify id github:tudisco
Output:
Primary: nostr:npub1tkf...
Verified identities:
- github:tudisco
Status: valid
Confidence: strong
Same shape for any channel:
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:
- Figured out which channel from the prefix.
- Fetched the proof from where you published it (gist, TXT, etc.).
- Decoded the envelope.
- 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 containskez. 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. The claim'ssubjectmust 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; exportGITHUB_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/<safe-primary>.jsonl. The CLI creates
the directory on first use; you don't manage it manually.
Add the github claim you already signed:
kez sigchain add github:tudisco --nsec nsec1FAKE...
Add a DNS claim too:
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:
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):
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:
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 (one
runs at https://sig.kez.lat):
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:
kez sigchain publish --nsec nsec1FAKE... \
--web --out kez-sigchain.jsonl
Then upload kez-sigchain.jsonl to
https://<your-domain>/.well-known/kez-sigchain.jsonl. Verifiers
fetch it directly. Hardest to censor; you own it.
To DNS
kez sigchain publish --nsec nsec1FAKE... --dns tud.ink
Prints a TXT record at _kez-chain.<domain> 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
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:
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:
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:
# 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:
kez 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.
8. Quick reference card
# Generate a fresh primary
kez identity new
kez identity new --key-type ed25519
# Sign a claim
kez claim create <subject> --nsec <nsec> # nostr key
kez claim create <subject> --ed25519-seed <hex-seed> # ed25519 key
kez claim create <subject> --nsec <nsec> --format markdown --out file.md
kez claim create <subject> --nsec <nsec> --format compact # one-liner
kez claim dns <domain> --nsec <nsec> # zone-file output
# Verify
kez verify id <subject> # live channel fetch
kez verify file <path> # local file
# Sigchain
kez sigchain add <subject> --nsec <nsec> [--proof-url <url>]
kez sigchain revoke <subject> --nsec <nsec>
kez sigchain show --nsec <nsec> # your own
kez sigchain show --primary <id> # someone else's
kez sigchain export --nsec <nsec> --format jsonl|compact [--out file]
kez sigchain publish --nsec <nsec> \
[--server <url>] [--web --out <path>] [--dns <domain>] [--nostr <relay>]
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 <subject> 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 https://kez.lat — same protocol, no CLI. Useful for showing non-technical friends.
../SPEC.md— the formal protocol, if you want to know exactly what every byte means.../rust-sig-server/— run your own sig-server, federate with others.- The channel plugin trait in
crates/kez-channels/src/lib.rs— ~40 lines, add a new channel in an afternoon.
That's the whole tutorial. Welcome to KEZ.