[php-src] PR #23506: ext/openssl: allow openssl_sign()/openssl_verify() to sign/verify a precomputed digest

From: Date: Sat, 29 Aug 2026 13:07:14 +0000
Subject: [php-src] PR #23506: ext/openssl: allow openssl_sign()/openssl_verify() to sign/verify a precomputed digest
Groups: php.git-pulls 
Request: Send a blank email to git-pulls+get-38200@lists.php.net to get a copy of this message
Pull Request: https://github.com/php/php-src/pull/23506 Author: 2akouwu Fixes #23422. ## Root cause openssl_sign() and openssl_verify() are implemented purely on top of OpenSSL's one-shot EVP_DigestSign()/EVP_DigestVerify() APIs. Those APIs always hash the buffer that is passed to them before performing the RSA/EC/etc. signature operation. That is correct when $data is the original message, but it means there is currently no way to sign or verify a digest that was already computed elsewhere (e.g. by a HSM, or incrementally via hash_update()/hash_final()): passing the digest as $data just hashes it a second time and produces sign(SHA256(digest)) instead of sign(digest). ## Why this fix OpenSSL exposes exactly the primitive needed for this: EVP_PKEY_sign_init()/EVP_PKEY_sign() and EVP_PKEY_verify_init()/EVP_PKEY_verify() operate on a buffer *without* hashing it, as long as the expected digest algorithm is registered on the EVP_PKEY_CTX via EVP_PKEY_CTX_set_signature_md() (RSA needs this to build the correct PKCS#1 DigestInfo prefix / PSS MGF1 digest; for EC/DSA it is used to validate the digest length). I added a new optional trailing parameter, bool $data_is_digest = false, to both openssl_sign() and openssl_verify(), defaulting to false so existing call sites are unaffected. When true, the implementation takes a separate branch that builds its own EVP_PKEY_CTX and drives the raw sign/verify primitives instead of EVP_DigestSign/EVP_DigestVerify. It reuses the existing php_openssl_setup_rsa_padding() and php_openssl_setup_rsa_pss_salt_length() static helpers unchanged, since they only operate on an EVP_PKEY_CTX and don't care how it was created, so padding/PSS-salt-length handling stays identical between both code paths. I considered instead exposing this only through a brand-new function (e.g. openssl_pkey_sign_digest()), but extending the existing functions with a defaulted parameter is smaller, keeps all the existing algorithm/padding/salt-length validation and error handling in one place, and mirrors how openssl_private_encrypt()/openssl_public_decrypt() etc. already expose several signing-adjacent primitives as parameterized variants of one function rather than a family of near-duplicate functions. For keys where OpenSSL doesn't support raw sign/verify at all (e.g. Ed25519, which only implements the one-shot digest-sign flow), EVP_PKEY_sign_init()/EVP_PKEY_verify_init() fail naturally and the existing php_openssl_store_errors() path surfaces that as a warning + false/-1, same as any other OpenSSL-level failure already handled by these functions. ext/openssl/openssl.stub.php and the generated ext/openssl/openssl_arginfo.h were updated together (the stub hash comment was recomputed by hand since gen_stub.php requires a PHP CLI that isn't available in this sandbox); UPGRADING documents the new parameter. ## Testing Added ext/openssl/tests/gh23422.phpt, which: - Generates a throwaway RSA key with openssl_pkey_new() (no fixture files needed). - Signs the same data once normally (openssl_sign($data, ..., OPENSSL_ALGO_SHA256)) and once as a precomputed digest (openssl_sign(hash('sha256', $data, true), ..., OPENSSL_ALGO_SHA256, 0, ..., true)), and asserts the two PKCS#1 v1.5 signatures are byte-identical — this is the core proof that signing a precomputed digest now produces the mathematically correct signature rather than double-hashing it. - Verifies the digest-produced signature against both the digest and the original data via openssl_verify(..., data_is_digest: true/false). - Flips one byte of the digest and confirms verification now fails (int(0)) instead of silently succeeding. - Repeats the sign/verify round trip with OPENSSL_PKCS1_PSS_PADDING to confirm padding/salt-length handling still applies on the new code path. I could not execute the test suite in this sandbox (no PHP/build toolchain available, and the bundle doesn't include the pre-generated RSA key fixtures used by some of the pre-existing openssl_sign*/openssl_verify* tests), so the new test intentionally avoids depending on any fixture files and generates its own key. I traced the OpenSSL/PHP control flow by hand to confirm the expected output, which is documented as --EXPECT-- in the phpt file.

« previous php.git-pulls (#38200) next »