#435 added wai/pq-conformance/ without the two entries every corpus directory needs on main: a `**Provenance.**` line in its README (tests/corpus_provenance.rs) and, having no sha256.json sidecar, a reason in the exclusion list of int_kernels_vectors' every_decoded_output_states_a_sha256. Both tests enumerate every *-conformance directory, so #435 fails them on its own; the train found it. The provenance line names the generator and its seeds, the ACVP vectors' derivation and notice, and the composite draft's vector; the reason says the corpus is signed content with no decoded output. Co-Authored-By: Claude Opus 5.5 <[email protected]>
11 KiB
pq-conformance — hybrid signatures
The corpus of extensions/pq-signatures.md §10: records signed
with Ed25519 and ML-DSA-65 together, the controls each rule refuses, and the published vectors the
ML-DSA implementations are held to before any of it.
Provenance. vectors.json is written by wai_pq_vectors gen (in wai/wai-rs, feature
pq_signatures) from published seeds, each one byte repeated 32 times, for the Ed25519 keys and
for ML-DSA's ξ, with deterministic signing. acvp/ holds ML-DSA-65's ACVP vectors as
acvp/extract.py derives them from the ACVP server repository's internal projections at a pinned
commit, under the notice their source publishes, and draft-composite-sigs-19.json is the case of
draft-ietf-lamps-pq-composite-sigs-19 Appendix E for this suite (Provenance of the vendored
vectors, below).
Files
vectors.json, written bywai_pq_vectors gen(wai-rs, featurepq_signatures):suite: the suite of §2, its label, prefix and lengths;codes: the codes of §8, in its order;originandlogs: the key logs the cases anchor and hold, each a key-log document (jwp-receipts §7.2), by name:hybrid(a hybrid root from its first revision, a revision that adds a hybrid key, a rotation to the committed hybrid root),hybrid-r0(its first revision),classical(a classical root committed to a hybrid successor, and a classical revision),classical-r0,rotated(the classical root rotated to that successor), and the controlsdowngraded,bad-pq-half,next-root-own,pq-key-twice,entry-suite-44,root-pq-swappedandclassical-child;cases: each with itsname, what it is (about), itsfamily—group(a group receipt's JSON members ingroup, or a group's receipts in the binary form of jwp-receipts §2.7.3 inreceipts_bin, hexadecimal),claim(a claim's text),record(a record of §3.4's pattern: its signingdomainand itstext) orkey_log(a log oflogs, anchored by pinning) — the verifier'spolicy(accept-classicalorrequire-hybrid, §5), for a group or a claim an optionalconfirm(the log the relying party pins,pin; whether its anchor requires hybrid,require_hybrid; the logs whose revisions it also holds,held), and the verdict it has,expect;sizes: the sizes §9 states, read from the corpus's own items.
acvp/: FIPS 204's ACVP test vectors for ML-DSA-65 (below), andextract.py, which derives them from the upstream files.draft-composite-sigs-19.json: the test vectordraft-ietf-lamps-pq-composite-sigs-19Appendix E gives forid-MLDSA65-Ed25519-SHA512(below).pq_verify.pyand_mldsa.py: the second implementation (below).
The verdicts
A case's expect is one line. An item's verdict first:
- a group:
accept link=<link hash>, withobjects=<n>when its objects were given and each was verified by inclusion (jwp-receipts §3, Verified by inclusion); orreject step=<s> reason=<code>(jwp-receipts §3, §3.3); - a claim:
accept link=<link hash>, orreject step=<s> reason=<code>(jwp-receipts §3.1); - a record:
accept form=hybridoraccept form=classical, orreject reason=<code>(§3.4: the extension's ownbad_keyandbad_signature, or a code of §8); - a key log:
anchored head=<revision hash>orrefused reason=<code>(jwp-receipts §7.3).
Then, for a case with confirm, | and the confirmation (jwp-receipts §7.4 with step 4a):
confirmed key_id=<id> head=<revision hash>, refused step=<s> reason=<code> or unconfirmed step=<s> reason=<code>. The anchor is the latest revision of the log pinned (§7.3); the held
revisions are those of held that pass rules 1 to 3.
Rules of the corpus
- Every key is made from a seed that is one byte repeated 32 times — an Ed25519 seed (RFC 8032
§5.1.5), or an ML-DSA seed ξ (FIPS 204 Algorithm 6) — so every key here is published and
attests nothing (jwp-receipts §7.8). The reference's
pq_sig::is_published_test_pq_keynames the ML-DSA ones, andkey_log::lists_published_test_keyreports a log that lists one. - Ed25519 is deterministic, and every ML-DSA signature here is made in FIPS 204's deterministic
variant (§3.4), so
wai_pq_vectors genwrites the same bytes on every machine. - Each case states the verdict it was written to have, and the generator refuses to write a corpus in which the reference does not reach every one, or which leaves a code of §8 ungiven.
- The directory holds nothing but the generated file, this README, the vendored vectors and
their derivation, and the second implementation;
wai_pq_vectors verifychecks that.
Provenance of the vendored vectors
ACVP. acvp/ml-dsa-65-keygen.json, acvp/ml-dsa-65-siggen.json and
acvp/ml-dsa-65-sigver.json are derived from the internal projections of the ACVP test vectors
for FIPS 204's ML-DSA, as published in the ACVP server's repository at commit
975de31eb83d87039ec88934fdc47d8c312b892d, directory gen-val/json-files/:
| upstream file | SHA-256 |
|---|---|
ML-DSA-keyGen-FIPS204/internalProjection.json |
e67ee6540d40e11506c3c4e3b1f79fc1cefcd49820db99fc61f87cc8ba463baf |
ML-DSA-sigGen-FIPS204/internalProjection.json |
72dcaf5f69853ca267ccd16af9cb40949786aca0fcfbf05d1ebeba132b93af22 |
ML-DSA-sigVer-FIPS204/internalProjection.json |
47cdd6314c7f746d02421ffcba89d4dbc7bb875ac49e07a029fdfc26fba55437 |
python3 -I acvp/extract.py <gen-val/json-files> acvp checks each upstream file against that
digest and keeps its ML-DSA-65 groups that pq-signatures uses: every key generation group; the
deterministic signature generation groups of the pure external interface (with a context string),
of the internal interface, and of the internal interface over an external μ; and the signature
verification groups of the same three interfaces. The pre-hash groups (HashML-DSA, FIPS 204 §5.4)
are left out. Each kept test is as upstream wrote it. The derived files, pinned by the reference's
tests (pq_sig::acvp):
| file | SHA-256 | tests |
|---|---|---|
acvp/ml-dsa-65-keygen.json |
06b637b695150bfc752748e8636858f2e2da9c77547ab9856ddd115af5754a0f |
25 |
acvp/ml-dsa-65-siggen.json |
8c47e6fa05e32abcaa9ae32a6a361771ca5bd8275847db9eac4b5a959eb9c327 |
45 |
acvp/ml-dsa-65-sigver.json |
cd48b587b423ec42b62971afcbfc1395491b994101192b0a10ce681a42e78a81 |
45, of which 36 modified |
These files were derived on 22 October 2026 from the upstream files above by keeping the groups named and nothing else. The upstream files carry this notice, kept intact here as it asks:
NIST-developed software is provided by NIST as a public service. You may use, copy, and distribute copies of the software in any medium, provided that you keep intact this entire notice. You may improve, modify, and create derivative works of the software or any portion of the software, and you may copy and distribute such modifications or works. Modified works should carry a notice stating that you changed the software and should note the date and nature of any such change. Please explicitly acknowledge the National Institute of Standards and Technology as the source of the software.
NIST-developed software is expressly provided "AS IS." NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED, IN FACT, OR ARISING BY OPERATION OF LAW, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND DATA ACCURACY. NIST NEITHER REPRESENTS NOR WARRANTS THAT THE OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR ERROR-FREE, OR THAT ANY DEFECTS WILL BE CORRECTED. NIST DOES NOT WARRANT OR MAKE ANY REPRESENTATIONS REGARDING THE USE OF THE SOFTWARE OR THE RESULTS THEREOF, INCLUDING BUT NOT LIMITED TO THE CORRECTNESS, ACCURACY, RELIABILITY, OR USEFULNESS OF THE SOFTWARE.
You are solely responsible for determining the appropriateness of using and distributing the software and you assume all risks associated with its use, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and the unavailability or interruption of operation. This software is not intended to be used in any situation where a failure could cause risk of injury or damage to property. The software developed by NIST employees is not subject to copyright protection within the United States.
The composite's vector. draft-composite-sigs-19.json is the case id-MLDSA65-Ed25519-SHA512
of draft-ietf-lamps-pq-composite-sigs-19 Appendix E, with the appendix's message m and context
ctx, read from the draft's text (whose SHA-256 is pq-signatures' header's) by removing the
line breaks the text wraps its values with, and written in hexadecimal: pk (the ML-DSA key and
the Ed25519 key, draft §4.1), sk (the ML-DSA seed and the Ed25519 seed, §4.2), s (over m
with no context) and sWithContext (over m with ctx), each mldsaSig ‖ tradSig (§4.3). Its
SHA-256, pinned by the reference's tests, is
d880a8fbb305e520620bff771b1e11bb21a83c97fbb9e286210d7c9e64898f20.
Equivalence
A conforming verifier gives every case its verdict. The reference reproduces every ACVP key and
signature byte for byte and gives every verification its verdict (cargo test --no-default-features --features pq_signatures --lib pq_sig::), regenerates vectors.json byte for byte and reaches every verdict
(wai_pq_vectors verify pq-conformance).
The second implementation
pq_verify.py, written from pq-signatures' text and the documents it cites, shares no code with
wai-rs. ML-DSA-65 verification is _mldsa.py, written from FIPS 204 with Python's integers and
the standard library's SHAKE; it signs nothing. It takes from ../claim-conformance/ claim_verify.py what pq-signatures reuses from jwp-receipts §3.1 (the JSON reading, JCS, the body
checks and class rules of a claim, strict Ed25519, BLAKE3), and from
../signer-conformance/signer_verify.py §7's classical rule 1, the signer_id grammar and a
claim's authorities. It checks every ACVP verification vector and every ACVP signature, the
composite's vector, every case, the sizes and §8's table; then it requires every case that
accepts, mutated (a flipped byte of either signature, the post-quantum half stripped), to be
refused, and the corpus to catch the verifier with any one of its own checks dropped.
python3 -I pq_verify.py . everything, about ten seconds
python3 -I pq_verify.py verdicts vectors.json name<TAB>verdict for every case
.github/workflows/wai-conformance.yml runs the reference's tests and both verifiers, and
requires the two to give every case the same verdict.