Preventing Timing Attacks on String Comparison with a Double HMAC Strategy

Modern cryptography protocols are more likely to be broken by side-channel information leaks than the persistent efforts of clever mathematicians.

One of the common cryptographic side-channels that developers should be aware of is how long a specific operation, such as a string comparison, takes to complete. Thus, they are called timing attacks.

A common place to find a timing attack vulnerability in a cryptography library is in the MAC validation logic.

if ($mac === hash_hmac('sha256', $message, $authKey)) { return decrypt($message); }

Timing attacks are possible because string comparison (usually implemented internally via memcmp() ) is optimized. For example: If you compare "apple" with "acorn", it will take measurably longer (on a nanosecond scale) than if you were to compare "apple" with "pecan", simply because the first character matches. This allows an attacker to slowly guess the first character, then the second, etc. of a valid Message Authentication Code for a forged message.

Plugging Timing Leaks

There are two basic defensive strategies against timing attacks:

Ensure that the string comparison always takes the same amount of time to complete. Use a strategy called Double HMAC to blind the side-channel.

The first is easily satisfied by helper functions (e.g. hash_equals() in PHP 5.6 and newer). Where libc functions like memcmp() will return false earlier if the first byte is different in both strings than it would if they matched, these functions compare every byte.

/** * Constant-time comparison * @param string $a * @param string $b * @return bool */ function constant_time_compare($a, $b) { $len = mb_strlen($a, '8bit'); // See mbstring.func_overload if ($len !== mb_strlen($b, '8bit')) { return false; // Length differs } $diff = 0; for ($i = 0; $i < $len; ++$i) { $diff |= ord($a[$i]) ^ ord($b[$i]); } // If all the bytes in $a and $b are identical, $diff should be equal to 0 return $diff === 0; }

This strategy is generally effective, but it's hard to explain to developers who have never seriously worked with cryptography before. (It doesn't help that most cryptographic side-channels sound like the result of H.P. Lovecraft writing about computer hardware.) Additionally, a sufficiently advanced compiler is indistinguishable from an adversary. How can you guarantee that your constant-time code is constant-time on every platform, with every compiler?

These concerns have led many security to propose a Double HMAC strategy instead of writing a constant time comparison loop where one is not already provided (e.g. PHP before 5.6.0).

A Word of Caution

If you are considering adopting a Double HMAC strategy instead of a constant-time string comparison loop because you believe that there is any security loss if the length of the strings leaks out, you are gravely mistaken.

The output of HMAC is dependent on hash functions, whose output lengths are public information. No sane cryptography protocol that employs constant time string comparison for comparing the output of two hash functions (i.e. for message integrity) depends on the length of those hash function outputs remaining a secret.

In fact, it's conceptually impossible to stop length from leaking out. Consider the following:

$m1 = str_repeat("\x80", 32); $m2 = str_repeat("\x80", 33554432); // Start the clock HERE: $hash1 = hash('sha256', $m1); $hash2 = hash('sha256', $m2);

Which hash operation takes longer to complete? The answer is predictable.

Don't worry about leaking the string length. It's a complete non-issue.

Simple Double HMAC Comparison

A naive approach to Double HMAC takes code that looks like this:

if ($mac === hash_hmac('sha256', $message, $authKey)) { return decrypt($message); }

... and instead makes it look like this:

define('CONSTANT_COMPARE_KEY', "\x00\x01\x02\x03\x04\x05\x06\x07". "\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F". "\x10\x11\x12\x13\x14\x15\x16\x17". "\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"); /** * Compare two strings with deterministic blinding * * @param string $a * @param string $b * @return bool */ function hmac_compare_static($a, $b) { return ( hash_hmac('sha256', $a, CONSTANT_COMPARE_KEY) === hash_hmac('sha256', $b, CONSTANT_COMPARE_KEY) ); } // ... $calc = hash_hmac('sha256', $message, $authKey); if (hmac_compare_static($mac, $calc)) { return decrypt($message); }

This works because instead of allowing an attacker to slowly forge a valid MAC for their invalid ciphertext, it blinds the comparison by the hash function (due to the avalanche effect). If you change one byte, the actual strings involved in the comparison will (with overwhelming probability) become something completely different, and thus you don't get a useful timing side-channel out of the mix.

However, this isn't a perfect solution:

You've just reduced the security of your hash comparison to depend on an additional secret key ( CONSTANT_COMPARE_KEY above). If you send the same message twice, the comparison is predictable, since hash functions are deterministic.

This concern isn't known to be practically exploitable (the cryptography literature is sparse on successful blind remote timing attacks), but there is a rule in cryptography: Attacks only get better, they never get worse. A tangential discussion about Blind Birthday attacks by Defuse Security indicates that they're not entirely impractical.

Double HMAC Comparison with a Random Key

To truly blind the operation (i.e. render it non-deterministic), simply drop the constant key for string comparison and instead use a random HMAC key each time (thus making it a nonce):

/** * Compare two strings with non-deterministic blinding * * @param string $a * @param string $b * @return bool */ function hmac_compare($a, $b) { $compare_key = random_bytes(32); return hash_hmac('sha256', $a, $compare_key) === hash_hmac('sha256', $b, $compare_key); } // ... $calc = hash_hmac('sha256', $message, $authKey); if (hmac_compare($mac, $calc)) { return decrypt($message); }

Assuming a cryptographically secure random number generator is available, this completely blinds the side-channel (send the same forged message twice, the time duration is non-deterministic; attackers will therefore never receive useful timing information). And in the worst case (a weak PRNG), you don't find yourself in a worse position than the deterministic Double HMAC comparison.

In Closing