open-standards/wai/extensions/pq-signatures.md
Transaction Science 4615c9bd1b pq-signatures: §1 names the keys a hybrid entry lists
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 05:07:44 -04:00

492 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WAI Extension: Hybrid Signatures (Ed25519 with ML-DSA-65)
> Status: Draft. Sub-spec of WAI v1.0. 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](jwp-receipts.md) §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/`](../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](confidentiality.md)'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](jwp-receipts.md) §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](jwp-receipts.md) §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](synthesis.md) §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](synthesis.md) 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](jwp-receipts.md) §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](jwp-receipts.md) §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](jwp-receipts.md) §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](synthesis.md)'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](transparency.md) §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](moq-streaming-format.md) §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](jwp-receipts.md), SPEC §7 *Signatures*.