Skip to content

Commit 6d645b7

Browse files
committed
Add OpenSSL TLS configurable session resumption support
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
1 parent 0471420 commit 6d645b7

25 files changed

Lines changed: 2482 additions & 58 deletions

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ PHP NEWS
9090
- OpenSSL:
9191
. Implemented GH-20310 (No critical extension indication in
9292
openssl_x509_parse() output). (StephenWall)
93+
. Added TLS session resumption support for streams with new context options
94+
and Openssl\Session class. (Jakub Zelenka)
9395

9496
- PDO_PGSQL:
9597
. Clear session-local state disconnect-equivalent processing.

UPGRADING

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,15 @@ PHP 8.6 UPGRADE NOTES
132132
. Added extra info about error location to the JSON error messages returned
133133
from json_last_error_msg() and JsonException message.
134134

135+
- OpenSSL:
136+
. Added TLS session resumption support for streams with new stream context
137+
options: session_data, session_new_cb, session_cache, session_cache_size,
138+
session_timeout, session_id_context, session_get_cb, session_remove_cb,
139+
and num_tickets. This allows saving and restoring client sessions across
140+
requests, implementing custom server-side session storage, and controlling
141+
session cache behavior.
142+
RFC: https://wiki.php.net/rfc/tls_session_resumption
143+
135144
- Phar:
136145
. Overriding the getMTime() and getPathname() methods of SplFileInfo now
137146
influences the result of the phar buildFrom family of functions.
@@ -218,6 +227,11 @@ PHP 8.6 UPGRADE NOTES
218227
7. New Classes and Interfaces
219228
========================================
220229

230+
- OpenSSL:
231+
. Openssl\OpensslException
232+
. Openssl\Session
233+
RFC: https://wiki.php.net/rfc/tls_session_resumption
234+
221235
- Standard:
222236
. enum SortDirection
223237
RFC: https://wiki.php.net/rfc/sort_direction_enum

ext/openssl/openssl.c

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ ZEND_DECLARE_MODULE_GLOBALS(openssl)
5656

5757
#include "openssl_arginfo.h"
5858

59+
/* OpenSSLException class */
60+
61+
zend_class_entry *php_openssl_exception_ce;
62+
5963
/* OpenSSLCertificate class */
6064

6165
zend_class_entry *php_openssl_certificate_ce;
@@ -165,6 +169,302 @@ static void php_openssl_pkey_free_obj(zend_object *object)
165169
zend_object_std_dtor(&key_object->std);
166170
}
167171

172+
/* OpenSSLSession class */
173+
174+
zend_class_entry *php_openssl_session_ce;
175+
176+
static zend_object_handlers php_openssl_session_object_handlers;
177+
178+
bool php_openssl_is_session_ce(zval *val)
179+
{
180+
return Z_TYPE_P(val) == IS_OBJECT && Z_OBJCE_P(val) == php_openssl_session_ce;
181+
}
182+
183+
SSL_SESSION *php_openssl_session_from_zval(zval *zv)
184+
{
185+
if (!php_openssl_is_session_ce(zv)) {
186+
return NULL;
187+
}
188+
return Z_OPENSSL_SESSION_P(zv)->session;
189+
}
190+
191+
void php_openssl_session_object_init(zval *zv, SSL_SESSION *session)
192+
{
193+
object_init_ex(zv, php_openssl_session_ce);
194+
php_openssl_session_object *obj = Z_OPENSSL_SESSION_P(zv);
195+
obj->session = session;
196+
197+
unsigned int id_len = 0;
198+
const unsigned char *id = SSL_SESSION_get_id(session, &id_len);
199+
zend_update_property_stringl(php_openssl_session_ce, Z_OBJ_P(zv),
200+
ZEND_STRL("id"), (char *)id, id_len);
201+
}
202+
203+
static zend_object *php_openssl_session_create_object(zend_class_entry *class_type)
204+
{
205+
php_openssl_session_object *intern = zend_object_alloc(sizeof(php_openssl_session_object), class_type);
206+
207+
zend_object_std_init(&intern->std, class_type);
208+
object_properties_init(&intern->std, class_type);
209+
210+
return &intern->std;
211+
}
212+
213+
static zend_function *php_openssl_session_get_constructor(zend_object *object)
214+
{
215+
zend_throw_error(NULL,
216+
"Cannot directly construct OpenSSLSession, use OpenSSLSession::import() or TLS session callbacks");
217+
return NULL;
218+
}
219+
220+
static void php_openssl_session_free_obj(zend_object *object)
221+
{
222+
php_openssl_session_object *session_object = php_openssl_session_from_obj(object);
223+
224+
if (session_object->session) {
225+
SSL_SESSION_free(session_object->session);
226+
session_object->session = NULL;
227+
}
228+
zend_object_std_dtor(&session_object->std);
229+
}
230+
231+
#define PHP_OPENSSL_SESSION_CHECK() \
232+
php_openssl_session_object *obj = Z_OPENSSL_SESSION_P(ZEND_THIS); \
233+
if (!obj->session) { \
234+
zend_throw_exception(php_openssl_exception_ce, "Session is not valid", 0); \
235+
RETURN_THROWS(); \
236+
}
237+
238+
PHP_METHOD(Openssl_Session, export)
239+
{
240+
zend_long format = ENCODING_PEM;
241+
242+
ZEND_PARSE_PARAMETERS_START(0, 1)
243+
Z_PARAM_OPTIONAL
244+
Z_PARAM_LONG(format)
245+
ZEND_PARSE_PARAMETERS_END();
246+
247+
PHP_OPENSSL_SESSION_CHECK();
248+
249+
if (format == ENCODING_DER) {
250+
int len = i2d_SSL_SESSION(obj->session, NULL);
251+
if (len <= 0) {
252+
zend_throw_exception(php_openssl_exception_ce, "Failed to export session", 0);
253+
RETURN_THROWS();
254+
}
255+
256+
zend_string *result = zend_string_alloc(len, 0);
257+
unsigned char *p = (unsigned char *)ZSTR_VAL(result);
258+
i2d_SSL_SESSION(obj->session, &p);
259+
ZSTR_VAL(result)[len] = '\0';
260+
261+
RETURN_NEW_STR(result);
262+
}
263+
264+
if (format == ENCODING_PEM) {
265+
BIO *bio = BIO_new(BIO_s_mem());
266+
if (!bio) {
267+
zend_throw_exception(php_openssl_exception_ce, "Failed to create BIO", 0);
268+
RETURN_THROWS();
269+
}
270+
271+
if (!PEM_write_bio_SSL_SESSION(bio, obj->session)) {
272+
BIO_free(bio);
273+
zend_throw_exception(php_openssl_exception_ce, "Failed to export session as PEM", 0);
274+
RETURN_THROWS();
275+
}
276+
277+
char *data;
278+
long len = BIO_get_mem_data(bio, &data);
279+
zend_string *result = zend_string_init(data, len, 0);
280+
BIO_free(bio);
281+
282+
RETURN_NEW_STR(result);
283+
}
284+
285+
zend_argument_value_error(1, "must be OPENSSL_ENCODING_DER or OPENSSL_ENCODING_PEM");
286+
RETURN_THROWS();
287+
}
288+
289+
PHP_METHOD(Openssl_Session, import)
290+
{
291+
zend_string *data;
292+
zend_long format = ENCODING_PEM;
293+
294+
ZEND_PARSE_PARAMETERS_START(1, 2)
295+
Z_PARAM_STR(data)
296+
Z_PARAM_OPTIONAL
297+
Z_PARAM_LONG(format)
298+
ZEND_PARSE_PARAMETERS_END();
299+
300+
SSL_SESSION *session = NULL;
301+
302+
if (format == ENCODING_DER) {
303+
const unsigned char *p = (const unsigned char *)ZSTR_VAL(data);
304+
session = d2i_SSL_SESSION(NULL, &p, ZSTR_LEN(data));
305+
} else if (format == ENCODING_PEM) {
306+
BIO *bio = BIO_new_mem_buf(ZSTR_VAL(data), ZSTR_LEN(data));
307+
if (bio) {
308+
session = PEM_read_bio_SSL_SESSION(bio, NULL, NULL, NULL);
309+
BIO_free(bio);
310+
}
311+
} else {
312+
zend_argument_value_error(2, "must be OPENSSL_ENCODING_DER or OPENSSL_ENCODING_PEM");
313+
RETURN_THROWS();
314+
}
315+
316+
if (!session) {
317+
zend_throw_exception(php_openssl_exception_ce, "Failed to import session data", 0);
318+
RETURN_THROWS();
319+
}
320+
321+
php_openssl_session_object_init(return_value, session);
322+
}
323+
324+
PHP_METHOD(Openssl_Session, isResumable)
325+
{
326+
ZEND_PARSE_PARAMETERS_NONE();
327+
PHP_OPENSSL_SESSION_CHECK();
328+
329+
RETURN_BOOL(SSL_SESSION_is_resumable(obj->session));
330+
}
331+
332+
PHP_METHOD(Openssl_Session, getTimeout)
333+
{
334+
ZEND_PARSE_PARAMETERS_NONE();
335+
PHP_OPENSSL_SESSION_CHECK();
336+
RETURN_LONG((zend_long)SSL_SESSION_get_timeout(obj->session));
337+
}
338+
339+
PHP_METHOD(Openssl_Session, getCreatedAt)
340+
{
341+
ZEND_PARSE_PARAMETERS_NONE();
342+
PHP_OPENSSL_SESSION_CHECK();
343+
#if PHP_OPENSSL_API_VERSION >= 0x30300
344+
RETURN_LONG((zend_long)SSL_SESSION_get_time_ex(obj->session));
345+
#else
346+
RETURN_LONG((zend_long)SSL_SESSION_get_time(obj->session));
347+
#endif
348+
}
349+
350+
PHP_METHOD(Openssl_Session, getProtocol)
351+
{
352+
ZEND_PARSE_PARAMETERS_NONE();
353+
PHP_OPENSSL_SESSION_CHECK();
354+
355+
int version = SSL_SESSION_get_protocol_version(obj->session);
356+
357+
switch (version) {
358+
case TLS1_3_VERSION:
359+
RETURN_STRING("TLSv1.3");
360+
case TLS1_2_VERSION:
361+
RETURN_STRING("TLSv1.2");
362+
case TLS1_1_VERSION:
363+
RETURN_STRING("TLSv1.1");
364+
case TLS1_VERSION:
365+
RETURN_STRING("TLSv1.0");
366+
default:
367+
RETURN_NULL();
368+
}
369+
}
370+
371+
PHP_METHOD(Openssl_Session, getCipher)
372+
{
373+
ZEND_PARSE_PARAMETERS_NONE();
374+
PHP_OPENSSL_SESSION_CHECK();
375+
376+
const SSL_CIPHER *cipher = SSL_SESSION_get0_cipher(obj->session);
377+
if (!cipher) {
378+
RETURN_NULL();
379+
}
380+
381+
RETURN_STRING(SSL_CIPHER_get_name(cipher));
382+
}
383+
384+
PHP_METHOD(Openssl_Session, hasTicket)
385+
{
386+
ZEND_PARSE_PARAMETERS_NONE();
387+
PHP_OPENSSL_SESSION_CHECK();
388+
389+
RETURN_BOOL(SSL_SESSION_has_ticket(obj->session));
390+
}
391+
392+
PHP_METHOD(Openssl_Session, getTicketLifetimeHint)
393+
{
394+
ZEND_PARSE_PARAMETERS_NONE();
395+
PHP_OPENSSL_SESSION_CHECK();
396+
397+
if (!SSL_SESSION_has_ticket(obj->session)) {
398+
RETURN_NULL();
399+
}
400+
401+
RETURN_LONG((zend_long)SSL_SESSION_get_ticket_lifetime_hint(obj->session));
402+
}
403+
PHP_METHOD(Openssl_Session, __serialize)
404+
{
405+
ZEND_PARSE_PARAMETERS_NONE();
406+
407+
PHP_OPENSSL_SESSION_CHECK();
408+
409+
BIO *bio = BIO_new(BIO_s_mem());
410+
if (!bio) {
411+
zend_throw_exception(php_openssl_exception_ce, "Failed to serialize session", 0);
412+
RETURN_THROWS();
413+
}
414+
415+
if (!PEM_write_bio_SSL_SESSION(bio, obj->session)) {
416+
BIO_free(bio);
417+
zend_throw_exception(php_openssl_exception_ce, "Failed to serialize session", 0);
418+
RETURN_THROWS();
419+
}
420+
421+
char *data;
422+
long len = BIO_get_mem_data(bio, &data);
423+
zend_string *pem = zend_string_init(data, len, 0);
424+
BIO_free(bio);
425+
426+
array_init(return_value);
427+
add_assoc_str(return_value, "pem", pem);
428+
}
429+
430+
PHP_METHOD(Openssl_Session, __unserialize)
431+
{
432+
HashTable *data;
433+
434+
ZEND_PARSE_PARAMETERS_START(1, 1)
435+
Z_PARAM_ARRAY_HT(data)
436+
ZEND_PARSE_PARAMETERS_END();
437+
438+
zval *pem_zv = zend_hash_str_find(data, ZEND_STRL("pem"));
439+
if (!pem_zv || Z_TYPE_P(pem_zv) != IS_STRING) {
440+
zend_throw_exception(php_openssl_exception_ce, "Invalid serialization data", 0);
441+
RETURN_THROWS();
442+
}
443+
444+
BIO *bio = BIO_new_mem_buf(Z_STRVAL_P(pem_zv), Z_STRLEN_P(pem_zv));
445+
if (!bio) {
446+
zend_throw_exception(php_openssl_exception_ce, "Failed to unserialize session", 0);
447+
RETURN_THROWS();
448+
}
449+
450+
SSL_SESSION *session = PEM_read_bio_SSL_SESSION(bio, NULL, NULL, NULL);
451+
BIO_free(bio);
452+
453+
if (!session) {
454+
zend_throw_exception(php_openssl_exception_ce, "Failed to unserialize session", 0);
455+
RETURN_THROWS();
456+
}
457+
458+
php_openssl_session_object *obj = Z_OPENSSL_SESSION_P(ZEND_THIS);
459+
obj->session = session;
460+
461+
/* Populate id property */
462+
unsigned int id_len = 0;
463+
const unsigned char *id = SSL_SESSION_get_id(session, &id_len);
464+
zend_update_property_stringl(php_openssl_session_ce, Z_OBJ_P(ZEND_THIS),
465+
ZEND_STRL("id"), (char *)id, id_len);
466+
}
467+
168468
#if defined(HAVE_OPENSSL_ARGON2)
169469
static const zend_module_dep openssl_deps[] = {
170470
ZEND_MOD_REQUIRED("standard")
@@ -381,6 +681,8 @@ PHP_INI_END()
381681
/* {{{ PHP_MINIT_FUNCTION */
382682
PHP_MINIT_FUNCTION(openssl)
383683
{
684+
php_openssl_exception_ce = register_class_Openssl_OpensslException(zend_ce_exception);
685+
384686
php_openssl_certificate_ce = register_class_OpenSSLCertificate();
385687
php_openssl_certificate_ce->create_object = php_openssl_certificate_create_object;
386688
php_openssl_certificate_ce->default_object_handlers = &php_openssl_certificate_object_handlers;
@@ -414,6 +716,17 @@ PHP_MINIT_FUNCTION(openssl)
414716
php_openssl_pkey_object_handlers.clone_obj = NULL;
415717
php_openssl_pkey_object_handlers.compare = zend_objects_not_comparable;
416718

719+
php_openssl_session_ce = register_class_Openssl_Session();
720+
php_openssl_session_ce->create_object = php_openssl_session_create_object;
721+
php_openssl_session_ce->default_object_handlers = &php_openssl_session_object_handlers;
722+
723+
memcpy(&php_openssl_session_object_handlers, &std_object_handlers, sizeof(zend_object_handlers));
724+
php_openssl_session_object_handlers.offset = XtOffsetOf(php_openssl_session_object, std);
725+
php_openssl_session_object_handlers.free_obj = php_openssl_session_free_obj;
726+
php_openssl_session_object_handlers.get_constructor = php_openssl_session_get_constructor;
727+
php_openssl_session_object_handlers.clone_obj = NULL;
728+
php_openssl_session_object_handlers.compare = zend_objects_not_comparable;
729+
417730
register_openssl_symbols(module_number);
418731

419732
php_openssl_backend_init();

0 commit comments

Comments
 (0)