Skip to content

Commit 2148a88

Browse files
committed
Add hash_equals() to perform string comparisons that are not vulnerable to timing attacks.
1 parent 2499b65 commit 2148a88

File tree

5 files changed

+98
-0
lines changed

5 files changed

+98
-0
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ PHP NEWS
3030
- Hash:
3131
. Fixed bug #66698 (Missing FNV1a32 and FNV1a64 hash functions).
3232
(Michael M Slusarz).
33+
. Implemented timing attack safe string comparison function
34+
(RFC: https://wiki.php.net/rfc/timing_attack). (Rouven Weßling)
3335

3436
- Intl:
3537
. Fixed bug #66873 (A reproductible crash in UConverter when given invalid

UPGRADING

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ PHP 5.6 UPGRADE NOTES
7171
- Added use function and use const.
7272
(https://wiki.php.net/rfc/use_function)
7373

74+
- Added a function for timing attack safe string comparison
75+
(https://wiki.php.net/rfc/timing_attack)
76+
7477
- Added gost-crypto (CryptoPro S-box) hash algorithm.
7578

7679
- Stream wrappers verify peer certificates and host names by default in
@@ -182,6 +185,9 @@ PHP 5.6 UPGRADE NOTES
182185
5. New Functions
183186
========================================
184187

188+
- Hash
189+
Added hash_equals($known_string, $user_string)
190+
185191
- GMP:
186192
Added gmp_root($a, $nth) and gmp_rootrem($a, $nth) for calculating nth roots.
187193

ext/hash/hash.c

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,46 @@ PHP_FUNCTION(hash_pbkdf2)
728728
}
729729
/* }}} */
730730

731+
/* {{{ proto bool hash_equals(string known_string, string user_string)
732+
Compares two strings using the same time whether they're equal or not.
733+
A difference in length will leak */
734+
PHP_FUNCTION(hash_equals)
735+
{
736+
zval *known_zval, *user_zval;
737+
char *known_str, *user_str;
738+
int result = 0, j;
739+
740+
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "zz", &known_zval, &user_zval) == FAILURE) {
741+
return;
742+
}
743+
744+
/* We only allow comparing string to prevent unexpected results. */
745+
if (Z_TYPE_P(known_zval) != IS_STRING) {
746+
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Expected known_string to be a string, %s given", zend_zval_type_name(known_zval));
747+
RETURN_FALSE;
748+
}
749+
750+
if (Z_TYPE_P(user_zval) != IS_STRING) {
751+
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Expected user_string to be a string, %s given", zend_zval_type_name(user_zval));
752+
RETURN_FALSE;
753+
}
754+
755+
if (Z_STRLEN_P(known_zval) != Z_STRLEN_P(user_zval)) {
756+
RETURN_FALSE;
757+
}
758+
759+
known_str = Z_STRVAL_P(known_zval);
760+
user_str = Z_STRVAL_P(user_zval);
761+
762+
/* This is security sensitive code. Do not optimize this for speed. */
763+
for (j = 0; j < Z_STRLEN_P(known_zval); j++) {
764+
result |= known_str[j] ^ user_str[j];
765+
}
766+
767+
RETURN_BOOL(0 == result);
768+
}
769+
/* }}} */
770+
731771
/* Module Housekeeping */
732772

733773
static void php_hash_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC) /* {{{ */
@@ -1152,6 +1192,11 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_hash_pbkdf2, 0, 0, 4)
11521192
ZEND_ARG_INFO(0, raw_output)
11531193
ZEND_END_ARG_INFO()
11541194

1195+
ZEND_BEGIN_ARG_INFO(arginfo_hash_equals, 0)
1196+
ZEND_ARG_INFO(0, known_string)
1197+
ZEND_ARG_INFO(0, user_string)
1198+
ZEND_END_ARG_INFO()
1199+
11551200
/* BC Land */
11561201
#ifdef PHP_MHASH_BC
11571202
ZEND_BEGIN_ARG_INFO(arginfo_mhash_get_block_size, 0)
@@ -1199,6 +1244,7 @@ const zend_function_entry hash_functions[] = {
11991244

12001245
PHP_FE(hash_algos, arginfo_hash_algos)
12011246
PHP_FE(hash_pbkdf2, arginfo_hash_pbkdf2)
1247+
PHP_FE(hash_equals, arginfo_hash_equals)
12021248

12031249
/* BC Land */
12041250
#ifdef PHP_HASH_MD5_NOT_IN_CORE

ext/hash/php_hash.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ PHP_FUNCTION(hash_update_file);
136136
PHP_FUNCTION(hash_final);
137137
PHP_FUNCTION(hash_algos);
138138
PHP_FUNCTION(hash_pbkdf2);
139+
PHP_FUNCTION(hash_equals);
139140

140141
PHP_HASH_API const php_hash_ops *php_hash_fetch_ops(const char *algo, int algo_len);
141142
PHP_HASH_API void php_hash_register_algo(const char *algo, const php_hash_ops *ops);

ext/hash/tests/hash_equals.phpt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
--TEST--
2+
hash_equals() function
3+
--FILE--
4+
<?php
5+
var_dump(hash_equals("same", "same"));
6+
var_dump(hash_equals("not1same", "not2same"));
7+
var_dump(hash_equals("short", "longer"));
8+
var_dump(hash_equals("longer", "short"));
9+
var_dump(hash_equals("", "notempty"));
10+
var_dump(hash_equals("notempty", ""));
11+
var_dump(hash_equals("", ""));
12+
var_dump(hash_equals(123, "NaN"));
13+
var_dump(hash_equals("NaN", 123));
14+
var_dump(hash_equals(123, 123));
15+
var_dump(hash_equals(null, ""));
16+
var_dump(hash_equals(null, 123));
17+
var_dump(hash_equals(null, null));
18+
--EXPECTF--
19+
bool(true)
20+
bool(false)
21+
bool(false)
22+
bool(false)
23+
bool(false)
24+
bool(false)
25+
bool(true)
26+
27+
Warning: hash_equals(): Expected known_string to be a string, integer given in %s on line %d
28+
bool(false)
29+
30+
Warning: hash_equals(): Expected user_string to be a string, integer given in %s on line %d
31+
bool(false)
32+
33+
Warning: hash_equals(): Expected known_string to be a string, integer given in %s on line %d
34+
bool(false)
35+
36+
Warning: hash_equals(): Expected known_string to be a string, null given in %s on line %d
37+
bool(false)
38+
39+
Warning: hash_equals(): Expected known_string to be a string, null given in %s on line %d
40+
bool(false)
41+
42+
Warning: hash_equals(): Expected known_string to be a string, null given in %s on line %d
43+
bool(false)

0 commit comments

Comments
 (0)