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;