Skip to content

pq-verify v2.11.0

Choose a tag to compare

@github-actions github-actions released this 07 Oct 20:05
ec8e5c3

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) and f·G − g·F = q must
    hold in ℤ[x]/(xⁿ+1); both Gram–Schmidt norms must be below 1.17²·q:
    ‖(g, −f)‖² and q²·[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-time runs 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. Stages ct:encaps, ct:decaps; the report's side_channel
    gains constant_time_checked with 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; memcmp in 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-dsa and --audit-harness; --accumulated N
    chooses 100 or 10 000, 0 skips. 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/mlkem and CIRCL. The vendor-audit tables gain the column.

Added — audit any language through Crucible-protocol harnesses

  • --audit-harness COMMAND PARAM_SET audits 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.py rebuilds Crucible's Go crypto/mlkem and 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-1024 audits a vendor's FN-DSA
    (Falcon) library through its own entry points (NIST API / PQClean, or
    liboqs OQS_SIG_falcon_*, padded variants included; --fndsa-symbol ROLE=SYMBOL when 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 as w + q: the same key mod q.
  • tools/vendor_audit.py pins 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-verify

Built 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.