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.
24 KiB
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:
{
"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.
{
"type": "kez.claim",
"version": 1,
"primary": "ed25519:2152f8d19b791d24...",
"subject": "github:jason",
"created_at": "2026-05-19T18:00:00Z"
}
Required fields: type, version, primary, subject, created_at.
Optional fields:
| Field | Type | Meaning |
|---|---|---|
expires_at |
RFC 3339 timestamp | After this time the claim is treated as expired. |
nonce |
opaque string | Prevents replay across publishing channels. |
note |
string ≤256 chars | Human-readable note. |
Type strings are dotted and namespaced (kez.claim,
kez.sigchain.event, future kez.device, etc.).
5.2 Sigchain event payload
{
"type": "kez.sigchain.event",
"version": 1,
"primary": "ed25519:2152f8d19b791d24...",
"seq": 7,
"prev": "sha256:<hex of prior envelope's JCS bytes>",
"created_at": "2026-05-20T12:00:00Z",
"op": "add",
"payload": { ... op-specific ... }
}
seq is strictly monotonic starting from 0. prev is sha256: plus
hex SHA-256 of the JCS bytes of the prior signed envelope (whole
envelope, not just its payload). The first event (seq: 0) has no
prev field.
Per-op payload shapes are defined in §8.
6. Wire Encodings
A signed envelope can be transported in four interchangeable forms. All carry the same logical content; choose the one that fits the channel.
6.1 JSON
Standard JSON serialization of the envelope (pretty or compact). Native
form. Used for HTTP fetches, /.well-known/kez.json, developer tooling.
Different implementations MAY produce different whitespace and field order in raw JSON, but the JCS-canonicalized payload must match. Signatures verify regardless of envelope whitespace.
6.2 Compact (kez:z1:)
URL-safe, copy-pasteable form designed for QR codes, DNS TXT records, chat messages, and other tight channels:
kez:z1:<base64url-no-pad(zstd(envelope-as-compact-json))>
z1is the compact format version. Future versions (z2, …) may change compression or framing.- Compression: zstd, default level 3.
- Encoding: base64url (RFC 4648 §5), no padding.
- The decoded payload MUST parse as a valid signature envelope per §4.
Why zstd over gzip: ~30% smaller for our JSON shapes, low CPU cost,
modern decoder available everywhere (built into Node 22.15+ via
node:zlib, the zstd crate in Rust, etc.).
Implementations MUST support encoding and decoding kez:z1:.
6.3 Markdown
Human-readable proof file suitable for a GitHub gist, profile README,
or any Markdown-rendering surface. The machine-readable proof lives
inside a fenced code block tagged ```kez:
# KEZ Proof
This account publishes a signed KEZ identity claim.
- Primary: `ed25519:2152f8d19b...`
- Subject: `github:jason`
- Created: `2026-05-19T18:00:00Z`
```kez
{
"kez": "claim",
"payload": { ... },
"signature": { ... }
}
```
Parsers MUST locate the first ```kez fence, take the
contents up to the next ```, trim whitespace, parse as
JSON. Surrounding prose is informational.
6.4 DNS TXT
For dns: subjects, the proof is published as a TXT record at:
_kez.<domain>
The TXT record value is the compact encoding (§6.2):
kez:z1:KLUv_QBY...
Raw JSON is NOT a valid DNS TXT proof — JSON exceeds the 255-byte TXT segment limit too easily and complicates quoting. The compact form fits common claims in a single segment (chunked across segments if needed, per standard DNS TXT semantics).
If a claim is too large even compressed, implementations SHOULD
publish a web: proof instead and add a dns: claim pointing at the
domain — chain, don't split.
6.5 Sigchain compact bundle (kez:zc1:)
For transporting an entire sigchain as one string (nostr event content, DNS TXT pointer, ActivityPub profile field):
kez:zc1:<base64url-no-pad(zstd(jsonl))>
Where the inner content is JSONL — one signed sigchain envelope per line. Parsers MUST decompress, split on newlines, and parse each line as a sigchain event envelope.
7. Channels
A channel is a system + the conventions for publishing a proof there so a verifier can fetch it.
| System | Where to publish | Wire encoding |
|---|---|---|
github |
Public gist (filename should include kez), or README on the user's <user>/<user> profile repo |
Markdown (§6.3) |
dns |
TXT record at _kez.<domain> |
Compact (§6.2) |
web |
https://<domain>/.well-known/kez.json |
JSON (§6.1) |
nostr |
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
seqMUST be strictly monotonic, starting at 0.prevMUST equalsha256:+ hex SHA-256 of the JCS-canonicalized envelope (not just payload) of the prior entry. The first event (seq: 0) has noprevfield.- A verifier MUST reject any sigchain with a broken hash chain,
missing
prev, or non-monotonicseq. - Multiple sigchain copies MAY exist across stores; the longest
valid chain wins. If two valid chains diverge at the same
seqwith 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
primaryfield MUST reflect the new key. - Verifiers walk rotations by following the chain: the old key signed
the rotation event, and the
new_key_sigfield within that event is the new key's signature over the same event (withnew_key_sigomitted 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)
- Fetch. Resolve the input identifier on its native channel; fetch the proof. (Real network fetch — see §10.5.)
- Verify signature. Parse the envelope; verify the signature
against
payload.primaryover JCS bytes of the payload. - Confirm channel ownership. The proof was found at a location only the input identity could publish to.
- 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. - 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 againstapi.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:
{
"type": "kez.claim",
"version": 1,
"subject": "github:jason",
"primary": "ed25519:2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12",
"created_at": "2026-01-01T00:00:00Z"
}
JCS-canonicalized form (UTF-8 bytes, no whitespace, keys sorted):
{"created_at":"2026-01-01T00:00:00Z","primary":"ed25519:2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","subject":"github:jason","type":"kez.claim","version":1}
JCS bytes (hex, for byte-level comparison):
7b22637265617465645f6174223a22323032362d30312d30315430303a30303a30
305a222c227072696d617279223a2265643235353139...
Ed25519 signature (over the JCS bytes above, using the seed above — PureEdDSA, no pre-hash):
bc338ba33c28aab2962041e115753865c37f0edca7bdc821ed4f5e8f45bf92e72fbce5623d6d977fa0f8d41b7fff9a47de9ac8123b4ab63429e08223f856540b
The full signed envelope (pretty JSON for display):
{
"kez": "claim",
"payload": {
"type": "kez.claim",
"version": 1,
"subject": "github:jason",
"primary": "ed25519:2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12",
"created_at": "2026-01-01T00:00:00Z"
},
"signature": {
"alg": "ed25519-sha512-jcs",
"key": "ed25519:2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12",
"sig": "bc338ba33c28aab2962041e115753865c37f0edca7bdc821ed4f5e8f45bf92e72fbce5623d6d977fa0f8d41b7fff9a47de9ac8123b4ab63429e08223f856540b"
}
}
Compact form (kez:z1:) of the same envelope:
kez:z1:KLUv_QBY5QgAlpZCIFCH1gEAeFb7A6gC1YAb2oLOI6pa3SWmN_aYp6OqqqoBPwA2ADoAyrzw02VjaYSyLBLJhILpgnMGoVEG6bJ5X3AGk4i6TCgUZnkCgII3pMtfNJp0uIStODR6kUgilD68dFnwhnQEJ9GTqD2bLpsegN_0Ly-RXHWojDV1_qi22jkIPrfy1TG-5U95_0bW37SIU1qobXlTyOKyrmsBvvrBP3Ghx4ohXqz6ms6qtt7N9iHUzhB6ni4HCvOGRJJ5hWSVm8Akg0Onw-UvjKYTA4VJ5JEC7pwADY9H-kmsmEW0y863TUh5J0er7HV7uFK3GONyTZDF03GtBhVEq_ifx12_LTqnyJ5jgq_LHgwEADhkaDUYyCo6IR0I9QQ
(The compact bytes encode the compact JSON of the full envelope — no pretty-printing — compressed with zstd and base64url-encoded. Implementations that emit raw JSON in different field order or with different whitespace will produce different compact strings even though the signature still verifies; the test above produced exactly these bytes against the reference Rust implementation.)
To reproduce: use the seed and inputs above with any conforming implementation; you should get the same JCS bytes and the same signature.
13. Versioning & Wire Compatibility
Spec versions are major.minor. The current spec is v0.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.3MUST 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
opMUST chain past it (validateseqandprev) 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.