Timing attacks turn your CPU into a spy
You are building an API gateway. You store a secret bearer token in your database. A request arrives with a token in the header. You grab the stored token, compare it to the incoming one using ==, and return a success or failure response. The code works perfectly. Valid tokens pass. Invalid tokens fail. Until an attacker starts measuring response times.
They send a token starting with "A". The server replies in 12 microseconds. They try "B". Also 12 microseconds. They try "S". The reply takes 14 microseconds. The CPU took longer to process "S" because it matched the first byte of the real token. The attacker repeats this byte by byte. Within minutes, they have reconstructed your entire secret. Your comparison function just handed over the keys.
What constant time actually means
Constant-time comparison means the code takes the exact same amount of time to run regardless of the data values. Execution time depends only on the length of the inputs, not on where they differ. If you compare two 32-byte slices, the function consumes the same CPU cycles whether the slices match perfectly or diverge at the very first byte.
Picture a nightclub bouncer checking a guest list. A standard bouncer scans the first letter of a name. If it is wrong, they turn you away immediately. If it matches, they check the second letter. This approach is fast for the bouncer, but it leaks information. You can guess how many letters matched by timing how long the bouncer takes to reject you.
A constant-time bouncer reads every single letter from start to finish, no matter what. They check the first letter, then the second, then the third, all the way to the end. They only make a decision after the final character. The time taken is identical for every guest. An observer watching the bouncer learns nothing about the name except its length.
Rust's standard equality operators like == and PartialEq behave like the standard bouncer. They return early on the first difference. This optimization is excellent for public data. It is catastrophic for secrets.
Minimal example
The constant_time_eq crate provides a drop-in function that compares byte slices without leaking timing information. It handles length differences safely and avoids branching on secret data.
use constant_time_eq::constant_time_eq;
fn main() {
// The secret token stored securely in the database.
let secret = b"my_super_secret_key";
// A correct token provided by the user.
let correct_input = b"my_super_secret_key";
// A wrong token that differs only at the final byte.
let wrong_input = b"my_super_secret_kex";
// Compares slices in constant time.
// Returns true only if lengths and all bytes match exactly.
assert!(constant_time_eq(secret, correct_input));
// Returns false, but consumes identical CPU cycles as the true case.
assert!(!constant_time_eq(secret, wrong_input));
}
Add constant_time_eq = "0.3" to your Cargo.toml dependencies. The function accepts &[u8] slices and returns a bool. The API stays simple because the cryptographic complexity lives inside the implementation.
Trust the accumulator. Never return early.
How the comparison avoids leaks
The function avoids leaks by eliminating branches that depend on secret data. Branches are the enemy of constant time. When the CPU encounters a conditional jump, it guesses which path the code will take. This is branch prediction. If the guess is wrong, the CPU flushes its instruction pipeline and restarts. The flush costs cycles. The cycle count depends on the secret data. The timing difference leaks the secret.
Even if your Rust source code looks branchless, the compiler might introduce branches during optimization. The CPU might also execute micro-operations that branch internally. You cannot prove constant time by reading assembly output. You must use patterns that are mathematically and empirically proven to be safe.
The constant_time_eq function relies on XOR accumulation. It loops through every byte position of both slices. For each index, it XORs the two bytes together. XOR returns zero if the bits are identical, and a non-zero value if they differ. The function feeds these results into a single accumulator variable. If any byte differs, the accumulator becomes non-zero. The function checks the accumulator only after the loop finishes.
// Simplified logic of constant-time comparison.
// Do not copy this into production. Use the audited crate instead.
fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
// Accumulate length differences to avoid early returns.
let mut acc: u8 = 0;
let len_diff = a.len() as isize - b.len() as isize;
acc |= (len_diff as u8);
// Iterate over the maximum possible length.
let max_len = std::cmp::max(a.len(), b.len());
for i in 0..max_len {
// Pad shorter slices with zeros to prevent panics.
let byte_a = if i < a.len() { a[i] } else { 0 };
let byte_b = if i < b.len() { b[i] } else { 0 };
// XOR bytes and merge into accumulator.
// Any mismatch flips bits in acc, guaranteeing non-zero.
acc |= byte_a ^ byte_b;
}
// Final check happens outside the loop.
acc == 0
}
The loop runs exactly max_len times. There are no early returns. The accumulator collects every difference. The final comparison against zero is a single operation. This pattern prevents the CPU from learning where the data diverges.
Convention note: The Rust security community treats constant-time code as a specialized domain. You will see #[inline(always)] or specific compiler hints in audited crates to prevent the optimizer from collapsing the loop. Do not guess at these hints. Rely on maintained crates.
The bouncer reads the whole name. Every time.
Realistic verification flow
In production systems, you wrap secrets in types that enforce constant-time checks. This prevents accidental use of == later in the codebase. You define a struct for the secret and implement a dedicated verification method.
use constant_time_eq::constant_time_eq;
/// Represents a secret token that must be compared in constant time.
struct SecretToken {
// Store the raw bytes privately.
data: Vec<u8>,
}
impl SecretToken {
/// Creates a new token from a byte slice.
fn new(data: &[u8]) -> Self {
Self {
// Clone to heap to own the data.
data: data.to_vec(),
}
}
/// Verifies an input against this token.
/// Returns true if the input matches exactly.
fn verify(&self, input: &[u8]) -> bool {
// Route all comparisons through constant-time logic.
// The private field prevents accidental == usage.
constant_time_eq(&self.data, input)
}
}
fn main() {
let token = SecretToken::new(b"api_key_12345");
// Simulate a valid authentication request.
let user_input = b"api_key_12345";
assert!(token.verify(user_input));
// Simulate a tampered request.
let wrong_input = b"api_key_12346";
assert!(!token.verify(wrong_input));
}
This pattern centralizes the comparison logic. Every verification call routes through verify. You cannot accidentally compare the inner data field with == because the field is private. The type system enforces the security policy at compile time.
Wrap secrets in types that force constant-time checks.
Pitfalls and compiler traps
Using constant-time comparison correctly requires navigating several traps. The compiler and the CPU will fight you at every turn.
Comparing String values directly causes a type mismatch. The constant_time_eq function expects &[u8] slices. If you pass String values directly, the compiler rejects this with E0308 (mismatched types). You must convert them to bytes using .as_bytes().
let secret_str = String::from("secret");
let input_str = String::from("secret");
// Triggers E0308: expected &[_], found String
// constant_time_eq(&secret_str, &input_str);
// Correct conversion to byte slices.
assert!(constant_time_eq(secret_str.as_bytes(), input_str.as_bytes()));
Length mismatches break naive implementations. Some custom constant-time functions panic or return early when slices have different lengths. The constant_time_eq crate handles length differences in constant time. It computes the length difference and accumulates it into the result variable. You do not need to check lengths manually. If you check lengths with if a.len() != b.len(), you introduce a branch that leaks the length. Let the crate handle it.
Writing your own implementation is a trap. It is tempting to write a simple loop with XOR. You might believe you have removed branches. The compiler might optimize your loop into something that branches. The CPU might predict branches in your indexing logic. Modern compilers also auto-vectorize loops using SIMD instructions, which can introduce data-dependent execution paths. The constant_time_eq crate uses specific patterns and compiler hints to prevent optimization. It has been audited by security researchers. Writing your own constant-time code is a fast track to vulnerabilities. The community convention is to use the crate. Never write constant-time logic yourself.
The subtle crate provides constant-time primitives for cryptographic library authors. It offers traits like ConstantTimeEq and operations like conditional selection. For application-level code, subtle is overkill. It requires more boilerplate and trait bounds. Use constant_time_eq for application code. Use subtle when you are building a cryptographic library and need trait integration.
Don't write your own constant-time code. The CPU will outsmart you.
Decision matrix
Use constant_time_eq when comparing secrets, tokens, passwords, MACs, or any data where an attacker can measure response time and gain an advantage from knowing where values differ. Use == or PartialEq when comparing public data, configuration values, non-secret identifiers, or any data where timing leaks provide no information to an attacker. Standard equality is faster and simpler. Use subtle::ConstantTimeEq when you are building a cryptographic library and need trait-based integration, conditional selection, or more advanced constant-time operations beyond simple equality. Reach for constant_time_eq::constant_time_eq for application-level code; it's the simplest API for the most common case.
Secrets get constant time. Everything else gets ==.