From df1f287b23ebef090e906951ac011da76e6e931c Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 23 Oct 2025 23:49:59 +0200 Subject: [PATCH] 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 62929246a07f0..e57cb03d4d617 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; @@ -1543,6 +1551,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) /* {{{ */ @@ -1566,6 +1949,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 @@ -1581,6 +1966,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); @@ -1589,6 +1976,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; @@ -1683,6 +2076,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) { @@ -1704,6 +2115,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) { @@ -2198,6 +2614,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;