open-standards/wai/extensions/pq-signatures.md
Transaction Science 9d273c286a
Some checks failed
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 28s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 32s
WAI operation catalogue / does this change touch it (push) Successful in 25s
wai-fleet service / does this change touch it (push) Successful in 27s
wai-app and Create / wai-app · does this change touch it (push) Successful in 29s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 30s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 28s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m51s
wai-fleet service / service · macos (push) Successful in 1m9s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 5m47s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 12m44s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 8s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 48s
wai-fleet service / service · linux (push) Successful in 2m39s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 6m0s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 52s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 3m31s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 9m13s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (push) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (push) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (push) Has been cancelled
One rule for versions: 1.0 is the WAI1 envelope's wire version, and the standard is a Draft
SPEC states the rule after its status line and describes the manifest's wai member as the
envelope's wire version; its title, the four extension headers, the README and two code
comments no longer call the standard itself v1.0. Text that names an envelope by its wire
version is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 18:28:23 -05:00

30 KiB
Raw Permalink Blame History

WAI Extension: Hybrid Signatures (Ed25519 with ML-DSA-65)

Status: Draft. Sub-spec of WAI. Gives WAI's signed records a second, post-quantum signature beside their Ed25519 one, so a record stays verifiable while either algorithm holds: one suite, ML-DSA-65 (FIPS 204) beside Ed25519 (RFC 8032), combined as the composite id-MLDSA65-Ed25519-SHA512 of draft-ietf-lamps-pq-composite-sigs-19. It covers the key logs of jwp-receipts §7 (an entry's post-quantum key, a hybrid root, an anchor that requires hybrid), group receipts (the third version of the labelled payload of jwp-receipts §2.3), claims (jwp-receipts §2.4.3), and every JSON record an extension signs as wai:<domain>\x01 ‖ JCS(record) (§3.4), through one shared check. It adds a verifier policy that refuses a record signed classically only, and a confirmation step that refuses a classical-only record signed by a key its origin lists as hybrid. Nothing changes for a publisher that signs classically: its records keep their bytes and verify as before. No new transport. The reference implementation is the pq_sig module of wai-rs (feature pq_signatures), with the readers of jwp_delivery, claim_receipt, key_log and synthesis; the corpus is pq-conformance/, and its second implementation is pq-conformance/pq_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.

Texts read for this revision, each pinned to the version named: FIPS 204, Module-Lattice-Based Digital Signature Standard (August 2024), whose PDF's SHA-256 as fetched on 22 October 2026 was 57239b9f84c03227eda3ca0991204dc7764c79af9ce2e6824eda774918d46b6b; draft-ietf-lamps-pq-composite-sigs-19 (the revision of 20 October 2026), whose text's SHA-256 as fetched that day was b4ee04416efc26e7d8de7c740dc3848ffe8b8cc9dcd035ad9d4e33e18844ec83; RFC 8032; and the ACVP test vectors for FIPS 204's ML-DSA at the upstream commit 975de31eb83d87039ec88934fdc47d8c312b892d (§10.1). A section number of one of those texts in this extension is that version's.


1. Scope

Every signature WAI defines is Ed25519 (SPEC §7 Signatures). A quantum computer large enough to compute discrete logarithms on Ed25519's curve recovers a private key from its public key, and its holder then signs anything under it: on the day such a computer exists, a record signed with Ed25519 alone no longer shows who signed it. Media provenance and delivery receipts are archival — a receipt signed today is asked to prove something years later — so the signature they carry has to outlast that day.

A hybrid signature is two signatures over the same bytes, one classical and one post-quantum, and a record verifies only if both do. While Ed25519 holds, the record is as strong as it was; once it does not, the record stands on the post-quantum half, provided that half's key is bound to the signer by something a forger cannot rewrite either (§6.1, §7).

In scope:

  • the suite (§2) and the members that carry its post-quantum half (§3);
  • signing and verifying, and why the classical half signs what it signs (§4);
  • the verifier policy that refuses a classical-only record (§5);
  • where hybrid signatures go: key logs, group receipts, claims, and the records of the wai:<domain>\x01 ‖ JCS pattern (§6);
  • sizes and costs, measured (§9); the corpus and the vendored vectors (§10).

Not in scope, in this revision:

  • the binary records of the live family (live events, rooms, takes and others), OER/2 receipts, the receipts of the quantum family, and §7.9's module grants and key statuses: each keeps Ed25519 alone until its own extension gives it a hybrid form. A key an origin's log lists in a hybrid entry signs none of them (§6.1, step 4a);
  • hash functions: BLAKE3-256 and SHA-256 are unchanged. Generic quantum search lowers a 256-bit hash's preimage resistance to about 2^128 operations, and nothing here depends on less;
  • confidentiality: key agreement for encrypted rooms is confidentiality's;
  • a transport.

2. The suite

One suite:

member value
pq_suite id-MLDSA65-Ed25519-SHA512 (draft-ietf-lamps-pq-composite-sigs-19 §6, OID 1.3.6.1.5.5.7.6.48)
post-quantum half ML-DSA-65 (FIPS 204 §4): a 1952-byte public key (pkEncode, Algorithm 22), a 3309-byte signature (sigEncode, Algorithm 26)
classical half Ed25519 (RFC 8032), the key a record's signer_pubkey already carries, verified by SPEC §7 Signatures
label COMPSIG-MLDSA65-Ed25519-SHA512, in ASCII (draft §6)
prefix CompositeAlgorithmSignatures2025, in ASCII (draft §2.2)
pre-hash SHA-512 (FIPS 180-4)

Why ML-DSA-65. The post-quantum half is the half that must still hold when the classical one does not, so its parameters are chosen for the life of the record, not for the life of the key. FIPS 204 §4 claims security category 3 for ML-DSA-65 and category 2 for ML-DSA-44; for records asked to prove something for decades, the margin of category 3 against advances in the cryptanalysis of module lattices is worth the 889 bytes per signature and 640 bytes per key it costs (FIPS 204 §4, Table 2; §9 measures what that is per record). ML-DSA-87 (category 5) adds 1318 bytes per signature more. The draft's §10.4 recommends this composite where the signature must be strongly unforgeable, and WAI's link hashes cover signature bytes (jwp-receipts §3 step 3, §2.4.3), so a second valid signature of the same bytes would be a second receipt (§7).

Why the composite. The draft's construction is one WAI can adopt as it stands: it prefixes and pre-hashes the message once for both halves, binds both to the suite by the label, and has a published analysis of what it gives when one half fails (draft §9.2). A WAI hybrid record carries the composite's two component signatures in two members, sig and pq_sig; pq_sig ‖ sig is the draft's composite signature value and pq_key ‖ signer_pubkey its composite public key (draft §4), so a composite verifier can check a WAI record given the bytes it signs. A simpler form — two signatures over the same domain-separated bytes — would also bind both halves where the record's own members include the post-quantum key, as every JSON record's do (§3.2); the composite adds that the classical half is over bytes no classical record of any family signs (§4.3), which holds for the binary group receipt too, whose members are not a JSON object.

Keys. A hybrid key is an Ed25519 key and an ML-DSA-65 key generated apart, each from its own 32-byte seed (RFC 8032 §5.1.5; FIPS 204 Algorithm 6, ML-DSA.KeyGen_internal(ξ)), and used only together (draft §3.1, §9.3): its Ed25519 key never signs an item with a classical signature only, and its ML-DSA key never signs outside this suite. §6.1 makes the first of these checkable.


3. The hybrid members

3.1 In a JSON record

A hybrid JSON record carries three members beside its signer_pubkey and sig:

member type meaning
pq_suite string The suite (§2): id-MLDSA65-Ed25519-SHA512.
pq_key 1952 B hex The signer's ML-DSA-65 public key. In a key-log revision, whose signer is its root, the member is root_pq_key (§6.1).
pq_sig 3309 B hex The ML-DSA-65 signature (§4).

Byte strings are lowercase hex, as every byte string of jwp-receipts §2.4.3. A record carries all three or none: with none it is classical and is read and verified as its family defines; with all three it is hybrid. A reader checks them in this order, and refuses the record at its family's member step (§6) with the reason given:

  1. one or two of the three present: pq_malformed;
  2. pq_suite not a string: pq_malformed; a string naming no suite of §2: pq_suite_unknown;
  3. pq_key, then pq_sig, not lowercase hex of exactly the suite's length: pq_malformed (FIPS 204 §3.6.2: a key or signature of another length is refused, never read).

A reader built without this extension does not know the members, and refuses a hybrid record as its family refuses any member it does not define (malformed, unknown_member, key_log_malformed, record_malformed), so a hybrid record is never read as a classical one.

3.2 What is signed

The message M of a hybrid record is the bytes its family's rule signs, computed with pq_suite and the key member among the record's members and with sig and pq_sig left out: for a claim, "moq-jwp:claim\x01" ‖ JCS(claim without "sig" and "pq_sig"); for a key-log revision, "wai:key-set\x01" ‖ JCS(revision without "sig" and "pq_sig"); for a record of the pattern of §3.4, domain ‖ JCS(record without "sig" and "pq_sig"). JCS is jwp-receipts §2.4.3's canonical form. A record's link hash and identity are unchanged in definition: they are over the whole record, both signatures and all three hybrid members included.

3.3 In a binary record

The group receipt's binary form carries the post-quantum key inside its signing payload and the post-quantum signature after sig, under the payload's third version, whose signing domain names the suite (§6.2). It has no pq_suite member: moq-jwp:group-sign\x03 is this suite, and a later suite takes a later version.

3.4 Records of the pattern wai:<domain>\x01 ‖ JCS

Many extensions sign their JSON records the same way: a record carries signer_pubkey and sig, and sig is Ed25519 over domain ‖ JCS(record without "sig"), domain being the extension's own wai:<name>\x01 (synthesis §2 is one). Any such record MAY carry the three members of §3.1 beside sig, and then is checked by one shared check, the same for every such extension:

  1. the hybrid members, as §3.1 reads them;
  2. signer_pubkey and sig present as 32 and 64 bytes of lowercase hex, or the record is the extension's own malformed record;
  3. §4.2 over M = domain ‖ JCS(record without "sig" and "pq_sig"), under the verifier's policy (§5): a key that fails is the extension's own bad-key reason, an Ed25519 half that fails its own bad-signature reason, and otherwise this extension's reasons (§8).

The extension's own member tables then apply to the record without its hybrid members, and its own checks to what it states. A classical record's message is the bytes its extension signs today, so a classical record reads exactly as before. An extension's verifier implements this section by calling the shared check where it checks a signature; the reference does so for synthesis records. A verifier of an extension that does not implement it refuses a hybrid record as malformed (§3.1).


4. Signing and verifying

4.1 Signing

Given M (§3.2), the signer computes the composite's message representative with an empty application context (draft §2.2, §3.2 step 2):

M'     = "CompositeAlgorithmSignatures2025" ‖ "COMPSIG-MLDSA65-Ed25519-SHA512" ‖ 0x00 ‖ SHA-512(M)
sig    = Ed25519(ed_private_key, M')                                   RFC 8032, 64 bytes
pq_sig = ML-DSA.Sign(ml_dsa_private_key, M', ctx = "COMPSIG-MLDSA65-Ed25519-SHA512")
                                                                       FIPS 204 Algorithm 2, 3309 bytes

The ML-DSA half takes the label as its context string (draft §3.2 step 4), so FIPS 204's own domain separation (Algorithm 2 line 10) binds it to the suite. A signer MAY sign in FIPS 204's hedged or deterministic variant (§3.4); a verifier cannot tell them apart. The corpus signs deterministically, so it regenerates byte for byte; a signer on a platform exposed to fault or side-channel attacks SHOULD sign hedged (FIPS 204 §3.4, §3.6.1).

4.2 Verifying

A verifier checks an item's signature in this order and reports the first that fails:

  1. signer_pubkey is an Ed25519 key SPEC §7 Signatures steps 1 and 2 accept: otherwise the family's bad-key reason (bad_signer_key, bad_key, key_log_signature_invalid);
  2. under the policy that requires hybrid (§5), an item with no post-quantum half: hybrid_required, before its signature is examined;
  3. the Ed25519 half under SPEC §7 Signatures steps 3 to 5, over M' for a hybrid item and over M for a classical one: otherwise the family's signature reason (signature_verify_failed, bad_signature, key_log_signature_invalid);
  4. the post-quantum half: the key and the signature of the suite's lengths (pq_malformed), and ML-DSA.Verify(pq_key, M', pq_sig, ctx = label) (FIPS 204 Algorithm 3) true: pq_signature_invalid otherwise, a signature whose hint does not decode or whose response is out of range (Algorithm 27, Algorithm 8 line 13) among them.

The classical half is verified by WAI's strict rule, which refuses some signatures RFC 8032's permissive reading accepts and no honest signer makes; the composite's verification (draft §3.3) requires each component to verify under its own specification, which this is.

4.3 What stripping does

The Ed25519 half of a hybrid record signs M', never M. Remove the post-quantum half from a hybrid record, and its sig verifies over no classical record's bytes: M' begins with the composite's prefix and holds a digest, not a family's domain and members (draft §9.2.3, weak non-separability). Remove only the members of §3.1 from a JSON record, and its M changes too, since pq_suite and the key member are signed. Either way the stripped record is refused at its signature step; neither way can a party that cannot forge Ed25519 turn a hybrid record into a classical one.

A party that can forge Ed25519 does not need to strip anything: it makes a classical record outright. Against it, what holds is policy, not construction: §5 for a relying party that requires hybrid of every record, and §6.1 for any record signed by a key its origin lists as hybrid.


5. The policy

A verifier that implements this extension MUST offer the policy require hybrid: an item with a classical signature only is refused (hybrid_required) at its family's signature step, after its key is checked and before its signature is examined. A relying party that relies on records for longer than it trusts Ed25519 SHOULD apply it to them. Without it, a verifier accepts classical and hybrid items alike, and verifies both halves of a hybrid one.

The policy is the relying party's, and is not carried in any record. Its counterpart in a key log is an anchor that requires hybrid (§6.1).


6. Where hybrid signatures go

6.1 Key logs (jwp-receipts §7)

A hybrid entry. A key entry of jwp-receipts §7.2 MAY carry pq_suite and pq_key (§3.1), together: the entry is then hybrid, and the origin states that every item its key signs is hybrid with that ML-DSA key. Rule 1 adds that no two entries share a pq_key, none is the revision's root_pq_key, and no entry's pair is the one next_root commits to (below). Rule 6 adds that an entry's form never changes: an entry listed hybrid keeps its pq_suite and pq_key, and one listed classical never gains them (key_log_rewrites_history). An origin that moves a key to hybrid lists a successor, as for any change of authority (jwp-receipts §7.6).

A hybrid root. A revision MAY carry pq_suite, root_pq_key and pq_sig (§3.1): its root is then hybrid, and both halves sign it, §4 over the M of §3.2. Rule 3 requires both to verify (key_log_signature_invalid). A hybrid root's ML-DSA key is part of the root: rule 5's "its root and next_root are its parent's" holds only if root_pq_key is the parent's too, present or absent alike.

The hybrid commitment. next_root commits to a classical root as jwp-receipts §7.2 defines, or to a hybrid one as

commitment_hybrid(K, P) = BLAKE3("wai:key-set-next\x02" ‖ K ‖ P)

over its Ed25519 key K and its ML-DSA key P. Rule 1 adds that next_root is not the commitment to the revision's own root in either form. A rotation (rule 5) is a child whose root is not its parent's and whose commitment, in the child's form, is the parent's next_root: a root becomes hybrid only by a rotation its parent committed to in the hybrid form, and a hybrid root's non-rotating children are signed by both its halves.

A classical root is no stronger than Ed25519: whoever can forge it signs revisions that add keys, hybrid ones among them, and a hybrid entry under such a root binds its ML-DSA key to the origin only as firmly as Ed25519 does. Hence:

An anchor that requires hybrid. A relying party that pins an origin (jwp-receipts §7.3) MAY record that it requires hybrid of it. Then:

  • only a hybrid-signed revision descends from the anchor: at §7.4 step 3, a revision without the hybrid members is no child of anything, as a revision signed by a key its parent does not authorise is not. The pinned revision itself may be classical: it is trusted by provisioning, and its hybrid successor arrives by a rotation it committed to;
  • the anchor moves only by pinning: a retrieval that holds it leaves it where it is, and the head walk follows its hybrid-signed descendants;
  • an item with a classical signature only is unconfirmed (hybrid_required) at step 4a.

Confirmation step 4a. After jwp-receipts §7.4 step 4 has found the entry and matched its key, in order:

  • the entry is hybrid and the item carries a classical signature only: refused, hybrid_downgrade. The log contradicts the item's claim to speak for the key: the origin states the key signs nothing classically, so the item is a forgery or the key was misused;
  • the item is hybrid and its ML-DSA key is not the entry's, or the entry is classical: refused, pq_key_mismatch;
  • the anchor requires hybrid and the item carries a classical signature only: unconfirmed, hybrid_required. Nothing contradicts the item; the verifier lacks what its anchor requires.

A key status and a module grant (jwp-receipts §7.9) have no hybrid form in this revision: a status is signed by the root's Ed25519 key alone, and a grant by a key that §6.1 refuses if its entry is hybrid. An origin lists classical keys for grants until §7.9 gives them a hybrid form; §7.9's freshness bound limits how long a status matters.

6.2 Group receipts (jwp-receipts §2.3, §2.4.1, §2.7)

The payload's third version. A hybrid group is signed over

signing_payload_v3 = "moq-jwp:group-sign\x03"
                  || the labelled payload's fields after its domain, in its order   // §2.3
                  || pq_key[1952]                                                   // the publisher's ML-DSA-65 key

by both halves (§4, M = the payload). A hybrid group is a labelled group: its class, coverage and parent link are the labelled payload's, under every rule of jwp-receipts §2.4.1. Its JSON form is the labelled group's members with sign_version 3 and two more, pq_key and pq_sig. Read at jwp-receipts §3 step 1: sign_version 3 with neither of them is a version jwp-receipts does not define (group_label_invalid); with one of them, or either of them in a group whose sign_version is not 3, or either not lowercase hex of the suite's length, pq_malformed. Verified at step 2 by §4.2.

Canonical bytes and link hash. canonical_bytes(G) is the payload, then sig, then pq_sig, so a hybrid group's link hash (jwp-receipts §3 step 3) covers both signatures. The binary form (jwp-receipts §2.7.1) of a hybrid group is its canonical bytes, read in that order: the fields of the labelled form, the 1952-byte key, the 64-byte sig, the 3309-byte pq_sig. A reader of jwp-receipts §2.7.4 that predates this extension finds a signing domain that is neither of §2.3's and refuses the record (malformed).

Objects. Object receipts are unchanged: they are bound to a hybrid group through its root as to any other (§2.2, §3 step 5), so the post-quantum cost is paid once per group, not per object.

Confirmation. A hybrid group is an item whose ML-DSA key is its pq_key (step 4a above).

6.3 Claims (jwp-receipts §2.4.3, §3.1)

A claim MAY carry the three members of §3.1; a claim has no other optional members. At §3.1 step 1, after the subject type is admitted, a verifier reads them as §3.1 says (pq_malformed, pq_suite_unknown), and the envelope's member check is of the claim without them. At step 3 it verifies by §4.2: bad_signer_key, hybrid_required, signature_verify_failed, pq_signature_invalid. The claim's link_hash is over the whole claim. A claim chain (§2.4.3) is per signer_pubkey, hybrid or not.

6.4 Records of the pattern

As §3.4 says. The reference wires the shared check into synthesis's records; the other extensions that sign as wai:<domain>\x01 ‖ JCS adopt it by the same call.


7. Security considerations

  • What a hybrid signature gives. Under the composite, a classical adversary cannot forge a record if either half is unforgeable, and a quantum adversary cannot if ML-DSA is (draft §9.2.1). It does not say who holds the ML-DSA key. A forger who can forge Ed25519 generates an ML-DSA key of its own and signs a hybrid record with it; only the key log tells the two apart (§6.1, step 4a pq_key_mismatch), and only a key log whose revisions from the anchor on are hybrid-signed tells them apart once Ed25519 is forged. A verifier that must check a record years from now keeps the revisions it confirmed it against (jwp-receipts §7.3) and pins an anchor that requires hybrid.
  • Records signed before. A record signed classically stays classical. Nothing here makes an old record hybrid, and once Ed25519 can be forged, a classical record proves nothing about its signer. An origin that wants its history to outlast that day registers its receipts with a transparency service while Ed25519 still holds (transparency §3), and re-signs what it still vouches for.
  • Strong unforgeability and link hashes. WAI names receipts by link hashes over their signature bytes. Ed25519 under SPEC §7's strict rule has one signature per key and message; ML-DSA is designed to be strongly unforgeable (FIPS 204 §3.1), so no one but the signer can make a second valid ML-DSA half. A signer that signs hedged can make two hybrid records of the same members with different link hashes; that is the signer's own fork, which jwp-receipts §3 step 3 and §3.1 step 5 refuse as for any other.
  • Key reuse. A hybrid key's Ed25519 half signing a classical item is the stripping attack draft §9.3 warns of; step 4a refuses it for every item confirmed under the key's origin. A key status (§6.1) is the one classical use left to a hybrid root, under its own signing domain.
  • The implementation. The reference's ML-DSA implementation is a maintained, permissively licensed (Apache-2.0 OR MIT) pure-Rust one that has had no independent audit. Before anything is built on it, it is held to FIPS 204's published ACVP vectors and the composite to the draft's own vector (§10.1), and the corpus's second implementation verifies ML-DSA from FIPS 204's text.
  • Side channels. Verification handles no secret. Signing does; see §4.1 on the hedged variant.

8. Reasons

The codes this extension adds. A family reports them at the step §6 names, beside its own codes, which keep their meaning: a hybrid item whose Ed25519 key or half fails is refused with the family's own reason; a key-log revision that breaks §6.1's additions to rule 1 is key_log_malformed, to rule 3 key_log_signature_invalid, to rule 5 key_log_root_not_committed, to rule 6 key_log_rewrites_history.

code where raised when
pq_malformed the family's member step one or two of the hybrid members present; pq_suite not a string; a key or signature not lowercase hex of the suite's length; a group's hybrid members outside its third version, or one of them missing in it
pq_suite_unknown the family's member step pq_suite names no suite of §2
pq_signature_invalid the family's signature step ML-DSA.Verify over M' with the label as context is false (§4.2 step 4)
hybrid_required the family's signature step; confirmation step 4a under the policy of §5, an item with a classical signature only; under an anchor that requires hybrid, the same item at confirmation (unconfirmed)
hybrid_downgrade confirmation step 4a the entry is hybrid and the item carries a classical signature only (refused)
pq_key_mismatch confirmation step 4a the item's ML-DSA key is not the entry's, or the entry is classical (refused)

9. Sizes and costs

Sizes are the corpus's own items (pq-conformance/vectors.json, sizes), the same on every machine. Times are wai_pq_vectors bench, built with --release, on the reference's build host: a 32-core arm64 workstation that other jobs shared during the runs (its load average between 15 and 24). Each time is the median of seven rounds over 64 distinct 512-byte messages, about a claim's signing input — ML-DSA's signing loop repeats a number of times that depends on the message (FIPS 204 §4, Table 1: 5.1 on average for ML-DSA-65), so one message would time one draw of it. The range is that median across three runs.

operation time per operation
Ed25519 sign 10.6–11.5 µs
Ed25519 verify, SPEC §7 strict 34.0–34.9 µs
ML-DSA-65 sign, deterministic 349–378 µs
ML-DSA-65 verify 75–88 µs
hybrid sign, both halves 323–366 µs
hybrid verify, both halves 114–128 µs

A hybrid record costs about thirty times an Ed25519 signature to sign and between three and four times to verify: some eight thousand hybrid verifications a second on one core.

item classical hybrid difference
group receipt, binary (jwp-receipts §2.7.1) 291 B 5552 B +5261 B: the key, 1952, and the signature, 3309
a group's receipts with 3 objects (WJR1, §2.7.3) 722 B 5983 B +5261 B
object receipt, binary 141 B 141 B none
claim, JSON 579 B 11164 B +10585 B: the key and signature in hex, and pq_suite
first key-log revision, JSON: classical root and one classical entry; hybrid root, one hybrid and one classical entry 684 B 15443 B +14759 B: the root's key and signature and the hybrid entry's key, in hex, and one entry more
origin claim of synthesis, JSON 557 B 11142 B +10585 B

A receipts track (moq-streaming-format §4.1) carries one group's receipts per media group. A hybrid publisher's track grows by 5261 bytes a group, whatever the group holds: at one group a second, by 42 kbit/s. Beside a group of one object (376 bytes classical) that is fifteen times the track; of 30 objects (7409 bytes), 1.71 times; of 60 (16439 bytes), 1.32 times. A room's receipts are its tracks' receipts tracks, each a publisher's: the cost is per publishing track and group, never per subscriber, and a subscriber pays it only for the tracks it subscribes to. The second implementation, in pure Python, verifies an ML-DSA-65 signature in about 4 ms on an arm64 laptop (median of 45, Python 3.15).


10. Conformance

10.1 The vendored vectors

pq-conformance/acvp/ holds FIPS 204's ACVP test vectors for ML-DSA-65, derived by extract.py from the internal projections at the upstream commit named above, each upstream file and each derived file pinned by SHA-256 (pq-conformance/README.md): key generation (25 vectors), the deterministic signature generation of the external interface with a context, of the internal interface, and of the internal interface over an external μ (15 each), and signature verification over the same three interfaces (15 each, four of every five modified). The reference reproduces every key and every signature byte for byte and gives every verification its verdict; the second implementation verifies every signature both files state. draft-composite-sigs-19.json is the draft's Appendix E vector for this suite, which both verify, with no context and with the draft's.

10.2 The corpus

pq-conformance/vectors.json, written by wai_pq_vectors gen from seeds that are one byte repeated 32 times (jwp-receipts §7.8: every key in it is published and attests nothing). Each case is a group receipt (JSON or a group's receipts in the binary form, its objects verified by inclusion), a claim, a record of §3.4's pattern, or a key log anchored by pinning; a group or claim may carry a confirmation against a pinned log, with or without the hybrid requirement. The negative controls include: the post-quantum half stripped, made over other bytes, and with a bit flipped; a classical item under the policy of §5, under a hybrid entry, and under an anchor that requires hybrid; a classical revision under such an anchor; a hybrid entry later listed without its key; a child signed by a hybrid root's Ed25519 key alone and with another ML-DSA key; another parameter set, named (id-MLDSA44-Ed25519-SHA512, validly signed) and by length. Every code of §8 is given.

A conforming verifier gives every case its stated verdict. wai_pq_vectors verify regenerates the file byte for byte and checks every verdict; pq_verify.py, written from this text and sharing no code with the reference, checks the vendored vectors, every case, the sizes of §9 and this section's table of codes, and then requires every mutated case to be refused and every one of its own checks, dropped, to be caught by the corpus.


11. References

  • FIPS 204, Module-Lattice-Based Digital Signature Standard, August 2024.
  • FIPS 180-4, Secure Hash Standard.
  • draft-ietf-lamps-pq-composite-sigs-19, Composite ML-DSA for use in X.509 Public Key Infrastructure.
  • RFC 8032, Edwards-Curve Digital Signature Algorithm (EdDSA).
  • RFC 8785, JSON Canonicalization Scheme (JCS).
  • jwp-receipts, SPEC §7 Signatures.