This RFC proposes adding comprehensive TLS session resumption support to PHP's OpenSSL stream implementation. Session resumption is a critical performance optimization that allows TLS clients and servers to skip the expensive full handshake by reusing cryptographic parameters from previous connections.
Currently, PHP's stream wrapper provides limited control over session management. While OpenSSL's internal session cache works automatically on the server side for some scenarios, developers cannot:
This proposal exposes OpenSSL's session management APIs through the existing stream context options, providing fine-grained control while maintaining backward compatibility.
<?php // Client: Resume previous session for faster reconnection $context = stream_context_create([ 'ssl' => [ 'peer_name' => 'api.example.com', 'session_data' => $_SESSION['tls_session'] ?? null, 'session_new_cb' => function($stream, $sessionId, $sessionData) { $_SESSION['tls_session'] = $sessionData; } ] ]); $fp = stream_socket_client('tls://api.example.com:443', context: $context); // Second connection will resume, saving ~100ms of handshake time ?>
This proposal adds the following SSL stream context options:
function(resource $stream, string $sessionId, string $sessionData): void$sessionId: Binary session identifier$sessionData: Serialized session (OpenSSL format via i2d_SSL_SESSION)function(resource $stream, string $sessionId): ?stringfunction(resource $stream, string $sessionId): voidWhen a client stream is created:
1. If ''session_data'' is provided, attempt to resume with that session
2. If ''session_new_cb'' is provided, call it when a new session is established or received
3. Server-only options (''session_get_cb'', ''session_remove_cb'', ''session_cache'', ''num_tickets'', etc.) are ignored
Note: Client-side session resumption requires explicit management via session_data and session_new_cb. PHP does not automatically cache sessions on the client side - each connection uses a fresh SSL context unless session_data is provided.
When a server stream is created:
Without External Cache (no session_get_cb):
session_cache controls whether caching is enabledsession_cache_size and session_timeout configure the internal cachesession_new_cb can be provided for notifications without external storage
With External Cache (session_get_cb provided):
session_new_cb becomes required (E_WARNING if missing)session_context_id becomes required (E_WARNING if missing)session_remove_cb is optionalsession_cache_size and session_timeout are ignored (application manages storage)<?php $sessions = []; function create_client_context(string $host): resource { global $sessions; return stream_context_create([ 'ssl' => [ 'peer_name' => $host, 'session_data' => $sessions[$host] ?? null, 'session_new_cb' => function($stream, $id, $data) use ($host) { global $sessions; $sessions[$host] = $data; error_log("Saved session for $host"); } ] ]); } // First connection - full handshake $fp1 = stream_socket_client( 'tls://api.example.com:443', $errno, $errstr, 30, STREAM_CLIENT_CONNECT, create_client_context('api.example.com') ); // Second connection - resumed! (much faster) $fp2 = stream_socket_client( 'tls://api.example.com:443', $errno, $errstr, 30, STREAM_CLIENT_CONNECT, create_client_context('api.example.com') ); ?>
<?php $context = stream_context_create([ 'ssl' => [ 'local_cert' => '/path/to/cert.pem', 'local_pk' => '/path/to/key.pem', // Session resumption enabled by default 'session_cache' => true, 'session_cache_size' => 1024, 'session_timeout' => 7200, // 2 hours ] ]); $server = stream_socket_server( 'tls://0.0.0.0:8443', $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context ); while ($conn = stream_socket_accept($server)) { // Handle connection // Client session resumption happens automatically } ?>
<?php // Example 1: Issue multiple tickets for connection parallelization $context = stream_context_create([ 'ssl' => [ 'local_cert' => '/path/to/cert.pem', 'num_tickets' => 4, // Issue 4 tickets per connection ] ]); $server = stream_socket_server('tls://0.0.0.0:8443', context: $context); // Example 2: Disable all resumption (maximum security) $context = stream_context_create([ 'ssl' => [ 'local_cert' => '/path/to/cert.pem', 'num_tickets' => 0, // No tickets = no resumption possible ] ]); $server = stream_socket_server('tls://0.0.0.0:8443', context: $context); ?>
<?php $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $context = stream_context_create([ 'ssl' => [ 'local_cert' => '/path/to/cert.pem', 'local_pk' => '/path/to/key.pem', 'session_context_id' => 'myapp', 'session_new_cb' => function($stream, $id, $data) use ($redis) { $key = 'tls_session:' . bin2hex($id); $redis->setex($key, 7200, $data); }, 'session_get_cb' => function($stream, $id) use ($redis) { $key = 'tls_session:' . bin2hex($id); $result = $redis->get($key); return $result !== false ? $result : null; }, 'session_remove_cb' => function($stream, $id) use ($redis) { $key = 'tls_session:' . bin2hex($id); $redis->del($key); }, ] ]); $server = stream_socket_server( 'tls://0.0.0.0:8443', $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context ); // Now sessions are shared across PHP-FPM workers via Redis while ($conn = stream_socket_accept($server)) { // Handle connection } ?>
<?php // For high-security scenarios $context = stream_context_create([ 'ssl' => [ 'local_cert' => '/path/to/cert.pem', 'session_cache' => false, // No session resumption ] ]); $server = stream_socket_server('tls://0.0.0.0:8443', context: $context); ?>
<?php // Invalid or expired session data is handled gracefully $context = stream_context_create([ 'ssl' => [ 'peer_name' => 'example.com', 'session_data' => 'corrupted_data', // Invalid ] ]); // E_WARNING emitted, falls back to full handshake $fp = stream_socket_client('tls://example.com:443', context: $context); ?>
This RFC introduces no backward incompatible changes.
Potential Considerations:
Next PHP 8.5
No impact. The changes are isolated to the OpenSSL stream wrapper implementation in ext openssl.
No impact to other extensions. Changes are confined to ext openssl's stream transport implementation.
Positive impacts:
Documentation needs:
None. All design questions have been resolved.
This RFC lays the groundwork for future TLS 1.3 enhancements:
These features are intentionally excluded from this RFC to maintain focus and allow for iterative development.
This is a simple yes/no vote requiring a 2/3 majority.
Implementation PR: https://github.com/php/php-src/pull/20296
After the project is implemented, this section should contain:
SSL_SESS_CACHE_CLIENT and SSL_SESS_CACHE_BOTH constants: These OpenSSL cache modes don't map to
PHP's stream architecture where each connection creates a separate SSL_CTX. Client-side resumption
is handled explicitly via the session_data option, making cache-mode constants unnecessary.
Only boolean session_cache option is provided for servers.
Automatic client-side caching: Considered adding a session_cache => true option for clients
that would automatically cache by peer_name, but rejected as it adds magic behavior and state
management concerns across requests. Developers can easily implement this pattern with
session_new_cb as shown in examples.
Session metadata in callbacks: Discussed passing additional information like protocol version, cipher suite, and expiry time to callbacks. Rejected to keep the API simple - developers can use OpenSSL functions directly if they need to inspect session details.