Cryptography

Constant-Time Comparison: How to Prevent Timing Attacks on HMAC

8 min read
By
Constant-Time Comparison: How to Prevent Timing Attacks on HMAC

Photo by Pixabay from Pexels

When you verify an HMAC, you compute the expected HMAC and compare it to the provided signature. If they match, the message is authentic. If they do not, it has been tampered with. This seems straightforward, but the way you compare them matters. A regular string comparison leaks information about the signature through timing, and an attacker can use that leakage to forge valid signatures.

This is called a timing attack, and it is one of the most subtle and dangerous vulnerabilities in HMAC verification. Let's look at how it works and how to prevent it.

How Regular String Comparison Leaks Information

Most programming languages compare strings byte by byte, starting from the first byte. If the first bytes differ, the comparison returns false immediately. If they match, it moves to the second byte, and so on. This is called short-circuit comparison, and it is an optimization for normal use.

But for security comparisons, short-circuiting is a vulnerability. Consider what happens when an attacker sends a signature for verification:

- If the first byte is wrong, the comparison returns false almost instantly. - If the first byte is correct but the second is wrong, the comparison takes slightly longer because it had to compare two bytes before returning false. - If the first two bytes are correct but the third is wrong, it takes slightly longer still.

The time difference is tiny, maybe nanoseconds. But with enough measurements and statistical analysis, an attacker can distinguish between "first byte correct" and "first byte wrong."

The Byte-by-Byte Attack

Here is how an attacker exploits this:

**Step 1: Guess the first byte.** The attacker tries all 256 possible values for the first byte of the signature. For each one, they send the request many times and measure the average response time. One value takes slightly longer than the others because the comparison proceeds to the second byte. That is the correct first byte.

**Step 2: Guess the second byte.** Now the attacker knows the first byte. They fix it and try all 256 values for the second byte. Again, one value takes slightly longer. That is the correct second byte.

**Step 3: Repeat for each byte.** The attacker continues this process for each byte of the signature. For a 32-byte HMAC-SHA256 signature, they need at most 256 x 32 = 8,192 attempts, plus the repeated measurements for each byte to get reliable timing data.

In practice, the attack requires thousands of measurements per byte to overcome network jitter, CPU scheduling noise, and other timing variations. But on a local network or a co-located server, the attack is feasible. There are documented cases of timing attacks being used to forge HMAC signatures in real systems.

Why This Works

The attack works because the comparison time is correlated with the number of correct bytes. The more bytes that match, the longer the comparison takes. This correlation is the side channel. The attacker does not need to see the expected signature. They just need to measure response times and find the input that produces the longest response.

This is not theoretical. Timing attacks have been demonstrated against:

- **API authentication** using HMAC-signed requests - **JWT verification** where the signature is compared with regular equality - **Webhook signature verification** where the developer used === instead of a constant-time function - **Password reset tokens** where the token is compared with regular string equality

In each case, the fix is the same: use a constant-time comparison function.

How Constant-Time Comparison Works

A constant-time comparison function does not short-circuit. It compares every byte of both strings, regardless of where differences occur. Here is the typical implementation:

function constantTimeEqual(a, b) { if (a.length !== b.length) return false; let result = 0; for (let i = 0; i < a.length; i++) { result |= a.charCodeAt(i) ^ b.charCodeAt(i); } return result === 0; }

The key is the XOR operation. a ^ b is zero if and only if a === b. The |= operation accumulates any non-zero result. After comparing all bytes, if any byte differed, the accumulated result is non-zero. If all bytes matched, it is zero.

Because the loop always runs for the full length of both strings, the time is the same regardless of where differences occur. The attacker cannot distinguish "first byte wrong" from "last byte wrong" because both take the same amount of time.

Using the Right Function

Do not implement constant-time comparison yourself. Subtle implementation details can reintroduce timing leaks. For example, if your function checks lengths first and returns false for different lengths, that is fine. But if your compiler optimizes the loop in a way that short-circuits, the timing leak returns.

Use the constant-time comparison function provided by your language's standard library or crypto library:

- **Node.js**: crypto.timingSafeEqual(a, b) - **Python**: hmac.compare_digest(a, b) - **Go**: subtle.ConstantTimeCompare(a, b) - **Java**: MessageDigest.isEqual(a, b) - **Ruby**: OpenSSL.fixed_length_secure_compare(a, b) (for fixed-length) or Rack::Utils.secure_compare(a, b) - **PHP**: hash_equals(known_string, user_string) - **Rust**: subtle::ConstantTimeEq

These functions are carefully implemented and tested to avoid timing leaks. They handle edge cases like different-length inputs. They are the right tool for the job.

When to Use Constant-Time Comparison

Use constant-time comparison whenever you compare secret values that an attacker could influence:

- **HMAC signature verification.** When comparing the expected HMAC to the provided signature. - **API token verification.** When comparing a provided API token to the stored token. - **Password reset tokens.** When comparing a provided reset token to the stored token. - **CSRF tokens.** When comparing the provided CSRF token to the stored token. - **Any authentication token.** Any time a user-provided value is compared to a secret stored on the server.

You do not need constant-time comparison when comparing non-secret values (like comparing two public IDs) or when the comparison result does not affect security (like checking if a feature flag matches a value).

Real-World Example: Stripe Webhook Verification

When verifying Stripe webhook signatures, you compute the expected HMAC-SHA256 and compare it to the signature in the Stripe-Signature header. If you use === for this comparison, you are vulnerable to a timing attack.

The Stripe SDK handles this correctly. It uses crypto.timingSafeEqual() internally. But if you are verifying signatures manually (which you should not do unless necessary), you must use a constant-time comparison function.

We cover the full Stripe webhook verification process in our Stripe webhook signature guide. The constant-time comparison is one critical step in that process.

The Bottom Line

Timing attacks on HMAC verification are real. They have been demonstrated in practice. They allow an attacker to forge valid signatures without knowing the secret key. The fix is simple: always use a constant-time comparison function when comparing HMACs, tokens, or any secret value that an attacker could influence.

Never use ===, ==, isEqual, or any regular equality operator for security-sensitive comparisons. Use crypto.timingSafeEqual, hmac.compare_digest, hash_equals, or whatever your language provides. It is a one-line change that closes a serious vulnerability.

For the broader context of how HMAC works and why it is used for authentication, read our HMAC vs hash explained guide. And to compute HMACs yourself, try our HMAC generator.

Frequently Asked Questions

What is a timing attack on HMAC verification?

A timing attack measures how long HMAC verification takes to determine how many bytes of the provided signature match the expected signature. Regular string comparison stops at the first mismatch, so a signature with the correct first byte takes slightly longer than one with a wrong first byte. An attacker can use this timing difference to guess the correct signature one byte at a time.

How does constant-time comparison prevent timing attacks?

Constant-time comparison always compares every byte of both strings, regardless of where differences occur. It accumulates the differences using XOR and OR operations, then returns whether the accumulated difference is zero. Because it never short-circuits, the comparison time is the same whether the first byte matches or the last byte matches, leaking no timing information.

Which constant-time comparison function should I use?

Use the one provided by your crypto library. In Node.js, use crypto.timingSafeEqual(). In Python, use hmac.compare_digest(). In Go, use subtle.ConstantTimeCompare(). In Java, use MessageDigest.isEqual(). Never implement your own constant-time comparison, as subtle implementation details can reintroduce timing leaks.

Do I need constant-time comparison for password hashing?

Yes, if you are comparing hashes directly. However, most password hashing libraries (bcrypt, argon2) handle comparison internally and use constant-time comparison under the hood. If you are comparing hashes manually, always use a constant-time function. For more on password hashing, see our guide on bcrypt vs SHA-256 for password storage.

Try NovelCrypt Tools

Experience military-grade encryption for your sensitive data. Create self-destructing messages, encrypt files, or explore our experimental lab tools.

Explore NovelCrypt