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

[email protected] (2akouwu)
Newsgroups php.git-pulls
Message-ID <[email protected]>
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.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.