From 387a3afc6b0831ed65f66e191e9bf328466b87ed Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Sat, 25 Jul 2026 22:48:02 +0200 Subject: [PATCH 1/2] Fix run-tests.php --file-cache-prime with multiple workers (GH-22886) * Fix run-tests.php --file-cache-prime with multiple workers We want to remove the directory only before running tests, rather than for every test. That causes races, but also defeats the purpose of priming the cache. * Move opcache.opt_debug_level tests These need to live in ext/opcache/tests to be skipped by run-tests.php for the --file-cache-use tests, which won't compile the files and thus won't dump the output. * Mark preloading test as opcache.file_cache_only=0 Preloading requires shm. --- Zend/tests/partial_application/preloading.phpt | 1 + .../array_map_foreach_optimization_007.phpt} | 0 .../array_map_foreach_optimization_008.phpt} | 0 run-tests.php | 17 ++++++++++++----- 4 files changed, 13 insertions(+), 5 deletions(-) rename ext/{standard/tests/array/array_map_foreach_optimization_006.phpt => opcache/tests/array_map_foreach_optimization_007.phpt} (100%) rename ext/{standard/tests/array/array_map_foreach_optimization_007.phpt => opcache/tests/array_map_foreach_optimization_008.phpt} (100%) diff --git a/Zend/tests/partial_application/preloading.phpt b/Zend/tests/partial_application/preloading.phpt index 23ad6edf2c4c..ccbbf2290811 100644 --- a/Zend/tests/partial_application/preloading.phpt +++ b/Zend/tests/partial_application/preloading.phpt @@ -4,6 +4,7 @@ PFA preloading opcache.enable=1 opcache.enable_cli=1 opcache.preload={PWD}/preloading.inc +opcache.file_cache_only=0 --SKIPIF-- Date: Sat, 25 Jul 2026 23:56:15 +0200 Subject: [PATCH 2/2] Add OpenSSL TLS 1.3 early data (0-RTT) support (#22802) This builds on the session resumption support to let clients send and servers accept TLS 1.3 early data through the stream SSL context. Clients set the early_data context option (a string) to send 0-RTT data, which is only sent when the resumed session allows it. Servers enable acceptance with max_early_data and receive the data through the early_data_cb callback, so nothing is buffered on the stream. The outcome is reported as 'accepted', 'rejected' or 'not_sent' in the early_data key of the crypto stream_get_meta_data() array. Enabling early data also shares the server SSL_CTX across accepted connections and keeps the session cache on, both of which 0-RTT resumption requires. --- NEWS | 2 + UPGRADING | 5 + ext/openssl/tests/tls_early_data_basic.phpt | 89 ++++++ ext/openssl/tests/tls_early_data_invalid.phpt | 50 ++++ ext/openssl/tests/tls_early_data_large.phpt | 80 +++++ .../tests/tls_early_data_unsupported.phpt | 79 +++++ ext/openssl/xp_ssl.c | 278 +++++++++++++++++- 7 files changed, 576 insertions(+), 7 deletions(-) create mode 100644 ext/openssl/tests/tls_early_data_basic.phpt create mode 100644 ext/openssl/tests/tls_early_data_invalid.phpt create mode 100644 ext/openssl/tests/tls_early_data_large.phpt create mode 100644 ext/openssl/tests/tls_early_data_unsupported.phpt diff --git a/NEWS b/NEWS index 484e462698d9..dfcbac915197 100644 --- a/NEWS +++ b/NEWS @@ -366,6 +366,8 @@ PHP NEWS and Openssl\Session class. (Jakub Zelenka) . Added TLS external PSK support for streams with new context options and Openssl\Psk class. (Jakub Zelenka) + . Added TLS 1.3 early data (0-RTT) support for streams with new context + options early_data, max_early_data and early_data_cb. (Jakub Zelenka) . Added stream crypto status for exposing OpenSSL WANT_READ / WANT_WRITE. (Jakub Zelenka) diff --git a/UPGRADING b/UPGRADING index 93245f87f42a..34817c10974f 100644 --- a/UPGRADING +++ b/UPGRADING @@ -299,6 +299,11 @@ PHP 8.6 UPGRADE NOTES RFC: https://wiki.php.net/rfc/tls_session_resumption . Added TLS external PSK support for streams with new stream context options: psk_client_cb and psk_server_cb. This allows setting and receiving PSK. + . Added TLS 1.3 early data (0-RTT) support for streams. Clients send early + data with the early_data context option; servers accept it with + max_early_data and receive it through the early_data_cb callback. The + outcome is reported as 'accepted', 'rejected' or 'not_sent' in the + early_data key of the crypto stream_get_meta_data() array. - Phar: . Overriding the getMTime() and getPathname() methods of SplFileInfo now diff --git a/ext/openssl/tests/tls_early_data_basic.phpt b/ext/openssl/tests/tls_early_data_basic.phpt new file mode 100644 index 000000000000..9e4691192705 --- /dev/null +++ b/ext/openssl/tests/tls_early_data_basic.phpt @@ -0,0 +1,89 @@ +--TEST-- +TLS 1.3 early data (0-RTT) basic client/server round-trip +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'session_id_context' => 'early-data-test', + 'max_early_data' => 16384, + 'early_data_cb' => function ($stream, string $data) use (&$early) { + $early .= $data; + }, + ]]); + + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx); + phpt_notify_server_start($server); + + for ($i = 0; $i < 2; $i++) { + $early = ''; + $client = @stream_socket_accept($server, 30); + if ($client) { + $meta = stream_get_meta_data($client); + $status = $meta['crypto']['early_data'] ?? 'none'; + fwrite($client, "early=[$early] status=$status\n"); + fclose($client); + } + } +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $sessionData = null; + + $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, capture the resumable session */ + $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, + 30, STREAM_CLIENT_CONNECT, $ctx); + echo trim(fgets($client1)) . "\n"; + fclose($client1); + + /* Second connection: resume and send 0-RTT early data */ + $ctx2 = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => $sessionData, + 'early_data' => 'ping-0rtt', + ]]); + + $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, + 30, STREAM_CLIENT_CONNECT, $ctx2); + echo trim(fgets($client2)) . "\n"; + $meta2 = stream_get_meta_data($client2); + echo "client early_data: " . ($meta2['crypto']['early_data'] ?? 'none') . "\n"; + fclose($client2); +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('tls_early_data_basic', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +early=[] status=not_sent +early=[ping-0rtt] status=accepted +client early_data: accepted diff --git a/ext/openssl/tests/tls_early_data_invalid.phpt b/ext/openssl/tests/tls_early_data_invalid.phpt new file mode 100644 index 000000000000..2989f5481ce3 --- /dev/null +++ b/ext/openssl/tests/tls_early_data_invalid.phpt @@ -0,0 +1,50 @@ +--TEST-- +TLS 1.3 early data with an invalid early_data option raises TypeError +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + ['local_cert' => '%s']]); + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx); + phpt_notify_server_start($server); + @stream_socket_accept($server, 3); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'early_data' => 123, + ]]); + try { + @stream_socket_client('tls://{{ ADDR }}', $errno, $errstr, + 5, STREAM_CLIENT_CONNECT, $ctx); + echo "no exception\n"; + } catch (TypeError $e) { + echo "caught: ", $e->getMessage(), "\n"; + } +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('tls_early_data_invalid', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +caught: early_data must be a string diff --git a/ext/openssl/tests/tls_early_data_large.phpt b/ext/openssl/tests/tls_early_data_large.phpt new file mode 100644 index 000000000000..15940f866d7d --- /dev/null +++ b/ext/openssl/tests/tls_early_data_large.phpt @@ -0,0 +1,80 @@ +--TEST-- +TLS 1.3 early data (0-RTT) larger than one read chunk +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'session_id_context' => 'early-data-large', + 'max_early_data' => 65536, + 'early_data_cb' => function ($stream, string $data) use (&$early) { + $early .= $data; + }, + ]]); + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx); + phpt_notify_server_start($server); + for ($i = 0; $i < 2; $i++) { + $early = ''; + $client = @stream_socket_accept($server, 30); + if ($client) { + $meta = stream_get_meta_data($client); + $status = $meta['crypto']['early_data'] ?? 'none'; + fwrite($client, "len=" . strlen($early) . " ok=" . + (int) ($early === str_repeat('A', 40000)) . " status=$status\n"); + fclose($client); + } + } +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $sessionData = null; + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => function ($stream, $session) use (&$sessionData) { + $sessionData = $session; + }, + ]]); + $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, + 30, STREAM_CLIENT_CONNECT, $ctx); + echo trim(fgets($client1)) . "\n"; + fclose($client1); + + $ctx2 = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => $sessionData, + 'early_data' => str_repeat('A', 40000), + ]]); + $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, + 30, STREAM_CLIENT_CONNECT, $ctx2); + echo trim(fgets($client2)) . "\n"; + fclose($client2); +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('tls_early_data_large', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +len=0 ok=0 status=not_sent +len=40000 ok=1 status=accepted diff --git a/ext/openssl/tests/tls_early_data_unsupported.phpt b/ext/openssl/tests/tls_early_data_unsupported.phpt new file mode 100644 index 000000000000..8d121dc9f48b --- /dev/null +++ b/ext/openssl/tests/tls_early_data_unsupported.phpt @@ -0,0 +1,79 @@ +--TEST-- +TLS 1.3 early data option is ignored when the session does not allow 0-RTT +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + [ + 'local_cert' => '%s', + 'session_cache' => true, + 'session_id_context' => 'early-data-off', + ]]); + $server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx); + phpt_notify_server_start($server); + for ($i = 0; $i < 2; $i++) { + $client = @stream_socket_accept($server, 30); + if ($client) { + fwrite($client, "hello\n"); + fclose($client); + } + } +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $sessionData = null; + $ctx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_new_cb' => function ($stream, $session) use (&$sessionData) { + $sessionData = $session; + }, + ]]); + $client1 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, + 30, STREAM_CLIENT_CONNECT, $ctx); + echo trim(fgets($client1)) . "\n"; + fclose($client1); + + $ctx2 = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + 'session_data' => $sessionData, + 'early_data' => 'ping-0rtt', + ]]); + $client2 = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, + 30, STREAM_CLIENT_CONNECT, $ctx2); + echo trim(fgets($client2)) . "\n"; + $meta2 = stream_get_meta_data($client2); + echo "reused: " . ($meta2['crypto']['session_reused'] ? 'yes' : 'no') . "\n"; + echo "early_data key present: " . (array_key_exists('early_data', $meta2['crypto']) ? 'yes' : 'no') . "\n"; + fclose($client2); +CODE; + +include 'CertificateGenerator.inc'; +$certificateGenerator = new CertificateGenerator(); +$certificateGenerator->saveNewCertAsFileWithKey('tls_early_data_unsupported', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +hello +hello +reused: yes +early_data key present: no diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index f2adfc49075f..a8105a15c43b 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -188,6 +188,24 @@ typedef struct _php_openssl_psk_callbacks_t { zend_fcall_info_cache server_cb; } php_openssl_psk_callbacks_t; +#ifdef HAVE_TLS13 +/* TLS 1.3 early data (0-RTT) handshake phase */ +typedef enum { + PHP_OPENSSL_EARLY_DATA_NONE = 0, + PHP_OPENSSL_EARLY_DATA_ACTIVE, + PHP_OPENSSL_EARLY_DATA_DONE, +} php_openssl_early_data_state_t; + +/* Size of the buffer used to drain server-side early data chunk by chunk */ +#define PHP_OPENSSL_EARLY_DATA_CHUNK 16384 + +/* Holds the server early data callback */ +typedef struct _php_openssl_early_data_callbacks_t { + int refcount; + zend_fcall_info_cache read_cb; +} php_openssl_early_data_callbacks_t; +#endif + /* Holds session callback */ typedef struct _php_openssl_session_callbacks_t { int refcount; @@ -222,6 +240,14 @@ typedef struct _php_openssl_netstream_data_t { * psk_use_session_cb call but OpenSSL doesn't free it, so we own it. */ unsigned char *psk_identity_buf; size_t psk_identity_len; +#ifdef HAVE_TLS13 + /* TLS 1.3 early data (0-RTT) */ + php_openssl_early_data_callbacks_t *early_data_callbacks; + /* Client payload to send as early data, borrowed for the handshake */ + zend_string *early_data_send; + size_t early_data_offset; + php_openssl_early_data_state_t early_data_state; +#endif char *url_name; unsigned state_set:1; unsigned _spare:31; @@ -1657,6 +1683,14 @@ static SSL_SESSION *php_openssl_psk_build_session(SSL *ssl, return NULL; } +#ifdef HAVE_TLS13 + /* Allow 0-RTT for PSK sessions (0 on the client, so a no-op there) */ + uint32_t max_early_data = SSL_get_max_early_data(ssl); + if (max_early_data > 0) { + SSL_SESSION_set_max_early_data(sess, max_early_data); + } +#endif + return sess; } @@ -2029,6 +2063,100 @@ static zend_result php_openssl_setup_server_psk(php_stream *stream, return SUCCESS; } +#ifdef HAVE_TLS13 +/* Stash the client early data (0-RTT) payload to be sent during the handshake. */ +static zend_result php_openssl_setup_client_early_data(php_stream *stream, + php_openssl_netstream_data_t *sslsock) +{ + zval *val; + + if (!GET_VER_OPT("early_data")) { + return SUCCESS; + } + + if (Z_TYPE_P(val) != IS_STRING) { + zend_type_error("early_data must be a string"); + return FAILURE; + } + + if (Z_STRLEN_P(val) > 0) { + sslsock->early_data_send = zend_string_dup(Z_STR_P(val), php_stream_is_persistent(stream)); + } + + return SUCCESS; +} + +/* Configure server acceptance of early data (0-RTT) and the receive callback. */ +static zend_result php_openssl_setup_server_early_data(php_stream *stream, + php_openssl_netstream_data_t *sslsock) +{ + zval *val; + zend_long max_early_data = 0; + + if (GET_VER_OPT("max_early_data")) { + max_early_data = zval_get_long(val); + if (max_early_data < 0 || (uint64_t) max_early_data > UINT32_MAX) { + zend_value_error("max_early_data must be between 0 and %u", UINT32_MAX); + return FAILURE; + } + } + + if (GET_VER_OPT("early_data_cb")) { + if (php_stream_is_persistent(stream)) { + php_stream_warn(stream, PersistentNotSupported, + "early_data_cb is not supported for persistent streams"); + return FAILURE; + } + + char *is_callable_error = NULL; + zend_fcall_info_cache fcc = {0}; + if (!zend_is_callable_ex(val, NULL, 0, NULL, &fcc, &is_callable_error)) { + if (is_callable_error) { + zend_type_error("early_data_cb must be a valid callback, %s", is_callable_error); + efree(is_callable_error); + } else { + zend_type_error("early_data_cb must be a valid callback"); + } + return FAILURE; + } + + sslsock->early_data_callbacks = pecalloc(1, sizeof(php_openssl_early_data_callbacks_t), 0); + sslsock->early_data_callbacks->refcount = 1; + zend_fcc_addref(&fcc); + sslsock->early_data_callbacks->read_cb = fcc; + + /* A callback without an explicit limit still enables 0-RTT */ + if (max_early_data == 0) { + max_early_data = PHP_OPENSSL_EARLY_DATA_CHUNK; + } + } + + if (max_early_data > 0) { + SSL_CTX_set_max_early_data(sslsock->ctx, (uint32_t) max_early_data); + /* Accept as much as we advertise (recv limit defaults to 16 KB otherwise) */ + SSL_CTX_set_recv_max_early_data(sslsock->ctx, (uint32_t) max_early_data); + /* 0-RTT requires resumption, so ensure the server cache is not left off */ + if (SSL_CTX_get_session_cache_mode(sslsock->ctx) == SSL_SESS_CACHE_OFF) { + SSL_CTX_set_session_cache_mode(sslsock->ctx, SSL_SESS_CACHE_SERVER); + } + } + + return SUCCESS; +} + +/* Whether the server context enables early data (0-RTT). */ +static bool php_openssl_is_server_early_data_enabled(php_stream *stream) +{ + zval *val; + + if (GET_VER_OPT("early_data_cb")) { + return true; + } + + return GET_VER_OPT("max_early_data") && zval_get_long(val) > 0; +} +#endif + /** * OpenSSL new session callback - called when a new session is established */ @@ -2558,6 +2686,11 @@ static zend_result php_openssl_create_server_ctx(php_stream *stream, if (FAILURE == php_openssl_setup_client_psk(stream, sslsock)) { return FAILURE; } +#ifdef HAVE_TLS13 + if (FAILURE == php_openssl_setup_client_early_data(stream, sslsock)) { + return FAILURE; + } +#endif } else if (PHP_STREAM_CONTEXT(stream)) { if (FAILURE == php_openssl_setup_server_session(stream, sslsock)) { return FAILURE; @@ -2565,6 +2698,11 @@ static zend_result php_openssl_create_server_ctx(php_stream *stream, if (FAILURE == php_openssl_setup_server_psk(stream, sslsock)) { return FAILURE; } +#ifdef HAVE_TLS13 + if (FAILURE == php_openssl_setup_server_early_data(stream, sslsock)) { + return FAILURE; + } +#endif if (FAILURE == php_openssl_set_server_specific_opts(stream, sslsock->ctx)) { return FAILURE; } @@ -2621,6 +2759,12 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, parent_sslsock->psk_callbacks->refcount++; sslsock->psk_callbacks = parent_sslsock->psk_callbacks; } +#ifdef HAVE_TLS13 + if (parent_sslsock->early_data_callbacks) { + parent_sslsock->early_data_callbacks->refcount++; + sslsock->early_data_callbacks = parent_sslsock->early_data_callbacks; + } +#endif sslsock->ssl_handle = SSL_new(sslsock->ctx); if (!sslsock->ssl_handle) { @@ -2739,6 +2883,80 @@ static zend_result php_openssl_set_blocking(php_openssl_netstream_data_t *sslsoc return result; } +#ifdef HAVE_TLS13 +/* Send pending client early data (0-RTT), then connect. Returns like SSL_connect(). */ +static int php_openssl_handshake_client_early_data(php_openssl_netstream_data_t *sslsock) +{ + while (sslsock->early_data_state == PHP_OPENSSL_EARLY_DATA_ACTIVE) { + size_t written = 0; + int ret = SSL_write_early_data(sslsock->ssl_handle, + ZSTR_VAL(sslsock->early_data_send) + sslsock->early_data_offset, + ZSTR_LEN(sslsock->early_data_send) - sslsock->early_data_offset, + &written); + if (ret <= 0) { + return ret; + } + sslsock->early_data_offset += written; + if (sslsock->early_data_offset >= ZSTR_LEN(sslsock->early_data_send)) { + sslsock->early_data_state = PHP_OPENSSL_EARLY_DATA_DONE; + } + } + + return SSL_connect(sslsock->ssl_handle); +} + +/* Receive server early data (0-RTT) chunk by chunk, then accept. Returns like SSL_accept(). */ +static int php_openssl_handshake_server_early_data(php_stream *stream, + php_openssl_netstream_data_t *sslsock) +{ + unsigned char buf[PHP_OPENSSL_EARLY_DATA_CHUNK]; + + while (sslsock->early_data_state == PHP_OPENSSL_EARLY_DATA_ACTIVE) { + size_t readbytes = 0; + int ret = SSL_read_early_data(sslsock->ssl_handle, buf, sizeof(buf), &readbytes); + + if (ret == SSL_READ_EARLY_DATA_ERROR) { + /* Caller inspects SSL_get_error() for WANT_READ/WRITE or error */ + return 0; + } + + if (ret == SSL_READ_EARLY_DATA_SUCCESS) { + if (readbytes > 0 && sslsock->early_data_callbacks + && ZEND_FCC_INITIALIZED(sslsock->early_data_callbacks->read_cb)) { + zval args[2]; + ZVAL_RES(&args[0], stream->res); + ZVAL_STRINGL(&args[1], (char *) buf, readbytes); + zend_call_known_fcc(&sslsock->early_data_callbacks->read_cb, + NULL, 2, args, NULL); + zval_ptr_dtor(&args[1]); + } + continue; + } + + /* SSL_READ_EARLY_DATA_FINISH */ + sslsock->early_data_state = PHP_OPENSSL_EARLY_DATA_DONE; + } + + return SSL_accept(sslsock->ssl_handle); +} +#endif + +/* Perform one handshake step, processing early data (0-RTT) first when active. */ +static int php_openssl_do_handshake(php_stream *stream, php_openssl_netstream_data_t *sslsock) +{ +#ifdef HAVE_TLS13 + if (sslsock->early_data_state == PHP_OPENSSL_EARLY_DATA_ACTIVE) { + return sslsock->is_client + ? php_openssl_handshake_client_early_data(sslsock) + : php_openssl_handshake_server_early_data(stream, sslsock); + } +#endif + + return sslsock->is_client + ? SSL_connect(sslsock->ssl_handle) + : SSL_accept(sslsock->ssl_handle); +} + static int php_openssl_enable_crypto(php_stream *stream, php_openssl_netstream_data_t *sslsock, php_stream_xport_crypto_param *cparam) /* {{{ */ @@ -2769,9 +2987,23 @@ static int php_openssl_enable_crypto(php_stream *stream, php_openssl_enable_client_sni(stream, sslsock); #endif SSL_set_connect_state(sslsock->ssl_handle); +#ifdef HAVE_TLS13 + /* Only send early data if the resumed session allows it */ + if (sslsock->early_data_send != NULL) { + SSL_SESSION *sess = SSL_get_session(sslsock->ssl_handle); + if (sess != NULL && SSL_SESSION_get_max_early_data(sess) > 0) { + sslsock->early_data_state = PHP_OPENSSL_EARLY_DATA_ACTIVE; + } + } +#endif } else { php_openssl_init_server_reneg_limit(stream, sslsock); SSL_set_accept_state(sslsock->ssl_handle); +#ifdef HAVE_TLS13 + if (SSL_get_max_early_data(sslsock->ssl_handle) > 0) { + sslsock->early_data_state = PHP_OPENSSL_EARLY_DATA_ACTIVE; + } +#endif } sslsock->state_set = 1; } @@ -2795,11 +3027,7 @@ static int php_openssl_enable_crypto(php_stream *stream, struct timeval cur_time, elapsed_time; ERR_clear_error(); - if (sslsock->is_client) { - n = SSL_connect(sslsock->ssl_handle); - } else { - n = SSL_accept(sslsock->ssl_handle); - } + n = php_openssl_do_handshake(stream, sslsock); if (has_timeout) { gettimeofday(&cur_time, NULL); @@ -3198,6 +3426,19 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle) /* {{{ efree(sslsock->psk_identity_buf); } +#ifdef HAVE_TLS13 + if (sslsock->early_data_callbacks && --sslsock->early_data_callbacks->refcount == 0) { + if (ZEND_FCC_INITIALIZED(sslsock->early_data_callbacks->read_cb)) { + zend_fcc_dtor(&sslsock->early_data_callbacks->read_cb); + } + pefree(sslsock->early_data_callbacks, php_stream_is_persistent(stream)); + } + + if (sslsock->early_data_send) { + zend_string_release(sslsock->early_data_send); + } +#endif + pefree(sslsock, php_stream_is_persistent(stream)); return 0; @@ -3335,6 +3576,25 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val 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_TLS13 + /* Report early data (0-RTT) outcome only when it was attempted */ + if (sslsock->early_data_state == PHP_OPENSSL_EARLY_DATA_DONE) { + const char *early_data_status; + switch (SSL_get_early_data_status(sslsock->ssl_handle)) { + case SSL_EARLY_DATA_ACCEPTED: + early_data_status = "accepted"; + break; + case SSL_EARLY_DATA_REJECTED: + early_data_status = "rejected"; + break; + default: + early_data_status = "not_sent"; + break; + } + add_assoc_string(&tmp, "early_data", early_data_status); + } +#endif + #ifdef HAVE_TLS_ALPN { const unsigned char *alpn_proto = NULL; @@ -3558,8 +3818,12 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val 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)) { + /* Create SSL_CTX early to share it for session resumption and early data */ + if (php_openssl_is_session_cache_enabled(stream, false) +#ifdef HAVE_TLS13 + || php_openssl_is_server_early_data_enabled(stream) +#endif + ) { if (FAILURE == php_openssl_create_server_ctx(stream, sslsock, sslsock->method)) { xparam->outputs.returncode = -1; }