Skip to main content
Key Recovery in 600 Queries: wolfSSL's Assembly ShortcutCryptography Fundamentals
4 min readFor Payment Security Engineers

Key Recovery in 600 Queries: wolfSSL's Assembly Shortcut

The Challenge

wolfSSL's hand-written SIMD assembly implementations of ML-KEM had a critical flaw: they didn't compare all the ciphertext bytes during the Fujisaki, Okamoto check. The x86-64 AVX2 path checked 1536 of 1568 bytes, while the ARM64 NEON implementation compared roughly half.

This wasn't just theoretical. The skipped bytes carried the tail of the decryption noise, an exact linear function of the secret key. An attacker could vary those unchecked bytes, observe the decapsulation output, and read the noise one coordinate at a time, building a linear system that ordinary least squares could solve.

The vulnerability had measurable outcomes: 98.0% of the ML-KEM-1024 private key was recovered at 400 ciphertexts on AVX2, and 98.5% at 600 on NEON. Full key recovery required about 1300 ciphertexts in the verified reference model.

The Environment and Constraints

ML-KEM achieves IND-CCA2 security through the FO check inside decapsulation. The receiver re-encrypts the recovered message and returns the true shared secret only if the result matches the received ciphertext exactly. Skipping bytes in that comparison creates a plaintext-checking oracle.

wolfSSL opted for hand-written SIMD assembly for performance, introducing risks that high-level code wouldn't face. These assembly optimizations prioritized speed but left gaps in the byte-by-byte comparison that the security proof assumed would be complete.

The vulnerability survives standard hardening. Key-mismatch attacks typically exploit chosen sparse-u ciphertexts, so the defensive response is to validate all of u. But when you're reading the oracle off the unchecked v-tail, full u validation doesn't close the attack path. The AVX2 implementation validated u completely, yet remained vulnerable.

The Approach Taken

The attack exploits the incomplete comparison, exposing decryption noise as a linear function of the secret key. An attacker varies the unchecked bytes systematically, submits ciphertexts to the decapsulation endpoint, and checks if each output matches the expected shared secret.

Each query reveals information about one coordinate of the noise vector. With enough measurements, you've built a linear system where the unknowns are the secret key coefficients. Ordinary least squares solves it directly.

This approach differs from key-mismatch attacks in reach, not efficiency. While key-mismatch needs a few thousand queries, reading off the v-tail requires 100,000 to 1,000,000 ciphertexts. But it works even when u is fully validated, expanding the attack surface beyond existing mitigations.

Results and Metrics

The attack recovered most of the ML-KEM-1024 private key against shipped binaries on both backends. On AVX2, 400 ciphertexts yielded 98.0% of the 2048 secret coefficients. On NEON, 600 ciphertexts recovered 98.5%.

The cost tracked the geometry of which bytes went unchecked more than their count. NEON left roughly 2.5 times more coordinates unchecked than AVX2, yet required more ciphertexts for comparable recovery rates. This suggests that the spatial distribution of the gap matters as much as its size.

Full key recovery required approximately 1300 ciphertexts in the verified reference model. That's within reach for any attacker with persistent access to a decapsulation endpoint.

What They Would Do Differently

The immediate fix is straightforward: compare all ciphertext bytes in the FO check, not a subset. That closes the oracle.

But the deeper lesson is about implementation validation. Cryptographic security proofs assume complete execution of the specified checks. When you optimize in assembly, you're responsible for proving that your optimization preserves every security-critical property the high-level specification relied on.

wolfSSL's case shows why hand-written SIMD assembly for cryptographic primitives requires extraordinary scrutiny. The performance gain must justify the expanded attack surface. If you're writing assembly for a check that's the sole source of IND-CCA2 security, you need formal verification or exhaustive testing to ensure every byte in the comparison actually gets compared.

The standard "validate all of u" hardening proved insufficient because it addressed key-mismatch attacks, not v-tail oracles. Defensive strategies need to consider what information incomplete checks leak, not just whether tampering gets detected.

Takeaways for Your Team

First, treat incomplete cryptographic checks as key-recovery vulnerabilities, not just IND-CCA2 weaknesses. The distinction matters for triage and response timelines.

Second, if you're implementing ML-KEM or any post-quantum scheme, verify that your FO check compares every byte of the ciphertext. This applies whether you're using library code or writing your own. Don't assume the implementation matches the specification.

Third, recognize that assembly optimizations in cryptographic code create verification obligations. You can't rely on the security proof if your implementation doesn't execute what the proof assumed. Every shortcut needs its own security analysis.

Fourth, understand that validation strategies designed for one attack class (like key-mismatch) may not address oracles that emerge from different implementation gaps. Your threat model needs to account for what partial checks leak, not just what they fail to catch.

Finally, when you're selecting cryptographic libraries, ask about their verification processes for assembly implementations. Libraries that use hand-written SIMD code should document how they ensure those optimizations preserve security properties. If they can't answer that question, consider alternatives.

The wolfSSL case shows that the distance between "mostly correct" and "secure" in cryptographic implementation is measured in unchecked bytes. In this instance, 32 bytes on AVX2 and roughly 800 on NEON were enough to recover the private key.

You Might Also Like