An Incomplete FO Check Turned wolfSSL’s ML-KEM Into Key Recovery
Table of Contents
15 Aug 2026 – On 15 August 2026, the IACR ePrint archive posted a preprint by researcher Bhabani Sankar Das demonstrating near-complete ML-KEM-1024 private-key recovery against shipped wolfSSL binaries. The vulnerabilities had already been patched in wolfSSL 5.9.2, released 23 June 2026.
The paper, “Incomplete Ciphertext Comparison in ML-KEM: From an IND-CCA2 Break to Key Recovery,” targets two bugs in wolfSSL’s hand-written SIMD assembly code for the Fujisaki–Okamoto (FO) ciphertext comparison, the single check that gives ML-KEM its chosen-ciphertext (IND-CCA2) security.
On the x86-64 AVX2 code path, the constant-time comparison function mlkem_cmp_avx2 compared only 1,536 of the 1,568 bytes in an ML-KEM-1024 ciphertext, skipping the final 32 bytes entirely (CVE-2026-10097, rated High at CVSS 4.0 8.3, CWE-697). On the ARM64 NEON code path, an instruction error (ins where ext was intended) caused the comparison to cover roughly half the ciphertext, leaving 784 of 1,568 bytes unchecked (CVE-2026-6330). wolfSSL’s own 5.9.2 release notes tagged both as Medium, and public vulnerability trackers score the NEON record inconsistently.
The NEON bug was independently discovered and reported by Nicholas Carlini of Anthropic. The AVX2 bug was reported by Das under the handle @007bsd. Both bugs affected wolfSSL versions 5.7.0 through 5.9.1 on their respective platforms (the NEON path specifically 5.7.4 through 5.9.1), spanning roughly two years of releases.
The AVX2 CVE record already described the bug as enabling key recovery. Das’s paper shows the same is true for both bugs and explains the mechanism: the unchecked ciphertext bytes encode the compressed tail of the decryption noise, which is an exact linear function of the secret key. By varying the unchecked bytes and observing the decapsulation output, an attacker can measure the noise one coordinate at a time, build an overdetermined linear system, and solve for the secret key using ordinary least squares. No lattice reduction is needed.
Against the shipped vulnerable binaries, the attack recovered 98.0% of the 2,048 secret coefficients using 400 chosen ciphertexts on AVX2 (native x86-64) and 98.5% using 600 ciphertexts on NEON (ARM64 under QEMU emulation). Against a verified Python reference model, full recovery of all 2,048 coefficients required approximately 1,300 ciphertexts. The remaining coefficients are recoverable with more ciphertexts because the noise has zero mean: each new observation reduces the error, and the estimate approaches the true coefficient. Each ciphertext requires multiple decapsulation queries, a binary search over quantization levels for each unchecked coordinate, putting the total query budget at 10⁵ to 10⁶ decapsulations per key.
The attack requires a static (reused) ML-KEM key and a plaintext-checking oracle, constructed in the paper against an exported keypair. The paper argues but does not demonstrate end-to-end that the same bit leaks through a fielded HPKE or KEMTLS AEAD path. Ephemeral hybrid TLS 1.3 handshakes, which draw a fresh key per session, are not vulnerable to key recovery; the bug is only a distinguishing break in that setting. Static-key deployments are the ones at risk: HPKE static recipients, KEMTLS long-term keys, and pinned or embedded keys common in IoT and embedded devices.
wolfSSL claims its products secure over five billion applications and devices across IoT, automotive, industrial automation, government, and military markets. The wolfCrypt cryptographic module holds FIPS 140-3 certificates #4718 and #5041, though those validated modules do not include ML-KEM (FIPS 203); the comparison bugs are in the open-source AVX2 and NEON assembly paths.
Das released full attack code and reproduction instructions at github.com/007bsd/ml-kem-key-recovery. The paper’s conclusion: an incomplete FO comparison is a key-recovery vulnerability and should be triaged as one.
My Analysis
ML-KEM the algorithm is fine. wolfSSL’s hand-written assembly was not. This is the cleanest demonstration I have seen that a partial FO comparison gives up the whole key, where the advisories recorded only a weakened CCA guarantee. One missing check in one comparison function turned a formally secure scheme into a key-recovery target reachable with a few hundred chosen ciphertexts, swept into roughly a million decapsulation queries against a static key.
What the FO Check Does and Why 32 Missing Bytes Broke It
ML-KEM’s entire chosen-ciphertext security depends on one operation inside decapsulation. After decrypting the incoming ciphertext, the receiver re-encrypts the recovered message with the same randomness and checks whether the result matches the ciphertext it received, byte for byte. A match returns the real shared secret. Any mismatch triggers implicit rejection: a pseudorandom secret is returned instead, and the attacker learns nothing. This is the Fujisaki–Okamoto transform, and the comparison is the whole of it in practice.
wolfSSL implemented that comparison in hand-written SIMD assembly for performance. On the AVX2 path, the function stopped 32 bytes short. On the NEON path, an ins instruction where an ext was intended discarded the high 64 bits of the 128-bit comparison accumulator after each deinterleaving step, leaving about half the bytes unchecked.
The consequence is more severe than the advisories suggested. wolfSSL tagged both CVEs as Medium, and the NEON CVE described only an IND-CCA2 weakening. The unchecked bytes are why.
An ML-KEM-1024 ciphertext is 1,568 bytes: 1,408 for the compressed $$u$$ component and 160 for the compressed $$v$$ component. The skipped bytes on AVX2 are the tail of $$v$$, roughly the last 51 compressed $$v$$-coefficients. Those bytes encode the compressed residual of the decryption noise, which is an exact linear function of the secret key.
Das proves this cleanly (Lemma 1 in the paper). After the mask cancels, the quantity the decoder thresholds to recover each message bit is: $v’_j – (s^\top u’)_j = \text{Decompress}_1(m)_j + \delta_j$
where $$\delta_j = (e^\top r – s^\top(e_1 + c_u) + e_2 + c_v)_j$$. Every term in $$\delta_j$$ except the secret $$(s, e)$$ is fixed by the attacker’s chosen ciphertext and publicly computable. The secret enters linearly, with known coefficients.
Because the v-tail bytes are unchecked, the attacker can set the compressed value of $$v_j$$ to any of its 32 quantization levels without triggering implicit rejection. Sweeping those levels shifts the decoder argument past the threshold, and the attacker measures $$\delta_j$$ from the transition, to a precision of about $$\pm q/64$$. Each unchecked coordinate contributes one equation per ciphertext.
The AVX2 bug exposes 51 coordinates. With 2,048 coefficients and 51 equations per ciphertext, the system needs $$\lceil 2048/51 \rceil = 41$$ ciphertexts before it is even determined, and reaches full rank at about 45. 5-bit compression noise adds uncertainty to each measurement, so the live-binary recoveries in Table 1 need a few hundred ciphertexts to average it out. Ordinary least squares solves the system; the 41-ciphertext floor is a theoretical minimum, not an achievable shortcut.
The attack exploits a logic bug in a comparison function, purely over the network, without timing leaks or physical access. It gives the attacker a plaintext-checking oracle that survives even when the $$u$$ component is fully validated, as it is on AVX2. The standard hardening against key-mismatch attacks (validate all of $$u$$) does not close this route. The oracle reads off the $$v$$-tail, which full $$u$$-validation leaves untouched. That is the paper’s primary contribution: reach into a regime where the known mitigations do not help.
One counterintuitive result from the paper’s ablation study: the NEON bug leaves about 2.5× more coordinates unchecked than AVX2 (125 vs. 51) yet needs more ciphertexts for recovery. The reason is precision. NEON’s per-coordinate measurement noise is about $$\pm q/32$$, twice the AVX2 figure of $$\pm q/64$$. More leaked coordinates lower the rank floor, but noisier measurements raise the ciphertext count by a larger factor. More leakage does not mean an easier attack.
The wolfSSL Footprint
wolfSSL is one of the most widely deployed embedded TLS libraries: lightweight, ANSI C, designed for microcontrollers, RTOS environments, and resource-constrained hardware. It ships in smart grid infrastructure, automotive systems, industrial controllers, VoIP equipment, routers, and military and government systems. wolfSSL’s own marketing claims over five billion applications and devices secured. Even discounting for marketing arithmetic, the deployment base is large. Much of it is IoT, embedded systems, and firmware that ships for years. Those are the deployments that reuse a key for years and update firmware least often. All 10⁵ to 10⁶ decapsulations must target one key before it rotates, so the strongest attack case is a pinned or embedded key that never rotates.
wolfSSL also holds FIPS 140-3 validation (certificates #4718 and #5041) and markets itself as “the world’s first cryptography provider supporting CNSA 2.0 compliance.” Those FIPS-validated modules do not include ML-KEM; the comparison bugs are in the open-source SIMD paths, and whether the FIPS-approved codebase ships the same assembly is undetermined. wolfSSL removed the liboqs, liblms, and libxmss integrations in version 5.9.2, the same release that patched the comparison, so its ML-KEM implementation is now wholly wolfSSL’s own code. That independence is real, and it produced the bug. Both defects are in comparison routines wolfSSL wrote by hand for its SIMD backends, and neither was inherited from upstream reference code.
The affected versions (5.7.0 through 5.9.1 for AVX2, 5.7.4 through 5.9.1 for NEON) span roughly two years of releases, from March 2024 to April 2026. Exposure requires ML-KEM actually compiled in, which in the earlier releases meant building with --enable-experimental. Builds without that flag are unaffected, so the version range overstates exposure at the early end. Deployments running ML-KEM on an unpatched build are exposed. Firmware update cycles in embedded devices are measured in quarters or years, which is longer than the months since 5.9.2 shipped.
Carlini, Mythos, and the NEON Bug
The NEON bug (CVE-2026-6330) was independently discovered and reported by Nicholas Carlini of Anthropic. wolfSSL’s 5.9.2 release notes credit Carlini for the report, and Das’s paper acknowledges the independent discovery in its final section.
Earlier in 2026, Carlini used Anthropic’s Mythos model to find CVE-2026-5194, a critical certificate-verification flaw in wolfSSL involving missing hash, digest-size, and object-identifier checks during certificate signature verification. wolfSSL confirmed in an April 2026 blog post that Anthropic had run Mythos against its codebase. Anthropic grants Mythos Preview access through Project Glasswing, its gated program for applying frontier models to security-critical software, though the CVE credit lines for the wolfSSL findings do not name the program.
Whether the NEON ML-KEM bug was also found via Mythos is not publicly stated: the release notes credit Carlini without mentioning tooling. What is publicly documented is that Das and Carlini independently converged on the same library’s ML-KEM comparison code within the same disclosure window. Hand-written cryptographic assembly is now an obvious place to look for both human and automated analysis. The practical question for organizations is whether their own PQC implementations have had equivalent scrutiny.
KyberSlash Rhymes
ML-KEM implementation bugs have surfaced across production libraries before. In December 2023 and early 2024, researchers disclosed KyberSlash: secret-dependent division timing in the decapsulation path of multiple Kyber implementations, including code derived from the official reference. Daniel Bernstein reported key recovery against end-of-2023 reference code on a Raspberry Pi 2.
The two episodes share a pattern and diverge on mechanism. KyberSlash was a timing side channel, the class of bug that constant-time coding practices are designed to prevent and that many teams watch for. The wolfSSL FO-check bugs are logic errors in SIMD assembly – a class of bug that timing-analysis tools will not catch, because the code is constant-time. It just compares the wrong number of bytes.
Classical cryptographic engineering learned this with RSA, AES, and elliptic-curve implementations over decades: sound mathematics does not guarantee correct code. PQC implementations are newer and less audited. Embedded and IoT systems are adopting them first, where patches arrive slowly and implementation flaws persist for years.
In 2024, PQShield’s Antoon Purnal reported that recent Clang versions recognised the reference implementation’s poly_frommsg function as a bit test and generated a secret-dependent branch absent from the source. The source was constant-time; the binary was not. That is a third failure mode, with the compiler as the source. Between developer logic errors (wolfSSL), inherited reference-code flaws (KyberSlash), and compiler-induced leakage (PQShield), the surface for PQC implementation bugs is broader than many migration plans assume.
What This Means for PQC Migration
I have spent the past three years arguing that PQC migration is the largest and most complex infrastructure project most organizations will undertake. This paper reinforces the part of that argument I find hardest to get across in boardrooms: the risk is not only that a cryptographically relevant quantum computer arrives before you are ready. The risk is also that the post-quantum algorithms you migrate to contain coding flaws that remain in your stack for months or years before anyone finds them.
Regulatory deadlines for PQC migration are already set. CNSA 2.0 timelines, the EU roadmap’s 2030 milestone for high-risk systems, and sector-specific mandates in finance and government are driving organizations to adopt ML-KEM now. They are right to do so. But adopting ML-KEM means choosing an implementation, and the wolfSSL paper shows what happens when that implementation gets one comparison function wrong.
Three concrete actions follow from this.
First, check your wolfSSL version. If you are running ML-KEM on 5.7.0 through 5.9.1 on x86-64 or 5.7.4 through 5.9.1 on ARM64, the FO check is incomplete and static-key deployments are exposed. Update to 5.9.2. The attack needs no quantum computer. It runs on commodity hardware and requires only sustained access to a decapsulation oracle.
Second, treat the FO comparison as a first-order audit target in any ML-KEM implementation you deploy. The specific test Das recommends is a sweep of single-byte mutations confined to the ciphertext tail: if any of them fail to trigger implicit rejection, the comparison is incomplete. This is a test that belongs in your cryptographic inventory and your vendor questionnaire.
Third, build crypto-agility into the migration architecture from the start. The wolfSSL bugs were logic errors in one library, found and fixed within a responsible-disclosure cycle. The next implementation bug may be in any library, on any platform, in any algorithm. Organizations that hard-code a single implementation into firmware that ships for a decade are building the same kind of monoculture risk that made Heartbleed so expensive. A cryptographic bill of materials that tracks which ML-KEM implementation runs where, derived from which codebase, compiled with which toolchain, is not bureaucratic overhead. It is the mechanism that turns a CVE advisory into an actionable remediation list.
The Correct Frame
ML-KEM is not broken. The algorithm’s mathematical security is unaffected by this paper. FIPS 203 is sound. What broke was one library’s assembly-language implementation of a comparison function, on two SIMD backends, in code that was not inherited from the reference but written independently for performance.
wolfSSL is not niche. It is one of the most widely deployed embedded TLS libraries in the world, with FIPS 140-3 validation. Much of its deployment base covers long-lived, hard-to-patch devices where static keys are common and implementation flaws persist longest.
The paper is precise about what it shows and what it does not. The live-binary recovery reaches 98% of the secret; the remaining 2% is recoverable with additional ciphertexts, since the measurement noise is zero-mean and the stranded coefficients are an averaging problem that more ciphertexts resolve. The oracle is constructed against an exported keypair; leakage of the same plaintext-checking bit through a fielded HPKE or KEMTLS AEAD path is argued but not demonstrated end-to-end. The leakage-geometry comparison uses one key, with spot-checks on a handful of others. The mathematical identity and the rank floor are exact and unaffected by these limits. Additional ciphertexts would extend the empirical reach; the attack’s capability is not in question.
The bug was found, disclosed responsibly, patched, and now documented in enough detail for every other ML-KEM implementer to test their own comparison routines against the same class of error. Das does not test other libraries. Every library maintainer deploying hand-optimized SIMD code for PQC should test their own FO comparison routines for completeness before someone else does.