From 5d63d1de7a3a677a53375bb033511f1c2c784568 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Thu, 1 Oct 2026 07:09:47 +0000 Subject: [PATCH 1/8] Add stream_get_channel_binding Related to https://github.com/php/php-src/issues/16766 --- ext/openssl/openssl.stub.php | 17 ++ ext/openssl/openssl_arginfo.h | 9 +- .../stream_get_channel_binding_errors.phpt | 49 ++++ .../stream_get_channel_binding_tls12.phpt | 107 +++++++++ .../stream_get_channel_binding_tls13.phpt | 105 ++++++++ ext/openssl/xp_ssl.c | 225 ++++++++++++++++++ 6 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 ext/openssl/tests/stream_get_channel_binding_errors.phpt create mode 100644 ext/openssl/tests/stream_get_channel_binding_tls12.phpt create mode 100644 ext/openssl/tests/stream_get_channel_binding_tls13.phpt diff --git a/ext/openssl/openssl.stub.php b/ext/openssl/openssl.stub.php index 3d3fa3ea634f..589b22116aa1 100644 --- a/ext/openssl/openssl.stub.php +++ b/ext/openssl/openssl.stub.php @@ -776,4 +776,21 @@ function openssl_password_hash(string $algo, #[\SensitiveParameter] string $pass function openssl_password_verify(string $algo, #[\SensitiveParameter] string $password, string $hash): bool {} #endif + /** + * Get TLS channel binding data for the connection behind a stream. + * + * The binding data is only available once the TLS handshake has completed; + * for a stream without transport encryption a RuntimeException is thrown. + * + * @param resource $stream + * @param string $channel_binding_type One of "tls-unique", + * "tls-server-endpoint" or "tls-exporter" (the IANA "Channel Binding" + * registry names, RFC 5929 / RFC 9266). + * @return string|null The channel binding data, or NULL when it is not + * applicable to the connection (currently "tls-unique" over TLS 1.3). + * @throws RuntimeException + * @throws ValueError + */ +function stream_get_channel_binding($stream, string $channel_binding_type): ?string {} + } diff --git a/ext/openssl/openssl_arginfo.h b/ext/openssl/openssl_arginfo.h index c7fdf82c7861..fcdae745838b 100644 --- a/ext/openssl/openssl_arginfo.h +++ b/ext/openssl/openssl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit openssl.stub.php instead. - * Stub hash: 7cad995b734d69f98d489edb97a7878a4ea8f47e */ + * Stub hash: 00e3e90742171820379330fb2037be2a59fd65ce */ #include "zend_attributes.h" #include "zend_constants.h" @@ -411,6 +411,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_password_verify, 0, 3, _ ZEND_END_ARG_INFO() #endif +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_get_channel_binding, 0, 2, IS_STRING, 1) + ZEND_ARG_INFO(0, stream) + ZEND_ARG_TYPE_INFO(0, channel_binding_type, IS_STRING, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Openssl_Psk___construct, 0, 0, 1) ZEND_ARG_TYPE_INFO(0, psk, IS_STRING, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, identity, IS_STRING, 1, "null") @@ -516,6 +521,7 @@ ZEND_FUNCTION(openssl_get_cert_locations); ZEND_FUNCTION(openssl_password_hash); ZEND_FUNCTION(openssl_password_verify); #endif +ZEND_FUNCTION(stream_get_channel_binding); ZEND_METHOD(Openssl_Psk, __construct); ZEND_METHOD(Openssl_Session, export); ZEND_METHOD(Openssl_Session, import); @@ -600,6 +606,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(openssl_password_hash, arginfo_openssl_password_hash) ZEND_FE(openssl_password_verify, arginfo_openssl_password_verify) #endif + ZEND_FE(stream_get_channel_binding, arginfo_stream_get_channel_binding) ZEND_FE_END }; diff --git a/ext/openssl/tests/stream_get_channel_binding_errors.phpt b/ext/openssl/tests/stream_get_channel_binding_errors.phpt new file mode 100644 index 000000000000..edc2f220a846 --- /dev/null +++ b/ext/openssl/tests/stream_get_channel_binding_errors.phpt @@ -0,0 +1,49 @@ +--TEST-- +stream_get_channel_binding(): argument and type error handling +--EXTENSIONS-- +openssl +--FILE-- + ValueError (checked before the stream). */ +try { + stream_get_channel_binding(fopen("php://memory", "r"), "not-a-type"); + echo "no error\n"; +} catch (ValueError $e) { + echo "ValueError\n"; +} + +/* Case-sensitivity: a mismatched case is also unknown. */ +try { + stream_get_channel_binding(fopen("php://memory", "r"), "TLS-UNIQUE"); + echo "no error\n"; +} catch (ValueError $e) { + echo "ValueError (case sensitive)\n"; +} + +/* Non-stream argument -> TypeError. */ +try { + stream_get_channel_binding(123, "tls-unique"); + echo "no error\n"; +} catch (TypeError $e) { + echo "TypeError\n"; +} + +/* A stream without transport encryption -> RuntimeException. */ +$plain = fopen("php://memory", "r"); +foreach (["tls-unique", "tls-server-endpoint", "tls-exporter"] as $t) { + try { + stream_get_channel_binding($plain, $t); + echo "$t: no error\n"; + } catch (RuntimeException $e) { + echo "$t: RuntimeException\n"; + } +} +fclose($plain); +?> +--EXPECT-- +ValueError +ValueError (case sensitive) +TypeError +tls-unique: RuntimeException +tls-server-endpoint: RuntimeException +tls-exporter: RuntimeException diff --git a/ext/openssl/tests/stream_get_channel_binding_tls12.phpt b/ext/openssl/tests/stream_get_channel_binding_tls12.phpt new file mode 100644 index 000000000000..e36d083ebd1a --- /dev/null +++ b/ext/openssl/tests/stream_get_channel_binding_tls12.phpt @@ -0,0 +1,107 @@ +--TEST-- +stream_get_channel_binding(): TLS 1.2 full handshake, client and server agree +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'verify_peer' => false, + 'verify_peer_name' => false, + 'capture_peer_cert' => true, + 'security_level' => 0, + ]]); + $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN; + $server = stream_socket_server("tlsv1.2://127.0.0.1:0", $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + $conn = stream_socket_accept($server, 30); + if ($conn === false) { + echo "SERVER_EXCEPTION accept failed\n"; + exit(1); + } + foreach (["tls-unique", "tls-server-endpoint", "tls-exporter"] as $t) { + $v = stream_get_channel_binding($conn, $t); + fwrite($conn, $t . "=" . (is_string($v) ? bin2hex($v) : "") . "\n"); + } + phpt_wait(); + fclose($conn); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +/* Client: complete the handshake, compute its own values, read the server's + * values and report whether they agree. Also check tls-server-endpoint against + * the SHA-256 fingerprint of the captured peer certificate. */ +$clientCode = <<<'CODE' + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'capture_peer_cert' => true, + 'security_level' => 0, + ]]); + $client = stream_socket_client("tlsv1.2://{{ ADDR }}", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $ctx); + if ($client === false) { + echo "client connect failed\n"; + exit(1); + } + + $my = []; + foreach (["tls-unique", "tls-server-endpoint", "tls-exporter"] as $t) { + $my[$t] = stream_get_channel_binding($client, $t); + } + + $peer = []; + for ($i = 0; $i < 3; $i++) { + $line = fgets($client); + if ($line === false) break; + $line = rtrim($line); + $eq = strpos($line, "="); + $t = substr($line, 0, $eq); + $enc = substr($line, $eq + 1); + $peer[$t] = ($enc === "") ? null : hex2bin($enc); + } + + foreach (["tls-unique", "tls-server-endpoint", "tls-exporter"] as $t) { + $c = $my[$t]; + $s = $peer[$t]; + $lc = is_string($c) ? strlen($c) : "null"; + $ls = is_string($s) ? strlen($s) : "null"; + printf("%s equal=%s len_client=%s len_server=%s\n", + $t, var_export($c === $s, true), $lc, $ls); + } + + $opts = stream_context_get_options($client); + $cert = $opts['ssl']['peer_certificate'] ?? null; + $fpr = $cert ? openssl_x509_fingerprint($cert, "sha256", true) : null; + echo "tse_matches_sha256_fingerprint=" + . var_export($fpr !== null && $my["tls-server-endpoint"] === $fpr, true) . "\n"; + + phpt_notify('server'); + fclose($client); +CODE; + +include 'CertificateGenerator.inc'; +$generator = new CertificateGenerator(); +$generator->saveNewCertAsFileWithKey('cb-tls12', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, ['server' => $serverCode]); +?> +--CLEAN-- + +--EXPECTF-- +tls-unique equal=true len_client=%d len_server=%d +tls-server-endpoint equal=true len_client=32 len_server=32 +tls-exporter equal=true len_client=32 len_server=32 +tse_matches_sha256_fingerprint=true diff --git a/ext/openssl/tests/stream_get_channel_binding_tls13.phpt b/ext/openssl/tests/stream_get_channel_binding_tls13.phpt new file mode 100644 index 000000000000..bbc51a9430c3 --- /dev/null +++ b/ext/openssl/tests/stream_get_channel_binding_tls13.phpt @@ -0,0 +1,105 @@ +--TEST-- +stream_get_channel_binding(): TLS 1.3, tls-unique is not applicable +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'verify_peer' => false, + 'verify_peer_name' => false, + 'capture_peer_cert' => true, + 'security_level' => 0, + ]]); + $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN; + $server = stream_socket_server("tlsv1.3://127.0.0.1:0", $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + $conn = stream_socket_accept($server, 30); + if ($conn === false) { + echo "SERVER_EXCEPTION accept failed\n"; + exit(1); + } + foreach (["tls-unique", "tls-server-endpoint", "tls-exporter"] as $t) { + $v = stream_get_channel_binding($conn, $t); + fwrite($conn, $t . "=" . (is_string($v) ? bin2hex($v) : "") . "\n"); + } + phpt_wait(); + fclose($conn); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'capture_peer_cert' => true, + 'security_level' => 0, + ]]); + $client = stream_socket_client("tlsv1.3://{{ ADDR }}", $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $ctx); + if ($client === false) { + echo "client connect failed\n"; + exit(1); + } + + $my = []; + foreach (["tls-unique", "tls-server-endpoint", "tls-exporter"] as $t) { + $my[$t] = stream_get_channel_binding($client, $t); + } + + $peer = []; + for ($i = 0; $i < 3; $i++) { + $line = fgets($client); + if ($line === false) break; + $line = rtrim($line); + $eq = strpos($line, "="); + $t = substr($line, 0, $eq); + $enc = substr($line, $eq + 1); + $peer[$t] = ($enc === "") ? null : hex2bin($enc); + } + + foreach (["tls-unique", "tls-server-endpoint", "tls-exporter"] as $t) { + $c = $my[$t]; + $s = $peer[$t]; + $lc = is_string($c) ? strlen($c) : "null"; + $ls = is_string($s) ? strlen($s) : "null"; + printf("%s equal=%s len_client=%s len_server=%s\n", + $t, var_export($c === $s, true), $lc, $ls); + } + + echo "tls_unique_is_null=" . var_export($my["tls-unique"] === null, true) . "\n"; + + $opts = stream_context_get_options($client); + $cert = $opts['ssl']['peer_certificate'] ?? null; + $fpr = $cert ? openssl_x509_fingerprint($cert, "sha256", true) : null; + echo "tse_matches_sha256_fingerprint=" + . var_export($fpr !== null && $my["tls-server-endpoint"] === $fpr, true) . "\n"; + + phpt_notify('server'); + fclose($client); +CODE; + +include 'CertificateGenerator.inc'; +$generator = new CertificateGenerator(); +$generator->saveNewCertAsFileWithKey('cb-tls13', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, ['server' => $serverCode]); +?> +--CLEAN-- + +--EXPECT-- +tls-unique equal=true len_client=null len_server=null +tls-server-endpoint equal=true len_client=32 len_server=32 +tls-exporter equal=true len_client=32 len_server=32 +tls_unique_is_null=true +tse_matches_sha256_fingerprint=true diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 854227318da0..02ee9840179d 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -23,8 +23,10 @@ #include "ext/standard/file.h" #include "ext/uri/php_uri.h" #include "streams/php_streams_int.h" +#include "main/php_streams.h" #include "zend_smart_str.h" #include "zend_exceptions.h" +#include "ext/spl/spl_exceptions.h" #include "php_openssl.h" #include "php_openssl_backend.h" #include "php_io.h" @@ -35,6 +37,7 @@ #include #include #include +#include #ifdef PHP_WIN32 #include "win32/winutil.h" @@ -3946,6 +3949,228 @@ static const php_stream_ops php_openssl_socket_ops = { php_openssl_sockop_set_option, }; +/* Channel binding data types (RFC 5929, RFC 9266), as used for SASL + * channel binding (e.g. SCRAM-SHA-*-PLUS, RFC 5802 / RFC 5801). The string + * values accepted by stream_get_channel_binding() are the IANA "Channel + * Binding" registry names. */ +enum php_openssl_channel_binding_type { + PHP_OSSL_CB_TLS_UNIQUE = 0, + PHP_OSSL_CB_TLS_SERVER_ENDPOINT, + PHP_OSSL_CB_TLS_EXPORTER, +}; + +/* Result of php_openssl_netstream_get_channel_binding(). */ +enum php_openssl_channel_binding_result { + PHP_OSSL_CB_OK = 0, /* *out holds a zend_string with the data */ + PHP_OSSL_CB_NOT_APPLICABLE, /* *out = NULL (e.g. tls-unique over TLS 1.3) */ + PHP_OSSL_CB_NOT_TLS, /* stream is not an active TLS stream */ + PHP_OSSL_CB_ERROR, /* an OpenSSL-level failure occurred */ +}; + +/* + * Get the NID of the message-digest algorithm used to sign a certificate, + * or NID_undef if it cannot be determined. The parameter list of + * X509_get_signature_info() changed between OpenSSL 1.1.1 and 3.0. + */ +static int php_openssl_get_cert_signature_md_nid(const X509 *cert) /* {{{ */ +{ + int md_nid; + +#if OPENSSL_VERSION_NUMBER < 0x30000000L + int sign_alg, secbits, pkeybits; + + if (!X509_get_signature_info((X509 *)cert, &sign_alg, &md_nid, &secbits, &pkeybits)) { + return NID_undef; + } +#else + int pkey_nid, secbits; + uint32_t flags = 0; + + if (!X509_get_signature_info((X509 *)cert, &md_nid, &pkey_nid, &secbits, &flags)) { + return NID_undef; + } +#endif + return md_nid; +} +/* }}} */ + +/* + * Extract TLS channel binding data from an established openssl stream. + * + * On success *out is set to a newly-allocated zend_string holding the data + * and PHP_OSSL_CB_OK is returned. For a type that is not applicable to the + * connection (currently: tls-unique over TLS 1.3) *out is set to NULL and + * PHP_OSSL_CB_NOT_APPLICABLE is returned. If the stream is not an active TLS + * stream PHP_OSSL_CB_NOT_TLS is returned, and if an OpenSSL call fails + * PHP_OSSL_CB_ERROR is returned; in both of these cases *out is set to NULL. + */ +static int php_openssl_netstream_get_channel_binding( /* {{{ */ + php_stream *stream, int type, zend_string **out) +{ + unsigned char buf[EVP_MAX_MD_SIZE]; + size_t len = 0; + php_openssl_netstream_data_t *sslsock; + SSL *ssl; + + *out = NULL; + + if (stream == NULL || stream->ops != &php_openssl_socket_ops) { + return PHP_OSSL_CB_NOT_TLS; + } + sslsock = (php_openssl_netstream_data_t *)stream->abstract; + if (sslsock == NULL || !sslsock->ssl_active || sslsock->ssl_handle == NULL) { + return PHP_OSSL_CB_NOT_TLS; + } + ssl = sslsock->ssl_handle; + + switch (type) { + case PHP_OSSL_CB_TLS_UNIQUE: + /* RFC 5929 section 3: tls-unique is not defined for TLS 1.3. */ + if (SSL_version(ssl) >= TLS1_3_VERSION) { + return PHP_OSSL_CB_NOT_APPLICABLE; + } + + /* + * In TLS 1.2 and earlier both endpoints always transmit a Finished + * message. Pick it so that the client and the server derive the same + * value: in a full handshake that is the client's Finished (the client + * reads it from its own send buffer, the server from its receive + * buffer), and in a resumed handshake the server's Finished. This is + * the same selection CPython's _ssl.get_channel_binding() makes. + */ + if (sslsock->is_client ^ SSL_session_reused(ssl)) { + len = SSL_get_finished(ssl, buf, sizeof(buf)); + } else { + len = SSL_get_peer_finished(ssl, buf, sizeof(buf)); + } + if (len == 0) { + return PHP_OSSL_CB_ERROR; + } + *out = zend_string_init((char *)buf, len, 0); + return *out != NULL ? PHP_OSSL_CB_OK : PHP_OSSL_CB_ERROR; + + case PHP_OSSL_CB_TLS_SERVER_ENDPOINT: + { + X509 *cert = sslsock->is_client ? + SSL_get_peer_certificate(ssl) : SSL_get_certificate(ssl); + int md_nid, rc; + const EVP_MD *md; + unsigned int digest_len = 0; + + if (cert == NULL) { + return PHP_OSSL_CB_ERROR; + } + + md_nid = php_openssl_get_cert_signature_md_nid(cert); + /* + * RFC 5929 section 4.1: if the digest used to sign the + * certificate is MD5 or SHA-1, or cannot be determined, use + * SHA-256 instead. + */ + if (md_nid == NID_md5 || md_nid == NID_sha1 || md_nid == NID_undef) { + md_nid = NID_sha256; + } + md = EVP_get_digestbynid(md_nid); + if (md == NULL) { + md = EVP_sha256(); + } + rc = X509_digest(cert, md, buf, &digest_len); + /* + * SSL_get_peer_certificate() returns a certificate with an + * incremented reference count that we must free; + * SSL_get_certificate() returns the certificate owned by the + * SSL_CTX, which must not be freed. + */ + if (sslsock->is_client) { + X509_free(cert); + } + if (rc != 1) { + return PHP_OSSL_CB_ERROR; + } + *out = zend_string_init((char *)buf, digest_len, 0); + return *out != NULL ? PHP_OSSL_CB_OK : PHP_OSSL_CB_ERROR; + } + + case PHP_OSSL_CB_TLS_EXPORTER: + /* + * RFC 9266 section 4 / RFC 5705: 32 octets, label + * "EXPORTER-Channel-Binding", empty context. For an empty context the + * use_context flag is immaterial (see tls13_export_keying_material() + * in OpenSSL, which zeroes the context length when it is clear). + */ + if (SSL_export_keying_material( + ssl, buf, 32, + "EXPORTER-Channel-Binding", sizeof("EXPORTER-Channel-Binding") - 1, + (const unsigned char *)"", 0, 1) != 1) { + return PHP_OSSL_CB_ERROR; + } + *out = zend_string_init((char *)buf, 32, 0); + return *out != NULL ? PHP_OSSL_CB_OK : PHP_OSSL_CB_ERROR; + + default: + return PHP_OSSL_CB_ERROR; + } +} +/* }}} */ + +/* {{{ */ +PHP_FUNCTION(stream_get_channel_binding) +{ + php_stream *stream = NULL; + zend_string *type = NULL; + zend_string *result = NULL; + int type_code; + int ret; + + ZEND_PARSE_PARAMETERS_START(2, 2) + PHP_Z_PARAM_STREAM(stream) + Z_PARAM_STR(type) + ZEND_PARSE_PARAMETERS_END(); + + if (zend_string_equals_literal(type, "tls-unique")) { + type_code = PHP_OSSL_CB_TLS_UNIQUE; + } else if (zend_string_equals_literal(type, "tls-server-endpoint")) { + type_code = PHP_OSSL_CB_TLS_SERVER_ENDPOINT; + } else if (zend_string_equals_literal(type, "tls-exporter")) { + type_code = PHP_OSSL_CB_TLS_EXPORTER; + } else { + zend_value_error( + "%s(): argument #2 ($channel_binding_type) \"%s\" is not a known " + "channel binding type, expected \"tls-unique\", " + "\"tls-server-endpoint\" or \"tls-exporter\"", + get_active_function_name(), ZSTR_VAL(type)); + RETURN_THROWS(); + } + + ret = php_openssl_netstream_get_channel_binding(stream, type_code, &result); + switch (ret) { + case PHP_OSSL_CB_OK: + RETURN_STR(result); + case PHP_OSSL_CB_NOT_APPLICABLE: + RETURN_NULL(); + case PHP_OSSL_CB_NOT_TLS: + zend_throw_exception_ex(spl_ce_RuntimeException, 0, + "Stream does not have transport encryption enabled"); + RETURN_THROWS(); + case PHP_OSSL_CB_ERROR: + { + unsigned long err = ERR_peek_last_error(); + if (err != 0) { + char errstr[256]; + + (void)ERR_error_string_n(err, errstr, sizeof(errstr)); + zend_throw_exception_ex(spl_ce_RuntimeException, 0, + "Failed to get channel binding data: %s", errstr); + } else { + zend_throw_exception_ex(spl_ce_RuntimeException, 0, + "Failed to get channel binding data"); + } + } + RETURN_THROWS(); + } +} +/* }}} */ + static zend_long php_openssl_get_crypto_method( php_stream_context *ctx, zend_long crypto_method) /* {{{ */ { From 929179533c512b234a5dfbb7aab3c993e887a9f0 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Thu, 1 Oct 2026 08:18:46 +0000 Subject: [PATCH 2/8] Use tls-server-end-point instead of tls-server-endpoint --- ext/openssl/openssl.stub.php | 2 +- ext/openssl/openssl_arginfo.h | 2 +- .../tests/stream_get_channel_binding_errors.phpt | 4 ++-- .../tests/stream_get_channel_binding_tls12.phpt | 12 ++++++------ .../tests/stream_get_channel_binding_tls13.phpt | 10 +++++----- ext/openssl/xp_ssl.c | 4 ++-- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/ext/openssl/openssl.stub.php b/ext/openssl/openssl.stub.php index 589b22116aa1..8e49866dab54 100644 --- a/ext/openssl/openssl.stub.php +++ b/ext/openssl/openssl.stub.php @@ -784,7 +784,7 @@ function openssl_password_verify(string $algo, #[\SensitiveParameter] string $pa * * @param resource $stream * @param string $channel_binding_type One of "tls-unique", - * "tls-server-endpoint" or "tls-exporter" (the IANA "Channel Binding" + * "tls-server-end-point" or "tls-exporter" (the IANA "Channel Binding" * registry names, RFC 5929 / RFC 9266). * @return string|null The channel binding data, or NULL when it is not * applicable to the connection (currently "tls-unique" over TLS 1.3). diff --git a/ext/openssl/openssl_arginfo.h b/ext/openssl/openssl_arginfo.h index fcdae745838b..f5ed0896ff15 100644 --- a/ext/openssl/openssl_arginfo.h +++ b/ext/openssl/openssl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit openssl.stub.php instead. - * Stub hash: 00e3e90742171820379330fb2037be2a59fd65ce */ + * Stub hash: 0176f3c5793093d29ee8b9dfe8868d0e3835c046 */ #include "zend_attributes.h" #include "zend_constants.h" diff --git a/ext/openssl/tests/stream_get_channel_binding_errors.phpt b/ext/openssl/tests/stream_get_channel_binding_errors.phpt index edc2f220a846..1677037f9faf 100644 --- a/ext/openssl/tests/stream_get_channel_binding_errors.phpt +++ b/ext/openssl/tests/stream_get_channel_binding_errors.phpt @@ -30,7 +30,7 @@ try { /* A stream without transport encryption -> RuntimeException. */ $plain = fopen("php://memory", "r"); -foreach (["tls-unique", "tls-server-endpoint", "tls-exporter"] as $t) { +foreach (["tls-unique", "tls-server-end-point", "tls-exporter"] as $t) { try { stream_get_channel_binding($plain, $t); echo "$t: no error\n"; @@ -45,5 +45,5 @@ ValueError ValueError (case sensitive) TypeError tls-unique: RuntimeException -tls-server-endpoint: RuntimeException +tls-server-end-point: RuntimeException tls-exporter: RuntimeException diff --git a/ext/openssl/tests/stream_get_channel_binding_tls12.phpt b/ext/openssl/tests/stream_get_channel_binding_tls12.phpt index e36d083ebd1a..008e4b139ba5 100644 --- a/ext/openssl/tests/stream_get_channel_binding_tls12.phpt +++ b/ext/openssl/tests/stream_get_channel_binding_tls12.phpt @@ -29,7 +29,7 @@ $serverCode = <<<'CODE' echo "SERVER_EXCEPTION accept failed\n"; exit(1); } - foreach (["tls-unique", "tls-server-endpoint", "tls-exporter"] as $t) { + foreach (["tls-unique", "tls-server-end-point", "tls-exporter"] as $t) { $v = stream_get_channel_binding($conn, $t); fwrite($conn, $t . "=" . (is_string($v) ? bin2hex($v) : "") . "\n"); } @@ -39,7 +39,7 @@ CODE; $serverCode = sprintf($serverCode, $certFile); /* Client: complete the handshake, compute its own values, read the server's - * values and report whether they agree. Also check tls-server-endpoint against + * values and report whether they agree. Also check tls-server-end-point against * the SHA-256 fingerprint of the captured peer certificate. */ $clientCode = <<<'CODE' $ctx = stream_context_create(['ssl' => [ @@ -55,7 +55,7 @@ $clientCode = <<<'CODE' } $my = []; - foreach (["tls-unique", "tls-server-endpoint", "tls-exporter"] as $t) { + foreach (["tls-unique", "tls-server-end-point", "tls-exporter"] as $t) { $my[$t] = stream_get_channel_binding($client, $t); } @@ -70,7 +70,7 @@ $clientCode = <<<'CODE' $peer[$t] = ($enc === "") ? null : hex2bin($enc); } - foreach (["tls-unique", "tls-server-endpoint", "tls-exporter"] as $t) { + foreach (["tls-unique", "tls-server-end-point", "tls-exporter"] as $t) { $c = $my[$t]; $s = $peer[$t]; $lc = is_string($c) ? strlen($c) : "null"; @@ -83,7 +83,7 @@ $clientCode = <<<'CODE' $cert = $opts['ssl']['peer_certificate'] ?? null; $fpr = $cert ? openssl_x509_fingerprint($cert, "sha256", true) : null; echo "tse_matches_sha256_fingerprint=" - . var_export($fpr !== null && $my["tls-server-endpoint"] === $fpr, true) . "\n"; + . var_export($fpr !== null && $my["tls-server-end-point"] === $fpr, true) . "\n"; phpt_notify('server'); fclose($client); @@ -102,6 +102,6 @@ ServerClientTestCase::getInstance()->run($clientCode, ['server' => $serverCode]) ?> --EXPECTF-- tls-unique equal=true len_client=%d len_server=%d -tls-server-endpoint equal=true len_client=32 len_server=32 +tls-server-end-point equal=true len_client=32 len_server=32 tls-exporter equal=true len_client=32 len_server=32 tse_matches_sha256_fingerprint=true diff --git a/ext/openssl/tests/stream_get_channel_binding_tls13.phpt b/ext/openssl/tests/stream_get_channel_binding_tls13.phpt index bbc51a9430c3..bd18dcc9bf9c 100644 --- a/ext/openssl/tests/stream_get_channel_binding_tls13.phpt +++ b/ext/openssl/tests/stream_get_channel_binding_tls13.phpt @@ -27,7 +27,7 @@ $serverCode = <<<'CODE' echo "SERVER_EXCEPTION accept failed\n"; exit(1); } - foreach (["tls-unique", "tls-server-endpoint", "tls-exporter"] as $t) { + foreach (["tls-unique", "tls-server-end-point", "tls-exporter"] as $t) { $v = stream_get_channel_binding($conn, $t); fwrite($conn, $t . "=" . (is_string($v) ? bin2hex($v) : "") . "\n"); } @@ -50,7 +50,7 @@ $clientCode = <<<'CODE' } $my = []; - foreach (["tls-unique", "tls-server-endpoint", "tls-exporter"] as $t) { + foreach (["tls-unique", "tls-server-end-point", "tls-exporter"] as $t) { $my[$t] = stream_get_channel_binding($client, $t); } @@ -65,7 +65,7 @@ $clientCode = <<<'CODE' $peer[$t] = ($enc === "") ? null : hex2bin($enc); } - foreach (["tls-unique", "tls-server-endpoint", "tls-exporter"] as $t) { + foreach (["tls-unique", "tls-server-end-point", "tls-exporter"] as $t) { $c = $my[$t]; $s = $peer[$t]; $lc = is_string($c) ? strlen($c) : "null"; @@ -80,7 +80,7 @@ $clientCode = <<<'CODE' $cert = $opts['ssl']['peer_certificate'] ?? null; $fpr = $cert ? openssl_x509_fingerprint($cert, "sha256", true) : null; echo "tse_matches_sha256_fingerprint=" - . var_export($fpr !== null && $my["tls-server-endpoint"] === $fpr, true) . "\n"; + . var_export($fpr !== null && $my["tls-server-end-point"] === $fpr, true) . "\n"; phpt_notify('server'); fclose($client); @@ -99,7 +99,7 @@ ServerClientTestCase::getInstance()->run($clientCode, ['server' => $serverCode]) ?> --EXPECT-- tls-unique equal=true len_client=null len_server=null -tls-server-endpoint equal=true len_client=32 len_server=32 +tls-server-end-point equal=true len_client=32 len_server=32 tls-exporter equal=true len_client=32 len_server=32 tls_unique_is_null=true tse_matches_sha256_fingerprint=true diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 02ee9840179d..0219f48208ca 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -4129,7 +4129,7 @@ PHP_FUNCTION(stream_get_channel_binding) if (zend_string_equals_literal(type, "tls-unique")) { type_code = PHP_OSSL_CB_TLS_UNIQUE; - } else if (zend_string_equals_literal(type, "tls-server-endpoint")) { + } else if (zend_string_equals_literal(type, "tls-server-end-point")) { type_code = PHP_OSSL_CB_TLS_SERVER_ENDPOINT; } else if (zend_string_equals_literal(type, "tls-exporter")) { type_code = PHP_OSSL_CB_TLS_EXPORTER; @@ -4137,7 +4137,7 @@ PHP_FUNCTION(stream_get_channel_binding) zend_value_error( "%s(): argument #2 ($channel_binding_type) \"%s\" is not a known " "channel binding type, expected \"tls-unique\", " - "\"tls-server-endpoint\" or \"tls-exporter\"", + "\"tls-server-end-point\" or \"tls-exporter\"", get_active_function_name(), ZSTR_VAL(type)); RETURN_THROWS(); } From 30820f94c61013b5bb70cb63f7a0b6ad6f054b83 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Thu, 1 Oct 2026 08:22:34 +0000 Subject: [PATCH 3/8] Remove parameter list switch for X509_get_signature_info --- ext/openssl/xp_ssl.c | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 0219f48208ca..6e43488a71a2 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -3969,27 +3969,17 @@ enum php_openssl_channel_binding_result { /* * Get the NID of the message-digest algorithm used to sign a certificate, - * or NID_undef if it cannot be determined. The parameter list of - * X509_get_signature_info() changed between OpenSSL 1.1.1 and 3.0. + * or NID_undef if it cannot be determined. */ static int php_openssl_get_cert_signature_md_nid(const X509 *cert) /* {{{ */ { - int md_nid; - -#if OPENSSL_VERSION_NUMBER < 0x30000000L - int sign_alg, secbits, pkeybits; - - if (!X509_get_signature_info((X509 *)cert, &sign_alg, &md_nid, &secbits, &pkeybits)) { - return NID_undef; - } -#else - int pkey_nid, secbits; + int md_nid, pkey_nid, secbits; uint32_t flags = 0; if (!X509_get_signature_info((X509 *)cert, &md_nid, &pkey_nid, &secbits, &flags)) { return NID_undef; } -#endif + return md_nid; } /* }}} */ From da5405bad45c6dc7cbf2dc68ed292f52e476ca3b Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Thu, 1 Oct 2026 08:28:15 +0000 Subject: [PATCH 4/8] Add stream_get_channel_binding to zend optimizer --- Zend/Optimizer/zend_func_infos.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Zend/Optimizer/zend_func_infos.h b/Zend/Optimizer/zend_func_infos.h index 0add5a32f911..2c8e3dbbd400 100644 --- a/Zend/Optimizer/zend_func_infos.h +++ b/Zend/Optimizer/zend_func_infos.h @@ -297,6 +297,7 @@ static const func_info_t func_infos[] = { F1("openssl_get_curve_names", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_STRING|MAY_BE_FALSE), #endif F1("openssl_get_cert_locations", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_STRING), + F1("stream_get_channel_binding", MAY_BE_STRING|MAY_BE_NULL), FN("pcntl_signal_get_handler", MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_STRING|MAY_BE_ARRAY_OF_OBJECT|MAY_BE_OBJECT|MAY_BE_LONG), FN("preg_replace", MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_STRING|MAY_BE_NULL), FN("preg_filter", MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_STRING|MAY_BE_NULL), From fb8217b7bf73aeb36eda3b430d9cb9f10997f48a Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Thu, 1 Oct 2026 09:46:27 +0000 Subject: [PATCH 5/8] Fix stream_get_channel_binding stub --- ext/openssl/openssl.stub.php | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/ext/openssl/openssl.stub.php b/ext/openssl/openssl.stub.php index 8e49866dab54..740c2fb36913 100644 --- a/ext/openssl/openssl.stub.php +++ b/ext/openssl/openssl.stub.php @@ -777,19 +777,8 @@ function openssl_password_verify(string $algo, #[\SensitiveParameter] string $pa #endif /** - * Get TLS channel binding data for the connection behind a stream. - * - * The binding data is only available once the TLS handshake has completed; - * for a stream without transport encryption a RuntimeException is thrown. - * * @param resource $stream - * @param string $channel_binding_type One of "tls-unique", - * "tls-server-end-point" or "tls-exporter" (the IANA "Channel Binding" - * registry names, RFC 5929 / RFC 9266). - * @return string|null The channel binding data, or NULL when it is not - * applicable to the connection (currently "tls-unique" over TLS 1.3). - * @throws RuntimeException - * @throws ValueError + * @refcount 1 */ function stream_get_channel_binding($stream, string $channel_binding_type): ?string {} From e932f5499e6ed45dab482c9ef26230f46fc2ee8a Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Thu, 1 Oct 2026 10:20:56 +0000 Subject: [PATCH 6/8] Fix arginfo --- ext/openssl/openssl_arginfo.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/openssl/openssl_arginfo.h b/ext/openssl/openssl_arginfo.h index f5ed0896ff15..a761d1d34b59 100644 --- a/ext/openssl/openssl_arginfo.h +++ b/ext/openssl/openssl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit openssl.stub.php instead. - * Stub hash: 0176f3c5793093d29ee8b9dfe8868d0e3835c046 */ + * Stub hash: f71f04eb6d0e02d4cb5589e8783b8ca615a2b371 */ #include "zend_attributes.h" #include "zend_constants.h" From f57f3e2e9a5c4195fb8cd88ac3d1d7cabdc2a7ec Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Mon, 5 Oct 2026 11:40:44 +0000 Subject: [PATCH 7/8] Rename to openssl_get_channel_binding --- Zend/Optimizer/zend_func_infos.h | 2 +- ext/openssl/openssl.stub.php | 2 +- ext/openssl/openssl_arginfo.h | 8 ++++---- ...rs.phpt => openssl_get_channel_binding_errors.phpt} | 10 +++++----- ...s12.phpt => openssl_get_channel_binding_tls12.phpt} | 6 +++--- ...s13.phpt => openssl_get_channel_binding_tls13.phpt} | 6 +++--- ext/openssl/xp_ssl.c | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) rename ext/openssl/tests/{stream_get_channel_binding_errors.phpt => openssl_get_channel_binding_errors.phpt} (75%) rename ext/openssl/tests/{stream_get_channel_binding_tls12.phpt => openssl_get_channel_binding_tls12.phpt} (94%) rename ext/openssl/tests/{stream_get_channel_binding_tls13.phpt => openssl_get_channel_binding_tls13.phpt} (94%) diff --git a/Zend/Optimizer/zend_func_infos.h b/Zend/Optimizer/zend_func_infos.h index 2c8e3dbbd400..7367018fef4d 100644 --- a/Zend/Optimizer/zend_func_infos.h +++ b/Zend/Optimizer/zend_func_infos.h @@ -297,7 +297,7 @@ static const func_info_t func_infos[] = { F1("openssl_get_curve_names", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_STRING|MAY_BE_FALSE), #endif F1("openssl_get_cert_locations", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_STRING), - F1("stream_get_channel_binding", MAY_BE_STRING|MAY_BE_NULL), + F1("openssl_get_channel_binding", MAY_BE_STRING|MAY_BE_NULL), FN("pcntl_signal_get_handler", MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_STRING|MAY_BE_ARRAY_OF_OBJECT|MAY_BE_OBJECT|MAY_BE_LONG), FN("preg_replace", MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_STRING|MAY_BE_NULL), FN("preg_filter", MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_STRING|MAY_BE_NULL), diff --git a/ext/openssl/openssl.stub.php b/ext/openssl/openssl.stub.php index 740c2fb36913..712cc8d22e94 100644 --- a/ext/openssl/openssl.stub.php +++ b/ext/openssl/openssl.stub.php @@ -780,6 +780,6 @@ function openssl_password_verify(string $algo, #[\SensitiveParameter] string $pa * @param resource $stream * @refcount 1 */ -function stream_get_channel_binding($stream, string $channel_binding_type): ?string {} +function openssl_get_channel_binding($stream, string $channel_binding_type): ?string {} } diff --git a/ext/openssl/openssl_arginfo.h b/ext/openssl/openssl_arginfo.h index a761d1d34b59..0f7ea643a219 100644 --- a/ext/openssl/openssl_arginfo.h +++ b/ext/openssl/openssl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit openssl.stub.php instead. - * Stub hash: f71f04eb6d0e02d4cb5589e8783b8ca615a2b371 */ + * Stub hash: 0c7e0942232271e2696203ab80ba01958b46c5ae */ #include "zend_attributes.h" #include "zend_constants.h" @@ -411,7 +411,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_password_verify, 0, 3, _ ZEND_END_ARG_INFO() #endif -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_stream_get_channel_binding, 0, 2, IS_STRING, 1) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_get_channel_binding, 0, 2, IS_STRING, 1) ZEND_ARG_INFO(0, stream) ZEND_ARG_TYPE_INFO(0, channel_binding_type, IS_STRING, 0) ZEND_END_ARG_INFO() @@ -521,7 +521,7 @@ ZEND_FUNCTION(openssl_get_cert_locations); ZEND_FUNCTION(openssl_password_hash); ZEND_FUNCTION(openssl_password_verify); #endif -ZEND_FUNCTION(stream_get_channel_binding); +ZEND_FUNCTION(openssl_get_channel_binding); ZEND_METHOD(Openssl_Psk, __construct); ZEND_METHOD(Openssl_Session, export); ZEND_METHOD(Openssl_Session, import); @@ -606,7 +606,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(openssl_password_hash, arginfo_openssl_password_hash) ZEND_FE(openssl_password_verify, arginfo_openssl_password_verify) #endif - ZEND_FE(stream_get_channel_binding, arginfo_stream_get_channel_binding) + ZEND_FE(openssl_get_channel_binding, arginfo_openssl_get_channel_binding) ZEND_FE_END }; diff --git a/ext/openssl/tests/stream_get_channel_binding_errors.phpt b/ext/openssl/tests/openssl_get_channel_binding_errors.phpt similarity index 75% rename from ext/openssl/tests/stream_get_channel_binding_errors.phpt rename to ext/openssl/tests/openssl_get_channel_binding_errors.phpt index 1677037f9faf..a2b7903d1f99 100644 --- a/ext/openssl/tests/stream_get_channel_binding_errors.phpt +++ b/ext/openssl/tests/openssl_get_channel_binding_errors.phpt @@ -1,12 +1,12 @@ --TEST-- -stream_get_channel_binding(): argument and type error handling +openssl_get_channel_binding(): argument and type error handling --EXTENSIONS-- openssl --FILE-- ValueError (checked before the stream). */ try { - stream_get_channel_binding(fopen("php://memory", "r"), "not-a-type"); + openssl_get_channel_binding(fopen("php://memory", "r"), "not-a-type"); echo "no error\n"; } catch (ValueError $e) { echo "ValueError\n"; @@ -14,7 +14,7 @@ try { /* Case-sensitivity: a mismatched case is also unknown. */ try { - stream_get_channel_binding(fopen("php://memory", "r"), "TLS-UNIQUE"); + openssl_get_channel_binding(fopen("php://memory", "r"), "TLS-UNIQUE"); echo "no error\n"; } catch (ValueError $e) { echo "ValueError (case sensitive)\n"; @@ -22,7 +22,7 @@ try { /* Non-stream argument -> TypeError. */ try { - stream_get_channel_binding(123, "tls-unique"); + openssl_get_channel_binding(123, "tls-unique"); echo "no error\n"; } catch (TypeError $e) { echo "TypeError\n"; @@ -32,7 +32,7 @@ try { $plain = fopen("php://memory", "r"); foreach (["tls-unique", "tls-server-end-point", "tls-exporter"] as $t) { try { - stream_get_channel_binding($plain, $t); + openssl_get_channel_binding($plain, $t); echo "$t: no error\n"; } catch (RuntimeException $e) { echo "$t: RuntimeException\n"; diff --git a/ext/openssl/tests/stream_get_channel_binding_tls12.phpt b/ext/openssl/tests/openssl_get_channel_binding_tls12.phpt similarity index 94% rename from ext/openssl/tests/stream_get_channel_binding_tls12.phpt rename to ext/openssl/tests/openssl_get_channel_binding_tls12.phpt index 008e4b139ba5..bb09b424aae8 100644 --- a/ext/openssl/tests/stream_get_channel_binding_tls12.phpt +++ b/ext/openssl/tests/openssl_get_channel_binding_tls12.phpt @@ -1,5 +1,5 @@ --TEST-- -stream_get_channel_binding(): TLS 1.2 full handshake, client and server agree +openssl_get_channel_binding(): TLS 1.2 full handshake, client and server agree --EXTENSIONS-- openssl --SKIPIF-- @@ -30,7 +30,7 @@ $serverCode = <<<'CODE' exit(1); } foreach (["tls-unique", "tls-server-end-point", "tls-exporter"] as $t) { - $v = stream_get_channel_binding($conn, $t); + $v = openssl_get_channel_binding($conn, $t); fwrite($conn, $t . "=" . (is_string($v) ? bin2hex($v) : "") . "\n"); } phpt_wait(); @@ -56,7 +56,7 @@ $clientCode = <<<'CODE' $my = []; foreach (["tls-unique", "tls-server-end-point", "tls-exporter"] as $t) { - $my[$t] = stream_get_channel_binding($client, $t); + $my[$t] = openssl_get_channel_binding($client, $t); } $peer = []; diff --git a/ext/openssl/tests/stream_get_channel_binding_tls13.phpt b/ext/openssl/tests/openssl_get_channel_binding_tls13.phpt similarity index 94% rename from ext/openssl/tests/stream_get_channel_binding_tls13.phpt rename to ext/openssl/tests/openssl_get_channel_binding_tls13.phpt index bd18dcc9bf9c..e147dc27e0a7 100644 --- a/ext/openssl/tests/stream_get_channel_binding_tls13.phpt +++ b/ext/openssl/tests/openssl_get_channel_binding_tls13.phpt @@ -1,5 +1,5 @@ --TEST-- -stream_get_channel_binding(): TLS 1.3, tls-unique is not applicable +openssl_get_channel_binding(): TLS 1.3, tls-unique is not applicable --EXTENSIONS-- openssl --SKIPIF-- @@ -28,7 +28,7 @@ $serverCode = <<<'CODE' exit(1); } foreach (["tls-unique", "tls-server-end-point", "tls-exporter"] as $t) { - $v = stream_get_channel_binding($conn, $t); + $v = openssl_get_channel_binding($conn, $t); fwrite($conn, $t . "=" . (is_string($v) ? bin2hex($v) : "") . "\n"); } phpt_wait(); @@ -51,7 +51,7 @@ $clientCode = <<<'CODE' $my = []; foreach (["tls-unique", "tls-server-end-point", "tls-exporter"] as $t) { - $my[$t] = stream_get_channel_binding($client, $t); + $my[$t] = openssl_get_channel_binding($client, $t); } $peer = []; diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 6e43488a71a2..5dddd0ea1ccd 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -4104,7 +4104,7 @@ static int php_openssl_netstream_get_channel_binding( /* {{{ */ /* }}} */ /* {{{ */ -PHP_FUNCTION(stream_get_channel_binding) +PHP_FUNCTION(openssl_get_channel_binding) { php_stream *stream = NULL; zend_string *type = NULL; From 8bd3d4c4ee2d21ac22c34253cae4097e8885ede7 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Mon, 5 Oct 2026 12:55:11 +0000 Subject: [PATCH 8/8] Move PHP function to openssl.c, don't throw SPL exceptions --- ext/openssl/openssl.c | 60 ++++++++++++++ ext/openssl/php_openssl.h | 19 +++++ .../openssl_get_channel_binding_errors.phpt | 28 +++---- ext/openssl/xp_ssl.c | 79 +------------------ 4 files changed, 94 insertions(+), 92 deletions(-) diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index b05ee7418d0c..8f7619c2eccc 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -5104,3 +5104,63 @@ PHP_FUNCTION(openssl_random_pseudo_bytes) } } /* }}} */ + +/* {{{ */ +PHP_FUNCTION(openssl_get_channel_binding) +{ + php_stream *stream = NULL; + zend_string *type = NULL; + zend_string *result = NULL; + int type_code; + int ret; + + ZEND_PARSE_PARAMETERS_START(2, 2) + PHP_Z_PARAM_STREAM(stream) + Z_PARAM_STR(type) + ZEND_PARSE_PARAMETERS_END(); + + if (zend_string_equals_literal(type, "tls-unique")) { + type_code = PHP_OSSL_CB_TLS_UNIQUE; + } else if (zend_string_equals_literal(type, "tls-server-end-point")) { + type_code = PHP_OSSL_CB_TLS_SERVER_ENDPOINT; + } else if (zend_string_equals_literal(type, "tls-exporter")) { + type_code = PHP_OSSL_CB_TLS_EXPORTER; + } else { + zend_value_error( + "%s(): argument #2 ($channel_binding_type) \"%s\" is not a known " + "channel binding type, expected \"tls-unique\", " + "\"tls-server-end-point\" or \"tls-exporter\"", + get_active_function_name(), ZSTR_VAL(type)); + RETURN_THROWS(); + } + + ret = php_openssl_netstream_get_channel_binding(stream, type_code, &result); + switch (ret) { + case PHP_OSSL_CB_OK: + RETURN_STR(result); + case PHP_OSSL_CB_NOT_APPLICABLE: + RETURN_NULL(); + case PHP_OSSL_CB_NOT_TLS: + zend_throw_exception_ex(php_openssl_exception_ce, 0, + "Stream does not have transport encryption enabled"); + RETURN_THROWS(); + case PHP_OSSL_CB_ERROR: + { + unsigned long err = ERR_peek_last_error(); + if (err != 0) { + char errstr[256]; + + (void)ERR_error_string_n(err, errstr, sizeof(errstr)); + zend_throw_exception_ex(php_openssl_exception_ce, 0, + "Failed to get channel binding data: %s", errstr); + } else { + zend_throw_exception_ex(php_openssl_exception_ce, 0, + "Failed to get channel binding data"); + } + } + RETURN_THROWS(); + default: + ZEND_UNREACHABLE(); + } +} +/* }}} */ diff --git a/ext/openssl/php_openssl.h b/ext/openssl/php_openssl.h index 588cca0e93d8..25516d33bbee 100644 --- a/ext/openssl/php_openssl.h +++ b/ext/openssl/php_openssl.h @@ -236,6 +236,25 @@ extern zend_class_entry *php_openssl_session_ce; void php_openssl_session_object_init(zval *zv, SSL_SESSION *session); bool php_openssl_is_session_ce(zval *val); SSL_SESSION *php_openssl_session_from_zval(zval *zv); +int php_openssl_netstream_get_channel_binding(struct _php_stream *stream, int type, zend_string **out); + +/* Channel binding data types (RFC 5929, RFC 9266), as used for SASL + * channel binding (e.g. SCRAM-SHA-*-PLUS, RFC 5802 / RFC 5801). The string + * values accepted by stream_get_channel_binding() are the IANA "Channel + * Binding" registry names. */ +enum php_openssl_channel_binding_type { + PHP_OSSL_CB_TLS_UNIQUE = 0, + PHP_OSSL_CB_TLS_SERVER_ENDPOINT, + PHP_OSSL_CB_TLS_EXPORTER, +}; + +/* Result of php_openssl_netstream_get_channel_binding(). */ +enum php_openssl_channel_binding_result { + PHP_OSSL_CB_OK = 0, /* *out holds a zend_string with the data */ + PHP_OSSL_CB_NOT_APPLICABLE, /* *out = NULL (e.g. tls-unique over TLS 1.3) */ + PHP_OSSL_CB_NOT_TLS, /* stream is not an active TLS stream */ + PHP_OSSL_CB_ERROR, /* an OpenSSL-level failure occurred */ +}; #if defined(HAVE_OPENSSL_ARGON2) diff --git a/ext/openssl/tests/openssl_get_channel_binding_errors.phpt b/ext/openssl/tests/openssl_get_channel_binding_errors.phpt index a2b7903d1f99..7563b0321ead 100644 --- a/ext/openssl/tests/openssl_get_channel_binding_errors.phpt +++ b/ext/openssl/tests/openssl_get_channel_binding_errors.phpt @@ -8,24 +8,24 @@ openssl try { openssl_get_channel_binding(fopen("php://memory", "r"), "not-a-type"); echo "no error\n"; -} catch (ValueError $e) { - echo "ValueError\n"; +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; } /* Case-sensitivity: a mismatched case is also unknown. */ try { openssl_get_channel_binding(fopen("php://memory", "r"), "TLS-UNIQUE"); echo "no error\n"; -} catch (ValueError $e) { - echo "ValueError (case sensitive)\n"; +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; } /* Non-stream argument -> TypeError. */ try { openssl_get_channel_binding(123, "tls-unique"); echo "no error\n"; -} catch (TypeError $e) { - echo "TypeError\n"; +} catch (Throwable $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; } /* A stream without transport encryption -> RuntimeException. */ @@ -34,16 +34,16 @@ foreach (["tls-unique", "tls-server-end-point", "tls-exporter"] as $t) { try { openssl_get_channel_binding($plain, $t); echo "$t: no error\n"; - } catch (RuntimeException $e) { - echo "$t: RuntimeException\n"; + } catch (Throwable $e) { + echo $t, ': ', $e::class, ': ', $e->getMessage(), "\n"; } } fclose($plain); ?> --EXPECT-- -ValueError -ValueError (case sensitive) -TypeError -tls-unique: RuntimeException -tls-server-end-point: RuntimeException -tls-exporter: RuntimeException +ValueError: openssl_get_channel_binding(): argument #2 ($channel_binding_type) "not-a-type" is not a known channel binding type, expected "tls-unique", "tls-server-end-point" or "tls-exporter" +ValueError: openssl_get_channel_binding(): argument #2 ($channel_binding_type) "TLS-UNIQUE" is not a known channel binding type, expected "tls-unique", "tls-server-end-point" or "tls-exporter" +TypeError: openssl_get_channel_binding(): Argument #1 ($stream) must be of type resource, int given +tls-unique: Openssl\OpensslException: Stream does not have transport encryption enabled +tls-server-end-point: Openssl\OpensslException: Stream does not have transport encryption enabled +tls-exporter: Openssl\OpensslException: Stream does not have transport encryption enabled diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 5dddd0ea1ccd..55f4eceebad1 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -26,7 +26,6 @@ #include "main/php_streams.h" #include "zend_smart_str.h" #include "zend_exceptions.h" -#include "ext/spl/spl_exceptions.h" #include "php_openssl.h" #include "php_openssl_backend.h" #include "php_io.h" @@ -3949,24 +3948,6 @@ static const php_stream_ops php_openssl_socket_ops = { php_openssl_sockop_set_option, }; -/* Channel binding data types (RFC 5929, RFC 9266), as used for SASL - * channel binding (e.g. SCRAM-SHA-*-PLUS, RFC 5802 / RFC 5801). The string - * values accepted by stream_get_channel_binding() are the IANA "Channel - * Binding" registry names. */ -enum php_openssl_channel_binding_type { - PHP_OSSL_CB_TLS_UNIQUE = 0, - PHP_OSSL_CB_TLS_SERVER_ENDPOINT, - PHP_OSSL_CB_TLS_EXPORTER, -}; - -/* Result of php_openssl_netstream_get_channel_binding(). */ -enum php_openssl_channel_binding_result { - PHP_OSSL_CB_OK = 0, /* *out holds a zend_string with the data */ - PHP_OSSL_CB_NOT_APPLICABLE, /* *out = NULL (e.g. tls-unique over TLS 1.3) */ - PHP_OSSL_CB_NOT_TLS, /* stream is not an active TLS stream */ - PHP_OSSL_CB_ERROR, /* an OpenSSL-level failure occurred */ -}; - /* * Get the NID of the message-digest algorithm used to sign a certificate, * or NID_undef if it cannot be determined. @@ -3994,7 +3975,7 @@ static int php_openssl_get_cert_signature_md_nid(const X509 *cert) /* {{{ */ * stream PHP_OSSL_CB_NOT_TLS is returned, and if an OpenSSL call fails * PHP_OSSL_CB_ERROR is returned; in both of these cases *out is set to NULL. */ -static int php_openssl_netstream_get_channel_binding( /* {{{ */ +int php_openssl_netstream_get_channel_binding( /* {{{ */ php_stream *stream, int type, zend_string **out) { unsigned char buf[EVP_MAX_MD_SIZE]; @@ -4103,64 +4084,6 @@ static int php_openssl_netstream_get_channel_binding( /* {{{ */ } /* }}} */ -/* {{{ */ -PHP_FUNCTION(openssl_get_channel_binding) -{ - php_stream *stream = NULL; - zend_string *type = NULL; - zend_string *result = NULL; - int type_code; - int ret; - - ZEND_PARSE_PARAMETERS_START(2, 2) - PHP_Z_PARAM_STREAM(stream) - Z_PARAM_STR(type) - ZEND_PARSE_PARAMETERS_END(); - - if (zend_string_equals_literal(type, "tls-unique")) { - type_code = PHP_OSSL_CB_TLS_UNIQUE; - } else if (zend_string_equals_literal(type, "tls-server-end-point")) { - type_code = PHP_OSSL_CB_TLS_SERVER_ENDPOINT; - } else if (zend_string_equals_literal(type, "tls-exporter")) { - type_code = PHP_OSSL_CB_TLS_EXPORTER; - } else { - zend_value_error( - "%s(): argument #2 ($channel_binding_type) \"%s\" is not a known " - "channel binding type, expected \"tls-unique\", " - "\"tls-server-end-point\" or \"tls-exporter\"", - get_active_function_name(), ZSTR_VAL(type)); - RETURN_THROWS(); - } - - ret = php_openssl_netstream_get_channel_binding(stream, type_code, &result); - switch (ret) { - case PHP_OSSL_CB_OK: - RETURN_STR(result); - case PHP_OSSL_CB_NOT_APPLICABLE: - RETURN_NULL(); - case PHP_OSSL_CB_NOT_TLS: - zend_throw_exception_ex(spl_ce_RuntimeException, 0, - "Stream does not have transport encryption enabled"); - RETURN_THROWS(); - case PHP_OSSL_CB_ERROR: - { - unsigned long err = ERR_peek_last_error(); - if (err != 0) { - char errstr[256]; - - (void)ERR_error_string_n(err, errstr, sizeof(errstr)); - zend_throw_exception_ex(spl_ce_RuntimeException, 0, - "Failed to get channel binding data: %s", errstr); - } else { - zend_throw_exception_ex(spl_ce_RuntimeException, 0, - "Failed to get channel binding data"); - } - } - RETURN_THROWS(); - } -} -/* }}} */ - static zend_long php_openssl_get_crypto_method( php_stream_context *ctx, zend_long crypto_method) /* {{{ */ {