From fb860314066b21c0b29a63c840c6e83a3e26fb95 Mon Sep 17 00:00:00 2001 From: Remi Collet Date: Thu, 16 May 2024 10:06:52 +0200 Subject: [PATCH] rebase to 8.3.7 backport Argon2 password hashing in OpenSSL ext Fix CVE-2024-2756 CVE-2024-2757 CVE-2024-3096 Resolves: RHEL-32794 Resolves: RHEL-32749 Resolves: RHEL-32762 Resolves: RHEL-36582 --- .gitignore | 24 +- php-8.3.7-argon2.patch | 842 +++++++++++++++++++++++++++++++++++++++++ php.spec | 10 +- sources | 4 +- 4 files changed, 856 insertions(+), 24 deletions(-) create mode 100644 php-8.3.7-argon2.patch diff --git a/.gitignore b/.gitignore index 0d09893..a778e1a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,11 @@ clog TODO *.md +/php-8.0.*.tar.xz +/php-8.0.*.tar.xz.asc /php-8.1.*.tar.xz /php-8.1.*.tar.xz.asc /php-8.2.*.tar.xz /php-8.2.*.tar.xz.asc -/php-8.3.0RC3.tar.xz -/php-8.3.0RC3.tar.xz.asc -/php-8.3.0RC4.tar.xz -/php-8.3.0RC4.tar.xz.asc -/php-8.3.0RC5.tar.xz -/php-8.3.0RC5.tar.xz.asc -/php-8.3.0RC6.tar.xz -/php-8.3.0RC6.tar.xz.asc -/php-8.3.0.tar.xz -/php-8.3.0.tar.xz.asc -/php-8.3.1RC3.tar.xz -/php-8.3.1RC3.tar.xz.asc -/php-8.3.1.tar.xz -/php-8.3.1.tar.xz.asc -/php-8.3.2RC1.tar.xz -/php-8.3.2RC1.tar.xz.asc -/php-8.3.2.tar.xz -/php-8.3.2.tar.xz.asc -/php-8.3.3RC1.tar.xz -/php-8.3.3RC1.tar.xz.asc +/php-8.3.7.tar.xz +/php-8.3.7.tar.xz.asc diff --git a/php-8.3.7-argon2.patch b/php-8.3.7-argon2.patch new file mode 100644 index 0000000..d6d2766 --- /dev/null +++ b/php-8.3.7-argon2.patch @@ -0,0 +1,842 @@ +From c6c39b2b1cb1ff9916a8db606b19fc4282feacd2 Mon Sep 17 00:00:00 2001 +From: Remi Collet +Date: Wed, 17 Apr 2024 15:58:49 +0200 +Subject: [PATCH] Implement PASSWORD_ARGON2 from OpenSSL 3.2 Backported from + 8.4 to 8.3 + +--- + ext/openssl/config0.m4 | 2 +- + ext/openssl/openssl.c | 24 + + ext/openssl/openssl_pwhash.c | 412 ++++++++++++++++++ + ext/openssl/openssl_pwhash.stub.php | 38 ++ + ext/openssl/openssl_pwhash_arginfo.h | Bin 0 -> 2571 bytes + ext/openssl/php_openssl.h | 31 +- + ext/openssl/tests/openssl_password.phpt | 42 ++ + .../tests/openssl_password_compat.phpt | 52 +++ + .../tests/openssl_password_compat2.phpt | 52 +++ + 9 files changed, 651 insertions(+), 2 deletions(-) + create mode 100644 ext/openssl/openssl_pwhash.c + create mode 100644 ext/openssl/openssl_pwhash.stub.php + create mode 100644 ext/openssl/openssl_pwhash_arginfo.h + create mode 100644 ext/openssl/tests/openssl_password.phpt + create mode 100644 ext/openssl/tests/openssl_password_compat.phpt + create mode 100644 ext/openssl/tests/openssl_password_compat2.phpt + +diff --git a/ext/openssl/config0.m4 b/ext/openssl/config0.m4 +index ffd4e0751c..aae3812752 100644 +--- a/ext/openssl/config0.m4 ++++ b/ext/openssl/config0.m4 +@@ -18,7 +18,7 @@ PHP_ARG_WITH([system-ciphers], + [no]) + + if test "$PHP_OPENSSL" != "no"; then +- PHP_NEW_EXTENSION(openssl, openssl.c xp_ssl.c, $ext_shared) ++ PHP_NEW_EXTENSION(openssl, openssl.c openssl_pwhash.c xp_ssl.c, $ext_shared) + PHP_SUBST(OPENSSL_SHARED_LIBADD) + + if test "$PHP_KERBEROS" != "no"; then +diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c +index 6f85e9852f..1f164a60b6 100644 +--- a/ext/openssl/openssl.c ++++ b/ext/openssl/openssl.c +@@ -263,9 +263,21 @@ static void php_openssl_pkey_free_obj(zend_object *object) + zend_object_std_dtor(&key_object->std); + } + ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++static const zend_module_dep openssl_deps[] = { ++ ZEND_MOD_REQUIRED("standard") ++ ZEND_MOD_END ++}; ++#endif ++ + /* {{{ openssl_module_entry */ + zend_module_entry openssl_module_entry = { ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ STANDARD_MODULE_HEADER_EX, NULL, ++ openssl_deps, ++#else + STANDARD_MODULE_HEADER, ++#endif + "openssl", + ext_functions, + PHP_MINIT(openssl), +@@ -1321,6 +1333,12 @@ PHP_MINIT_FUNCTION(openssl) + + REGISTER_INI_ENTRIES(); + ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ if (FAILURE == PHP_MINIT(openssl_pwhash)(INIT_FUNC_ARGS_PASSTHRU)) { ++ return FAILURE; ++ } ++#endif ++ + return SUCCESS; + } + /* }}} */ +@@ -1395,6 +1413,12 @@ PHP_MSHUTDOWN_FUNCTION(openssl) + php_stream_xport_unregister("tlsv1.3"); + #endif + ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ if (FAILURE == PHP_MSHUTDOWN(openssl_pwhash)(SHUTDOWN_FUNC_ARGS_PASSTHRU)) { ++ return FAILURE; ++ } ++#endif ++ + /* reinstate the default tcp handler */ + php_stream_xport_register("tcp", php_stream_generic_socket_factory); + +diff --git a/ext/openssl/openssl_pwhash.c b/ext/openssl/openssl_pwhash.c +new file mode 100644 +index 0000000000..56ab62ff83 +--- /dev/null ++++ b/ext/openssl/openssl_pwhash.c +@@ -0,0 +1,412 @@ ++/* ++ +----------------------------------------------------------------------+ ++ | Copyright (c) The PHP Group | ++ +----------------------------------------------------------------------+ ++ | This source file is subject to version 3.01 of the PHP license, | ++ | that is bundled with this package in the file LICENSE, and is | ++ | available through the world-wide-web at the following url: | ++ | https://www.php.net/license/3_01.txt | ++ | If you did not receive a copy of the PHP license and are unable to | ++ | obtain it through the world-wide-web, please send a note to | ++ | license@php.net so we can mail you a copy immediately. | ++ +----------------------------------------------------------------------+ ++ | Authors: Remi Collet | ++ +----------------------------------------------------------------------+ ++*/ ++ ++#ifdef HAVE_CONFIG_H ++# include "config.h" ++#endif ++ ++#include "php.h" ++#include "ext/standard/php_password.h" ++#include "php_openssl.h" ++ ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++#include "Zend/zend_attributes.h" ++#include "openssl_pwhash_arginfo.h" ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#define PHP_OPENSSL_MEMLIMIT_MIN 8u ++#define PHP_OPENSSL_MEMLIMIT_MAX UINT32_MAX ++#define PHP_OPENSSL_ITERLIMIT_MIN 1u ++#define PHP_OPENSSL_ITERLIMIT_MAX UINT32_MAX ++#define PHP_OPENSSL_THREADS_MIN 1u ++#define PHP_OPENSSL_THREADS_MAX UINT32_MAX ++ ++#define PHP_OPENSSL_ARGON_VERSION 0x13 ++ ++#define PHP_OPENSSL_SALT_SIZE 16 ++#define PHP_OPENSSL_HASH_SIZE 32 ++#define PHP_OPENSSL_DIGEST_SIZE 128 ++ ++static inline zend_result get_options(zend_array *options, uint32_t *memlimit, uint32_t *iterlimit, uint32_t *threads) ++{ ++ zval *opt; ++ ++ *iterlimit = PHP_OPENSSL_PWHASH_ITERLIMIT; ++ *memlimit = PHP_OPENSSL_PWHASH_MEMLIMIT; ++ *threads = PHP_OPENSSL_PWHASH_THREADS; ++ ++ if (!options) { ++ return SUCCESS; ++ } ++ if ((opt = zend_hash_str_find(options, "memory_cost", strlen("memory_cost")))) { ++ zend_long smemlimit = zval_get_long(opt); ++ ++ if ((smemlimit < 0) || (smemlimit < PHP_OPENSSL_MEMLIMIT_MIN) || (smemlimit > (PHP_OPENSSL_MEMLIMIT_MAX))) { ++ zend_value_error("Memory cost is outside of allowed memory range"); ++ return FAILURE; ++ } ++ *memlimit = smemlimit; ++ } ++ if ((opt = zend_hash_str_find(options, "time_cost", strlen("time_cost")))) { ++ zend_long siterlimit = zval_get_long(opt); ++ if ((siterlimit < PHP_OPENSSL_ITERLIMIT_MIN) || (siterlimit > PHP_OPENSSL_ITERLIMIT_MAX)) { ++ zend_value_error("Time cost is outside of allowed time range"); ++ return FAILURE; ++ } ++ *iterlimit = siterlimit; ++ } ++ if ((opt = zend_hash_str_find(options, "threads", strlen("threads"))) && (zval_get_long(opt) != 1)) { ++ zend_long sthreads = zval_get_long(opt); ++ if ((sthreads < PHP_OPENSSL_THREADS_MIN) || (sthreads > PHP_OPENSSL_THREADS_MAX)) { ++ zend_value_error("Invalid number of threads"); ++ return FAILURE; ++ } ++ *threads = sthreads; ++ } ++ return SUCCESS; ++} ++ ++static bool php_openssl_argon2_compute_hash( ++ const char *algo, ++ uint32_t version, uint32_t memlimit, uint32_t iterlimit, uint32_t threads, ++ const char *pass, size_t pass_len, ++ const unsigned char *salt, size_t salt_len, ++ unsigned char *hash, size_t hash_len) ++{ ++ OSSL_PARAM params[7], *p = params; ++ EVP_KDF *kdf = NULL; ++ EVP_KDF_CTX *kctx = NULL; ++ bool ret = false; ++ ++ if (threads > 1) { ++ if (OSSL_set_max_threads(NULL, threads) != 1) { ++ goto fail; ++ } ++ } ++ p = params; ++ *p++ = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_THREADS, ++ &threads); ++ *p++ = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_LANES, ++ &threads); ++ *p++= OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ITER, ++ &iterlimit); ++ *p++ = OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_MEMCOST, ++ &memlimit); ++ *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT, ++ (void *)salt, salt_len); ++ *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_PASSWORD, ++ (void *)pass, pass_len); ++ *p++ = OSSL_PARAM_construct_end(); ++ ++ if ((kdf = EVP_KDF_fetch(NULL, algo, NULL)) == NULL) { ++ goto fail; ++ } ++ if ((kctx = EVP_KDF_CTX_new(kdf)) == NULL) { ++ goto fail; ++ } ++ if (EVP_KDF_derive(kctx, hash, hash_len, params) != 1) { ++ zend_value_error("Unexpected failure hashing password"); ++ goto fail; ++ } ++ ++ ret = true; ++ ++fail: ++ EVP_KDF_free(kdf); ++ EVP_KDF_CTX_free(kctx); ++ ++ if (threads > 1) { ++ OSSL_set_max_threads(NULL, 0); ++ } ++ return ret; ++} ++ ++static zend_string *php_openssl_argon2_hash(const zend_string *password, zend_array *options, const char *algo) ++{ ++ uint32_t iterlimit, memlimit, threads, version = PHP_OPENSSL_ARGON_VERSION; ++ zend_string *digest = NULL, *salt64 = NULL, *hash64 = NULL; ++ unsigned char hash[PHP_OPENSSL_HASH_SIZE+1], salt[PHP_OPENSSL_SALT_SIZE+1]; ++ ++ if ((ZSTR_LEN(password) >= UINT32_MAX)) { ++ zend_value_error("Password is too long"); ++ return NULL; ++ } ++ if (get_options(options, &memlimit, &iterlimit, &threads) == FAILURE) { ++ return NULL; ++ } ++ if (RAND_bytes(salt, PHP_OPENSSL_SALT_SIZE) <= 0) { ++ return NULL; ++ } ++ ++ if (!php_openssl_argon2_compute_hash(algo, version, memlimit, iterlimit, threads, ++ ZSTR_VAL(password), ZSTR_LEN(password), salt, PHP_OPENSSL_SALT_SIZE, hash, PHP_OPENSSL_HASH_SIZE)) { ++ return NULL; ++ } ++ ++ hash64 = php_base64_encode(hash, PHP_OPENSSL_HASH_SIZE); ++ /* No padding utsing 32 *4 / 3 = 42.6 (43 + 1 padding char) */ ++ ZEND_ASSERT(ZSTR_LEN(hash64)==44 && ZSTR_VAL(hash64)[43]=='='); ++ ZSTR_VAL(hash64)[43] = 0; ++ ZSTR_LEN(hash64) = 43; ++ ++ salt64 = php_base64_encode(salt, PHP_OPENSSL_SALT_SIZE); ++ /* No padding using 16 *4 / 3 = 21.3 (22 + 2 padding char) */ ++ ZEND_ASSERT(ZSTR_LEN(salt64)==24 && ZSTR_VAL(salt64)[22]=='=' && ZSTR_VAL(salt64)[23]=='='); ++ ZSTR_VAL(salt64)[22] = 0; ++ ZSTR_LEN(salt64) = 22; ++ ++ digest = zend_string_alloc(PHP_OPENSSL_DIGEST_SIZE, 0); ++ ZSTR_LEN(digest) = snprintf(ZSTR_VAL(digest), ZSTR_LEN(digest), "$%s$v=%d$m=%u,t=%u,p=%u$%s$%s", ++ algo, version, memlimit, iterlimit, threads, ZSTR_VAL(salt64), ZSTR_VAL(hash64)); ++ ++ zend_string_release(salt64); ++ zend_string_release(hash64); ++ ++ return digest; ++} ++ ++static int php_openssl_argon2_extract( ++ const zend_string *digest, uint32_t *version, uint32_t *memlimit, uint32_t *iterlimit, ++ uint32_t *threads, zend_string **salt, zend_string **hash) ++{ ++ const char *p; ++ char *hash64, *salt64; ++ ++ if (!digest || (ZSTR_LEN(digest) < sizeof("$argon2id$"))) { ++ return FAILURE; ++ } ++ p = ZSTR_VAL(digest); ++ if (!memcmp(p, "$argon2i$", strlen("$argon2i$"))) { ++ p += strlen("$argon2i$"); ++ } else if (!memcmp(p, "$argon2id$", strlen("$argon2id$"))) { ++ p += strlen("$argon2id$"); ++ } else { ++ return FAILURE; ++ } ++ if (sscanf(p, "v=%" PRIu32 "$m=%" PRIu32 ",t=%" PRIu32 ",p=%" PRIu32, ++ version, memlimit, iterlimit, threads) != 4) { ++ return FAILURE; ++ } ++ if (salt && hash) { ++ /* start of param */ ++ p = strchr(p, '$'); ++ if (!p) { ++ return FAILURE; ++ } ++ /* start of salt */ ++ p = strchr(p+1, '$'); ++ if (!p) { ++ return FAILURE; ++ } ++ salt64 = estrdup(p+1); ++ /* start of hash */ ++ hash64 = strchr(salt64, '$'); ++ if (!hash64) { ++ efree(salt64); ++ return FAILURE; ++ } ++ *hash64++ = 0; ++ *salt = php_base64_decode((unsigned char *)salt64, strlen(salt64)); ++ *hash = php_base64_decode((unsigned char *)hash64, strlen(hash64)); ++ efree(salt64); ++ } ++ return SUCCESS; ++} ++ ++static bool php_openssl_argon2_verify(const zend_string *password, const zend_string *digest, const char *algo) ++{ ++ uint32_t version, iterlimit, memlimit, threads; ++ zend_string *salt, *hash, *new; ++ bool ret = false; ++ ++ if ((ZSTR_LEN(password) >= UINT32_MAX) || (ZSTR_LEN(digest) >= UINT32_MAX)) { ++ return false; ++ } ++ if (FAILURE == php_openssl_argon2_extract(digest, &version, &memlimit, &iterlimit, &threads, &salt, &hash)) { ++ return false; ++ } ++ ++ new = zend_string_alloc(ZSTR_LEN(hash), 0); ++ if (php_openssl_argon2_compute_hash(algo, version, memlimit, iterlimit, threads, ++ ZSTR_VAL(password), ZSTR_LEN(password), (unsigned char *)ZSTR_VAL(salt), ++ ZSTR_LEN(salt), (unsigned char *)ZSTR_VAL(new), ZSTR_LEN(new))) { ++ ret = (php_safe_bcmp(hash, new) == 0); ++ } ++ ++ zend_string_release(new); ++ zend_string_release(salt); ++ zend_string_release(hash); ++ ++ return ret; ++} ++ ++static bool php_openssl_argon2i_verify(const zend_string *password, const zend_string *digest) ++{ ++ return php_openssl_argon2_verify(password, digest, "argon2i"); ++} ++ ++static bool php_openssl_argon2id_verify(const zend_string *password, const zend_string *digest) ++{ ++ return php_openssl_argon2_verify(password, digest, "argon2id"); ++} ++ ++static bool php_openssl_argon2_needs_rehash(const zend_string *hash, zend_array *options) ++{ ++ uint32_t version, iterlimit, memlimit, threads; ++ uint32_t new_version = PHP_OPENSSL_ARGON_VERSION, new_iterlimit, new_memlimit, new_threads; ++ ++ if (FAILURE == get_options(options, &new_memlimit, &new_iterlimit, &new_threads)) { ++ return true; ++ } ++ if (FAILURE == php_openssl_argon2_extract(hash, &version, &memlimit, &iterlimit, &threads, NULL, NULL)) { ++ return true; ++ } ++ ++ // Algo already checked in pasword_needs_rehash implementation ++ return (version != new_version) || ++ (iterlimit != new_iterlimit) || ++ (memlimit != new_memlimit) || ++ (threads != new_threads); ++} ++ ++static int php_openssl_argon2_get_info(zval *return_value, const zend_string *hash) ++{ ++ uint32_t v, threads; ++ uint32_t memory_cost; ++ uint32_t time_cost; ++ ++ if (FAILURE == php_openssl_argon2_extract(hash, &v, &memory_cost, &time_cost, &threads, NULL, NULL)) { ++ return FAILURE; ++ } ++ add_assoc_long(return_value, "memory_cost", memory_cost); ++ add_assoc_long(return_value, "time_cost", time_cost); ++ add_assoc_long(return_value, "threads", threads); ++ ++ return SUCCESS; ++} ++ ++ ++static zend_string *php_openssl_argon2i_hash(const zend_string *password, zend_array *options) ++{ ++ return php_openssl_argon2_hash(password, options, "argon2i"); ++} ++ ++static const php_password_algo openssl_algo_argon2i = { ++ "argon2i", ++ php_openssl_argon2i_hash, ++ php_openssl_argon2i_verify, ++ php_openssl_argon2_needs_rehash, ++ php_openssl_argon2_get_info, ++ NULL, ++}; ++ ++static zend_string *php_openssl_argon2id_hash(const zend_string *password, zend_array *options) ++{ ++ return php_openssl_argon2_hash(password, options, "argon2id"); ++} ++ ++static const php_password_algo openssl_algo_argon2id = { ++ "argon2id", ++ php_openssl_argon2id_hash, ++ php_openssl_argon2id_verify, ++ php_openssl_argon2_needs_rehash, ++ php_openssl_argon2_get_info, ++ NULL, ++}; ++ ++PHP_FUNCTION(openssl_password_hash) ++{ ++ zend_string *password, *algo, *digest; ++ zend_array *options = NULL; ++ ++ ZEND_PARSE_PARAMETERS_START(2, 3) ++ Z_PARAM_STR(algo) ++ Z_PARAM_STR(password) ++ Z_PARAM_OPTIONAL ++ Z_PARAM_ARRAY_HT(options) ++ ZEND_PARSE_PARAMETERS_END(); ++ ++ if (strcmp(ZSTR_VAL(algo), "argon2i") && strcmp(ZSTR_VAL(algo), "argon2id")) { ++ zend_argument_value_error(1, "must be a valid password openssl hashing algorithm"); ++ RETURN_THROWS(); ++ } ++ ++ digest = php_openssl_argon2_hash(password, options, ZSTR_VAL(algo)); ++ if (!digest) { ++ if (!EG(exception)) { ++ zend_throw_error(NULL, "Password hashing failed for unknown reason"); ++ } ++ RETURN_THROWS(); ++ } ++ ++ RETURN_NEW_STR(digest); ++} ++ ++PHP_FUNCTION(openssl_password_verify) ++{ ++ zend_string *password, *algo, *digest; ++ ++ ZEND_PARSE_PARAMETERS_START(3, 3) ++ Z_PARAM_STR(algo) ++ Z_PARAM_STR(password) ++ Z_PARAM_STR(digest) ++ ZEND_PARSE_PARAMETERS_END(); ++ ++ if (strcmp(ZSTR_VAL(algo), "argon2i") && strcmp(ZSTR_VAL(algo), "argon2id")) { ++ zend_argument_value_error(1, "must be a valid password openssl hashing algorithm"); ++ RETURN_THROWS(); ++ } ++ ++ RETURN_BOOL(php_openssl_argon2_verify(password, digest, ZSTR_VAL(algo))); ++} ++ ++PHP_MINIT_FUNCTION(openssl_pwhash) ++{ ++ zend_string *argon2i = ZSTR_INIT_LITERAL("argon2i", 1); ++ ++ zend_register_functions(NULL, ext_functions, NULL, type); ++ ++ if (php_password_algo_find(argon2i)) { ++ /* Nothing to do. Core or sodium has registered these algorithms for us. */ ++ zend_string_release(argon2i); ++ return SUCCESS; ++ } ++ zend_string_release(argon2i); ++ ++ register_openssl_pwhash_symbols(module_number); ++ ++ if (FAILURE == php_password_algo_register("argon2i", &openssl_algo_argon2i)) { ++ return FAILURE; ++ } ++ if (FAILURE == php_password_algo_register("argon2id", &openssl_algo_argon2id)) { ++ return FAILURE; ++ } ++ ++ return SUCCESS; ++} ++ ++PHP_MSHUTDOWN_FUNCTION(openssl_pwhash) ++{ ++ zend_unregister_functions(ext_functions, -1, NULL); ++ ++ return SUCCESS; ++} ++#endif /* PHP_OPENSSL_API_VERSION >= 0x30200 */ +diff --git a/ext/openssl/openssl_pwhash.stub.php b/ext/openssl/openssl_pwhash.stub.php +new file mode 100644 +index 0000000000..85c2f04d55 +--- /dev/null ++++ b/ext/openssl/openssl_pwhash.stub.php +@@ -0,0 +1,38 @@ ++= 0x30200 ++/** ++ * @var string ++ */ ++const PASSWORD_ARGON2I = "argon2i"; ++/** ++ * @var string ++ */ ++const PASSWORD_ARGON2ID = "argon2id"; ++/** ++ * @var int ++ * @cvalue PHP_OPENSSL_PWHASH_MEMLIMIT ++ */ ++const PASSWORD_ARGON2_DEFAULT_MEMORY_COST = UNKNOWN; ++/** ++ * @var int ++ * @cvalue PHP_OPENSSL_PWHASH_ITERLIMIT ++ */ ++const PASSWORD_ARGON2_DEFAULT_TIME_COST = UNKNOWN; ++/** ++ * @var int ++ * @cvalue PHP_OPENSSL_PWHASH_THREADS ++ */ ++const PASSWORD_ARGON2_DEFAULT_THREADS = UNKNOWN; ++/** ++ * @var string ++ */ ++const PASSWORD_ARGON2_PROVIDER = "openssl"; ++ ++function openssl_password_hash(string $algo, #[\SensitiveParameter] string $password, array $options = []): string {} ++function openssl_password_verify(string $algo, #[\SensitiveParameter] string $password, string $hash): bool {} ++ ++#endif ++ +diff --git a/ext/openssl/openssl_pwhash_arginfo.h b/ext/openssl/openssl_pwhash_arginfo.h +new file mode 100644 +index 0000000000..f60a1f5b08 +--- /dev/null ++++ b/ext/openssl/openssl_pwhash_arginfo.h +@@ -0,0 +1,68 @@ ++/* This is a generated file, edit the .stub.php file instead. ++ * Stub hash: a01216f790c4c42499bd85448aacb3a6d58acc94 */ ++ ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_password_hash, 0, 2, IS_STRING, 0) ++ ZEND_ARG_TYPE_INFO(0, algo, IS_STRING, 0) ++ ZEND_ARG_TYPE_INFO(0, password, IS_STRING, 0) ++ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, options, IS_ARRAY, 0, "[]") ++ZEND_END_ARG_INFO() ++#endif ++ ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_password_verify, 0, 3, _IS_BOOL, 0) ++ ZEND_ARG_TYPE_INFO(0, algo, IS_STRING, 0) ++ ZEND_ARG_TYPE_INFO(0, password, IS_STRING, 0) ++ ZEND_ARG_TYPE_INFO(0, hash, IS_STRING, 0) ++ZEND_END_ARG_INFO() ++#endif ++ ++ ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ZEND_FUNCTION(openssl_password_hash); ++#endif ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ZEND_FUNCTION(openssl_password_verify); ++#endif ++ ++ ++static const zend_function_entry ext_functions[] = { ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ ZEND_FE(openssl_password_hash, arginfo_openssl_password_hash) ++#endif ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ ZEND_FE(openssl_password_verify, arginfo_openssl_password_verify) ++#endif ++ ZEND_FE_END ++}; ++ ++static void register_openssl_pwhash_symbols(int module_number) ++{ ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ REGISTER_STRING_CONSTANT("PASSWORD_ARGON2I", "argon2i", CONST_PERSISTENT); ++#endif ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ REGISTER_STRING_CONSTANT("PASSWORD_ARGON2ID", "argon2id", CONST_PERSISTENT); ++#endif ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ REGISTER_LONG_CONSTANT("PASSWORD_ARGON2_DEFAULT_MEMORY_COST", PHP_OPENSSL_PWHASH_MEMLIMIT, CONST_PERSISTENT); ++#endif ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ REGISTER_LONG_CONSTANT("PASSWORD_ARGON2_DEFAULT_TIME_COST", PHP_OPENSSL_PWHASH_ITERLIMIT, CONST_PERSISTENT); ++#endif ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ REGISTER_LONG_CONSTANT("PASSWORD_ARGON2_DEFAULT_THREADS", PHP_OPENSSL_PWHASH_THREADS, CONST_PERSISTENT); ++#endif ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ REGISTER_STRING_CONSTANT("PASSWORD_ARGON2_PROVIDER", "openssl", CONST_PERSISTENT); ++#endif ++ ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ ++ zend_add_parameter_attribute(zend_hash_str_find_ptr(CG(function_table), "openssl_password_hash", sizeof("openssl_password_hash") - 1), 1, ZSTR_KNOWN(ZEND_STR_SENSITIVEPARAMETER), 0); ++#endif ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++ ++ zend_add_parameter_attribute(zend_hash_str_find_ptr(CG(function_table), "openssl_password_verify", sizeof("openssl_password_verify") - 1), 1, ZSTR_KNOWN(ZEND_STR_SENSITIVEPARAMETER), 0); ++#endif ++} +diff --git a/ext/openssl/php_openssl.h b/ext/openssl/php_openssl.h +index 304854b4bf..9532cfe6f9 100644 +--- a/ext/openssl/php_openssl.h ++++ b/ext/openssl/php_openssl.h +@@ -39,8 +39,10 @@ extern zend_module_entry openssl_module_entry; + #define PHP_OPENSSL_API_VERSION 0x10002 + #elif OPENSSL_VERSION_NUMBER < 0x30000000L + #define PHP_OPENSSL_API_VERSION 0x10100 +-#else ++#elif OPENSSL_VERSION_NUMBER < 0x30200000L + #define PHP_OPENSSL_API_VERSION 0x30000 ++#else ++#define PHP_OPENSSL_API_VERSION 0x30200 + #endif + #endif + +@@ -158,6 +160,33 @@ static inline php_openssl_certificate_object *php_openssl_certificate_from_obj(z + + #define Z_OPENSSL_CERTIFICATE_P(zv) php_openssl_certificate_from_obj(Z_OBJ_P(zv)) + ++#if PHP_OPENSSL_API_VERSION >= 0x30200 ++/** ++ * MEMLIMIT is normalized to KB even though sodium uses Bytes in order to ++ * present a consistent user-facing API. ++ * ++ * When updating these values, synchronize ext/standard/php_password.h values. ++ */ ++#if defined(PHP_PASSWORD_ARGON2_MEMORY_COST) ++#define PHP_OPENSSL_PWHASH_MEMLIMIT PHP_PASSWORD_ARGON2_MEMORY_COST ++#else ++#define PHP_OPENSSL_PWHASH_MEMLIMIT (64 << 10) ++#endif ++#if defined(PHP_PASSWORD_ARGON2_TIME_COST) ++#define PHP_OPENSSL_PWHASH_ITERLIMIT PHP_PASSWORD_ARGON2_TIME_COST ++#else ++#define PHP_OPENSSL_PWHASH_ITERLIMIT 4 ++#endif ++#if defined(PHP_PASSWORD_ARGON2_THREADS) ++#define PHP_OPENSSL_PWHASH_THREADS PHP_PASSWORD_ARGON2_THREADS ++#else ++#define PHP_OPENSSL_PWHASH_THREADS 1 ++#endif ++ ++PHP_MINIT_FUNCTION(openssl_pwhash); ++PHP_MSHUTDOWN_FUNCTION(openssl_pwhash); ++#endif ++ + PHP_MINIT_FUNCTION(openssl); + PHP_MSHUTDOWN_FUNCTION(openssl); + PHP_MINFO_FUNCTION(openssl); +diff --git a/ext/openssl/tests/openssl_password.phpt b/ext/openssl/tests/openssl_password.phpt +new file mode 100644 +index 0000000000..7881803038 +--- /dev/null ++++ b/ext/openssl/tests/openssl_password.phpt +@@ -0,0 +1,42 @@ ++--TEST-- ++Basic features of password_hash ++--EXTENSIONS-- ++openssl ++--SKIPIF-- ++ ++--FILE-- ++ PASSWORD_ARGON2_DEFAULT_MEMORY_COST / $mem, ++ 'time_cost' => PASSWORD_ARGON2_DEFAULT_TIME_COST / $time, ++ 'threads' => PASSWORD_ARGON2_DEFAULT_THREADS, ++ ]; ++ foreach(['argon2i', 'argon2id'] as $algo) { ++ $pass = "secret$mem$time$algo"; ++ $hash = openssl_password_hash($algo, $pass, $opts); ++ var_dump(openssl_password_verify($algo, $pass, $hash)); ++ } ++ } ++} ++?> ++--EXPECTF-- ++Argon2 provider: string(%d) "%s" ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++ +diff --git a/ext/openssl/tests/openssl_password_compat.phpt b/ext/openssl/tests/openssl_password_compat.phpt +new file mode 100644 +index 0000000000..0de683616a +--- /dev/null ++++ b/ext/openssl/tests/openssl_password_compat.phpt +@@ -0,0 +1,52 @@ ++--TEST-- ++Compatibility of password_hash (libsodium / openssl) ++--EXTENSIONS-- ++openssl ++sodium ++--SKIPIF-- ++ ++--FILE-- ++ PASSWORD_ARGON2_DEFAULT_MEMORY_COST / $mem, ++ 'time_cost' => PASSWORD_ARGON2_DEFAULT_TIME_COST / $time, ++ 'threads' => PASSWORD_ARGON2_DEFAULT_THREADS, ++ ]; ++ $algo = 'argon2id'; ++ $pass = "secret$mem$time$algo"; ++ ++ /* hash with libsodium / verify with openssl */ ++ $hash = sodium_crypto_pwhash_str($pass, PASSWORD_ARGON2_DEFAULT_TIME_COST / $time, PASSWORD_ARGON2_DEFAULT_MEMORY_COST / $mem); ++ var_dump(openssl_password_verify($algo, $pass, $hash)); ++ ++ /* hash with openssl / verify with libsodium */ ++ $hash = openssl_password_hash($algo, $pass, $opts); ++ var_dump(sodium_crypto_pwhash_str_verify($hash, $pass)); ++ } ++} ++?> ++--EXPECTF-- ++Argon2 provider: string(%d) "%s" ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++ +diff --git a/ext/openssl/tests/openssl_password_compat2.phpt b/ext/openssl/tests/openssl_password_compat2.phpt +new file mode 100644 +index 0000000000..42cf8682fd +--- /dev/null ++++ b/ext/openssl/tests/openssl_password_compat2.phpt +@@ -0,0 +1,52 @@ ++--TEST-- ++Compatibility of password_hash (libargon2 / openssl) ++--EXTENSIONS-- ++openssl ++sodium ++--SKIPIF-- ++ ++--FILE-- ++ PASSWORD_ARGON2_DEFAULT_MEMORY_COST / $mem, ++ 'time_cost' => PASSWORD_ARGON2_DEFAULT_TIME_COST / $time, ++ 'threads' => PASSWORD_ARGON2_DEFAULT_THREADS, ++ ]; ++ $algo = 'argon2id'; ++ $pass = "secret$mem$time$algo"; ++ ++ /* hash with libargon2 / verify with openssl */ ++ $hash = password_hash($pass, PASSWORD_ARGON2ID, $opts); ++ var_dump(openssl_password_verify($algo, $pass, $hash)); ++ ++ /* hash with openssl / verify with libargon2 */ ++ $hash = openssl_password_hash($algo, $pass, $opts); ++ var_dump(password_verify($pass, $hash)); ++ } ++} ++?> ++--EXPECT-- ++Argon2 provider: string(8) "standard" ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++bool(true) ++ +-- +2.45.0 + diff --git a/php.spec b/php.spec index eb5c644..27aebda 100644 --- a/php.spec +++ b/php.spec @@ -64,8 +64,7 @@ %bcond_with imap %bcond_without lmdb -%global upver 8.3.3 -%global rcver RC1 +%global upver 8.3.7 Summary: PHP scripting language for creating dynamic web sites Name: php @@ -123,6 +122,8 @@ Patch47: php-8.1.0-phpinfo.patch # Always warn about missing curve_name # Both Fedora and RHEL do not support arbitrary EC parameters Patch48: php-8.3.0-openssl-ec-param.patch +# Backport Argon2 password hashing in OpenSSL ext +Patch49: php-8.3.7-argon2.patch # Upstream fixes (100+) @@ -725,6 +726,7 @@ in pure PHP. %patch -P45 -p1 -b .ldap_r %patch -P47 -p1 -b .phpinfo %patch -P48 -p1 -b .ec-param +%patch -P49 -p1 -b .argon2 # upstream patches @@ -1552,6 +1554,10 @@ systemctl try-restart php-fpm.service >/dev/null 2>&1 || : %changelog +* Thu May 16 2024 Remi Collet - 8.3.7-1 +- rebase to 8.3.7 +- backport Argon2 password hashing in OpenSSL ext + * Wed Jan 31 2024 Remi Collet - 8.3.2~RC1-1 - update to 8.3.3RC1 - drop GCC 14 patch merged upstream diff --git a/sources b/sources index 471e05d..66c6646 100644 --- a/sources +++ b/sources @@ -1,2 +1,2 @@ -SHA512 (php-8.3.3RC1.tar.xz) = be802a7e9eb328add2e8b2a6d3fab3f9012469d43b3a5d6feacb9feed02ce446b617bb3fc7807697cd54f2935cbe4048a0356cc19f0f5b8e43b54877ab0719c9 -SHA512 (php-8.3.3RC1.tar.xz.asc) = 6a91cc39aeeea7cd6e8c93d152b05fc0afe3f25cac16443fcab5e193a650df51e8d6af7dc519f7b7aecbbd49b323fb073de77b6ff176acce64acc85438a801ff +SHA512 (php-8.3.7.tar.xz) = ff2c16a5cc08b1a59a61eee9df75c4c9a6dda7054d48198b75d104c194e934109fed3665005ba798eeca3d7294d7dc81df3a14e63a527baf9f196e229068d9a3 +SHA512 (php-8.3.7.tar.xz.asc) = 2fce3c0302dd93c6a841af53fc6dc498b58ce38250fa047b5fb77e9e98b138f00441c759018589c646366f0656303bea6738db30febed9f689f01847551c5769