#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]>
143 lines
11 KiB
Markdown
143 lines
11 KiB
Markdown
# pq-conformance — hybrid signatures
|
|
|
|
The corpus of [`extensions/pq-signatures.md`](../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. <!-- allow-name: the licence notice of the vendored ACVP vectors, which it requires kept intact -->
|
|
>
|
|
> 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. <!-- allow-name: the licence notice of the vendored ACVP vectors, which it requires kept intact -->
|
|
>
|
|
> 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. <!-- allow-name: the licence notice of the vendored ACVP vectors, which it requires kept intact -->
|
|
|
|
**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.
|