Repository navigation
pq-verify v2.11.0
Fixed — supersingular curves, and engine targets that overclaimed
- Every j = 0 and j = 1728 curve was reported supersingular. The rule is
exact only with the prime: for p > 3, j = 0 is supersingular iff
p ≡ 2 mod 3 and j = 1728 iff p ≡ 3 mod 4 (Deuring); any other j only when
the trace is 0, which the exact point count now decides. Three of the four
"supersingular" curves in the adversarial suite's B2 were ordinary
(y² = x³ + 1 mod 97 has 84 points; y² = x³ + x mod 17 and mod 97 have 16
and 80), so B2 passed by sharing the check's mistake. B2 now holds six
supersingular curves (including j = 8 mod 17, found only by the count, and
j = 1728 mod a prime above 10¹⁵, decided by the rule alone) and must leave
four ordinary j = 0 / 1728 controls unflagged. - SafeCurves' "ordinary curve" control could not fail: it passed at any
severity of at least OK. An OK expectation is now exact. - Engine targets no longer claim what they do not do. The curve engines
were listed as heading toward "the ECDH half of hybrid KEMs"; X25519 is
neither j = 0 nor j = 1728, and hybrids are checked by--verify-hybrid,
which does not use these engines. Engine 6 was listed as heading toward
"mod-p Hasse–Witt test vectors for SQIsign", a route that does not exist.
Both now say they have no PQC target.
Added — exact FN-DSA key checks (--audit-fndsa, draft track)
- keyGen checks that every secret key is a valid Falcon key: G is
recomputed (g·F/f mod q, centred, |G| ≤ 127) andf·G − g·F = qmust
hold in ℤ[x]/(xⁿ+1); both Gram–Schmidt norms must be below 1.17²·q:
‖(g, −f)‖²andq²·[1/(f f* + g g*)]₀, the latter an exact rational
through the field-norm tower. No floating point. The third polynomial in
the secret key, decoded and discarded until now, is F (it was named G). - A mutant whose key generator stores an F that does not solve the NTRU
equation is caught; PQClean passes at both sets (keyGen 30/30). - A library that refuses to sign with its own key is now a sign failure,
not a crash of the audit.
Added — constant-time audit (--constant-time)
--audit-kem PATH SET --constant-timeruns the vendor's own Encaps
(m secret) and Decaps (the K-PKE secret key and z secret, on a valid and on
a random ciphertext) under Valgrind memcheck with the secret bytes marked,
and fails on every secret-dependent branch or memory address, naming the
function. Stagesct:encaps,ct:decaps; the report'sside_channel
gainsconstant_time_checkedwith the exact scope.- A self-test first: a deliberate branch on a marked secret must be
reported, or the check is unavailable -- memcheck's silence is never read
as a pass. Asked for and unable to run (no valgrind) fails the audit. - What it does not see, stated in every report: instruction timing (a secret
division, as in KyberSlash -- division instructions are listed for review
instead), key generation (rho is derived from the secret seed and
rejection-sampled; libraries declassify it internally), and anything
physical. - mlkem-native and PQClean pass at all three sets. Three leaks planted in
PQClean, each leaving every output correct (a secret branch in
poly_frommsg, the Clangover class; a secret-indexed table in
poly_tomsg;memcmpin the FO re-encryption check), pass every
correctness stage including the accumulated cases and are caught here. CI
installs valgrind and requires all of it (tools/vendor_audit.py).
Added — accumulated vectors
- 10 000 seeded random cases per audit, hashed into one digest and
compared with a pinned value: ML-KEM (keyGen, encaps, decaps of the right
and of a random ciphertext) and ML-DSA (keyGen, deterministic signing,
verification), the way Go and BoringSSL test themselves. On by default for
--audit-kem,--audit-dsaand--audit-harness;--accumulated N
chooses 100 or 10 000,0skips. A stage of its own (accumulated) in
every report, with where its digest comes from. - They reach what no fixed vector set does. A planted bug that returns a
wrong shared key on about 1 ciphertext in 8 192 passes every NIST,
Wycheproof and CCTV vector and is caught by the accumulated run (ML-KEM-512
and -768; at 1024 its trigger never came up in 10 000 cases -- coverage is
probabilistic, and the docs say so). - C2SP CCTV's published ML-KEM accumulated digests are the FIPS 203
draft. With the draft's K-PKE.KeyGen G(d) instead of the final
G(d || k), pq-verify reproduces CCTV's ML-KEM-512 value exactly; no
final-FIPS-203 implementation can match them, so every correct library
would have failed. pq-verify uses Go's construction and Go's published
ML-KEM-768 digests instead, and pins ML-KEM-512/1024 digests computed only
where independent implementations agree (PQClean, kyber-py, CIRCL, Go),
labelled as computed, not published. CCTV's ML-DSA digests are final FIPS
204 and are used as published. - Every pinned library passes: mlkem-native, PQClean (ML-KEM and ML-DSA),
mldsa-native, the pq-crystals reference; through harnesses, Go
crypto/mlkemand CIRCL. The vendor-audit tables gain the column.
Added — audit any language through Crucible-protocol harnesses
--audit-harness COMMAND PARAM_SETaudits an ML-KEM or ML-DSA
implementation through a harness speaking Crucible's JSON-line protocol
(Go, Java, Rust, JS, Zig ...). Only the wire format is Crucible's: NIST's
vectors and invalid keys and the Wycheproof/CCTV edge cases are scored by
the same code as--audit-kem/--audit-dsa. JSON
(pq-verify/harness-audit-result) and SARIF; the harness file is hashed.- The harness is untrusted. A crash, hang or protocol violation is CANNOT
VERIFY, never a refusal (as a refusal it would pass every invalid-input
check). Determinism is tested: checks a harness cannot be held to are not
applicable. Its ML-DSA message convention (M′, or M with an empty context)
is settled against a NIST signature. It must accept its own fresh signature
before its rejections are scored. - Seed-form decapsulation keys (d ‖ z, FIPS 203 §7.1) are checked as such,
not failed. tools/harness_audit.pyrebuilds Crucible's Gocrypto/mlkemand CIRCL
harnesses at a pinned commit with Go 1.26.1 and requires their results in
CI: Go ML-KEM-768/1024 and CIRCL ML-KEM-512/768/1024 and ML-DSA-44/65/87
VERIFIED.pq_verify.edge: a backend may declare inputs it cannot be handed
(accepts), counted as not applicable.
Added — FN-DSA library audit, draft track (FIPS 206)
--audit-fndsa PATH FN-DSA-512|FN-DSA-1024audits a vendor's FN-DSA
(Falcon) library through its own entry points (NIST API / PQClean, or
liboqsOQS_SIG_falcon_*, padded variants included;--fndsa-symbol ROLE=SYMBOLwhen a library exports more than one candidate). Report
pq-verify/fndsa-audit-result,track: "draft", JSON and SARIF, run in an
isolated child process like every other audit.- Stages: the pinned signatures verify and open; every malformed input is
refused (including a public key with the wrong header); the library's keys
are canonical and satisfy h·f = g mod q; its signatures verify under
pq-verify's verifier and its own, are in range, and never reuse a nonce. - The public-key range check is now tested so only the decoder can catch
it. The case used to set a coefficient to 0x3FFF, which also changed the
key mod q, so a verifier without the range check still rejected it at the
norm check. A PQClean mutant with the check removed passed. The case now
re-encodes a coefficient asw + q: the same key mod q. tools/vendor_audit.pypins PQClean's Falcon-512/1024 (225/225) and seven
mutants, each failed by the audit in CI; AUDITS.md publishes the table.
Verifying this release
gh attestation verify pq_verify-2.11.0-py3-none-any.whl \
--repo bigDSanalyst/pq-verifyBuilt by .github/workflows/release.yml from commit ec8e5c3a62f935c47d0964c0920862a6973600c1,
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.