Table of Contents

PHP RFC: TLS Session Resumption Support for Streams

Introduction

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 client session can be copied using stream_socket_enable_crypto session_stream parameter, 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
$previousSession = $_SESSION['tls_session'] ?? null;
 
$context = stream_context_create([
    'ssl' => [
        'peer_name' => 'api.example.com',
        'session_data' => $previousSession ? OpenSSLSession::import($previousSession) : null,
        'session_new_cb' => function($stream, OpenSSLSession $session) {
            $_SESSION['tls_session'] = $session->export();
        }
    ]
]);
 
$fp = stream_socket_client('tls://api.example.com:443', context: $context);
// Second connection will resume, saving ~100ms of handshake time
?>

Proposal

OpenSSLSession class

The proposal introduces a new class representing an SSL session that is used by new stream context options and callbacks. The class wraps an underlying SSL_SESSION and provides methods for inspection, serialization, and import/export.

Class Definition

final class OpenSSLSession
{
    /** @var string Binary session ID assigned by the server */
    public readonly string $id;
 
    /**
     * Export the session to a portable format.
     *
     * @param int $format One of existing OPENSSL_ENCODING_PEM (default) or OPENSSL_ENCODING_DER
     * @return string Encoded session data
     * @throws \Exception on export failure
     */
    public function export(int $format = OPENSSL_ENCODING_PEM): string {}
 
    /**
     * Import a session from previously exported data.
     *
     * @param string $data Encoded session data (DER or PEM)
     * @param int $format One of exiting OPENSSL_ENCODING_PEM (default) or OPENSSL_ENCODING_DER
     * @return OpenSSLSession
     * @throws \Exception on import failure
     */
    public static function import(string $data, int $format = OPENSSL_ENCODING_PEM): OpenSSLSession {}
 
    /** Whether this session can be used for resumption */
    public function isResumable(): bool {}
 
    /** Session lifetime in seconds as configured by the server */
    public function getTimeout(): int {}
 
    /** Unix timestamp of when the session was created */
    public function getCreatedAt(): int {}
 
    /** TLS protocol version string (e.g. "TLSv1.3") or null if unknown */
    public function getProtocol(): ?string {}
 
    /** Cipher suite name (e.g. "TLS_AES_256_GCM_SHA384") or null */
    public function getCipher(): ?string {}
 
    /** Whether the session contains a session ticket */
    public function hasTicket(): bool {}
 
    /** Server-suggested ticket lifetime in seconds, or null if no ticket */
    public function getTicketLifetimeHint(): ?int {}
 
    /** @internal Serializes to PEM for use with serialize()/unserialize() */
    public function __serialize(): array {}
 
    /** @internal Restores from PEM data */
    public function __unserialize(array $data): void {}
}

Construction

OpenSSLSession cannot be directly instantiated. Instances are obtained through:

Export/Import Formats

Two encoding formats are supported via the existing OPENSSL_ENCODING_DER and OPENSSL_ENCODING_PEM constants:

<?php
// Export as PEM (default, text-based)
$pem = $session->export(); // or $session->export(OPENSSL_ENCODING_PEM)
$restored = OpenSSLSession::import($pem);
 
// Export as DER (compact, binary)
$der = $session->export(OPENSSL_ENCODING_DER);
$restored = OpenSSLSession::import($der, OPENSSL_ENCODING_DER);
 
// PHP serialization also works (uses PEM internally)
$serialized = serialize($session);
$restored = unserialize($serialized);
?>

Introspection

The class provides read-only access to session metadata for logging, debugging, and cache management:

<?php
// Inside session_new_cb
$newCb = function($stream, OpenSSLSession $session) {
    echo "Session ID: " . bin2hex($session->id) . "\n";
    echo "Protocol: " . $session->getProtocol() . "\n";       // "TLSv1.3"
    echo "Cipher: " . $session->getCipher() . "\n";           // "TLS_AES_256_GCM_SHA384"
    echo "Resumable: " . ($session->isResumable() ? "yes" : "no") . "\n";
    echo "Has ticket: " . ($session->hasTicket() ? "yes" : "no") . "\n";
    echo "Timeout: " . $session->getTimeout() . "s\n";
    echo "Created: " . date('c', $session->getCreatedAt()) . "\n";
 
    if ($session->hasTicket()) {
        echo "Ticket lifetime hint: " . $session->getTicketLifetimeHint() . "s\n";
    }
};
?>

New Stream Context Options

This proposal adds the following SSL stream context options:

Client-Only Options

Server-Only Options

Behavior

Client Behavior

When a client stream is created:

1. If ''session_data'' is provided as an ''OpenSSLSession'' instance, attempt to resume with that session
2. If ''session_new_cb'' is provided, call it with the stream and an ''OpenSSLSession'' object when a new session is established or a new session ticket is received
3. If ''session_stream'' is provided (existing option), the SSL context and session are copied from the provided stream
4. When ''session_new_cb'' or ''session_data'' is set, client-side external cache mode is enabled (''SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL'')

Note: Client-side session resumption requires either explicit management via session_data and session_new_cb or setting session_stream which copies the session from the provided stream.

Server Behavior

When a server stream is created:

Without External Cache (no session_get_cb):

With External Cache (session_get_cb provided):

Session Context ID Requirements:

Error Handling

The following validation errors are raised:

Examples

Client: Simple Session Caching

<?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, OpenSSLSession $session) use ($host) {
                global $sessions;
                $sessions[$host] = $session;
            }
        ]
    ]);
}
 
// 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')
);
?>

Client: Cross-Request Persistence

<?php
// Save session to file/cache for use in subsequent HTTP requests
$sessionFile = '/tmp/tls_session_api.pem';
 
$previousSession = null;
if (file_exists($sessionFile)) {
    $previousSession = OpenSSLSession::import(file_get_contents($sessionFile));
    if (!$previousSession->isResumable()) {
        $previousSession = null;
    }
}
 
$context = stream_context_create([
    'ssl' => [
        'peer_name' => 'api.example.com',
        'session_data' => $previousSession,
        'session_new_cb' => function($stream, OpenSSLSession $session) use ($sessionFile) {
            file_put_contents($sessionFile, $session->export());
        }
    ]
]);
 
$fp = stream_socket_client('tls://api.example.com:443', context: $context);
?>

Server: Internal Cache (Default Behavior)

<?php
$context = stream_context_create([
    'ssl' => [
        'local_cert' => '/path/to/cert.pem',
        'local_pk' => '/path/to/key.pem',
        // Session resumption enabled by default
        'session_id_context' => 'example.com',
        '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
}
?>

Server: Control Ticket Issuance (TLS 1.3)

<?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);
?>

Server: External Cache (Redis)

<?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_id_context' => 'myapp',
 
        'session_new_cb' => function($stream, OpenSSLSession $session) use ($redis) {
            $key = 'tls_session:' . bin2hex($session->id);
            $redis->setex($key, 7200, $session->export());
        },
 
        'session_get_cb' => function($stream, string $id) use ($redis): ?OpenSSLSession {
            $key = 'tls_session:' . bin2hex($id);
            $result = $redis->get($key);
            return $result !== false ? OpenSSLSession::import($result) : null;
        },
 
        'session_remove_cb' => function($stream, string $id) use ($redis): void {
            $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
}
?>

Server: Disable Session Resumption

<?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);
?>

Session mode differences

When session is enabled there are some differences in behaviour that is good to keep in mind.

Backward Incompatible Changes

This RFC introduces no backward incompatible changes

Proposed PHP Version(s)

Next PHP 8.5

RFC Impact

To SAPIs

No impact. The changes are isolated to the OpenSSL stream wrapper implementation in ext openssl.

To Existing Extensions

No impact to other extensions. Changes are confined to ext openssl's stream transport implementation.

To the Ecosystem

Positive impacts:

Documentation needs:

Open Issues

None. All design questions have been resolved.

Future Scope

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.

Voting Choices

This is a simple yes/no vote requiring a 2/3 majority.

Add TLS session resumption support to streams as described in this RFC?
Real name Yes No Abstain
Final result: 0 0 0
This poll has been closed.

Patches and Tests

Implementation PR: https://github.com/php/php-src/pull/20296

Implementation

After the project is implemented, this section should contain:

  1. the version it was merged into
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature
  4. a link to the language specification section (if any)

References

Changelog