Constant Time Comparison: Why Equality Checks Leak Secrets

Constant Time Comparison: Why Equality Checks Leak Secrets

Written by

in

When your code checks a secret, a MAC tag, a session token, a password hash, the way it compares matters as much as the comparison itself. An ordinary equality check stops at the first byte that differs. That shortcut saves time, and the time it saves is exactly what leaks. Constant time comparison is the fix: compare two secrets in a way that takes the same time no matter where or whether they differ, so an attacker learns nothing from the clock.

Why an ordinary equality check leaks

Most string and byte comparisons are written to be fast. They walk both inputs left to right and return the moment they find a mismatch. In code that looks like this:

def insecure_equal(a, b):
    if len(a) != len(b):
        return False
    for x, y in zip(a, b):
        if x != y:
            return False   # returns early on the first wrong byte
    return True

That early return is the bug. If the first byte is wrong, the function returns almost instantly. If the first ten bytes match and the eleventh is wrong, it runs ten comparisons first, then returns. The more leading bytes an attacker guesses correctly, the longer the check takes. The running time is a direct readout of how many leading bytes matched.

How timing turns into a forged tag

Imagine a server that accepts a request only if a supplied MAC tag equals the tag it computes. The tag is 16 bytes. An attacker who cannot compute the tag can still forge one byte by byte, using only the response time.

  • Fix all 16 bytes to zero and try every value for the first byte, 256 requests. The correct first byte makes the server compare one extra byte before failing, so that guess is measurably slower.
  • Lock in the winning first byte, then sweep the second byte the same way. The right value adds another comparison and another small delay.
  • Repeat down the tag. Each byte costs at most 256 tries, so the whole 16 byte tag takes about 16 times 256 requests, a few thousand, instead of the 2 to the 128th an offline guess would need.

An early exit comparison does not just say yes or no. It quietly tells the attacker how close they are, and anything that measures your closeness to a secret can be climbed one byte at a time.

The per byte timing difference is tiny, often nanoseconds, and the network adds noise. Attackers beat the noise by averaging many requests per guess and using statistics to find the slow one. Over a local network, or against a tag checked many times, the signal is recoverable. This is the same family as the RSA timing attack, where the time a private key operation takes leaks the key, and the Lucky Thirteen attack, where tiny timing differences in MAC checking during decryption leak plaintext.

What constant time comparison does instead

A safe comparison never branches on the secret and never returns early. It looks at every byte of both inputs every time, accumulates the differences, and only reports the result at the end.

def constant_time_equal(a, b):
    if len(a) != len(b):
        return False
    result = 0
    for x, y in zip(a, b):
        result |= x ^ y      # OR in any difference, never branch
    return result == 0

Every byte is XORed and folded into result. A match contributes zero, a mismatch flips some bits and they stay flipped. The loop runs the full length regardless of where a difference is, so the time no longer depends on how many leading bytes matched. The clock says nothing useful, and the byte by byte climb dies.

One caution about length

Notice the length check returns early. Comparing strings of different length can leak the length of the secret through timing. When the length itself is sensitive, compare fixed size values, or hash both inputs to a fixed length first and compare the hashes, so length stops being observable.

How to get constant time comparison right

  • Use the library function. Most platforms ship one built for this. Python has hmac.compare_digest, many languages have a constantTimeCompare or timingSafeEqual. Use it for every secret comparison rather than writing your own.
  • Compare the right things. MAC tags, session tokens, CSRF tokens, API keys, password reset tokens, and the output of a password hash verification all need it. Anything an attacker can submit and retry is a target.
  • Verify MACs, then decrypt. Check the authentication tag with a constant time compare before you act on the message. A leaky tag check is what the Lucky Thirteen family exploits.
  • Watch the compiler. An optimizer can turn your careful loop back into an early exit. Use the vetted library primitive, which is written and tested to stay constant time, rather than trusting hand written code to survive optimization.
  • Do not compare raw passwords. Store and compare a slow password hash, and use the verify function the hashing library provides, which is built to avoid this leak.

This same discipline, never branching on a secret, is what stops the nonce bit leaks behind a biased nonce attack. For the wider family of these bugs, see our hub on cryptographic oracle attacks, or start with the vulnerability basics category.

The assumption that breaks

A missing constant time comparison is a clean example of correct logic with a hidden side channel. The equality check returns the right answer. It just takes a little longer when more of the guess is right, and that timing is a channel the code never meant to open. It hides in review because if tag == expected reads as obviously correct. Finding it means reasoning about how a secret is actually compared in the app and what an attacker can measure about that comparison, not just whether the check exists. That is what an autonomous researcher that tests assumptions is built to do. Read more on our about page.

Frequently asked questions

What is constant time comparison?

It is a way to compare two secrets, such as a MAC tag or a token, that takes the same amount of time no matter where or whether they differ. Instead of returning at the first mismatched byte, it inspects every byte of both inputs and reports the result only at the end, so the running time reveals nothing about how many leading bytes matched.

How does an ordinary equality check leak a secret?

A normal comparison walks both inputs left to right and returns as soon as it finds a mismatch. The more leading bytes an attacker guesses correctly, the further the loop runs before failing, so the response takes slightly longer. That timing difference tells the attacker how close the guess is, which is exactly the information they need.

Can timing really be used to forge a tag?

Yes. An attacker fixes all bytes, sweeps the first byte through all 256 values, and keeps the one that makes the check measurably slower because it matched. They lock that byte in and move to the next. A 16 byte tag falls in about 16 times 256 requests instead of an infeasible offline guess. Network noise is beaten by averaging many requests per guess.

How do I implement constant time comparison correctly?

Use the platform function built for it, such as compare_digest in Python or timingSafeEqual elsewhere, rather than writing your own, because compilers can reintroduce an early exit. Apply it to every attacker submitted secret: MAC tags, tokens, API keys, and password hash checks. Be aware that comparing different length inputs can still leak length, so compare fixed size values when length is sensitive.


Put an autonomous researcher on your own systems

UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.