open-standards/wai/pq-conformance
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science f338e61b9a pq-conformance: a provenance line, and its reason under the sha256 rule
#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]>
2026-10-22 10:52:10 -04:00
..
acvp pq-signatures: ML-DSA-65 held to FIPS 204's ACVP vectors and the composite's own vector 2026-10-22 04:11:14 -04:00
_mldsa.py pq-signatures: the extension, its second implementation, CI and the registry 2026-10-22 04:45:30 -04:00
draft-composite-sigs-19.json pq-signatures: ML-DSA-65 held to FIPS 204's ACVP vectors and the composite's own vector 2026-10-22 04:11:14 -04:00
pq_verify.py pq-signatures: synthesis §2 names the hybrid members; the Python verifier writes errors as verdicts 2026-10-22 04:55:47 -04:00
README.md pq-conformance: a provenance line, and its reason under the sha256 rule 2026-10-22 10:52:10 -04:00
vectors.json pq-signatures: the extension, its second implementation, CI and the registry 2026-10-22 04:45:30 -04:00

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 by wai_pq_vectors gen (wai-rs, feature pq_signatures):
    • suite: the suite of §2, its label, prefix and lengths;
    • codes: the codes of §8, in its order;
    • origin and logs: 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 controls downgraded, bad-pq-half, next-root-own, pq-key-twice, entry-suite-44, root-pq-swapped and classical-child;
    • cases: each with its name, what it is (about), its family — group (a group receipt's JSON members in group, or a group's receipts in the binary form of jwp-receipts §2.7.3 in receipts_bin, hexadecimal), claim (a claim's text), record (a record of §3.4's pattern: its signing domain and its text) or key_log (a log of logs, anchored by pinning) — the verifier's policy (accept-classical or require-hybrid, §5), for a group or a claim an optional confirm (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), and extract.py, which derives them from the upstream files.
  • draft-composite-sigs-19.json: the test vector draft-ietf-lamps-pq-composite-sigs-19 Appendix E gives for id-MLDSA65-Ed25519-SHA512 (below).
  • pq_verify.py and _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>, with objects=<n> when its objects were given and each was verified by inclusion (jwp-receipts §3, Verified by inclusion); or reject step=<s> reason=<code> (jwp-receipts §3, §3.3);
  • a claim: accept link=<link hash>, or reject step=<s> reason=<code> (jwp-receipts §3.1);
  • a record: accept form=hybrid or accept form=classical, or reject reason=<code> (§3.4: the extension's own bad_key and bad_signature, or a code of §8);
  • a key log: anchored head=<revision hash> or refused 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_key names the ML-DSA ones, and key_log::lists_published_test_key reports 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 gen writes 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 verify checks 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.