Kez/SPEC.md
Jason Tudisco b237ac2d9e docs+fix: SPEC v0.4, Node nostr channel checks kinds 0/1/30078, NOSTR-CHAT.md current
SPEC.md → v0.4
- §3.1 BIP-39 recovery phrases (12-word hashed with "kez-bip39-12-v1",
  24-word = seed), parsing rules, test vectors.
- §7 nostr: kind 30078 canonical (MUST); verifiers SHOULD also check
  kind-1 notes and the kind-0 `about`.
- §9.1 sigchain HTTP store API; §11 BIP-39 primitive; §15 Python and
  kez-chat; §16 applications (kez-chat tags); changelog.

nodejs kez-channels
- NostrChannel now requests kinds 0, 1, 30078 and extracts the proof
  body per kind (kind-0 `about` JSON-decoded), mirroring Rust's
  extract_proof_body. Proofs posted as kind-1 notes (e.g. via NIP-07
  from kez-chat) previously failed to verify in Node.
- Tests for kind-1 Markdown notes, kind-0 about, and extractProofBody.

Docs
- kez-chat/web/NOSTR-CHAT.md rewritten for the current protocol: `q`
  routing tag, kinds 4242/4244/10142/0/10002, KEZ-signed heartbeats,
  v1–v4 envelopes, receipts, relay ordering, honest metadata model.
- Stale `#h` comments in nostr-id.ts / nostr-transport.ts.
- rust/README.md nostr channel rows list all three kinds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:00:51 -06:00

29 KiB
Raw Blame History

KEZ Specification

Status: Draft v0.4 Date: 2026-09-27 Conformance: Implementations MUST conform to the formats and behaviors defined here. Two implementations conform iff a claim signed by one verifies in the other and vice versa (see §15).


1. Summary

KEZ is a portable identity graph. 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.

"These accounts, keys, domains, and identities are all me."

No central authority blesses the claim. Verification is done by cryptographic signatures against keys the user already controls.


2. Glossary

Term Meaning
Primary key The key the user owns; the root of their identity. One per user. Supported algorithms in §11.
Identity A canonical system:identifier string (e.g. github:jason, nostr:npub1abc...). §3.
Subject The identity being claimed about, in a claim payload.
Claim payload The unsigned JSON object asserting that the primary key controls a subject. §5.1.
Signature envelope The wrapper that wraps any payload with {kez, payload, signature}. The unit of transport. §4.
Proof A signature envelope (containing a claim) that has been published in a location only the claimed subject controls.
Channel A protocol/system where a proof can be published and fetched. §7.
Sigchain event An entry in a user's sigchain — add, revoke, rotate, etc. §5.2.
Sigchain The append-only signed log of sigchain events for one primary key. §8.
Identity graph The set of currently-active subjects reachable from a primary key, walked through its sigchain.
Recovery phrase A BIP-39 English word list (12 or 24 words) that deterministically yields an Ed25519 primary key's 32-byte seed. §3.1.

3. Identifiers

A KEZ identifier is always system:identifier — the system prefix is required, never optional. A human glancing at the string can tell what namespace it lives in.

Canonical examples:

System Example
nostr nostr:npub1abc...
ed25519 ed25519:2152f8d19b791d24... (64 lowercase hex chars)
github github:jason
dns dns:jason.example.com
web web:https://jason.example.com
bluesky bluesky:jason.bsky.social
did did:plc:abc...
ens ens:jason.eth
ap ap:@jason@mastodon.social (alias: mastodon:@...)
farcaster farcaster:fid:12345

Bare npub1... (no nostr: prefix) is not a valid stored or signed identifier. CLI tools MAY accept it as input ergonomics, but MUST normalize 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 (see §10).

3.1 Recovery phrases (Ed25519 seeds)

An Ed25519 primary key is a 32-byte seed (RFC 8032). Implementations that offer a human-writable backup MUST encode it as a BIP-39 phrase over the BIP-39 English wordlist, with these semantics:

Words Entropy Seed
24 32 bytes seed = entropy — a bijection; the phrase can be recovered from any seed
12 16 bytes seed = SHA-256("kez-bip39-12-v1" ‖ entropy) — one-way; a seed has no 12-word form
  • The domain tag is the 15 ASCII bytes kez-bip39-12-v1. Changing it would break every existing 12-word identity; it MUST NOT change.
  • Entropy is the raw BIP-39 entropy (checksum-validated). BIP-39's PBKDF2 to_seed(passphrase) and BIP-32 derivation are not used: KEZ has one identity per phrase and no derivation tree. KEZ phrases are therefore not interchangeable with wallet derivations of the same words.
  • Parsers MUST trim surrounding whitespace and collapse internal runs of whitespace to single spaces before decoding. Any other word count is an error.
  • New identities SHOULD be generated as 12-word phrases. Restore flows MUST accept both lengths. To display a backup for a seed that has no known 12-word phrase, use its 24-word encoding.

Test vectors (all implementations MUST match byte-for-byte; more in python/MNEMONIC-TEST-VECTORS.md):

24 words: abandon ×23, art
  seed    0000000000000000000000000000000000000000000000000000000000000000
  primary ed25519:3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29

12 words: abandon ×11, about
  seed    09451c0f06588db78205e32a793536e15ae263c8f9ee6d14f5c6fd82b8bd20da
  primary ed25519:9403c32e0d3b4ce51105c0bcac09a0d73be0cca98a6bf7b3cd434651be866d70

4. The Signature Envelope

Every signed thing in KEZ — claims, sigchain events, future payload types — is wrapped in the same three-field envelope:

{
  "kez": "<envelope-tag>",
  "payload": { ... },
  "signature": {
    "alg": "<signature-suite>",
    "key": "<primary-identity>",
    "sig": "<hex-encoded-signature>"
  }
}

The envelope is the unit of transport. Everything on the wire is an envelope; payloads never appear bare.

4.1 Envelope fields

Field Type Meaning
kez string Envelope tag — identifies the payload kind without parsing the payload. Examples: "claim", "sigchain_event".
payload object The payload — the actual data being signed. Shape depends on the envelope tag (see §5).
signature.alg string Full signature suite, identifying key family + hash + canonicalization. §11.
signature.key string The signing key as a KEZ identity. MUST equal payload.primary for claim and sigchain-event envelopes.
signature.sig string The signature, lowercase hex, 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.2 Canonicalization for signing

Signatures are computed over the JCS-canonicalized bytes of the payload (RFC 8785, JSON Canonicalization Scheme). Implementations MUST NOT rely on incidental key ordering — JCS sorts keys lexicographically, normalizes numbers, escapes strings deterministically.

This is what enables interop: byte-identical canonical bytes across implementations means byte-identical signatures (for deterministic algorithms) or universally-verifiable signatures (for randomized ones).


5. Payload Shapes

Two payload shapes are defined in this version.

5.1 Claim payload

A claim asserts that a primary key controls a subject identity.

{
  "type": "kez.claim",
  "version": 1,
  "primary": "ed25519:2152f8d19b791d24...",
  "subject": "github:jason",
  "created_at": "2026-05-19T18:00:00Z"
}

Required fields: type, version, primary, subject, created_at.

Optional fields:

Field Type Meaning
expires_at RFC 3339 timestamp After this time the claim is treated as expired.
nonce opaque string Prevents replay across publishing channels.
note string ≤256 chars Human-readable note.

Type strings are dotted and namespaced (kez.claim, kez.sigchain.event, future kez.device, etc.).

5.2 Sigchain event payload

{
  "type": "kez.sigchain.event",
  "version": 1,
  "primary": "ed25519:2152f8d19b791d24...",
  "seq": 7,
  "prev": "sha256:<hex of prior envelope's JCS bytes>",
  "created_at": "2026-05-20T12:00:00Z",
  "op": "add",
  "payload": { ... op-specific ... }
}

seq is strictly monotonic starting from 0. prev is sha256: plus hex SHA-256 of the JCS bytes of the prior signed envelope (whole envelope, not just its payload). The first event (seq: 0) has no prev field.

Per-op payload shapes are defined in §8.


6. Wire Encodings

A signed envelope can be transported in four interchangeable forms. All carry the same logical content; choose the one that fits the channel.

6.1 JSON

Standard JSON serialization of the envelope (pretty or compact). Native form. Used for HTTP fetches, /.well-known/kez.json, developer tooling.

Different implementations MAY produce different whitespace and field order in raw JSON, but the JCS-canonicalized payload must match. Signatures verify regardless of envelope whitespace.

6.2 Compact (kez:z1:)

URL-safe, copy-pasteable form designed for QR codes, DNS TXT records, chat messages, and other tight channels:

kez:z1:<base64url-no-pad(zstd(envelope-as-compact-json))>
  • z1 is the compact format version. Future versions (z2, …) may change compression or framing.
  • Compression: zstd, default level 3.
  • Encoding: base64url (RFC 4648 §5), no padding.
  • The decoded payload MUST parse as a valid signature envelope per §4.

Why zstd over gzip: ~30% smaller for our JSON shapes, low CPU cost, modern decoder available everywhere (built into Node 22.15+ via node:zlib, the zstd crate in Rust, etc.).

Implementations MUST support encoding and decoding kez:z1:.

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

# KEZ Proof

This account publishes a signed KEZ identity claim.

- Primary: `ed25519:2152f8d19b...`
- 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, parse as JSON. Surrounding prose is informational.

6.4 DNS TXT

For dns: subjects, the proof is published as a TXT record at:

_kez.<domain>

The TXT record value is the compact encoding (§6.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 (chunked across segments if needed, per standard DNS TXT semantics).

If a claim is too large even compressed, implementations SHOULD publish a web: proof instead and add a dns: claim pointing at the domain — chain, don't split.

6.5 Sigchain compact bundle (kez:zc1:)

For transporting an entire sigchain as one string (nostr event content, DNS TXT pointer, ActivityPub profile field):

kez:zc1:<base64url-no-pad(zstd(jsonl))>

Where the inner content is JSONL — one signed sigchain envelope per line. Parsers MUST decompress, split on newlines, and parse each line as a sigchain event envelope.


7. Channels

A channel is a system + the conventions for publishing a proof there so a verifier can fetch it.

System Where to publish Wire encoding
github Public gist (filename should include kez), or README on the user's <user>/<user> profile repo Markdown (§6.3)
dns TXT record at _kez.<domain> Compact (§6.2)
web https://<domain>/.well-known/kez.json JSON (§6.1)
nostr Canonical: event of kind 30078 (NIP-78 app data), d tag kez. Verifiers MUST check kind 30078 and SHOULD also check kind 1 text notes and the kind 0 profile about field, where clients like NIP-07 extensions commonly publish JSON in kind-30078 content; Markdown fence (§6.3) or compact (§6.2) in kinds 1 and 0
bluesky Public post on the user's PDS JSON or Markdown
ap (alias mastodon) Profile field (preferred) or bio of the user's actor object; resolved via WebFinger → actor JSON Compact (§6.2) inline, or Markdown

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.

Channel implementations are pluggable. Adding a new channel does not change the wire format or signatures — only adds a new resolver for a new system: prefix.


8. Sigchain Semantics

The sigchain is the append-only, signed log of identity events belonging to one primary key. It's how a verifier knows:

  • What subjects the user has currently claimed
  • What subjects have been revoked
  • What devices / secondary keys the user controls
  • When (if ever) the primary key was rotated

8.1 Operations

Each sigchain event has an op string and a payload tailored to it.

op Payload Effect
add { "subject": "github:jason", "proof_url": "..." } (proof_url optional) Asserts the user controls subject.
revoke { "subject": "github:jason" } Withdraws a previously added subject.
rotate { "new_primary": "ed25519:...", "new_key_sig": "<hex>" } Rotates the primary key. The old key signs this event; new_key_sig is the new key's signature over the JCS of the same event with new_key_sig omitted, proving both keys consent.
add_device { "device_key": "ed25519:...", "label": "macbook" } Adds a secondary key authorized to sign on behalf of this primary. (Semantics for device-signed events TBD in a future minor version.)

8.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 event (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, a verifier SHOULD report a fork rather than silently picking one.

8.3 Key rotation

After a rotate:

  • All subsequent entries MUST be signed by the new primary.
  • Each entry's primary field MUST reflect the new key.
  • Verifiers walk rotations by following the chain: the old key signed the rotation event, and the new_key_sig field within that event is the new key's signature over the same event (with new_key_sig omitted from the JCS input), closing the loop.

8.4 Sigchain in pictures

seq 0          seq 1                       seq 2                       seq 3
add github   add dns:example.com         revoke github               rotate to new_pk
                                                                      ↓
┌────────┐    ┌────────────────┐          ┌────────────────┐          ┌────────────────┐
│type:   │    │type: ...event  │          │type: ...event  │          │type: ...event  │
│ ...    │    │seq: 1          │          │seq: 2          │          │seq: 3          │
│seq: 0  │    │prev: sha256:H0 │ ────────►│prev: sha256:H1 │ ────────►│prev: sha256:H2 │
│prev: - │    │op: add         │          │op: revoke      │          │op: rotate      │
│op: add │    │payload: ...    │          │payload: ...    │          │new_key_sig:... │
│sub:gh:.│    └────────────────┘          └────────────────┘          └────────────────┘
└────────┘    │ signed by old │          │ signed by old │          │ signed by old │
│signed by │    │ primary       │          │ primary       │          │  + new_key_sig│
│primary    │  └────────────────┘          └────────────────┘          └────────────────┘
└──────────┘
   H0 =                                                                  After this event,
   sha256(                                                               subsequent events
   JCS(seq 0                                                             are signed by
   envelope))                                                            new_pk and have
                                                                          primary: new_pk

H₀, H₁, H₂ are the SHA-256s of the full JCS envelopes. Each event's prev points back to the prior envelope's hash, forming the chain.


9. Storage

KEZ is store-agnostic. Verifiers 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
  • ActivityPub (WebFinger + actor JSON)
  • Local filesystem (for development and testing)

There is no required central KEZ server. Storage servers (e.g. a sigchain HTTP store) MAY exist as convenience tiers, but the signatures are the source of truth — a verifier MUST validate signatures locally, not trust a server's claim about validity.

Implementations SHOULD support multiple stores in parallel and treat them as mirrors.

9.1 Sigchain HTTP store (informative)

The reference store (rust-sig-server/) exposes:

Method Path
GET /v1/sigchains/<scheme>/<id> Whole chain as JSONL (one signed envelope per line); 404 if empty
GET /v1/sigchains/<scheme>/<id>/head Latest {seq, hash}
POST /v1/sigchains/<scheme>/<id>/events Append one signed sigchain event; the store re-checks tag, primary, seq, prev, and signature before accepting

<scheme>/<id> is the primary key split at the colon (e.g. ed25519/2152f8…). A store MAY be fronted by another origin (kez-chat proxies it at https://<server>/v1/sigchains/…); clients should use whatever sigchain_url they are given rather than constructing one.


10. Verification

10.1 Starting point

A verifier always starts from one identifier supplied externally — e.g. someone shared @chris@kez.lat, a QR code embedded nostr:npub1abc..., a profile page passed dns:chris.com. KEZ cannot resolve "Chris" from a human name; it always needs a concrete starting identifier.

Same mental model as the web: you don't know every URL; you start at one and follow links.

10.2 Input and output

Input: one identifier.

Output: a graph of identities reachable from the input, each with a status. Example:

Primary: ed25519:2152f8d19b791d24...

Verified identities:
  github:jason               valid
  dns:jason.example          valid
  bluesky:jason.bsky.social  valid
  ens:jason.eth              unreachable
Status:     valid
Confidence: strong

Confidence is implementation-defined but is typically a function of: number of independently-verified subjects, channel diversity, sigchain length and age.

10.3 Algorithm (5 phases)

  1. Fetch. Resolve the input identifier on its native channel; fetch the proof. (Real network fetch — see §10.5.)
  2. Verify signature. Parse the envelope; verify the signature against payload.primary over JCS bytes of the payload.
  3. Confirm channel ownership. The proof was found at a location only the input identity could publish to.
  4. Walk sigchain. Locate and load the sigchain for the primary key. Validate it end-to-end (signatures, hash chain, monotonic seq, rotation closure). Reject the proof if the input subject's most recent op is revoke, or any required event fails validation.
  5. Walk graph. For each other subject in the sigchain, attempt to re-verify its proof against its own channel (best-effort).

10.4 Failure modes

A verifier MUST distinguish:

Status Cause
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 MUST NOT be conflated. A verifier MUST surface the distinction.

10.5 Network fetching is expected

A complete verifier performs real network fetches for each channel:

  • dns: — real DNS resolution; reads the TXT record at _kez.<domain>.
  • github: — HTTPS fetch against api.github.com / raw.githubusercontent.com.
  • web: — HTTPS fetch of /.well-known/kez.json.
  • nostr: — connect to one or more relays; fetch the relevant event.
  • bluesky: — HTTPS fetch against the user's PDS.
  • ap: — HTTPS fetch of WebFinger followed by actor JSON.

A library that only checks signatures over local data is a useful helper but not a complete verifier. Complete verifiers MAY expose a local-file mode for testing; it MUST be clearly labeled as such.


11. Cryptographic Primitives

Primitive Role
BIP-340 Schnorr (over secp256k1) nostr-key signatures. Suite: nostr-secp256k1-schnorr-sha256-jcs. Signatures are deterministic (zero aux randomness).
Ed25519 (RFC 8032) ed25519-key signatures. Suite: ed25519-sha512-jcs. PureEdDSA — the signature is over the raw JCS bytes; SHA-512 is performed internally.
BIP-39 (English wordlist) Recovery phrases for Ed25519 seeds (§3.1). Raw entropy only — no PBKDF2 seed, no BIP-32.
SHA-256 Sigchain prev chaining; pre-hash for Schnorr signing (sig = schnorr(SHA-256(JCS(payload)), key)); 12-word phrase → seed (§3.1).
SHA-512 Internal to Ed25519.
JCS (RFC 8785) Canonicalize the bytes to be signed.
zstd (default level 3) Compress the JSON envelope for the compact wire form.
base64url (RFC 4648 §5, no padding) Encode compressed bytes for URL-safe transport.
hex (lowercase, no 0x) Encode signatures and hashes wherever they appear in JSON.

The suite string names every choice (<key-family>-<hash>-<canonicalization>) so a verifier can dispatch without guessing. Future suites follow the same pattern.


12. Worked Example (reproducible)

Anyone implementing this spec should produce byte-identical results for these inputs, modulo signature aux randomness (Ed25519 is deterministic so the signature is byte-identical too).

Inputs (TEST ONLY — do not use this key for anything real):

Ed25519 seed (32 bytes, hex):
4242424242424242424242424242424242424242424242424242424242424242
Subject:    github:jason
created_at: 2026-01-01T00:00:00Z

Derived public key:

ed25519 pubkey (hex):
2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12

KEZ identity:
ed25519:2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12

The claim payload:

{
  "type": "kez.claim",
  "version": 1,
  "subject": "github:jason",
  "primary": "ed25519:2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12",
  "created_at": "2026-01-01T00:00:00Z"
}

JCS-canonicalized form (UTF-8 bytes, no whitespace, keys sorted):

{"created_at":"2026-01-01T00:00:00Z","primary":"ed25519:2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","subject":"github:jason","type":"kez.claim","version":1}

JCS bytes (hex, for byte-level comparison):

7b22637265617465645f6174223a22323032362d30312d30315430303a30303a30
305a222c227072696d617279223a2265643235353139...

Ed25519 signature (over the JCS bytes above, using the seed above — PureEdDSA, no pre-hash):

bc338ba33c28aab2962041e115753865c37f0edca7bdc821ed4f5e8f45bf92e72fbce5623d6d977fa0f8d41b7fff9a47de9ac8123b4ab63429e08223f856540b

The full signed envelope (pretty JSON for display):

{
  "kez": "claim",
  "payload": {
    "type": "kez.claim",
    "version": 1,
    "subject": "github:jason",
    "primary": "ed25519:2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12",
    "created_at": "2026-01-01T00:00:00Z"
  },
  "signature": {
    "alg": "ed25519-sha512-jcs",
    "key": "ed25519:2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12",
    "sig": "bc338ba33c28aab2962041e115753865c37f0edca7bdc821ed4f5e8f45bf92e72fbce5623d6d977fa0f8d41b7fff9a47de9ac8123b4ab63429e08223f856540b"
  }
}

Compact form (kez:z1:) of the same envelope:

kez:z1:KLUv_QBY5QgAlpZCIFCH1gEAeFb7A6gC1YAb2oLOI6pa3SWmN_aYp6OqqqoBPwA2ADoAyrzw02VjaYSyLBLJhILpgnMGoVEG6bJ5X3AGk4i6TCgUZnkCgII3pMtfNJp0uIStODR6kUgilD68dFnwhnQEJ9GTqD2bLpsegN_0Ly-RXHWojDV1_qi22jkIPrfy1TG-5U95_0bW37SIU1qobXlTyOKyrmsBvvrBP3Ghx4ohXqz6ms6qtt7N9iHUzhB6ni4HCvOGRJJ5hWSVm8Akg0Onw-UvjKYTA4VJ5JEC7pwADY9H-kmsmEW0y863TUh5J0er7HV7uFK3GONyTZDF03GtBhVEq_ifx12_LTqnyJ5jgq_LHgwEADhkaDUYyCo6IR0I9QQ

(The compact bytes encode the compact JSON of the full envelope — no pretty-printing — compressed with zstd and base64url-encoded. Implementations that emit raw JSON in different field order or with different whitespace will produce different compact strings even though the signature still verifies; the test above produced exactly these bytes against the reference Rust implementation.)

To reproduce: use the seed and inputs above with any conforming implementation; you should get the same JCS bytes and the same signature.


13. Versioning & Wire Compatibility

Spec versions are major.minor. The current spec is v0.4.

What's non-breaking (minor bump)

  • Adding optional payload fields
  • Adding new envelope tags (kez: "...")
  • Adding new wire encodings (kez:z2:, future)
  • Adding new sigchain ops (existing verifiers MUST ignore unknown ops, preserving chain validity)
  • Adding new signature suites
  • Adding new channels (new system: prefixes)

What's breaking (major bump)

  • Adding required fields
  • Renaming or removing fields
  • Changing the meaning of an existing field
  • Changing the canonicalization scheme
  • Changing the hashing or signature dispatch

Implementation policy

  • An implementation declaring v0.4 MUST accept all v0.4 features.
  • Implementations MAY accept newer minor versions if they ignore unknown additive content safely.
  • Verifiers that encounter an event with an unknown op MUST chain past it (validate seq and prev) but MUST NOT use it for graph-resolution decisions.

14. Changelog

Version Notable
v0.4 (current) BIP-39 recovery phrases for Ed25519 seeds (§3.1), with test vectors. Nostr channel: verifiers SHOULD also accept kinds 1 and 0. Sigchain HTTP store API documented (§9.1). Python implementation added to the conformance set. kez-chat listed as an application (§16).
v0.3 Restructured around the signature envelope; added glossary, cryptographic primitives table, reproducible worked example, versioning policy. Added kez:zc1: sigchain bundle encoding. ActivityPub channel formalized; mastodon: is an alias.
v0.2 Spec-aligned types across implementations. Added Ed25519 (ed25519-sha512-jcs). JCS canonicalization required. Five channels formalized (dns, github, nostr, bluesky, ap).
v0.1 Initial draft. Single signature suite (Schnorr/nostr).

15. Implementation Layout

Each language implementation lives in its own top-level directory. Current implementations:

Directory Language What it ships
rust/ Rust kez-core (lib), kez-channels (lib), kez-cli (binary kez)
nodejs/ TypeScript @kez/core, @kez/channels, @kez/cli — npm workspaces
python/ Python kez package + CLI (python -m kez)
rust-sig-server/ Rust Optional HTTP server for sigchain storage (§9.1)
kez-chat/ Rust + TypeScript Application built on KEZ (§16) — not part of the conformance set

Conformance between implementations is verified by crosstest.sh at the repo root — a harness that generates artifacts with one implementation and verifies them with another, in every direction and every wire encoding, including recovery phrases (§3.1). A passing cross-test is the operational definition of "implementations interoperate."

Canonical fixtures live next to the implementation that owns them (currently python/MNEMONIC-TEST-VECTORS.md). A shared vectors/ directory may collect them for third-party implementations.


16. Applications (informative)

Applications MAY define their own envelope tags and payload types on top of §4; these are outside KEZ conformance but reuse its envelope, canonicalization, and signature suites.

kez-chat — end-to-end encrypted chat over nostr, live at kez.lat. A chat account is an Ed25519 KEZ primary key (recovered from a §3.1 phrase), and profile proofs are ordinary KEZ claims mirrored to the user's sigchain. It adds:

Tag / type Purpose
handle_registration / kez.chat.handle_registration Signed claim of a handle@server name on a chat server
kez-heartbeat-v1 (t field of a JCS payload signed with the primary key, ed25519-sha512-jcs) Binds a published message-encryption key (X25519 + ML-KEM-768) to the primary key

Its messaging protocol (envelope versions, heartbeat ratchet, post-quantum hybrid, nostr event kinds) is specified in kez-chat/README.md and kez-chat/SECURITY.md.