Why timing attacks matter

A timing attack exploits measurable differences in how long a program takes to process different inputs. The classic example is comparing a user-supplied secret against a stored secret:

  • == on byte slices may stop at the first mismatch.
  • Early returns can reveal how many prefix bytes were correct.
  • Branches based on secret data can create measurable patterns.

These differences may be tiny, but over a network they can still be exploitable when an attacker can repeat requests and average out noise.

Common targets include:

  • password verification
  • HMAC or MAC validation
  • token comparison
  • API key checks
  • one-time codes and reset tokens

The unsafe pattern: ordinary equality

A naive implementation often looks harmless:

fn is_valid_token(provided: &[u8], expected: &[u8]) -> bool {
    provided == expected
}

This is correct functionally, but it is not designed to resist timing analysis. If provided differs from expected near the beginning, the comparison may finish sooner than if it differs near the end.

The same issue appears in string comparisons, prefix checks, and manual loops that return early on mismatch.

Why “just use Rust” is not enough

Rust prevents memory corruption, but timing leaks are a logic-level vulnerability. Safe code can still reveal secrets if it uses data-dependent control flow or data-dependent memory access patterns.

That means secure authentication code requires deliberate design, not just safe syntax.

Use constant-time comparison primitives

For secret comparisons, use a constant-time equality function from a well-reviewed crate instead of ==.

A common choice is the subtle crate, which provides constant-time comparison traits and helpers.

Example: comparing authentication tags

use subtle::ConstantTimeEq;

fn verify_tag(provided: &[u8], expected: &[u8]) -> bool {
    if provided.len() != expected.len() {
        return false;
    }

    provided.ct_eq(expected).into()
}

This approach helps avoid short-circuit behavior during the byte-by-byte comparison. The length check is still a branch, but in many protocols the tag length is fixed and public. If the length itself is sensitive, you need a different protocol design.

Important caveat

Constant-time comparison only helps if:

  • both inputs are already normalized to the same length
  • you do not branch on secret-dependent intermediate results
  • you do not leak secrets elsewhere in the function

If you compare a password hash output, HMAC, or token digest, the comparison should be against a fixed-size value.

Design for fixed-size secrets

Constant-time code is easiest when the secret representation has a fixed length.

Secret typeGood practiceWhy
Password hashCompare derived hashes of fixed sizeAvoids variable-length secret comparison
HMAC tagCompare fixed-size tag bytesDesigned for equality checks
API key digestStore and compare a keyed or hashed digestPrevents direct plaintext comparison
One-time tokenNormalize to fixed-length bytes before comparisonReduces timing variation

If you must compare variable-length secrets, consider whether the length is already public. If not, redesign the protocol so the comparison happens on a fixed-size derived value.

Avoid secret-dependent branching

Even if you use constant-time equality, surrounding logic can still leak information.

Problematic example

fn check_admin_token(token: &[u8], expected: &[u8]) -> bool {
    if token.len() != expected.len() {
        return false;
    }

    if token.ct_eq(expected).into() {
        true
    } else {
        false
    }
}

The final if is unnecessary and can be simplified. More importantly, any additional branching based on secret-derived values can reintroduce timing differences.

Better pattern

use subtle::ConstantTimeEq;

fn check_admin_token(token: &[u8], expected: &[u8]) -> bool {
    token.len() == expected.len() && token.ct_eq(expected).into()
}

This is cleaner and avoids extra control flow. In practice, the length check is acceptable when the length is public and fixed by the protocol.

Compare derived values, not raw secrets

A strong pattern is to avoid direct comparison of user secrets altogether. Instead:

  1. derive a fixed-size digest from the secret
  2. store the digest securely
  3. compare digests in constant time

This is especially useful for API keys and tokens.

Example: API key verification with a keyed digest

use subtle::ConstantTimeEq;
use sha2::{Digest, Sha256};

fn digest_key(key: &[u8], salt: &[u8]) -> [u8; 32] {
    let mut hasher = Sha256::new();
    hasher.update(salt);
    hasher.update(key);

    let result = hasher.finalize();
    let mut out = [0u8; 32];
    out.copy_from_slice(&result);
    out
}

fn verify_api_key(provided: &[u8], stored_digest: &[u8; 32], salt: &[u8]) -> bool {
    let candidate = digest_key(provided, salt);
    candidate.ct_eq(stored_digest).into()
}

This pattern is not a substitute for a password hashing scheme when verifying passwords, but it is a useful approach for opaque API keys and similar bearer secrets.

Passwords need password hashing, not plain hashing

Passwords are not random tokens. They are low-entropy and vulnerable to offline guessing. For password verification, use a password hashing algorithm such as Argon2, scrypt, or bcrypt.

The timing concern here has two parts:

  • the password hashing function should be appropriate for password storage
  • the final hash comparison should be constant-time

Recommended flow

  1. Hash the password with a password hashing algorithm.
  2. Store the encoded hash string.
  3. On login, verify the password using the library’s verification API.
  4. Ensure the final comparison is constant-time.

Most mature password-hashing libraries already handle the comparison safely. Prefer their verification functions over manual parsing and comparison.

Normalize inputs before comparison

Secret comparisons often fail because of encoding mismatches rather than actual authentication failure. Normalization should happen before any secret comparison, but be careful not to introduce secret-dependent behavior.

Examples:

  • decode base64 tokens into bytes first
  • trim only protocol-defined whitespace, not arbitrary user input
  • canonicalize encodings consistently
  • reject malformed input before secret comparison

Good rule

Perform all parsing and validation on public structure first, then compare fixed-size secret material in constant time.

This reduces the chance that malformed inputs trigger different code paths based on secret content.

Keep error handling uniform

Timing attacks are often paired with information leaks in error messages. Even if your comparison is constant-time, different error paths can still reveal whether a token had the right length, format, or prefix.

Prefer uniform failure behavior

  • return the same status code for all authentication failures
  • avoid messages like “token length invalid” vs. “token mismatch”
  • keep response time as similar as practical across failure cases

A login endpoint should usually respond with a generic failure such as “authentication failed,” regardless of whether the username was unknown or the password was wrong.

Be careful with early exits in loops

Manual loops are a common source of timing leaks.

Leaky example

fn starts_with_secret_prefix(input: &[u8], prefix: &[u8]) -> bool {
    for (a, b) in input.iter().zip(prefix.iter()) {
        if a != b {
            return false;
        }
    }
    true
}

This returns as soon as a mismatch is found, which leaks how much of the prefix matched.

If the prefix is secret, this is unsafe. If the prefix is public, it may be fine. The key question is whether the compared data is sensitive.

Safer alternative

Use a constant-time primitive when the compared bytes are secret. If the logic is about public protocol structure, early exit may be acceptable.

Practical checklist for secure comparisons

Use this checklist when reviewing Rust authentication code:

  • Are you comparing secrets with == or !=?
  • Does any loop return early based on secret data?
  • Are lengths or formats sensitive?
  • Are error messages distinguishable?
  • Are you comparing fixed-size derived values instead of raw secrets?
  • Does your password verification library already provide safe comparison?
  • Are you avoiding secret-dependent branches after the comparison?

If the answer to any of the first four questions is “yes,” review the code carefully.

A complete example: token verification endpoint

The following example shows a simple token verification flow using fixed-size digests and constant-time comparison.

use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;

fn digest_token(token: &[u8]) -> [u8; 32] {
    let mut hasher = Sha256::new();
    hasher.update(token);
    let result = hasher.finalize();

    let mut out = [0u8; 32];
    out.copy_from_slice(&result);
    out
}

fn verify_token(provided: &[u8], stored_digest: &[u8; 32]) -> bool {
    let candidate = digest_token(provided);
    candidate.ct_eq(stored_digest).into()
}

fn main() {
    let stored = digest_token(b"super-secret-token");

    assert!(verify_token(b"super-secret-token", &stored));
    assert!(!verify_token(b"wrong-token", &stored));
}

This example is intentionally simple. In a real service, you would also:

  • protect the stored digest with access controls
  • rate-limit repeated failures
  • log authentication events without revealing secret values
  • use a stronger secret-handling strategy for long-lived credentials

Testing and review tips

Timing attacks are difficult to prove absent, but you can still improve confidence.

What to look for in code review

  • direct string or byte equality on secrets
  • branches based on secret-derived values
  • variable-length comparisons where length is sensitive
  • different error messages for different failure modes
  • custom crypto logic that reimplements library behavior

What to test

  • valid and invalid inputs should follow the same public response path
  • malformed inputs should not produce distinct secret-related messages
  • benchmarks should not show obvious data-dependent differences in authentication code

Benchmarks are not a security proof, but they can catch accidental early exits and other obvious leaks.

Summary

Constant-time programming is about reducing information leakage through execution time. In Rust, the main risks come from ordinary comparisons, early returns, and secret-dependent branching—not from unsafe memory access.

For secure authentication code:

  • use constant-time comparison primitives
  • compare fixed-size derived values when possible
  • avoid branching on secret data
  • keep failure responses uniform
  • rely on mature password hashing and verification libraries

When you treat timing as part of your threat model, Rust gives you the tools to build authentication code that is both safe and resistant to practical side-channel attacks.

Learn more with useful resources