PHP Velho Oeste 2024

hash_equals

(PHP 5 >= 5.6.0, PHP 7, PHP 8)

hash_equalsタイミング攻撃に対しても安全な文字列比較

説明

hash_equals(string $known_string, string $user_string): bool

実行している間に known_string の内容を漏らすことなく、 ふたつの文字列が等しいかどうかを調べます。

この関数は、タイミング攻撃を緩和する目的で使えます。 === を用いた通常の比較は、値が異なるかどうかや、 値が最初に異なる位置に応じて、比較に多少時間がかかります。 よって、その間に秘密の known_string の内容が漏洩してしまいます。

警告

最初のパラメータよりはむしろ、 2つ目の文字列をユーザーが指定することが重要です。

パラメータ

known_string

秘密にしておく必要がある、既知の文字列

user_string

ユーザーが指定する、比較対象の文字列

戻り値

ふたつの文字列が等しい場合に true を返します。 そうでない場合に、false を返します。

例1 hash_equals() の例

<?php
$secretKey
= '8uRhAeH89naXfFXKGOEj';

// $value と $signature の値はユーザが提供します。
// 例: URL内の値と、$_GET から取得した値です
$value = 'username=rasmuslerdorf';
$signature = '8c35009d3b50caf7f5d2c1e031842e6b7823a1bb781d33c5237cd27b57b5f327';

if (
hash_equals(hash_hmac('sha256', $value, $secretKey), $signature)) {
echo
"The value is correctly signed.", PHP_EOL;
} else {
echo
"The value was tampered with.", PHP_EOL;
}
?>

上の例の出力は以下となります。

The value is correctly signed.

注意

注意:

指定するパラメータは両方、長さが等しくなければいけません。 違う長さの文字列が指定されると、この関数はすぐに false を返すので、 タイミング攻撃が行われた場合に既知の文字列の長さが漏洩するかもしれません。

参考

  • hash_hmac() - HMAC 方式を使用してハッシュ値を生成する

add a note add a note

User Contributed Notes 8 notes

up
9
examplehash at user dot com
3 years ago
None of the polyfills for this function to date are actually correct.  A timing attack defensive string comparison function must not take *any* shortcuts.  If the function performs a single if-then statement other than validating input types (i.e. making sure inputs are strings), then it is doing the comparison incorrectly.  But I'd argue that by the time you call the function, your inputs should have already been sanitized.  The top-most voted item by asphp incorrectly shortcuts on a comparison of strlen() (enabling a precision oracle attack on string length), while Markus shortcuts on knownstring (enabling an oracle attack on length of knownstring), and others shortcut in similar ways.

Here is a correct implementation in PHP adapted from a correct C implementation:

        function CTstrcmp($secret, $userinput)
        {
            $sx = 0;
            $sy = strlen($secret);
            $uy = strlen($userinput);
            $result = $sy - $uy;
            for ($ux = 0; $ux < $uy; $ux++)
            {
                $result |= ord($userinput[$ux]) ^ ord($secret[$sx]);
                $sx = ($sx + 1) % $sy;
            }

            return $result;
        }

Here, every byte of userinput is executed over regardless of the length of either string.  An oracle length attack is not possible since there is no comparison of lengths - just assignment and math.  The amount of time spent in the function is entirely dependent upon the length of userinput and can only be correct (i.e. return 0) if userinput exactly matches the secret.  Please take your time to understand the key differences between this function and the others.

If you are porting the above to another language, be aware of how strings and whatever strlen() equivalent works behind the scenes in your language.  PHP stores the size of the data separately so it can strlen() in O(1) time.
up
82
asphp at dsgml dot com
9 years ago
To transparently support this function on older versions of PHP use this:

<?php
if(!function_exists('hash_equals')) {
  function
hash_equals($str1, $str2) {
    if(
strlen($str1) != strlen($str2)) {
      return
false;
    } else {
     
$res = $str1 ^ $str2;
     
$ret = 0;
      for(
$i = strlen($res) - 1; $i >= 0; $i--) $ret |= ord($res[$i]);
      return !
$ret;
    }
  }
}
?>
up
35
Markus P. N.
9 years ago
I don't know why asphp at dsgml dot com got that many downvotes, the function seems to work.

I extended it a bit to support strings of diffent length and to handle errors and ran some tests:

The test results and how to reproduce them: http://pastebin.com/mLMXJeva

The function:
<?php

if (!function_exists('hash_equals')) {

   
/**
     * Timing attack safe string comparison
     *
     * Compares two strings using the same time whether they're equal or not.
     * This function should be used to mitigate timing attacks; for instance, when testing crypt() password hashes.
     *
     * @param string $known_string The string of known length to compare against
     * @param string $user_string The user-supplied string
     * @return boolean Returns TRUE when the two strings are equal, FALSE otherwise.
     */
   
function hash_equals($known_string, $user_string)
    {
        if (
func_num_args() !== 2) {
           
// handle wrong parameter count as the native implentation
           
trigger_error('hash_equals() expects exactly 2 parameters, ' . func_num_args() . ' given', E_USER_WARNING);
            return
null;
        }
        if (
is_string($known_string) !== true) {
           
trigger_error('hash_equals(): Expected known_string to be a string, ' . gettype($known_string) . ' given', E_USER_WARNING);
            return
false;
        }
       
$known_string_len = strlen($known_string);
       
$user_string_type_error = 'hash_equals(): Expected user_string to be a string, ' . gettype($user_string) . ' given'; // prepare wrong type error message now to reduce the impact of string concatenation and the gettype call
       
if (is_string($user_string) !== true) {
           
trigger_error($user_string_type_error, E_USER_WARNING);
           
// prevention of timing attacks might be still possible if we handle $user_string as a string of diffent length (the trigger_error() call increases the execution time a bit)
           
$user_string_len = strlen($user_string);
           
$user_string_len = $known_string_len + 1;
        } else {
           
$user_string_len = $known_string_len + 1;
           
$user_string_len = strlen($user_string);
        }
        if (
$known_string_len !== $user_string_len) {
           
$res = $known_string ^ $known_string; // use $known_string instead of $user_string to handle strings of diffrent length.
           
$ret = 1; // set $ret to 1 to make sure false is returned
       
} else {
           
$res = $known_string ^ $user_string;
           
$ret = 0;
        }
        for (
$i = strlen($res) - 1; $i >= 0; $i--) {
           
$ret |= ord($res[$i]);
        }
        return
$ret === 0;
    }

}

?>
up
10
s rotondo90 at gmail com
7 years ago
asphp has done a great job and that one of Markus P. N. is also good too.

However i made my own more concise version of the asphp code that supports different lenght strings and i used the same tests of Markus in order see how it works.

<?php
if(!function_exists('hash_equals')) {
    function
hash_equals($known_string, $user_string) {
       
$ret = 0;
       
        if (
strlen($known_string) !== strlen($user_string)) {
           
$user_string = $known_string;
           
$ret = 1;
        }
       
       
$res = $known_string ^ $user_string;
       
        for (
$i = strlen($res) - 1; $i >= 0; --$i) {
           
$ret |= ord($res[$i]);
        }
       
        return !
$ret;
    }
}
?>
up
6
David Grudl
8 years ago
Very short timing attack safe string comparison for PHP < 5.6

<?php
function hash_equals($a, $b) {
    return
substr_count($a ^ $b, "\0") * 2 === strlen($a . $b);
}
?>
up
9
Cedric Van Bockhaven
9 years ago
Our server does not support the hash_equals function. We are using the following snippet which also has support for strings of different length:

<?php
if(!function_exists('hash_equals')) {
    function
hash_equals($a, $b) {
       
$ret = strlen($a) ^ strlen($b);
       
$ret |= array_sum(unpack("C*", $a^$b));
        return !
$ret;
    }
}
?>
up
-17
jcastromail at yahoo dot es
6 years ago
What is a timing attack?

In short, a timing attack is a way to predict a encryption by using the time to takes to compare two encrypted values.  Depending the encryption, if you want to compare two encrypted values, if one of them is completely different then the system takes less time than if it has  something similarities   The difference of time is usually in microseconds.   So, giving several attempts, in theory its possible to crack a system.

However, usually a system is composed by several services and serving more than a customer at the same time. So, even the interval of time of execute an operation PHP is always variable, its not exact at all. For example, if you run a benchmark in php

<?php
$time_start
= microtime(true);
$expected  = crypt('12345', '$2a$07$usesomesillystringforsalt$');
$correct   = crypt('12345', '$2a$07$usesomesillystringforsalt$');
$result=hash_equals($expected, $correct);

$time_end = microtime(true);
echo 
$time_end - $time_start;
?>

or
<?php
$time_start
= microtime(true);

$expected  = crypt('12345', '$2a$07$usesomesillystringforsalt$');
$correct   = crypt('12345', '$2a$07$usesomesillystringforsalt$');
$result=$expected==$correct;

$time_end = microtime(true);
echo 
$time_end - $time_start;
?>

you will get different values each time you run it. So, we are free of timing attack without even using this function, and we aren't even considering the delay of the network, delay of the browser and so on.   Even in the same machine and running

So, in theory a timing attack is possible, however empirically its not.
up
-55
enclaved
6 years ago
This is a WARNING to everyone!

Using hash_equals() is fine in itself, but the NEED for such comparison isn't, and should be an alarm to you that your PHP program design is severely flawed somewhere. The golden rule has it: NEVER do any sort of crypto in PHP in the first place. Cryptography doesn't belong in PHP code for a variety of reasons.

First of all, it is a very sloppy and insecure enterprise; I keep seeing script kiddos trying to roll their own crypto again and again without a iota of understanding. Homemade crypto appliance is worse than no crypto, it is pure vice. If you're not a professional, well-educated, experienced cryptographer, then forget about writing your own crypto code forever, don't even dare to consider it before you master the subject in every tiny aspect, because you WILL make mistakes. Dangerous, fatal mistakes. To add insult to injury, in PHP security flaws may be exceptionally non-obvious due to the complexity of Zend--the chances are there will always be a side channel attack opportunity, and you'll never figure it out, because your code MAY appear bug-less, but some internal aspect of the Zend framework still opens a side channel attack vector. Remember: crypto does not belong in scripting languages because of their very misleading and unpredictable nature (in terms of cryptographic environments); crypto must be written in solid, compiled, purely deterministic languages like C, C++, and Fortran.

Second, PHP is neither the right tool nor the right environment for cryptography. In a typical site system, PHP is not in place to do crypto, because there are better places to do it: the HTTP server and the RDBMS. Just memorize this single rule of thumb: cryptographic secrets must never cross subsystem/layer boundaries:

    Database <-- ! --> CGI program <-- ! --> HTTP server

Cryptographic tasks are performed either by the HTTP server (e.g. authentication of users with client SSL certificates) or the RDBMS (e.g. password-based access to data), and these tasks must be ENCAPSULATED inside the facility, self-contained. For example, if you store KDF-derived digests of passwords in an SQL database, you must NOT compare digests in PHP, but only in SQL queries or stored procedures. Once produced and put into the database, a password digest (or any other sensitive data) must not exit it in any way as-is, be it a SELECT query or some other way, that is considered a leak in the cryptosystem. Use ONLY database-provided means to perform any crypto operations.

As PostgreSQL is the usual database of choice for technically advanced and sound WWW or intranet sites, my advice is to use its pgcrypto extension, it is mature, well-tested, and has all the right tools. Here's a textbook password handling example to illustrate how secrets can be confined within the database layer without extracting them into the PHP layer. Password digest derivation and storing:

    INSERT INTO account (digest) VALUES (crypt('password', gen_salt('bf')));

Verification:

    SELECT digest = crypt('password', gen_salt(digest)) FROM account;

Exceptionally simple, elegant, clean, and secure (Blowfish is more than enough for user-set passwords), isn't it? You can clearly see that after the initial INSERT (or any subsequent UPDATEs) the password digest never leaves the RDBMS, that is, never gets trasmitted in any form over the client-server link, the entire checking procedure is wholly executed by the RDBMS, i.e. it is encapsulated and isolated. And the best part about it: no PHP involved in crypto! This IS the way to go if application security is on your checklist.

The SELECT query above performs password checking without disclosing the digest it is comparing against, which is exactly my point. It would be illogical, impractical, and just stupid to use hash_equals() in this scenario when the RDBMS itself can do just fine.
To Top