Kez/SPEC.md
Tudisco 6dfd5a6938 spec: v0.3 — restructure, add glossary, worked example, primitives,
versioning policy, changelog

Full sweep across the three buckets discussed:

Bucket A (quick wins — staleness/bugs):
- Fix §3 (was §2): drop wrong mastodon row with double-@; ActivityPub
  channel formalized as `ap:` with mastodon as alias; consolidated
  with current channel set.
- §7 channels table now matches the actually-shipped channel adapters
  in rust-channels and node-channels.
- Drop §12 Test Vectors (the directory never existed). Replaced with
  one paragraph in §15 pointing at the crosstest.sh harness, which
  is what we actually use for inter-implementation conformance.
- Replace §10 historical "MVP Scope (v0.2)" with §14 Changelog.
- §15 Implementation Layout now points at actual repos (rust/,
  nodejs/, rust-sig-server/) rather than the never-existed kez-web.

Bucket B (simplifications):
- Folded §9 Starting Points into §10.1 (one paragraph).
- Consolidated §1 Core Concepts and §13 One-Sentence Summary into
  the new opener (§1 Summary + §2 Glossary).
- §3.1 Canonicalization inlined into §4.2 (where it actually applies).
- §8 Verification trimmed from 9 conflated steps to 5 clean phases.
- §8.5 "MUST" softened to "expected" for libraries; complete verifiers
  do network, helpers don't.

Bucket C (real improvements + restructure):
- §2 Glossary added (primary key, claim, subject, proof, channel,
  sigchain, signature envelope, identity graph — all in one place).
- §11 Cryptographic Primitives table — every algo we use, its role.
- §12 Worked Example with REAL reproducible bytes: fixed Ed25519
  seed (4242... — clearly labeled TEST ONLY), specific subject and
  timestamp, the exact JCS bytes, the exact deterministic Ed25519
  signature, the exact compact form. Generated against the reference
  Rust implementation; any conforming implementation should produce
  identical bytes.
- §13 Versioning & Wire Compatibility policy — what bumps major,
  what bumps minor, how implementations handle unknown ops.
- §14 Changelog — v0.1 / v0.2 / v0.3 with notable changes.
- §8.4 Sigchain in pictures — ASCII diagram showing 5 events with
  hash chaining and rotation.

Structural reorganization:
- §1 summary → §2 glossary → §3 identifiers → §4 signature envelope
  → §5 payload shapes → §6 wire encodings → §7 channels → §8 sigchain
  → §9 storage → §10 verification → §11 crypto → §12 worked example
  → §13 versioning → §14 changelog → §15 implementation layout.
- The envelope (the unit of transport) is now described before the
  payloads it wraps, matching what's actually on the wire.

Also: added §6.5 documenting `kez:zc1:` (compact sigchain bundle)
that exists in the implementations but was missing from the spec.
2026-05-24 22:51:51 -06:00

665 lines
24 KiB
Markdown

# KEZ Specification
**Status:** Draft v0.3
**Date:** 2026-05-25
**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. |
---
## 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).
---
## 4. The Signature Envelope
Every signed thing in KEZ — claims, sigchain events, future payload
types — is wrapped in the same three-field envelope:
```json
{
"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 v0.3.
### 5.1 Claim payload
A claim asserts that a primary key controls a subject identity.
```json
{
"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
```json
{
"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 ````:
````markdown
# 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` | Nostr event of kind `30078` (parameterized replaceable), `d` tag `kez` | JSON in event content |
| `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.
---
## 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. |
| **SHA-256** | Sigchain `prev` chaining; pre-hash for Schnorr signing (`sig = schnorr(SHA-256(JCS(payload)), key)`). |
| **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:**
```json
{
"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):**
```json
{
"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.3**.
### 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.3` MUST accept all v0.3 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.3** (current) | 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 |
| `rust-sig-server/` | Rust | Optional HTTP server for sigchain storage |
Conformance between implementations is verified by `crosstest.sh` at
the repo root — a harness that generates artifacts with one
implementation and verifies them with the other, in every direction
and every wire encoding. A passing cross-test is the operational
definition of "two implementations interoperate."
A future `vectors/` directory may hold canonical test fixtures for
third-party implementations to validate against. Not required while
two interop-tested implementations exist.