Constant Time Comparisons in Rust and Why Equality Is Unsafe
Comparing secrets with == returns at the first difference, which leaks how many bytes matched. Use subtle ConstantTimeEq for tokens and MACs and understand what constant time can and cannot promise.

An API checks the caller's token with if provided == expected. Looks harmless. The comparison returns as soon as it finds a mismatch, so a token that matches the first byte takes slightly longer to reject than one that does not. Measure enough requests and an attacker learns the token one byte at a time. The attack is real, and the fix is one function call. This post shows it. It is part of the cryptographic hygiene in security for Rust developers.
subtle 2, hmac 0.12, Rust 1.90.
The leak
fn insecure_eq(a: &[u8], b: &[u8]) -> bool {
a == b
}For slices, == is optimized: it compares lengths, then uses memcmp, which may return early. The exact timing behavior depends on the implementation, and a leak does not need to be large to be exploitable over many requests. The rule is simple: if either side is a secret, do not use ==.
The fix
[dependencies]
subtle = "2"use subtle::ConstantTimeEq;
fn token_matches(provided: &[u8], expected: &[u8]) -> bool {
provided.ct_eq(expected).into()
}
fn main() {
println!("{}", token_matches(b"s3cret-token", b"s3cret-token"));
println!("{}", token_matches(b"s3cret-tokex", b"s3cret-token"));
}It prints true and false. ct_eq examines every byte and combines the results with bitwise operations, so the time does not depend on where the first mismatch is. It returns a Choice, a wrapper around u8 that discourages branching on it until the end. Slices of different lengths compare unequal in time that depends on the length only, so compare values of fixed size, for example hash both sides first.
Prefer the library's verifier
For MACs:
use hmac::{Hmac, Mac};
use sha2::Sha256;
fn verify(key: &[u8], message: &[u8], tag: &[u8]) -> bool {
let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("any key length works");
mac.update(message);
mac.verify_slice(tag).is_ok()
}verify_slice performs the comparison in constant time. The same holds for argon2's verify_password (password hashing in Rust) and for AEAD decryption in the RustCrypto crates, which authenticate before returning plaintext. Using the high-level verifier is safer than hand-rolling ct_eq.
Where it matters
| Compare | Constant time? |
|---|---|
| API key, session token, CSRF token | yes |
| HMAC or signature | yes (use verify) |
| Password hash | use the hasher's verify |
| User id, username, public data | no need |
For tokens stored in a database, a common design stores a hash of the token and looks up by the hash; the lookup index is not constant-time, and does not need to be, because a hash does not leak prefixes of the secret.
What it cannot promise
The compiler can introduce branches, and CPUs have variable-time instructions for some operations. subtle uses barriers to resist optimization but documents that it is a best effort. Cache timing, speculative execution and microarchitectural leaks sit outside what a comparison function can solve. Network noise reduces exploitability and does not remove it.
Limits
Constant time is for comparison. Authorization decisions, string equality of public identifiers and sorting do not need it. For handling the secrets themselves, see zeroize and secrecy. The crate's docs are at docs.rs/subtle.


