Keys
The Label 309 key model — one 32-byte seed, three algorithm keypairs derived from it by domain-separated HKDF-SHA-256, and how recipient public keys are encoded and exchanged.
Label 309 needs three kinds of asymmetric key: an Ed25519 key that signs
records, an X25519 key that receives classical sealed payloads, and an
X-Wing (mlkem768x25519) hybrid key that receives post-quantum sealed
payloads. The standard does not treat these as three independent secrets to
store and shuffle. It defines exactly one secret — a 32-byte seed — and a
deterministic rule that expands it into all three keypairs.
This page specifies that derivation: the seed, the three domain-separated HKDF expansions that produce each algorithm's private key, why the domains are kept separate, and how the resulting recipient public keys are encoded for exchange. What an implementation does with the seed beyond this — where it lives, how it is unlocked, whether one human holds several — is out of scope. Label 309 cares only that, given the same 32 bytes, every conformant implementation derives the same keys.
The seed
A Label 309 key set is rooted in a single value:
| Property | Value |
|---|---|
| Length | 32 bytes (256 bits) |
| Source | A cryptographically secure RNG, or any 32-byte value the user owns |
| Role | Input keying material for the three HKDF expansions below |
The seed is a bare entropy source, not a key in any one algorithm's sense. It carries no curve, no length tied to a primitive, no encoding ceremony. The keys an implementation actually uses are negotiated per derivation; the seed outlives the algorithm choices made over it. A producer MAY generate the seed freshly from the platform CSPRNG or import an existing 32-byte value; either way it MUST decode to exactly 32 bytes. No low-entropy pattern is rejected at the derivation layer — an all-zero seed is a valid input, which is what makes the all-zero seed usable as a reproducible conformance fixture.
The seed is the whole identity
Every public-key fact Label 309 expresses about a party — the key that vouches for a record, the keys that receive a sealed payload — is a deterministic function of these 32 bytes. Reproduce the seed and you reproduce all three keypairs, byte for byte.
Deriving the three keypairs
Each algorithm's private key is an independent HKDF-SHA-256 expansion of the
same seed, per RFC 5869. The three
expansions share their input keying material and their (absent) salt, and differ
only in a single parameter — the info string that names the algorithm:
| Algorithm | info string | Output |
|---|---|---|
| Ed25519 | cardano-poe-ed25519-v1 | 32-byte Ed25519 secret seed |
| X25519 | cardano-poe-x25519-v1 | 32-byte X25519 secret seed |
mlkem768x25519 | cardano-poe-mlkem768x25519-v1 | 32-byte X-Wing decapsulation-key seed |
The derivation in pseudo-code:
ed25519_priv = HKDF-SHA-256(ikm = seed, salt = "", info = "cardano-poe-ed25519-v1", length = 32)
x25519_priv = HKDF-SHA-256(ikm = seed, salt = "", info = "cardano-poe-x25519-v1", length = 32)
mlkem768x25519_priv = HKDF-SHA-256(ikm = seed, salt = "", info = "cardano-poe-mlkem768x25519-v1", length = 32)Three rules make these outputs interoperable across implementations:
- The salt is empty. The HKDF salt MUST be the zero-length byte string.
Per RFC 5869 §2.2 an
absent salt is treated as
HashLenzero bytes — 32 zero bytes for SHA-256 — so every conformant library reaches the same extract step. - The output is 32 bytes. Each expansion requests exactly 32 bytes (a single HKDF block for SHA-256).
- The
infostrings are exact ASCII. Eachinfovalue MUST be encoded as precisely the bytes shown — no surrounding whitespace, no zero terminator, no byte-order mark, no trailing newline. The three strings are 22, 21, and 29 bytes respectively.
The 32 output bytes are the algorithm's secret seed, not its expanded curve scalar. RFC 8032 §5.1.5 draws this distinction for Ed25519: the secret seed is 32 bytes, and the signing library expands it (via SHA-512, then clamping) into the actual scalar and signing prefix internally. The same holds for X25519, where clamping is applied inside the primitive per RFC 7748 §5. An implementation MUST pass the raw 32-byte HKDF output to the primitive and let the library perform expansion and clamping — it does not pre-clamp or pre-expand. For X-Wing the 32-byte output is the X-Wing decapsulation-key seed, from which the full keypair — including the 1216-byte public key — is regenerated deterministically by X-Wing key generation. In every case the compact 32-byte seed, never an expanded key, is the canonical form to store and transport.
Why three domains, not one
HKDF's info parameter is its domain-separation tag: it binds the expanded
output to a specific application context, and
RFC 5869 §3.1 strongly
recommends supplying one when context is available. Label 309 supplies a distinct
tag per algorithm rather than reusing one expansion across all three, even though
all three private keys happen to be 32 bytes wide. The reason is isolation:
- Failures stay contained. If two keypairs shared identical bytes, a weakness specific to one algorithm — a nonce-derivation flaw, a side-channel on a scalar multiplication — could expose the key of an unrelated algorithm. Domain separation guarantees the three private keys are independent functions of the seed, so a compromise of one teaches an attacker nothing about the others.
- Migration stays additive. Each
infostring ends in-v1. Adopting a different curve or a different hybrid in a future revision derives a fresh-v2key from the same seed under a new tag, with no collision against deployed v1 keys. This mirrors the algorithm-agility the wire format itself relies on.
The third tag, cardano-poe-mlkem768x25519-v1, gives the post-quantum hybrid its
own domain even though its decapsulation-key seed is the same 32-byte width as the
classical X25519 secret. A flaw in ML-KEM-768, in X25519, or in the X-Wing
combiner therefore cannot cross-contaminate the classical encryption key or the
signing key.
Recipient public-key encodings
A sealed-PoE sender needs the recipient's public key in a portable string form. Label 309 reuses the age ecosystem's Bech32 recipient encodings, one per registered key-encapsulation mechanism:
KEM (enc.kem) | Public key | Recipient string |
|---|---|---|
x25519 | 32-byte X25519 public key | Bech32 with HRP age1, e.g. age1… (~62 chars) |
mlkem768x25519 | 1216-byte X-Wing public key | Bech32 with HRP age1pqc, e.g. age1pqc… |
The X-Wing public key concatenates an ML-KEM-768 encapsulation key (1184 bytes)
with an X25519 public key (32 bytes), so its age1pqc… recipient string is
1960 characters.
BIP-173 caps a
Bech32 string at 90 characters, but that cap exists for human-typed payment
addresses and does not apply here. An implementation MUST encode and
decode the age1pqc… string without enforcing the 90-character limit while still
applying the Bech32 checksum and charset rules. The distinct HRP age1pqc keeps
the hybrid recipient from colliding with any classical age1… recipient; the
classical encoding stays within ordinary lengths and is handled unchanged.
These strings are recipient-discovery conveniences only. A recipient public key
never appears on a Label 309 record's encryption envelope — an enc.slots[]
entry carries per-slot key material and a wrap value, and the KEM identifier
appears once at enc.kem. How the envelope and slots are built is covered in
Sealed PoE.
The Ed25519 public key as a signature kid
The Ed25519 public key plays no recipient role; it is the key identifier a
verifier resolves a signature against. When a producer signs a record, the raw
32-byte Ed25519 public key is the kid (label 4) in the COSE_Sign1 protected
header, per RFC 9052. A verifier reads
that 32-byte value straight from the on-chain signature and checks the record
body against it — the public key travels with the signature, so no separate
lookup is required to verify authorship. The full signing construction, the
signed payload, and the verification rules are specified in
Signatures.
Out-of-band key exchange
Label 309 specifies how recipient public keys are encoded, not how they are
discovered. The standard prescribes no directory, no registry, and no
on-chain announcement format for recipient keys. A party who wants to receive a
sealed payload publishes their age1… or age1pqc… string through whatever
channel both sides already trust — a hand-off in person, a record signed under
their own Ed25519 key, a record at a stable web or content-addressed location —
and the sender is responsible for the provenance of any key they encrypt to.
This is a deliberate boundary. The same property that lets a record be verified without trusting a server means key exchange must not smuggle a trusted intermediary back in. A name placed next to a key is an attestation by whoever placed it, never a cryptographic claim: two parties using the same handle still produce keys with different bytes, and a verifier compares the bytes. Mapping human-readable names to keys is something an application built on Label 309 MAY offer, but it is an application feature, outside the protocol.
Related pages
- Signatures — how the Ed25519 key signs a record and how the
kidis verified. - Sealed PoE — how the X25519 and X-Wing public keys address an encrypted payload to specific recipients.
- Algorithm registries — the named identifiers for signatures, KEMs, AEADs, and KDFs referenced here.
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.
Signatures
The optional record-level `sigs` array — a detached COSE_Sign1 over the whole record body, its domain-separated signed payload, the two signer-key paths, and strict Ed25519 verification.