Hybrid signatures: Ed25519 with ML-DSA-65 on key logs, group receipts, claims and records, so receipts stay verifiable after Ed25519 can be forged (PQ1) #435

Closed
dcharlot wants to merge 0 commits from wai/pq-receipts into main
Owner

PQ1: receipts that stay verifiable after Ed25519 can be forged — hybrid Ed25519 + ML-DSA-65

Every WAI receipt, claim, key log and record is signed with Ed25519 alone. A quantum computer large enough to compute discrete logarithms on Ed25519's curve recovers a private key from its public key, and media provenance and delivery receipts are archival. This adds a hybrid signature: ML-DSA-65 (FIPS 204) beside Ed25519, combined as the composite id-MLDSA65-Ed25519-SHA512 of draft-ietf-lamps-pq-composite-sigs-19, so a record verifies only while both halves hold and stands on either.

What is in it

  • wai/extensions/pq-signatures.md (new): the suite (§2), the hybrid members (§3), signing and verifying in WAI's order and what stripping does (§4), the require hybrid policy (§5), where it goes (§6: key logs, group receipts, claims, the wai:<domain>\x01 ‖ JCS pattern), security considerations (§7), reasons (§8), sizes and costs measured (§9), conformance (§10).
  • Amendments: jwp-receipts §2.3, §2.4.1, §2.4.3, §2.7.1, §3 step 2, §3.1 step 3, §3.3, §7.2, §7.4 and §7.7 point to it; synthesis §2 names the hybrid members its reader now takes; SPEC §5 paragraph and SPEC §7 Signatures paragraph; CHANGELOG.
  • wai-rs, feature pq_signatures (one new dependency, the pure-Rust ml-dsa 0.1.1, Apache-2.0 OR MIT, default features off): src/pq_sig.rs (suite, composite, verify, HybridKey, the members, the shared record check, policy, reasons); the readers of jwp_delivery (the payload's third version moq-jwp:group-sign\x03, JSON and binary), claim_receipt (hybrid claims, verify_policy), key_log (hybrid entries and roots, the hybrid commitment, rules 1/3/5/6, an anchor that requires hybrid, confirmation step 4a), synthesis (the shared check wired in). Without the feature nothing changes: the readers refuse the hybrid members as before.
  • Before anything else: pq-conformance/acvp/ vendors FIPS 204's ACVP vectors for ML-DSA-65 (keyGen 25, deterministic sigGen 45 over three interfaces, sigVer 45 with 36 modified), derived by extract.py from the upstream internal projections at a pinned commit, each file pinned by SHA-256; draft-composite-sigs-19.json is the draft's Appendix E vector for this suite. pq_sig::acvp reproduces every key and signature byte for byte and gives every verification its verdict; both halves of the draft's vector verify, with and without its context, and Ed25519 re-signs its classical half byte for byte.
  • Corpus pq-conformance/vectors.json (wai_pq_vectors gen|verify|verdicts|bench): 46 cases from published seeds (one byte repeated 32 times, for Ed25519 and for ML-DSA's ξ), deterministic signing, byte-stable. Group receipts (JSON and WJR1 with objects inclusion-verified), claims, records of the pattern, key logs; confirmations under pinned anchors with and without the hybrid requirement. Negative controls: the PQ half stripped (group JSON and binary, claim, record), made over other bytes, bit-flipped; a classical-only item under require-hybrid (group, claim, record); a classical item signed by a hybrid entry's key (hybrid_downgrade, group and claim); a hybrid item with another ML-DSA key or under a classical entry (pq_key_mismatch); a classical item under a hybrid anchor (hybrid_required, unconfirmed); a classical revision under a hybrid anchor (not followed: key_not_listed); a hybrid entry later listed without its key (key_log_rewrites_history, at anchoring and in the head walk); a revision's bad PQ half; a child signed by a hybrid root's Ed25519 key alone, and with another ML-DSA key (key_log_root_not_committed); wrong parameter set by suite (id-MLDSA44-Ed25519-SHA512, validly signed: pq_suite_unknown) and by length (pq_malformed); sign_version 3 with no hybrid member (group_label_invalid).
  • Second implementation pq-conformance/pq_verify.py (python3 -I): ML-DSA-65 verification written from FIPS 204 in pure Python (_mldsa.py, standard library SHAKE), checks all 90 ACVP vectors and the draft's vector, every case, the sizes and §8's table; then requires 26 mutated cases to be refused and the corpus to catch the verifier with any one of its 14 checks dropped. About 4 ms per ML-DSA verification on an arm64 laptop; the whole run about 10 s. It reuses claim_verify.py (JSON reading, JCS, claim body and class rules, strict Ed25519, BLAKE3) and signer_verify.py (§7's classical rule 1, the signer_id grammar, a claim's authorities), as other verifiers reuse theirs.
  • CI: a step in independent-verifiers (reference tests, pq_verify.py, and a diff of the two implementations' verdicts), the corpus added to the cross-platform regeneration loop (linux, linux-arm64, macos), extensions/pq-signatures.md added to the workflow's paths.
  • Registry: the suite registered as a wire identifier (id-MLDSA65-Ed25519-SHA512, scheme, pq-signatures §2); export regenerated.

Design decisions, and why

  1. ML-DSA-65, not -44. The post-quantum half is the one that must hold when Ed25519 does not, so it is chosen for the record's life. Category 3 (FIPS 204 §4) over category 2, for 889 more bytes per signature and 640 per key. The draft's §10.4 recommends exactly this composite where strong unforgeability matters, and WAI's link hashes cover signature bytes.
  2. The draft's composite, not two signatures over the same bytes. Both halves sign M' = Prefix ‖ Label ‖ 0 ‖ SHA-512(M), the ML-DSA half with the label as its FIPS 204 context; M is the bytes the record's own rule signs (with pq_suite and the key member included, sig/pq_sig excluded). The Ed25519 half of a hybrid record therefore verifies over no classical record's bytes (classical WAI messages begin with wai:/moq-jwp: domains), so stripping the PQ half never yields a valid classical record, and for JSON records the signed key member is a second, independent guard. pq_sig ‖ sig is the draft's composite signature, so a composite verifier can check a WAI record.
  3. The ML-DSA public key travels in the record (pq_key, inside the group payload), so §3/§3.1 verification stays self-contained as it is with signer_pubkey. What binds that key to the signer is the key log (§6.1), not the record: a forger who can forge Ed25519 makes its own ML-DSA key, and step 4a's pq_key_mismatch is what tells them apart.
  4. Key logs must be hybrid too. A classical root can be forged, and a forged revision can add any key. So a root becomes hybrid only by a rotation its parent committed to in the hybrid form (BLAKE3("wai:key-set-next\x02" ‖ K ‖ P)), a hybrid root's ML-DSA key is part of the root identity under rule 5, an entry's form never changes under rule 6, and a pinned anchor that requires hybrid follows only hybrid-signed revisions and moves only by pinning.
  5. Downgrade is policy, not construction. Against a party that can forge Ed25519, non-separability is moot; require hybrid (§5) and step 4a (hybrid_downgrade) are what refuse its classical records. 4a is refused for a hybrid entry with a classical item and for a mismatched/unlisted ML-DSA key; unconfirmed (not refused) for a classical item under a hybrid anchor, which contradicts nothing in the log.
  6. Hybrid groups are labelled groups (version 3 of the labelled payload): they sign the parent link, which a chain kept for decades needs. sign_version: 3 with no hybrid member stays group_label_invalid, so the existing JWP corpus gives every vector its verdict with the feature on or off.
  7. One shared check for the pattern records, wired into synthesis's reader; synthesis's Reason gains Pq(..) as it has Room(..), and its §12 codes are unchanged. Other pattern extensions adopt it by the same call (open item below).
  8. Records of the pattern are not confirmed under §7 (they carry no signer_id); a relying party requires hybrid of them by the policy.

Measured (pq-signatures §9)

Sizes from the corpus's own items: group receipt binary 291 → 5552 B (+5261: key 1952 + signature 3309), once per group, never per object; claim JSON 579 → 11164 B; origin record 557 → 11142 B. Receipts track at one group a second: +42 kbit/s; a group of 1 / 30 / 60 objects grows 15× / 1.71× / 1.32×. Per publishing track and group, never per subscriber.

Times, wai_pq_vectors bench, release, the build host (32-core arm64, shared, load 15–24), median of 7 rounds over 64 distinct 512-byte messages, range across 3 runs: Ed25519 sign 10.6–11.5 µs, verify (strict) 34.0–34.9 µs; ML-DSA-65 sign 349–378 µs, verify 75–88 µs; hybrid sign 323–366 µs, verify 114–128 µs.

Checks run

check command result
ACVP + composite + hybrid unit tests cargo test --no-default-features --features pq_signatures --lib pq_sig:: (rb) 11 passed
synthesis reader incl. hybrid records cargo test --no-default-features --features synthesis,pq_signatures --lib synthesis:: (rb) 18 passed
corpus test cargo test --no-default-features --features pq_signatures --test pq_signatures (rb) 3 passed
corpus regenerates, every verdict wai_pq_vectors verify ../pq-conformance (rb) PASS, 46 cases
Python verifier python3 -I pq-conformance/pq_verify.py pq-conformance (local and rb) all checks pass (90 ACVP, 46 cases, sizes, §8 table, 26 mutants, 14 dropped checks)
Rust/Python differential diff of both verdicts outputs (rb) identical, 46 cases
registry cargo test --no-default-features --lib codecs::registry; wai registry export --check (rb) 14 passed; up to date
CI lib unit tests, CI feature set, and with pq_signatures,synthesis cargo test --lib --no-default-features --features world,…,http[,pq_signatures,synthesis] (rb) 843 passed; 886 passed
integration tests whose corpora build key logs, with the feature on --test capture_take derive_edge energy_steering live_ads ad_presentation live_metering live_record live_room live_watermark wai_keys_cli cli_http staged_moq synthesis (rb, feature union) all passed
every cargo-class corpus, F + pq_signatures the CI loop, 22 corpora (rb) all reproduce
JWP/claims/signers corpora, feature on and off wai_{jwp,claim,signer}_vectors verify (rb) all vectors pass both ways
synthesis and receipt verifiers that read touched texts synthesis_verify.py, synthesis_mark_verify.py, claim_verify.py, jwp_verify.py, signer_verify.py; --test synthesis --test synthesis_mark synthesis 41 vectors PASS, synthesis-mark PASS, --test synthesis 4 passed, --release --test synthesis_mark 5 passed; claims 567, JWP 164, signers 161 vectors, 0 failures
clippy, all targets, feature on and off cargo clippy --no-default-features --features pq_signatures,synthesis --all-targets vs origin/main 108 notes = baseline's 108; none in new code
lockfiles every Cargo.lock holding wai (10) cargo metadata --locked --offline all OK; only wai-rs/Cargo.lock changed: 11 packages added, none removed or re-versioned (existing entries' references to der/pkcs8/signature/spki gained version qualifiers because a second version of each now exists)
C header build with the feature, header regenerated unchanged (lengths are crate-internal consts with public fns, as key_log does)
repo checks check-tests-in-ci.py, check-workflow-concurrency.sh, check-spec-refs.py, check-wai-section-refs.py, check-no-vanity-metrics.sh, test-public-text-checks.sh, check-comparative-prose.sh, check-orphan-sources.py all exit 0
public names check-public-names.sh on 46 changed files incl. corpus data; commit messages against the denylist clean; 0 hits

Falsifications (each committed first, broken on the build host, then restored)

broken what failed
one byte of acvp/ml-dsa-65-sigver.json every_pinned_file_has_its_bytes, acvp_sigver (exit 101)
ML-DSA half not checked in pq_sig::verify every_case_reaches_its_verdict: 5 cases (group/claim/record PQ over other bytes or flipped, log_bad_pq_half) accept (exit 101)
ML-DSA context "" instead of the label (sign and verify) the_composite_test_vector, the non-separation unit test (exit 101)
step 4a without hybrid_downgrade group_downgrade, claim_downgrade fall through to hybrid_required (exit 101)
hybrid group payload without the ML-DSA key wai_pq_vectors verify panics: the generator no longer reaches its verdicts (exit 101)
a stated verdict changed in vectors.json Rust verify FAIL (exit 1); pq_verify.py FAIL (exit 1)
_mldsa.py UseHint turned the wrong way ACVP sigVer fails in Python (exit 1)
each of pq_verify.py's 14 checks dropped the corpus catches each (self-test)
key_log::lists_published_test_key blind to ML-DSA keys a_log_listing_a_published_ml_dsa_key_is_reported (exit 101)

Not done (open items)

  • Other wai:<domain>\x01 ‖ JCS extensions (exploration, settlement, access-services, the live JSON records…) are not wired: each adopts the shared check with the call synthesis now makes (~20 lines); until then their verifiers refuse hybrid records as malformed, which the spec states.
  • Binary live-family records, OER/2, quantum receipts, §7.9 module grants and key statuses keep Ed25519 alone; a hybrid entry's key can sign none of them (step 4a refuses), and a key status is signed by a hybrid root's Ed25519 key alone. Each needs a hybrid form in its own extension.
  • wai-keys CLI cannot yet write hybrid entries or roots (the library can).
  • joule-moq's independent group-receipt reader refuses version-3 groups (as any pre-hybrid reader does).
  • ReceiptChains applies no policy itself; GroupReceipt::verify_policy is the policy entry point.
  • The ML-DSA dependency has had no independent audit (its README says so); it is held to the ACVP vectors and the draft's vector, and the Python verifier is a second implementation of verification only.
  • The corpus signs deterministically for byte stability; §4.1 asks signers exposed to fault/side-channel attacks to sign hedged.

🤖 Generated with Claude Code


Closed: landed on main through merge train C (#461, e0ac15f5); this PR's head is an ancestor of main.

## PQ1: receipts that stay verifiable after Ed25519 can be forged — hybrid Ed25519 + ML-DSA-65 Every WAI receipt, claim, key log and record is signed with Ed25519 alone. A quantum computer large enough to compute discrete logarithms on Ed25519's curve recovers a private key from its public key, and media provenance and delivery receipts are archival. This adds a **hybrid** signature: ML-DSA-65 (FIPS 204) beside Ed25519, combined as the composite `id-MLDSA65-Ed25519-SHA512` of `draft-ietf-lamps-pq-composite-sigs-19`, so a record verifies only while both halves hold and stands on either. ### What is in it - **`wai/extensions/pq-signatures.md`** (new): the suite (§2), the hybrid members (§3), signing and verifying in WAI's order and what stripping does (§4), the `require hybrid` policy (§5), where it goes (§6: key logs, group receipts, claims, the `wai:<domain>\x01 ‖ JCS` pattern), security considerations (§7), reasons (§8), sizes and costs measured (§9), conformance (§10). - **Amendments**: jwp-receipts §2.3, §2.4.1, §2.4.3, §2.7.1, §3 step 2, §3.1 step 3, §3.3, §7.2, §7.4 and §7.7 point to it; synthesis §2 names the hybrid members its reader now takes; SPEC §5 paragraph and SPEC §7 *Signatures* paragraph; CHANGELOG. - **`wai-rs`, feature `pq_signatures`** (one new dependency, the pure-Rust `ml-dsa` 0.1.1, Apache-2.0 OR MIT, default features off): `src/pq_sig.rs` (suite, composite, `verify`, `HybridKey`, the members, the shared record check, policy, reasons); the readers of `jwp_delivery` (the payload's third version `moq-jwp:group-sign\x03`, JSON and binary), `claim_receipt` (hybrid claims, `verify_policy`), `key_log` (hybrid entries and roots, the hybrid commitment, rules 1/3/5/6, an anchor that requires hybrid, confirmation step 4a), `synthesis` (the shared check wired in). Without the feature nothing changes: the readers refuse the hybrid members as before. - **Before anything else**: `pq-conformance/acvp/` vendors FIPS 204's ACVP vectors for ML-DSA-65 (keyGen 25, deterministic sigGen 45 over three interfaces, sigVer 45 with 36 modified), derived by `extract.py` from the upstream internal projections at a pinned commit, each file pinned by SHA-256; `draft-composite-sigs-19.json` is the draft's Appendix E vector for this suite. `pq_sig::acvp` reproduces every key and signature byte for byte and gives every verification its verdict; both halves of the draft's vector verify, with and without its context, and Ed25519 re-signs its classical half byte for byte. - **Corpus** `pq-conformance/vectors.json` (`wai_pq_vectors gen|verify|verdicts|bench`): 46 cases from published seeds (one byte repeated 32 times, for Ed25519 and for ML-DSA's ξ), deterministic signing, byte-stable. Group receipts (JSON and `WJR1` with objects inclusion-verified), claims, records of the pattern, key logs; confirmations under pinned anchors with and without the hybrid requirement. Negative controls: the PQ half stripped (group JSON and binary, claim, record), made over other bytes, bit-flipped; a classical-only item under `require-hybrid` (group, claim, record); a classical item signed by a hybrid entry's key (`hybrid_downgrade`, group and claim); a hybrid item with another ML-DSA key or under a classical entry (`pq_key_mismatch`); a classical item under a hybrid anchor (`hybrid_required`, unconfirmed); a classical revision under a hybrid anchor (not followed: `key_not_listed`); a hybrid entry later listed without its key (`key_log_rewrites_history`, at anchoring and in the head walk); a revision's bad PQ half; a child signed by a hybrid root's Ed25519 key alone, and with another ML-DSA key (`key_log_root_not_committed`); wrong parameter set by suite (`id-MLDSA44-Ed25519-SHA512`, validly signed: `pq_suite_unknown`) and by length (`pq_malformed`); `sign_version` 3 with no hybrid member (`group_label_invalid`). - **Second implementation** `pq-conformance/pq_verify.py` (`python3 -I`): ML-DSA-65 verification written from FIPS 204 in pure Python (`_mldsa.py`, standard library SHAKE), checks all 90 ACVP vectors and the draft's vector, every case, the sizes and §8's table; then requires 26 mutated cases to be refused and the corpus to catch the verifier with any one of its 14 checks dropped. About 4 ms per ML-DSA verification on an arm64 laptop; the whole run about 10 s. It reuses claim_verify.py (JSON reading, JCS, claim body and class rules, strict Ed25519, BLAKE3) and signer_verify.py (§7's classical rule 1, the signer_id grammar, a claim's authorities), as other verifiers reuse theirs. - **CI**: a step in `independent-verifiers` (reference tests, `pq_verify.py`, and a diff of the two implementations' verdicts), the corpus added to the cross-platform regeneration loop (linux, linux-arm64, macos), `extensions/pq-signatures.md` added to the workflow's paths. - **Registry**: the suite registered as a wire identifier (`id-MLDSA65-Ed25519-SHA512`, scheme, pq-signatures §2); export regenerated. ### Design decisions, and why 1. **ML-DSA-65, not -44.** The post-quantum half is the one that must hold when Ed25519 does not, so it is chosen for the record's life. Category 3 (FIPS 204 §4) over category 2, for 889 more bytes per signature and 640 per key. The draft's §10.4 recommends exactly this composite where strong unforgeability matters, and WAI's link hashes cover signature bytes. 2. **The draft's composite, not two signatures over the same bytes.** Both halves sign `M' = Prefix ‖ Label ‖ 0 ‖ SHA-512(M)`, the ML-DSA half with the label as its FIPS 204 context; `M` is the bytes the record's own rule signs (with `pq_suite` and the key member included, `sig`/`pq_sig` excluded). The Ed25519 half of a hybrid record therefore verifies over no classical record's bytes (classical WAI messages begin with `wai:`/`moq-jwp:` domains), so stripping the PQ half never yields a valid classical record, and for JSON records the signed key member is a second, independent guard. `pq_sig ‖ sig` is the draft's composite signature, so a composite verifier can check a WAI record. 3. **The ML-DSA public key travels in the record** (`pq_key`, inside the group payload), so §3/§3.1 verification stays self-contained as it is with `signer_pubkey`. What binds that key to the signer is the key log (§6.1), not the record: a forger who can forge Ed25519 makes its own ML-DSA key, and step 4a's `pq_key_mismatch` is what tells them apart. 4. **Key logs must be hybrid too.** A classical root can be forged, and a forged revision can add any key. So a root becomes hybrid only by a rotation its parent committed to in the hybrid form (`BLAKE3("wai:key-set-next\x02" ‖ K ‖ P)`), a hybrid root's ML-DSA key is part of the root identity under rule 5, an entry's form never changes under rule 6, and a pinned anchor that requires hybrid follows only hybrid-signed revisions and moves only by pinning. 5. **Downgrade is policy, not construction.** Against a party that can forge Ed25519, non-separability is moot; `require hybrid` (§5) and step 4a (`hybrid_downgrade`) are what refuse its classical records. 4a is refused for a hybrid entry with a classical item and for a mismatched/unlisted ML-DSA key; unconfirmed (not refused) for a classical item under a hybrid anchor, which contradicts nothing in the log. 6. **Hybrid groups are labelled groups** (version 3 of the labelled payload): they sign the parent link, which a chain kept for decades needs. `sign_version: 3` with no hybrid member stays `group_label_invalid`, so the existing JWP corpus gives every vector its verdict with the feature on or off. 7. **One shared check for the pattern records**, wired into synthesis's reader; synthesis's `Reason` gains `Pq(..)` as it has `Room(..)`, and its §12 codes are unchanged. Other pattern extensions adopt it by the same call (open item below). 8. **Records of the pattern are not confirmed under §7** (they carry no `signer_id`); a relying party requires hybrid of them by the policy. ### Measured (pq-signatures §9) Sizes from the corpus's own items: group receipt binary 291 → 5552 B (+5261: key 1952 + signature 3309), once per group, never per object; claim JSON 579 → 11164 B; origin record 557 → 11142 B. Receipts track at one group a second: +42 kbit/s; a group of 1 / 30 / 60 objects grows 15× / 1.71× / 1.32×. Per publishing track and group, never per subscriber. Times, `wai_pq_vectors bench`, release, the build host (32-core arm64, shared, load 15–24), median of 7 rounds over 64 distinct 512-byte messages, range across 3 runs: Ed25519 sign 10.6–11.5 µs, verify (strict) 34.0–34.9 µs; ML-DSA-65 sign 349–378 µs, verify 75–88 µs; hybrid sign 323–366 µs, verify 114–128 µs. ### Checks run | check | command | result | |---|---|---| | ACVP + composite + hybrid unit tests | `cargo test --no-default-features --features pq_signatures --lib pq_sig::` (rb) | 11 passed | | synthesis reader incl. hybrid records | `cargo test --no-default-features --features synthesis,pq_signatures --lib synthesis::` (rb) | 18 passed | | corpus test | `cargo test --no-default-features --features pq_signatures --test pq_signatures` (rb) | 3 passed | | corpus regenerates, every verdict | `wai_pq_vectors verify ../pq-conformance` (rb) | PASS, 46 cases | | Python verifier | `python3 -I pq-conformance/pq_verify.py pq-conformance` (local and rb) | all checks pass (90 ACVP, 46 cases, sizes, §8 table, 26 mutants, 14 dropped checks) | | Rust/Python differential | `diff` of both `verdicts` outputs (rb) | identical, 46 cases | | registry | `cargo test --no-default-features --lib codecs::registry`; `wai registry export --check` (rb) | 14 passed; up to date | | CI lib unit tests, CI feature set, and with `pq_signatures,synthesis` | `cargo test --lib --no-default-features --features world,…,http[,pq_signatures,synthesis]` (rb) | 843 passed; 886 passed | | integration tests whose corpora build key logs, with the feature on | `--test capture_take derive_edge energy_steering live_ads ad_presentation live_metering live_record live_room live_watermark wai_keys_cli cli_http staged_moq synthesis` (rb, feature union) | all passed | | every cargo-class corpus, F + `pq_signatures` | the CI loop, 22 corpora (rb) | all reproduce | | JWP/claims/signers corpora, feature on and off | `wai_{jwp,claim,signer}_vectors verify` (rb) | all vectors pass both ways | | synthesis and receipt verifiers that read touched texts | `synthesis_verify.py`, `synthesis_mark_verify.py`, `claim_verify.py`, `jwp_verify.py`, `signer_verify.py`; `--test synthesis --test synthesis_mark` | synthesis 41 vectors PASS, synthesis-mark PASS, `--test synthesis` 4 passed, `--release --test synthesis_mark` 5 passed; claims 567, JWP 164, signers 161 vectors, 0 failures | | clippy, all targets, feature on and off | `cargo clippy --no-default-features --features pq_signatures,synthesis --all-targets` vs origin/main | 108 notes = baseline's 108; none in new code | | lockfiles | every `Cargo.lock` holding `wai` (10) `cargo metadata --locked --offline` | all OK; only `wai-rs/Cargo.lock` changed: 11 packages added, none removed or re-versioned (existing entries' references to `der`/`pkcs8`/`signature`/`spki` gained version qualifiers because a second version of each now exists) | | C header | build with the feature, header regenerated | unchanged (lengths are crate-internal consts with public fns, as key_log does) | | repo checks | `check-tests-in-ci.py`, `check-workflow-concurrency.sh`, `check-spec-refs.py`, `check-wai-section-refs.py`, `check-no-vanity-metrics.sh`, `test-public-text-checks.sh`, `check-comparative-prose.sh`, `check-orphan-sources.py` | all exit 0 | | public names | `check-public-names.sh` on 46 changed files incl. corpus data; commit messages against the denylist | clean; 0 hits | ### Falsifications (each committed first, broken on the build host, then restored) | broken | what failed | |---|---| | one byte of `acvp/ml-dsa-65-sigver.json` | `every_pinned_file_has_its_bytes`, `acvp_sigver` (exit 101) | | ML-DSA half not checked in `pq_sig::verify` | `every_case_reaches_its_verdict`: 5 cases (group/claim/record PQ over other bytes or flipped, `log_bad_pq_half`) accept (exit 101) | | ML-DSA context `""` instead of the label (sign and verify) | `the_composite_test_vector`, the non-separation unit test (exit 101) | | step 4a without `hybrid_downgrade` | `group_downgrade`, `claim_downgrade` fall through to `hybrid_required` (exit 101) | | hybrid group payload without the ML-DSA key | `wai_pq_vectors verify` panics: the generator no longer reaches its verdicts (exit 101) | | a stated verdict changed in `vectors.json` | Rust verify FAIL (exit 1); `pq_verify.py` FAIL (exit 1) | | `_mldsa.py` UseHint turned the wrong way | ACVP sigVer fails in Python (exit 1) | | each of `pq_verify.py`'s 14 checks dropped | the corpus catches each (self-test) | | `key_log::lists_published_test_key` blind to ML-DSA keys | `a_log_listing_a_published_ml_dsa_key_is_reported` (exit 101) | ### Not done (open items) - **Other `wai:<domain>\x01 ‖ JCS` extensions** (exploration, settlement, access-services, the live JSON records…) are not wired: each adopts the shared check with the call synthesis now makes (~20 lines); until then their verifiers refuse hybrid records as malformed, which the spec states. - **Binary live-family records, OER/2, quantum receipts, §7.9 module grants and key statuses** keep Ed25519 alone; a hybrid entry's key can sign none of them (step 4a refuses), and a key status is signed by a hybrid root's Ed25519 key alone. Each needs a hybrid form in its own extension. - **`wai-keys` CLI** cannot yet write hybrid entries or roots (the library can). - **joule-moq's independent group-receipt reader** refuses version-3 groups (as any pre-hybrid reader does). - **`ReceiptChains`** applies no policy itself; `GroupReceipt::verify_policy` is the policy entry point. - **The ML-DSA dependency has had no independent audit** (its README says so); it is held to the ACVP vectors and the draft's vector, and the Python verifier is a second implementation of verification only. - The corpus signs deterministically for byte stability; §4.1 asks signers exposed to fault/side-channel attacks to sign hedged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- **Closed:** landed on main through merge train C (#461, e0ac15f5); this PR's head is an ancestor of main.
Adds the pq_signatures feature (one new dependency, ml-dsa 0.1.1, default
features off) and src/pq_sig.rs: the suite id-MLDSA65-Ed25519-SHA512 of
draft-ietf-lamps-pq-composite-sigs-19 (M' = Prefix || Label || 0 || SHA-512(M),
the ML-DSA half with the label as context, the Ed25519 half under WAI's strict
rule), the policy that requires hybrid, the hybrid JSON members and the shared
helper for records signed as wai:<domain>\x01 || JCS.

Before anything is built on it, the ML-DSA-65 implementation is checked
against the ACVP internal projections vendored in pq-conformance/acvp
(keyGen 25, deterministic sigGen 45 over three interfaces, sigVer 45), each
file pinned by SHA-256, and the composite against the draft's Appendix E
vector for this suite. Corpus, bin and test are stubs here.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Under the pq_signatures feature each family reads and verifies the hybrid form
beside the classical one, whose bytes and verdicts are unchanged:

- group receipts: the labelled payload's third version, moq-jwp:group-sign\x03,
  the v2 fields then the ML-DSA-65 key; canonical bytes end with both
  signatures, so the link hash covers both; JSON sign_version 3 with pq_key and
  pq_sig, and the binary form reads it; verify_policy(RequireHybrid);
- claims: pq_suite, pq_key and pq_sig, all or none, the first two signed;
- key logs: a hybrid entry (pq_suite, pq_key), a hybrid root (root_pq_key,
  pq_sig) committed to by BLAKE3(wai:key-set-next\x02 || K || P), rules 1, 3,
  5 and 6 extended, an anchor that requires hybrid (only hybrid-signed
  revisions descend from it, it moves only by pinning), and confirmation step
  4a: hybrid_downgrade, pq_key_mismatch, hybrid_required;
- synthesis records: the shared check of pq_sig::verify_record.

A labelled group whose sign_version is 3 with no hybrid member stays
group_label_invalid, so the JWP corpus gives every vector its verdict with the
feature on or off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
pq-conformance/vectors.json, written by wai_pq_vectors gen from published
seeds (one byte repeated 32 times, for Ed25519 and for ML-DSA's xi) and
deterministic signing, so it regenerates byte for byte. 45 cases: hybrid
group receipts (JSON and the WJR1 binary form, objects inclusion-verified),
claims, records of the wai:<domain>\x01 || JCS pattern and key logs, with the
negative controls the extension names (the post-quantum half stripped, made
over other bytes, flipped; a classical item under a hybrid key and under
require_hybrid; a hybrid entry later listed without its key; another
parameter set by suite and by length) and confirmations under pinned
anchors that do and do not require hybrid. The sizes section 9 states are
read from the corpus's own items.

wai_pq_vectors verify regenerates and checks every verdict; verdicts prints
them for the differential; bench times both suites.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- extensions/pq-signatures.md: the suite (id-MLDSA65-Ed25519-SHA512 of
  draft-ietf-lamps-pq-composite-sigs-19, ML-DSA-65 of FIPS 204 beside
  Ed25519) and why -65 and why the composite; the hybrid members; signing and
  verifying in WAI's order and what stripping does; the require-hybrid policy;
  key logs (hybrid entries and roots, the hybrid commitment, anchors that
  require hybrid, confirmation step 4a), group receipts (the payload's third
  version), claims and the shared check for wai:<domain>\x01 || JCS records;
  security considerations; the reasons; sizes and costs measured on the build
  host; the corpus.
- jwp-receipts: pointers at section 2.3, 2.4.1, 2.4.3, 2.7.1, 3, 3.1, 3.3, 7.2,
  7.4 and 7.7; SPEC section 5 and section 7 Signatures paragraphs; CHANGELOG.
- pq-conformance/pq_verify.py and _mldsa.py: ML-DSA-65 verification written
  from FIPS 204 in pure Python (all 90 ACVP vectors), every case, the sizes,
  section 8's table, 26 mutated cases refused, and every one of 14 checks
  caught when dropped. README: the corpus rules and the provenance of the
  vendored vectors, with the notice their licence asks kept intact.
- the suite registered as a wire identifier; registry export regenerated.
- CI: the reference's tests, the corpus's cross-platform regeneration, both
  verifiers, and a diff of their verdicts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
pq-signatures: a log listing a published ML-DSA key is reported
Some checks failed
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · every patch and mutation in exactly one shard (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Has been cancelled
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
wai-quantum WebGPU member / member · DX12 and Vulkan (cbio-windows) (pull_request) Has been cancelled
7eaf392745
Co-Authored-By: Claude Opus 5.5 <[email protected]>
dcharlot closed this pull request 2026-10-25 23:51:32 -04:00
Some checks failed
wai-web wasm32 / does this change touch it (pull_request) Has been cancelled
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been cancelled
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Has been cancelled
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Has been cancelled
WAI mbt2018_mean integer decode (regenerated vectors) / does this change touch it (pull_request) Has been cancelled
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Has been cancelled
wai-quantum WebGPU member / does this change touch it (pull_request) Has been cancelled
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Has been cancelled
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Has been cancelled
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Has been cancelled
wai-quantum WebGPU member / member · discrete GPU, Vulkan (jetson-hub) (pull_request) Has been cancelled
wai-quantum WebGPU member / member · integrated GPU, Vulkan (framework-128gb) (pull_request) Has been cancelled
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Has been cancelled
wai-quantum studio and desktop / does this change touch it (pull_request) Has been cancelled
wai-quantum studio and desktop / studio · browser (pull_request) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (pull_request) Has been cancelled
wai-pack / wai-pack · does this change touch it (pull_request) Has been cancelled
wai-pack / wai-pack · linux (pull_request) Has been cancelled
wai-pack / wai-pack · linux-arm64 (pull_request) Has been cancelled
wai-pack / wai-pack · macos (pull_request) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (pull_request) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (pull_request) Has been cancelled
wai-pack / wai-pack · dependency licences (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Has been cancelled
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Has been cancelled
WAI neural sink gates / does this change touch it (pull_request) Has been cancelled
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Has been cancelled
WAI neural decoders end-to-end / does this change touch it (pull_request) Has been cancelled
WAI neural decoders end-to-end / synthetic fixtures → Rust decode → reference (±4) (pull_request) Has been cancelled
WAI JPEG-AI whole decode (pinned inputs, required) / does this change touch it (pull_request) Has been cancelled
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Has been cancelled
WAI HTTP delivery / WAI HTTP · does this change touch it (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · every patch and mutation in exactly one shard (pull_request) Has been cancelled
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Has been cancelled
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
wai-quantum WebGPU member / member · DX12 and Vulkan (cbio-windows) (pull_request) Has been cancelled

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!435
No description provided.