Security model

What a Label 309 verifier trusts and what it does not — the standalone-verifiability invariant, the privacy guarantees of sealed PoE, the normative crypto rules every implementation must uphold, and the known limits of the wire format.

Label 309's security rests on one idea: a proof is something the relying party checks for itself. A verifier trusts the public Cardano chain and — for content claims — the bytes it is handed. It trusts nothing else: not the publisher, not a domain, not a server, not the operator of whatever gateway it happened to read the transaction through. Every guarantee on this page either follows from that stance, marks its boundary, or names a residual risk that lies outside it.

This page is the threat model for the standard itself: the trust model a conformant verifier operates under, the privacy properties of an unsigned sealed PoE, the cryptographic obligations every implementation MUST uphold, and the limits the wire format does not — and cannot — overcome. It does not describe any particular deployment's operational security; it describes what the format guarantees to anyone who implements it correctly.

The trust model

A relying party that checks a Label 309 proof is asking a narrow question: did these bytes exist on or before this block's time, and does this verdict depend on anyone's good behaviour besides Cardano's? The standard is built so the answer to the second half is always "no". There is no issuer-controlled intermediary at any step: the chain gateway and the storage gateway a verifier consults are untrusted data sources, verified cryptographically where possible and cross-checked for inclusion and finality.

What a verifier trusts:

  • The public Cardano chain. The transaction, its metadata under label 309, and its block time are facts replicated across every Cardano node. A verifier reads them through a gateway of its own choosing and may cross-check several.
  • The content bytes, when checking a content claim. A digest in the record commits to a specific byte sequence. A verifier that holds those bytes recomputes the hash and compares; it does not take anyone's word that the bytes match.

What a verifier does not trust:

  • The publisher. Label 309 has no issuer field. Any wallet can publish any record; the record's validity is independent of who submitted it. A verifier never asks "is this publisher reputable?" — the question has no place in the model. See the issuer-agnostic invariant on Introduction.
  • A server or domain. No step of verification contacts a publisher-operated service. There is no canonical registry to consult, no status endpoint to poll, no identity authority to resolve a key against.
  • A gateway operator. The Cardano, Arweave, and IPFS gateways a verifier reads through are interchangeable and content-checked (below). A gateway is a transport, not a trust root.

Standalone-verifiability is the core security property

A Label 309 proof must remain checkable even if every party who created it disappears — the publisher, their server, their company. Given a transaction reference, a public Cardano gateway, and (for content claims) the bytes, any conformant implementation reaches the same verdict from the public chain alone. If a verification path silently needs a specific operator's cooperation, that path is non-conformant.

Why gateways are not a trust root

A verifier may read the same transaction through any Cardano gateway, and read the same content through any Arweave or IPFS gateway. None of these is trusted to return honest data, because what they return is independently checkable:

  • A gateway cannot forge a record that carries a valid signature — it does not hold the signer's private key, and the verifier recomputes the signed bytes and checks them itself. See Signatures.
  • A gateway cannot substitute content under a content-addressed URI without detection: an ipfs:// CID is a multihash the verifier recomputes from the fetched bytes, and an ar:// transaction id commits to the data under Arweave consensus. On a conformant item, the on-chain hashes map is a second, independent binding to the plaintext, recomputed at fetch time.
  • A gateway that withholds a transaction or misreports how deeply it is buried is a denial-of-service shape, not a forgery shape. A verifier that reads through more than one gateway and aborts on disagreement turns "one dishonest gateway" into "collusion across every gateway it chose" — which it controls.

Block depth — how many confirmations a verifier demands before treating a record as settled — is verifier policy, not a value Label 309 mandates. The standard fixes no numeric depth; a relying party sets a threshold for its own risk tolerance. See Verification.

Privacy properties of sealed PoE

A sealed PoE keeps a plaintext confidential while still anchoring its timestamp. Beyond confidentiality, the construction is designed so the chain leaks as little as possible about the message and nothing about its audience. These are wire-format guarantees: they hold for any correct implementation, because they are properties of the bytes, not of any deployment.

Sender anonymity for unsigned sealed PoE

An unsigned sealed PoE — a record carrying enc and no sigsMUST NOT disclose the sender's identity on chain. Its on-wire bytes MUST be independent of the sender's long-term keys.

This holds structurally:

  • No signer key on the wire. A sender's identity key reaches the chain only through a sigs entry. A record with no sigs carries no signer-side byte at all. Authorship is opt-in; anonymity is the default. The sender's long-term X25519 and Ed25519 keys never appear in an unsigned record.
  • Fresh per-slot encapsulation. Each recipient slot carries per-record, per-slot KEM material, freshly generated at seal time — the X25519 ephemeral public key in slot.epk on the x25519 path, or the X-Wing ciphertext in slot.kem_ct on the mlkem768x25519 path. It is unrelated to the sender's long-term key and reveals nothing that links records to a common author.
  • Shuffled slots. The slot array is shuffled with a CSPRNG before publication, so positional ordering ("primary recipient first") carries no signal.

A producer who later signs a record discloses their identity key in that record only; it appears in no earlier unsigned record, and no operation maps the earlier records' per-slot KEM material back to the author. The identity X25519 and Ed25519 keys derive from the master seed via different HKDF context strings, and an Ed25519 public key cannot derive or match an X25519 public key, so signing one record never links the unsigned ones. Opting into authorship is a forward act, not a retroactive deanonymisation.

This invariant covers only the label-309 record bytes. The fee-paying Cardano address in the transaction inputs, the producer's network-level identity as seen by a gateway, and any metadata in the off-chain ciphertext blob are outside the wire format — Label 309 lives in the metadata, not in the transaction inputs, and cannot defend against fee-payer correlation. A producer who needs fee-payer anonymity must address it at the transaction-construction layer.

Recipient unlinkability

A sealed record never names its recipients. A recipient recognises a message as theirs only by successfully trial-decrypting a slot; there is no addressee field to read. Two distinct guarantees follow, and they must not be conflated.

The first holds for both KEMs and for any observer:

  • Recipients cannot see each other. Each slot is an independently wrapped key. A recipient who opens their own slot derives nothing about any other slot and cannot tell who else was addressed.
  • No recipient public keys on the wire. Only the per-slot encapsulation (epk or kem_ct) and the wrapped key appear. An observer with no candidate keys cannot enumerate recipients — it learns only the slot count, the KEM family (enc.kem), and the sealed-versus-open distinction.

The second — recipient anonymity against an adversary who already holds a set of candidate recipient public keys and wants to test whether a slot is addressed to one of them — is a stronger, KEM-specific property, and the standard claims it only for the classical path:

  • x25519 is key-private. The per-slot epk is a fresh ephemeral public key statistically independent of the recipient key. From epk and wrap alone, a key-holding adversary cannot decide which candidate (if any) a slot targets without the matching private key.
  • mlkem768x25519 does not claim it. Recipient anonymity against a key-holding adversary is a separate property not implied by the IND-CCA security of the hybrid KEM, and is not claimed for the X-Wing path unless and until it is independently justified for X-Wing. A deployment whose threat model requires recipient anonymity against a key-holding adversary MUST NOT rely on the hybrid path for that property. The honest leakages above — slot count, KEM family, sealed-versus-open — are identical for both KEMs; nothing more about the recipients is exposed on either path.

Plaintext-binding

The on-chain digest commits to the plaintext, not to the ciphertext. A recipient who decrypts a sealed PoE recomputes the plaintext hash and compares it to the on-chain hashes map. This means a storage layer that serves a tampered ciphertext is caught after decryption, independent of the URI's own integrity model — the timestamp claim is a claim about the plaintext the sender committed to, and nothing the gateway does can satisfy it with different bytes.

Rebroadcast independence

A record's signatures are fixed at the moment its transaction is submitted. The chain has no notion of an "extended signer set" accumulating across transactions.

Rebroadcasting an identical record body in a separate transaction with a different sigs set creates a separate, independent record at its own block time — never an extension of the original.

A party who wants to endorse an already-published record publishes their own record — referencing the same content, or pointing at the prior transaction via supersedes — signed by their key. They do not append a witness to someone else's record. Consequently, an attacker who copies a signed record's bytes into a fresh transaction does not become a co-signer of the original: they produce a later, duplicate claim with whatever sigs they chose, and the original's earlier block time remains the canonical record of existence. Verifiers and indexers MUST NOT collapse two transactions with identical record bodies into a single "merged signers" view; doing so would falsify the per-record block-time semantics downstream consumers rely on.

Normative rules for implementations

The following are security-normative for any conformant implementation, regardless of language or platform. RFC 2119 / RFC 8174 keywords are used in the normative sense.

RuleObligation
No novel cryptographyEvery primitive is a documented IETF / NIST / W3C standard from an audited library. No hand-rolled ciphers, KDFs, signature schemes, or stanzas.
Authenticated encryption onlyAll symmetric encryption is AEAD. A tag-verification failure MUST raise a typed error; it MUST NOT silently return partial plaintext.
Constant-time secret comparesCompare MAC tags, AEAD tags, and any secret-derived bytes in constant time. A data-dependent compare on these surfaces is an authentication oracle.
Strict Ed25519 verificationVerify signatures in canonical (strict) mode. A cofactored / lax verifier accepts edge-case signatures a strict verifier rejects and disagrees with the reference.
Never log secrets or plaintextKey material, derived keys, and decrypted plaintext MUST NOT appear in logs at any level. Byte-typed values are redacted unconditionally.

No novel cryptography

Label 309 deliberately admits only well-analysed primitives, each named in the algorithm registries and implemented by an audited library. An implementation MUST NOT introduce a custom symmetric cipher, KDF, signature construction, or key-wrapping stanza. The wire format is a composition of standard parts precisely so its security reduces to the security of those parts.

Authenticated encryption only

Every symmetric encryption in Label 309 — the content layer and the per-slot key wrap — is AEAD. There is no unauthenticated cipher mode anywhere in the format. A decryptor whose AEAD tag check fails MUST surface a structured error with a stable code discriminator and MUST NOT fall through to return unauthenticated bytes. The additional authenticated data is part of the tag computation, so a wrong AAD fails identically to a wrong key — there is no partial-success state to probe.

Constant-time comparison of secrets

A comparison loop whose running time depends on secret bytes is an oracle. Every equality check over a secret or a MAC/AEAD tag MUST be constant-time:

  • The slot-set MAC and any HMAC tag — a per-byte timing leak on a 32-byte MAC can, under adversarial repetition, recover the tag.
  • AEAD tag verification — the underlying library's decrypt already returns failure without revealing which byte mismatched; implementations MUST NOT re-implement this check.
  • Signature verification — use the library's verify, never a hand-rolled equation.

A bare == / === on byte arrays touching any of these surfaces is a defect. Routing every such comparison through a single typed helper makes the property auditable.

Constant-time discipline extends to the shape of the trial-decrypt loop, not just to individual comparisons. To preserve the hidden-recipient property, a recipient verifier MUST process all slots within a single private key's pass — a constant number of slot operations per key, with no early break on the first match — so a network-level observer with timing access cannot infer which slot index a key unwrapped, or whether any did. The candidate key is selected in constant time. The validity of the X25519 share is captured by a secret-independent bit, kem_ok = NOT constantTimeEqual(shared, 0^32), computed with a constant-time compare rather than an early branch; the KEK is then a constant-time select between the real KEK and a dummy KEK derived from an all-zero shared secret under the same salt and info, and kem_ok is folded into the slot's acceptance (ok = kem_ok AND open_ok AND mac_ok) so an invalid-ECDH slot can never be accepted while the loop still performs identical work. The slot array is published in CSPRNG-shuffled order, so wire position already carries no "primary recipient first" signal; combined with the constant-time pass, an observer learns only the slot count. A recipient holding several private keys (for example archived keys across an identity rotation) iterates key × slot and MAY short-circuit across keys — that leaks only the weak "which key matched" signal — but MUST stay constant-time across the slots of any single key, re-deriving the recipient-key half of the KEK salt per key, since both KEMs commit the salt to that key's canonical wire encoding (exactly the 32-byte X25519 public key, or exactly the pinned 1216-byte X-Wing public-key bytes — never a non-canonical re-encoding).

Strict Ed25519 verification

Label 309 signatures MUST be verified in strict (canonical) mode. A lax, cofactored verifier accepts certain malleable or low-order-key signatures that a strict verifier rejects, which would let two conformant implementations disagree on the same record — breaking the single-verdict property the whole standard depends on. The cross-language conformance corpus pins a low-order test vector exactly to catch a verifier that has slipped into lax mode. See Signatures.

Never log secret material

Seed bytes, derived private keys, key-encryption keys, content-encryption keys, passphrase-derived material, and decrypted plaintext MUST NOT reach logs at any level. Path-based redaction in a logger config is necessary but not sufficient on its own: a value nested deeper than the redaction glob reaches can slip through, so implementations MUST also treat every byte-typed value (Uint8Array, bytes, and the like) as opaque-redacted unconditionally, even for protocol-public values like a nonce. The cheapest secret to protect is the one that never enters a log line.

Sealed-PoE construction guarantees

Three further properties are specific to the sealed PoE construction. They are security-normative for any implementation that produces or opens a sealed record.

The slot-set MAC is a ~128-bit commitment

The slot-set MAC is what makes a recovered content key a commitment to the slot set the recipient matched: a malicious sender cannot construct two distinct slot sets that a single recipient accepts as "theirs". The property required is restricted key commitment for the envelope content key, in the sense of RFC 9771 — the recovered key binds to a single slot transcript — not a full committing AEAD over arbitrary inputs. The recipient derives a key from a slot, then requires that same key to reproduce slots_mac over the slots-transcript hash before treating the slot as its own. Concretely the commitment rests on the multi-key collision resistance of

CEK ↦ HMAC-SHA-256( HKDF-SHA-256(CEK, info="cardano-poe-slots-mac-v1"), slots_hash )

for adversarially chosen CEKs and transcripts. Because the adversary controls both the CEK and the transcript, the relevant bound is the generic-collision (birthday) bound on the 256-bit HMAC output: finding two (CEK, transcript) pairs that yield the same slots_mac is a ~128-bit search, not the 256-bit second-preimage level. A 128-bit generic-collision margin is the security level this commitment relies on, and is adequate for the threat model.

Pre-hashing the transcript to a 32-byte slots_hash before the HMAC does not weaken this: a slots_mac collision implies either a slots_hash collision (an SHA-256 collision) or a collision of the keyed HMAC over equal slots_hash, both at the ~128-bit level. Tamper-evidence of the transcript itself inherits SHA-256's 2^128 collision bound: any change to a committed header field or slot byte alters slots_hash, and forging an unchanged slots_hash over a different transcript is exactly that ~128-bit collision search. A direct consequence is that the per-slot wrap need not be a committing AEAD: the commitment is supplied by slots_mac, not by the wrap, so a non-committing wrap (the default ChaCha20-Poly1305) is sound here.

Zero-nonce wrap safety rests on per-slot key uniqueness

Each slot wraps the content key under a per-slot key-encryption key with ChaCha20-Poly1305 at a zero nonce. A zero nonce is safe only because the key-encryption key is per-slot and used for exactly one wrap: every slot draws fresh KEM randomness, so the wrap key is unique per slot at negligible collision probability. The safety condition is exactly per-slot key uniqueness, and a producer MUST NOT introduce anything that could repeat a (key, nonce) pair — a CSPRNG failure that repeats KEM randomness, deterministic or colliding encapsulation, reuse of a slot across records, caching a derived key by (recipient, ephemeral), or deduplicating two appearances of the same recipient into one slot.

This splits cleanly into a verifier-checkable slice and a producer-only obligation:

  • Within-record duplicates are verifier-rejected. A verifier MUST require that the encapsulation material be distinct within a single slots[] — all epk values distinct on the x25519 path, all kem_ct values distinct on the mlkem768x25519 path. A duplicate is rejected before any KEM or AEAD primitive runs, with the typed code ENC_SLOTS_DUPLICATE_KEM_MATERIAL. The conformance corpus exercises this with a record carrying two slots that share an epk (or kem_ct) and requires that rejection. This rejection fires on a repeated epk / kem_ct only; it does not forbid sealing to the same recipient twice, since honest duplication still draws fresh per-slot KEM randomness for each appearance (see the multiple-matching-slots rule below).
  • Cross-record and cross-key reuse is a producer obligation. No verifier can detect that a producer reused KEM material across two different records or recipients; that lies outside any single record's bytes and remains a producer responsibility. Were a future revision to permit key reuse, the zero nonce would have to be replaced with a fresh nonce in the same change.

Multiple matching slots: padding is allowed, a CEK conflict is not

A recipient's private key MAY legitimately match more than one slot. Sealing the same content key to the same recipient in several slots — each with fresh per-slot ephemerals — is a valid padding and privacy technique, raising the visible slot count without revealing that two of the slots share a recipient. It is distinct from the within-record duplicate-encapsulation rejection above: honest duplication draws fresh KEM randomness, so its epk / kem_ct differ and it never trips ENC_SLOTS_DUPLICATE_KEM_MATERIAL. A verifier therefore MUST select the first matching slot's content key and MUST NOT reject merely because more than one slot matched.

The one anomaly a verifier MUST reject is two matching slots that recover different content keys, compared in constant time. The trial-decrypt loop carries a cek_conflict bit across all slots and surfaces the single generic failure if any later match recovers a key that differs from the one already selected. This is defence-in-depth: under the commitment property above a distinct-key match is already infeasible — it is exactly the multi-key collision that property rules out — so the check fails closed against a broken implementation or a future weakening of the assumption. The distinct-key negative is not constructible as a byte vector (constructing one would be the commitment collision itself), so the property is pinned by implementation-level behavioural tests rather than a fixture, alongside a positive fixture for legitimate recipient duplication.

Parser resource bounds before any primitive

A verifier MUST bound parser resource use before invoking any KEM or AEAD primitive, so an oversized envelope cannot impose work before it is rejected. The reference bounds are MAX_SLOTS = 1024 slots and 65536 bytes for the decoded enc envelope — both far above the ~16 KiB Cardano transaction-metadata ceiling that constrains any honest record, so a record exceeding either bound is malformed. An over-count is rejected with ENC_SLOTS_TOO_MANY, an over-size envelope with ENC_ENVELOPE_TOO_LARGE. These are verifier-enforced, deployment-pinned constants — not wire fields — and a deployment MAY tighten them. The analogous guard on the passphrase path is a maximum raw passphrase input length enforced before normalization and Argon2id (reference bound 4096 UTF-8 bytes, MAX_PASSPHRASE_INPUT_BYTES), so a pathological passphrase cannot drive a pre-KDF denial of service.

X-Wing public-key validity bounds the hybrid floor

The hybrid mlkem768x25519 KEM never drops below X25519's classical security as a floor — but that floor is scoped to validly generated recipient keys. XWing.Encapsulate MUST apply the pinned X-Wing revision's public-key validity check to the recipient key and refuse to encapsulate to a key that fails it, rather than treating a malformed key as if it carried the classical guarantee. A producer that skips the check forfeits the floor for that recipient; the check is the precondition under which the floor holds.

Bounded payload size

The content layer is a segmented STREAM: the plaintext is split into 65536-byte chunks, each sealed under the content key with a distinct counter nonce. The 88-bit per-chunk counter admits 2^88 chunks and each chunk stays well within the RFC 8439 single-invocation limit, so — unlike a single-shot AEAD over the whole plaintext — there is no counter-overflow keystream collision to guard against and the format imposes no cryptographic payload ceiling. The maximum a deployment enforces is therefore a denial-of-service policy, not a cryptographic bound: producers and verifiers SHOULD cap payload size to fit their resources and enforce it incrementally as the stream is written or read, aborting before an oversized payload is buffered. Truncation is caught structurally by the per-chunk final flag — a missing final chunk, trailing data, or a short non-final chunk fails decryption — rather than by a size check. The posture is identical on the slots path and the passphrase path.

Algorithm agility as a security property

Every cryptographic choice in a Label 309 record is named by a string from an extensible registry — the hash, the AEAD, the KEM, the KDF, the signature scheme. This is not a stylistic choice; it is the migration substrate that lets the format outlive any single algorithm.

If a primitive is weakened by cryptanalysis:

  1. The relevant registry gains a successor identifier. Nothing in the existing format changes — the new name is additive.
  2. Existing records stay valid. A verifier continues to recognise the old identifier for historical records; their block-time claims do not evaporate because a newer algorithm exists. An implementation MAY surface a record that relies only on a since-weakened algorithm as lower-assurance, but MUST NOT erase or reject it on that basis alone.
  3. New records publish under the successor. A producer who wants defence in depth MAY commit a content hash under two algorithms from distinct design families, so a single-family break does not collapse the record's evidentiary value — though single-algorithm records remain fully conformant.

Because identifiers are strings and the registries are open, migrating to post-quantum hashes, KEMs, or signatures is an additive registry change, never a breaking format revision. The hybrid post-quantum KEM already in the registry is an instance of this property in action, not a special case bolted on.

Known limits of the standard

These are not defects to be fixed in a later revision; they are inherent properties of anchoring proofs on a permanent public ledger and of public-key encryption to long-term keys. An honest security model names them.

On-chain permanence

Cardano metadata is permanent and globally replicated. A published Label 309 record cannot be deleted, redacted, or invalidated — that durability is the entire point of proof of existence. The consequence is a publishing-time responsibility: a content hash, a stake-linked signature, or anything else committed under label 309 is on chain forever. Label 309 deliberately omits free-form descriptive metadata from the record to keep the on-chain footprint minimal, but a producer MUST understand that what they publish is irrevocable before they publish it.

Recipient count is visible

A sealed PoE never names its recipients, but the number of recipient slots is the array length, plainly visible on chain — as is the KEM family (enc.kem) and the sealed-versus-open distinction. Recipient public keys are off the wire and slots are shuffled, but count-hiding padding is not part of the baseline. A sender for whom the recipient count is itself sensitive must account for this metadata leak operationally — for instance by padding the slot set themselves, or by publishing separate records.

No per-recipient revocation

There is no on-chain primitive to revoke a single recipient's access to an already-published sealed PoE. Once a record is encrypted to a public key and anchored, that binding is permanent. This is an inherent property of public-key encryption to a long-term key, not a gap in the format. A key believed compromised is handled going forward — by addressing new records to a fresh key, optionally publishing a superseding record as a caveat-emptor signal — never by reaching back to alter what is already on chain.

No recipient forward secrecy, by design

The holder of a recipient private key can decrypt every sealed PoE ever addressed to it, forever. The off-chain ciphertext is permanent and there is no revocation primitive, so a sealed record is private from the world but not from its recipient. This is the expected behaviour of encrypting to a long-term public key. A sender who needs to limit a recipient's future reach must encrypt to a short-lived recipient key rather than a long-term identity key — a sender-side choice the format permits but does not mandate.

Trial-decrypt failures leak no key material

A recipient discovers their slot by trial-decryption. Internally — for a trusted local caller's diagnostics only — the failure paths carry distinct typed codes: "no slot opened under my key" (WRONG_RECIPIENT_KEY), "a slot opened but no candidate key reproduced slots_mac" (TAMPERED_HEADER), and "the content AEAD failed after a key was recovered and the MAC verified" (TAMPERED_CIPHERTEXT). These codes carry no oracle value: an adversary without a matching private key opens no slot at all, and every error path MUST exclude the underlying secret bytes (shared secret, key-encryption key, content-encryption key) from its message and cause chain.

An untrusted external caller, however, MUST receive exactly one generic failure shape regardless of why decryption failed, and the external response MUST NOT distinguish the three causes by shape, nor reveal which slot matched. The internal codes exist for local debugging; they MUST NOT leak to an outside observer through a distinguishable response.

The timing model is deliberately scoped. A verifier MAY return at the no-match check, before content decryption — so a non-recipient (no slot opened) may be timed apart from a recipient whose slot opened but whose content ciphertext fails to open. That distinction reveals only recipient-vs-non-recipient; it never reveals which slot matched, nor any key material. Uniform timing between the non-recipient case and the bad-ciphertext case is NOT required, and a dummy content open MUST NOT be mandated — forcing content-decryption cost on every non-recipient would buy nothing. The constant-time guarantee that does hold is the across-slots invariant (see Constant-time comparison of secrets): within a single key's pass the loop runs over all slots with constant-time compares, so an observer cannot learn which slot, if any, a given key unwrapped — and beyond the recipient-vs-non distinction, learns only the slot count.

  • Introduction — the five invariants, including issuer-agnostic and standalone-verifiable.
  • Signatures — strict Ed25519 verification and the fail-soft authorship model.
  • Sealed PoE — the encryption envelope and the privacy properties summarised here.
  • Algorithm registries — the named identifiers that make algorithm agility possible.
  • Verification — the pipeline, confirmation-depth policy, and error catalogue.