diff --git a/NEWS b/NEWS index c548f32d052a..d34e08c69caa 100644 --- a/NEWS +++ b/NEWS @@ -326,6 +326,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 145f2239b56b..e02304f35df8 100644 --- a/UPGRADING +++ b/UPGRADING @@ -288,6 +288,11 @@ PHP 8.6 UPGRADE NOTES RFC: https://wiki.php.net/rfc/tls_session_resumption . Added TLS external PSK support for streams with new strem 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 d4158634e5bc..75fb86e2bbd8 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -188,6 +188,22 @@ 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 */ +#define PHP_OPENSSL_EARLY_DATA_NONE 0 +#define PHP_OPENSSL_EARLY_DATA_ACTIVE 1 +#define PHP_OPENSSL_EARLY_DATA_DONE 2 + +/* 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 +238,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; + unsigned char early_data_state; +#endif char *url_name; unsigned state_set:1; unsigned _spare:31; @@ -1657,6 +1681,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 +2061,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 +2684,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 +2696,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 +2757,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 +2881,83 @@ 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 retval; + ZVAL_RES(&args[0], stream->res); + ZVAL_STRINGL(&args[1], (char *) buf, readbytes); + ZVAL_UNDEF(&retval); + zend_call_known_fcc(&sslsock->early_data_callbacks->read_cb, + &retval, 2, args, NULL); + zval_ptr_dtor(&args[1]); + zval_ptr_dtor(&retval); + } + 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 +2988,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 +3028,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 +3427,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 +3577,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 +3819,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; }