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.
This commit is contained in:
parent
7b8b136e92
commit
6dfd5a6938
702
SPEC.md
702
SPEC.md
@ -1,149 +1,228 @@
|
|||||||
# KEZ Specification
|
# KEZ Specification
|
||||||
|
|
||||||
**Status:** Draft v0.2
|
**Status:** Draft v0.3
|
||||||
**Date:** 2026-05-23
|
**Date:** 2026-05-25
|
||||||
|
**Conformance:** Implementations MUST conform to the formats and behaviors
|
||||||
KEZ is a portable, decentralized identity graph. It lets a person say:
|
defined here. Two implementations conform iff a claim signed by one
|
||||||
|
verifies in the other and vice versa (see §15).
|
||||||
> "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. Summary
|
||||||
|
|
||||||
### 1.1 Primary Key
|
KEZ is a portable identity graph. Users sign claims connecting their many
|
||||||
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:
|
accounts, publish those claims in places they control, and anyone can
|
||||||
|
verify the connections without trusting a central server.
|
||||||
|
|
||||||
- `nostr` / `secp256k1` (Schnorr signatures, BIP-340)
|
```
|
||||||
- `ed25519`
|
"These accounts, keys, domains, and identities are all me."
|
||||||
|
```
|
||||||
|
|
||||||
Future, non-normative: passkeys, Bluesky DID keys, Ethereum keys, GPG keys.
|
No central authority blesses the claim. Verification is done by
|
||||||
|
cryptographic signatures against keys the user already controls.
|
||||||
### 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
|
## 2. Glossary
|
||||||
|
|
||||||
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.
|
| 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:
|
Canonical examples:
|
||||||
|
|
||||||
| System | Example |
|
| System | Example |
|
||||||
|------------|--------------------------------------|
|
|---|---|
|
||||||
| `nostr` | `nostr:npub1abc...` |
|
| `nostr` | `nostr:npub1abc...` |
|
||||||
|
| `ed25519` | `ed25519:2152f8d19b791d24...` (64 lowercase hex chars) |
|
||||||
| `github` | `github:jason` |
|
| `github` | `github:jason` |
|
||||||
| `dns` | `dns:jason.example.com` |
|
| `dns` | `dns:jason.example.com` |
|
||||||
|
| `web` | `web:https://jason.example.com` |
|
||||||
| `bluesky` | `bluesky:jason.bsky.social` |
|
| `bluesky` | `bluesky:jason.bsky.social` |
|
||||||
| `did` | `did:plc:abc...` |
|
| `did` | `did:plc:abc...` |
|
||||||
| `ens` | `ens:jason.eth` |
|
| `ens` | `ens:jason.eth` |
|
||||||
| `mastodon` | `mastodon:@jason@server.social` |
|
| `ap` | `ap:@jason@mastodon.social` (alias: `mastodon:@...`) |
|
||||||
| `farcaster` | `farcaster:fid:12345` |
|
| `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.
|
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).
|
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.
|
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. The Claim Format
|
## 4. The Signature Envelope
|
||||||
|
|
||||||
A claim is a UTF-8 JSON object with the following required fields:
|
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
|
```json
|
||||||
{
|
{
|
||||||
"type": "kez.claim",
|
"type": "kez.claim",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"primary": "nostr:npub1abc...",
|
"primary": "ed25519:2152f8d19b791d24...",
|
||||||
"subject": "github:jason",
|
"subject": "github:jason",
|
||||||
"created_at": "2026-05-19T18:00:00Z"
|
"created_at": "2026-05-19T18:00:00Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
KEZ uses dotted, namespaced type strings throughout: `kez.claim`, `kez.sigchain.event`, `kez.device`, etc.
|
Required fields: `type`, `version`, `primary`, `subject`, `created_at`.
|
||||||
|
|
||||||
Optional fields:
|
Optional fields:
|
||||||
|
|
||||||
- `expires_at` — RFC 3339 timestamp; after this time the claim is treated as expired.
|
| Field | Type | Meaning |
|
||||||
- `nonce` — opaque string to prevent replay across publishing channels.
|
|---|---|---|
|
||||||
- `note` — short human-readable note (≤256 chars).
|
| `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. |
|
||||||
|
|
||||||
### 3.1 Canonicalization
|
Type strings are dotted and namespaced (`kez.claim`,
|
||||||
For signing and verification, the claim MUST be serialized as **JCS** (RFC 8785, JSON Canonicalization Scheme). Implementations MUST NOT rely on incidental key ordering.
|
`kez.sigchain.event`, future `kez.device`, etc.).
|
||||||
|
|
||||||
### 3.2 Signature Envelope
|
### 5.2 Sigchain event payload
|
||||||
A signed claim is wrapped in:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"kez": "claim",
|
"type": "kez.sigchain.event",
|
||||||
"payload": { ... the claim object ... },
|
"version": 1,
|
||||||
"signature": {
|
"primary": "ed25519:2152f8d19b791d24...",
|
||||||
"alg": "nostr-secp256k1-schnorr-sha256-jcs",
|
"seq": 7,
|
||||||
"key": "nostr:npub1abc...",
|
"prev": "sha256:<hex of prior envelope's JCS bytes>",
|
||||||
"sig": "<hex signature over JCS(payload)>"
|
"created_at": "2026-05-20T12:00:00Z",
|
||||||
}
|
"op": "add",
|
||||||
|
"payload": { ... op-specific ... }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `kez` (top-level) tags the envelope kind: `"claim"`, `"sigchain_event"`, etc. This lets generic tooling route envelopes without parsing the payload.
|
`seq` is strictly monotonic starting from 0. `prev` is `sha256:` plus
|
||||||
- `alg` is the **full signature suite**, identifying key family + hash + canonicalization. Defined suites in v1:
|
hex SHA-256 of the JCS bytes of the **prior signed envelope** (whole
|
||||||
- `nostr-secp256k1-schnorr-sha256-jcs` — BIP-340 Schnorr over secp256k1, SHA-256, JCS.
|
envelope, not just its payload). The first event (`seq: 0`) has no
|
||||||
- `ed25519-sha512-jcs` — Ed25519 (RFC 8032), JCS-canonicalized payload.
|
`prev` field.
|
||||||
- `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`.
|
Per-op `payload` shapes are defined in §8.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Proof Encodings
|
## 6. Wire 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.
|
A signed envelope can be transported in four interchangeable forms.
|
||||||
|
All carry the same logical content; choose the one that fits the
|
||||||
|
channel.
|
||||||
|
|
||||||
### 4.1 JSON
|
### 6.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:`)
|
Standard JSON serialization of the envelope (pretty or compact). Native
|
||||||
A URL-safe, copy-pasteable form designed for QR codes, DNS TXT records, chat messages, and other tight channels:
|
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(json-envelope))>
|
kez:z1:<base64url-no-pad(zstd(envelope-as-compact-json))>
|
||||||
```
|
```
|
||||||
|
|
||||||
- `z1` is the compact format version. Future versions (`z2`, …) MAY change the compression or framing.
|
- `z1` is the compact format version. Future versions (`z2`, …) may
|
||||||
- Compression: zstd, default level 3.
|
change compression or framing.
|
||||||
- Encoding: base64url (RFC 4648 §5), **no padding**.
|
- Compression: **zstd**, default level 3.
|
||||||
- The decoded payload MUST be a valid signature envelope per section 3.2.
|
- 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:`.
|
Implementations MUST support encoding and decoding `kez:z1:`.
|
||||||
|
|
||||||
### 4.3 Markdown
|
### 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 ````:
|
|
||||||
|
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
|
````markdown
|
||||||
# KEZ Proof
|
# KEZ Proof
|
||||||
|
|
||||||
This account publishes a signed KEZ identity claim.
|
This account publishes a signed KEZ identity claim.
|
||||||
|
|
||||||
- Primary: `nostr:npub1abc...`
|
- Primary: `ed25519:2152f8d19b...`
|
||||||
- Subject: `github:jason`
|
- Subject: `github:jason`
|
||||||
- Created: `2026-05-19T18:00:00Z`
|
- Created: `2026-05-19T18:00:00Z`
|
||||||
|
|
||||||
@ -156,84 +235,150 @@ This account publishes a signed KEZ identity claim.
|
|||||||
```
|
```
|
||||||
````
|
````
|
||||||
|
|
||||||
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.
|
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
|
||||||
|
|
||||||
### 4.4 DNS TXT
|
|
||||||
For `dns:` subjects, the proof is published as a TXT record at:
|
For `dns:` subjects, the proof is published as a TXT record at:
|
||||||
|
|
||||||
```
|
```
|
||||||
_kez.<domain>
|
_kez.<domain>
|
||||||
```
|
```
|
||||||
|
|
||||||
The TXT record value is the **compact encoding** (section 4.2):
|
The TXT record value is the **compact encoding** (§6.2):
|
||||||
|
|
||||||
```
|
```
|
||||||
kez:z1:KLUv_QBY...
|
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.
|
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 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.
|
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:`)
|
||||||
|
|
||||||
## 5. Publishing Proofs
|
For transporting an entire sigchain as one string (nostr event content,
|
||||||
|
DNS TXT pointer, ActivityPub profile field):
|
||||||
|
|
||||||
The claim must be published somewhere only the claimed identity can publish to. Each `system` defines its own channel(s) and default encoding:
|
```
|
||||||
|
kez:zc1:<base64url-no-pad(zstd(jsonl))>
|
||||||
| System | Channel | Encoding |
|
|
||||||
|------------|------------------------------------------------------------|------------------|
|
|
||||||
| `github` | Public gist owned by the user, or `README` on profile repo | Markdown (§4.3) |
|
|
||||||
| `dns` | TXT record at `_kez.<domain>` | Compact (§4.2) |
|
|
||||||
| `web` | `https://<domain>/.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:<hex SHA-256 of the prior entry's JCS envelope>",
|
|
||||||
"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"`.
|
Where the inner content is JSONL — one signed sigchain envelope per
|
||||||
|
line. Parsers MUST decompress, split on newlines, and parse each line
|
||||||
### 6.1 Operations
|
as a sigchain event envelope.
|
||||||
|
|
||||||
- **`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": "<hex>" }`. 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
|
## 7. Channels
|
||||||
|
|
||||||
KEZ is store-agnostic. The verifier MUST be able to fetch sigchains and proofs from any of:
|
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
|
- nostr relays
|
||||||
- GitHub (gists, repos)
|
- GitHub (gists, repos)
|
||||||
@ -241,37 +386,41 @@ KEZ is store-agnostic. The verifier MUST be able to fetch sigchains and proofs f
|
|||||||
- DNS (TXT records)
|
- DNS (TXT records)
|
||||||
- IPFS
|
- IPFS
|
||||||
- Bluesky PDS
|
- Bluesky PDS
|
||||||
- Local filesystem (for development/testing)
|
- ActivityPub (WebFinger + actor JSON)
|
||||||
|
- Local filesystem (for development and 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.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Verification
|
## 10. Verification
|
||||||
|
|
||||||
### 8.1 Verifier Input
|
### 10.1 Starting point
|
||||||
```
|
|
||||||
kez verify <identifier>
|
|
||||||
```
|
|
||||||
|
|
||||||
Example: `kez verify github:jason`
|
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.
|
||||||
|
|
||||||
### 8.2 Algorithm
|
Same mental model as the web: you don't know every URL; you start at
|
||||||
|
one and follow links.
|
||||||
|
|
||||||
1. **Fetch the proof** at the channel native to the input identifier's system (real network fetch — see §8.5).
|
### 10.2 Input and output
|
||||||
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
|
Input: one identifier.
|
||||||
|
|
||||||
|
Output: a graph of identities reachable from the input, each with a
|
||||||
|
status. Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
Primary: nostr:npub1abc...
|
Primary: ed25519:2152f8d19b791d24...
|
||||||
|
|
||||||
Verified identities:
|
Verified identities:
|
||||||
github:jason valid
|
github:jason valid
|
||||||
@ -282,85 +431,234 @@ Status: valid
|
|||||||
Confidence: strong
|
Confidence: strong
|
||||||
```
|
```
|
||||||
|
|
||||||
`Confidence` is a function of: number of independently-verified identities, channel diversity, sigchain length and age.
|
`Confidence` is implementation-defined but is typically a function of:
|
||||||
|
number of independently-verified subjects, channel diversity, sigchain
|
||||||
|
length and age.
|
||||||
|
|
||||||
### 8.4 Failure Modes
|
### 10.3 Algorithm (5 phases)
|
||||||
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.
|
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).
|
||||||
|
|
||||||
### 8.5 Network Fetching Is Required
|
### 10.4 Failure modes
|
||||||
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.<domain>`. Verifying a TXT *value* a caller already obtained is a development helper, not conformant verification.
|
A verifier MUST distinguish:
|
||||||
- `github:` — HTTPS fetch against `api.github.com` and/or `raw.githubusercontent.com` for the user's gist or profile README.
|
|
||||||
|
| 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`.
|
- `web:` — HTTPS fetch of `/.well-known/kez.json`.
|
||||||
- `nostr:` — connect to one or more relays and fetch the relevant event.
|
- `nostr:` — connect to one or more relays; fetch the relevant event.
|
||||||
- `bluesky:` — HTTPS fetch against the user's PDS.
|
- `bluesky:` — HTTPS fetch against the user's PDS.
|
||||||
|
- `ap:` — HTTPS fetch of WebFinger followed by actor JSON.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Starting Points (Discovery)
|
## 11. Cryptographic Primitives
|
||||||
|
|
||||||
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.
|
| 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. |
|
||||||
|
|
||||||
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.
|
The suite string names every choice (`<key-family>-<hash>-<canonicalization>`)
|
||||||
|
so a verifier can dispatch without guessing. Future suites follow the
|
||||||
|
same pattern.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. MVP Scope (v0.2)
|
## 12. Worked Example (reproducible)
|
||||||
|
|
||||||
A v0.2 implementation MUST support:
|
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).
|
||||||
|
|
||||||
1. The claim format defined in section 3, including the envelope (§3.2).
|
**Inputs (TEST ONLY — do not use this key for anything real):**
|
||||||
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.<domain>`)
|
|
||||||
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
|
Ed25519 seed (32 bytes, hex):
|
||||||
kez-channels network adapters: github, dns, web, nostr, bluesky, ...
|
4242424242424242424242424242424242424242424242424242424242424242
|
||||||
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).
|
```
|
||||||
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Test Vectors
|
## 13. Versioning & Wire Compatibility
|
||||||
|
|
||||||
A `vectors/` directory at the repo root contains canonical inputs and expected outputs:
|
Spec versions are `major.minor`. The current spec is **v0.3**.
|
||||||
|
|
||||||
- `vectors/claims/` — signed claim JSON + the expected JCS bytes + signature + compact encoding + markdown encoding.
|
### What's non-breaking (minor bump)
|
||||||
- `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.
|
- 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 13. One-Sentence Summary
|
## 14. Changelog
|
||||||
|
|
||||||
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.
|
| 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.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user