Commit b1242c32bca for php.net
commit b1242c32bcaff0b4766eaca7e1846c80687f7006
Author: Jakub Zelenka <bukka@php.net>
Date: Fri May 1 19:23:03 2026 +0200
Add OpenSSL TLS configurable session resumption support (#20296)
This adds support for various session options to the stream SSL context.
It allows setting a new session callback and session data on the client,
and get and remove session callbacks on the server. The server also offers
options to configure session cache parameters and the number of session
tickets. A new Openssl\Session class is introduced for session
import/export and introspection, along with Openssl\OpensslException
as the base exception for the extension.
RFC: https://wiki.php.net/rfc/tls_session_resumption_api
Closes GH-20296
diff --git a/NEWS b/NEWS
index 9d002531912..5453a5fc094 100644
--- a/NEWS
+++ b/NEWS
@@ -96,6 +96,8 @@ PHP NEWS
. Added AES-SIV support. (jordikroon)
. Implemented GH-20310 (No critical extension indication in
openssl_x509_parse() output). (StephenWall)
+ . Added TLS session resumption support for streams with new context options
+ and Openssl\Session class. (Jakub Zelenka)
- PDO_PGSQL:
. Clear session-local state disconnect-equivalent processing.
diff --git a/UPGRADING b/UPGRADING
index 4942b6e88ed..e55e03be48b 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -141,6 +141,15 @@ PHP 8.6 UPGRADE NOTES
. Added extra info about error location to the JSON error messages returned
from json_last_error_msg() and JsonException message.
+- OpenSSL:
+ . Added TLS session resumption support for streams with new stream context
+ options: session_data, session_new_cb, session_cache, session_cache_size,
+ session_timeout, session_id_context, session_get_cb, session_remove_cb,
+ and num_tickets. This allows saving and restoring client sessions across
+ requests, implementing custom server-side session storage, and controlling
+ session cache behavior.
+ RFC: https://wiki.php.net/rfc/tls_session_resumption
+
- Phar:
. Overriding the getMTime() and getPathname() methods of SplFileInfo now
influences the result of the phar buildFrom family of functions.
@@ -232,6 +241,11 @@ PHP 8.6 UPGRADE NOTES
7. New Classes and Interfaces
========================================
+- OpenSSL:
+ . Openssl\OpensslException
+ . Openssl\Session
+ RFC: https://wiki.php.net/rfc/tls_session_resumption
+
- Standard:
. enum SortDirection
RFC: https://wiki.php.net/rfc/sort_direction_enum
diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c
index 09440b2660a..c85ca073f44 100644
--- a/ext/openssl/openssl.c
+++ b/ext/openssl/openssl.c
@@ -56,6 +56,10 @@ ZEND_DECLARE_MODULE_GLOBALS(openssl)
#include "openssl_arginfo.h"
+/* OpenSSLException class */
+
+zend_class_entry *php_openssl_exception_ce;
+
/* OpenSSLCertificate class */
zend_class_entry *php_openssl_certificate_ce;
@@ -165,6 +169,302 @@ static void php_openssl_pkey_free_obj(zend_object *object)
zend_object_std_dtor(&key_object->std);
}
+/* OpenSSLSession class */
+
+zend_class_entry *php_openssl_session_ce;
+
+static zend_object_handlers php_openssl_session_object_handlers;
+
+bool php_openssl_is_session_ce(zval *val)
+{
+ return Z_TYPE_P(val) == IS_OBJECT && Z_OBJCE_P(val) == php_openssl_session_ce;
+}
+
+SSL_SESSION *php_openssl_session_from_zval(zval *zv)
+{
+ if (!php_openssl_is_session_ce(zv)) {
+ return NULL;
+ }
+ return Z_OPENSSL_SESSION_P(zv)->session;
+}
+
+void php_openssl_session_object_init(zval *zv, SSL_SESSION *session)
+{
+ object_init_ex(zv, php_openssl_session_ce);
+ php_openssl_session_object *obj = Z_OPENSSL_SESSION_P(zv);
+ obj->session = session;
+
+ unsigned int id_len = 0;
+ const unsigned char *id = SSL_SESSION_get_id(session, &id_len);
+ zend_update_property_stringl(php_openssl_session_ce, Z_OBJ_P(zv),
+ ZEND_STRL("id"), (char *)id, id_len);
+}
+
+static zend_object *php_openssl_session_create_object(zend_class_entry *class_type)
+{
+ php_openssl_session_object *intern = zend_object_alloc(sizeof(php_openssl_session_object), class_type);
+
+ zend_object_std_init(&intern->std, class_type);
+ object_properties_init(&intern->std, class_type);
+
+ return &intern->std;
+}
+
+static zend_function *php_openssl_session_get_constructor(zend_object *object)
+{
+ zend_throw_error(NULL,
+ "Cannot directly construct OpenSSLSession, use OpenSSLSession::import() or TLS session callbacks");
+ return NULL;
+}
+
+static void php_openssl_session_free_obj(zend_object *object)
+{
+ php_openssl_session_object *session_object = php_openssl_session_from_obj(object);
+
+ if (session_object->session) {
+ SSL_SESSION_free(session_object->session);
+ session_object->session = NULL;
+ }
+ zend_object_std_dtor(&session_object->std);
+}
+
+#define PHP_OPENSSL_SESSION_CHECK() \
+ php_openssl_session_object *obj = Z_OPENSSL_SESSION_P(ZEND_THIS); \
+ if (!obj->session) { \
+ zend_throw_exception(php_openssl_exception_ce, "Session is not valid", 0); \
+ RETURN_THROWS(); \
+ }
+
+PHP_METHOD(Openssl_Session, export)
+{
+ zend_long format = ENCODING_PEM;
+
+ ZEND_PARSE_PARAMETERS_START(0, 1)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_LONG(format)
+ ZEND_PARSE_PARAMETERS_END();
+
+ PHP_OPENSSL_SESSION_CHECK();
+
+ if (format == ENCODING_DER) {
+ int len = i2d_SSL_SESSION(obj->session, NULL);
+ if (len <= 0) {
+ zend_throw_exception(php_openssl_exception_ce, "Failed to export session", 0);
+ RETURN_THROWS();
+ }
+
+ zend_string *result = zend_string_alloc(len, 0);
+ unsigned char *p = (unsigned char *)ZSTR_VAL(result);
+ i2d_SSL_SESSION(obj->session, &p);
+ ZSTR_VAL(result)[len] = '\0';
+
+ RETURN_NEW_STR(result);
+ }
+
+ if (format == ENCODING_PEM) {
+ BIO *bio = BIO_new(BIO_s_mem());
+ if (!bio) {
+ zend_throw_exception(php_openssl_exception_ce, "Failed to create BIO", 0);
+ RETURN_THROWS();
+ }
+
+ if (!PEM_write_bio_SSL_SESSION(bio, obj->session)) {
+ BIO_free(bio);
+ zend_throw_exception(php_openssl_exception_ce, "Failed to export session as PEM", 0);
+ RETURN_THROWS();
+ }
+
+ char *data;
+ long len = BIO_get_mem_data(bio, &data);
+ zend_string *result = zend_string_init(data, len, 0);
+ BIO_free(bio);
+
+ RETURN_NEW_STR(result);
+ }
+
+ zend_argument_value_error(1, "must be OPENSSL_ENCODING_DER or OPENSSL_ENCODING_PEM");
+ RETURN_THROWS();
+}
+
+PHP_METHOD(Openssl_Session, import)
+{
+ zend_string *data;
+ zend_long format = ENCODING_PEM;
+
+ ZEND_PARSE_PARAMETERS_START(1, 2)
+ Z_PARAM_STR(data)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_LONG(format)
+ ZEND_PARSE_PARAMETERS_END();
+
+ SSL_SESSION *session = NULL;
+
+ if (format == ENCODING_DER) {
+ const unsigned char *p = (const unsigned char *)ZSTR_VAL(data);
+ session = d2i_SSL_SESSION(NULL, &p, ZSTR_LEN(data));
+ } else if (format == ENCODING_PEM) {
+ BIO *bio = BIO_new_mem_buf(ZSTR_VAL(data), ZSTR_LEN(data));
+ if (bio) {
+ session = PEM_read_bio_SSL_SESSION(bio, NULL, NULL, NULL);
+ BIO_free(bio);
+ }
+ } else {
+ zend_argument_value_error(2, "must be OPENSSL_ENCODING_DER or OPENSSL_ENCODING_PEM");
+ RETURN_THROWS();
+ }
+
+ if (!session) {
+ zend_throw_exception(php_openssl_exception_ce, "Failed to import session data", 0);
+ RETURN_THROWS();
+ }
+
+ php_openssl_session_object_init(return_value, session);
+}
+
+PHP_METHOD(Openssl_Session, isResumable)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+ PHP_OPENSSL_SESSION_CHECK();
+
+ RETURN_BOOL(SSL_SESSION_is_resumable(obj->session));
+}
+
+PHP_METHOD(Openssl_Session, getTimeout)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+ PHP_OPENSSL_SESSION_CHECK();
+ RETURN_LONG((zend_long)SSL_SESSION_get_timeout(obj->session));
+}
+
+PHP_METHOD(Openssl_Session, getCreatedAt)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+ PHP_OPENSSL_SESSION_CHECK();
+#if PHP_OPENSSL_API_VERSION >= 0x30300
+ RETURN_LONG((zend_long)SSL_SESSION_get_time_ex(obj->session));
+#else
+ RETURN_LONG((zend_long)SSL_SESSION_get_time(obj->session));
+#endif
+}
+
+PHP_METHOD(Openssl_Session, getProtocol)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+ PHP_OPENSSL_SESSION_CHECK();
+
+ int version = SSL_SESSION_get_protocol_version(obj->session);
+
+ switch (version) {
+ case TLS1_3_VERSION:
+ RETURN_STRING("TLSv1.3");
+ case TLS1_2_VERSION:
+ RETURN_STRING("TLSv1.2");
+ case TLS1_1_VERSION:
+ RETURN_STRING("TLSv1.1");
+ case TLS1_VERSION:
+ RETURN_STRING("TLSv1.0");
+ default:
+ RETURN_NULL();
+ }
+}
+
+PHP_METHOD(Openssl_Session, getCipher)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+ PHP_OPENSSL_SESSION_CHECK();
+
+ const SSL_CIPHER *cipher = SSL_SESSION_get0_cipher(obj->session);
+ if (!cipher) {
+ RETURN_NULL();
+ }
+
+ RETURN_STRING(SSL_CIPHER_get_name(cipher));
+}
+
+PHP_METHOD(Openssl_Session, hasTicket)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+ PHP_OPENSSL_SESSION_CHECK();
+
+ RETURN_BOOL(SSL_SESSION_has_ticket(obj->session));
+}
+
+PHP_METHOD(Openssl_Session, getTicketLifetimeHint)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+ PHP_OPENSSL_SESSION_CHECK();
+
+ if (!SSL_SESSION_has_ticket(obj->session)) {
+ RETURN_NULL();
+ }
+
+ RETURN_LONG((zend_long)SSL_SESSION_get_ticket_lifetime_hint(obj->session));
+}
+PHP_METHOD(Openssl_Session, __serialize)
+{
+ ZEND_PARSE_PARAMETERS_NONE();
+
+ PHP_OPENSSL_SESSION_CHECK();
+
+ BIO *bio = BIO_new(BIO_s_mem());
+ if (!bio) {
+ zend_throw_exception(php_openssl_exception_ce, "Failed to serialize session", 0);
+ RETURN_THROWS();
+ }
+
+ if (!PEM_write_bio_SSL_SESSION(bio, obj->session)) {
+ BIO_free(bio);
+ zend_throw_exception(php_openssl_exception_ce, "Failed to serialize session", 0);
+ RETURN_THROWS();
+ }
+
+ char *data;
+ long len = BIO_get_mem_data(bio, &data);
+ zend_string *pem = zend_string_init(data, len, 0);
+ BIO_free(bio);
+
+ array_init(return_value);
+ add_assoc_str(return_value, "pem", pem);
+}
+
+PHP_METHOD(Openssl_Session, __unserialize)
+{
+ HashTable *data;
+
+ ZEND_PARSE_PARAMETERS_START(1, 1)
+ Z_PARAM_ARRAY_HT(data)
+ ZEND_PARSE_PARAMETERS_END();
+
+ zval *pem_zv = zend_hash_str_find(data, ZEND_STRL("pem"));
+ if (!pem_zv || Z_TYPE_P(pem_zv) != IS_STRING) {
+ zend_throw_exception(php_openssl_exception_ce, "Invalid serialization data", 0);
+ RETURN_THROWS();
+ }
+
+ BIO *bio = BIO_new_mem_buf(Z_STRVAL_P(pem_zv), Z_STRLEN_P(pem_zv));
+ if (!bio) {
+ zend_throw_exception(php_openssl_exception_ce, "Failed to unserialize session", 0);
+ RETURN_THROWS();
+ }
+
+ SSL_SESSION *session = PEM_read_bio_SSL_SESSION(bio, NULL, NULL, NULL);
+ BIO_free(bio);
+
+ if (!session) {
+ zend_throw_exception(php_openssl_exception_ce, "Failed to unserialize session", 0);
+ RETURN_THROWS();
+ }
+
+ php_openssl_session_object *obj = Z_OPENSSL_SESSION_P(ZEND_THIS);
+ obj->session = session;
+
+ /* Populate id property */
+ unsigned int id_len = 0;
+ const unsigned char *id = SSL_SESSION_get_id(session, &id_len);
+ zend_update_property_stringl(php_openssl_session_ce, Z_OBJ_P(ZEND_THIS),
+ ZEND_STRL("id"), (char *)id, id_len);
+}
+
#if defined(HAVE_OPENSSL_ARGON2)
static const zend_module_dep openssl_deps[] = {
ZEND_MOD_REQUIRED("standard")
@@ -381,6 +681,8 @@ PHP_INI_END()
/* {{{ PHP_MINIT_FUNCTION */
PHP_MINIT_FUNCTION(openssl)
{
+ php_openssl_exception_ce = register_class_Openssl_OpensslException(zend_ce_exception);
+
php_openssl_certificate_ce = register_class_OpenSSLCertificate();
php_openssl_certificate_ce->create_object = php_openssl_certificate_create_object;
php_openssl_certificate_ce->default_object_handlers = &php_openssl_certificate_object_handlers;
@@ -414,6 +716,17 @@ PHP_MINIT_FUNCTION(openssl)
php_openssl_pkey_object_handlers.clone_obj = NULL;
php_openssl_pkey_object_handlers.compare = zend_objects_not_comparable;
+ php_openssl_session_ce = register_class_Openssl_Session();
+ php_openssl_session_ce->create_object = php_openssl_session_create_object;
+ php_openssl_session_ce->default_object_handlers = &php_openssl_session_object_handlers;
+
+ memcpy(&php_openssl_session_object_handlers, &std_object_handlers, sizeof(zend_object_handlers));
+ php_openssl_session_object_handlers.offset = offsetof(php_openssl_session_object, std);
+ php_openssl_session_object_handlers.free_obj = php_openssl_session_free_obj;
+ php_openssl_session_object_handlers.get_constructor = php_openssl_session_get_constructor;
+ php_openssl_session_object_handlers.clone_obj = NULL;
+ php_openssl_session_object_handlers.compare = zend_objects_not_comparable;
+
register_openssl_symbols(module_number);
php_openssl_backend_init();
diff --git a/ext/openssl/openssl.stub.php b/ext/openssl/openssl.stub.php
index 0111cc0cc7b..86dcc8f4f55 100644
--- a/ext/openssl/openssl.stub.php
+++ b/ext/openssl/openssl.stub.php
@@ -2,6 +2,46 @@
/** @generate-class-entries */
+namespace Openssl {
+
+ class OpensslException extends Exception
+ {
+ }
+
+ /**
+ * @strict-properties
+ */
+ final class Session
+ {
+ public readonly string $id;
+
+ public function export(int $format = OPENSSL_ENCODING_PEM): string {}
+
+ public static function import(string $data, int $format = OPENSSL_ENCODING_PEM): Session {}
+
+ public function isResumable(): bool {}
+
+ public function getTimeout(): int {}
+
+ public function getCreatedAt(): int {}
+
+ public function getProtocol(): ?string {}
+
+ public function getCipher(): ?string {}
+
+ public function hasTicket(): bool {}
+
+ public function getTicketLifetimeHint(): ?int {}
+
+ public function __serialize(): array {}
+
+ public function __unserialize(array $data): void {}
+ }
+
+}
+
+namespace {
+
/**
* @var string
* @cvalue OPENSSL_VERSION_TEXT
@@ -409,7 +449,6 @@
*/
const OPENSSL_ENCODING_PEM = UNKNOWN;
-
/**
* @strict-properties
* @not-serializable
@@ -699,3 +738,5 @@ function openssl_get_cert_locations(): array {}
function openssl_password_hash(string $algo, #[\SensitiveParameter] string $password, array $options = []): string {}
function openssl_password_verify(string $algo, #[\SensitiveParameter] string $password, string $hash): bool {}
#endif
+
+}
diff --git a/ext/openssl/openssl_arginfo.h b/ext/openssl/openssl_arginfo.h
index 32002cd81d5..851ba2e913b 100644
Binary files a/ext/openssl/openssl_arginfo.h and b/ext/openssl/openssl_arginfo.h differ
diff --git a/ext/openssl/php_openssl.h b/ext/openssl/php_openssl.h
index df5ee3f7cd6..72fd98745e1 100644
--- a/ext/openssl/php_openssl.h
+++ b/ext/openssl/php_openssl.h
@@ -30,8 +30,10 @@ extern zend_module_entry openssl_module_entry;
#define PHP_OPENSSL_API_VERSION 0x10100
#elif OPENSSL_VERSION_NUMBER < 0x30200000L
#define PHP_OPENSSL_API_VERSION 0x30000
-#else
+#elif OPENSSL_VERSION_NUMBER < 0x30300000L
#define PHP_OPENSSL_API_VERSION 0x30200
+#else
+#define PHP_OPENSSL_API_VERSION 0x30300
#endif
#define PHP_OPENSSL_ERR_BUFFER_SIZE 16
@@ -201,6 +203,28 @@ static inline php_openssl_pkey_object *php_openssl_pkey_from_obj(zend_object *ob
bool php_openssl_is_pkey_ce(zval *val);
void php_openssl_pkey_object_init(zval *zv, EVP_PKEY *pkey, bool is_private);
+/* OpenSSLSession class */
+
+#include <openssl/ssl.h>
+
+typedef struct _php_openssl_session_object {
+ SSL_SESSION *session;
+ zend_object std;
+} php_openssl_session_object;
+
+static inline php_openssl_session_object *php_openssl_session_from_obj(zend_object *obj) {
+ return (php_openssl_session_object *)((char *)(obj) - offsetof(php_openssl_session_object, std));
+}
+
+#define Z_OPENSSL_SESSION_P(zv) php_openssl_session_from_obj(Z_OBJ_P(zv))
+
+/* Extern declarations for xp_ssl.c */
+extern zend_class_entry *php_openssl_session_ce;
+
+void php_openssl_session_object_init(zval *zv, SSL_SESSION *session);
+bool php_openssl_is_session_ce(zval *val);
+SSL_SESSION *php_openssl_session_from_zval(zval *zv);
+
#if defined(HAVE_OPENSSL_ARGON2)
/**
diff --git a/ext/openssl/tests/ServerClientTestCase.inc b/ext/openssl/tests/ServerClientTestCase.inc
index 8eedbfdebee..f0336fdd392 100644
--- a/ext/openssl/tests/ServerClientTestCase.inc
+++ b/ext/openssl/tests/ServerClientTestCase.inc
@@ -179,6 +179,9 @@ class ServerClientTestCase
if (empty($addr)) {
throw new \Exception("Failed server start");
}
+ if (strpos($addr, 'SERVER_EXCEPTION') !== false) {
+ echo $addr;
+ }
if ($code === false) {
$clientCode = preg_replace('/{{\s*ADDR\s*}}/', $addr, $clientCode);
} else {
diff --git a/ext/openssl/tests/session_resumption_cache_disabled.phpt b/ext/openssl/tests/session_resumption_cache_disabled.phpt
new file mode 100644
index 00000000000..9e0e8a82f39
--- /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--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_cache_disabled.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+ $ctx = stream_context_create(['ssl' => [
+ '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);
+ }
+ }
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $globalSession = null;
+
+ $flags = STREAM_CLIENT_CONNECT;
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'session_new_cb' => function($stream, $session) use (&$globalSession) {
+ $globalSession = $session;
+ }
+ ]]);
+
+ /* First connection */
+ $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx);
+ if ($client1) {
+ echo trim(fgets($client1)) . "\n";
+ $meta1 = stream_get_meta_data($client1);
+ echo "First connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ fclose($client1);
+ }
+
+ /* Second connection - server won't use cached session */
+ $ctx2 = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'session_data' => $globalSession,
+ ]]);
+
+ $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx2);
+ if ($client2) {
+ echo trim(fgets($client2)) . "\n";
+ $meta2 = stream_get_meta_data($client2);
+ echo "Second connection resumed: " . ($meta2['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ fclose($client2);
+ }
+CODE;
+
+include 'CertificateGenerator.inc';
+$certificateGenerator = new CertificateGenerator();
+$certificateGenerator->saveNewCertAsFileWithKey('session_disabled_test', $certFile);
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_cache_disabled.pem.tmp');
+?>
+--EXPECT--
+No cache connection 1
+First connection resumed: no
+No cache connection 2
+Second connection resumed: no
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 00000000000..ee1d126a6d9
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_client_basic.phpt
@@ -0,0 +1,89 @@
+--TEST--
+TLS session resumption - client basic resumption
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_resumption_client.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'session_cache' => true,
+ 'session_id_context' => 'test-basic',
+ ]]);
+
+ $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx);
+ 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 = '';
+
+ $flags = STREAM_CLIENT_CONNECT;
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'session_new_cb' => function($stream, $session) use (&$sessionData) {
+ $sessionData = $session;
+ }
+ ]]);
+
+ /* First connection - full handshake */
+ $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx);
+ if ($client1) {
+ echo trim(fgets($client1)) . "\n";
+ $meta1 = stream_get_meta_data($client1);
+ echo "First connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ echo "Session data received: " . (!empty($sessionData) ? "yes" : "no") . "\n";
+ fclose($client1);
+ }
+
+ /* 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) {
+ echo trim(fgets($client2)) . "\n";
+ $meta2 = stream_get_meta_data($client2);
+ echo "Second connection resumed: " . ($meta2['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ fclose($client2);
+ }
+CODE;
+
+include 'CertificateGenerator.inc';
+$certificateGenerator = new CertificateGenerator();
+$certificateGenerator->saveNewCertAsFileWithKey('session_resumption_test', $certFile);
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_resumption_client.pem.tmp');
+?>
+--EXPECT--
+Hello from server
+First connection resumed: no
+Session data received: yes
+Hello from server
+Second connection resumed: yes
diff --git a/ext/openssl/tests/session_resumption_get_cb_no_ticket.phpt b/ext/openssl/tests/session_resumption_get_cb_no_ticket.phpt
new file mode 100644
index 00000000000..f87f831a785
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_get_cb_no_ticket.phpt
@@ -0,0 +1,79 @@
+--TEST--
+TLS session resumption - warning when trying to enable tickets with session_get_cb
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_no_ticket.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+
+ /* Trying to enable tickets with external cache - should warn */
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'session_id_context' => 'test-app',
+ 'no_ticket' => false, // Explicitly trying to enable tickets
+ 'session_new_cb' => function($stream, $sessionData) {
+ // Store session
+ },
+ 'session_get_cb' => function($stream, $sessionId) {
+ return null;
+ }
+ ]]);
+
+ try {
+ $server = @stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx);
+ phpt_notify_server_start($server);
+
+ $client = @stream_socket_accept($server, 30);
+ if ($client === false) {
+ phpt_notify(message: "SERVER_FAILED_UNEXPECTEDLY");
+ } else {
+ phpt_notify(message: "SERVER_CREATED_UNEXPECTEDLY");
+ fclose($server);
+ }
+ } catch (\Throwable $e) {
+ phpt_notify(message: "SERVER_EXCEPTION: " . $e->getMessage());
+ }
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$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--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_no_ticket.pem.tmp');
+?>
+--EXPECT--
+SERVER_EXCEPTION: Session tickets cannot be enabled when session_get_cb is setConnection 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 00000000000..5563b0c22cf
--- /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--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_num_tickets.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+
+ // Test with num_tickets = 3 (should issue 3 tickets)
+ $ctx = stream_context_create(['ssl' => [
+ '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, $session) 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--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_num_tickets.pem.tmp');
+?>
+--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 00000000000..13654cd451e
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt
@@ -0,0 +1,112 @@
+--TEST--
+TLS session resumption - num_tickets = 0 disables tickets, forces session IDs
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_no_tickets_zero.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $sessionStore = [];
+ $newCbCalled = 0;
+
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'session_id_context' => 'test-no-tickets',
+ 'num_tickets' => 0, // Disable ticket issuance
+ 'session_new_cb' => function($stream, $session) use (&$sessionStore, &$newCbCalled) {
+ $key = bin2hex($session->id);
+ $sessionStore[$key] = $session;
+ $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, $session) use (&$sessionData, &$clientTickets) {
+ $sessionData = $session;
+ $clientTickets++;
+ }
+ ]]);
+
+ // First connection
+ $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx);
+ if ($client1) {
+ $meta1 = stream_get_meta_data($client1);
+ echo "Client first connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ echo trim(fgets($client1)) . "\n";
+ usleep(100000); // Wait for session storage
+ fclose($client1);
+ }
+
+ 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) {
+ $meta2 = stream_get_meta_data($client2);
+ echo "Client second connection resumed: " . ($meta2['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ echo trim(fgets($client2)) . "\n";
+ fclose($client2);
+ }
+
+ $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--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_no_tickets_zero.pem.tmp');
+?>
+--EXPECT--
+Client first connection resumed: no
+Response 1
+Client received tickets on first connection: 0
+Client second connection resumed: no
+Response 2
+Server: NEW_CB_CALLS:0
diff --git a/ext/openssl/tests/session_resumption_import_export_session.phpt b/ext/openssl/tests/session_resumption_import_export_session.phpt
new file mode 100644
index 00000000000..0d9a3274b11
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_import_export_session.phpt
@@ -0,0 +1,98 @@
+--TEST--
+TLS session resumption - import and export session
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_resumption_serialize.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'session_cache' => true,
+ 'session_id_context' => 'test-basic',
+ ]]);
+
+ $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx);
+ phpt_notify_server_start($server);
+
+ /* Accept single connections */
+ $client = @stream_socket_accept($server, 30);
+ if ($client) {
+ fwrite($client, "Hello from server\n");
+ fclose($client);
+ }
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $sessionData = '';
+
+ $flags = STREAM_CLIENT_CONNECT;
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'session_new_cb' => function($stream, $session) use (&$sessionData) {
+ if (empty($sessionData)) {
+ // default should be pem
+ $pemSessionData = $session->export();
+ var_dump($pemSessionData);
+ $session = Openssl\Session::import($pemSessionData);
+ $pemSessionData = $session->export(OPENSSL_ENCODING_PEM);
+ var_dump($pemSessionData);
+ $session = Openssl\Session::import($pemSessionData, OPENSSL_ENCODING_PEM);
+ $derSessionData = $session->export(OPENSSL_ENCODING_DER);
+ var_dump(strlen($derSessionData) > 0);
+ var_dump(strpos($derSessionData, 'BEGIN SSL SESSION PARAMETERS') === false);
+ $session = Openssl\Session::import($derSessionData, OPENSSL_ENCODING_DER);
+ var_dump($session);
+ }
+ $sessionData = $session;
+ }
+ ]]);
+
+ /* First connection - full handshake */
+ $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx);
+ if ($client1) {
+ echo trim(fgets($client1)) . "\n";
+ $meta1 = stream_get_meta_data($client1);
+ echo "First connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ echo "Session data received: " . (!empty($sessionData) ? "yes" : "no") . "\n";
+ fclose($client1);
+ }
+CODE;
+
+include 'CertificateGenerator.inc';
+$certificateGenerator = new CertificateGenerator();
+$certificateGenerator->saveNewCertAsFileWithKey('session_resumption_test', $certFile);
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_resumption_serialize.pem.tmp');
+?>
+--EXPECTF--
+string(%d) "-----BEGIN SSL SESSION PARAMETERS-----
+%a
+-----END SSL SESSION PARAMETERS-----
+"
+string(%d) "-----BEGIN SSL SESSION PARAMETERS-----
+%a
+-----END SSL SESSION PARAMETERS-----
+"
+bool(true)
+bool(true)
+object(Openssl\Session)#%d (1) {
+ ["id"]=>
+ string(32) "%a"
+}
+Hello from server
+First connection resumed: no
+Session data received: yes
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 00000000000..b6cfc90a055
--- /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--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_invalid_cb.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+ $ctx = stream_context_create(['ssl' => [
+ '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--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_invalid_cb.pem.tmp');
+?>
+--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 00000000000..7e50b9235a2
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_invalid_data.phpt
@@ -0,0 +1,66 @@
+--TEST--
+TLS session resumption - invalid session data is fatal
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_invalid.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+ $ctx = stream_context_create(['ssl' => [
+ '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',
+ ]]);
+
+ try {
+ $client = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx);
+
+ if ($client === false) {
+ echo "Connection failed unexpectedlyd\n";
+ }
+ } catch (\Throwable $e) {
+ echo "Type error thrown: " . $e->getMessage() . "\n";
+ }
+CODE;
+
+include 'CertificateGenerator.inc';
+$certificateGenerator = new CertificateGenerator();
+$certificateGenerator->saveNewCertAsFileWithKey('session_invalid_test', $certFile);
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_invalid.pem.tmp');
+?>
+--EXPECTF--
+
+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
+Type error thrown: session_data must be an OpenSSLSession instance
diff --git a/ext/openssl/tests/session_resumption_invalid_session_import.phpt b/ext/openssl/tests/session_resumption_invalid_session_import.phpt
new file mode 100644
index 00000000000..a9c5b65f205
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_invalid_session_import.phpt
@@ -0,0 +1,23 @@
+--TEST--
+TLS session resumption - invalid session import
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+
+try {
+ Openssl\Session::import('invalid');
+} catch (Openssl\OpensslException $e) {
+ echo $e->getMessage() . "\n";
+}
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_cache_disabled.pem.tmp');
+?>
+--EXPECT--
+Failed to import session data
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 00000000000..ee2852c54ad
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_new_cb_no_context.phpt
@@ -0,0 +1,78 @@
+--TEST--
+TLS session resumption - warning when session_new_cb without session_id_context and verify_peer enabled
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_no_context_verify.pem.tmp';
+$caCertFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_no_context_ca.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+
+ /* session_new_cb without session_id_context, with verify_peer - should warn */
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'verify_peer' => true,
+ 'cafile' => '%s',
+ 'session_new_cb' => function($stream, $session) {
+ echo "not called new_cb\n";
+ },
+ 'session_get_cb' => function($stream, $sessionId) {
+ echo "not called new_cb\n";
+ return null;
+ }
+ /* Missing: 'session_id_context' => 'myapp' */
+ ]]);
+
+ try {
+ $server = @stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx);
+ phpt_notify_server_start($server);
+
+ $client = @stream_socket_accept($server, 30);
+ if ($client === false) {
+ phpt_notify(message: "SERVER_FAILED_UNEXPECTEDLY");
+ } else {
+ phpt_notify(message: "SERVER_CREATED_UNEXPECTEDLY");
+ fclose($server);
+ }
+ } catch (\Throwable $e) {
+ phpt_notify(message: "SERVER_EXCEPTION: " . $e->getMessage());
+ }
+CODE;
+$serverCode = sprintf($serverCode, $certFile, $caCertFile);
+
+$clientCode = <<<'CODE'
+ $flags = STREAM_CLIENT_CONNECT;
+
+ /* Try to use corrupted session data */
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false
+ ]]);
+
+ $client = @stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx);
+
+ $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--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_no_context_verify.pem.tmp');
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_no_context_ca.pem.tmp');
+?>
+--EXPECT--
+SERVER_EXCEPTION: session_id_context must be set if session_new_cb is set
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 00000000000..835e9bb5164
--- /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--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_persistent.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+ $ctx = stream_context_create(['ssl' => [
+ '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, $session) {
+ 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--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_persistent.pem.tmp');
+?>
+--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 00000000000..a08408e2d90
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_require_new_cb.phpt
@@ -0,0 +1,73 @@
+--TEST--
+TLS session resumption - server requires session_new_cb with session_get_cb
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_require_new_cb.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+
+ /* Provide session_get_cb without session_new_cb - should fail */
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'session_get_cb' => function($stream, $sessionId) {
+ return null;
+ }
+ ]]);
+
+ try {
+ $server = @stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx);
+ phpt_notify_server_start($server);
+
+ $client = @stream_socket_accept($server, 30);
+ if ($client === false) {
+ phpt_notify(message: "SERVER_FAILED_UNEXPECTEDLY");
+ } else {
+ phpt_notify(message: "SERVER_CREATED_UNEXPECTEDLY");
+ fclose($server);
+ }
+ } catch (\Throwable $e) {
+ phpt_notify(message: "SERVER_EXCEPTION: " . $e->getMessage());
+ }
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$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--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_require_new_cb.pem.tmp');
+?>
+--EXPECT--
+SERVER_EXCEPTION: session_new_cb is required when session_get_cb is providedConnection failed as expected
diff --git a/ext/openssl/tests/session_resumption_serialize_session.phpt b/ext/openssl/tests/session_resumption_serialize_session.phpt
new file mode 100644
index 00000000000..f2f3c98e5e5
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_serialize_session.phpt
@@ -0,0 +1,84 @@
+--TEST--
+TLS session resumption - serialize session
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_resumption_serialize.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'session_cache' => true,
+ 'session_id_context' => 'test-basic',
+ ]]);
+
+ $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx);
+ phpt_notify_server_start($server);
+
+ /* Accept single connections */
+ $client = @stream_socket_accept($server, 30);
+ if ($client) {
+ fwrite($client, "Hello from server\n");
+ fclose($client);
+ }
+CODE;
+$serverCode = sprintf($serverCode, $certFile);
+
+$clientCode = <<<'CODE'
+ $sessionData = '';
+
+ $flags = STREAM_CLIENT_CONNECT;
+ $ctx = stream_context_create(['ssl' => [
+ 'verify_peer' => false,
+ 'verify_peer_name' => false,
+ 'session_new_cb' => function($stream, $session) use (&$sessionData) {
+ if (empty($sessionData)) {
+ $serializedSessionData = serialize($session);
+ var_dump($serializedSessionData);
+ $session = unserialize($serializedSessionData);
+ var_dump($session);
+ }
+ $sessionData = $session;
+ }
+ ]]);
+
+ /* First connection - full handshake */
+ $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx);
+ if ($client1) {
+ echo trim(fgets($client1)) . "\n";
+ $meta1 = stream_get_meta_data($client1);
+ echo "First connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ echo "Session data received: " . (!empty($sessionData) ? "yes" : "no") . "\n";
+ fclose($client1);
+ }
+CODE;
+
+include 'CertificateGenerator.inc';
+$certificateGenerator = new CertificateGenerator();
+$certificateGenerator->saveNewCertAsFileWithKey('session_resumption_test', $certFile);
+
+include 'ServerClientTestCase.inc';
+ServerClientTestCase::getInstance()->run($clientCode, $serverCode);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_resumption_serialize.pem.tmp');
+?>
+--EXPECTF--
+string(%d) "O:15:"Openssl\Session":1:{s:3:"pem";s:%d:"-----BEGIN SSL SESSION PARAMETERS-----
+%a
+-----END SSL SESSION PARAMETERS-----
+";}"
+object(Openssl\Session)#9 (1) {
+ ["id"]=>
+%a
+}
+Hello from server
+First connection resumed: no
+Session data received: yes
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 00000000000..02c6a7dcfdc
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt
@@ -0,0 +1,116 @@
+--TEST--
+TLS session resumption - server external cache callbacks with context id
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_external_proper.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $sessionStore = [];
+ $newCbCalled = false;
+ $getCbCalled = false;
+
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'session_id_context' => 'test-server',
+ 'session_new_cb' => function($stream, $session) use (&$sessionStore, &$newCbCalled) {
+ $key = bin2hex($session->id);
+ $sessionStore[$key] = $session;
+ $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, $session) use (&$sessionData) {
+ $sessionData = $session;
+ }
+ ]]);
+
+ /* First connection */
+ $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx);
+ if ($client1) {
+ $meta1 = stream_get_meta_data($client1);
+ echo "Client first connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ echo trim(fgets($client1)) . "\n";
+ fclose($client1);
+ }
+
+ 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) {
+ $meta2 = stream_get_meta_data($client2);
+ echo "Client second connection resumed: " . ($meta2['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ echo trim(fgets($client2)) . "\n";
+ fclose($client2);
+ }
+
+ /* 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--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_external_proper.pem.tmp');
+?>
+--EXPECTF--
+Client first connection resumed: no
+Response 1
+Session captured: YES
+Client second connection resumed: yes
+Response 2
+Server: NEW_CB_CALLED,GET_CB_CALLED,SESSIONS:3
diff --git a/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt b/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt
new file mode 100644
index 00000000000..bdc3d2ce1bf
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_server_external_with_context_id_tls12.phpt
@@ -0,0 +1,116 @@
+--TEST--
+TLS session resumption - server external cache callbacks with context id for TLS 1.2
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_external_proper.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $sessionStore = [];
+ $newCbCalled = false;
+ $getCbCalled = false;
+
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'session_id_context' => 'test-server', // Proper configuration
+ 'session_new_cb' => function($stream, $session) use (&$sessionStore, &$newCbCalled) {
+ $key = bin2hex($session->id);
+ $sessionStore[$key] = $session;
+ $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, $session) use (&$sessionData) {
+ $sessionData = $session;
+ }
+ ]]);
+
+ /* First connection */
+ $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx);
+ if ($client1) {
+ $meta1 = stream_get_meta_data($client1);
+ echo "Client first connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ echo trim(fgets($client1)) . "\n";
+ fclose($client1);
+ }
+
+ 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) {
+ $meta2 = stream_get_meta_data($client2);
+ echo "Client second connection resumed: " . ($meta2['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ echo trim(fgets($client2)) . "\n";
+ fclose($client2);
+ }
+
+ /* 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--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_external_proper.pem.tmp');
+?>
+--EXPECTF--
+Client first connection resumed: no
+Response 1
+Session captured: YES
+Client second connection resumed: yes
+Response 2
+Server: NEW_CB_CALLED,GET_CB_CALLED,SESSIONS:1
diff --git a/ext/openssl/tests/session_resumption_server_external_with_no_verify.phpt b/ext/openssl/tests/session_resumption_server_external_with_no_verify.phpt
new file mode 100644
index 00000000000..28bb97faff2
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_server_external_with_no_verify.phpt
@@ -0,0 +1,121 @@
+--TEST--
+TLS session resumption - server external cache callbacks with no verify
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_resumption_server.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $sessionStore = [];
+ $newCbCalled = false;
+ $getCbCalled = false;
+ $removeCbCalled = false;
+
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'verify_peer' => false,
+ 'no_ticket' => true,
+ 'session_cache' => true,
+ 'session_new_cb' => function($stream, $session) use (&$sessionStore, &$newCbCalled) {
+ $key = bin2hex($session->id);
+ $sessionStore[$key] = $session;
+ $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, $session) use (&$sessionData) {
+ $sessionData = $session;
+ }
+ ]]);
+
+ /* First connection */
+ $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx);
+ if ($client1) {
+ $meta1 = stream_get_meta_data($client1);
+ echo "Client first connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ echo trim(fgets($client1)) . "\n";
+ fclose($client1);
+ }
+
+ /* 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) {
+ $meta2 = stream_get_meta_data($client2);
+ echo "Client second connection resumed: " . ($meta2['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ echo trim(fgets($client2)) . "\n";
+ fclose($client2);
+ }
+
+ /* 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--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_resumption_server.pem.tmp');
+?>
+--EXPECTF--
+Client first connection resumed: no
+Response 1
+Client second connection resumed: yes
+Response 2
+Server callbacks: NEW_CB_CALLED,GET_CB_CALLED
diff --git a/ext/openssl/tests/session_resumption_server_internal.phpt b/ext/openssl/tests/session_resumption_server_internal.phpt
new file mode 100644
index 00000000000..d7c2633601e
--- /dev/null
+++ b/ext/openssl/tests/session_resumption_server_internal.phpt
@@ -0,0 +1,99 @@
+--TEST--
+TLS session resumption - server internal cache
+--EXTENSIONS--
+openssl
+--SKIPIF--
+<?php
+if (!function_exists("proc_open")) die("skip no proc_open");
+?>
+--FILE--
+<?php
+$certFile = __DIR__ . DIRECTORY_SEPARATOR . 'session_internal_cache.pem.tmp';
+
+$serverCode = <<<'CODE'
+ $flags = STREAM_SERVER_BIND|STREAM_SERVER_LISTEN;
+ $ctx = stream_context_create(['ssl' => [
+ 'local_cert' => '%s',
+ 'session_id_context' => 'test-server',
+ 'session_cache' => true,
+ 'session_cache_size' => 1024,
+ 'session_timeout' => 300,
+ ]]);
+
+ $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, $session) use (&$sessionData) {
+ $sessionData = $session;
+ }
+ ]]);
+
+ /* First connection */
+ $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx);
+ if ($client1) {
+ $meta1 = stream_get_meta_data($client1);
+ echo "Client first connection resumed: " . ($meta1['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ echo trim(fgets($client1)) . "\n";
+ fclose($client1);
+ }
+
+ echo "Session data received: " . ($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) {
+ $meta2 = stream_get_meta_data($client2);
+ echo "Client second connection resumed: " . ($meta2['crypto']['session_reused'] ? "yes" : "no") . "\n";
+ echo trim(fgets($client2)) . "\n";
+ fclose($client2);
+ }
+
+ /* 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--
+<?php
+@unlink(__DIR__ . DIRECTORY_SEPARATOR . 'session_internal_cache.pem.tmp');
+?>
+--EXPECTF--
+Client first connection resumed: no
+Connection 1
+Session data received: YES
+Client second connection resumed: yes
+Connection 2
+SERVER_DONE
diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c
index 64f49ca4538..ed72ca49677 100644
--- a/ext/openssl/xp_ssl.c
+++ b/ext/openssl/xp_ssl.c
@@ -176,6 +176,14 @@ typedef struct _php_openssl_alpn_ctx_t {
} php_openssl_alpn_ctx;
#endif
+/* Holds session callback */
+typedef struct _php_openssl_session_callbacks_t {
+ int refcount;
+ zval new_cb;
+ zval get_cb;
+ zval remove_cb;
+} 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!
@@ -195,6 +203,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;
@@ -1547,37 +1556,404 @@ static int php_openssl_server_alpn_callback(SSL *ssl_handle,
#endif
-static zend_result php_openssl_setup_crypto(php_stream *stream,
- php_openssl_netstream_data_t *sslsock,
- php_stream_xport_crypto_param *cparam) /* {{{ */
+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;
+ }
+
+ /* Increment reference - we're giving ownership to the PHP object */
+ SSL_SESSION_up_ref(session);
+
+ zval args[2];
+ zval retval;
+
+ ZVAL_RES(&args[0], stream->res);
+ php_openssl_session_object_init(&args[1], session);
+
+ if (call_user_function(EG(function_table), NULL, &sslsock->session_callbacks->new_cb,
+ &retval, 2, args) == SUCCESS) {
+ zval_ptr_dtor(&retval);
+ }
+
+ zval_ptr_dtor(&args[1]);
+
+ 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 (php_openssl_is_session_ce(&retval)) {
+ /* Get session from object and increment ref since OpenSSL will own it */
+ php_openssl_session_object *obj = Z_OPENSSL_SESSION_P(&retval);
+ if (obj->session) {
+ SSL_SESSION_up_ref(obj->session);
+ session = obj->session;
+ }
+ zval_ptr_dtor(&retval);
+ } else if (Z_TYPE(retval) != IS_NULL) {
+ zend_type_error("session_get_cb return type must be null or OpenSSLSession");
+ return NULL;
+ }
+ }
+
+ zval_ptr_dtor(&args[1]);
+
+ *copy = 0;
+ 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);
+ sslsock->session_callbacks->refcount = 1;
+ }
+
+ 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)
{
- const SSL_METHOD *method;
- int ssl_ctx_options;
- int method_flags;
- zend_long min_version = 0;
- zend_long max_version = 0;
- char *cipherlist = NULL;
- char *alpn_protocols = NULL;
zval *val;
- bool verify_peer = false;
+ bool enable_client_cache = false;
+ bool is_persistent = php_stream_is_persistent(stream);
+
+ if (GET_VER_OPT("session_data")) {
+ if (php_openssl_is_session_ce(val)) {
+ enable_client_cache = true;
+ } else if (Z_TYPE_P(val) != IS_NULL) {
+ zend_type_error("session_data must be an OpenSSLSession instance");
+ return FAILURE;
+ }
+ }
- if (sslsock->ssl_handle) {
- if (sslsock->s.is_blocked) {
- php_error_docref(NULL, E_WARNING, "SSL/TLS already set-up for this 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;
+ }
+
+ if (enable_client_cache) {
+ SSL_CTX_set_session_cache_mode(sslsock->ctx,
+ SSL_SESS_CACHE_CLIENT | SSL_SESS_CACHE_NO_INTERNAL);
+ }
+
+ return SUCCESS;
+}
+
+static bool php_openssl_is_session_cache_enabled(php_stream *stream, bool internal_only)
+{
+ zval *val;
+
+ if (GET_VER_OPT("session_cache")) {
+ return zend_is_true(val);
+ }
+
+ if (internal_only) {
+ return false;
+ }
+
+ return GET_VER_OPT("session_get_cb");
+}
+
+/**
+ * Configure session resumption options for server connections
+ */
+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_id_context = 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_id_context")) {
+ if (Z_TYPE_P(val) != IS_STRING || Z_STRLEN_P(val) == 0) {
+ zend_type_error("session_id_context must be a non empty string");
+ return FAILURE;
+ }
+ SSL_CTX_set_session_id_context(sslsock->ctx, (const unsigned char *) Z_STRVAL_P(val),
+ Z_STRLEN_P(val));
+ has_session_id_context = 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_id_context &&
+ (SSL_CTX_get_verify_mode(sslsock->ctx) & SSL_VERIFY_PEER) != 0) {
+ zend_value_error("session_id_context must be set if session_new_cb is set");
+ return FAILURE;
+ }
+ }
+
+ /* Validate: if session_get_cb is provided, session_new_cb is required */
+ if (has_get_cb && !has_new_cb) {
+ zend_value_error("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)) {
+ zend_value_error("Session tickets cannot be enabled when session_get_cb is set");
+ }
+ } else if (php_openssl_is_session_cache_enabled(stream, true)) {
+ if (!has_session_id_context &&
+ (SSL_CTX_get_verify_mode(sslsock->ctx) & SSL_VERIFY_PEER) != 0) {
+ zend_value_error("session_id_context must be set for internal session cache");
+ }
+
+ /* Internal cache mode */
+ SSL_CTX_set_session_cache_mode(sslsock->ctx, SSL_SESS_CACHE_SERVER);
+
+ /* 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 {
+ zend_value_error("session_cache_size must be positive");
+ }
} else {
- return SUCCESS;
+ /* 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 {
+ zend_value_error("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);
}
- ERR_clear_error();
+ return SUCCESS;
+}
- /* We need to do slightly different things based on client/server method
- * so let's remember which method was selected */
- sslsock->is_client = cparam->inputs.method & STREAM_CRYPTO_IS_CLIENT;
- method_flags = cparam->inputs.method & ~STREAM_CRYPTO_IS_CLIENT;
+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")) {
+ SSL_SESSION *session = NULL;
+ bool needs_free = false;
+
+ if (php_openssl_is_session_ce(val)) {
+ session = php_openssl_session_from_zval(val);
+ if (!session) {
+ php_error_docref(NULL, E_WARNING,
+ "Invalid OpenSSLSession object, falling back to full handshake");
+ return FAILURE;
+ }
+ /* Object owns the session, we just borrow it */
+ needs_free = false;
+ } else if (Z_TYPE_P(val) != IS_NULL) {
+ zend_type_error("session_data must be an OpenSSLSession instance");
+ return FAILURE;
+ }
+
+ if (session) {
+ 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");
+ if (needs_free) {
+ SSL_SESSION_free(session);
+ }
+ ERR_clear_error();
+ return FAILURE;
+ }
+
+ if (needs_free) {
+ SSL_SESSION_free(session);
+ }
+ }
+ }
+
+ return SUCCESS;
+}
+
+static zend_result php_openssl_create_server_ctx(php_stream *stream,
+ php_openssl_netstream_data_t *sslsock, int method_flags)
+{
+ zval *val;
- method = sslsock->is_client ? SSLv23_client_method() : SSLv23_server_method();
+ const SSL_METHOD *method = sslsock->is_client ? SSLv23_client_method() : SSLv23_server_method();
sslsock->ctx = SSL_CTX_new(method);
if (sslsock->ctx == NULL) {
@@ -1585,14 +1961,24 @@ 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);
+
+ zend_long min_version = 0;
+ zend_long max_version = 0;
GET_VER_OPT_LONG("min_proto_version", min_version);
GET_VER_OPT_LONG("max_proto_version", max_version);
method_flags = php_openssl_get_proto_version_flags(method_flags, min_version, max_version);
- ssl_ctx_options = SSL_OP_ALL;
+ int ssl_ctx_options = SSL_OP_ALL;
if (GET_VER_OPT("no_ticket") && zend_is_true(val)) {
ssl_ctx_options |= SSL_OP_NO_TICKET;
}
+ 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;
@@ -1605,6 +1991,7 @@ static zend_result php_openssl_setup_crypto(php_stream *stream,
ssl_ctx_options |= SSL_OP_NO_COMPRESSION;
}
+ bool verify_peer = false;
if (GET_VER_OPT("verify_peer") && !zend_is_true(val)) {
php_openssl_disable_peer_verification(sslsock->ctx, stream);
} else {
@@ -1620,6 +2007,7 @@ static zend_result php_openssl_setup_crypto(php_stream *stream,
SSL_CTX_set_default_passwd_cb(sslsock->ctx, php_openssl_passwd_callback);
}
+ char *cipherlist = NULL;
GET_VER_OPT_STRING("ciphers", cipherlist);
#ifndef USE_OPENSSL_SYSTEM_CIPHERS
if (!cipherlist) {
@@ -1642,6 +2030,7 @@ static zend_result php_openssl_setup_crypto(php_stream *stream,
#endif
}
+ char *alpn_protocols = NULL;
GET_VER_OPT_STRING("alpn_protocols", alpn_protocols);
if (alpn_protocols) {
#ifdef HAVE_TLS_ALPN
@@ -1680,10 +2069,96 @@ static zend_result php_openssl_setup_crypto(php_stream *stream,
SSL_CTX_set_min_proto_version(sslsock->ctx, php_openssl_get_min_proto_version(method_flags));
SSL_CTX_set_max_proto_version(sslsock->ctx, php_openssl_get_max_proto_version(method_flags));
- if (sslsock->is_client == 0 &&
- PHP_STREAM_CONTEXT(stream) &&
- FAILURE == php_openssl_set_server_specific_opts(stream, sslsock->ctx)
- ) {
+ if (sslsock->is_client) {
+ /* Setup client session resumption */
+ if (FAILURE == php_openssl_setup_client_session(stream, sslsock)) {
+ return FAILURE;
+ }
+ } else 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;
+ }
+ }
+
+#ifdef HAVE_TLS_SNI
+ /* Enable server-side SNI */
+ if (!sslsock->is_client && php_openssl_enable_server_sni(stream, sslsock, verify_peer) == FAILURE) {
+ return FAILURE;
+ }
+#endif
+
+ return SUCCESS;
+}
+
+static zend_result php_openssl_setup_crypto(php_stream *stream,
+ php_openssl_netstream_data_t *sslsock,
+ php_stream_xport_crypto_param *cparam) /* {{{ */
+{
+ if (sslsock->ssl_handle) {
+ if (sslsock->s.is_blocked) {
+ php_error_docref(NULL, E_WARNING, "SSL/TLS already set-up for this stream");
+ return FAILURE;
+ } else {
+ return SUCCESS;
+ }
+ }
+
+ ERR_clear_error();
+
+ /* We need to do slightly different things based on client/server method
+ * so let's remember which method was selected */
+ sslsock->is_client = cparam->inputs.method & STREAM_CRYPTO_IS_CLIENT;
+ int method_flags = cparam->inputs.method & ~STREAM_CRYPTO_IS_CLIENT;
+
+ /* Re-use SSL_CTX if session is set */
+ if (cparam->inputs.session) {
+ php_openssl_netstream_data_t *parent_sslsock;
+
+ if (cparam->inputs.session->ops != &php_openssl_socket_ops) {
+ php_error_docref(NULL, E_WARNING, "Supplied session stream must be an SSL enabled stream");
+ } else if ((parent_sslsock = cparam->inputs.session->abstract)->ctx == NULL) {
+ php_error_docref(NULL, E_WARNING, "Supplied SSL session stream is not set up");
+ } else if (sslsock->is_client && parent_sslsock->ssl_handle == NULL) {
+ php_error_docref(NULL, E_WARNING, "Supplied SSL session stream is not initialized");
+ } else {
+ SSL_CTX_up_ref(parent_sslsock->ctx);
+ sslsock->ctx = parent_sslsock->ctx;
+ if (parent_sslsock->session_callbacks) {
+ parent_sslsock->session_callbacks->refcount++;
+ sslsock->session_callbacks = parent_sslsock->session_callbacks;
+ }
+
+ sslsock->ssl_handle = SSL_new(sslsock->ctx);
+ if (!sslsock->ssl_handle) {
+ php_error_docref(NULL, E_WARNING, "SSL handle creation failure");
+ SSL_CTX_free(sslsock->ctx);
+ sslsock->ctx = NULL;
+ return FAILURE;
+ }
+
+ SSL_set_ex_data(sslsock->ssl_handle, php_openssl_get_ssl_stream_data_index(), stream);
+
+ if (!SSL_set_fd(sslsock->ssl_handle, sslsock->s.socket)) {
+ php_openssl_handle_ssl_error(stream, 0, true);
+ }
+
+ if (sslsock->is_client) {
+ if (SSL_copy_session_id(sslsock->ssl_handle, parent_sslsock->ssl_handle)) {
+ SSL_CTX_set_session_cache_mode(sslsock->ctx, SSL_SESS_CACHE_CLIENT);
+ } else {
+ php_error_docref(NULL, E_WARNING, "SSL session copying failed creation failure");
+ }
+ }
+
+ return SUCCESS;
+ }
+ }
+
+ if (php_openssl_create_server_ctx(stream, sslsock, method_flags) == FAILURE) {
return FAILURE;
}
@@ -1707,32 +2182,6 @@ static zend_result php_openssl_setup_crypto(php_stream *stream,
php_openssl_handle_ssl_error(stream, 0, true);
}
-#ifdef HAVE_TLS_SNI
- /* Enable server-side SNI */
- if (!sslsock->is_client && php_openssl_enable_server_sni(stream, sslsock, verify_peer) == FAILURE) {
- return FAILURE;
- }
-#endif
-
- /* Enable server-side handshake renegotiation rate-limiting */
- if (!sslsock->is_client) {
- php_openssl_init_server_reneg_limit(stream, sslsock);
- }
-
-#ifdef SSL_MODE_RELEASE_BUFFERS
- SSL_set_mode(sslsock->ssl_handle, SSL_MODE_RELEASE_BUFFERS);
-#endif
-
- if (cparam->inputs.session) {
- if (cparam->inputs.session->ops != &php_openssl_socket_ops) {
- php_error_docref(NULL, E_WARNING, "Supplied session stream must be an SSL enabled stream");
- } else if (((php_openssl_netstream_data_t*)cparam->inputs.session->abstract)->ssl_handle == NULL) {
- php_error_docref(NULL, E_WARNING, "Supplied SSL session stream is not initialized");
- } else {
- SSL_copy_session_id(sslsock->ssl_handle, ((php_openssl_netstream_data_t*)cparam->inputs.session->abstract)->ssl_handle);
- }
- }
-
return SUCCESS;
}
/* }}} */
@@ -1813,10 +2262,22 @@ static int php_openssl_enable_crypto(php_stream *stream,
struct timeval start_time, *timeout;
bool blocked = sslsock->s.is_blocked, has_timeout = false;
-#ifdef HAVE_TLS_SNI
if (sslsock->is_client) {
+ /* Set session data for client */
+ if ( php_openssl_apply_client_session_data(stream, sslsock)) {
+ return FAILURE;
+ }
+#ifdef HAVE_TLS_SNI
php_openssl_enable_client_sni(stream, sslsock);
+#endif
+ } else {
+ php_openssl_init_server_reneg_limit(stream, sslsock);
}
+
+#ifdef PHP_OPENSSL_TLS_DEBUG
+ BIO *b_out = BIO_new_fp(stdout, BIO_NOCLOSE | BIO_FP_TEXT);
+ SSL_set_msg_callback(sslsock->ssl_handle, SSL_trace);
+ SSL_set_msg_callback_arg(sslsock->ssl_handle, b_out);
#endif
if (!sslsock->state_set) {
@@ -1828,6 +2289,8 @@ static int php_openssl_enable_crypto(php_stream *stream,
sslsock->state_set = 1;
}
+ SSL_set_mode(sslsock->ssl_handle, SSL_MODE_RELEASE_BUFFERS);
+
if (SUCCESS == php_openssl_set_blocking(sslsock, 0)) {
/* The following mode are added only if we are able to change socket
* to non blocking mode which is also used for read and write */
@@ -2201,6 +2664,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 && --sslsock->session_callbacks->refcount == 0) {
+ 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;
@@ -2277,7 +2747,7 @@ static inline int php_openssl_tcp_sockop_accept(php_stream *stream, php_openssl_
clisockdata->method = sock->method;
if (php_stream_xport_crypto_setup(xparam->outputs.client, clisockdata->method,
- NULL) < 0 || php_stream_xport_crypto_enable(
+ sock->ctx ? stream : NULL) < 0 || php_stream_xport_crypto_enable(
xparam->outputs.client, 1) < 0) {
php_error_docref(NULL, E_WARNING, "Failed to enable crypto");
@@ -2336,6 +2806,7 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
add_assoc_string(&tmp, "cipher_name", (char *) SSL_CIPHER_get_name(cipher));
add_assoc_long(&tmp, "cipher_bits", SSL_CIPHER_get_bits(cipher, NULL));
add_assoc_string(&tmp, "cipher_version", SSL_CIPHER_get_version(cipher));
+ add_assoc_bool(&tmp, "session_reused", SSL_session_reused(sslsock->ssl_handle));
#ifdef HAVE_TLS_ALPN
{
@@ -2536,7 +3007,14 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
(xparam->op == STREAM_XPORT_OP_CONNECT_ASYNC &&
xparam->outputs.returncode == 1 && xparam->outputs.error_code == EINPROGRESS)))
{
- if (php_stream_xport_crypto_setup(stream, sslsock->method, NULL) < 0 ||
+ zval *val;
+ php_stream *session_stream = NULL;
+
+ if (GET_VER_OPT("session_stream")) {
+ php_stream_from_zval_no_verify(session_stream, val);
+ }
+
+ if (php_stream_xport_crypto_setup(stream, sslsock->method, session_stream) < 0 ||
php_stream_xport_crypto_enable(stream, 1) < 0) {
php_error_docref(NULL, E_WARNING, "Failed to enable crypto");
xparam->outputs.returncode = -1;
@@ -2544,6 +3022,21 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val
}
return PHP_STREAM_OPTION_RETURN_OK;
+ case STREAM_XPORT_OP_LISTEN:
+ /* Do normal listen first */
+ xparam->outputs.returncode = php_stream_socket_ops.set_option(
+ stream, option, value, ptrparam);
+
+ if (xparam->outputs.returncode == 0 && sslsock->enable_on_connect) {
+ /* Check if we should create SSL_CTX early for session resumption */
+ if (php_openssl_is_session_cache_enabled(stream, false)) {
+ if (FAILURE == php_openssl_create_server_ctx(stream, sslsock, sslsock->method)) {
+ xparam->outputs.returncode = -1;
+ }
+ }
+ }
+ return PHP_STREAM_OPTION_RETURN_OK;
+
case STREAM_XPORT_OP_ACCEPT:
/* we need to copy the additional fields that the underlying tcp transport
* doesn't know about */