Martin's Blog

Side-channel pitfalls in RSA: timing leaks, fault attacks, and the trouble with constant-time code

Introduction

A cryptographic algorithm can be mathematically correct and still leak its secrets: through how long it takes to run, what it accesses in memory, or how it fails when something goes wrong. These are side-channel attacks, and they have broken real deployments of otherwise correct cryptography for nearly 30 years.

Three pitfalls show up over and over in code that touches an RSA private key: timing variability in the private-key operation itself, fault attacks on the CRT variant used for performance, and non-constant-time comparison of the resulting signatures, MACs, and tags. Each has a well-known fix. Each has also been broken by people who tried to implement the fix themselves.

Timing attacks on RSA

Kocher’s 1996 paper introduced the basic idea [1]. Square-and-multiply modular exponentiation conditionally performs a multiplication based on each bit of the secret exponent, and those operations take different amounts of time on real hardware. An attacker who can measure that time recovers the exponent bit by bit.

For a long time the consensus was that this only applied to smart cards and other devices the attacker could physically prod. That ended in 2003 with Brumley and Boneh, who mounted a remote timing attack against an OpenSSL-backed HTTPS server on the same LAN and recovered the server’s private RSA key [2]. The leaks they exploited (input-dependent Montgomery reductions and the Karatsuba/schoolbook multiplication switch) were supposed to be masked by CRT and Montgomery form. They were not. OpenSSL turned on RSA blinding by default in response, in the 0.9.7b release shipped 17 March 2003 [3].

Blinding alone is not the end of the story. CacheBleed (2016) attacked OpenSSL’s constant-time RSA in version 1.0.2f [4]. The scatter-gather implementation was constant-time with respect to cache lines but not with respect to cache banks on Sandy Bridge. From a co-located process, roughly 16,000 observed 4096-bit RSA operations were enough to recover the full private key. The OpenSSL fix was not to switch to a fully constant-time implementation, but to use 128-bit accesses and shuffle the access pattern across cache banks [4].

Mitigations, in order of importance:

  1. Keep RSA blinding on. OpenSSL exposes it via RSA_blinding_on(3) [5] and enables it by default for private keys. Never disable it on a key that handles attacker-influenced input.

  2. Use a library with constant-time modular exponentiation. Modern OpenSSL, BoringSSL, and libsodium do this. Older versions and hand-rolled implementations often do not.

  3. Do not implement RSA yourself. RSA is one of the most attacked primitives in cryptography. There is no good reason to write your own in 2026.

Fault attacks: verify your RSA-CRT signatures

This is often conflated with timing attacks, but it is a different hazard. Boneh, DeMillo and Lipton showed in 1997 [6] (the “Bellcore attack”) that a CRT-RSA implementation which produces even a single faulty signature can be catastrophically broken. Given a correct signature S and a faulty signature Ŝ for the same message, the attacker computes gcd(N, S − Ŝ) and recovers one of the prime factors of N directly. One fault, full key compromise.

Faults are not just a smart-card problem. They come from voltage glitches, clock glitches, laser injection, Rowhammer-induced bit flips, cosmic rays, and ordinary hardware bugs. Practical fault injection on commodity hardware has been demonstrated repeatedly.

The simplest defense is also the most reliable: after producing a signature with CRT, verify it with the public key before returning it. A correct signature verifies. A faulty signature does not, and the implementation returns an error instead of leaking the key. OpenSSL has done this for CRT-RSA since the early 2000s. Java’s RSAPrivateCrtKey path does it. If your library does not, switch libraries.

Timing attacks exploit how long the operation took. Fault attacks exploit that the operation went wrong. They require different defenses and you need both.

Constant-time comparison

Signature, MAC, HMAC, and authentication-tag verification all end with the same operation: comparing the computed value against the expected value. A standard memcmp short-circuits on the first byte mismatch, which lets a remote attacker iterate one byte at a time and forge tags in linear rather than exponential work.

The textbook fix:

 1/*
 2 * Constant-time byte comparison.
 3 * Returns 0 if equal, non-zero if different.
 4 * Always reads every byte regardless of where (or whether) the inputs
 5 * differ.
 6 */
 7int secure_compare(const uint8_t *a, const uint8_t *b, size_t length) {
 8    volatile uint8_t result = 0;
 9
10    for (size_t i = 0; i < length; i++) {
11        result |= a[i] ^ b[i];
12    }
13
14    return result;
15}

The volatile qualifier tells the compiler the accumulator may be observed externally, which is intended to prevent dead-store elimination and early termination. This is necessary, but it is not sufficient.

Why hand-rolled constant-time code keeps breaking

Modern optimizing compilers transform “constant-time” source code into something that is not, in ways that are not always visible without reading the generated assembly:

“Breaking Bad: How Compilers Break Constant-Time Implementations” (2024) tested 44,604 builds across x86-64, i386, armv7, aarch64, RISC-V and MIPS-32 and found compiler-induced secret-dependent operations in formally verified cryptographic libraries [8]. A 2021 survey of 44 cryptographic-library developers found that even maintainers of major libraries disagree on which defensive techniques actually work [9].

If you must inspect the generated code:

1gcc -O3 -march=native -S secure_compare.c -o secure_compare.s
2# Look for: conditional jumps that depend on `result`, any `rep`
3# instruction, any vectorization with a scalar epilogue,
4# any stack spill of intermediates.

The binary’s constant-time behavior also depends on the specific CPU it runs on. CacheBleed [4] is the canonical example. Inspecting the assembly is necessary but not sufficient.

Use a library function

These are audited, maintained, and updated when a new compiler version breaks them:

 1// libsodium - returns 0 if equal, -1 otherwise
 2#include <sodium.h>
 3if (sodium_memcmp(a, b, length) == 0) { /* equal */ }
 4
 5// OpenSSL / LibreSSL - returns 0 if equal, non-zero otherwise
 6#include <openssl/crypto.h>
 7if (CRYPTO_memcmp(a, b, length) == 0) { /* equal */ }
 8
 9// BSD libc
10#include <string.h>
11if (timingsafe_memcmp(a, b, length) == 0) { /* equal */ }

Equivalents exist in every serious ecosystem: subtle.ConstantTimeCompare in Go (crypto/subtle), hmac.compare_digest in Python, MessageDigest.isEqual in Java, the subtle crate in Rust, and CryptographicOperations.FixedTimeEquals in .NET.

Note the return-value semantics. sodium_memcmp returns -1 for unequal. CRYPTO_memcmp returns “non-zero” with no further guarantee. Neither can be used for ordering. Always compare against zero explicitly. Do not write if (sodium_memcmp(...)) and assume the value is a usable boolean. That is a real bug class.

Practical recommendations

For code that handles RSA private keys or verifies MACs and signatures:

  1. Use RSA blinding. Do not disable it. If you are running an OpenSSL version where it is not on by default, upgrade.

  2. Verify CRT-RSA signatures with the public key before returning them. This is a one-line defense against a complete key-recovery attack.

  3. Do not write your own constant-time comparison. Use sodium_memcmp, CRYPTO_memcmp, timingsafe_memcmp, or the equivalent in your language. Compare against zero explicitly.

  4. If you must hand-write any of the above, inspect the assembly at the exact optimization flags your release build uses, and re-inspect every time you bump compiler version.

  5. Assume the attacker can measure wall time, cache state, and branch predictor state of any process they can co-locate with yours, and plan accordingly.

Cryptographic correctness is necessary but not sufficient. The implementation has to survive an attacker who measures everything the hardware lets them measure, and that set keeps growing. Delegate as much as possible to libraries written by people who do this for a living.

References

  1. Kocher, P. Timing Attacks on Implementations of Diffie-Hellman, RSA, DSS, and Other Systems. CRYPTO 1996. (https://paulkocher.com/doc/TimingAttacks.pdf)

  2. Brumley, D. and Boneh, D. Remote Timing Attacks are Practical. USENIX Security 2003. (https://crypto.stanford.edu/~dabo/papers/ssl-timing.pdf)

  3. OpenSSL Security Advisory, Timing-based attacks on RSA keys, 17 March 2003. (https://www.openssl.org/news/secadv/20030317.txt)

  4. Yarom, Y., Genkin, D. and Heninger, N. CacheBleed: A Timing Attack on OpenSSL Constant-Time RSA. CHES 2016. (https://faculty.cc.gatech.edu/~genkin/cachebleed/cachebleed.pdf)

  5. OpenSSL, RSA_blinding_on(3). (https://docs.openssl.org/1.0.2/man3/RSA_blinding_on/)

  6. Boneh, D., DeMillo, R. and Lipton, R. On the Importance of Checking Cryptographic Protocols for Faults. EUROCRYPT 1997. Survey at (https://crypto.stanford.edu/~dabo/papers/RSA-survey.pdf)

  7. Trail of Bits, Introducing constant-time support for LLVM to protect cryptographic code, December 2025. (https://blog.trailofbits.com/2025/12/02/introducing-constant-time-support-for-llvm-to-protect-cryptographic-code/)

  8. Breaking Bad: How Compilers Break Constant-Time Implementations. arXiv 2410.13489, 2024. (https://arxiv.org/pdf/2410.13489)

  9. Jancar, J. et al. “They’re not that hard to mitigate”: What Cryptographic Library Developers Think About Timing Attacks. IEEE S&P 2022. (https://eprint.iacr.org/2021/1650)

  10. libsodium helpers documentation. (https://libsodium.gitbook.io/doc/helpers)

  11. OpenSSL CRYPTO_memcmp(3). (https://docs.openssl.org/1.1.1/man3/CRYPTO_memcmp/)