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:

PropertyValue
Length32 bytes (256 bits)
SourceA cryptographically secure RNG, or any 32-byte value the user owns
RoleInput 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:

Algorithminfo stringOutput
Ed25519cardano-poe-ed25519-v132-byte Ed25519 secret seed
X25519cardano-poe-x25519-v132-byte X25519 secret seed
mlkem768x25519cardano-poe-mlkem768x25519-v132-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:

  1. 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 HashLen zero bytes — 32 zero bytes for SHA-256 — so every conformant library reaches the same extract step.
  2. The output is 32 bytes. Each expansion requests exactly 32 bytes (a single HKDF block for SHA-256).
  3. The info strings are exact ASCII. Each info value 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 info string ends in -v1. Adopting a different curve or a different hybrid in a future revision derives a fresh -v2 key 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 keyRecipient string
x2551932-byte X25519 public keyBech32 with HRP age1, e.g. age1… (~62 chars)
mlkem768x255191216-byte X-Wing public keyBech32 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.

  • Signatures — how the Ed25519 key signs a record and how the kid is 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.