Skip to content

Commit 4bf7ef0

Browse files
narfbgnikic
authored andcommitted
Add hash_hkdf()
1 parent 687b027 commit 4bf7ef0

8 files changed

+440
-0
lines changed

NEWS

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ PHP NEWS
1212
. Fixed bug #73904 (php-cgi fails to load -c specified php.ini file). (Anatol)
1313
. Fixed bug #72898 (PHP_FCGI_CHILDREN is not included in phpinfo()). (Anatol)
1414

15+
- Hash:
16+
. Added hash_hkdf() function. (Andrey Andreev)
17+
1518
- Mysqlnd:
1619
. Fixed bug #69899 (segfault on close() after free_result() with mysqlnd).
1720
(Richard Fussenegger)

UPGRADING

+5
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,11 @@ PHP 7.1 UPGRADE NOTES
258258
. Added curl_share_strerror() to convert error code to error message text
259259
describing the error.
260260

261+
- Hash:
262+
. In PHP 7.1.2: Added hash_hkdf() function, which implements the HMAC-based
263+
Key Derivation Function (HKDF) algorithm according to RFC 5869. The
264+
implementation combines the Extract and Expand steps.
265+
261266
- pcntl:
262267
. Added pcntl_signal_get_handler() that returns the current signal handler
263268
for a particular signal.

ext/hash/hash.c

+125
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,122 @@ PHP_FUNCTION(hash_algos)
597597
}
598598
/* }}} */
599599

600+
static inline zend_bool php_hash_is_crypto(const char *algo, size_t algo_len) {
601+
602+
char *blacklist[] = { "adler32", "crc32", "crc32b", "fnv132", "fnv1a32", "fnv164", "fnv1a64", "joaat", NULL };
603+
char *lower = zend_str_tolower_dup(algo, algo_len);
604+
int i = 0;
605+
606+
while (blacklist[i]) {
607+
if (strcmp(lower, blacklist[i]) == 0) {
608+
efree(lower);
609+
return 0;
610+
}
611+
612+
i++;
613+
}
614+
615+
efree(lower);
616+
return 1;
617+
}
618+
619+
/* {{{ proto string hash_hkdf(string algo, string ikm [, int length = 0, string info = '', string salt = ''])
620+
RFC5869 HMAC-based key derivation function */
621+
PHP_FUNCTION(hash_hkdf)
622+
{
623+
zend_string *returnval, *ikm, *algo, *info = NULL, *salt = NULL;
624+
zend_long length = 0;
625+
unsigned char *prk, *digest, *K;
626+
int i, rounds;
627+
const php_hash_ops *ops;
628+
void *context;
629+
630+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "SS|lSS", &algo, &ikm, &length, &info, &salt) == FAILURE) {
631+
return;
632+
}
633+
634+
ops = php_hash_fetch_ops(ZSTR_VAL(algo), ZSTR_LEN(algo));
635+
if (!ops) {
636+
php_error_docref(NULL, E_WARNING, "Unknown hashing algorithm: %s", ZSTR_VAL(algo));
637+
RETURN_FALSE;
638+
}
639+
640+
if (!php_hash_is_crypto(ZSTR_VAL(algo), ZSTR_LEN(algo))) {
641+
php_error_docref(NULL, E_WARNING, "Non-cryptographic hashing algorithm: %s", ZSTR_VAL(algo));
642+
RETURN_FALSE;
643+
}
644+
645+
if (ZSTR_LEN(ikm) == 0) {
646+
php_error_docref(NULL, E_WARNING, "Input keying material cannot be empty");
647+
RETURN_FALSE;
648+
}
649+
650+
if (length < 0) {
651+
php_error_docref(NULL, E_WARNING, "Length must be greater than or equal to 0: " ZEND_LONG_FMT, length);
652+
RETURN_FALSE;
653+
} else if (length == 0) {
654+
length = ops->digest_size;
655+
} else if (length > ops->digest_size * 255) {
656+
php_error_docref(NULL, E_WARNING, "Length must be less than or equal to %d: " ZEND_LONG_FMT, ops->digest_size * 255, length);
657+
RETURN_FALSE;
658+
}
659+
660+
context = emalloc(ops->context_size);
661+
662+
// Extract
663+
ops->hash_init(context);
664+
K = emalloc(ops->block_size);
665+
php_hash_hmac_prep_key(K, ops, context,
666+
(unsigned char *) (salt ? ZSTR_VAL(salt) : ""), salt ? ZSTR_LEN(salt) : 0);
667+
668+
prk = emalloc(ops->digest_size);
669+
php_hash_hmac_round(prk, ops, context, K, (unsigned char *) ZSTR_VAL(ikm), ZSTR_LEN(ikm));
670+
php_hash_string_xor_char(K, K, 0x6A, ops->block_size);
671+
php_hash_hmac_round(prk, ops, context, K, prk, ops->digest_size);
672+
ZEND_SECURE_ZERO(K, ops->block_size);
673+
674+
// Expand
675+
returnval = zend_string_alloc(length, 0);
676+
digest = emalloc(ops->digest_size);
677+
for (i = 1, rounds = (length - 1) / ops->digest_size + 1; i <= rounds; i++) {
678+
// chr(i)
679+
unsigned char c[1];
680+
c[0] = (i & 0xFF);
681+
682+
php_hash_hmac_prep_key(K, ops, context, prk, ops->digest_size);
683+
ops->hash_init(context);
684+
ops->hash_update(context, K, ops->block_size);
685+
686+
if (i > 1) {
687+
ops->hash_update(context, digest, ops->digest_size);
688+
}
689+
690+
if (info != NULL && ZSTR_LEN(info) > 0) {
691+
ops->hash_update(context, (unsigned char *) ZSTR_VAL(info), ZSTR_LEN(info));
692+
}
693+
694+
ops->hash_update(context, c, 1);
695+
ops->hash_final(digest, context);
696+
php_hash_string_xor_char(K, K, 0x6A, ops->block_size);
697+
php_hash_hmac_round(digest, ops, context, K, digest, ops->digest_size);
698+
memcpy(
699+
ZSTR_VAL(returnval) + ((i - 1) * ops->digest_size),
700+
digest,
701+
(i == rounds ? length - ((i - 1) * ops->digest_size) : ops->digest_size)
702+
);
703+
}
704+
705+
ZEND_SECURE_ZERO(K, ops->block_size);
706+
ZEND_SECURE_ZERO(digest, ops->digest_size);
707+
ZEND_SECURE_ZERO(prk, ops->digest_size);
708+
efree(K);
709+
efree(context);
710+
efree(prk);
711+
efree(digest);
712+
ZSTR_VAL(returnval)[length] = 0;
713+
RETURN_STR(returnval);
714+
}
715+
600716
/* {{{ proto string hash_pbkdf2(string algo, string password, string salt, int iterations [, int length = 0, bool raw_output = false])
601717
Generate a PBKDF2 hash of the given password and salt
602718
Returns lowercase hexits by default */
@@ -1205,6 +1321,14 @@ ZEND_BEGIN_ARG_INFO(arginfo_hash_equals, 0)
12051321
ZEND_ARG_INFO(0, user_string)
12061322
ZEND_END_ARG_INFO()
12071323

1324+
ZEND_BEGIN_ARG_INFO_EX(arginfo_hash_hkdf, 0, 0, 2)
1325+
ZEND_ARG_INFO(0, ikm)
1326+
ZEND_ARG_INFO(0, algo)
1327+
ZEND_ARG_INFO(0, length)
1328+
ZEND_ARG_INFO(0, string)
1329+
ZEND_ARG_INFO(0, salt)
1330+
ZEND_END_ARG_INFO()
1331+
12081332
/* BC Land */
12091333
#ifdef PHP_MHASH_BC
12101334
ZEND_BEGIN_ARG_INFO(arginfo_mhash_get_block_size, 0)
@@ -1253,6 +1377,7 @@ const zend_function_entry hash_functions[] = {
12531377
PHP_FE(hash_algos, arginfo_hash_algos)
12541378
PHP_FE(hash_pbkdf2, arginfo_hash_pbkdf2)
12551379
PHP_FE(hash_equals, arginfo_hash_equals)
1380+
PHP_FE(hash_hkdf, arginfo_hash_hkdf)
12561381

12571382
/* BC Land */
12581383
#ifdef PHP_HASH_MD5_NOT_IN_CORE

ext/hash/php_hash.h

+1
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ extern zend_module_entry hash_module_entry;
130130

131131
PHP_FUNCTION(hash);
132132
PHP_FUNCTION(hash_file);
133+
PHP_FUNCTION(hash_hkdf);
133134
PHP_FUNCTION(hash_hmac);
134135
PHP_FUNCTION(hash_hmac_file);
135136
PHP_FUNCTION(hash_init);

ext/hash/tests/hash_hkdf_basic.phpt

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
--TEST--
2+
Test hash_hkdf() function: basic functionality
3+
--SKIPIF--
4+
<?php extension_loaded('hash') or die('skip: hash extension not loaded.'); ?>
5+
--FILE--
6+
<?php
7+
8+
/* Prototype : string hkdf ( string $algo , string $ikm [, int $length , string $info = '' , string $salt = '' ] )
9+
* Description: HMAC-based Key Derivation Function
10+
* Source code: ext/hash/hash.c
11+
*/
12+
13+
echo "*** Testing hash_hkdf(): basic functionality ***\n";
14+
15+
$ikm = 'input key material';
16+
17+
echo 'md2: ', bin2hex(hash_hkdf('md2', $ikm)), "\n";
18+
echo 'md4: ', bin2hex(hash_hkdf('md4', $ikm)), "\n";
19+
echo 'md5: ', bin2hex(hash_hkdf('md5', $ikm)), "\n";
20+
echo 'sha1: ', bin2hex(hash_hkdf('sha1', $ikm)), "\n";
21+
echo 'sha224: ', bin2hex(hash_hkdf('sha224', $ikm)), "\n";
22+
echo 'sha256: ', bin2hex(hash_hkdf('sha256', $ikm)), "\n";
23+
echo 'sha384: ', bin2hex(hash_hkdf('sha384', $ikm)), "\n";
24+
echo 'sha512: ', bin2hex(hash_hkdf('sha512', $ikm)), "\n";
25+
echo 'ripemd128: ', bin2hex(hash_hkdf('ripemd128', $ikm)), "\n";
26+
echo 'ripemd160: ', bin2hex(hash_hkdf('ripemd160', $ikm)), "\n";
27+
echo 'ripemd256: ', bin2hex(hash_hkdf('ripemd256', $ikm)), "\n";
28+
echo 'ripemd320: ', bin2hex(hash_hkdf('ripemd320', $ikm)), "\n";
29+
echo 'whirlpool: ', bin2hex(hash_hkdf('whirlpool', $ikm)), "\n";
30+
echo 'tiger128,3: ', bin2hex(hash_hkdf('tiger128,3', $ikm)), "\n";
31+
echo 'tiger160,3: ', bin2hex(hash_hkdf('tiger160,3', $ikm)), "\n";
32+
echo 'tiger192,3: ', bin2hex(hash_hkdf('tiger192,3', $ikm)), "\n";
33+
echo 'tiger128,4: ', bin2hex(hash_hkdf('tiger128,4', $ikm)), "\n";
34+
echo 'tiger160,4: ', bin2hex(hash_hkdf('tiger160,4', $ikm)), "\n";
35+
echo 'tiger192,4: ', bin2hex(hash_hkdf('tiger192,4', $ikm)), "\n";
36+
echo 'haval128,3: ', bin2hex(hash_hkdf('haval128,3', $ikm)), "\n";
37+
echo 'haval160,3: ', bin2hex(hash_hkdf('haval160,3', $ikm)), "\n";
38+
echo 'haval192,3: ', bin2hex(hash_hkdf('haval192,3', $ikm)), "\n";
39+
echo 'haval224,3: ', bin2hex(hash_hkdf('haval224,3', $ikm)), "\n";
40+
echo 'haval256,3: ', bin2hex(hash_hkdf('haval256,3', $ikm)), "\n";
41+
echo 'haval128,4: ', bin2hex(hash_hkdf('haval128,4', $ikm)), "\n";
42+
echo 'haval160,4: ', bin2hex(hash_hkdf('haval160,4', $ikm)), "\n";
43+
echo 'haval192,4: ', bin2hex(hash_hkdf('haval192,4', $ikm)), "\n";
44+
echo 'haval224,4: ', bin2hex(hash_hkdf('haval224,4', $ikm)), "\n";
45+
echo 'haval256,4: ', bin2hex(hash_hkdf('haval256,4', $ikm)), "\n";
46+
echo 'haval128,5: ', bin2hex(hash_hkdf('haval128,5', $ikm)), "\n";
47+
echo 'haval160,5: ', bin2hex(hash_hkdf('haval160,5', $ikm)), "\n";
48+
echo 'haval192,5: ', bin2hex(hash_hkdf('haval192,5', $ikm)), "\n";
49+
echo 'haval224,5: ', bin2hex(hash_hkdf('haval224,5', $ikm)), "\n";
50+
echo 'haval256,5: ', bin2hex(hash_hkdf('haval256,5', $ikm)), "\n";
51+
echo 'snefru: ', bin2hex(hash_hkdf('snefru', $ikm)), "\n";
52+
echo 'snefru256: ', bin2hex(hash_hkdf('snefru256', $ikm)), "\n";
53+
echo 'gost: ', bin2hex(hash_hkdf('gost', $ikm)), "\n";
54+
55+
?>
56+
--EXPECTF--
57+
*** Testing hash_hkdf(): basic functionality ***
58+
md2: 87779851d2377dab25da16fd7aadfdf5
59+
md4: 422c6bd8dd2a6baae8abadef618c3ede
60+
md5: 98b16391063ecee006a3ca8ee5776b1e
61+
sha1: a71863230e3782240265126a53e137af6667e988
62+
sha224: 51678ceb17e803505187b2cf6451c30fbc572fda165bb69bbd117c7a
63+
sha256: d8f0bede4b652933c32a92eccf7723f7eeb4701744c81325dc3f0fa9fda24499
64+
sha384: f600680e677bb417a7a22a4da8b167c0d91823a7a5d56a49aeb1838bb2320c05068d15d6d980824fee542a279d310c3a
65+
sha512: fb1b86549e941b81821a89ac6ba7c4f93465077b3f2af94352ebf1d041efcd3c5694469c1ae31bb10db4c1d2ab84f07e4518ba33a3eadd4a149425750285c640
66+
ripemd128: cb6418fc0dc9efaeb7e9654390fa7f14
67+
ripemd160: ba42dbb34f08e9337ace15295f218754a41d6c39
68+
ripemd256: f2e96b292935e2395b59833ed89d928ac1197ff62c8031ebc06a3f5bad19513f
69+
ripemd320: a13a682072525ceb4c4a5fef59096e682096e1096e6e7e238c7bd48a6f6c6a9ba3d7d9fbee6b68c4
70+
whirlpool: 497c717e04d896c3d582742c614435b7d0963b39de12dcf532540d39164b3b85214014620dfdff4a089a06b06aff43c39a3b4d9b806913cf6309de58ff1151f5
71+
tiger128,3: e13c2e7262892c6bd8dfc24121e7cb34
72+
tiger160,3: 48cc5a9f5e5d7029eb0544662222c0ba13822b7b
73+
tiger192,3: 5a665d23b6cbb405668160e58b01aebef74eba979f4bc70b
74+
tiger128,4: 8acf517ecf58cccbd65c1186d71e4116
75+
tiger160,4: cc0e33ee26700a2eb9a994bbb0e6cef29b429441
76+
tiger192,4: 97fa02d42331321fdc05c7f8dbc756d751ca36ce1aee69b0
77+
haval128,3: 2accab8029d42fb15fdbe9d3e2a470ca
78+
haval160,3: 496fd29e7fc8351d2971b96a3733a7b3de000064
79+
haval192,3: 238a731801439b1f195e1a1568ce75251e1dd719d904a8a2
80+
haval224,3: d863e596ff6b2bdba1ed7b313df1c3d177176312e81b47e9290f7566
81+
haval256,3: 96f555fe41255c34fe57b275f1ae40bbb8f07c6a2a6d68c849748fbb393ff443
82+
haval128,4: 9822af229cc59527a72e231a690fad3b
83+
haval160,4: 1bbbc4d632daaf94d5ba167efaa70af5b753effe
84+
haval192,4: dd12a8f8919cbf5632497f0918b30236371dd1b55f71e824
85+
haval224,4: 8af449fb4eb627eb8887507c1279a116ac4325b5806dd22e2f2af410
86+
haval256,4: bd74a6d5fa1ec23a92ce1fd76c36bc8be36f5eddbea821545a91810e1f8d6fc5
87+
haval128,5: 84564f3450a6ccf6041162207dc8acba
88+
haval160,5: b55cd1b3c514457b9e61c51ad22f302f6ec7cca1
89+
haval192,5: d1db7a8e69b327455d530d1ac60f774023b8b4bdd6bbbf92
90+
haval224,5: c5a2576511f1143c6e29f63d82d6e0be8f67d0bea448e27238be5000
91+
haval256,5: 9dbab73d13f1fd3a1b41398fe90ba1f298329681d861b023373c33f1051bd4d3
92+
snefru: 798eac954e5ece38e9acb63b50c1c2ecb799d34356358cec5a80eeeea91c8de9
93+
snefru256: 798eac954e5ece38e9acb63b50c1c2ecb799d34356358cec5a80eeeea91c8de9
94+
gost: 64edd584b87a2dfdd1f2b44ed2db8bd27af8386aafe751c2aebaed32dfa3852e

ext/hash/tests/hash_hkdf_edges.phpt

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
Test hash_hkdf() function: edge cases
3+
--SKIPIF--
4+
<?php extension_loaded('hash') or die('skip: hash extension not loaded.'); ?>
5+
--FILE--
6+
<?php
7+
8+
/* Prototype : string hkdf ( string $algo , string $ikm [, int $length , string $info = '' , string $salt = '' ] )
9+
* Description: HMAC-based Key Derivation Function
10+
* Source code: ext/hash/hash.c
11+
*/
12+
13+
echo "*** Testing hash_hkdf(): edge cases ***\n";
14+
15+
$ikm = 'input key material';
16+
17+
echo 'Length < digestSize: ', bin2hex(hash_hkdf('md5', $ikm, 7)), "\n";
18+
echo 'Length % digestSize != 0: ', bin2hex(hash_hkdf('md5', $ikm, 17)), "\n";
19+
echo 'Algo name case-sensitivity: ', (bin2hex(hash_hkdf('Md5', $ikm, 7)) === '98b16391063ece' ? 'true' : 'false'), "\n";
20+
echo "Non-crypto algo name case-sensitivity:\n";
21+
var_dump(hash_hkdf('jOaAt', $ikm));
22+
23+
?>
24+
--EXPECTF--
25+
*** Testing hash_hkdf(): edge cases ***
26+
Length < digestSize: 98b16391063ece
27+
Length % digestSize != 0: 98b16391063ecee006a3ca8ee5776b1e5f
28+
Algo name case-sensitivity: true
29+
Non-crypto algo name case-sensitivity:
30+
31+
Warning: hash_hkdf(): Non-cryptographic hashing algorithm: jOaAt in %s on line %d
32+
bool(false)

0 commit comments

Comments
 (0)