diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index fd7bc6ac57e9a..8ceb0aadc2f7e 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -167,6 +167,289 @@ 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(zend_ce_exception, "Session is not valid", 0); \ + RETURN_THROWS(); \ + } + +PHP_METHOD(OpenSSLSession, export) +{ + zend_long format = ENCODING_DER; + + 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(zend_ce_exception, "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(zend_ce_exception, "Failed to create BIO", 0); + RETURN_THROWS(); + } + + if (!PEM_write_bio_SSL_SESSION(bio, obj->session)) { + BIO_free(bio); + zend_throw_exception(zend_ce_exception, "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(OpenSSLSession, import) +{ + zend_string *data; + zend_long format = ENCODING_DER; + + 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(zend_ce_exception, "Failed to import session data", 0); + RETURN_THROWS(); + } + + php_openssl_session_object_init(return_value, session); +} + +PHP_METHOD(OpenSSLSession, isResumable) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PHP_OPENSSL_SESSION_CHECK(); + + RETURN_BOOL(SSL_SESSION_is_resumable(obj->session)); +} + +PHP_METHOD(OpenSSLSession, getTimeout) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PHP_OPENSSL_SESSION_CHECK(); + + RETURN_LONG((zend_long)SSL_SESSION_get_timeout(obj->session)); +} + +PHP_METHOD(OpenSSLSession, getCreatedAt) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PHP_OPENSSL_SESSION_CHECK(); + + RETURN_LONG((zend_long)SSL_SESSION_get_time(obj->session)); +} + +PHP_METHOD(OpenSSLSession, 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(OpenSSLSession, 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(OpenSSLSession, hasTicket) +{ + ZEND_PARSE_PARAMETERS_NONE(); + PHP_OPENSSL_SESSION_CHECK(); + + RETURN_BOOL(SSL_SESSION_has_ticket(obj->session)); +} + +PHP_METHOD(OpenSSLSession, 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(OpenSSLSession, __serialize) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + PHP_OPENSSL_SESSION_CHECK(); + + int len = i2d_SSL_SESSION(obj->session, NULL); + if (len <= 0) { + zend_throw_exception(zend_ce_exception, "Failed to serialize session", 0); + RETURN_THROWS(); + } + + zend_string *der = zend_string_alloc(len, 0); + unsigned char *p = (unsigned char *)ZSTR_VAL(der); + i2d_SSL_SESSION(obj->session, &p); + ZSTR_VAL(der)[len] = '\0'; + + array_init(return_value); + add_assoc_str(return_value, "der", der); +} + +PHP_METHOD(OpenSSLSession, __unserialize) +{ + HashTable *data; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(data) + ZEND_PARSE_PARAMETERS_END(); + + zval *der_zv = zend_hash_str_find(data, ZEND_STRL("der")); + if (!der_zv || Z_TYPE_P(der_zv) != IS_STRING) { + zend_throw_exception(zend_ce_exception, "Invalid serialization data", 0); + RETURN_THROWS(); + } + + const unsigned char *p = (const unsigned char *)Z_STRVAL_P(der_zv); + SSL_SESSION *session = d2i_SSL_SESSION(NULL, &p, Z_STRLEN_P(der_zv)); + + if (!session) { + zend_throw_exception(zend_ce_exception, "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") @@ -416,6 +699,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_OpenSSLSession(); + 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 = XtOffsetOf(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 94902a4acf0da..fcb7b73505a47 100644 --- a/ext/openssl/openssl.stub.php +++ b/ext/openssl/openssl.stub.php @@ -434,6 +434,36 @@ final class OpenSSLAsymmetricKey { } +/** + * @strict-properties + */ +final class OpenSSLSession +{ + public readonly string $id; + + public function export(int $format = OPENSSL_ENCODING_PEM): string {} + + public static function import(string $data, int $format = OPENSSL_ENCODING_PEM): OpenSSLSession {} + + 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 {} +} + function openssl_x509_export_to_file(OpenSSLCertificate|string $certificate, string $output_filename, bool $no_text = true): bool {} /** @param string $output */ diff --git a/ext/openssl/openssl_arginfo.h b/ext/openssl/openssl_arginfo.h index bae435e90241e..336e288b6f55f 100644 --- a/ext/openssl/openssl_arginfo.h +++ b/ext/openssl/openssl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit openssl.stub.php instead. - * Stub hash: 8233a8abc8ab7145d905d0fa51478edfe1e55a06 */ + * Stub hash: 893e7752dc37d60a03f8e3819374932e62a8a5da */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_x509_export_to_file, 0, 2, _IS_BOOL, 0) ZEND_ARG_OBJ_TYPE_MASK(0, certificate, OpenSSLCertificate, MAY_BE_STRING, NULL) @@ -406,6 +406,39 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_openssl_password_verify, 0, 3, _ ZEND_END_ARG_INFO() #endif +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_OpenSSLSession_export, 0, 0, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, format, IS_LONG, 0, "OPENSSL_ENCODING_PEM") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_OpenSSLSession_import, 0, 1, OpenSSLSession, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, format, IS_LONG, 0, "OPENSSL_ENCODING_PEM") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_OpenSSLSession_isResumable, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_OpenSSLSession_getTimeout, 0, 0, IS_LONG, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_OpenSSLSession_getCreatedAt arginfo_class_OpenSSLSession_getTimeout + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_OpenSSLSession_getProtocol, 0, 0, IS_STRING, 1) +ZEND_END_ARG_INFO() + +#define arginfo_class_OpenSSLSession_getCipher arginfo_class_OpenSSLSession_getProtocol + +#define arginfo_class_OpenSSLSession_hasTicket arginfo_class_OpenSSLSession_isResumable + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_OpenSSLSession_getTicketLifetimeHint, 0, 0, IS_LONG, 1) +ZEND_END_ARG_INFO() + +#define arginfo_class_OpenSSLSession___serialize arginfo_openssl_get_cert_locations + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_OpenSSLSession___unserialize, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + ZEND_FUNCTION(openssl_x509_export_to_file); ZEND_FUNCTION(openssl_x509_export); ZEND_FUNCTION(openssl_x509_fingerprint); @@ -473,6 +506,17 @@ ZEND_FUNCTION(openssl_get_cert_locations); ZEND_FUNCTION(openssl_password_hash); ZEND_FUNCTION(openssl_password_verify); #endif +ZEND_METHOD(OpenSSLSession, export); +ZEND_METHOD(OpenSSLSession, import); +ZEND_METHOD(OpenSSLSession, isResumable); +ZEND_METHOD(OpenSSLSession, getTimeout); +ZEND_METHOD(OpenSSLSession, getCreatedAt); +ZEND_METHOD(OpenSSLSession, getProtocol); +ZEND_METHOD(OpenSSLSession, getCipher); +ZEND_METHOD(OpenSSLSession, hasTicket); +ZEND_METHOD(OpenSSLSession, getTicketLifetimeHint); +ZEND_METHOD(OpenSSLSession, __serialize); +ZEND_METHOD(OpenSSLSession, __unserialize); static const zend_function_entry ext_functions[] = { ZEND_FE(openssl_x509_export_to_file, arginfo_openssl_x509_export_to_file) @@ -548,6 +592,21 @@ static const zend_function_entry ext_functions[] = { ZEND_FE_END }; +static const zend_function_entry class_OpenSSLSession_methods[] = { + ZEND_ME(OpenSSLSession, export, arginfo_class_OpenSSLSession_export, ZEND_ACC_PUBLIC) + ZEND_ME(OpenSSLSession, import, arginfo_class_OpenSSLSession_import, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(OpenSSLSession, isResumable, arginfo_class_OpenSSLSession_isResumable, ZEND_ACC_PUBLIC) + ZEND_ME(OpenSSLSession, getTimeout, arginfo_class_OpenSSLSession_getTimeout, ZEND_ACC_PUBLIC) + ZEND_ME(OpenSSLSession, getCreatedAt, arginfo_class_OpenSSLSession_getCreatedAt, ZEND_ACC_PUBLIC) + ZEND_ME(OpenSSLSession, getProtocol, arginfo_class_OpenSSLSession_getProtocol, ZEND_ACC_PUBLIC) + ZEND_ME(OpenSSLSession, getCipher, arginfo_class_OpenSSLSession_getCipher, ZEND_ACC_PUBLIC) + ZEND_ME(OpenSSLSession, hasTicket, arginfo_class_OpenSSLSession_hasTicket, ZEND_ACC_PUBLIC) + ZEND_ME(OpenSSLSession, getTicketLifetimeHint, arginfo_class_OpenSSLSession_getTicketLifetimeHint, ZEND_ACC_PUBLIC) + ZEND_ME(OpenSSLSession, __serialize, arginfo_class_OpenSSLSession___serialize, ZEND_ACC_PUBLIC) + ZEND_ME(OpenSSLSession, __unserialize, arginfo_class_OpenSSLSession___unserialize, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + static void register_openssl_symbols(int module_number) { REGISTER_STRING_CONSTANT("OPENSSL_VERSION_TEXT", OPENSSL_VERSION_TEXT, CONST_PERSISTENT); @@ -780,3 +839,19 @@ static zend_class_entry *register_class_OpenSSLAsymmetricKey(void) return class_entry; } + +static zend_class_entry *register_class_OpenSSLSession(void) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "OpenSSLSession", class_OpenSSLSession_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES); + + zval property_id_default_value; + ZVAL_UNDEF(&property_id_default_value); + zend_string *property_id_name = zend_string_init("id", sizeof("id") - 1, true); + zend_declare_typed_property(class_entry, property_id_name, &property_id_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_STRING)); + zend_string_release_ex(property_id_name, true); + + return class_entry; +} diff --git a/ext/openssl/php_openssl.h b/ext/openssl/php_openssl.h index 92ccd9a546f9e..61a0c83e0c97e 100644 --- a/ext/openssl/php_openssl.h +++ b/ext/openssl/php_openssl.h @@ -201,6 +201,27 @@ 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 + +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) - XtOffsetOf(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); + #if defined(HAVE_OPENSSL_ARGON2) /** diff --git a/ext/openssl/tests/ServerClientTestCase.inc b/ext/openssl/tests/ServerClientTestCase.inc index 8eedbfdebee8b..f0336fdd39219 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 0000000000000..9e0e8a82f399a --- /dev/null +++ b/ext/openssl/tests/session_resumption_cache_disabled.phpt @@ -0,0 +1,86 @@ +--TEST-- +TLS session resumption - server with cache disabled +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'session_cache' => false, /* Disable session caching */ + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + /* Accept two connections */ + for ($i = 0; $i < 2; $i++) { + $client = @stream_socket_accept($server, 30); + if ($client) { + fwrite($client, "No cache connection " . ($i + 1) . "\n"); + fclose($client); + } + } +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-- + +--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 0000000000000..ee1d126a6d9f6 --- /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-- + +--FILE-- + [ + '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-- + +--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 0000000000000..f87f831a78596 --- /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-- + +--FILE-- + [ + '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-- + +--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 0000000000000..5563b0c22cf88 --- /dev/null +++ b/ext/openssl/tests/session_resumption_get_cb_num_tickets_positive.phpt @@ -0,0 +1,82 @@ +--TEST-- +TLS session resumption - num_tickets controls ticket generation (TLS 1.3) +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_SERVER, + 'num_tickets' => 3, // Issue 3 tickets per connection + ]]); + + $server = stream_socket_server('tlsv1.3://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + // Accept one connection + $client = @stream_socket_accept($server, 30); + if ($client) { + fwrite($client, "Ticket test\n"); + // Keep connection open briefly to allow tickets to be sent + usleep(100000); // 100ms + fclose($client); + } + + phpt_notify(message: "SERVER_DONE"); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $ticketCount = 0; + + $flags = STREAM_CLIENT_CONNECT; + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT, + 'session_new_cb' => function($stream, $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-- + +--EXPECTF-- +Ticket test +Tickets received: 3 +SERVER_DONE diff --git a/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt b/ext/openssl/tests/session_resumption_get_cb_num_tickets_zero.phpt new file mode 100644 index 0000000000000..13654cd451e94 --- /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-- + +--FILE-- + [ + '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-- + +--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_invalid_callback.phpt b/ext/openssl/tests/session_resumption_invalid_callback.phpt new file mode 100644 index 0000000000000..b6cfc90a0554c --- /dev/null +++ b/ext/openssl/tests/session_resumption_invalid_callback.phpt @@ -0,0 +1,60 @@ +--TEST-- +TLS session resumption - invalid callback throws TypeError +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + $client = @stream_socket_accept($server, 30); + if ($client) { + fclose($client); + } +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $flags = STREAM_CLIENT_CONNECT; + + /* Try to use invalid callback */ + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => 'not_a_valid_function', + ]]); + + try { + $client = @stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + echo "Should not reach here\n"; + } catch (TypeError $e) { + echo "TypeError caught: " . (strpos($e->getMessage(), 'session_new_cb must be a valid callback') !== false ? "YES" : "NO"); + echo "\n"; + } +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_invalid_cb_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- +TypeError caught: YES diff --git a/ext/openssl/tests/session_resumption_invalid_data.phpt b/ext/openssl/tests/session_resumption_invalid_data.phpt new file mode 100644 index 0000000000000..1c1d23df13a9f --- /dev/null +++ b/ext/openssl/tests/session_resumption_invalid_data.phpt @@ -0,0 +1,64 @@ +--TEST-- +TLS session resumption - invalid session data is fatal +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + $client = @stream_socket_accept($server, 30); + if ($client) { + fclose($client); + } +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $flags = STREAM_CLIENT_CONNECT; + + /* Try to use corrupted session data */ + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => 'this_is_invalid_session_data', + ]]); + + $client = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + + if ($client === false) { + echo "Connection failed as expected\n"; + } +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('session_invalid_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECTF-- + +Warning: stream_socket_client(): Invalid or corrupted session_data, falling back to full handshake in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to %s in %s on line %d +Connection failed as expected diff --git a/ext/openssl/tests/session_resumption_new_cb_no_context.phpt b/ext/openssl/tests/session_resumption_new_cb_no_context.phpt new file mode 100644 index 0000000000000..4ae4fb164a0b1 --- /dev/null +++ b/ext/openssl/tests/session_resumption_new_cb_no_context.phpt @@ -0,0 +1,80 @@ +--TEST-- +TLS session resumption - warning when session_new_cb without session_id_context and verify_peer enabled +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'verify_peer' => true, + 'cafile' => '%s', + 'session_new_cb' => function($stream, $session) { + echo "Callback might not be called\n"; + } + /* Missing: 'session_id_context' => 'myapp' */ + ]]); + + $server = @stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + try { + $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, + 'session_data' => 'this_is_invalid_session_data', + ]]); + + $client = @stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 30, $flags, $ctx); + + if ($client === false) { + echo "Connection failed as expected\n"; + } + + $result = phpt_wait(); + echo trim($result) . "\n"; +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveCaCert($caCertFile); +$certificateGenerator->saveNewCertAsFileWithKey('session_verify_test', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +Connection failed as expected +SERVER_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 0000000000000..835e9bb516488 --- /dev/null +++ b/ext/openssl/tests/session_resumption_persistent_reject.phpt @@ -0,0 +1,66 @@ +--TEST-- +TLS session resumption - callbacks rejected on persistent streams +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, $flags, $ctx); + phpt_notify_server_start($server); + + $client = @stream_socket_accept($server, 30); + if ($client) { + fclose($client); + } +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT; + + /* Try to use callback with persistent stream */ + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => function($stream, $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-- + +--EXPECTF-- + +Warning: stream_socket_client(): session_new_cb is not supported for persistent streams in %s on line %d + +Warning: stream_socket_client(): Failed to enable crypto in %s on line %d + +Warning: stream_socket_client(): Unable to connect to %s in %s on line %d +Connection failed as expected with persistent stream diff --git a/ext/openssl/tests/session_resumption_require_new_cb.phpt b/ext/openssl/tests/session_resumption_require_new_cb.phpt new file mode 100644 index 0000000000000..a08408e2d90b8 --- /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-- + +--FILE-- + [ + '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-- + +--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_server_external_with_context_id.phpt b/ext/openssl/tests/session_resumption_server_external_with_context_id.phpt new file mode 100644 index 0000000000000..02c6a7dcfdc7a --- /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-- + +--FILE-- + [ + '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-- + +--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 0000000000000..bdc3d2ce1bf61 --- /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-- + +--FILE-- + [ + '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-- + +--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 0000000000000..28bb97faff28d --- /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-- + +--FILE-- + [ + '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-- + +--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 0000000000000..d7c2633601e8a --- /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-- + +--FILE-- + [ + '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-- + +--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 bd174f30095c6..9170b30a14809 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -182,6 +182,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! @@ -201,6 +209,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; @@ -1545,37 +1554,413 @@ 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; + } + } else if (Z_TYPE(retval) == IS_STRING && Z_STRLEN(retval) > 0) { + /* Backward compatibility: accept raw DER string */ + const unsigned char *p = (const unsigned char *)Z_STRVAL(retval); + session = d2i_SSL_SESSION(NULL, &p, Z_STRLEN(retval)); + } + zval_ptr_dtor(&retval); + } + + zval_ptr_dtor(&args[1]); + + *copy = 0; + 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 (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 (GET_VER_OPT("session_data")) { + if (php_openssl_is_session_ce(val) || + (Z_TYPE_P(val) == IS_STRING && Z_STRLEN_P(val) > 0)) { + 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_STRING && Z_STRLEN_P(val) > 0) { + /* Legacy: deserialize session from DER format */ + const unsigned char *p = (const unsigned char *)Z_STRVAL_P(val); + session = d2i_SSL_SESSION(NULL, &p, Z_STRLEN_P(val)); + + if (!session) { + php_error_docref(NULL, E_WARNING, + "Invalid or corrupted session_data, falling back to full handshake"); + ERR_clear_error(); + return FAILURE; + } + needs_free = true; + } + + 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; + } - method = sslsock->is_client ? SSLv23_client_method() : SSLv23_server_method(); + 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; + + const SSL_METHOD *method = sslsock->is_client ? SSLv23_client_method() : SSLv23_server_method(); sslsock->ctx = SSL_CTX_new(method); if (sslsock->ctx == NULL) { @@ -1583,14 +1968,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; @@ -1603,6 +1998,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 { @@ -1618,6 +2014,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) { @@ -1640,6 +2037,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 @@ -1685,25 +2083,22 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, return FAILURE; } - sslsock->ssl_handle = SSL_new(sslsock->ctx); - - if (sslsock->ssl_handle == NULL) { - php_error_docref(NULL, E_WARNING, "SSL handle creation failure"); - SSL_CTX_free(sslsock->ctx); - sslsock->ctx = NULL; -#ifdef HAVE_TLS_ALPN - if (sslsock->alpn_ctx.data) { - pefree(sslsock->alpn_ctx.data, php_stream_is_persistent(stream)); - sslsock->alpn_ctx.data = NULL; + if (sslsock->is_client) { + /* Setup client session resumption */ + if (FAILURE == php_openssl_setup_client_session(stream, sslsock)) { + return FAILURE; } -#endif - return FAILURE; } else { - 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); + /* Setup server session resumption */ + if (PHP_STREAM_CONTEXT(stream)) { + if (FAILURE == php_openssl_setup_server_session(stream, sslsock)) { + return FAILURE; + } + } + /* Original server-specific setup */ + if (FAILURE == php_openssl_set_server_specific_opts(stream, sslsock->ctx)) { + return FAILURE; + } } #ifdef HAVE_TLS_SNI @@ -1713,25 +2108,98 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, } #endif - /* Enable server-side handshake renegotiation rate-limiting */ - if (!sslsock->is_client) { - php_openssl_init_server_reneg_limit(stream, sslsock); + 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; + } } -#ifdef SSL_MODE_RELEASE_BUFFERS - SSL_set_mode(sslsock->ssl_handle, SSL_MODE_RELEASE_BUFFERS); -#endif + 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 (((php_openssl_netstream_data_t*)cparam->inputs.session->abstract)->ssl_handle == NULL) { + } 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_copy_session_id(sslsock->ssl_handle, ((php_openssl_netstream_data_t*)cparam->inputs.session->abstract)->ssl_handle); + 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; + } + + sslsock->ssl_handle = SSL_new(sslsock->ctx); + + if (sslsock->ssl_handle == NULL) { + php_error_docref(NULL, E_WARNING, "SSL handle creation failure"); + SSL_CTX_free(sslsock->ctx); + sslsock->ctx = NULL; +#ifdef HAVE_TLS_ALPN + if (sslsock->alpn_ctx.data) { + pefree(sslsock->alpn_ctx.data, php_stream_is_persistent(stream)); + sslsock->alpn_ctx.data = NULL; + } +#endif + return FAILURE; + } else { + 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); + } + return SUCCESS; } /* }}} */ @@ -1812,10 +2280,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) { @@ -1827,6 +2307,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 */ @@ -2200,6 +2682,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; @@ -2276,7 +2765,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"); @@ -2335,6 +2824,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 { @@ -2537,7 +3027,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; @@ -2545,6 +3042,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 */