From 91ad65fca48e0ca48bccb6efb57c197a45f012b8 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Wed, 11 Nov 2026 18:55:33 +0100 Subject: [PATCH] ext/openssl: Fix SNI certs not used on streams sharing a session SSL_CTX Since b1242c32bca a server stream seeded from a session stream shares the parent's SSL_CTX and ignores its own context options. The SNI callback registered on that context looks up the certificate table of the stream doing the handshake, but the table was left only on the parent, so the child always served the default certificate. Give the child its own table that references the parent's per-name contexts. --- NEWS | 2 + .../tests/session_resumption_server_sni.phpt | 78 +++++++++++++++++++ ext/openssl/xp_ssl.c | 30 +++++++ 3 files changed, 110 insertions(+) create mode 100644 ext/openssl/tests/session_resumption_server_sni.phpt diff --git a/NEWS b/NEWS index 9a901f31dbb0..a024bae23063 100644 --- a/NEWS +++ b/NEWS @@ -89,6 +89,8 @@ PHP NEWS structures with persistent streams). (cxxz16, David Carlier) . Fixed an exception thrown by the session_get_cb stream context callback being replaced with a TypeError. (Ilia Alshanetsky) + . Fixed SNI_server_certs being ignored on a server stream that reuses the + SSL context of a session stream. (Jakub Zelenka) - PDO_Firebird: . Fixed stack buffer overflow with long connection user names, passwords, diff --git a/ext/openssl/tests/session_resumption_server_sni.phpt b/ext/openssl/tests/session_resumption_server_sni.phpt new file mode 100644 index 000000000000..4607d197e301 --- /dev/null +++ b/ext/openssl/tests/session_resumption_server_sni.phpt @@ -0,0 +1,78 @@ +--TEST-- +Server SNI certificate selection on a stream seeded from a session stream +--EXTENSIONS-- +openssl +--FILE-- +saveNewCertAsFileWithKey('cs.php.net', $csFile); +$certificateGenerator->saveNewCertAsFileWithKey('uk.php.net', $ukFile); +$certificateGenerator->saveNewCertAsFileWithKey('us.php.net', $usFile); + +$server = stream_socket_server('tcp://127.0.0.1:0'); +$addr = stream_socket_get_name($server, false); + +function handshake(string $addr, $server, string $peerName, $session, array $serverOptions = []): array +{ + $client = stream_socket_client("tcp://$addr", $errno, $errstr, 5, STREAM_CLIENT_CONNECT, + stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'capture_peer_cert' => true, + 'peer_name' => $peerName, + ]])); + $accepted = stream_socket_accept($server); + if ($serverOptions) { + stream_context_set_options($accepted, ['ssl' => $serverOptions]); + } + stream_set_blocking($client, false); + stream_set_blocking($accepted, false); + + $clientDone = $serverDone = 0; + for ($i = 0; $i < 1000 && ($clientDone !== true || $serverDone !== true); $i++) { + if ($clientDone !== true) { + $clientDone = stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_CLIENT); + } + if ($serverDone !== true) { + $serverDone = stream_socket_enable_crypto($accepted, true, STREAM_CRYPTO_METHOD_TLS_SERVER, $session); + } + usleep(1000); + } + + $cert = stream_context_get_options($client)['ssl']['peer_certificate']; + var_dump(openssl_x509_parse($cert)['subject']['CN']); + + return [$accepted, $client]; +} + +$parent = handshake($addr, $server, 'uk.php.net', null, [ + 'local_cert' => $csFile, + 'SNI_server_certs' => [ + 'cs.php.net' => $csFile, + 'uk.php.net' => $ukFile, + 'us.php.net' => $usFile, + ], +]); +$child = handshake($addr, $server, 'us.php.net', $parent[0]); + +fclose($parent[0]); +fclose($parent[1]); +unset($parent); + +$grandchild = handshake($addr, $server, 'uk.php.net', $child[0]); +?> +--CLEAN-- + +--EXPECT-- +string(10) "uk.php.net" +string(10) "us.php.net" +string(10) "uk.php.net" diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 60cbf78ae719..82d14ba5d8c9 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -1584,6 +1584,33 @@ static zend_result php_openssl_enable_server_sni( return SUCCESS; } +static void php_openssl_share_server_sni(php_stream *stream, php_openssl_netstream_data_t *sslsock, + const php_openssl_netstream_data_t *parent_sslsock) +{ + if (sslsock->sni_certs || !parent_sslsock->sni_certs || !parent_sslsock->sni_cert_count) { + return; + } + + bool persistent = php_stream_is_persistent(stream); + unsigned count = 0; + + sslsock->sni_certs = safe_pemalloc(parent_sslsock->sni_cert_count, + sizeof(php_openssl_sni_cert_t), 0, persistent); + + for (unsigned i = 0; i < parent_sslsock->sni_cert_count; i++) { + const php_openssl_sni_cert_t *parent_cert = &parent_sslsock->sni_certs[i]; + if (!parent_cert->ctx) { + continue; + } + SSL_CTX_up_ref(parent_cert->ctx); + sslsock->sni_certs[count].ctx = parent_cert->ctx; + sslsock->sni_certs[count].name = pestrdup(parent_cert->name, persistent); + count++; + } + + sslsock->sni_cert_count = count; +} + static void php_openssl_enable_client_sni(php_stream *stream, php_openssl_netstream_data_t *sslsock) /* {{{ */ { zval *val; @@ -2846,6 +2873,9 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, } SSL_set_ex_data(sslsock->ssl_handle, php_openssl_get_ssl_stream_data_index(), stream); +#ifdef HAVE_TLS_SNI + php_openssl_share_server_sni(stream, sslsock, parent_sslsock); +#endif if (!SSL_set_fd(sslsock->ssl_handle, sslsock->s.socket)) { php_openssl_handle_ssl_error(stream, 0, true);