
Full ML-KEM-1024 key recovery from a partial Fujisaki-Okamoto comparison in wolfSSL (CVE-2026-6330 NEON, CVE-2026-10097 AVX2)
CVE-2026-6330 (NEON) and CVE-2026-10097 (AVX2). Fixed in wolfSSL 5.9.2.
The full technical write-up is published as
Cryptology ePrint Archive, Paper 2026/1682
(local copy in paper/). See Citation below.
ML-KEM (Kyber, FIPS 203) resists chosen-ciphertext attacks because of one check inside decapsulation: the receiver re-encrypts the message it just decrypted and returns the real shared secret only if the result matches the input ciphertext exactly. Any mismatch triggers implicit rejection, and a pseudorandom value is returned instead. That check is the Fujisaki-Okamoto transform, the single gate separating the IND-CCA2 scheme from the malleable IND-CPA scheme underneath.
wolfSSL implemented that comparison in hand-written assembly, and on two backends it compared only part of the ciphertext. On ARM64 it compared about half, which () found and reported (). On x86-64 it compared 1536 of the 1568 bytes in ML-KEM-1024 ().
On either backend a tampered ciphertext is accepted as genuine, which was reported as a
weakening of IND-CCA2 security. This write-up shows the flaw goes further, on both
backends. The bytes the check skips leak the decapsulation's decryption noise, and that
noise is a linear function of the secret key, so the key can be recovered by
least-squares regression. It is not a
lattice problem, and it is not the classical key-mismatch attack: on the AVX2 backend the
flaw validates all of u, so the chosen-u ciphertexts those attacks require are rejected.
The decryption-noise regression instead recovers the key from the unchecked v-coefficients.
The full ML-KEM-1024 private key is recovered end-to-end against the live vulnerable binary
on both the AVX2 and NEON backends.
The attack needs a reused ML-KEM key: HPKE recipients, KEMTLS, or pinned and embedded keys. An ephemeral hybrid TLS 1.3 key share uses a fresh key per handshake and is not key-recoverable this way; there the flaw is only a distinguishing break. Both flaws are fixed and public, and this is a post-disclosure write-up.
| NEON (CVE-2026-6330) | AVX2 (CVE-2026-10097) | |
|---|---|---|
| Backend | ARM64 NEON | x86-64 AVX2 |
| Flaw | compares ~half the ciphertext | compares 1536 of 1568 bytes |
| Bug reported by | Nicholas Carlini (Anthropic) | 007bsd |
| CVE severity | Medium (CVSS 4.0 6.3, CWE-327) | High (CVSS 4.0 8.3, CWE-697) |
| Key recovery, model | full (~500 ct) | full (~1300 ct) |
| Key recovery, live binary | 98.5% @ 600 ct (QEMU-emulated) | 98.1% @ 350 ct (native) |
| Fix | PR #10192 | PR #10430 |
"live" is the demonstrated result on the real binary: NEON needs more ciphertexts (600 vs 350) because its per-coefficient measurement is far noisier (about 4x). "model" is a noise-free check that the regression recovers the full key (all 2048 coefficients, 100%); its ciphertext count is set by how many equations each ciphertext yields, not by noise, so it is not comparable to the live figure and does not reflect attack difficulty.
Both flaws were found independently in the same wolfSSL release window and fixed together
in 5.9.2 (see the wolfSSL
security vulnerabilities page for
both CVE entries). Carlini's NEON bug was documented as an IND-CCA2 weakening, and the AVX2 bug is
CVE'd as key recovery. The decryption-noise regression attack in this repository recovers
the key on both. Backend-specific detail is in
neon-cve-2026-6330/ and
avx2-cve-2026-10097/.
An incomplete comparison in wolfSSL's ML-KEM implicit-rejection check, present in two SIMD assembly backends. It compared fewer than all of the ciphertext bytes, so decapsulation accepts ciphertexts that a correct implementation rejects. The unchecked bytes turn decapsulation into an oracle for the decryption noise, and that is enough to recover the private key when the key is reused.
You are affected if you used wolfSSL with ML-KEM on a build with the vulnerable SIMD assembly: AVX2 (x86-64) in 5.7.0-5.9.1, or ARM64 NEON in 5.7.4-5.9.1. The key-recovery attack additionally needs the ML-KEM private key to be reused across decapsulations, as with HPKE recipients, KEMTLS, or a pinned or embedded key. An ephemeral hybrid TLS 1.3 key share uses a fresh key per handshake and is not key-recoverable; there the flaw is only a distinguishing break. The fix is in wolfSSL 5.9.2.
Recover the entire ML-KEM-1024 secret key on either backend, by sending crafted decapsulation queries and observing, for each, whether the real secret or a reject value comes back. Against the live binaries this recovered the key with roughly 10^5 to 10^6 decapsulation queries.
Take one ciphertext coefficient in the unchecked region. Its plaintext bit is decided by
rounding, m'_j = Compress_1(v_j - (s^T u)_j). Because those bytes are not compared,
sweeping the compressed value of v_j and watching the one decapsulation output that
changes locates a rounding boundary, and the boundary position measures the decryption
noise δ_j to a precision of about ±q/64. In terms of the secret (s, e) and the
encryption randomness the attacker chose:
δ_j = ( e^T y - s^T(e1 + c_u) + e2 + c_v )_j (verified exact; small; never wraps mod q)
That is a linear equation in the 2048-coefficient secret (s, e), with coefficients the
attacker knows. Each ciphertext gives one such equation per unchecked coefficient. Stacking
enough ciphertexts and solving the normal equations recovers the whole key, and the last
few coefficients are pinned by the public-key relation e = t - A·s ∈ CBD(η). It is
ordinary least squares, not a
lattice problem.
The two backends differ in how much they leak. NEON leaves about 50% of the ciphertext unchecked (125 coefficients in three bands), against about 2% for AVX2 (51 coefficients in one band). NEON still needs more ciphertexts, because its unchecked bytes are non-contiguous and its per-coefficient measurement is noisier, so leaking more does not make the recovery easier here.
Plaintext-checking and key-mismatch attacks (Ravi et al. 2020, Qin et al. 2021, Băetu et
al. 2019) recover the key in a few thousand queries by submitting ciphertexts with
attacker-chosen sparse u to isolate one secret coefficient per query. The AVX2 flaw
rejects all of them: it skips only 32 bytes of v and validates the whole of u, so on
the live binary a sparse-u ciphertext is rejected and flipping a single u bit is
rejected 30 times out of 30. The NEON flaw is broader, leaving about half of u unchecked
as well, so that argument is AVX2-specific; but the attack does not rely on it. The
decryption-noise regression recovers the key from the unchecked v-coefficients on both
backends, surviving the part of the check each bug leaves intact.
The relevant precedent, then, is not those key-mismatch attacks but decryption-failure attacks (D'Anvers et al. 2019, ref 6). They exploit the same decryption-noise term, a linear function of the secret, but only through the rare event of that term crossing the decoding boundary and causing a decryption failure, so recovery there is statistical and needs vastly more ciphertexts. The incomplete comparison instead makes that same term directly measurable, to about ±q/64, so no failures are needed and the attack collapses into the ordinary least-squares regression above.
In a faithful reference model (kyber-py) for both backends, the identity above is verified exact and regression recovers all 2048 secret coefficients. Against the live pre-fix wolfSSL binaries, attacking a reused exported key, each harness runs a self-check against the oracle's exported ground-truth key before it reports any number:
(s, e) at 400 ciphertexts. The secret s alone is 1005/1024 = 98.1% at
350 ct, the figure recorded in the CVE.(s, e) at 600 ciphertexts, with the solver error converging
monotonically toward exact.The recovered key is the wolfSSL-generated keypair the oracle exports, and the attack runs
against the shipped-form SIMD assembly rather than a model of it. Full transcripts:
avx2-cve-2026-10097/live_recover_avx2.out,
neon-cve-2026-6330/live_recover_neon.out.
High, though not catastrophic. It needs a reused key, an accept/reject oracle, and a large but practical number of queries. It is not a side channel: there is no timing or power measurement, only a logic bug in a comparison. It does not affect correct implementations or the ML-KEM standard itself. For comparison, a compiler-induced timing leak in liboqs's ML-KEM decapsulation (CVE-2024-36405), a related full-secret-key-recovery issue from the 2024 wave of ML-KEM timing leaks alongside KyberSlash, was rated CVSS 7.5 by NIST.
Per-backend attack code, analysis, and reproduction steps are in
avx2-cve-2026-10097/ and
neon-cve-2026-6330/.
This repository documents two sibling findings from the same wolfSSL release window, and one attack that breaks both.
Both issues were reported privately, fixed, and assigned CVEs before this write-up. Fixed in wolfSSL 5.9.2.
The full write-up is published as
Cryptology ePrint Archive, Paper 2026/1682
(local copy: paper/).
@misc{cryptoeprint:2026/1682,
author = {Bhabani Sankar Das},
title = {Incomplete Ciphertext Comparison in {ML}-{KEM}: From an {IND}-{CCA2} Break to Key Recovery},
howpublished = {Cryptology {ePrint} Archive, Paper 2026/1682},
year = {2026},
url = {https://eprint.iacr.org/2026/1682}
}