[php-src] master: ext/openssl: fix OpenSSL 4 build and test compatibility (#22267)

Shivam Mathur via GitHub <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Shivam Mathur (shivammathur)
Committer: GitHub (web-flow)
Pusher: shivammathur
Date: 2026-07-03T19:15:40+05:30

Commit: https://github.com/php/php-src/commit/6aca6e30bb4ecf062d3d863de254b8b1d0914a8f
Raw diff: https://github.com/php/php-src/commit/6aca6e30bb4ecf062d3d863de254b8b1d0914a8f.diff

ext/openssl: fix OpenSSL 4 build and test compatibility (#22267)

* Fix OpenSSL 4 test compatibility

* Fix OpenSSL 4 build compatibility

* Use OpenSSL 4 in macOS CI

* Fix OpenSSL 1.1 build compatibility

* Preserve raw CN matching with OpenSSL 4

Changed paths:
  M  .github/actions/brew/action.yml
  M  .github/actions/configure-macos/action.yml
  M  ext/openssl/openssl.c
  M  ext/openssl/openssl_backend_common.c
  M  ext/openssl/openssl_backend_v3.c
  M  ext/openssl/php_openssl.h
  M  ext/openssl/php_openssl_backend.h
  M  ext/openssl/tests/bug28382.phpt
  M  ext/openssl/tests/ecc_custom_params.phpt
  M  ext/openssl/tests/openssl_x509_parse_basic_openssl32.phpt
  M  ext/openssl/xp_ssl.c


Diff:

diff --git a/.github/actions/brew/action.yml b/.github/actions/brew/action.yml
index f90a66f239de..db43010f396f 100644
--- a/.github/actions/brew/action.yml
+++ b/.github/actions/brew/action.yml
@@ -33,4 +33,6 @@ runs:
           t1lib \
           libxml2 \
           libjpeg \
-          libxslt
+          libxslt \
+          openssl@4
+        brew link --force --overwrite openssl@4
diff --git a/.github/actions/configure-macos/action.yml b/.github/actions/configure-macos/action.yml
index f0af959d2102..b9739a790530 100644
--- a/.github/actions/configure-macos/action.yml
+++ b/.github/actions/configure-macos/action.yml
@@ -11,7 +11,7 @@ runs:
         set -x
         BREW_OPT="$(brew --prefix)"/opt
         export PATH="$BREW_OPT/bison/bin:$PATH"
-        export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:$BREW_OPT/openssl/lib/pkgconfig"
+        export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:$BREW_OPT/openssl@4/lib/pkgconfig"
         export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:$BREW_OPT/curl/lib/pkgconfig"
         export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:$BREW_OPT/libffi/lib/pkgconfig"
         export PKG_CONFIG_PATH="$PKG_CONFIG_PATH:$BREW_OPT/libxml2/lib/pkgconfig"
diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c
index 1e63eb1381f6..e11ad5c3428c 100644
--- a/ext/openssl/openssl.c
+++ b/ext/openssl/openssl.c
@@ -821,7 +821,7 @@ PHP_MINIT_FUNCTION(openssl)
 	ssl_stream_data_index = SSL_get_ex_new_index(0, "PHP stream index", NULL, NULL, NULL);
 
 	php_stream_xport_register("ssl", php_openssl_ssl_socket_factory);
-#ifndef OPENSSL_NO_SSL3
+#if OPENSSL_VERSION_NUMBER < 0x40000000L && !defined(OPENSSL_NO_SSL3)
 	php_stream_xport_register("sslv3", php_openssl_ssl_socket_factory);
 #endif
 	php_stream_xport_register("tls", php_openssl_ssl_socket_factory);
@@ -895,7 +895,7 @@ PHP_MSHUTDOWN_FUNCTION(openssl)
 	php_unregister_url_stream_wrapper("ftps");
 
 	php_stream_xport_unregister("ssl");
-#ifndef OPENSSL_NO_SSL3
+#if OPENSSL_VERSION_NUMBER < 0x40000000L && !defined(OPENSSL_NO_SSL3)
 	php_stream_xport_unregister("sslv3");
 #endif
 	php_stream_xport_unregister("tls");
@@ -1395,8 +1395,8 @@ PHP_FUNCTION(openssl_x509_parse)
 	zval subitem;
 	zval critext;
 	int critcount = 0;
-	X509_EXTENSION *extension;
-	X509_NAME *subject_name;
+	PHP_OPENSSL_X509_EXTENSION *extension;
+	const X509_NAME *subject_name;
 	char *cert_name;
 	char *extname;
 	BIO *bio_out;
@@ -2383,7 +2383,7 @@ PHP_FUNCTION(openssl_csr_get_subject)
 	zend_object *csr_obj;
 	zend_string *csr_str;
 	bool use_shortnames = 1;
-	X509_NAME *subject;
+	const X509_NAME *subject;
 
 	ZEND_PARSE_PARAMETERS_START(1, 2)
 		Z_PARAM_OBJ_OF_CLASS_OR_STR(csr_obj, php_openssl_request_ce, csr_str)
diff --git a/ext/openssl/openssl_backend_common.c b/ext/openssl/openssl_backend_common.c
index 8115a7c91b3a..7261a617af32 100644
--- a/ext/openssl/openssl_backend_common.c
+++ b/ext/openssl/openssl_backend_common.c
@@ -33,16 +33,16 @@
 /* true global; readonly after module startup */
 static char default_ssl_conf_filename[MAXPATHLEN];
 
-void php_openssl_add_assoc_name_entry(zval * val, char * key, X509_NAME * name, int shortname)
+void php_openssl_add_assoc_name_entry(zval * val, char * key, const X509_NAME * name, int shortname)
 {
 	zval *data;
 	zval subitem, tmp;
 	int i;
 	char *sname;
 	int nid;
-	X509_NAME_ENTRY * ne;
-	ASN1_STRING * str = NULL;
-	ASN1_OBJECT * obj;
+	const X509_NAME_ENTRY * ne;
+	const ASN1_STRING * str = NULL;
+	const ASN1_OBJECT * obj;
 
 	if (key != NULL) {
 		array_init(&subitem);
@@ -106,7 +106,7 @@ void php_openssl_add_assoc_name_entry(zval * val, char * key, X509_NAME * name,
 	}
 }
 
-void php_openssl_add_assoc_asn1_string(zval * val, char * key, ASN1_STRING * str)
+void php_openssl_add_assoc_asn1_string(zval * val, char * key, const ASN1_STRING * str)
 {
 	add_assoc_stringl(val, key, (const char *)ASN1_STRING_get0_data(str), ASN1_STRING_length(str));
 }
@@ -622,11 +622,11 @@ zend_string* php_openssl_x509_fingerprint(X509 *peer, const char *method, bool r
 /* Special handling of subjectAltName, see CVE-2013-4073
  * Christian Heimes
  */
-int openssl_x509v3_subjectAltName(BIO *bio, X509_EXTENSION *extension)
+int openssl_x509v3_subjectAltName(BIO *bio, PHP_OPENSSL_X509_EXTENSION *extension)
 {
 	GENERAL_NAMES *names;
 	const X509V3_EXT_METHOD *method = NULL;
-	ASN1_OCTET_STRING *extension_data;
+	const ASN1_OCTET_STRING *extension_data;
 	long i, length, num;
 	const unsigned char *p;
 
@@ -982,7 +982,12 @@ zend_result php_openssl_csr_make(struct php_x509_request * req, X509_REQ * csr,
 		zval *item, *subitem;
 		zend_string *strindex = NULL;
 
-		subj = X509_REQ_get_subject_name(csr);
+		subj = X509_NAME_new();
+		if (subj == NULL) {
+			php_openssl_store_errors();
+			return FAILURE;
+		}
+
 		/* apply values from the dn hash */
 		ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(dn), strindex, item) {
 			if (strindex) {
@@ -991,10 +996,12 @@ zend_result php_openssl_csr_make(struct php_x509_request * req, X509_REQ * csr,
 					if (Z_TYPE_P(item) == IS_ARRAY) {
 						ZEND_HASH_FOREACH_NUM_KEY_VAL(Z_ARRVAL_P(item), i, subitem) {
 							if (php_openssl_csr_add_subj_entry(subitem, subj, nid) == FAILURE) {
+								X509_NAME_free(subj);
 								return FAILURE;
 							}
 						} ZEND_HASH_FOREACH_END();
 					} else if (php_openssl_csr_add_subj_entry(item, subj, nid) == FAILURE) {
+						X509_NAME_free(subj);
 						return FAILURE;
 					}
 				} else {
@@ -1045,13 +1052,23 @@ zend_result php_openssl_csr_make(struct php_x509_request * req, X509_REQ * csr,
 			if (!X509_NAME_add_entry_by_txt(subj, type, MBSTRING_UTF8, (unsigned char*)v->value, -1, -1, 0)) {
 				php_openssl_store_errors();
 				php_error_docref(NULL, E_WARNING, "add_entry_by_txt %s -> %s (failed)", type, v->value);
+				X509_NAME_free(subj);
 				return FAILURE;
 			}
 			if (!X509_NAME_entry_count(subj)) {
 				php_error_docref(NULL, E_WARNING, "No objects specified in config file");
+				X509_NAME_free(subj);
 				return FAILURE;
 			}
 		}
+
+		if (!X509_REQ_set_subject_name(csr, subj)) {
+			php_openssl_store_errors();
+			X509_NAME_free(subj);
+			return FAILURE;
+		}
+		X509_NAME_free(subj);
+
 		if (attribs) {
 			ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(attribs), strindex, item) {
 				int nid;
diff --git a/ext/openssl/openssl_backend_v3.c b/ext/openssl/openssl_backend_v3.c
index 339ff53f7b62..375c0104fac5 100644
--- a/ext/openssl/openssl_backend_v3.c
+++ b/ext/openssl/openssl_backend_v3.c
@@ -402,7 +402,7 @@ EVP_PKEY *php_openssl_pkey_init_ec(zval *data, bool *is_private) {
 		}
 
 		OPENSSL_PKEY_SET_BN(data, cofactor);
-		if (!OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_EC_COFACTOR, cofactor) ||
+		if ((cofactor && !OSSL_PARAM_BLD_push_BN(bld, OSSL_PKEY_PARAM_EC_COFACTOR, cofactor)) ||
 			!EC_GROUP_set_generator(group, point_g, order, cofactor)) {
 			goto cleanup;
 		}
diff --git a/ext/openssl/php_openssl.h b/ext/openssl/php_openssl.h
index b88a6c59e4e9..588cca0e93d8 100644
--- a/ext/openssl/php_openssl.h
+++ b/ext/openssl/php_openssl.h
@@ -23,6 +23,7 @@ extern zend_module_entry openssl_module_entry;
 #include "php_version.h"
 #define PHP_OPENSSL_VERSION PHP_VERSION
 
+#include <openssl/opensslconf.h>
 #include <openssl/opensslv.h>
 /* OpenSSL version check */
 #if OPENSSL_VERSION_NUMBER < 0x30000000L
diff --git a/ext/openssl/php_openssl_backend.h b/ext/openssl/php_openssl_backend.h
index bd12a5fe312f..fcbb8d42542f 100644
--- a/ext/openssl/php_openssl_backend.h
+++ b/ext/openssl/php_openssl_backend.h
@@ -35,6 +35,12 @@
 #include <openssl/pkcs12.h>
 #include <openssl/cms.h>
 
+#if defined(OPENSSL_VERSION_MAJOR) && OPENSSL_VERSION_MAJOR >= 4
+typedef const X509_EXTENSION PHP_OPENSSL_X509_EXTENSION;
+#else
+typedef X509_EXTENSION PHP_OPENSSL_X509_EXTENSION;
+#endif
+
 /* number conversion flags checks */
 #define PHP_OPENSSL_CHECK_NUMBER_CONVERSION(_cond, _name, _arg_num) \
 	do { \
@@ -166,8 +172,8 @@ struct php_x509_request {
 	const EVP_CIPHER * priv_key_encrypt_cipher;
 };
 
-void php_openssl_add_assoc_name_entry(zval * val, char * key, X509_NAME * name, int shortname);
-void php_openssl_add_assoc_asn1_string(zval * val, char * key, ASN1_STRING * str);
+void php_openssl_add_assoc_name_entry(zval * val, char * key, const X509_NAME * name, int shortname);
+void php_openssl_add_assoc_asn1_string(zval * val, char * key, const ASN1_STRING * str);
 time_t php_openssl_asn1_time_to_time_t(ASN1_UTCTIME * timestr);
 int php_openssl_config_check_syntax(const char * section_label, const char * config_filename, const char * section, CONF *config);
 char *php_openssl_conf_get_string(CONF *conf, const char *group, const char *name);
@@ -265,7 +271,7 @@ X509 *php_openssl_x509_from_zval(
 zend_string* php_openssl_x509_fingerprint(
 		X509 *peer, const char *method, bool raw, struct _php_stream *stream);
 
-int openssl_x509v3_subjectAltName(BIO *bio, X509_EXTENSION *extension);
+int openssl_x509v3_subjectAltName(BIO *bio, PHP_OPENSSL_X509_EXTENSION *extension);
 
 STACK_OF(X509) *php_openssl_load_all_certs_from_file(
 		char *cert_file, size_t cert_file_len, uint32_t arg_num);
diff --git a/ext/openssl/tests/bug28382.phpt b/ext/openssl/tests/bug28382.phpt
index e2842023135c..7627da2931b5 100644
--- a/ext/openssl/tests/bug28382.phpt
+++ b/ext/openssl/tests/bug28382.phpt
@@ -6,6 +6,12 @@ openssl
 <?php
 $cert = file_get_contents(__DIR__ . "/bug28382cert.txt");
 $ext = openssl_x509_parse($cert);
+
+if (OPENSSL_VERSION_NUMBER >= 0x40000000) {
+    $extensions = &$ext['extensions'];
+    $extensions['crlDistributionPoints'] = preg_replace('/^Full Name:\R\s*/', '', trim($extensions['crlDistributionPoints']));
+}
+
 var_dump($ext['extensions']);
 /*
  * The reason for %A at the end of crlDistributionPoints and authorityKeyIdentifier is that
@@ -30,7 +36,7 @@ array(11) {
   string(59) "B0:A7:FF:F9:41:15:DE:23:39:BD:DD:31:0F:97:A0:B2:A2:74:E0:FC"
   ["authorityKeyIdentifier"]=>
   string(%d) "DirName:/C=RO/ST=Romania/L=Craiova/O=Sergiu/OU=Sergiu SRL/CN=Sergiu CA/[email protected]
-serial:00%A"
+serial:%A"
   ["keyUsage"]=>
   string(71) "Digital Signature, Non Repudiation, Key Encipherment, Data Encipherment"
   ["nsBaseUrl"]=>
diff --git a/ext/openssl/tests/ecc_custom_params.phpt b/ext/openssl/tests/ecc_custom_params.phpt
index 0c63af1450ba..e3854eefa4d8 100644
--- a/ext/openssl/tests/ecc_custom_params.phpt
+++ b/ext/openssl/tests/ecc_custom_params.phpt
@@ -5,6 +5,30 @@ openssl
 --SKIPIF--
 <?php
 if (!defined("OPENSSL_KEYTYPE_EC")) die("skip EC disabled");
+
+if (OPENSSL_VERSION_NUMBER >= 0x40000000) {
+    $d = hex2bin('8D0AC65AAEA0D6B96254C65817D4A143A9E7A03876F1A37D');
+    $p = hex2bin('BDB6F4FE3E8B1D9E0DA8C0D46F4C318CEFE4AFE3B6B8551F');
+    $a = hex2bin('BB8E5E8FBC115E139FE6A814FE48AAA6F0ADA1AA5DF91985');
+    $b = hex2bin('1854BEBDC31B21B7AEFC80AB0ECD10D5B1B3308E6DBF11C1');
+    $g_x = hex2bin('4AD5F7048DE709AD51236DE65E4D4B482C836DC6E4106640');
+    $g_y = hex2bin('02BB3A02D4AAADACAE24817A4CA3A1B014B5270432DB27D2');
+    $order = hex2bin('BDB6F4FE3E8B1D9E0DA8C0D40FC962195DFAE76F56564677');
+
+    if (@openssl_pkey_new(array(
+        'ec' => array(
+            'p' => $p,
+            'a' => $a,
+            'b' => $b,
+            'order' => $order,
+            'g_x' => $g_x,
+            'g_y' => $g_y,
+            'd' => $d,
+        ),
+    )) === false) {
+        die("skip EC custom params unsupported with OpenSSL 4");
+    }
+}
 ?>
 --FILE--
 <?php
diff --git a/ext/openssl/tests/openssl_x509_parse_basic_openssl32.phpt b/ext/openssl/tests/openssl_x509_parse_basic_openssl32.phpt
index a0b125fb4ea0..84415d133f55 100644
--- a/ext/openssl/tests/openssl_x509_parse_basic_openssl32.phpt
+++ b/ext/openssl/tests/openssl_x509_parse_basic_openssl32.phpt
@@ -8,12 +8,28 @@ if (OPENSSL_VERSION_NUMBER < 0x30200000) die('skip For OpenSSL >= 3.2');
 ?>
 --FILE--
 <?php
+function normalize_openssl4_x509_parse_output(array $cert): array {
+    if (OPENSSL_VERSION_NUMBER < 0x40000000 || !isset($cert['serialNumberHex'], $cert['extensions']['authorityKeyIdentifier'])) {
+        return $cert;
+    }
+
+    $serial = strtoupper($cert['serialNumberHex']);
+    if (strlen($serial) % 2) {
+        $serial = '0' . $serial;
+    }
+    $serial = implode(':', str_split($serial, 2));
+    $cert['extensions']['authorityKeyIdentifier'] = preg_replace('/^serial:\d+$/m', 'serial:' . $serial, $cert['extensions']['authorityKeyIdentifier']);
+
+    return $cert;
+}
+
 $cert = "file://" . __DIR__ . "/cert.crt";
 
 $parsedCert = openssl_x509_parse($cert);
-var_dump($parsedCert === openssl_x509_parse(openssl_x509_read($cert)));
+$parsedCert = normalize_openssl4_x509_parse_output($parsedCert);
+var_dump($parsedCert === normalize_openssl4_x509_parse_output(openssl_x509_parse(openssl_x509_read($cert))));
 var_dump($parsedCert);
-var_dump(openssl_x509_parse($cert, false));
+var_dump(normalize_openssl4_x509_parse_output(openssl_x509_parse($cert, false)));
 ?>
 --EXPECTF--
 bool(true)
diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c
index 93a9971b4a78..d4158634e5bc 100644
--- a/ext/openssl/xp_ssl.c
+++ b/ext/openssl/xp_ssl.c
@@ -93,7 +93,7 @@
 #define HAVE_SEC_LEVEL 1
 #endif
 
-#ifndef OPENSSL_NO_SSL3
+#if OPENSSL_VERSION_NUMBER < 0x40000000L && !defined(OPENSSL_NO_SSL3)
 #define HAVE_SSL3 1
 #define PHP_OPENSSL_MIN_PROTO_VERSION STREAM_CRYPTO_METHOD_SSLv3
 #else
@@ -274,7 +274,7 @@ static int php_openssl_handle_ssl_error(php_stream *stream, int nr_bytes, bool i
 			errno = EAGAIN;
 			retry = is_init ? true : sslsock->s.is_blocked;
 			if (!retry) {
-				sslsock->last_status = err == SSL_ERROR_WANT_READ ? 
+				sslsock->last_status = err == SSL_ERROR_WANT_READ ?
 						STREAM_CRYPTO_STATUS_WANT_READ : STREAM_CRYPTO_STATUS_WANT_WRITE;
 			}
 			break;
@@ -566,26 +566,50 @@ static bool php_openssl_matches_san_list(X509 *peer, const char *subject_name) /
 
 static bool php_openssl_matches_common_name(php_stream *stream, const X509 *peer, const char *subject_name) /* {{{ */
 {
-	char buf[1024];
-	X509_NAME *cert_name;
+	unsigned char *cert_name = NULL;
+#if PHP_OPENSSL_API_VERSION < 0x30000
+	X509_NAME *name;
+#else
+	const X509_NAME *name;
+#endif
+	const X509_NAME_ENTRY *name_entry;
+	const ASN1_STRING *name_asn1;
 	bool is_match = false;
+	int name_index;
 	int cert_name_len;
 
-	cert_name = X509_get_subject_name(peer);
-	cert_name_len = X509_NAME_get_text_by_NID(cert_name, NID_commonName, buf, sizeof(buf));
+	name = X509_get_subject_name(peer);
+	name_index = X509_NAME_get_index_by_NID(name, NID_commonName, -1);
+	if (name_index == -1) {
+		php_stream_warn(stream, NetworkRecvFailed, "Unable to locate peer certificate CN");
+		return false;
+	}
 
-	if (cert_name_len == -1) {
+	name_entry = X509_NAME_get_entry(name, name_index);
+	if (name_entry == NULL) {
 		php_stream_warn(stream, NetworkRecvFailed, "Unable to locate peer certificate CN");
-	} else if ((size_t)cert_name_len != strlen(buf)) {
-		php_stream_warn(stream, AuthFailed, "Peer certificate CN=`%.*s' is malformed", cert_name_len, buf);
-	} else if (php_openssl_matches_wildcard_name(subject_name, buf)) {
+		return false;
+	}
+	name_asn1 = X509_NAME_ENTRY_get_data(name_entry);
+	cert_name_len = ASN1_STRING_length(name_asn1);
+	cert_name = (unsigned char *) OPENSSL_strndup((const char *) ASN1_STRING_get0_data(name_asn1), cert_name_len);
+	if (cert_name == NULL) {
+		php_stream_warn(stream, NetworkRecvFailed, "Unable to locate peer certificate CN");
+		return false;
+	}
+
+	if ((size_t)cert_name_len != strlen((const char *)cert_name)) {
+		php_stream_warn(stream, AuthFailed, "Peer certificate CN=`%.*s' is malformed", cert_name_len, (const char *)cert_name);
+	} else if (php_openssl_matches_wildcard_name(subject_name, (const char *)cert_name)) {
 		is_match = true;
 	} else {
 		php_stream_warn(stream, AuthFailed,
 			"Peer certificate CN=`%.*s' did not match expected CN=`%s'",
-			cert_name_len, buf, subject_name);
+			cert_name_len, (const char *)cert_name, subject_name);
 	}
 
+	OPENSSL_free(cert_name);
+
 	return is_match;
 }
 /* }}} */
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.