Algorithm registries
The named-identifier registries for hashes, AEADs, KEMs, KDFs, and signatures — and the agility rule that makes post-quantum migration additive rather than breaking.
Every cryptographic choice in Label 309 is named by a string identifier drawn
from an extensible registry — sha2-256, chacha20-poly1305-stream64k, EdDSA,
and so on. A record never carries a raw algorithm number or an implicit assumption; it
states which primitive it used, and a verifier looks that identifier up. This is
the mechanism behind the algorithm-agile invariant: the registries can grow
over time, and a verifier that meets an identifier it does not implement
rejects the record with a stable, typed error — it never crashes, and it
never silently accepts something it cannot check.
That single rule is what makes the migration to post-quantum algorithms additive. A new identifier is a new row in a table, not a new version of the wire format. Older records keep verifying exactly as before, and older verifiers fail closed against records they were never built to understand.
Two standing rules
Across every registry, two constraints are absolute. Implementations MUST NOT invent novel cryptography — every primitive traces to a named public standard. And all encryption MUST be authenticated: only AEAD constructions are permitted, never an unauthenticated cipher with a bolt-on (or absent) integrity check.
Hash
The content hash is the primary claim of every record, so the hash registry is the most load-bearing. Both registered functions produce a 32-byte digest, and both are mandatory for a conformant implementation to support.
| Identifier | Algorithm | Digest |
|---|---|---|
sha2-256 | SHA-256 (FIPS 180-4) | 32 B |
blake2b-256 | BLAKE2b-256 (RFC 7693) | 32 B |
A producer MAY hash the same content under both functions for defense in depth; a single hash is sufficient for a valid record.
Merkle commitment
For committing to an ordered list of leaves under a single on-chain root, Label 309
registers one Merkle-commitment identifier. It is the IANA-registered string for
the SHA-256 binary Merkle tree, sharing the leaf-prefix (0x00) and
internal-node-prefix (0x01) domain separation that prevents leaf/node
collisions.
| Identifier | Algorithm | Root |
|---|---|---|
rfc9162-sha256 | RFC 9162 binary Merkle tree, SHA-256 | 32 B |
A single-leaf tree commits to SHA-256(0x00 ‖ leaf), not to the bare leaf —
so a one-file proof MUST use a plain hash identifier, never a 1-leaf tree.
AEAD
The AEAD registry governs which content format protects a sealed payload on the
wire — the enc.aead field. Exactly one identifier is registered under
enc.scheme: 1, and it is a segmented format rather than a single-shot cipher.
| Identifier | Algorithm | Key / Nonce / Per-chunk nonce / Tag | Status |
|---|---|---|---|
chacha20-poly1305-stream64k | ChaCha20-Poly1305, 64 KiB segmented STREAM | 32 B / 24 B / 12 B / 16 B per chunk | Mandatory — the wire format |
aes-256-gcm | AES-256-GCM | — | Reserved (future profile) |
chacha20-poly1305-stream64k is ChaCha20-Poly1305
(RFC 8439) in the 64 KiB segmented STREAM
layout of the age v1 specification:
the plaintext is split into 65536-byte chunks, and each chunk is sealed under the
content key with a 12-byte per-chunk nonce uint88_be(counter) ‖ final_flag
(counter from 0, final_flag 0x01 on the last chunk) and an empty per-chunk AAD,
producing a 16-byte tag per chunk. The 24-byte enc.nonce is not a chunk nonce:
it is the envelope-unique salt of the content-key HKDF, which is what keeps the
counter nonces safe — the content key is single-use, so no two streams ever share a
(key, nonce) pair and stateless producers never coordinate nonces across
envelopes. The segmented layout lets a verifier authenticate and release a large
payload incrementally with bounded memory, and the final flag makes truncation
detectable. The on-wire spelling is exactly chacha20-poly1305-stream64k;
alternative spellings MUST NOT be produced. The full construction is on
Sealed PoE.
chacha20-poly1305-stream64k is the only identifier that may appear as the on-wire
content format. A separate construction, chacha20-poly1305
(RFC 8439, 32-byte key / 12-byte nonce /
16-byte tag), is used internally to wrap a per-recipient key inside the sealed
construction: a 12-byte all-zero nonce, AAD set to the chosen KEM's info label,
producing a 48-byte wrap (32-byte wrapped key + 16-byte tag). It is a building
block, not a wire identifier, and a record that names it in enc.aead MUST be
rejected. aes-256-gcm is named but inactive; it is reserved for a future
encryption profile (enc.scheme: 2) and a v1 verifier rejects any record that
selects it.
KEM
The KEM registry covers the key-encapsulation mechanisms used to address a sealed payload to specific recipients. Label 309 registers a classical curve KEM and a post-quantum hybrid that ships active from the first release.
| Identifier | Algorithm | Public key / Secret | Ciphertext / Shared secret |
|---|---|---|---|
x25519 | X25519 ECDH (RFC 7748) | 32 B / 32 B | 32 B / 32 B |
mlkem768x25519 | X-Wing hybrid (ML-KEM-768 + X25519) | 1216 B / 32 B | 1120 B / 32 B |
mlkem768x25519 is the X-Wing construction from
draft-connolly-cfrg-xwing-kem-10:
it pairs ML-KEM-768 (FIPS 203) with
X25519 (RFC 7748) so that an attacker
must break both to recover the shared secret. The public key is the
ML-KEM-768 encapsulation key concatenated with the X25519 public key
(1184 B ‖ 32 B = 1216 B); the secret is a 32-byte seed from which the full key is
derived. Each recipient's ciphertext is the ML-KEM-768 ciphertext concatenated
with an ephemeral X25519 public key (1088 B ‖ 32 B = 1120 B), and the two shared
secrets are combined by the X-Wing SHA3-256 combiner
(FIPS 202) into the final 32-byte
secret. Label 309 consumes X-Wing as a black-box KEM — encapsulate,
decapsulate, the 32-byte shared secret — and relies on no property of the
combiner's internal hashing. The identifier is written without internal hyphens to
match the established X-Wing spelling.
The hybrid is selected per-record by the encryption header, independently of the content AEAD. Because it is already registered, post-quantum confidentiality is a matter of choosing the identifier — not waiting for a new wire-format version.
A single record names exactly one enc.kem, and every slot uses that KEM's shape;
a slot of the wrong shape is ENC_SLOT_INVALID_SHAPE, a wrong-length epk
(≠ 32 B) or kem_ct (≠ 1120 B) is KEM_EPK_LENGTH_MISMATCH /
KEM_CT_LENGTH_MISMATCH, and an unregistered enc.kem is UNSUPPORTED_KEM_ALG.
The encapsulation material must also be distinct within one slots[]: all epk
values (for x25519) or all kem_ct values (for mlkem768x25519) MUST differ. A
within-record
duplicate is rejected with ENC_SLOTS_DUPLICATE_KEM_MATERIAL before any KEM or
AEAD primitive runs, because a repeated epk or kem_ct breaks the per-slot key
uniqueness the zero-nonce wrap depends on. A verifier also bounds parser resource
use before any primitive: an envelope whose slots[] exceeds the reference bound of
1024 slots is ENC_SLOTS_TOO_MANY, and a decoded enc envelope exceeding 65 536
bytes is ENC_ENVELOPE_TOO_LARGE. Both bounds sit far above the ~16 KiB Cardano
metadata ceiling that caps any honest record; they are verifier-enforced,
deployment-pinned constants — not wire fields — and deployments MAY tighten them.
KDF
The KDF registry names the key-derivation functions. hkdf-sha256 derives keys
inside the sealed construction; argon2id stretches a human passphrase against
brute force and carries a mandatory parameter floor.
| Identifier | Algorithm | Parameters |
|---|---|---|
hkdf-sha256 | HKDF-SHA-256 (RFC 5869) | salt (optional), info (optional), output length |
argon2id | Argon2id (RFC 9106) | memory ≥ 65536 KiB, iterations ≥ 3, parallelism ≥ 1 |
The Argon2id floor is normative: a passphrase-protected payload MUST use at least
64 MiB of memory, at least three iterations, and at least one lane. A producer
MAY choose stronger parameters, and they travel with the record so a verifier can
reproduce the derivation. Where the platform supports it, producers SHOULD set the
parallelism p = 4 — the second recommended profile of
RFC 9106 §4 — while a verifier
MAY accept any p ≥ 1, subject to deployment ceilings. Those ceilings are an
implementation SHOULD, not a MAY: a verifier SHOULD enforce upper bounds against a
verifier-side denial-of-service from absurd parameters, reporting
ENC_PASSPHRASE_PARAMS_EXCEED_POLICY. The ceiling is hardware-dependent and
non-normative, and MUST NOT be conflated with the floor code
ENC_PASSPHRASE_ARGON2_PARAMS_TOO_LOW.
Only argon2id is wire-selectable: it is the sole identifier a record may
name in enc.passphrase.alg. hkdf-sha256 is an internal building block — it
is the fixed extract-and-expand step behind the seed-to-key derivation, the
per-slot KEK derivation, the slot-set MAC key, the passphrase-commitment MAC key,
and the content-key derivation — and it carries no wire identifier. A record that
names hkdf-sha256 in enc.passphrase.alg MUST be rejected: HKDF is built for
high-entropy inputs, not for stretching a low-entropy passphrase.
Internal labels are constants, never on the wire
The sealed construction draws its domain separation from a fixed set of label
literals — HKDF info tags and SHA-256 prefixes (KEK-salt prefixes, transcript
prefixes, and the item-hashes prefix). Each is a constant of enc.scheme: 1,
exact ASCII with no terminator or length prefix; none is ever serialised, and none
is selectable through any registry. There are eleven:
| Label | Role |
|---|---|
cardano-poe-kek-v1 | HKDF info for the per-slot KEK on the x25519 path |
cardano-poe-kek-mlkem768x25519-v1 | HKDF info for the per-slot KEK on the mlkem768x25519 path |
cardano-poe-x25519-kek-salt-v1 | SHA-256 prefix for the x25519 KEK HKDF salt |
cardano-poe-xwing-kek-salt-v1 | SHA-256 prefix for the mlkem768x25519 KEK HKDF salt |
cardano-poe-item-hashes-v1 | SHA-256 prefix for the item-hashes digest hashes_hash |
cardano-poe-slots-transcript-v1 | SHA-256 prefix for the slots-transcript hash slots_hash |
cardano-poe-slots-mac-v1 | HKDF info for the slot-set MAC key |
cardano-poe-passphrase-transcript-v1 | SHA-256 prefix for the passphrase-transcript hash pw_hash |
cardano-poe-passphrase-mac-v1 | HKDF info for the passphrase commitment MAC key |
cardano-poe-payload-v1 | HKDF info for the slots-path content key |
cardano-poe-payload-passphrase-v1 | HKDF info for the passphrase-path content key |
Both KEK salts share one labelled-hash shape —
SHA-256(label ‖ enc.nonce ‖ <slot KEM material> ‖ pub_R) — under their own
per-KEM label. These labels are distinct from the seed-derivation info strings on
Keys and from the record-signing domain prefix on
Signatures: the set is collision-free and prefix-free, so no
per-record sealed label equals — or is a byte-prefix of — a long-term-key-derivation
label, and identity-key derivation and per-record key wrapping never collide. A
verifier MUST use each literal byte-for-byte; a single divergent byte yields a
slots_mac, a commitment, or an AEAD tag the honest producer cannot reproduce. The
byte-level construction that consumes each label is on Sealed PoE.
Signature
Label 309 registers one signature algorithm. Authorship signatures are always
optional, but when present they are carried as COSE_Sign1 (RFC 9052) using
Ed25519.
| Identifier | COSE alg | Algorithm | Wrapper |
|---|---|---|---|
EdDSA | -8 | Ed25519 (RFC 8032) | COSE_Sign1 (RFC 9052) |
Verification is strict per RFC 8032 §5.1.7: implementations MUST reject non-canonical signature encodings and small-order points (no cofactor-clearing extension). This matches the conservative acceptance criteria used across Cardano wallets, so a signature that verifies under one conformant implementation verifies under all of them.
Signature support is independent of the content claim. A verifier that does not implement a record's signature algorithm marks that signature slot as unsupported and leaves the timestamp and content claims fully valid — an unknown signature algorithm never invalidates the record itself.
Reserved identifiers
Several identifiers are named but not yet active. They mark the agreed migration path so that future profiles use stable, pre-committed names rather than ad-hoc strings. A conformant producer MUST NOT emit them, and a conformant verifier MUST reject any record that uses one, with the matching typed error.
| Identifier | Algorithm | Role |
|---|---|---|
aes-256-gcm | AES-256-GCM (NIST SP 800-38D) | Content AEAD |
ml-kem-768 | ML-KEM-768 (FIPS 203), standalone | KEM |
ml-dsa-65 | ML-DSA (FIPS 204) | Signature |
slh-dsa-sha2-128s | SLH-DSA (FIPS 205) | Signature |
ml-kem-768 is the bare post-quantum KEM, distinct from the registered hybrid
mlkem768x25519; the hybrid is what Label 309 ships, on the principle that a
classical fallback should remain even after the post-quantum half is added.
Algorithm agility and migration
Adding an algorithm is a self-contained, additive operation: cite a public standard for the primitive, add the identifier to the appropriate registry, provide a vetted, well-reviewed implementation of it, and publish a cross-language conformance fixture so independent implementations agree byte-for-byte. The wire-format version does not change, because the schema does not change — only the set of recognized strings grows.
The consequences follow directly from the registry design:
- Old records stay verifiable. Their identifiers are still in the registry, so every existing record verifies exactly as it did the day it was published.
- Old verifiers fail closed. A verifier that predates a new identifier
rejects records using it with a stable
UNSUPPORTED_*error rather than guessing — there is no path to silent acceptance. - Post-quantum support is additive. Because
mlkem768x25519is already registered, and because new KEMs and signatures slot into the same mechanism, the post-quantum transition is a registry growth, not a breaking migration.
A wire-format version bump is reserved for genuinely breaking schema changes — a new required field, a removed field, a changed type. Registry growth never qualifies, which is precisely what lets the cryptographic catalogue evolve without ever stranding a published proof.