Skip to content

pq-verify v2.10.0

Choose a tag to compare

@github-actions github-actions released this 07 Oct 04:28
5f6e4c5

Changed — read before upgrading

  • Failing results now fail the exit status by default. 0 verified, 1 a
    task found problems or could not verify, 2 bad input. Before, every task
    exited 0 unless --fail-on-finding was passed; the GitHub Action did not
    pass it to --acvp-all, so a failed ACVP run published verified=true.
    --no-fail restores report-only behaviour; --fail-on-finding is 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 not verified when a requested suite did not
    run.
    A missing kyber-py dropped ML-KEM from both sides of the ratio and
    the API reported verified: True; the result now lists not_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-timeout used to be ignored silently.
  • --emit-prompt without --fresh-key says 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 --fndsa checks 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.py regenerates them from a checkout and refuses
    output that does not match; --check and tools/doctor.py re-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=native retried 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.04 ahead of ubuntu-latest moving 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.h gains three optional functions
    (pqv_hbs_state_keygen, pqv_hbs_state_sign, pqv_hbs_state_skip,
    capability PQV_HBS_CAN_STATE) through which the library manages its own
    key in a file. --audit-hbs adds a <scheme> state stage per sampled key:
    the key matches pqv_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.py pins 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 K derives every input from a 256-bit seed
    written only to K (0600, never overwritten) and writes the questions in
    NIST's ACVP layout, so an ACVP harness answers them unchanged;
    --verify-response R --fresh-key K re-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-count sets 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 is CANNOT VERIFY with 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_environment and isolation; a file that changes mid-audit is
    CANNOT VERIFY. PQV_IN_PROCESS=1 runs in-process, for debugging.
  • scope on --audit-dsa and --audit-hbs reports: 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 | ntt and param-set drive the
    vendor audits; every input reaches the shell through env:, closing a
    script-injection path through library, symbol and the file inputs; the
    ACVP step is gated and writes a JSON report; verified is derived from
    every task that ran.

Security

  • The release workflow and the Action pin every action to a commit SHA (the
    release job holds contents: write and the OIDC identity PyPI trusts).
    The release verify job also runs doctor.py and --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).
  • LMS/XMSS vendor audits in CI, with mutants. cisco/hash-sigs
    44e6c7d and xmss-reference 171ccbd are pinned in
    tools/vendor_audits.json with 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.py implements 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-all and --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-xmss runs 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-full builds 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.
  • --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-abi and --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 of randombytes(), it lets pq-verify
      serve NIST's seed and rnd, so the randomised keypair() 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.
  • ML-DSA vendor audits in CI, with mutants. tools/vendor_audits.json
    pins mldsa-native 159509d, pq-crystals dilithium ref d35ba3f and
    PQClean 0586a82, 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's additionalRandomness.
      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's sign() 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
      additionalRandomness misses every randomised case.
  • --slhdsa-acvp runs 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-all and pqverify_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-dsa reference was already part of pq-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-Server 112690e) 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 --apply rewrites 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 slhdsa input did nothing. It was declared but
    never read. SLH-DSA keyGen and sigVer now run with the ACVP suites, and
    slhdsa: siggen adds 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-finding would pass on SLH-DSA's 624/624 alone. A
    suite that did not run now makes the report CANNOT VERIFY and is listed
    under summary.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
    install slh-dsa and fail unless all three ACVP suites ran, because
    --fail-on-finding alone 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-verify

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