Skip to content
#edge computing Open access

pq-verify: Independent verification for ML-KEM / ML-DSA implementations

Oct 2026 · Zenodo (CERN European Organization for Nuclear Research)

Abstract

Fixed A response answering one question twice could verify. A second answer to the same tcId replaced the first, so a response carrying a wrong answer followed by the right one reported VERIFIED. Any question answered more than once is now a finding, whatever the answers say. A truncated .json.gz response crashed the CLI with EOFError instead of reporting CANNOT VERIFY. Readers bound their input. A response larger than 64 MiB (measured after decompression, so a gzip bomb cannot expand in memory) or a hybrid transcript larger than 1 MiB is CANNOT VERIFY, and at most one byte past the limit is read. The largest genuine documents are about 3.3 MB and 8 KB. Freivalds used published seeds. Every Freivalds check drew its random vector from a constant in the source (42, trial + 1, ...), so anyone could compute it and build an NTT output that is wrong in several coefficients yet passes. The seed is now drawn from the OS once per run and printed; PQV_FREIVALDS_SEED=0x... replays a run. The Hasse check rejected genuine curves. It used 2*isqrt(p), which is one short of ⌊2√p⌋ for p = 7, 13, ..., and reported y² = x³ + 3 over F₇ (t = −5) as CRITICAL. The bound is now isqrt(4p). Since no curve can exceed it, a violation is now reported as a defect in pq-verify's point count, not the curve's, and the "near-extreme trace" MEDIUM finding, which is not a known weakness, is gone. Two self-suite checks could not fail. "100,000 NTT butterflies" compared (a + w*b) % q with itself; it now runs every butterfly through the engine's Montgomery multiply and compares with integer arithmetic. "Freivalds throughput" passed unconditionally; it now requires every correct NTT to be accepted. The self-suite is still 160 checks. The Coq certificates proved almost nothing about the run. The "Full Kyber-768 NTT" certificate contained one layer-0 butterfly, and the batch certificate proved sums of random numbers drawn for the purpose. See Changed. Changed --verify-hybrid recomputes the ML-KEM half, or does not say VERIFIED. Nothing tied the ciphertext in the server share to the ML-KEM secret in the combined secret, so a transcript with a corrupted ciphertext was reported VERIFIED. A transcript may now carry the client's ephemeral ML-KEM decapsulation key (clientMlkemDecapsulationKey, optional, like the ECDHE private scalars): pq-verify checks it against the client share and FIPS 203 §7.3, decapsulates the ciphertext and compares the result byte-for-byte. Without the key that check is NOT CHECKED and the result is PARTIAL, so transcripts that verified before without it now report PARTIAL. Coq certificates are real, and checked for axioms. The NTT certificate defines the FIPS 203 forward NTT and zeta table in Coq and proves ntt input = output for all 256 coefficients (896 butterflies), plus 17¹²⁸ ≡ −1 (mod 3329). The batch certificate proves pq-verify's ML-KEM and ML-DSA zeta tables are root^brv(i) mod q as FIPS 203/204 define them. A certificate passes only if coqc accepts it and Print Assumptions reports every theorem closed, so an Admitted proof (which coqc accepts) or an added axiom fails. The "Coq-certified" tagline is replaced by what is actually proved. Added General proofs, pq-verify --proofs. pq_verify/coq/ now holds theorems that quantify over every input, checked by coqc with every theorem required to be closed (no axioms, no Admitted): NTT.v: the FIPS 203 (ML-KEM) and FIPS 204 (ML-DSA) forward NTT equal the Chinese-remainder map they are defined to compute, for every 256-coefficient input. The transform is written once, generic over its arithmetic; a map that preserves the arithmetic commutes with it, so running it once on symbolic linear forms gives its matrix, which Coq checks entry by entry against the CRT matrix. Reduce.v: montgomery_reduce (ML-KEM, ML-DSA), barrett_reduce (ML-KEM, all 65,536 int16 inputs) and reduce32 (ML-DSA) are congruent to their input and within bound for every input in range, with no intermediate overflow. Per-run NTT certificates now emit NTT.v's transform verbatim, so they are about the proved definition. Finding: two documented bounds in pq-crystals/dilithium ref/reduce.c are off by one. montgomery_reduce documents -Q < r < Q for -2^31 Q <= a <= Q 2^31, but a = Q 2^31 returns Q; reduce32 documents r >= -6283008, but a = -255·2^23 - 2^22 returns -6283009. The code is right and no ML-DSA input comes near either point; the comments overstate it. The proofs state the true bounds, and both witnesses are checked theorems. (ML-KEM's comment excludes its corresponding point and is exact.) Wycheproof and CCTV edge-case vectors, pinned. 24 files from C2SP Wycheproof (3fa63dd) and CCTV (50a8ecf) ship in pq_verify/vectors/edge_vectors.json.gz, each with its upstream sha256 in EDGE_MANIFEST.json; tools/pin_edge_vectors.py re-pins them deterministically. They cover what NIST's ACVP vectors mostly do not: strcmp-trap ciphertexts, unlucky XOF sampling, every coefficient value q…4095 at every position of an encapsulation key, corrupted decapsulation keys, malleated ciphertexts, and ML-DSA hint, norm-bound and context edges. --audit-kem runs them against the vendor library as three new stages, edgeValid, edgeEk, edgeDk (edge=False to skip). The pinned vendor table records them: mlkem-native 4,320/4,320; PQClean accepts all 2,931 invalid encapsulation keys and all 6 invalid decapsulation keys while every valid output is byte-exact. pq-verify --edge-cases [SET] runs them against pq-verify's own references (kyber-py, dilithium-py), with --json and --fail-on-finding. The doctor checks the bundle's digests offline and runs the vectors against the installed references (--fast skips that run): a failure not in KNOWN_REFERENCE_DEFECTS BLOCKs. Finding: dilithium-py 1.4.0 accepts a repeated hint index. FIPS 204 Algorithm 21 (HintBitUnpack) requires strictly increasing indices; dilithium-py compares with < instead of <=, so Wycheproof's "repeated hint" signature verifies for ML-DSA-44/65/87. It is fixed upstream (GiacomoPope/dilithium-py bd9b552) but in no release. pq-verify grades third-party signatures against NIST's expected results, not dilithium-py's verdict, so no third-party result changes; --edge-cases reports the reference as FINDINGS PRESENT until a fixed release can be pinned. tests/fuzz_readers.py — a structure-aware fuzzer for the readers that take files from outside parties. It mutates genuine responses and transcripts ~25 ways (type confusion, truncation, deep nesting, oversized and gzipped input, flipped hex digits, duplicate entries) and requires, for every case, no exception, an honest status, prompt termination, a CLI exit of 0/1/2, and no VERIFIED for a document that differs from a genuine one. It found all three bugs above and the hybrid gap. A seeded slice runs in the test suite. Verifying this release gh attestation verify pq_verify-2.9.0-py3-none-any.whl \ --repo bigDSanalyst/pq-verify Built by .github/workflows/release.yml from commit c029a633f46b4400e34b8c3c5cbcb1bbee854dd6, after the full suite and all 855 NIST ACVP vectors passed on Python 3.9 through 3.13. An SPDX SBOM is attached and attested.

View source

Similar papers

#computer vision Review Sep 2017

Agile Software Development Methods: Review and Analysis

This publication proposes a definition and a classification of agile software development approaches and analyses ten software development methods that can be characterized as being "agile" against the defined criterion.

P. Abrahamsson, O. Salo, Jussi Ronkainen et al. · 727 citations · ⚡54
#computer vision Jun 2008

The impact of agile practices on communication in software development

The study shows that agile practices improve both informal and formal communication, but indicates that, in larger development situations involving multiple external stakeholders, a mismatch of adequate communication mechanisms can sometimes even hinder the communication.

M. Pikkarainen, Jukka Haikara, O. Salo et al. · 401 citations · ⚡48
#machine learning Review Open access Oct 2014

Software development in startup companies: A systematic mapping study

The results indicate that software engineering work practices are chosen opportunistically, adapted and configured to provide value under the constrains imposed by the startup context.

Nicolò Paternoster, Carmine Giardino, M. Unterkalmsteiner et al. · 394 citations · ⚡54

Related blog posts

Microsoft Research Blog Oct 6, 2026

What AI gets wrong and what failure teaches us

Jennifer Neville did not want to go into computer science—but that’s exactly where she landed. Neville discusses the starts and stops that led to her professional sweet spot and her work identifying “surprising failures” making it hard for AI to handle complexity.  The post What AI gets wrong and what failure teaches us appeared first on Microsoft Research.

We use cookies to run the site and, with your consent, for analytics and to show ads. See our Cookie Policy.