Skip to content

Commit bf7b4ab

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 dc807bc commit bf7b4ab

25 files changed

Lines changed: 2481 additions & 58 deletions

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ PHP NEWS
9696
. Added AES-SIV support. (jordikroon)
9797
. Implemented GH-20310 (No critical extension indication in
9898
openssl_x509_parse() output). (StephenWall)
99+
. Added TLS session resumption support for streams with new context options
100+
and Openssl\Session class. (Jakub Zelenka)
99101

100102
- PDO_PGSQL:
101103
. Clear session-local state disconnect-equivalent processing.

UPGRADING

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

144+
- OpenSSL:
145+
. Added TLS session resumption support for streams with new stream context
146+
options: session_data, session_new_cb, session_cache, session_cache_size,
147+
session_timeout, session_id_context, session_get_cb, session_remove_cb,
148+
and num_tickets. This allows saving and restoring client sessions across
149+
requests, implementing custom server-side session storage, and controlling
150+
session cache behavior.
151+
RFC: https://wiki.php.net/rfc/tls_session_resumption
152+
144153
- Phar:
145154
. Overriding the getMTime() and getPathname() methods of SplFileInfo now
146155
influences the result of the phar buildFrom family of functions.
@@ -232,6 +241,11 @@ PHP 8.6 UPGRADE NOTES
232241
7. New Classes and Interfaces
233242
========================================
234243

244+
- OpenSSL:
245+
. Openssl\OpensslException
246+
. Openssl\Session
247+
RFC: https://wiki.php.net/rfc/tls_session_resumption
248+
235249
- Standard:
236250
. enum SortDirection
237251
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 = offsetof(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)