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.
665 lines
24 KiB
Markdown
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.
|