From 1e9c65636521eee1cc0a4fa260a39b4e9ac07bc5 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 23 Oct 2025 23:49:59 +0200 Subject: [PATCH 1/7] Add OpenSSL TLS configurable session resumption support This adds support for verious session options to stream ssl context. It allows setting new session callback and session data on client and get and delete session callbacks to server. The server also offers options to configure session cache parameters and number of session tickets. --- .../session_resumption_cache_disabled.phpt | 86 ++++ .../session_resumption_client_basic.phpt | 91 ++++ .../session_resumption_get_cb_no_ticket.phpt | 75 ++++ ...esumption_get_cb_num_tickets_positive.phpt | 82 ++++ ...on_resumption_get_cb_num_tickets_zero.phpt | 106 +++++ .../session_resumption_invalid_callback.phpt | 60 +++ .../session_resumption_invalid_data.phpt | 64 +++ .../session_resumption_new_cb_no_context.phpt | 76 ++++ .../session_resumption_persistent_reject.phpt | 66 +++ .../session_resumption_require_new_cb.phpt | 71 +++ ...ption_server_external_with_context_id.phpt | 110 +++++ ...server_external_with_context_id_tls12.phpt | 110 +++++ ...mption_server_external_with_no_verify.phpt | 115 +++++ .../session_resumption_server_internal.phpt | 92 ++++ ext/openssl/xp_ssl.c | 423 ++++++++++++++++++ 15 files changed, 1627 insertions(+) create mode 100644 ext/openssl/tests/session_resumption_cache_disabled.phpt create mode 100644 ext/openssl/tests/session_resumption_client_basic.phpt create mode 100644 ext/openssl/tests/session_resumption_get_cb_no_ticket.phpt create mode 100644 ext/openssl/tests/session_resumption_get_cb_num_tickets_positive.phpt create mode 100644 ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt create mode 100644 ext/openssl/tests/session_resumption_invalid_callback.phpt create mode 100644 ext/openssl/tests/session_resumption_invalid_data.phpt create mode 100644 ext/openssl/tests/session_resumption_new_cb_no_context.phpt create mode 100644 ext/openssl/tests/session_resumption_persistent_reject.phpt create mode 100644 ext/openssl/tests/session_resumption_require_new_cb.phpt create mode 100644 ext/openssl/tests/session_resumption_server_external_with_context_id.phpt create mode 100644 ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt create mode 100644 ext/openssl/tests/session_resumption_server_external_with_no_verify.phpt create mode 100644 ext/openssl/tests/session_resumption_server_internal.phpt diff --git a/ext/openssl/tests/session_resumption_cache_disabled.phpt b/ext/openssl/tests/session_resumption_cache_disabled.phpt new file mode 100644 index 0000000000000..cc53863014476 --- /dev/null +++ b/ext/openssl/tests/session_resumption_cache_disabled.phpt @@ -0,0 +1,86 @@ +--TEST-- +TLS session resumption - server with cache disabled +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'session_cache' => false, /* Disable session caching */ + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + /* Accept two connections */ + for ($i = 0; $i < 2; $i++) { + $client = @stream_socket_accept($server, 30); + if ($client) { + fwrite($client, "No cache connection " . ($i + 1) . "\n"); + fclose($client); + } + } + + phpt_notify(message: "CACHE_DISABLED_TEST_DONE"); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $sessionData = null; + + $flags = STREAM_CLIENT_CONNECT; + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => function($stream, $sessionId, $data) use (&$sessionData) { + $sessionData = $data; + } + ]]); + + /* First connection */ + $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + if ($client1) { + echo trim(fgets($client1)) . "\n"; + fclose($client1); + } + + /* Second connection - server won't use cached session */ + $ctx2 = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => $sessionData, + ]]); + + $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2); + if ($client2) { + echo trim(fgets($client2)) . "\n"; + fclose($client2); + } + + $result = phpt_wait(); + echo trim($result) . "\n"; +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_disabled_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +No cache connection 1 +No cache connection 2 +CACHE_DISABLED_TEST_DONE diff --git a/ext/openssl/tests/session_resumption_client_basic.phpt b/ext/openssl/tests/session_resumption_client_basic.phpt new file mode 100644 index 0000000000000..63a29e15226bb --- /dev/null +++ b/ext/openssl/tests/session_resumption_client_basic.phpt @@ -0,0 +1,91 @@ +--TEST-- +TLS session resumption - client basic resumption +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + /* Accept two connections */ + for ($i = 0; $i < 2; $i++) { + $client = @stream_socket_accept($server, 30); + if ($client) { + fwrite($client, "Hello from server\n"); + fclose($client); + } + } +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $sessionData = ''; + $sessionReceived = false; + + $flags = STREAM_CLIENT_CONNECT; + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => function($stream, $sessionId, $sessionDataArg) use (&$sessionReceived, &$sessionData) { + $sessionData = $sessionDataArg; + $sessionReceived = true; + } + ]]); + + /* First connection - full handshake */ + $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + if ($client1) { + $response = fgets($client1); + echo "First connection: " . trim($response) . "\n"; + fclose($client1); + } + + var_dump($sessionReceived); + var_dump(strlen($sessionData) > 0); + + /* Second connection - resumed session */ + $ctx2 = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => $sessionData, + ]]); + + $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2); + if ($client2) { + $response = fgets($client2); + echo "Second connection: " . trim($response) . "\n"; + fclose($client2); + } + + echo "Session resumption test completed\n"; +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_resumption_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +First connection: Hello from server +bool(true) +bool(true) +Second connection: Hello from server +Session resumption test completed diff --git a/ext/openssl/tests/session_resumption_get_cb_no_ticket.phpt b/ext/openssl/tests/session_resumption_get_cb_no_ticket.phpt new file mode 100644 index 0000000000000..fe225b6bf3472 --- /dev/null +++ b/ext/openssl/tests/session_resumption_get_cb_no_ticket.phpt @@ -0,0 +1,75 @@ +--TEST-- +TLS session resumption - warning when trying to enable tickets with session_get_cb +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'session_context_id' => 'test-app', + 'no_ticket' => false, // Explicitly trying to enable tickets + 'session_new_cb' => function($stream, $sessionId, $sessionData) { + // Store session + }, + 'session_get_cb' => function($stream, $sessionId) { + return null; + } + ]]); + + $server = @stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + $client = @stream_socket_accept($server, 30); + if ($client === false) { + phpt_notify(message: "SERVER_FAILED_AS_EXPECTED"); + } else { + phpt_notify(message: "SERVER_CREATED_UNEXPECTEDLY"); + fclose($server); + } +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $flags = STREAM_CLIENT_CONNECT; + + /* Try to use corrupted session data */ + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => 'this_is_invalid_session_data', + ]]); + + $client = @stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + + if ($client === false) { + echo "Connection failed as expected\n"; + } + + $result = phpt_wait(); + echo trim($result) . "\n"; +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_no_ticket_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +Connection failed as expected +SERVER_FAILED_AS_EXPECTED diff --git a/ext/openssl/tests/session_resumption_get_cb_num_tickets_positive.phpt b/ext/openssl/tests/session_resumption_get_cb_num_tickets_positive.phpt new file mode 100644 index 0000000000000..148df74987ef0 --- /dev/null +++ b/ext/openssl/tests/session_resumption_get_cb_num_tickets_positive.phpt @@ -0,0 +1,82 @@ +--TEST-- +TLS session resumption - num_tickets controls ticket generation (TLS 1.3) +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_SERVER, + 'num_tickets' => 3, // Issue 3 tickets per connection + ]]); + + $server = stream_socket_server('tlsv1.3://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + // Accept one connection + $client = @stream_socket_accept($server, 30); + if ($client) { + fwrite($client, "Ticket test\n"); + // Keep connection open briefly to allow tickets to be sent + usleep(100000); // 100ms + fclose($client); + } + + phpt_notify(message: "SERVER_DONE"); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $ticketCount = 0; + + $flags = STREAM_CLIENT_CONNECT; + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT, + 'session_new_cb' => function($stream, $sessionId, $data) use (&$ticketCount) { + $ticketCount++; + } + ]]); + + $client = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + if ($client) { + $response = fgets($client); + echo trim($response) . "\n"; + + // Keep connection open briefly to receive all tickets + usleep(150000); // 150ms + fclose($client); + } + + echo "Tickets received: $ticketCount\n"; + + $result = phpt_wait(); + echo trim($result) . "\n"; +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_num_tickets_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +Ticket test +Tickets received: 3 +SERVER_DONE diff --git a/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt b/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt new file mode 100644 index 0000000000000..8c6a840758363 --- /dev/null +++ b/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt @@ -0,0 +1,106 @@ +--TEST-- +TLS session resumption - num_tickets = 0 disables tickets, forces session IDs +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'session_context_id' => 'test-no-tickets', + 'num_tickets' => 0, // Disable ticket issuance + 'session_new_cb' => function($stream, $sessionId, $sessionData) use (&$sessionStore, &$newCbCalled) { + $key = bin2hex($sessionId); + $sessionStore[$key] = $sessionData; + $newCbCalled++; + }, + 'session_get_cb' => function($stream, $sessionId) use (&$sessionStore) { + $key = bin2hex($sessionId); + return $sessionStore[$key] ?? null; + }, + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + // Accept two connections + for ($i = 0; $i < 2; $i++) { + $client = @stream_socket_accept($server, 30); + if ($client) { + fwrite($client, "Response " . ($i + 1) . "\n"); + usleep(50000); // Allow session storage + fclose($client); + } + } + + phpt_notify(message: "NEW_CB_CALLS:$newCbCalled"); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $sessionData = null; + $clientTickets = 0; + + $flags = STREAM_CLIENT_CONNECT; + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => function($stream, $sessionId, $data) use (&$sessionData, &$clientTickets) { + $sessionData = $data; + $clientTickets++; + } + ]]); + + // First connection + $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + if ($client1) { + echo trim(fgets($client1)) . "\n"; + usleep(100000); // Wait for session storage + fclose($client1); + } + + echo "Client received tickets on first connection: $clientTickets\n"; + + // Second connection with resumption + $ctx2 = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => $sessionData, + ]]); + + $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2); + if ($client2) { + echo trim(fgets($client2)) . "\n"; + fclose($client2); + } + + $result = phpt_wait(); + echo "Server: " . trim($result) . "\n"; +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_no_tickets_zero_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +Response 1 +Client received tickets on first connection: 0 +Response 2 +Server: NEW_CB_CALLS:0 diff --git a/ext/openssl/tests/session_resumption_invalid_callback.phpt b/ext/openssl/tests/session_resumption_invalid_callback.phpt new file mode 100644 index 0000000000000..b6cfc90a0554c --- /dev/null +++ b/ext/openssl/tests/session_resumption_invalid_callback.phpt @@ -0,0 +1,60 @@ +--TEST-- +TLS session resumption - invalid callback throws TypeError +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + $client = @stream_socket_accept($server, 30); + if ($client) { + fclose($client); + } +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $flags = STREAM_CLIENT_CONNECT; + + /* Try to use invalid callback */ + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => 'not_a_valid_function', + ]]); + + try { + $client = @stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + echo "Should not reach here\n"; + } catch (TypeError $e) { + echo "TypeError caught: " . (strpos($e->getMessage(), 'session_new_cb must be a valid callback') !== false ? "YES" : "NO"); + echo "\n"; + } +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_invalid_cb_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +TypeError caught: YES diff --git a/ext/openssl/tests/session_resumption_invalid_data.phpt b/ext/openssl/tests/session_resumption_invalid_data.phpt new file mode 100644 index 0000000000000..1c1d23df13a9f --- /dev/null +++ b/ext/openssl/tests/session_resumption_invalid_data.phpt @@ -0,0 +1,64 @@ +--TEST-- +TLS session resumption - invalid session data is fatal +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + $client = @stream_socket_accept($server, 30); + if ($client) { + fclose($client); + } +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $flags = STREAM_CLIENT_CONNECT; + + /* Try to use corrupted session data */ + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => 'this_is_invalid_session_data', + ]]); + + $client = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + + if ($client === false) { + echo "Connection failed as expected\n"; + } +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_invalid_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- + +Warning: stream_socket_client(): Invalid or corrupted session_data, falling back to full handshake in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to %s in %s on line %d +Connection failed as expected diff --git a/ext/openssl/tests/session_resumption_new_cb_no_context.phpt b/ext/openssl/tests/session_resumption_new_cb_no_context.phpt new file mode 100644 index 0000000000000..a1bc078960009 --- /dev/null +++ b/ext/openssl/tests/session_resumption_new_cb_no_context.phpt @@ -0,0 +1,76 @@ +--TEST-- +TLS session resumption - warning when session_new_cb without session_context_id and verify_peer enabled +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'verify_peer' => true, + 'cafile' => '%s', + 'session_new_cb' => function($stream, $sessionId, $sessionData) { + echo "Callback might not be called\n"; + } + /* Missing: 'session_context_id' => 'myapp' */ + ]]); + + $server = @stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + $client = @stream_socket_accept($server, 30); + if ($client === false) { + phpt_notify(message: "SERVER_FAILED_AS_EXPECTED"); + } else { + phpt_notify(message: "SERVER_CREATED_UNEXPECTEDLY"); + fclose($server); + } +CODE; +$serverCode = sprintf($serverCode, $certFile, $caCertFile); + +$clientCode = <<<'CODE' + $flags = STREAM_CLIENT_CONNECT; + + /* Try to use corrupted session data */ + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => 'this_is_invalid_session_data', + ]]); + + $client = @stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + + if ($client === false) { + echo "Connection failed as expected\n"; + } + + $result = phpt_wait(); + echo trim($result) . "\n"; +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveCaCert($caCertFile); +$certificateGenerator->saveNewCertAsFileWithKey('session_verify_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +Connection failed as expected +SERVER_FAILED_AS_EXPECTED diff --git a/ext/openssl/tests/session_resumption_persistent_reject.phpt b/ext/openssl/tests/session_resumption_persistent_reject.phpt new file mode 100644 index 0000000000000..f87b14e1776db --- /dev/null +++ b/ext/openssl/tests/session_resumption_persistent_reject.phpt @@ -0,0 +1,66 @@ +--TEST-- +TLS session resumption - callbacks rejected on persistent streams +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + $client = @stream_socket_accept($server, 30); + if ($client) { + fclose($client); + } +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT; + + /* Try to use callback with persistent stream */ + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => function($stream, $sessionId, $sessionData) { + echo "This should never be called\n"; + } + ]]); + + $client = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + + if ($client === false) { + echo "Connection failed as expected with persistent stream\n"; + } +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_persistent_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- + +Warning: stream_socket_client(): session_new_cb is not supported for persistent streams in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to %s in %s on line %d +Connection failed as expected with persistent stream diff --git a/ext/openssl/tests/session_resumption_require_new_cb.phpt b/ext/openssl/tests/session_resumption_require_new_cb.phpt new file mode 100644 index 0000000000000..0628d9e69ab44 --- /dev/null +++ b/ext/openssl/tests/session_resumption_require_new_cb.phpt @@ -0,0 +1,71 @@ +--TEST-- +TLS session resumption - server requires session_new_cb with session_get_cb +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'session_get_cb' => function($stream, $sessionId) { + return null; + } + ]]); + + $server = @stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + $client = @stream_socket_accept($server, 30); + + if ($client === false) { + phpt_notify(message: "SERVER_FAILED_AS_EXPECTED"); + } else { + phpt_notify(message: "SERVER_CREATED_UNEXPECTEDLY"); + fclose($server); + } +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $flags = STREAM_CLIENT_CONNECT; + + /* Try to use corrupted session data */ + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => 'this_is_invalid_session_data', + ]]); + + $client = @stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + + if ($client === false) { + echo "Connection failed as expected\n"; + } + + $result = phpt_wait(); + echo trim($result) . "\n"; +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_require_cb_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +Connection failed as expected +SERVER_FAILED_AS_EXPECTED diff --git a/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt b/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt new file mode 100644 index 0000000000000..6979c4cd7583c --- /dev/null +++ b/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt @@ -0,0 +1,110 @@ +--TEST-- +TLS session resumption - server external cache callbacks with context id +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'session_context_id' => 'test-server', // Proper configuration + 'session_new_cb' => function($stream, $sessionId, $sessionData) use (&$sessionStore, &$newCbCalled) { + $key = bin2hex($sessionId); + $sessionStore[$key] = $sessionData; + $newCbCalled = true; + }, + 'session_get_cb' => function($stream, $sessionId) use (&$sessionStore, &$getCbCalled) { + $key = bin2hex($sessionId); + $getCbCalled = true; + return $sessionStore[$key] ?? null; + }, + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + /* Accept two connections */ + for ($i = 0; $i < 2; $i++) { + $client = @stream_socket_accept($server, 30); + if ($client) { + fwrite($client, "Response " . ($i + 1) . "\n"); + fclose($client); + } + } + + /* Report results */ + $result = []; + if ($newCbCalled) $result[] = "NEW_CB_CALLED"; + if ($getCbCalled) $result[] = "GET_CB_CALLED"; + $result[] = "SESSIONS:" . count($sessionStore); + + phpt_notify(message: implode(",", $result)); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $sessionData = null; + + $flags = STREAM_CLIENT_CONNECT; + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => function($stream, $sessionId, $data) use (&$sessionData) { + $sessionData = $data; + } + ]]); + + /* First connection */ + $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + if ($client1) { + echo trim(fgets($client1)) . "\n"; + fclose($client1); + } + + echo "Session captured: " . ($sessionData !== null ? "YES" : "NO") . "\n"; + + /* Second connection with session resumption */ + $ctx2 = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => $sessionData, + ]]); + + $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2); + if ($client2) { + echo trim(fgets($client2)) . "\n"; + fclose($client2); + } + + /* Get server callback results */ + $result = phpt_wait(); + echo "Server: " . trim($result) . "\n"; +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_external_proper_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +Response 1 +Session captured: YES +Response 2 +Server: NEW_CB_CALLED,GET_CB_CALLED,SESSIONS:3 diff --git a/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt b/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt new file mode 100644 index 0000000000000..f0e8cc153f71b --- /dev/null +++ b/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt @@ -0,0 +1,110 @@ +--TEST-- +TLS session resumption - server external cache callbacks with context id for TLS 1.2 +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'session_context_id' => 'test-server', // Proper configuration + 'session_new_cb' => function($stream, $sessionId, $sessionData) use (&$sessionStore, &$newCbCalled) { + $key = bin2hex($sessionId); + $sessionStore[$key] = $sessionData; + $newCbCalled = true; + }, + 'session_get_cb' => function($stream, $sessionId) use (&$sessionStore, &$getCbCalled) { + $key = bin2hex($sessionId); + $getCbCalled = true; + return $sessionStore[$key] ?? null; + }, + ]]); + + $server = stream_socket_server('tlsv1.2://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + /* Accept two connections */ + for ($i = 0; $i < 2; $i++) { + $client = @stream_socket_accept($server, 30); + if ($client) { + fwrite($client, "Response " . ($i + 1) . "\n"); + fclose($client); + } + } + + /* Report results */ + $result = []; + if ($newCbCalled) $result[] = "NEW_CB_CALLED"; + if ($getCbCalled) $result[] = "GET_CB_CALLED"; + $result[] = "SESSIONS:" . count($sessionStore); + + phpt_notify(message: implode(",", $result)); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $sessionData = null; + + $flags = STREAM_CLIENT_CONNECT; + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => function($stream, $sessionId, $data) use (&$sessionData) { + $sessionData = $data; + } + ]]); + + /* First connection */ + $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + if ($client1) { + echo trim(fgets($client1)) . "\n"; + fclose($client1); + } + + echo "Session captured: " . ($sessionData !== null ? "YES" : "NO") . "\n"; + + /* Second connection with session resumption */ + $ctx2 = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => $sessionData, + ]]); + + $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2); + if ($client2) { + echo trim(fgets($client2)) . "\n"; + fclose($client2); + } + + /* Get server callback results */ + $result = phpt_wait(); + echo "Server: " . trim($result) . "\n"; +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_external_proper_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +Response 1 +Session captured: YES +Response 2 +Server: NEW_CB_CALLED,GET_CB_CALLED,SESSIONS:1 diff --git a/ext/openssl/tests/session_resumption_server_external_with_no_verify.phpt b/ext/openssl/tests/session_resumption_server_external_with_no_verify.phpt new file mode 100644 index 0000000000000..ef0cc70c1e96b --- /dev/null +++ b/ext/openssl/tests/session_resumption_server_external_with_no_verify.phpt @@ -0,0 +1,115 @@ +--TEST-- +TLS session resumption - server external cache callbacks with no verify +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'verify_peer' => false, + 'no_ticket' => true, + 'session_cache' => true, + 'session_new_cb' => function($stream, $sessionId, $sessionData) use (&$sessionStore, &$newCbCalled) { + $key = bin2hex($sessionId); + $sessionStore[$key] = $sessionData; + $newCbCalled = true; + }, + 'session_get_cb' => function($stream, $sessionId) use (&$sessionStore, &$getCbCalled) { + $key = bin2hex($sessionId); + $getCbCalled = true; + return $sessionStore[$key] ?? null; + }, + 'session_remove_cb' => function($stream, $sessionId) use (&$sessionStore, &$removeCbCalled) { + $key = bin2hex($sessionId); + unset($sessionStore[$key]); + $removeCbCalled = true; + } + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + /* Accept two connections */ + for ($i = 0; $i < 2; $i++) { + $client = @stream_socket_accept($server, 30); + if ($client) { + fwrite($client, "Response " . ($i + 1) . "\n"); + fclose($client); + } + } + + /* Notify client about callback invocations */ + $result = []; + if ($newCbCalled) $result[] = "NEW_CB_CALLED"; + if ($getCbCalled) $result[] = "GET_CB_CALLED"; + if ($removeCbCalled) $result[] = "REMOVE_CB_CALLED"; + + phpt_notify(message: implode(",", $result)); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $sessionData = null; + + $flags = STREAM_CLIENT_CONNECT; + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => function($stream, $sessionId, $data) use (&$sessionData) { + $sessionData = $data; + } + ]]); + + /* First connection */ + $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + if ($client1) { + echo trim(fgets($client1)) . "\n"; + fclose($client1); + } + + /* Second connection with session resumption */ + $ctx2 = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => $sessionData, + ]]); + + $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2); + if ($client2) { + echo trim(fgets($client2)) . "\n"; + fclose($client2); + } + + /* Get server callback results */ + $result = phpt_wait(); + echo "Server callbacks: " . trim($result) . "\n"; +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_server_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +Response 1 +Response 2 +Server callbacks: NEW_CB_CALLED,GET_CB_CALLED diff --git a/ext/openssl/tests/session_resumption_server_internal.phpt b/ext/openssl/tests/session_resumption_server_internal.phpt new file mode 100644 index 0000000000000..ed3bb006ec830 --- /dev/null +++ b/ext/openssl/tests/session_resumption_server_internal.phpt @@ -0,0 +1,92 @@ +--TEST-- +TLS session resumption - server internal cache +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'session_cache' => true, + 'session_cache_size' => 1024, + 'session_timeout' => 300, + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + /* Accept two connections */ + for ($i = 0; $i < 2; $i++) { + $client = @stream_socket_accept($server, 30); + if ($client) { + fwrite($client, "Connection " . ($i + 1) . "\n"); + fclose($client); + } + } + + phpt_notify(message: "SERVER_DONE"); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $sessionData = null; + + $flags = STREAM_CLIENT_CONNECT; + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => function($stream, $sessionId, $data) use (&$sessionData) { + $sessionData = $data; + } + ]]); + + /* First connection */ + $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + if ($client1) { + echo trim(fgets($client1)) . "\n"; + fclose($client1); + } + + echo "Session data received: " . (strlen($sessionData) > 0 ? "YES" : "NO") . "\n"; + + /* Second connection with session resumption */ + $ctx2 = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => $sessionData, + ]]); + + $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2); + if ($client2) { + echo trim(fgets($client2)) . "\n"; + fclose($client2); + } + + /* Wait for server */ + $result = phpt_wait(); + echo trim($result) . "\n"; +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_internal_cache_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +Connection 1 +Session data received: YES +Connection 2 +SERVER_DONE diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index bd174f30095c6..6009f58b16453 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -182,6 +182,13 @@ typedef struct _php_openssl_alpn_ctx_t { } php_openssl_alpn_ctx; #endif +/* Holds session callback */ +typedef struct _php_openssl_session_callbacks_t { + zval new_cb; // Callback for new sessions + zval get_cb; // Callback to retrieve sessions (server only) + zval remove_cb; // Callback when session removed (server only) +} php_openssl_session_callbacks_t; + /* This implementation is very closely tied to the that of the native * sockets implemented in the core. * Don't try this technique in other extensions! @@ -201,6 +208,7 @@ typedef struct _php_openssl_netstream_data_t { #ifdef HAVE_TLS_ALPN php_openssl_alpn_ctx alpn_ctx; #endif + php_openssl_session_callbacks_t *session_callbacks; char *url_name; unsigned state_set:1; unsigned _spare:31; @@ -1545,6 +1553,381 @@ static int php_openssl_server_alpn_callback(SSL *ssl_handle, #endif +static int php_openssl_get_ctx_stream_data_index(void) +{ + static int ctx_data_index = -1; + if (ctx_data_index < 0) { + ctx_data_index = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); + } + return ctx_data_index; +} + +/** + * OpenSSL new session callback - called when a new session is established + */ +static int php_openssl_session_new_cb(SSL *ssl, SSL_SESSION *session) +{ + php_stream *stream = (php_stream *)SSL_get_ex_data(ssl, php_openssl_get_ssl_stream_data_index()); + if (!stream) { + return 0; + } + + php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t *)stream->abstract; + if (!sslsock || !sslsock->session_callbacks) { + return 0; + } + + /* Serialize session to DER format */ + int session_len = i2d_SSL_SESSION(session, NULL); + if (session_len <= 0) { + return 0; + } + + unsigned char *session_data = emalloc(session_len); + unsigned char *p = session_data; + i2d_SSL_SESSION(session, &p); + + unsigned int session_id_len = 0; + const unsigned char *session_id = SSL_SESSION_get_id(session, &session_id_len); + + zval args[3]; + zval retval; + + ZVAL_RES(&args[0], stream->res); + ZVAL_STRINGL(&args[1], (char *)session_id, session_id_len); + ZVAL_STRINGL(&args[2], (char *)session_data, session_len); + + if (call_user_function(EG(function_table), NULL, &sslsock->session_callbacks->new_cb, + &retval, 3, args) == SUCCESS) { + zval_ptr_dtor(&retval); + } + + zval_ptr_dtor(&args[1]); + zval_ptr_dtor(&args[2]); + efree(session_data); + + return 0; +} + +/** + * OpenSSL get session callback - called when server needs to retrieve a session + */ +static SSL_SESSION *php_openssl_session_get_cb(SSL *ssl, const unsigned char *session_id, + int session_id_len, int *copy) +{ + php_stream *stream = (php_stream *)SSL_get_ex_data(ssl, php_openssl_get_ssl_stream_data_index()); + if (!stream) { + *copy = 0; + return NULL; + } + + php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t *)stream->abstract; + if (!sslsock || !sslsock->session_callbacks) { + *copy = 0; + return NULL; + } + + zval args[2]; + zval retval; + + ZVAL_RES(&args[0], stream->res); + ZVAL_STRINGL(&args[1], (char *)session_id, session_id_len); + + SSL_SESSION *session = NULL; + + if (call_user_function(EG(function_table), NULL, &sslsock->session_callbacks->get_cb, + &retval, 2, args) == SUCCESS) { + if (Z_TYPE(retval) == IS_STRING && Z_STRLEN(retval) > 0) { + const unsigned char *p = (const unsigned char *)Z_STRVAL(retval); + session = d2i_SSL_SESSION(NULL, &p, Z_STRLEN(retval)); + } + zval_ptr_dtor(&retval); + } + + zval_ptr_dtor(&args[1]); + + *copy = 0; /* We return a new reference, OpenSSL will own it */ + return session; +} + +/** + * OpenSSL remove session callback - called when a session is evicted from cache + */ +static void php_openssl_session_remove_cb(SSL_CTX *ctx, SSL_SESSION *session) +{ + php_stream *stream = (php_stream *)SSL_CTX_get_ex_data(ctx, php_openssl_get_ctx_stream_data_index()); + if (!stream) { + return; + } + + php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t *)stream->abstract; + if (!sslsock || !sslsock->session_callbacks) { + return; + } + + unsigned int session_id_len = 0; + const unsigned char *session_id = SSL_SESSION_get_id(session, &session_id_len); + + zval args[2]; + zval retval; + + ZVAL_RES(&args[0], stream->res); + ZVAL_STRINGL(&args[1], (char *)session_id, session_id_len); + + if (call_user_function(EG(function_table), NULL, &sslsock->session_callbacks->remove_cb, + &retval, 2, args) == SUCCESS) { + zval_ptr_dtor(&retval); + } + + zval_ptr_dtor(&args[1]); +} + +/** + * Validate callable and allocate callback structure if needed. + */ +static zend_result php_openssl_validate_and_allocate_callback( + php_openssl_netstream_data_t *sslsock, zval *callable, + const char *callback_name, bool is_persistent) +{ + zend_fcall_info_cache fcc; + char *is_callable_error = NULL; + + /* Callbacks not supported for persistent streams */ + if (is_persistent) { + php_error_docref(NULL, E_WARNING, + "%s is not supported for persistent streams", callback_name); + return FAILURE; + } + + /* Validate callable */ + if (!zend_is_callable_ex(callable, NULL, 0, NULL, &fcc, &is_callable_error)) { + if (is_callable_error) { + zend_type_error("%s must be a valid callback, %s", callback_name, is_callable_error); + efree(is_callable_error); + } else { + zend_type_error("%s must be a valid callback", callback_name); + } + return FAILURE; + } + + /* Allocate callback structure if not already allocated */ + if (!sslsock->session_callbacks) { + sslsock->session_callbacks = (php_openssl_session_callbacks_t *)pemalloc( + sizeof(php_openssl_session_callbacks_t), is_persistent); + ZVAL_UNDEF(&sslsock->session_callbacks->new_cb); + ZVAL_UNDEF(&sslsock->session_callbacks->get_cb); + ZVAL_UNDEF(&sslsock->session_callbacks->remove_cb); + } + + return SUCCESS; +} + +/** + * Configure session resumption options for client connections + */ +static zend_result php_openssl_setup_client_session(php_stream *stream, + php_openssl_netstream_data_t *sslsock) +{ + zval *val; + bool enable_client_cache = false; + bool is_persistent = php_stream_is_persistent(stream); + + if (GET_VER_OPT("session_new_cb")) { + if (FAILURE == php_openssl_validate_and_allocate_callback( + sslsock, val, "session_new_cb", is_persistent)) { + return FAILURE; + } + + ZVAL_COPY(&sslsock->session_callbacks->new_cb, val); + SSL_CTX_sess_set_new_cb(sslsock->ctx, php_openssl_session_new_cb); + enable_client_cache = true; + } + + /* Handle session_data - must be done after SSL_new() */ + if (GET_VER_OPT("session_data") && Z_TYPE_P(val) == IS_STRING && Z_STRLEN_P(val) > 0) { + /* It just needs to be enabled as it will be applied after SSL handle is created */ + enable_client_cache = true; + } + + if (enable_client_cache) { + SSL_CTX_set_session_cache_mode(sslsock->ctx, + SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL); + } + + return SUCCESS; +} + +/** + * Configure session resumption options for server connections + */ +static zend_result php_openssl_setup_server_session(php_stream *stream, + php_openssl_netstream_data_t *sslsock) +{ + zval *val; + bool has_get_cb = false; + bool has_new_cb = false; + bool has_remove_cb = false; + bool has_session_context_id = false; + bool is_persistent = php_stream_is_persistent(stream); + + /* Check for session_get_cb first (determines cache mode) */ + if (GET_VER_OPT("session_get_cb")) { + if (FAILURE == php_openssl_validate_and_allocate_callback( + sslsock, val, "session_new_cb", is_persistent)) { + return FAILURE; + } + ZVAL_COPY(&sslsock->session_callbacks->get_cb, val); + has_get_cb = true; + } + + if (GET_VER_OPT("session_context_id")) { + if (Z_TYPE_P(val) != IS_STRING || Z_STRLEN_P(val) == 0) { + zend_type_error("session_context_id must be a non empty string"); + return FAILURE; + } + SSL_CTX_set_session_id_context(sslsock->ctx, (const unsigned char *) Z_STRVAL_P(val), + Z_STRLEN_P(val)); + has_session_context_id = true; + } + + /* Check for session_new_cb */ + if (GET_VER_OPT("session_new_cb")) { + if (FAILURE == php_openssl_validate_and_allocate_callback( + sslsock, val, "session_new_cb", is_persistent)) { + return FAILURE; + } + ZVAL_COPY(&sslsock->session_callbacks->new_cb, val); + has_new_cb = true; + + if (!has_session_context_id && + (SSL_CTX_get_verify_mode(sslsock->ctx) & SSL_VERIFY_PEER) != 0) { + php_error_docref(NULL, E_WARNING, + "session_new_cb is ignored as no session_context_id is set and verify_peer is enabled"); + } + } + + /* Validate: if session_get_cb is provided, session_new_cb is required */ + if (has_get_cb && !has_new_cb) { + php_error_docref(NULL, E_WARNING, + "session_new_cb is required when session_get_cb is provided"); + return FAILURE; + } + + /* Check for session_remove_cb (optional) */ + if (GET_VER_OPT("session_remove_cb")) { + if (FAILURE == php_openssl_validate_and_allocate_callback( + sslsock, val, "session_remove_cb", is_persistent)) { + return FAILURE; + } + + ZVAL_COPY(&sslsock->session_callbacks->remove_cb, val); + has_remove_cb = true; + } + + /* Configure cache mode based on whether external callbacks are provided */ + if (has_get_cb) { + /* External cache mode - disable internal cache */ + SSL_CTX_set_session_cache_mode(sslsock->ctx, + SSL_SESS_CACHE_SERVER | SSL_SESS_CACHE_NO_INTERNAL); + + /* Set callbacks */ + SSL_CTX_sess_set_new_cb(sslsock->ctx, php_openssl_session_new_cb); + SSL_CTX_sess_set_get_cb(sslsock->ctx, php_openssl_session_get_cb); + + if (has_remove_cb) { + SSL_CTX_sess_set_remove_cb(sslsock->ctx, php_openssl_session_remove_cb); + } + + // Disable tickets (they won't work anyway) and warn if explicity enabled + SSL_CTX_set_options(sslsock->ctx, SSL_OP_NO_TICKET); + if (GET_VER_OPT("no_ticket") && !zend_is_true(val)) { + php_error_docref(NULL, E_WARNING, + "Session tickets cannot be enabled when session_get_cb is set"); + } + } else { + /* Internal cache mode (default) */ + + /* Handle session_cache option */ + bool session_cache_enabled = true; + if (GET_VER_OPT("session_cache")) { + session_cache_enabled = zend_is_true(val); + } + + if (session_cache_enabled) { + SSL_CTX_set_session_cache_mode(sslsock->ctx, SSL_SESS_CACHE_SERVER); + + /* Handle session_cache_size */ + if (GET_VER_OPT("session_cache_size")) { + zend_long cache_size = zval_get_long(val); + if (cache_size > 0) { + SSL_CTX_sess_set_cache_size(sslsock->ctx, cache_size); + } else { + php_error_docref(NULL, E_WARNING, "session_cache_size must be positive"); + } + } else { + /* Default cache size from RFC */ + SSL_CTX_sess_set_cache_size(sslsock->ctx, 20480); + } + + /* Handle session_timeout */ + if (GET_VER_OPT("session_timeout")) { + zend_long timeout = zval_get_long(val); + if (timeout > 0) { + SSL_CTX_set_timeout(sslsock->ctx, timeout); + } else { + php_error_docref(NULL, E_WARNING, "session_timeout must be positive"); + } + } else { + /* Default timeout from RFC */ + SSL_CTX_set_timeout(sslsock->ctx, 300); + } + + /* Optional notification callback for internal cache */ + if (has_new_cb) { + SSL_CTX_sess_set_new_cb(sslsock->ctx, php_openssl_session_new_cb); + } + } else { + /* Session caching disabled */ + SSL_CTX_set_session_cache_mode(sslsock->ctx, SSL_SESS_CACHE_OFF); + } + } + + return SUCCESS; +} + +static zend_result php_openssl_apply_client_session_data(php_stream *stream, + php_openssl_netstream_data_t *sslsock) +{ + zval *val; + + if (GET_VER_OPT("session_data")) { + if (Z_TYPE_P(val) == IS_STRING && Z_STRLEN_P(val) > 0) { + /* Deserialize session from DER format */ + const unsigned char *p = (const unsigned char *)Z_STRVAL_P(val); + SSL_SESSION *session = d2i_SSL_SESSION(NULL, &p, Z_STRLEN_P(val)); + + if (session == NULL) { + php_error_docref(NULL, E_WARNING, + "Invalid or corrupted session_data, falling back to full handshake"); + ERR_clear_error(); + return FAILURE; + } + + if (SSL_set_session(sslsock->ssl_handle, session) != 1) { + php_error_docref(NULL, E_WARNING, + "Failed to set session for resumption, falling back to full handshake"); + SSL_SESSION_free(session); + ERR_clear_error(); + return FAILURE; + } + + SSL_SESSION_free(session); + } + } + + return SUCCESS; +} + static zend_result php_openssl_setup_crypto(php_stream *stream, php_openssl_netstream_data_t *sslsock, php_stream_xport_crypto_param *cparam) /* {{{ */ @@ -1568,6 +1951,8 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, } } + sslsock->session_callbacks = NULL; + ERR_clear_error(); /* We need to do slightly different things based on client/server method @@ -1583,6 +1968,8 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, return FAILURE; } + SSL_CTX_set_ex_data(sslsock->ctx, php_openssl_get_ctx_stream_data_index(), stream); + GET_VER_OPT_LONG("min_proto_version", min_version); GET_VER_OPT_LONG("max_proto_version", max_version); method_flags = php_openssl_get_proto_version_flags(method_flags, min_version, max_version); @@ -1591,6 +1978,12 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, if (GET_VER_OPT("no_ticket") && zend_is_true(val)) { ssl_ctx_options |= SSL_OP_NO_TICKET; } + if (GET_VER_OPT("num_tickets")) { + zend_long num_tickets = zval_get_long(val); + if (num_tickets >= 0) { + SSL_CTX_set_num_tickets(sslsock->ctx, num_tickets); + } + } ssl_ctx_options &= ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS; @@ -1685,6 +2078,24 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, return FAILURE; } + if (sslsock->is_client) { + /* Setup client session resumption */ + if (FAILURE == php_openssl_setup_client_session(stream, sslsock)) { + return FAILURE; + } + } else { + /* Setup server session resumption */ + if (PHP_STREAM_CONTEXT(stream)) { + if (FAILURE == php_openssl_setup_server_session(stream, sslsock)) { + return FAILURE; + } + } + /* Original server-specific setup */ + if (FAILURE == php_openssl_set_server_specific_opts(stream, sslsock->ctx)) { + return FAILURE; + } + } + sslsock->ssl_handle = SSL_new(sslsock->ctx); if (sslsock->ssl_handle == NULL) { @@ -1706,6 +2117,11 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, php_openssl_handle_ssl_error(stream, 0, true); } + /* Set session data for client */ + if (sslsock->is_client && php_openssl_apply_client_session_data(stream, sslsock)) { + return FAILURE; + } + #ifdef HAVE_TLS_SNI /* Enable server-side SNI */ if (!sslsock->is_client && php_openssl_enable_server_sni(stream, sslsock, verify_peer) == FAILURE) { @@ -2200,6 +2616,13 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle) /* {{{ pefree(sslsock->reneg, php_stream_is_persistent(stream)); } + if (sslsock->session_callbacks) { + zval_ptr_dtor(&sslsock->session_callbacks->new_cb); + zval_ptr_dtor(&sslsock->session_callbacks->get_cb); + zval_ptr_dtor(&sslsock->session_callbacks->remove_cb); + pefree(sslsock->session_callbacks, php_stream_is_persistent(stream)); + } + pefree(sslsock, php_stream_is_persistent(stream)); return 0; From 5441629d8894c3c14a13018a3e65952dd9ec38a5 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 29 Jan 2026 23:26:23 +0100 Subject: [PATCH 2/7] Add session_reused stream meta and use in session tests --- .../session_resumption_cache_disabled.phpt | 14 ++++---- .../session_resumption_client_basic.phpt | 33 +++++++++---------- ...on_resumption_get_cb_num_tickets_zero.phpt | 5 +++ ...ption_server_external_with_context_id.phpt | 6 ++++ ...server_external_with_context_id_tls12.phpt | 6 ++++ ...mption_server_external_with_no_verify.phpt | 6 ++++ .../session_resumption_server_internal.phpt | 6 ++++ ext/openssl/xp_ssl.c | 1 + 8 files changed, 52 insertions(+), 25 deletions(-) diff --git a/ext/openssl/tests/session_resumption_cache_disabled.phpt b/ext/openssl/tests/session_resumption_cache_disabled.phpt index cc53863014476..749afa72b09de 100644 --- a/ext/openssl/tests/session_resumption_cache_disabled.phpt +++ b/ext/openssl/tests/session_resumption_cache_disabled.phpt @@ -28,8 +28,6 @@ $serverCode = <<<'CODE' fclose($client); } } - - phpt_notify(message: "CACHE_DISABLED_TEST_DONE"); CODE; $serverCode = sprintf($serverCode, $certFile); @@ -49,6 +47,8 @@ $clientCode = <<<'CODE' $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); if ($client1) { echo trim(fgets($client1)) . "\n"; + $meta1 = stream_get_meta_data($client1); + echo "First connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n"; fclose($client1); } @@ -62,11 +62,10 @@ $clientCode = <<<'CODE' $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2); if ($client2) { echo trim(fgets($client2)) . "\n"; + $meta2 = stream_get_meta_data($client2); + echo "Second connection resumed: " . ($meta2['crypto']['session_reused'] ? "yes" : "no") . "\n"; fclose($client2); } - - $result = phpt_wait(); - echo trim($result) . "\n"; CODE; include 'CertificateGenerator.inc'; @@ -80,7 +79,8 @@ ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ---EXPECTF-- +--EXPECT-- No cache connection 1 +First connection resumed: no No cache connection 2 -CACHE_DISABLED_TEST_DONE +Second connection resumed: no diff --git a/ext/openssl/tests/session_resumption_client_basic.phpt b/ext/openssl/tests/session_resumption_client_basic.phpt index 63a29e15226bb..3cbe32ab16dcd 100644 --- a/ext/openssl/tests/session_resumption_client_basic.phpt +++ b/ext/openssl/tests/session_resumption_client_basic.phpt @@ -14,6 +14,7 @@ $serverCode = <<<'CODE' $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN; $ctx = stream_context_create(['ssl' => [ 'local_cert' => '%s', + 'session_cache' => true, ]]); $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); @@ -32,29 +33,26 @@ $serverCode = sprintf($serverCode, $certFile); $clientCode = <<<'CODE' $sessionData = ''; - $sessionReceived = false; $flags = STREAM_CLIENT_CONNECT; $ctx = stream_context_create(['ssl' => [ 'verify_peer' => false, 'verify_peer_name' => false, - 'session_new_cb' => function($stream, $sessionId, $sessionDataArg) use (&$sessionReceived, &$sessionData) { + 'session_new_cb' => function($stream, $sessionId, $sessionDataArg) use (&$sessionData) { $sessionData = $sessionDataArg; - $sessionReceived = true; } ]]); /* First connection - full handshake */ $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); if ($client1) { - $response = fgets($client1); - echo "First connection: " . trim($response) . "\n"; + echo trim(fgets($client1)) . "\n"; + $meta1 = stream_get_meta_data($client1); + echo "First connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n"; + echo "Session data received: " . (strlen($sessionData) > 0 ? "yes" : "no") . "\n"; fclose($client1); } - var_dump($sessionReceived); - var_dump(strlen($sessionData) > 0); - /* Second connection - resumed session */ $ctx2 = stream_context_create(['ssl' => [ 'verify_peer' => false, @@ -64,12 +62,11 @@ $clientCode = <<<'CODE' $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2); if ($client2) { - $response = fgets($client2); - echo "Second connection: " . trim($response) . "\n"; + echo trim(fgets($client2)) . "\n"; + $meta2 = stream_get_meta_data($client2); + echo "Second connection resumed: " . ($meta2['crypto']['session_reused'] ? "yes" : "no") . "\n"; fclose($client2); } - - echo "Session resumption test completed\n"; CODE; include 'CertificateGenerator.inc'; @@ -83,9 +80,9 @@ ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ---EXPECTF-- -First connection: Hello from server -bool(true) -bool(true) -Second connection: Hello from server -Session resumption test completed +--EXPECT-- +Hello from server +First connection resumed: no +Session data received: yes +Hello from server +Second connection resumed: yes diff --git a/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt b/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt index 8c6a840758363..dd9c4906a581e 100644 --- a/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt +++ b/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt @@ -64,6 +64,8 @@ $clientCode = <<<'CODE' // First connection $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); if ($client1) { + $meta1 = stream_get_meta_data($client1); + echo "Client first connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n"; echo trim(fgets($client1)) . "\n"; usleep(100000); // Wait for session storage fclose($client1); @@ -80,6 +82,8 @@ $clientCode = <<<'CODE' $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2); if ($client2) { + $meta2 = stream_get_meta_data($client2); + echo "Client second connection resumed: " . ($meta2['crypto']['session_reused'] ? "yes" : "no") . "\n"; echo trim(fgets($client2)) . "\n"; fclose($client2); } @@ -103,4 +107,5 @@ ServerClientTestCase::getInstance()->run($clientCode, $serverCode); Response 1 Client received tickets on first connection: 0 Response 2 +Client second connection resumed: yes Server: NEW_CB_CALLS:0 diff --git a/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt b/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt index 6979c4cd7583c..bd1228dd1c3e1 100644 --- a/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt +++ b/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt @@ -68,6 +68,8 @@ $clientCode = <<<'CODE' /* First connection */ $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); if ($client1) { + $meta1 = stream_get_meta_data($client1); + echo "Client first connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n"; echo trim(fgets($client1)) . "\n"; fclose($client1); } @@ -83,6 +85,8 @@ $clientCode = <<<'CODE' $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2); if ($client2) { + $meta2 = stream_get_meta_data($client2); + echo "Client second connection resumed: " . ($meta2['crypto']['session_reused'] ? "yes" : "no") . "\n"; echo trim(fgets($client2)) . "\n"; fclose($client2); } @@ -104,7 +108,9 @@ ServerClientTestCase::getInstance()->run($clientCode, $serverCode); @unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_external_proper.pem.tmp'); ?> --EXPECTF-- +Client first connection resumed: no Response 1 Session captured: YES +Client second connection resumed: yes Response 2 Server: NEW_CB_CALLED,GET_CB_CALLED,SESSIONS:3 diff --git a/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt b/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt index f0e8cc153f71b..9f61661209281 100644 --- a/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt +++ b/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt @@ -68,6 +68,8 @@ $clientCode = <<<'CODE' /* First connection */ $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); if ($client1) { + $meta1 = stream_get_meta_data($client1); + echo "Client first connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n"; echo trim(fgets($client1)) . "\n"; fclose($client1); } @@ -83,6 +85,8 @@ $clientCode = <<<'CODE' $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2); if ($client2) { + $meta2 = stream_get_meta_data($client2); + echo "Client second connection resumed: " . ($meta2['crypto']['session_reused'] ? "yes" : "no") . "\n"; echo trim(fgets($client2)) . "\n"; fclose($client2); } @@ -104,7 +108,9 @@ ServerClientTestCase::getInstance()->run($clientCode, $serverCode); @unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_external_proper.pem.tmp'); ?> --EXPECTF-- +Client first connection resumed: no Response 1 Session captured: YES +Client second connection resumed: yes Response 2 Server: NEW_CB_CALLED,GET_CB_CALLED,SESSIONS:1 diff --git a/ext/openssl/tests/session_resumption_server_external_with_no_verify.phpt b/ext/openssl/tests/session_resumption_server_external_with_no_verify.phpt index ef0cc70c1e96b..a4376e43e304e 100644 --- a/ext/openssl/tests/session_resumption_server_external_with_no_verify.phpt +++ b/ext/openssl/tests/session_resumption_server_external_with_no_verify.phpt @@ -76,6 +76,8 @@ $clientCode = <<<'CODE' /* First connection */ $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); if ($client1) { + $meta1 = stream_get_meta_data($client1); + echo "Client first connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n"; echo trim(fgets($client1)) . "\n"; fclose($client1); } @@ -89,6 +91,8 @@ $clientCode = <<<'CODE' $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2); if ($client2) { + $meta2 = stream_get_meta_data($client2); + echo "Client second connection resumed: " . ($meta2['crypto']['session_reused'] ? "yes" : "no") . "\n"; echo trim(fgets($client2)) . "\n"; fclose($client2); } @@ -110,6 +114,8 @@ ServerClientTestCase::getInstance()->run($clientCode, $serverCode); @unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_resumption_server.pem.tmp'); ?> --EXPECTF-- +Client first connection resumed: no Response 1 +Client second connection resumed: yes Response 2 Server callbacks: NEW_CB_CALLED,GET_CB_CALLED diff --git a/ext/openssl/tests/session_resumption_server_internal.phpt b/ext/openssl/tests/session_resumption_server_internal.phpt index ed3bb006ec830..0a3a788e86360 100644 --- a/ext/openssl/tests/session_resumption_server_internal.phpt +++ b/ext/openssl/tests/session_resumption_server_internal.phpt @@ -50,6 +50,8 @@ $clientCode = <<<'CODE' /* First connection */ $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); if ($client1) { + $meta1 = stream_get_meta_data($client1); + echo "Client first connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n"; echo trim(fgets($client1)) . "\n"; fclose($client1); } @@ -65,6 +67,8 @@ $clientCode = <<<'CODE' $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2); if ($client2) { + $meta2 = stream_get_meta_data($client2); + echo "Client second connection resumed: " . ($meta2['crypto']['session_reused'] ? "yes" : "no") . "\n"; echo trim(fgets($client2)) . "\n"; fclose($client2); } @@ -86,7 +90,9 @@ ServerClientTestCase::getInstance()->run($clientCode, $serverCode); @unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_internal_cache.pem.tmp'); ?> --EXPECTF-- +Client first connection resumed: no Connection 1 Session data received: YES +Client second connection resumed: yes Connection 2 SERVER_DONE diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 6009f58b16453..494bdc6ad36f0 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -2758,6 +2758,7 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val add_assoc_string(&tmp, "cipher_name", (char *) SSL_CIPHER_get_name(cipher)); add_assoc_long(&tmp, "cipher_bits", SSL_CIPHER_get_bits(cipher, NULL)); add_assoc_string(&tmp, "cipher_version", SSL_CIPHER_get_version(cipher)); + add_assoc_bool(&tmp, "session_reused", SSL_session_reused(sslsock->ssl_handle)); #ifdef HAVE_TLS_ALPN { From abd4ee8030beab5add79761b7e607b81203a01c7 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 31 Jan 2026 17:30:54 +0100 Subject: [PATCH 3/7] Add initial parent SSL_CTX sharing --- ext/openssl/xp_ssl.c | 267 ++++++++++++++++++++++++++----------------- 1 file changed, 165 insertions(+), 102 deletions(-) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 494bdc6ad36f0..0c75a70b6c691 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -1757,6 +1757,21 @@ static zend_result php_openssl_setup_client_session(php_stream *stream, return SUCCESS; } +static bool php_openssl_is_session_cache_enabled(php_stream *stream, bool internal_only) +{ + zval *val; + + if (GET_VER_OPT("session_cache")) { + return zend_is_true(val); + } + + if (internal_only) { + return false; + } + + return GET_VER_OPT("session_get_cb"); +} + /** * Configure session resumption options for server connections */ @@ -1844,52 +1859,43 @@ static zend_result php_openssl_setup_server_session(php_stream *stream, php_error_docref(NULL, E_WARNING, "Session tickets cannot be enabled when session_get_cb is set"); } - } else { - /* Internal cache mode (default) */ - - /* Handle session_cache option */ - bool session_cache_enabled = true; - if (GET_VER_OPT("session_cache")) { - session_cache_enabled = zend_is_true(val); - } + } else if (php_openssl_is_session_cache_enabled(stream, true)) { + /* Internal cache mode */ + SSL_CTX_set_session_cache_mode(sslsock->ctx, SSL_SESS_CACHE_SERVER); - if (session_cache_enabled) { - SSL_CTX_set_session_cache_mode(sslsock->ctx, SSL_SESS_CACHE_SERVER); - - /* Handle session_cache_size */ - if (GET_VER_OPT("session_cache_size")) { - zend_long cache_size = zval_get_long(val); - if (cache_size > 0) { - SSL_CTX_sess_set_cache_size(sslsock->ctx, cache_size); - } else { - php_error_docref(NULL, E_WARNING, "session_cache_size must be positive"); - } + /* Handle session_cache_size */ + if (GET_VER_OPT("session_cache_size")) { + zend_long cache_size = zval_get_long(val); + if (cache_size > 0) { + SSL_CTX_sess_set_cache_size(sslsock->ctx, cache_size); } else { - /* Default cache size from RFC */ - SSL_CTX_sess_set_cache_size(sslsock->ctx, 20480); + php_error_docref(NULL, E_WARNING, "session_cache_size must be positive"); } + } else { + /* Default cache size from RFC */ + SSL_CTX_sess_set_cache_size(sslsock->ctx, 20480); + } - /* Handle session_timeout */ - if (GET_VER_OPT("session_timeout")) { - zend_long timeout = zval_get_long(val); - if (timeout > 0) { - SSL_CTX_set_timeout(sslsock->ctx, timeout); - } else { - php_error_docref(NULL, E_WARNING, "session_timeout must be positive"); - } + /* Handle session_timeout */ + if (GET_VER_OPT("session_timeout")) { + zend_long timeout = zval_get_long(val); + if (timeout > 0) { + SSL_CTX_set_timeout(sslsock->ctx, timeout); } else { - /* Default timeout from RFC */ - SSL_CTX_set_timeout(sslsock->ctx, 300); - } - - /* Optional notification callback for internal cache */ - if (has_new_cb) { - SSL_CTX_sess_set_new_cb(sslsock->ctx, php_openssl_session_new_cb); + php_error_docref(NULL, E_WARNING, "session_timeout must be positive"); } } else { - /* Session caching disabled */ - SSL_CTX_set_session_cache_mode(sslsock->ctx, SSL_SESS_CACHE_OFF); + /* Default timeout from RFC */ + SSL_CTX_set_timeout(sslsock->ctx, 300); + } + + /* Optional notification callback for internal cache */ + if (has_new_cb) { + SSL_CTX_sess_set_new_cb(sslsock->ctx, php_openssl_session_new_cb); } + } else { + /* Session caching disabled */ + SSL_CTX_set_session_cache_mode(sslsock->ctx, SSL_SESS_CACHE_OFF); } return SUCCESS; @@ -1928,39 +1934,13 @@ static zend_result php_openssl_apply_client_session_data(php_stream *stream, return SUCCESS; } -static zend_result php_openssl_setup_crypto(php_stream *stream, - php_openssl_netstream_data_t *sslsock, - php_stream_xport_crypto_param *cparam) /* {{{ */ + +static zend_result php_openssl_create_server_ctx(php_stream *stream, + php_openssl_netstream_data_t *sslsock, int method_flags) { - const SSL_METHOD *method; - int ssl_ctx_options; - int method_flags; - zend_long min_version = 0; - zend_long max_version = 0; - char *cipherlist = NULL; - char *alpn_protocols = NULL; zval *val; - bool verify_peer = false; - - if (sslsock->ssl_handle) { - if (sslsock->s.is_blocked) { - php_error_docref(NULL, E_WARNING, "SSL/TLS already set-up for this stream"); - return FAILURE; - } else { - return SUCCESS; - } - } - sslsock->session_callbacks = NULL; - - ERR_clear_error(); - - /* We need to do slightly different things based on client/server method - * so let's remember which method was selected */ - sslsock->is_client = cparam->inputs.method & STREAM_CRYPTO_IS_CLIENT; - method_flags = cparam->inputs.method & ~STREAM_CRYPTO_IS_CLIENT; - - method = sslsock->is_client ? SSLv23_client_method() : SSLv23_server_method(); + const SSL_METHOD *method = sslsock->is_client ? SSLv23_client_method() : SSLv23_server_method(); sslsock->ctx = SSL_CTX_new(method); if (sslsock->ctx == NULL) { @@ -1970,10 +1950,12 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, SSL_CTX_set_ex_data(sslsock->ctx, php_openssl_get_ctx_stream_data_index(), stream); + zend_long min_version = 0; + zend_long max_version = 0; GET_VER_OPT_LONG("min_proto_version", min_version); GET_VER_OPT_LONG("max_proto_version", max_version); method_flags = php_openssl_get_proto_version_flags(method_flags, min_version, max_version); - ssl_ctx_options = SSL_OP_ALL; + int ssl_ctx_options = SSL_OP_ALL; if (GET_VER_OPT("no_ticket") && zend_is_true(val)) { ssl_ctx_options |= SSL_OP_NO_TICKET; @@ -1996,6 +1978,7 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, ssl_ctx_options |= SSL_OP_NO_COMPRESSION; } + bool verify_peer = false; if (GET_VER_OPT("verify_peer") && !zend_is_true(val)) { php_openssl_disable_peer_verification(sslsock->ctx, stream); } else { @@ -2011,6 +1994,7 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, SSL_CTX_set_default_passwd_cb(sslsock->ctx, php_openssl_passwd_callback); } + char *cipherlist = NULL; GET_VER_OPT_STRING("ciphers", cipherlist); #ifndef USE_OPENSSL_SYSTEM_CIPHERS if (!cipherlist) { @@ -2033,6 +2017,7 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, #endif } + char *alpn_protocols = NULL; GET_VER_OPT_STRING("alpn_protocols", alpn_protocols); if (alpn_protocols) { #ifdef HAVE_TLS_ALPN @@ -2096,6 +2081,82 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, } } +#ifdef HAVE_TLS_SNI + /* Enable server-side SNI */ + if (!sslsock->is_client && php_openssl_enable_server_sni(stream, sslsock, verify_peer) == FAILURE) { + return FAILURE; + } +#endif + + return SUCCESS; +} + +static zend_result php_openssl_setup_crypto(php_stream *stream, + php_openssl_netstream_data_t *sslsock, + php_stream_xport_crypto_param *cparam) /* {{{ */ +{ + if (sslsock->ssl_handle) { + if (sslsock->s.is_blocked) { + php_error_docref(NULL, E_WARNING, "SSL/TLS already set-up for this stream"); + return FAILURE; + } else { + return SUCCESS; + } + } + + sslsock->session_callbacks = NULL; + + ERR_clear_error(); + + /* We need to do slightly different things based on client/server method + * so let's remember which method was selected */ + sslsock->is_client = cparam->inputs.method & STREAM_CRYPTO_IS_CLIENT; + int method_flags = cparam->inputs.method & ~STREAM_CRYPTO_IS_CLIENT; + + /* Re-use SSL_CTX if session is set */ + if (cparam->inputs.session) { + php_openssl_netstream_data_t *parent_sslsock; + + if (cparam->inputs.session->ops != &php_openssl_socket_ops) { + php_error_docref(NULL, E_WARNING, "Supplied session stream must be an SSL enabled stream"); + } else if ((parent_sslsock = cparam->inputs.session->abstract)->ctx == NULL) { + php_error_docref(NULL, E_WARNING, "Supplied SSL session stream is not set up"); + } else if (sslsock->is_client && parent_sslsock->ssl_handle == NULL) { + php_error_docref(NULL, E_WARNING, "Supplied SSL session stream is not initialized"); + } else { + SSL_CTX_up_ref(parent_sslsock->ctx); + sslsock->ctx = parent_sslsock->ctx; + + sslsock->ssl_handle = SSL_new(sslsock->ctx); + if (!sslsock->ssl_handle) { + php_error_docref(NULL, E_WARNING, "SSL handle creation failure"); + SSL_CTX_free(sslsock->ctx); + sslsock->ctx = NULL; + return FAILURE; + } + + SSL_set_ex_data(sslsock->ssl_handle, php_openssl_get_ssl_stream_data_index(), stream); + + if (!SSL_set_fd(sslsock->ssl_handle, sslsock->s.socket)) { + php_openssl_handle_ssl_error(stream, 0, true); + } + + if (sslsock->is_client) { + if (SSL_copy_session_id(sslsock->ssl_handle, parent_sslsock->ssl_handle)) { + SSL_CTX_set_session_cache_mode(sslsock->ctx, SSL_SESS_CACHE_CLIENT); + } else { + php_error_docref(NULL, E_WARNING, "SSL session copying failed creation failure"); + } + } + + return SUCCESS; + } + } + + if (php_openssl_create_server_ctx(stream, sslsock, method_flags) == FAILURE) { + return FAILURE; + } + sslsock->ssl_handle = SSL_new(sslsock->ctx); if (sslsock->ssl_handle == NULL) { @@ -2117,37 +2178,6 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, php_openssl_handle_ssl_error(stream, 0, true); } - /* Set session data for client */ - if (sslsock->is_client && php_openssl_apply_client_session_data(stream, sslsock)) { - return FAILURE; - } - -#ifdef HAVE_TLS_SNI - /* Enable server-side SNI */ - if (!sslsock->is_client && php_openssl_enable_server_sni(stream, sslsock, verify_peer) == FAILURE) { - return FAILURE; - } -#endif - - /* Enable server-side handshake renegotiation rate-limiting */ - if (!sslsock->is_client) { - php_openssl_init_server_reneg_limit(stream, sslsock); - } - -#ifdef SSL_MODE_RELEASE_BUFFERS - SSL_set_mode(sslsock->ssl_handle, SSL_MODE_RELEASE_BUFFERS); -#endif - - if (cparam->inputs.session) { - if (cparam->inputs.session->ops != &php_openssl_socket_ops) { - php_error_docref(NULL, E_WARNING, "Supplied session stream must be an SSL enabled stream"); - } else if (((php_openssl_netstream_data_t*)cparam->inputs.session->abstract)->ssl_handle == NULL) { - php_error_docref(NULL, E_WARNING, "Supplied SSL session stream is not initialized"); - } else { - SSL_copy_session_id(sslsock->ssl_handle, ((php_openssl_netstream_data_t*)cparam->inputs.session->abstract)->ssl_handle); - } - } - return SUCCESS; } /* }}} */ @@ -2228,9 +2258,18 @@ static int php_openssl_enable_crypto(php_stream *stream, struct timeval start_time, *timeout; bool blocked = sslsock->s.is_blocked, has_timeout = false; + if (!sslsock->is_client) { + php_openssl_init_server_reneg_limit(stream, sslsock); + } + #ifdef HAVE_TLS_SNI if (sslsock->is_client) { php_openssl_enable_client_sni(stream, sslsock); + + /* Set session data for client */ + if ( php_openssl_apply_client_session_data(stream, sslsock)) { + return FAILURE; + } } #endif @@ -2243,6 +2282,8 @@ static int php_openssl_enable_crypto(php_stream *stream, sslsock->state_set = 1; } + SSL_set_mode(sslsock->ssl_handle, SSL_MODE_RELEASE_BUFFERS); + if (SUCCESS == php_openssl_set_blocking(sslsock, 0)) { /* The following mode are added only if we are able to change socket * to non blocking mode which is also used for read and write */ @@ -2699,7 +2740,7 @@ static inline int php_openssl_tcp_sockop_accept(php_stream *stream, php_openssl_ clisockdata->method = sock->method; if (php_stream_xport_crypto_setup(xparam->outputs.client, clisockdata->method, - NULL) < 0 || php_stream_xport_crypto_enable( + sock->ctx ? stream : NULL) < 0 || php_stream_xport_crypto_enable( xparam->outputs.client, 1) < 0) { php_error_docref(NULL, E_WARNING, "Failed to enable crypto"); @@ -2961,7 +3002,14 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val (xparam->op == STREAM_XPORT_OP_CONNECT_ASYNC && xparam->outputs.returncode == 1 && xparam->outputs.error_code == EINPROGRESS))) { - if (php_stream_xport_crypto_setup(stream, sslsock->method, NULL) < 0 || + zval *val; + php_stream *session_stream = NULL; + + if (GET_VER_OPT("session_stream")) { + php_stream_from_zval_no_verify(session_stream, val); + } + + if (php_stream_xport_crypto_setup(stream, sslsock->method, session_stream) < 0 || php_stream_xport_crypto_enable(stream, 1) < 0) { php_error_docref(NULL, E_WARNING, "Failed to enable crypto"); xparam->outputs.returncode = -1; @@ -2969,6 +3017,21 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val } return PHP_STREAM_OPTION_RETURN_OK; + case STREAM_XPORT_OP_LISTEN: + /* Do normal listen first */ + xparam->outputs.returncode = php_stream_socket_ops.set_option( + stream, option, value, ptrparam); + + if (xparam->outputs.returncode == 0 && sslsock->enable_on_connect) { + /* Check if we should create SSL_CTX early for session resumption */ + if (php_openssl_is_session_cache_enabled(stream, false)) { + if (FAILURE == php_openssl_create_server_ctx(stream, sslsock, sslsock->method)) { + xparam->outputs.returncode = -1; + } + } + } + return PHP_STREAM_OPTION_RETURN_OK; + case STREAM_XPORT_OP_ACCEPT: /* we need to copy the additional fields that the underlying tcp transport * doesn't know about */ From fa430a31e1a1d665cf499b2b445c929e4b46f44f Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 31 Jan 2026 23:17:44 +0100 Subject: [PATCH 4/7] Fix missing session_id_context for server --- ext/openssl/xp_ssl.c | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 0c75a70b6c691..895102f4a621c 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -1782,7 +1782,7 @@ static zend_result php_openssl_setup_server_session(php_stream *stream, bool has_get_cb = false; bool has_new_cb = false; bool has_remove_cb = false; - bool has_session_context_id = false; + bool has_session_id_context = false; bool is_persistent = php_stream_is_persistent(stream); /* Check for session_get_cb first (determines cache mode) */ @@ -1795,14 +1795,14 @@ static zend_result php_openssl_setup_server_session(php_stream *stream, has_get_cb = true; } - if (GET_VER_OPT("session_context_id")) { + if (GET_VER_OPT("session_id_context")) { if (Z_TYPE_P(val) != IS_STRING || Z_STRLEN_P(val) == 0) { - zend_type_error("session_context_id must be a non empty string"); + zend_type_error("session_id_context must be a non empty string"); return FAILURE; } SSL_CTX_set_session_id_context(sslsock->ctx, (const unsigned char *) Z_STRVAL_P(val), Z_STRLEN_P(val)); - has_session_context_id = true; + has_session_id_context = true; } /* Check for session_new_cb */ @@ -1814,10 +1814,10 @@ static zend_result php_openssl_setup_server_session(php_stream *stream, ZVAL_COPY(&sslsock->session_callbacks->new_cb, val); has_new_cb = true; - if (!has_session_context_id && + if (!has_session_id_context && (SSL_CTX_get_verify_mode(sslsock->ctx) & SSL_VERIFY_PEER) != 0) { php_error_docref(NULL, E_WARNING, - "session_new_cb is ignored as no session_context_id is set and verify_peer is enabled"); + "session_new_cb is ignored as no session_id_context is set and verify_peer is enabled"); } } @@ -1863,6 +1863,20 @@ static zend_result php_openssl_setup_server_session(php_stream *stream, /* Internal cache mode */ SSL_CTX_set_session_cache_mode(sslsock->ctx, SSL_SESS_CACHE_SERVER); + /* Set ID context */ + char *session_id_context = NULL; + GET_VER_OPT_STRING("session_id_context", session_id_context); + + if (session_id_context == NULL) { + /* Default context - could also use script path or similar */ + static const unsigned char default_ctx[] = "PHP"; + SSL_CTX_set_session_id_context(sslsock->ctx, default_ctx, sizeof(default_ctx) - 1); + } else { + SSL_CTX_set_session_id_context(sslsock->ctx, + (unsigned char *)session_id_context, + strlen(session_id_context)); + } + /* Handle session_cache_size */ if (GET_VER_OPT("session_cache_size")) { zend_long cache_size = zval_get_long(val); From 4101cbaf0c0164ad54b4d620a7b12e53f393b2af Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 31 Jan 2026 23:18:53 +0100 Subject: [PATCH 5/7] Add hidden TLS debugging --- ext/openssl/xp_ssl.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 895102f4a621c..c6f10092925e7 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -2287,6 +2287,12 @@ static int php_openssl_enable_crypto(php_stream *stream, } #endif +#ifdef PHP_OPENSSL_TLS_DEBUG + BIO *b_out = BIO_new_fp(stdout, BIO_NOCLOSE | BIO_FP_TEXT); + SSL_set_msg_callback(sslsock->ssl_handle, SSL_trace); + SSL_set_msg_callback_arg(sslsock->ssl_handle, b_out); +#endif + if (!sslsock->state_set) { if (sslsock->is_client) { SSL_set_connect_state(sslsock->ssl_handle); From 5c656ddf99d48de41920267f2a6975b6cb2001a8 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Sat, 31 Jan 2026 23:27:31 +0100 Subject: [PATCH 6/7] Rename session_context_id to session_id_context in all tests --- ext/openssl/tests/session_resumption_get_cb_no_ticket.phpt | 2 +- .../tests/session_resumption_get_cb_num_tickets_zero.phpt | 2 +- ext/openssl/tests/session_resumption_new_cb_no_context.phpt | 6 +++--- .../session_resumption_server_external_with_context_id.phpt | 2 +- ...on_resumption_server_external_with_context_id_tls12.phpt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ext/openssl/tests/session_resumption_get_cb_no_ticket.phpt b/ext/openssl/tests/session_resumption_get_cb_no_ticket.phpt index fe225b6bf3472..55329dca672a7 100644 --- a/ext/openssl/tests/session_resumption_get_cb_no_ticket.phpt +++ b/ext/openssl/tests/session_resumption_get_cb_no_ticket.phpt @@ -16,7 +16,7 @@ $serverCode = <<<'CODE' /* Trying to enable tickets with external cache - should warn */ $ctx = stream_context_create(['ssl' => [ 'local_cert' => '%s', - 'session_context_id' => 'test-app', + 'session_id_context' => 'test-app', 'no_ticket' => false, // Explicitly trying to enable tickets 'session_new_cb' => function($stream, $sessionId, $sessionData) { // Store session diff --git a/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt b/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt index dd9c4906a581e..3ae492058f9bc 100644 --- a/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt +++ b/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt @@ -17,7 +17,7 @@ $serverCode = <<<'CODE' $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN; $ctx = stream_context_create(['ssl' => [ 'local_cert' => '%s', - 'session_context_id' => 'test-no-tickets', + 'session_id_context' => 'test-no-tickets', 'num_tickets' => 0, // Disable ticket issuance 'session_new_cb' => function($stream, $sessionId, $sessionData) use (&$sessionStore, &$newCbCalled) { $key = bin2hex($sessionId); diff --git a/ext/openssl/tests/session_resumption_new_cb_no_context.phpt b/ext/openssl/tests/session_resumption_new_cb_no_context.phpt index a1bc078960009..7a90480e64dd6 100644 --- a/ext/openssl/tests/session_resumption_new_cb_no_context.phpt +++ b/ext/openssl/tests/session_resumption_new_cb_no_context.phpt @@ -1,5 +1,5 @@ --TEST-- -TLS session resumption - warning when session_new_cb without session_context_id and verify_peer enabled +TLS session resumption - warning when session_new_cb without session_id_context and verify_peer enabled --EXTENSIONS-- openssl --SKIPIF-- @@ -14,7 +14,7 @@ $caCertFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_no_context_ca.pem.tmp'; $serverCode = <<<'CODE' $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN; - /* session_new_cb without session_context_id, with verify_peer - should warn */ + /* session_new_cb without session_id_context, with verify_peer - should warn */ $ctx = stream_context_create(['ssl' => [ 'local_cert' => '%s', 'verify_peer' => true, @@ -22,7 +22,7 @@ $serverCode = <<<'CODE' 'session_new_cb' => function($stream, $sessionId, $sessionData) { echo "Callback might not be called\n"; } - /* Missing: 'session_context_id' => 'myapp' */ + /* Missing: 'session_id_context' => 'myapp' */ ]]); $server = @stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); diff --git a/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt b/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt index bd1228dd1c3e1..5c4190832bf2d 100644 --- a/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt +++ b/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt @@ -18,7 +18,7 @@ $serverCode = <<<'CODE' $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN; $ctx = stream_context_create(['ssl' => [ 'local_cert' => '%s', - 'session_context_id' => 'test-server', // Proper configuration + 'session_id_context' => 'test-server', // Proper configuration 'session_new_cb' => function($stream, $sessionId, $sessionData) use (&$sessionStore, &$newCbCalled) { $key = bin2hex($sessionId); $sessionStore[$key] = $sessionData; diff --git a/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt b/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt index 9f61661209281..3db6270a9db3f 100644 --- a/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt +++ b/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt @@ -18,7 +18,7 @@ $serverCode = <<<'CODE' $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN; $ctx = stream_context_create(['ssl' => [ 'local_cert' => '%s', - 'session_context_id' => 'test-server', // Proper configuration + 'session_id_context' => 'test-server', // Proper configuration 'session_new_cb' => function($stream, $sessionId, $sessionData) use (&$sessionStore, &$newCbCalled) { $key = bin2hex($sessionId); $sessionStore[$key] = $sessionData; From 1df21876596e9ffe761c60665690ddc6a1e930f8 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 2 Feb 2026 23:04:41 +0100 Subject: [PATCH 7/7] Fix session errors, session_id_context handling and tests --- ext/openssl/tests/ServerClientTestCase.inc | 3 ++ .../session_resumption_client_basic.phpt | 1 + ...on_resumption_get_cb_num_tickets_zero.phpt | 5 +-- .../session_resumption_new_cb_no_context.phpt | 18 ++++++---- .../session_resumption_require_new_cb.phpt | 24 +++++++------ ...ption_server_external_with_context_id.phpt | 2 +- .../session_resumption_server_internal.phpt | 1 + ext/openssl/xp_ssl.c | 35 ++++++------------- 8 files changed, 44 insertions(+), 45 deletions(-) diff --git a/ext/openssl/tests/ServerClientTestCase.inc b/ext/openssl/tests/ServerClientTestCase.inc index 8eedbfdebee8b..f0336fdd39219 100644 --- a/ext/openssl/tests/ServerClientTestCase.inc +++ b/ext/openssl/tests/ServerClientTestCase.inc @@ -179,6 +179,9 @@ class ServerClientTestCase if (empty($addr)) { throw new \Exception("Failed server start"); } + if (strpos($addr, 'SERVER_EXCEPTION') !== false) { + echo $addr; + } if ($code === false) { $clientCode = preg_replace('/{{\s*ADDR\s*}}/', $addr, $clientCode); } else { diff --git a/ext/openssl/tests/session_resumption_client_basic.phpt b/ext/openssl/tests/session_resumption_client_basic.phpt index 3cbe32ab16dcd..e4e1d67901d8b 100644 --- a/ext/openssl/tests/session_resumption_client_basic.phpt +++ b/ext/openssl/tests/session_resumption_client_basic.phpt @@ -15,6 +15,7 @@ $serverCode = <<<'CODE' $ctx = stream_context_create(['ssl' => [ 'local_cert' => '%s', 'session_cache' => true, + 'session_id_context' => 'test-basic', ]]); $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); diff --git a/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt b/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt index 3ae492058f9bc..6196e7d1ef361 100644 --- a/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt +++ b/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt @@ -103,9 +103,10 @@ ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ---EXPECTF-- +--EXPECT-- +Client first connection resumed: no Response 1 Client received tickets on first connection: 0 +Client second connection resumed: no Response 2 -Client second connection resumed: yes Server: NEW_CB_CALLS:0 diff --git a/ext/openssl/tests/session_resumption_new_cb_no_context.phpt b/ext/openssl/tests/session_resumption_new_cb_no_context.phpt index 7a90480e64dd6..af08867b6c9d5 100644 --- a/ext/openssl/tests/session_resumption_new_cb_no_context.phpt +++ b/ext/openssl/tests/session_resumption_new_cb_no_context.phpt @@ -28,12 +28,16 @@ $serverCode = <<<'CODE' $server = @stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); phpt_notify_server_start($server); - $client = @stream_socket_accept($server, 30); - if ($client === false) { - phpt_notify(message: "SERVER_FAILED_AS_EXPECTED"); - } else { - phpt_notify(message: "SERVER_CREATED_UNEXPECTEDLY"); - fclose($server); + try { + $client = @stream_socket_accept($server, 30); + if ($client === false) { + phpt_notify(message: "SERVER_FAILED_UNEXPECTEDLY"); + } else { + phpt_notify(message: "SERVER_CREATED_UNEXPECTEDLY"); + fclose($server); + } + } catch (\Throwable $e) { + phpt_notify(message: "SERVER_EXCEPTION: " . $e->getMessage()); } CODE; $serverCode = sprintf($serverCode, $certFile, $caCertFile); @@ -73,4 +77,4 @@ ServerClientTestCase::getInstance()->run($clientCode, $serverCode); ?> --EXPECT-- Connection failed as expected -SERVER_FAILED_AS_EXPECTED +SERVER_EXCEPTION: session_id_context must be set if session_new_cb is set diff --git a/ext/openssl/tests/session_resumption_require_new_cb.phpt b/ext/openssl/tests/session_resumption_require_new_cb.phpt index 0628d9e69ab44..a08408e2d90b8 100644 --- a/ext/openssl/tests/session_resumption_require_new_cb.phpt +++ b/ext/openssl/tests/session_resumption_require_new_cb.phpt @@ -21,16 +21,19 @@ $serverCode = <<<'CODE' } ]]); - $server = @stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); - phpt_notify_server_start($server); + try { + $server = @stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); - $client = @stream_socket_accept($server, 30); - - if ($client === false) { - phpt_notify(message: "SERVER_FAILED_AS_EXPECTED"); - } else { - phpt_notify(message: "SERVER_CREATED_UNEXPECTEDLY"); - fclose($server); + $client = @stream_socket_accept($server, 30); + if ($client === false) { + phpt_notify(message: "SERVER_FAILED_UNEXPECTEDLY"); + } else { + phpt_notify(message: "SERVER_CREATED_UNEXPECTEDLY"); + fclose($server); + } + } catch (\Throwable $e) { + phpt_notify(message: "SERVER_EXCEPTION: " . $e->getMessage()); } CODE; $serverCode = sprintf($serverCode, $certFile); @@ -67,5 +70,4 @@ ServerClientTestCase::getInstance()->run($clientCode, $serverCode); @unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_require_new_cb.pem.tmp'); ?> --EXPECT-- -Connection failed as expected -SERVER_FAILED_AS_EXPECTED +SERVER_EXCEPTION: session_new_cb is required when session_get_cb is providedConnection failed as expected diff --git a/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt b/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt index 5c4190832bf2d..b5cc543191796 100644 --- a/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt +++ b/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt @@ -18,7 +18,7 @@ $serverCode = <<<'CODE' $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN; $ctx = stream_context_create(['ssl' => [ 'local_cert' => '%s', - 'session_id_context' => 'test-server', // Proper configuration + 'session_id_context' => 'test-server', 'session_new_cb' => function($stream, $sessionId, $sessionData) use (&$sessionStore, &$newCbCalled) { $key = bin2hex($sessionId); $sessionStore[$key] = $sessionData; diff --git a/ext/openssl/tests/session_resumption_server_internal.phpt b/ext/openssl/tests/session_resumption_server_internal.phpt index 0a3a788e86360..be8e95a35c812 100644 --- a/ext/openssl/tests/session_resumption_server_internal.phpt +++ b/ext/openssl/tests/session_resumption_server_internal.phpt @@ -14,6 +14,7 @@ $serverCode = <<<'CODE' $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN; $ctx = stream_context_create(['ssl' => [ 'local_cert' => '%s', + 'session_id_context' => 'test-server', 'session_cache' => true, 'session_cache_size' => 1024, 'session_timeout' => 300, diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index c6f10092925e7..9938169289871 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -1816,15 +1816,14 @@ static zend_result php_openssl_setup_server_session(php_stream *stream, if (!has_session_id_context && (SSL_CTX_get_verify_mode(sslsock->ctx) & SSL_VERIFY_PEER) != 0) { - php_error_docref(NULL, E_WARNING, - "session_new_cb is ignored as no session_id_context is set and verify_peer is enabled"); + zend_value_error("session_id_context must be set if session_new_cb is set"); + return FAILURE; } } /* Validate: if session_get_cb is provided, session_new_cb is required */ if (has_get_cb && !has_new_cb) { - php_error_docref(NULL, E_WARNING, - "session_new_cb is required when session_get_cb is provided"); + zend_value_error("session_new_cb is required when session_get_cb is provided"); return FAILURE; } @@ -1856,34 +1855,24 @@ static zend_result php_openssl_setup_server_session(php_stream *stream, // Disable tickets (they won't work anyway) and warn if explicity enabled SSL_CTX_set_options(sslsock->ctx, SSL_OP_NO_TICKET); if (GET_VER_OPT("no_ticket") && !zend_is_true(val)) { - php_error_docref(NULL, E_WARNING, - "Session tickets cannot be enabled when session_get_cb is set"); + zend_value_error("Session tickets cannot be enabled when session_get_cb is set"); } } else if (php_openssl_is_session_cache_enabled(stream, true)) { + if (!has_session_id_context && + (SSL_CTX_get_verify_mode(sslsock->ctx) & SSL_VERIFY_PEER) != 0) { + zend_value_error("session_id_context must be set for internal session cache"); + } + /* Internal cache mode */ SSL_CTX_set_session_cache_mode(sslsock->ctx, SSL_SESS_CACHE_SERVER); - /* Set ID context */ - char *session_id_context = NULL; - GET_VER_OPT_STRING("session_id_context", session_id_context); - - if (session_id_context == NULL) { - /* Default context - could also use script path or similar */ - static const unsigned char default_ctx[] = "PHP"; - SSL_CTX_set_session_id_context(sslsock->ctx, default_ctx, sizeof(default_ctx) - 1); - } else { - SSL_CTX_set_session_id_context(sslsock->ctx, - (unsigned char *)session_id_context, - strlen(session_id_context)); - } - /* Handle session_cache_size */ if (GET_VER_OPT("session_cache_size")) { zend_long cache_size = zval_get_long(val); if (cache_size > 0) { SSL_CTX_sess_set_cache_size(sslsock->ctx, cache_size); } else { - php_error_docref(NULL, E_WARNING, "session_cache_size must be positive"); + zend_value_error("session_cache_size must be positive"); } } else { /* Default cache size from RFC */ @@ -1896,7 +1885,7 @@ static zend_result php_openssl_setup_server_session(php_stream *stream, if (timeout > 0) { SSL_CTX_set_timeout(sslsock->ctx, timeout); } else { - php_error_docref(NULL, E_WARNING, "session_timeout must be positive"); + zend_value_error("session_timeout must be positive"); } } else { /* Default timeout from RFC */ @@ -2118,8 +2107,6 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, } } - sslsock->session_callbacks = NULL; - ERR_clear_error(); /* We need to do slightly different things based on client/server method