From ebeb15c82a6c49d9e17eedeafb92160c2a6a128d Mon Sep 17 00:00:00 2001
From: Jakub Zelenka <bukka@php.net>
Date: Sat, 9 Aug 2025 16:17:55 +0200
Subject: [PATCH] Implement #80495: Enable to set padding in
 openssl_(sign|verify)

This adds support for RSA PSS
---
 ext/openssl/openssl.c                       | 57 ++++++++++++++++++---
 ext/openssl/openssl.stub.php                |  9 +++-
 ext/openssl/openssl_arginfo.h               |  5 +-
 ext/openssl/tests/openssl_sign_basic.phpt   | 18 ++++++-
 ext/openssl/tests/openssl_verify_basic.phpt |  6 +++
 5 files changed, 85 insertions(+), 10 deletions(-)

diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c
index 2f9e160b57e58..b5ec771d8656b 100644
--- a/ext/openssl/openssl.c
+++ b/ext/openssl/openssl.c
@@ -3989,6 +3989,30 @@ PHP_FUNCTION(openssl_error_string)
 }
 /* }}} */
 
+static zend_result php_openssl_setup_rsa_padding(EVP_PKEY_CTX *pctx, EVP_PKEY *pkey, zend_long padding)
+{
+	int key_type = EVP_PKEY_type(EVP_PKEY_id(pkey));
+
+	if (padding != 0) { // 0 = default/unspecified
+		if (key_type != EVP_PKEY_RSA) {
+			php_error_docref(NULL, E_WARNING, "Padding parameter is only supported for RSA keys");
+			return FAILURE;
+		}
+
+		if (padding == RSA_PKCS1_PSS_PADDING) {
+			if (EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PSS_PADDING) <= 0) {
+				php_openssl_store_errors();
+				return FAILURE;
+			}
+		} else if (padding != RSA_PKCS1_PADDING) {
+			php_error_docref(NULL, E_WARNING, "Unknown padding type");
+			return FAILURE;
+		}
+	}
+
+	return SUCCESS;
+}
+
 /* {{{ Signs data */
 PHP_FUNCTION(openssl_sign)
 {
@@ -4001,14 +4025,17 @@ PHP_FUNCTION(openssl_sign)
 	zend_string *method_str = NULL;
 	zend_long method_long = OPENSSL_ALGO_SHA1;
 	const EVP_MD *mdtype;
+	zend_long padding = 0;
+	EVP_PKEY_CTX *pctx;
 	bool can_default_digest = ZEND_THREEWAY_COMPARE(PHP_OPENSSL_API_VERSION, 0x30000) >= 0;
 
-	ZEND_PARSE_PARAMETERS_START(3, 4)
+	ZEND_PARSE_PARAMETERS_START(3, 5)
 		Z_PARAM_STRING(data, data_len)
 		Z_PARAM_ZVAL(signature)
 		Z_PARAM_ZVAL(key)
 		Z_PARAM_OPTIONAL
 		Z_PARAM_STR_OR_LONG(method_str, method_long)
+		Z_PARAM_LONG(padding)
 	ZEND_PARSE_PARAMETERS_END();
 
 	pkey = php_openssl_pkey_from_zval(key, 0, "", 0, 3);
@@ -4033,7 +4060,8 @@ PHP_FUNCTION(openssl_sign)
 	md_ctx = EVP_MD_CTX_create();
 	size_t siglen;
 	if (md_ctx != NULL &&
-			EVP_DigestSignInit(md_ctx, NULL, mdtype, NULL, pkey) &&
+			EVP_DigestSignInit(md_ctx, &pctx, mdtype, NULL, pkey) &&
+			php_openssl_setup_rsa_padding(pctx, pkey, padding) == SUCCESS &&
 			EVP_DigestSign(md_ctx, NULL, &siglen, (unsigned char*)data, data_len) &&
 			(sigbuf = zend_string_alloc(siglen, 0)) != NULL &&
 			EVP_DigestSign(md_ctx, (unsigned char*)ZSTR_VAL(sigbuf), &siglen, (unsigned char*)data, data_len)) {
@@ -4066,14 +4094,17 @@ PHP_FUNCTION(openssl_verify)
 	size_t signature_len;
 	zend_string *method_str = NULL;
 	zend_long method_long = OPENSSL_ALGO_SHA1;
+	zend_long padding = 0;
+	EVP_PKEY_CTX *pctx;
 	bool can_default_digest = ZEND_THREEWAY_COMPARE(PHP_OPENSSL_API_VERSION, 0x30000) >= 0;
 
-	ZEND_PARSE_PARAMETERS_START(3, 4)
+	ZEND_PARSE_PARAMETERS_START(3, 5)
 		Z_PARAM_STRING(data, data_len)
 		Z_PARAM_STRING(signature, signature_len)
 		Z_PARAM_ZVAL(key)
 		Z_PARAM_OPTIONAL
 		Z_PARAM_STR_OR_LONG(method_str, method_long)
+		Z_PARAM_LONG(padding)
 	ZEND_PARSE_PARAMETERS_END();
 
 	PHP_OPENSSL_CHECK_SIZE_T_TO_UINT(signature_len, signature, 2);
@@ -4098,11 +4129,25 @@ PHP_FUNCTION(openssl_verify)
 	}
 
 	md_ctx = EVP_MD_CTX_create();
-	if (md_ctx == NULL ||
-			!EVP_DigestVerifyInit(md_ctx, NULL, mdtype, NULL, pkey) ||
-			(err = EVP_DigestVerify(md_ctx, (unsigned char *)signature, signature_len, (unsigned char*)data, data_len)) < 0) {
+	if (md_ctx == NULL) {
 		php_openssl_store_errors();
+		err = -1;
+		goto cleanup;
 	}
+
+	if (!EVP_DigestVerifyInit(md_ctx, &pctx, mdtype, NULL, pkey) ||
+			php_openssl_setup_rsa_padding(pctx, pkey, padding) == FAILURE) {
+		php_openssl_store_errors();
+		err = -1;
+		goto cleanup;
+	}
+
+	err = EVP_DigestVerify(md_ctx, (unsigned char *)signature, signature_len, (unsigned char*)data, data_len);
+	if (err < 0) {
+		php_openssl_store_errors();
+	}
+
+cleanup:
 	EVP_MD_CTX_destroy(md_ctx);
 	php_openssl_release_evp_md(mdtype);
 	EVP_PKEY_free(pkey);
diff --git a/ext/openssl/openssl.stub.php b/ext/openssl/openssl.stub.php
index b35c15f2b524c..a7d1a5cda6e95 100644
--- a/ext/openssl/openssl.stub.php
+++ b/ext/openssl/openssl.stub.php
@@ -236,6 +236,11 @@
  * @cvalue RSA_PKCS1_OAEP_PADDING
  */
 const OPENSSL_PKCS1_OAEP_PADDING = UNKNOWN;
+/**
+ * @var int
+ * @cvalue RSA_PKCS1_PSS_PADDING
+ */
+const OPENSSL_PKCS1_PSS_PADDING = UNKNOWN;
 
 /* Informational stream wrapper constants */
 
@@ -595,10 +600,10 @@ function openssl_error_string(): string|false {}
  * @param string $signature
  * @param OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $private_key
  */
-function openssl_sign(string $data, &$signature, #[\SensitiveParameter] $private_key, string|int $algorithm = OPENSSL_ALGO_SHA1): bool {}
+function openssl_sign(string $data, &$signature, #[\SensitiveParameter] $private_key, string|int $algorithm = OPENSSL_ALGO_SHA1, int $padding = 0): bool {}
 
 /** @param OpenSSLAsymmetricKey|OpenSSLCertificate|array|string $public_key */
-function openssl_verify(string $data, string $signature, $public_key, string|int $algorithm = OPENSSL_ALGO_SHA1): int|false {}
+function openssl_verify(string $data, string $signature, $public_key, string|int $algorithm = OPENSSL_ALGO_SHA1, int $padding = 0): int|false {}
 
 /**
  * @param string $sealed_data
diff --git a/ext/openssl/openssl_arginfo.h b/ext/openssl/openssl_arginfo.h
index 8f47d491b8f36..22b4f10dfab7b 100644
--- a/ext/openssl/openssl_arginfo.h
+++ b/ext/openssl/openssl_arginfo.h
@@ -1,5 +1,5 @@
 /* This is a generated file, edit the .stub.php file instead.
- * Stub hash: 4186e01a05ec179f1f2196a2afd1860671f793d5 */
+ * Stub hash: ff620528ee3035e295099637cc9f94b78a53416b */
 
 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_x509_export_to_file, 0, 2, _IS_BOOL, 0)
 	ZEND_ARG_OBJ_TYPE_MASK(0, certificate, OpenSSLCertificate, MAY_BE_STRING, NULL)
@@ -284,6 +284,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_sign, 0, 3, _IS_BOOL, 0)
 	ZEND_ARG_INFO(1, signature)
 	ZEND_ARG_INFO(0, private_key)
 	ZEND_ARG_TYPE_MASK(0, algorithm, MAY_BE_STRING|MAY_BE_LONG, "OPENSSL_ALGO_SHA1")
+	ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, padding, IS_LONG, 0, "0")
 ZEND_END_ARG_INFO()
 
 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_openssl_verify, 0, 3, MAY_BE_LONG|MAY_BE_FALSE)
@@ -291,6 +292,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_openssl_verify, 0, 3, MAY_BE_LON
 	ZEND_ARG_TYPE_INFO(0, signature, IS_STRING, 0)
 	ZEND_ARG_INFO(0, public_key)
 	ZEND_ARG_TYPE_MASK(0, algorithm, MAY_BE_STRING|MAY_BE_LONG, "OPENSSL_ALGO_SHA1")
+	ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, padding, IS_LONG, 0, "0")
 ZEND_END_ARG_INFO()
 
 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_openssl_seal, 0, 5, MAY_BE_LONG|MAY_BE_FALSE)
@@ -599,6 +601,7 @@ static void register_openssl_symbols(int module_number)
 #endif
 	REGISTER_LONG_CONSTANT("OPENSSL_NO_PADDING", RSA_NO_PADDING, CONST_PERSISTENT);
 	REGISTER_LONG_CONSTANT("OPENSSL_PKCS1_OAEP_PADDING", RSA_PKCS1_OAEP_PADDING, CONST_PERSISTENT);
+	REGISTER_LONG_CONSTANT("OPENSSL_PKCS1_PSS_PADDING", RSA_PKCS1_PSS_PADDING, CONST_PERSISTENT);
 	REGISTER_STRING_CONSTANT("OPENSSL_DEFAULT_STREAM_CIPHERS", OPENSSL_DEFAULT_STREAM_CIPHERS, CONST_PERSISTENT);
 #if !defined(OPENSSL_NO_RC2)
 	REGISTER_LONG_CONSTANT("OPENSSL_CIPHER_RC2_40", PHP_OPENSSL_CIPHER_RC2_40, CONST_PERSISTENT);
diff --git a/ext/openssl/tests/openssl_sign_basic.phpt b/ext/openssl/tests/openssl_sign_basic.phpt
index 047028101893c..9d2edbec59f7a 100644
--- a/ext/openssl/tests/openssl_sign_basic.phpt
+++ b/ext/openssl/tests/openssl_sign_basic.phpt
@@ -8,11 +8,27 @@ $data = "Testing openssl_sign()";
 $privkey = "file://" . __DIR__ . "/private_rsa_1024.key";
 $wrong = "wrong";
 
-var_dump(openssl_sign($data, $sign, $privkey, OPENSSL_ALGO_SHA256));                 // no output
+var_dump(openssl_sign($data, $sign1, $privkey, OPENSSL_ALGO_SHA256));
+var_dump(bin2hex($sign1));
+var_dump(openssl_sign($data, $sign2, $privkey, OPENSSL_ALGO_SHA256));
+var_dump($sign1 === $sign2);
+var_dump(openssl_sign($data, $sign1, $privkey, OPENSSL_ALGO_SHA256, OPENSSL_PKCS1_PSS_PADDING));
+var_dump(strlen($sign1));
+var_dump(openssl_sign($data, $sign2, $privkey, OPENSSL_ALGO_SHA256, OPENSSL_PKCS1_PSS_PADDING));
+var_dump(strlen($sign2));
+var_dump($sign1 === $sign2);
 var_dump(openssl_sign($data, $sign, $wrong));
 ?>
 --EXPECTF--
 bool(true)
+string(256) "5eff033d92208fcbf52edc9cbf6c9d4bd7c06b7b5a6a22c7d641d1494a09d6b0898d321c0a8fdb55c10b9bf25c2bb777c2b4660f867001f79879d089de7321a28df5f037cc02b68c47d1eb28d98a9199876961adb02524a489872a12fd3675db6a957d623ff04b9f715b565f516806cea247264c82a7569871dbd0b86cfe4689"
+bool(true)
+bool(true)
+bool(true)
+int(128)
+bool(true)
+int(128)
+bool(false)
 
 Warning: openssl_sign(): Supplied key param cannot be coerced into a private key in %s on line %d
 bool(false)
diff --git a/ext/openssl/tests/openssl_verify_basic.phpt b/ext/openssl/tests/openssl_verify_basic.phpt
index 674a3c58a9ea3..1b28c1371083a 100644
--- a/ext/openssl/tests/openssl_verify_basic.phpt
+++ b/ext/openssl/tests/openssl_verify_basic.phpt
@@ -9,6 +9,10 @@ $privkey = "file://" . __DIR__ . "/private_rsa_1024.key";
 $pubkey = "file://" . __DIR__ . "/public.key";
 $wrong = "wrong";
 
+
+openssl_sign($data, $sign, $privkey, OPENSSL_ALGO_SHA256, OPENSSL_PKCS1_PSS_PADDING);
+var_dump(openssl_verify($data, $sign, $pubkey, OPENSSL_ALGO_SHA256, OPENSSL_PKCS1_PSS_PADDING));
+var_dump(openssl_verify($data, $sign, $pubkey, OPENSSL_ALGO_SHA256));
 openssl_sign($data, $sign, $privkey, OPENSSL_ALGO_SHA256);
 var_dump(openssl_verify($data, $sign, $pubkey, OPENSSL_ALGO_SHA256));
 var_dump(openssl_verify($data, $sign, $privkey, OPENSSL_ALGO_SHA256));
@@ -18,6 +22,8 @@ var_dump(openssl_verify($wrong, $sign, $pubkey, OPENSSL_ALGO_SHA256));
 ?>
 --EXPECTF--
 int(1)
+int(0)
+int(1)
 
 Warning: openssl_verify(): Supplied key param cannot be coerced into a public key in %s on line %d
 bool(false)
