From 4bf7ef08061720586cb0a2f410720e26719d97f3 Mon Sep 17 00:00:00 2001 From: Andrey Andreev Date: Wed, 11 Jan 2017 21:54:38 +0200 Subject: [PATCH] Add hash_hkdf() --- NEWS | 3 + UPGRADING | 5 ++ ext/hash/hash.c | 125 ++++++++++++++++++++++++++ ext/hash/php_hash.h | 1 + ext/hash/tests/hash_hkdf_basic.phpt | 94 +++++++++++++++++++ ext/hash/tests/hash_hkdf_edges.phpt | 32 +++++++ ext/hash/tests/hash_hkdf_error.phpt | 100 +++++++++++++++++++++ ext/hash/tests/hash_hkdf_rfc5869.phpt | 80 +++++++++++++++++ 8 files changed, 440 insertions(+) create mode 100644 ext/hash/tests/hash_hkdf_basic.phpt create mode 100644 ext/hash/tests/hash_hkdf_edges.phpt create mode 100644 ext/hash/tests/hash_hkdf_error.phpt create mode 100644 ext/hash/tests/hash_hkdf_rfc5869.phpt diff --git a/NEWS b/NEWS index cd47e3d27b..f1b065d020 100644 --- a/NEWS +++ b/NEWS @@ -12,6 +12,9 @@ PHP NEWS . Fixed bug #73904 (php-cgi fails to load -c specified php.ini file). (Anatol) . Fixed bug #72898 (PHP_FCGI_CHILDREN is not included in phpinfo()). (Anatol) +- Hash: + . Added hash_hkdf() function. (Andrey Andreev) + - Mysqlnd: . Fixed bug #69899 (segfault on close() after free_result() with mysqlnd). (Richard Fussenegger) diff --git a/UPGRADING b/UPGRADING index 03b3b6799a..9e23d7a247 100644 --- a/UPGRADING +++ b/UPGRADING @@ -258,6 +258,11 @@ PHP 7.1 UPGRADE NOTES . Added curl_share_strerror() to convert error code to error message text describing the error. +- Hash: + . In PHP 7.1.2: Added hash_hkdf() function, which implements the HMAC-based + Key Derivation Function (HKDF) algorithm according to RFC 5869. The + implementation combines the Extract and Expand steps. + - pcntl: . Added pcntl_signal_get_handler() that returns the current signal handler for a particular signal. diff --git a/ext/hash/hash.c b/ext/hash/hash.c index 6d39985059..06c9f36705 100644 --- a/ext/hash/hash.c +++ b/ext/hash/hash.c @@ -597,6 +597,122 @@ PHP_FUNCTION(hash_algos) } /* }}} */ +static inline zend_bool php_hash_is_crypto(const char *algo, size_t algo_len) { + + char *blacklist[] = { "adler32", "crc32", "crc32b", "fnv132", "fnv1a32", "fnv164", "fnv1a64", "joaat", NULL }; + char *lower = zend_str_tolower_dup(algo, algo_len); + int i = 0; + + while (blacklist[i]) { + if (strcmp(lower, blacklist[i]) == 0) { + efree(lower); + return 0; + } + + i++; + } + + efree(lower); + return 1; +} + +/* {{{ proto string hash_hkdf(string algo, string ikm [, int length = 0, string info = '', string salt = '']) +RFC5869 HMAC-based key derivation function */ +PHP_FUNCTION(hash_hkdf) +{ + zend_string *returnval, *ikm, *algo, *info = NULL, *salt = NULL; + zend_long length = 0; + unsigned char *prk, *digest, *K; + int i, rounds; + const php_hash_ops *ops; + void *context; + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "SS|lSS", &algo, &ikm, &length, &info, &salt) == FAILURE) { + return; + } + + ops = php_hash_fetch_ops(ZSTR_VAL(algo), ZSTR_LEN(algo)); + if (!ops) { + php_error_docref(NULL, E_WARNING, "Unknown hashing algorithm: %s", ZSTR_VAL(algo)); + RETURN_FALSE; + } + + if (!php_hash_is_crypto(ZSTR_VAL(algo), ZSTR_LEN(algo))) { + php_error_docref(NULL, E_WARNING, "Non-cryptographic hashing algorithm: %s", ZSTR_VAL(algo)); + RETURN_FALSE; + } + + if (ZSTR_LEN(ikm) == 0) { + php_error_docref(NULL, E_WARNING, "Input keying material cannot be empty"); + RETURN_FALSE; + } + + if (length < 0) { + php_error_docref(NULL, E_WARNING, "Length must be greater than or equal to 0: " ZEND_LONG_FMT, length); + RETURN_FALSE; + } else if (length == 0) { + length = ops->digest_size; + } else if (length > ops->digest_size * 255) { + php_error_docref(NULL, E_WARNING, "Length must be less than or equal to %d: " ZEND_LONG_FMT, ops->digest_size * 255, length); + RETURN_FALSE; + } + + context = emalloc(ops->context_size); + + // Extract + ops->hash_init(context); + K = emalloc(ops->block_size); + php_hash_hmac_prep_key(K, ops, context, + (unsigned char *) (salt ? ZSTR_VAL(salt) : ""), salt ? ZSTR_LEN(salt) : 0); + + prk = emalloc(ops->digest_size); + php_hash_hmac_round(prk, ops, context, K, (unsigned char *) ZSTR_VAL(ikm), ZSTR_LEN(ikm)); + php_hash_string_xor_char(K, K, 0x6A, ops->block_size); + php_hash_hmac_round(prk, ops, context, K, prk, ops->digest_size); + ZEND_SECURE_ZERO(K, ops->block_size); + + // Expand + returnval = zend_string_alloc(length, 0); + digest = emalloc(ops->digest_size); + for (i = 1, rounds = (length - 1) / ops->digest_size + 1; i <= rounds; i++) { + // chr(i) + unsigned char c[1]; + c[0] = (i & 0xFF); + + php_hash_hmac_prep_key(K, ops, context, prk, ops->digest_size); + ops->hash_init(context); + ops->hash_update(context, K, ops->block_size); + + if (i > 1) { + ops->hash_update(context, digest, ops->digest_size); + } + + if (info != NULL && ZSTR_LEN(info) > 0) { + ops->hash_update(context, (unsigned char *) ZSTR_VAL(info), ZSTR_LEN(info)); + } + + ops->hash_update(context, c, 1); + ops->hash_final(digest, context); + php_hash_string_xor_char(K, K, 0x6A, ops->block_size); + php_hash_hmac_round(digest, ops, context, K, digest, ops->digest_size); + memcpy( + ZSTR_VAL(returnval) + ((i - 1) * ops->digest_size), + digest, + (i == rounds ? length - ((i - 1) * ops->digest_size) : ops->digest_size) + ); + } + + ZEND_SECURE_ZERO(K, ops->block_size); + ZEND_SECURE_ZERO(digest, ops->digest_size); + ZEND_SECURE_ZERO(prk, ops->digest_size); + efree(K); + efree(context); + efree(prk); + efree(digest); + ZSTR_VAL(returnval)[length] = 0; + RETURN_STR(returnval); +} + /* {{{ proto string hash_pbkdf2(string algo, string password, string salt, int iterations [, int length = 0, bool raw_output = false]) Generate a PBKDF2 hash of the given password and salt Returns lowercase hexits by default */ @@ -1205,6 +1321,14 @@ ZEND_BEGIN_ARG_INFO(arginfo_hash_equals, 0) ZEND_ARG_INFO(0, user_string) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO_EX(arginfo_hash_hkdf, 0, 0, 2) + ZEND_ARG_INFO(0, ikm) + ZEND_ARG_INFO(0, algo) + ZEND_ARG_INFO(0, length) + ZEND_ARG_INFO(0, string) + ZEND_ARG_INFO(0, salt) +ZEND_END_ARG_INFO() + /* BC Land */ #ifdef PHP_MHASH_BC ZEND_BEGIN_ARG_INFO(arginfo_mhash_get_block_size, 0) @@ -1253,6 +1377,7 @@ const zend_function_entry hash_functions[] = { PHP_FE(hash_algos, arginfo_hash_algos) PHP_FE(hash_pbkdf2, arginfo_hash_pbkdf2) PHP_FE(hash_equals, arginfo_hash_equals) + PHP_FE(hash_hkdf, arginfo_hash_hkdf) /* BC Land */ #ifdef PHP_HASH_MD5_NOT_IN_CORE diff --git a/ext/hash/php_hash.h b/ext/hash/php_hash.h index 51c8ff7a83..45a598c4dd 100644 --- a/ext/hash/php_hash.h +++ b/ext/hash/php_hash.h @@ -130,6 +130,7 @@ extern zend_module_entry hash_module_entry; PHP_FUNCTION(hash); PHP_FUNCTION(hash_file); +PHP_FUNCTION(hash_hkdf); PHP_FUNCTION(hash_hmac); PHP_FUNCTION(hash_hmac_file); PHP_FUNCTION(hash_init); diff --git a/ext/hash/tests/hash_hkdf_basic.phpt b/ext/hash/tests/hash_hkdf_basic.phpt new file mode 100644 index 0000000000..7a8ffde6a1 --- /dev/null +++ b/ext/hash/tests/hash_hkdf_basic.phpt @@ -0,0 +1,94 @@ +--TEST-- +Test hash_hkdf() function: basic functionality +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +*** Testing hash_hkdf(): basic functionality *** +md2: 87779851d2377dab25da16fd7aadfdf5 +md4: 422c6bd8dd2a6baae8abadef618c3ede +md5: 98b16391063ecee006a3ca8ee5776b1e +sha1: a71863230e3782240265126a53e137af6667e988 +sha224: 51678ceb17e803505187b2cf6451c30fbc572fda165bb69bbd117c7a +sha256: d8f0bede4b652933c32a92eccf7723f7eeb4701744c81325dc3f0fa9fda24499 +sha384: f600680e677bb417a7a22a4da8b167c0d91823a7a5d56a49aeb1838bb2320c05068d15d6d980824fee542a279d310c3a +sha512: fb1b86549e941b81821a89ac6ba7c4f93465077b3f2af94352ebf1d041efcd3c5694469c1ae31bb10db4c1d2ab84f07e4518ba33a3eadd4a149425750285c640 +ripemd128: cb6418fc0dc9efaeb7e9654390fa7f14 +ripemd160: ba42dbb34f08e9337ace15295f218754a41d6c39 +ripemd256: f2e96b292935e2395b59833ed89d928ac1197ff62c8031ebc06a3f5bad19513f +ripemd320: a13a682072525ceb4c4a5fef59096e682096e1096e6e7e238c7bd48a6f6c6a9ba3d7d9fbee6b68c4 +whirlpool: 497c717e04d896c3d582742c614435b7d0963b39de12dcf532540d39164b3b85214014620dfdff4a089a06b06aff43c39a3b4d9b806913cf6309de58ff1151f5 +tiger128,3: e13c2e7262892c6bd8dfc24121e7cb34 +tiger160,3: 48cc5a9f5e5d7029eb0544662222c0ba13822b7b +tiger192,3: 5a665d23b6cbb405668160e58b01aebef74eba979f4bc70b +tiger128,4: 8acf517ecf58cccbd65c1186d71e4116 +tiger160,4: cc0e33ee26700a2eb9a994bbb0e6cef29b429441 +tiger192,4: 97fa02d42331321fdc05c7f8dbc756d751ca36ce1aee69b0 +haval128,3: 2accab8029d42fb15fdbe9d3e2a470ca +haval160,3: 496fd29e7fc8351d2971b96a3733a7b3de000064 +haval192,3: 238a731801439b1f195e1a1568ce75251e1dd719d904a8a2 +haval224,3: d863e596ff6b2bdba1ed7b313df1c3d177176312e81b47e9290f7566 +haval256,3: 96f555fe41255c34fe57b275f1ae40bbb8f07c6a2a6d68c849748fbb393ff443 +haval128,4: 9822af229cc59527a72e231a690fad3b +haval160,4: 1bbbc4d632daaf94d5ba167efaa70af5b753effe +haval192,4: dd12a8f8919cbf5632497f0918b30236371dd1b55f71e824 +haval224,4: 8af449fb4eb627eb8887507c1279a116ac4325b5806dd22e2f2af410 +haval256,4: bd74a6d5fa1ec23a92ce1fd76c36bc8be36f5eddbea821545a91810e1f8d6fc5 +haval128,5: 84564f3450a6ccf6041162207dc8acba +haval160,5: b55cd1b3c514457b9e61c51ad22f302f6ec7cca1 +haval192,5: d1db7a8e69b327455d530d1ac60f774023b8b4bdd6bbbf92 +haval224,5: c5a2576511f1143c6e29f63d82d6e0be8f67d0bea448e27238be5000 +haval256,5: 9dbab73d13f1fd3a1b41398fe90ba1f298329681d861b023373c33f1051bd4d3 +snefru: 798eac954e5ece38e9acb63b50c1c2ecb799d34356358cec5a80eeeea91c8de9 +snefru256: 798eac954e5ece38e9acb63b50c1c2ecb799d34356358cec5a80eeeea91c8de9 +gost: 64edd584b87a2dfdd1f2b44ed2db8bd27af8386aafe751c2aebaed32dfa3852e diff --git a/ext/hash/tests/hash_hkdf_edges.phpt b/ext/hash/tests/hash_hkdf_edges.phpt new file mode 100644 index 0000000000..633efa4301 --- /dev/null +++ b/ext/hash/tests/hash_hkdf_edges.phpt @@ -0,0 +1,32 @@ +--TEST-- +Test hash_hkdf() function: edge cases +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +*** Testing hash_hkdf(): edge cases *** +Length < digestSize: 98b16391063ece +Length % digestSize != 0: 98b16391063ecee006a3ca8ee5776b1e5f +Algo name case-sensitivity: true +Non-crypto algo name case-sensitivity: + +Warning: hash_hkdf(): Non-cryptographic hashing algorithm: jOaAt in %s on line %d +bool(false) diff --git a/ext/hash/tests/hash_hkdf_error.phpt b/ext/hash/tests/hash_hkdf_error.phpt new file mode 100644 index 0000000000..ddda8df43b --- /dev/null +++ b/ext/hash/tests/hash_hkdf_error.phpt @@ -0,0 +1,100 @@ +--TEST-- +Test hash_hkdf() function: error conditions +--SKIPIF-- + +--FILE-- + +===Done=== +--EXPECTF-- +*** Testing hash_hkdf(): error conditions *** + +-- Testing hash_hkdf() function with less than expected no. of arguments -- + +Warning: hash_hkdf() expects at least 2 parameters, 0 given in %s on line %d +NULL + +Warning: hash_hkdf() expects at least 2 parameters, 1 given in %s on line %d +NULL + +-- Testing hash_hkdf() function with more than expected no. of arguments -- + +Warning: hash_hkdf() expects at most 5 parameters, 6 given in %s on line %d +NULL + +-- Testing hash_hkdf() function with invalid hash algorithm -- + +Warning: hash_hkdf(): Unknown hashing algorithm: foo in %s on line %d +bool(false) + +-- Testing hash_hkdf() function with non-cryptographic hash algorithm -- + +Warning: hash_hkdf(): Non-cryptographic hashing algorithm: adler32 in %s on line %d +bool(false) + +Warning: hash_hkdf(): Non-cryptographic hashing algorithm: crc32 in %s on line %d +bool(false) + +Warning: hash_hkdf(): Non-cryptographic hashing algorithm: crc32b in %s on line %d +bool(false) + +Warning: hash_hkdf(): Non-cryptographic hashing algorithm: fnv132 in %s on line %d +bool(false) + +Warning: hash_hkdf(): Non-cryptographic hashing algorithm: fnv1a32 in %s on line %d +bool(false) + +Warning: hash_hkdf(): Non-cryptographic hashing algorithm: fnv164 in %s on line %d +bool(false) + +Warning: hash_hkdf(): Non-cryptographic hashing algorithm: fnv1a64 in %s on line %d +bool(false) + +Warning: hash_hkdf(): Non-cryptographic hashing algorithm: joaat in %s on line %d +bool(false) + +-- Testing hash_hkdf() function with invalid parameters -- + +Warning: hash_hkdf(): Input keying material cannot be empty in %s on line %d +bool(false) + +Warning: hash_hkdf(): Length must be greater than or equal to 0: -1 in %s on line %d +bool(false) + +Warning: hash_hkdf(): Length must be less than or equal to 5100: 5101 in %s on line %d +bool(false) +===Done=== diff --git a/ext/hash/tests/hash_hkdf_rfc5869.phpt b/ext/hash/tests/hash_hkdf_rfc5869.phpt new file mode 100644 index 0000000000..592d6aee9a --- /dev/null +++ b/ext/hash/tests/hash_hkdf_rfc5869.phpt @@ -0,0 +1,80 @@ +--TEST-- +Test hash_hkdf() function: RFC 5869 test vectors +--SKIPIF-- + +--FILE-- + +===Done=== +--EXPECTF-- +*** Testing hash_hkdf(): RFC 5869 test vectors *** +Test case 1 (SHA-256): 3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865 +Test case 2 (SHA-256 with longer inputs/outputs): b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87 +Test case 3 (SHA-256 with zero-length salt, info): 8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8 +Test case 4 (SHA-1): 085a01ea1b10f36933068b56efa5ad81a4f14b822f5b091568a9cdd4f155fda2c22e422478d305f3f896 +Test case 5 (SHA-1 with longer inputs/outputs): 0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e927336d0441f4c4300e2cff0d0900b52d3b4 +Test case 6 (SHA-1 with zero-length salt, info): 0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0ea00033de03984d34918 +Test case 7 (SHA-1 with zero-length info, salt not provided): 2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5673a081d70cce7acfc48 +===Done=== -- 2.40.0