# KEZ Specification **Status:** Draft v0.2 **Date:** 2026-05-23 KEZ is a portable, decentralized identity graph. It lets a person say: > "These accounts, keys, domains, and identities are all me." …without depending on any central authority to bless the claim. Verification is done by cryptographic signatures against keys the user already controls. This document is the language-agnostic specification. Implementations (Rust, and others) MUST conform to the formats and behaviors defined here. --- ## 1. Core Concepts ### 1.1 Primary Key The user chooses one key as their current "main identity key." KEZ does not invent a new key type — it reuses keys people already hold. Supported key types in v1: - `nostr` / `secp256k1` (Schnorr signatures, BIP-340) - `ed25519` Future, non-normative: passkeys, Bluesky DID keys, Ethereum keys, GPG keys. ### 1.2 Claim A signed JSON object asserting that the primary key also controls some other identity (a GitHub account, a domain, a Bluesky handle, etc.). ### 1.3 Proof A claim, plus evidence that it was published in a location only the claimed identity can publish to (e.g. the GitHub user's own gist, the domain's DNS). ### 1.4 Sigchain A signed, append-only log of identity events (adds, revokes, key rotations) owned by a primary key. ### 1.5 Identity Graph The set of all currently-active proofs reachable from a starting identifier, walked through the sigchain. --- ## 2. Identifiers A KEZ identifier is always `system:identifier` — the system prefix is required, never optional. This lets a human glance at an identifier and know where the key/account lives. Canonical examples: | System | Example | |------------|--------------------------------------| | `nostr` | `nostr:npub1abc...` | | `github` | `github:jason` | | `dns` | `dns:jason.example.com` | | `bluesky` | `bluesky:jason.bsky.social` | | `did` | `did:plc:abc...` | | `ens` | `ens:jason.eth` | | `mastodon` | `mastodon:@jason@server.social` | | `farcaster`| `farcaster:fid:12345` | | `web` | `web:https://jason.example.com` | A bare `npub1...` (no `nostr:` prefix) is **not** a valid KEZ identifier. CLI tools MAY accept it as input ergonomics, but they MUST normalize it to `nostr:npub1...` before signing, publishing, or storing. Identifier strings are case-sensitive except where the underlying system normalizes case (e.g. DNS — lowercased before comparison). KEZ is **not a phone book.** It cannot resolve "Jason" from a human name. A verifier always needs a concrete starting identifier. --- ## 3. The Claim Format A claim is a UTF-8 JSON object with the following required fields: ```json { "type": "kez.claim", "version": 1, "primary": "nostr:npub1abc...", "subject": "github:jason", "created_at": "2026-05-19T18:00:00Z" } ``` KEZ uses dotted, namespaced type strings throughout: `kez.claim`, `kez.sigchain.event`, `kez.device`, etc. Optional fields: - `expires_at` — RFC 3339 timestamp; after this time the claim is treated as expired. - `nonce` — opaque string to prevent replay across publishing channels. - `note` — short human-readable note (≤256 chars). ### 3.1 Canonicalization For signing and verification, the claim MUST be serialized as **JCS** (RFC 8785, JSON Canonicalization Scheme). Implementations MUST NOT rely on incidental key ordering. ### 3.2 Signature Envelope A signed claim is wrapped in: ```json { "kez": "claim", "payload": { ... the claim object ... }, "signature": { "alg": "nostr-secp256k1-schnorr-sha256-jcs", "key": "nostr:npub1abc...", "sig": "" } } ``` - `kez` (top-level) tags the envelope kind: `"claim"`, `"sigchain_event"`, etc. This lets generic tooling route envelopes without parsing the payload. - `alg` is the **full signature suite**, identifying key family + hash + canonicalization. Defined suites in v1: - `nostr-secp256k1-schnorr-sha256-jcs` — BIP-340 Schnorr over secp256k1, SHA-256, JCS. - `ed25519-sha512-jcs` — Ed25519 (RFC 8032), JCS-canonicalized payload. - `key` MUST equal `payload.primary` (same key, expressed as a `system:identifier` string in the encoding native to its system). - `sig` is the signature, **hex-encoded** (lowercase, no `0x` prefix). The signature covers `JCS(payload)`, not the envelope. The on-disk file extension for a signed claim envelope is `.kez`. The MIME type is `application/vnd.kez+json`. --- ## 4. Proof Encodings A "proof" is a signed claim envelope, encoded for transport over a specific channel. KEZ defines four encodings; the same envelope round-trips losslessly between them. ### 4.1 JSON The signature envelope serialized as standard JSON (pretty or compact). Native form. Used for HTTP fetches, `.well-known/kez.json`, and developer tooling. ### 4.2 Compact (`kez:z1:`) A URL-safe, copy-pasteable form designed for QR codes, DNS TXT records, chat messages, and other tight channels: ``` kez:z1: ``` - `z1` is the compact format version. Future versions (`z2`, …) MAY change the compression or framing. - Compression: zstd, default level 3. - Encoding: base64url (RFC 4648 §5), **no padding**. - The decoded payload MUST be a valid signature envelope per section 3.2. Implementations MUST support encoding and decoding `kez:z1:`. ### 4.3 Markdown Human-readable proof file suitable for a GitHub gist, profile README, or any Markdown-rendering surface. The machine-readable proof lives inside a fenced code block tagged ```` ```kez ````: ````markdown # KEZ Proof This account publishes a signed KEZ identity claim. - Primary: `nostr:npub1abc...` - Subject: `github:jason` - Created: `2026-05-19T18:00:00Z` ```kez { "kez": "claim", "payload": { ... }, "signature": { ... } } ``` ```` Parsers MUST locate the first ```` ```kez ```` fence, take the contents up to the next ```` ``` ````, trim whitespace, and parse as JSON. Surrounding prose is informational only. ### 4.4 DNS TXT For `dns:` subjects, the proof is published as a TXT record at: ``` _kez. ``` The TXT record value is the **compact encoding** (section 4.2): ``` kez:z1:KLUv_QBY... ``` Raw JSON is NOT a valid DNS TXT proof — JSON exceeds the 255-byte TXT segment limit too easily and complicates quoting. The compact form fits common claims in a single segment. If a claim is too large for a single TXT record even compressed, implementations MUST publish a `web:` proof instead and add a `dns:` claim pointing at the domain — chaining, not splitting. --- ## 5. Publishing Proofs The claim must be published somewhere only the claimed identity can publish to. Each `system` defines its own channel(s) and default encoding: | System | Channel | Encoding | |------------|------------------------------------------------------------|------------------| | `github` | Public gist owned by the user, or `README` on profile repo | Markdown (§4.3) | | `dns` | TXT record at `_kez.` | Compact (§4.2) | | `web` | `https:///.well-known/kez.json` | JSON (§4.1) | | `nostr` | Nostr event of kind `30078` (parameterized replaceable) | JSON (§4.1) | | `bluesky` | Public post or record on the user's PDS | JSON or Markdown | | `mastodon` | Public profile post or pinned status | Markdown (§4.3) | Verifying a proof always means: **fetch from the channel the identity itself controls, then verify the signature against the primary key.** A claim that signs correctly but is not found at the identity's own channel is **not a valid proof.** --- ## 6. The Sigchain The sigchain is an append-only, signed log of events for a single primary key. Each entry's payload is: ```json { "type": "kez.sigchain.event", "version": 1, "primary": "nostr:npub1abc...", "seq": 7, "prev": "sha256:", "created_at": "2026-05-20T12:00:00Z", "op": "add" | "revoke" | "rotate" | "add_device", "payload": { ... op-specific fields ... } } ``` Wrapped in the same signature envelope as a claim, with top-level tag `"kez": "sigchain_event"`. ### 6.1 Operations - **`add`** — payload: `{ "subject": "github:jason", "proof_url": "..." }`. Adds an identity to the graph. - **`revoke`** — payload: `{ "subject": "github:jason" }`. Removes a previously-added identity. - **`rotate`** — payload: `{ "new_primary": "nostr:npub1xyz...", "new_key_sig": "" }`. Rotates the primary key. `new_key_sig` is the new key signing over the JCS of the rotation event with `new_key_sig` omitted, proving the rotation is two-way authorized. - **`add_device`** — payload: `{ "device_key": "...", "label": "..." }`. Adds a secondary key authorized to sign on behalf of this primary. ### 6.2 Integrity Rules - `seq` MUST be strictly monotonic, starting at 0. - `prev` MUST equal `sha256:` + hex SHA-256 of the **JCS-canonicalized envelope** (not just payload) of the prior entry. The first entry (`seq: 0`) has no `prev` field. - A verifier MUST reject any sigchain with a broken hash chain, missing `prev`, or non-monotonic `seq`. - Multiple sigchain copies MAY exist across stores; the longest valid chain wins. If two valid chains diverge at the same `seq` with different envelope hashes, the verifier MUST report a **fork** rather than silently picking one. ### 6.3 Key Rotation After a `rotate`, all subsequent entries MUST be signed by the new primary, and each entry's `primary` field MUST reflect the new key. A verifier walks rotations by following the chain: the old key's `rotate` event authorizes the new key, and the new key's `new_key_sig` over that same event closes the loop. --- ## 7. Storage KEZ is store-agnostic. The verifier MUST be able to fetch sigchains and proofs from any of: - nostr relays - GitHub (gists, repos) - HTTP(S) URLs (websites, `/.well-known/kez.json`) - DNS (TXT records) - IPFS - Bluesky PDS - Local filesystem (for development/testing) There is no required KEZ server. Implementations SHOULD support multiple stores in parallel and treat them as mirrors, not sources of truth — the signatures are the source of truth. --- ## 8. Verification ### 8.1 Verifier Input ``` kez verify ``` Example: `kez verify github:jason` ### 8.2 Algorithm 1. **Fetch the proof** at the channel native to the input identifier's system (real network fetch — see §8.5). 2. **Decode** the proof from its on-the-wire encoding (JSON, compact, or Markdown) into a signature envelope. 3. **Verify signature** against the embedded primary key over `JCS(payload)`. 4. **Confirm channel ownership** — the claim was found at a location only the input identity controls. 5. **Locate the sigchain** for the primary key (try known stores; multiple stores allowed). 6. **Validate the sigchain** end-to-end (signatures, hash chain, monotonic seq, rotation closures). 7. **Check current status** of the input identity — its `add` must not be followed by a matching `revoke`, and any `expires_at` must not have passed. 8. **Walk other identities** in the sigchain. For each, re-fetch and re-verify its proof against its own channel (best-effort; some may be unreachable). 9. **Return the graph** with per-identity status. ### 8.3 Verifier Output ``` Primary: nostr:npub1abc... Verified identities: github:jason valid dns:jason.example valid bluesky:jason.bsky.social valid ens:jason.eth unreachable Status: valid Confidence: strong ``` `Confidence` is a function of: number of independently-verified identities, channel diversity, sigchain length and age. ### 8.4 Failure Modes A verifier MUST distinguish between: - **invalid** — a signature failed, or the chain is broken. - **revoked** — the identity was explicitly removed via a sigchain `revoke`. - **expired** — the claim's `expires_at` has passed. - **unreachable** — the channel could not be fetched (network/transient). - **fork** — the sigchain has diverging valid branches at the same `seq`. These statuses MUST NOT be conflated. A verifier MUST surface the distinction to its caller. ### 8.5 Network Fetching Is Required A conformant verifier performs **real network fetches** for each channel: - `dns:` — real DNS resolution against the system resolver (or a configurable resolver), reading the TXT record at `_kez.`. Verifying a TXT *value* a caller already obtained is a development helper, not conformant verification. - `github:` — HTTPS fetch against `api.github.com` and/or `raw.githubusercontent.com` for the user's gist or profile README. - `web:` — HTTPS fetch of `/.well-known/kez.json`. - `nostr:` — connect to one or more relays and fetch the relevant event. - `bluesky:` — HTTPS fetch against the user's PDS. A verifier that only reads local files is not a conformant verifier — it's a signature-checking helper. Conformant verifiers MAY expose a local-file mode for testing, but it MUST be clearly labeled as such. --- ## 9. Starting Points (Discovery) A KEZ verifier does not magically know every account a user has. It always starts from one identifier provided externally (by an app, a QR code, a chat message, a posting key). From that starting point it walks outward via the sigchain. This is the same model as the web: you don't know every URL, you start at one and follow links. KEZ is the signed-identity equivalent. --- ## 10. MVP Scope (v0.2) A v0.2 implementation MUST support: 1. The claim format defined in section 3, including the envelope (§3.2). 2. All four proof encodings: JSON, Compact (`kez:z1:`), Markdown, and DNS TXT (sections 4.1–4.4). 3. Two proof channels with **real network fetch** (§8.5): - GitHub gist / profile README - DNS TXT (`_kez.`) 4. Two key types: - `ed25519` (suite: `ed25519-sha512-jcs`) - `secp256k1-schnorr` (suite: `nostr-secp256k1-schnorr-sha256-jcs`) 5. The sigchain format defined in section 6, including hash-chain validation and rotation closure. 6. A verifier CLI matching section 8, distinguishing all five failure modes (§8.4). Anything beyond this list is post-MVP. --- ## 11. Suggested Implementation Layout Language-agnostic, but recommended: ``` kez-core claim + sigchain types, signing, verification, all four encodings kez-channels network adapters: github, dns, web, nostr, bluesky, ... kez-cli command-line verifier kez-web paste an identifier, see the graph ``` Each language implementation lives in its own top-level directory (e.g. `rust/`, `ts/`, `go/`). All implementations conform to this spec; cross-language conformance is verified by sharing test vectors (see section 12). --- ## 12. Test Vectors A `vectors/` directory at the repo root contains canonical inputs and expected outputs: - `vectors/claims/` — signed claim JSON + the expected JCS bytes + signature + compact encoding + markdown encoding. - `vectors/sigchains/` — full sigchain examples (valid, revoked, rotated, forked). - `vectors/proofs/` — fixtures for each channel adapter. Every implementation's test suite MUST run against these vectors. Two implementations conform iff they produce identical outputs for the same vectors. --- ## 13. One-Sentence Summary KEZ is a portable identity graph where users sign claims connecting their many accounts, publish those claims in places they control, and anyone can verify the connections without trusting a central server.