Repository navigation
pq-verify v2.10.0
Changed — read before upgrading
- Failing results now fail the exit status by default.
0verified,1a
task found problems or could not verify,2bad input. Before, every task
exited 0 unless--fail-on-findingwas passed; the GitHub Action did not
pass it to--acvp-all, so a failed ACVP run publishedverified=true.
--no-failrestores report-only behaviour;--fail-on-findingis accepted
and changes nothing. A failed self-suite check now fails the exit status too
(it never did, even with the flag). pqverify_acvp_all()is notverifiedwhen a requested suite did not
run. A missing kyber-py dropped ML-KEM from both sides of the ratio and
the API reportedverified: True; the result now listsnot_run. The
per-suite functions are not verified at 0/0.- A modifier without its task is an input error (exit 2):
--audit-hbs-full,--dsa-abi,--dsa-symbol,--kem-*,--fresh-*and
--audit-timeoutused to be ignored silently. --emit-promptwithout--fresh-keysays its answers are public. Its
questions are NIST's; so are the answers. The old claim that a matching
response "proves the responder computes the standard" is withdrawn.
Added — FN-DSA verification, draft track (FIPS 206)
pq-verify --fndsachecks FN-DSA (Falcon) signature verification
ahead of the final FIPS 206, in its own report (pq-verify/fndsa-result,
track: "draft"). It is not part of the self-suite's 160 checks, the 1566
ACVP vectors, or any FIPS 203/204/205 verdict.- Vectors: Falcon-512/1024 outputs of PQClean's reference code at
0586a82
(its NIST KAT harness and deterministic generator), 30 signatures, each file
pinned to the sha256 PQClean publishes in META.yml.
tools/pin_fndsa_vectors.pyregenerates them from a checkout and refuses
output that does not match;--checkandtools/doctor.pyre-verify
offline. - For every detached signature, the encodings a verifier must refuse, each
required to be refused for the right reason: a decoder that accepts-0
would pass a norm check unchanged, so the suite distinguishes the two. - The NTT mod 12289 (n = 512, 1024) against the negacyclic definition, and
every butterfly through the native Z_q engine, unchanged. - Signing is not checked: it needs the final standard's vectors.
Added — engine tracks, macOS
- Every self-suite check carries a track —
pqc(54),harness(13),
classical(45),research(48) — printed per track in the summary and
recorded in the JSON report with the post-quantum work each non-PQC engine
is being realigned toward (FN-DSA's NTT, HQC, Classic McEliece, hybrid-KEM
curves, isogeny test vectors). Only the PQC track is PQC evidence; a check
without a track fails the test suite. - macOS (Apple silicon) is tested in CI: Mach-O symbols (
nm -gU), dyld's
image list for the loaded-object binding, portable engine flags (-lrt
dropped on macOS,-march=nativeretried as-mcpu=native
before building untuned, with the flags each engine got recorded in
core.ENGINE_FLAGS; C++ engines built as
C++17). Engine 6 build failures are now DEGRADED with the compiler's
message and their checks recorded as skipped, rather than vanishing from
the count. - CI pinned to
ubuntu-24.04ahead ofubuntu-latestmoving to
Ubuntu 26; failing tests on the macOS job are reported as annotations.
Added — LMS in the prompt/response path
- LMS, fresh and pinned, for implementations that cannot be loaded.
--emit-prompt LMS_SHA256_M32_H5/LMOTS_SHA256_N32_W1 --fresh-key K(any
SP 800-208 pairing) poses keyGen, sigGen and sigVer in NIST's LMS ACVP
layout; NIST's own LMS prompts join the pinned path too. sigGen is answered
as ACVP asks — the responder signs with its own key and reports it per
group — so each signature is verified under that key, the key must be of
the group's type, and a leaf signing twice under one key anywhere in the
response is a finding: the key-state check, applied to a black box.
Sets too large to build in Python (over 3M hash calls, e.g. height 20) get
sigGen alone.
Added — LMS/XMSS key state
- LMS/XMSS key-state audit: a one-time key used twice is broken, and no
vector can show it.pqv_hbs.hgains three optional functions
(pqv_hbs_state_keygen,pqv_hbs_state_sign,pqv_hbs_state_skip,
capabilityPQV_HBS_CAN_STATE) through which the library manages its own
key in a file.--audit-hbsadds a<scheme> statestage per sampled key:
the key matchespqv_hbs_keygen; every issued signature verifies; no leaf
is issued twice; after each signature a restart from the stored key never
reissues a released leaf (state durable before release); and the key signs
its last leaf, then refuses — reached by skipping ahead, so 2^40-leaf keys
are checked. Sampling takes the cheapest key of each distinct tree height
first. An adapter without the functions reports the stage not applicable,
so the scope is partial. Both shipped adapters implement them. - Two defects found in xmss-reference (upstream
master, 171ccbd): the
last leaf of every key returns success with an invalid signature (the key
is wiped before signing), and XMSS^MT h = 40 keys never refuse once
exhausted (the all-ones marker equals the last valid index; the index then
wraps to 0). Pinned as known failing checks; hash-sigs passes every state
check. Three new mutants (count never written back; index not advanced;
exhaustion check removed) are caught by the state stage alone.
Changed — vendor audit tooling
tools/vendor_audit.pypins each LMS/XMSS row's failing checks by name
(failing), and a mutant counts as caught only when it fails a check the
library itself passes — a stage the library already fails no longer
catches every mutant vacuously.- A not-run count with several reasons (sampled, over budget) lists each,
rather than the last one seen.
Added
- Fresh, unpublished ACVP questions (
pq_verify/fresh.py):
--emit-prompt SET --fresh-key Kderives every input from a 256-bit seed
written only toK(0600, never overwritten) and writes the questions in
NIST's ACVP layout, so an ACVP harness answers them unchanged;
--verify-response R --fresh-key Kre-derives them, checks the promptId and
computes the answers then. ML-KEM (keyGen, encapsulation, decapsulation with
implicit-rejection ciphertexts, both key checks), ML-DSA (keyGen; sigGen and
sigVer over every interface, deterministic and hedged, boundary context
lengths) and all twelve SLH-DSA sets (keyGen, sigGen, sigVer).
--fresh-countsets tests per group. Replaying NIST's public answers scores
nothing. The pinned prompt path gains SLH-DSA sigGen and sigVer. - Every vendor audit runs in a child process (
pq_verify/isolate.py,
--audit-timeout). A crash or a hang isCANNOT VERIFYwith the signal or
the limit, and a report is still written. The artifact binding gains
loaded_objects(every shared object the audit mapped, with sha256),
loader_environmentandisolation; a file that changes mid-audit is
CANNOT VERIFY.PQV_IN_PROCESS=1runs in-process, for debugging. scopeon--audit-dsaand--audit-hbsreports: checked, not
applicable and not run, with a one-line statement.--require-full-coverage
fails a partial scope.--check-no-harness PATH: exit 1 if a build carries the deterministic
randombytes() harness or a pqv_hbs adapter.- GitHub Action:
audit: kem | dsa | hbs | nttandparam-setdrive the
vendor audits; every input reaches the shell throughenv:, closing a
script-injection path throughlibrary,symboland the file inputs; the
ACVP step is gated and writes a JSON report;verifiedis derived from
every task that ran.
Security
- The release workflow and the Action pin every action to a commit SHA (the
release job holdscontents: writeand the OIDC identity PyPI trusts).
The release verify job also runsdoctor.pyand--lms-xmss.
Added (LMS/XMSS)
-
--audit-hbs: a vendor's own LMS/HSS or XMSS/XMSS^MT library.
pqverify_audit_hbs()loads a library through a pqv_hbs adapter
(pq_verify/harness/hbs/pqv_hbs.h, ABI 1): five C functions that map its
API onto one encoding, since LMS and XMSS libraries share no C API.
Adapters for cisco/hash-sigs and xmss-reference ship in the package.- Stages: verify (every pinned vector: NIST LMS, the pqc-kat LMS and
XMSS sets, liboqs XMSS/XMSS^MT/HSS, RFC 8554), keyGen and sigGen
byte-exact (LMS with the ACVP derivation), and malformed: signatures
derived from valid ones, wrong in exactly one field (leaf index,
typecodes, randomizer, chain values, authentication nodes, length,
message, key), each confirmed invalid by pq-verify's own verifier. - What the library does not implement is not applicable; key generation
and signing over the budget, or beyond two cases per parameter set,
are not run (--audit-hbs-full). Neither counts as a pass. - JSON (
pq-verify/hbs-audit-result) and SARIF (new rule PQV009).
- Stages: verify (every pinned vector: NIST LMS, the pqc-kat LMS and
-
LMS/XMSS vendor audits in CI, with mutants. cisco/hash-sigs
44e6c7dand xmss-reference171ccbdare pinned in
tools/vendor_audits.jsonwith their mutants, all caught (final counts,
with the key-state stage, are under "LMS/XMSS key state" above). A verifier
that ignores the LM-OTS typecode in the signature passes every published
vector and is caught only by the malformed stage. Two candidate mutants
were discarded as equivalent (no verdict changes), and AUDITS.md says
which and why. -
LMS/HSS and XMSS/XMSS^MT (RFC 8554, RFC 8391, SP 800-208), the
stateful hash-based signatures CNSA 2.0 requires for firmware signing.
pq_verify/hbs.pyimplements both from the specifications, every
SP 800-208 hash family (SHA-256, SHA-256/192, SHAKE256/256, SHAKE256/192)
and RFC 8391's SHA-512 and SHAKE128 sets, with SP 800-208's pseudorandom
key generation.- NIST's 87 ACVP LMS vectors (ACVP-Server
2972def) are pinned with
the other NIST files and run in--acvp-alland--lms-acvp: 9 keyGen
byte-exact, 16 sigVer verdicts, and 62 sigGen signatures verified under
their published keys (NIST's sigGen vectors carry no private key). --lms-xmssruns every other pinned source (hbs_vectors.json.gz,
HBS_MANIFEST.json,tools/pin_hbs_vectors.py), each labelled by origin:
ACVP-format LMS and XMSS vectors for every family from
post-quantum-cryptography/KAT, liboqs's XMSS^MT and HSS KATs, and
RFC 8554 Appendix F's HSS test cases via cisco/hash-sigs. Every signature
is verified; key generation and signing are byte-exact for each tree
within a hash budget. The default budget takes about 10 s (1,921 checks);
--lms-xmss-fullbuilds every height-10 tree. A case over the budget is
reported as not run with its cost, never as passed.- The doctor checks the new bundle's digests offline, and the watcher
tracks NIST's five LMS directories.
- NIST's 87 ACVP LMS vectors (ACVP-Server
-
--audit-dsa: a vendor's own ML-DSA, against every NIST vector.
pqverify_audit_dsa()drives a compiled library's key generation, signing
and verification with all of NIST's ACVP ML-DSA vectors for a parameter set
(25 keyGen, 120 sigGen, 60 sigVer) and Wycheproof's ML-DSA verify and sign
vectors.- Every FIPS 204 interface is its own stage (internal, pure, pre-hash over
twelve hashes, external μ). Each goes through the most public entry
point the library has for it, and the report names that symbol.
Interfaces the API lacks are reported as not applicable with the
reason, never as passes. - The pq-crystals/PQClean and mldsa-native calling conventions are
detected from symbol names;--dsa-abiand--dsa-symbol ROLE=SYM
override. An ambiguous symbol is refused, not guessed. - Randomness harness (
pq_verify/harness/pqv_randombytes.c, shipped
in the package). Linked in place ofrandombytes(), it lets pq-verify
serve NIST's seed andrnd, so the randomisedkeypair()and
signature()that users call are audited byte-exactly, not only the
seed-taking internals. A call that draws more or less randomness than
FIPS 204 calls for is a finding. - JSON (
pq-verify/dsa-audit-result) and SARIF (new rule PQV008) reports
name the first failing NIST tcId or Wycheproof case.
- Every FIPS 204 interface is its own stage (internal, pure, pre-hash over
-
ML-DSA vendor audits in CI, with mutants.
tools/vendor_audits.json
pins mldsa-native159509d, pq-crystals dilithium refd35ba3fand
PQClean0586a82, all three parameter sets each. All three are byte-exact
on every interface they expose: 1,503, 1,335 and 1,065 checks. Each row
carries mutants, one planted bug each, and CI requires the audit to fail
every one; all ten are caught. Two of them, a hint decoder that accepts a
repeated index and a verifier that skips the ‖z‖ bound, pass every NIST
sigVer vector and are caught only by Wycheproof. AUDITS.md publishes both
tables, and tests hold them equal to the pinned file. -
SLH-DSA signatures, against every NIST ACVP vector (FIPS 205, all 12
parameter sets). Until now pq-verify checked SLH-DSA key generation only.- sigVer, 504 vectors, on by default. Every verdict must match NIST's
testPassed: valid signatures, and ones with a modified message, R, FORS
or hypertree part, or one byte too short or too long. - sigGen, 624 vectors, opt-in (
--slhdsa-siggen,
pqverify_slhdsa_acvp(siggen=True)). Every signature must be byte-exact
with NIST's, deterministic and with NIST'sadditionalRandomness.
Signing an 's' parameter set takes seconds per signature, so the full run
takes about 30 minutes; a weekly workflow runs it with one job per
parameter set, and on changes to the code, the vectors or the reference. - Both cover the internal interface and the external pure and pre-hash
ones, the latter over all twelve approved hash functions. pq-verify builds
M′ itself (domain separator, context, DER OID, digest) rather than calling
the reference's wrappers, so those encodings are checked against NIST too.
The reference'ssign()cannot take injected randomness, so signing uses
FIPS 205 Algorithm 19 written over its FORS and hypertree primitives. - Negative controls: a verifier that always accepts or always rejects
fails exactly 432 or 72 vectors; dropping the context from M′ fails every
valid external signature with a non-empty one; a signer that ignores
additionalRandomnessmisses every randomised case.
- sigVer, 504 vectors, on by default. Every verdict must match NIST's
-
--slhdsa-acvpruns the SLH-DSA suite on its own (keyGen and sigVer);
param_sets=limits it to chosen parameter sets.
Changed
-
The Wycheproof edge-case runner counts ML-DSA cases a backend cannot
express as not applicable. They used to be skipped without a count. -
--acvp-allandpqverify_acvp_all()include SLH-DSA keyGen and
sigVer (624) and NIST's LMS vectors (87): 1566 vectors, up from 855. The run takes about
a minute.pqverify_acvp_all(slhdsa=False, lms=False)gives the previous 855; the
slh-dsareference was already part ofpq-verify[full]. If it is missing,
the SLH-DSA suite is reported as not run instead of being left out of the
report. -
The SLH-DSA signature vectors ship in a second archive,
pq_verify/vectors/slhdsa_sig_vectors.json.gz(39 MB). It holds NIST's
files verbatim (ACVP-Server112690e) and is opened only when an SLH-DSA
signature suite runs. The doctor checks each entry's sha256 offline, the
watcher tracks all four files, and--applyrewrites only the archive that
changed. The wheel grows from about 15 MB to about 54 MB. -
The watcher's structural fingerprint tells signature groups apart by
interface (signatureInterface/preHash/deterministic), so a change
report no longer merges the six groups that share a parameter set.
Fixed
- SARIF filed some findings under the wrong rule. Rules were chosen by
the first matching keyword, and "malformed" (CannotVerify) matched before
a scheme's own prefix, so a finding about malformed signatures being
accepted would have been reported as "could not verify". Scheme prefixes
are now matched first. - The GitHub Action's
slhdsainput did nothing. It was declared but
never read. SLH-DSA keyGen and sigVer now run with the ACVP suites, and
slhdsa: siggenadds sigGen. - An ACVP report was VERIFIED when a requested suite could not run. Only
the suites that ran were counted, so with kyber-py and dilithium-py missing,
--acvp-all --fail-on-findingwould pass on SLH-DSA's 624/624 alone. A
suite that did not run now makes the report CANNOT VERIFY and is listed
undersummary.not_run. - Nothing in CI ran the SLH-DSA ACVP suite. The documented keyGen
120/120 was never checked on a push. CI and the release workflow now
installslh-dsaand fail unless all three ACVP suites ran, because
--fail-on-findingalone passes a suite that could not run.
Verifying this release
gh attestation verify pq_verify-2.10.0-py3-none-any.whl \
--repo bigDSanalyst/pq-verifyBuilt by .github/workflows/release.yml from commit 5f6e4c52c908f97fb229e8a06ee97996c93e9589,
after the full suite and all 1566 NIST ACVP vectors passed on Python 3.9 through 3.13.
An SPDX SBOM is attached and attested.