Also mentioned
ML-KEMML-KEMModule-Lattice-based Key Encapsulation MechanismModule-Lattice-Based Key-Encapsulation Mechanism, the NIST-standardized post-quantum KEM derived from CRYSTALS-Kyber and specified in FIPS 203.Read the full entry (new tab), ML-DSAML-DSAModule-Lattice-based Digital Signature AlgorithmModule-Lattice-Based Digital Signature Algorithm, the NIST-standardized post-quantum signature scheme derived from CRYSTALS-Dilithium and specified in FIPS 204.Read the full entry (new tab), SLH-DSASLH-DSAStateless Hash-based Digital Signature AlgorithmStateless Hash-Based Digital Signature Algorithm, the NIST-standardized signature scheme derived from SPHINCS+ and specified in FIPS 205.Read the full entry (new tab), KEMKEMkey encapsulation mechanismA public-key mechanism for securely establishing a shared secret key: the sender encapsulates a random secret to the recipient's public key, and the recipient decapsulates it with their private key.Read the full entry (new tab), FIPSFIPSFederal Information Processing StandardFederal Information Processing Standards, publicly announced standards developed by NIST for use in U.S. government computer systems, including cryptographic algorithms and modules.Read the full entry (new tab) are defined in the glossary.
Read this first
Two reviews of @noble/post-quantum, eighteen findings, and not one of them is a break in the maths. Round one: six defects, all merged within a day, the worst an unbounded signing loop in Falcon. Round two, over the repaired code: twelve more, eight of which merged in pull request 49 on 21 August. Rechecked on 12 September: sixteen of eighteen are resolved as of 0.7.1, published 27 August. Seed retention for portable private-key export and the missing public warning on the custom composite signer remain open. The full review is at quantakrypto.com/audits/paulmillr/noble-post-quantum.
If you install a post-quantum library in JavaScript today, there is a good chance it is this one. @noble/post-quantum implements ML-KEM, ML-DSA, SLH-DSA and Falcon in pure TypeScript, with no native dependency and no WASM blob, which is exactly why it ends up in browser bundles and edge runtimes where the alternatives cannot go.
We reviewed it in August, twice. The headline is easy to state and worth stating first: the primitives are correct. Every FIPS 203 input-validation check is present. The FIPS 204 rejection bounds and hint canonicality are right. All twelve SLH-DSA parameter sets are correctly parameterized. X-Wing matches its draft byte for byte. Across eighteen findings there is no key recovery, no forgery, and no plaintext disclosure.
Everything we found lives at the edges: in the options a caller passes, in the paths the tests do not reach, and in the gap between what the README promises and what the code does.
Round one: a signing loop that never ends
The first review, on 15 August, turned up six defects. The one that mattered was in Falcon, and it is not a cryptographic break: it is an availability bug that makes sign() hang forever.
Falcon's sampler mutated two of its arguments in place. At the top sampling level those arguments are the sampler's own persistent state, so after the first rejection they held the wrong values, and the loop that was supposed to retry could never terminate. The detail that makes this a good finding rather than a lucky one: the same function already copied a third argument defensively, with a comment explaining exactly why. Two of the three were missing the same protection the third had been given.
- A hybrid decapsulation path left expanded child secret keys in memory when a child KEM threw, reachable from an attacker-supplied ciphertext.
- K-PKE decryption wiped three buffers but not the polynomial holding the decrypted message, one threshold away from the plaintext itself.
- An SLH-DSA conformance test used
returnwhere its two ML-DSA siblings usecontinue, so the first vector below the security level ended the whole suite while it still reported success. - A README promising a non-aliased copy from an API whose own source comment says it returns a view into the caller's buffer.
Paul Miller merged four of them in pull request 48 within two hours of receiving the report, and the other two the same day. That is a faster turnaround than most vendors manage for an acknowledgement.
Round two: reading the repaired code
Six days later we went back over the fixed tree, this time as four independent reviews covering FIPS conformance, interoperability, developer experience and implementation security. Twelve more findings, none of them in the primitives either.
Two findings concerned documented API paths. At the follow-up review, externalMu was presented as a public option although neither public entry point supported it. Version 0.7.1 resolves that contract: the README identifies it as internal-only, and public calls reject it explicitly. The seed-retention issue remains: synchronous ML-DSA and ML-KEM keygen() return expanded keys without their generated 32-byte or 64-byte seeds. Callers needing seed-based private-key export must generate and retain the seed themselves; that requirement still needs prominent documentation.
Then there is the option validator. Unknown keys were silently ignored, so a misspelled context removed a domain-separation parameter the caller believed was applied, with no signal at any layer. A one-character typo, and the security property is simply gone.
Two findings were in the test suite rather than the library, and they are the kind that hides the others. Two Wycheproof families covering the expanded-key import path were being skipped, which is the exact path a key arriving from liboqs, Bouncy Castle or Java 24 travels. And a failed vector download cached an empty file, after which the suite passed while testing nothing at all.
The part where the maintainer audits the auditor
We sent round two as patches rather than as a list. Eight of the twelve merged on 21 August in pull request 49, six commits covering the option validator, the pre-hash digest length, zeroization of library-generated secrets, the encapsulate documentation, a uniform Falcon rejection, and the skipped test vectors.
What happened next is the reason we are writing this up as a story rather than a scorecard. Miller read the patches and found two defects in them, both in fixes we had written:
- Our
checkOptKeys()only examines an object's own enumerable properties, while thevalidateOpts()it is meant to guard accepts class instances, inherited properties and non-enumerable ones. The check is narrower than the surface it protects. - In ML-KEM encapsulation the random
mis generated before the public key is validated, and wiped only on success. A malformed public key therefore throws aftermhas been allocated and before anything clears it. Key generation and prepared encapsulation follow the same success-only cleanup pattern, so the same shape of gap exists in three places.
merging as is, then fixing in main branch— Paul Miller, on pull request 49
The second one is a nice illustration of how zeroization findings behave. We reported that the library never wipes its own generated seed and encapsulation message. The fix wipes them. The remaining gap is not whether they are wiped but when, on which path, and the error path is the one that was still uncovered. A fix that is right in the happy case and wrong in the failure case is exactly the residue this class of bug leaves behind.
He fixed both forty-seven minutes later, in a commit called Hardening. That revision made checkOptKeys walk the prototype chain with Reflect.ownKeys. The released 0.7.1 implementation subsequently adopted frozen, null-prototype snapshots of enumerable own options and rejects custom-prototype option bags, keeping later validation consistent with the values actually consumed. ML-KEM keygen, encapsulate and prepared encapsulation each moved their cleanup into a finally, so the generated randomness is wiped whichever way the call ends.
Round two took six days from report to merged patches, and the two defects in those patches took under an hour from report to fix. We are recording that because it is the part of an audit nobody publishes: the review is only as good as the reviewing that happens after it, and here that ran in both directions.
What is still open, and what has shipped
Two findings remain open after our 12 September recheck: synchronous key generation still discards the seed without prominent retention guidance, and combineSigners still lacks a public warning that it is a custom, non-standard scheme. The externalMu contract is fixed, and the 0.7.1 manifest now pins all three runtime dependencies exactly to 2.4.0.
Version 0.7.1 was published on 27 August and includes the previously unreleased fixes. We verified the npm tarball against registry integrity metadata, inspected the shipped implementation and tagged test changes, and reran focused API checks. All six first-round findings and ten of twelve follow-up findings are resolved as of this release. This was a remediation check, not a new full cryptographic audit.
The point
The interesting finding of this review is not any single defect. It is where all eighteen of them were: never in the lattice arithmetic, always in the seam between the library and the person calling it. An option that is dropped, a format that cannot be exported, a test that passes on an empty file, a secret that is wiped on the success path only. Formal verification of the primitives would have caught none of them.
It is also the most useful review we have run, precisely because the maintainer pushed back on the patches. An audit that ends when the report is delivered gets you a list. One that continues into the diff gets you a second reviewer for your own work, and in this case it found two defects in ours before a single user could install them.
References
- The full review at quantakrypto.com, with every finding, its evidence and its status
- Pull request 48 and pull request 49 on github.com/paulmillr/noble-post-quantum
- FIPS 203, FIPS 204 and FIPS 205 at csrc.nist.gov
This is what an implementation audit looks like when it runs properly, and it is the kind of reading we do. If you maintain post-quantum code and want it read this closely, talk to us. We publish the findings and the merges, and we are happy to be corrected in public.