492 lines
30 KiB
Markdown
492 lines
30 KiB
Markdown
# 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*.
|