aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexandr Nedvedicky <[email protected]>2026-04-09 12:03:52 +0200
committerNorbert Pocs <[email protected]>2026-05-13 09:32:57 +0200
commitdc7931e5412784167c40caccf1e0f6374257d90c (patch)
treee55126fddf228b6b78e6fc2170f20b821103009f
parentMake test_ssl_trace() in sslapi test less strict (diff)
downloadopenssl-feature/quic-0-rtt.tar.gz
openssl-feature/quic-0-rtt.zip
make TLS session resumption working for QUICfeature/quic-0-rtt
getting the TLS session resumption to work for QUIC is the first step towards supporting 0-RTT. Reviewed-by: Matt Caswell <[email protected]> Reviewed-by: Neil Horman <[email protected]> MergeDate: Wed May 13 07:32:58 2026 (Merged from https://github.com/openssl/openssl/pull/30750)
-rw-r--r--include/internal/quic_channel.h2
-rw-r--r--include/internal/quic_ssl.h8
-rw-r--r--ssl/quic/quic_channel.c63
-rw-r--r--ssl/quic/quic_channel_local.h2
-rw-r--r--ssl/quic/quic_impl.c47
-rw-r--r--ssl/ssl_lib.c38
-rw-r--r--ssl/ssl_sess.c6
-rw-r--r--test/build.info6
-rw-r--r--test/helpers/quictestlib.c312
-rw-r--r--test/helpers/quictestlib.h4
-rw-r--r--test/quic_0rtt_test.c475
-rw-r--r--test/recipes/70-test_quic_0rtt.t27
12 files changed, 977 insertions, 13 deletions
diff --git a/include/internal/quic_channel.h b/include/internal/quic_channel.h
index 147642d0fb5..7498a3b48d8 100644
--- a/include/internal/quic_channel.h
+++ b/include/internal/quic_channel.h
@@ -545,6 +545,8 @@ int ossl_quic_bind_channel(QUIC_CHANNEL *ch, const BIO_ADDR *peer,
void ossl_quic_channel_set_tcause(QUIC_CHANNEL *ch, uint64_t app_error_code,
const char *app_reason);
+
+void ossl_quic_update_parms_from_session(QUIC_CHANNEL *ch);
#endif
#endif
diff --git a/include/internal/quic_ssl.h b/include/internal/quic_ssl.h
index 88168d169c2..f5a63ff180d 100644
--- a/include/internal/quic_ssl.h
+++ b/include/internal/quic_ssl.h
@@ -181,6 +181,14 @@ int ossl_quic_conn_poll_events(SSL *ssl, uint64_t events, int do_tick,
int ossl_quic_get_notifier_fd(SSL *ssl);
void ossl_quic_enter_blocking_section(SSL *ssl, QUIC_REACTOR_WAIT_CTX *wctx);
void ossl_quic_leave_blocking_section(SSL *ssl, QUIC_REACTOR_WAIT_CTX *wctx);
+int ossl_quic_read_early_data(SSL *s, void *buf, size_t num, size_t *readbytes);
+int ossl_quic_write_early_data(SSL *s, const void *buf, size_t num, size_t *written);
+int ossl_quic_get_early_data_status(const SSL *s);
+int ossl_quic_set_max_early_data(SSL *ssl, uint32_t max_early_data);
+uint32_t ossl_quic_get_max_early_data(const SSL *ssl);
+int ossl_quic_set_recv_max_early_data(SSL *ssl, uint32_t recv_max_early_data);
+uint32_t ossl_quic_get_recv_max_early_data(const SSL *ssl);
+void ossl_quic_update_params(SSL *s);
#endif
diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c
index 3ce445c1a64..87eca9da0c4 100644
--- a/ssl/quic/quic_channel.c
+++ b/ssl/quic/quic_channel.c
@@ -434,7 +434,7 @@ void ossl_quic_channel_bind_qrx(QUIC_CHANNEL *tserver_ch, OSSL_QRX *qrx)
QUIC_CHANNEL *ossl_quic_channel_alloc(const QUIC_CHANNEL_ARGS *args)
{
- QUIC_CHANNEL *ch = NULL;
+ QUIC_CHANNEL *ch;
if ((ch = OPENSSL_zalloc(sizeof(*ch))) == NULL)
return NULL;
@@ -450,10 +450,8 @@ QUIC_CHANNEL *ossl_quic_channel_alloc(const QUIC_CHANNEL_ARGS *args)
ch->use_qlog = args->use_qlog;
if (ch->use_qlog && args->qlog_title != NULL) {
- if ((ch->qlog_title = OPENSSL_strdup(args->qlog_title)) == NULL) {
- OPENSSL_free(ch);
- return NULL;
- }
+ if ((ch->qlog_title = OPENSSL_strdup(args->qlog_title)) == NULL)
+ goto err;
}
#endif
@@ -473,10 +471,18 @@ QUIC_CHANNEL *ossl_quic_channel_alloc(const QUIC_CHANNEL_ARGS *args)
if (!ossl_quic_rxfc_init(&ch->conn_rxfc, NULL,
ch->tx_init_max_data,
DEFAULT_CONN_RXFC_MAX_WND_MUL * ch->tx_init_max_data,
- get_time, ch))
- return NULL;
+ get_time, ch)) {
+ goto err;
+ }
return ch;
+
+err:
+#ifndef OPENSSL_NO_QLOG
+ OPENSSL_free(ch->qlog_title);
+#endif
+ OPENSSL_free(ch);
+ return NULL;
}
void ossl_quic_channel_free(QUIC_CHANNEL *ch)
@@ -1614,7 +1620,7 @@ static int ch_on_transport_params(const unsigned char *params,
goto malformed;
}
- assert(ch->max_local_streams_bidi == 0);
+ assert(ch->resumed || ch->max_local_streams_bidi == 0);
ch->max_local_streams_bidi = v;
ch->rx_init_max_streams_bidi = v;
got_initial_max_streams_bidi = 1;
@@ -1633,7 +1639,7 @@ static int ch_on_transport_params(const unsigned char *params,
goto malformed;
}
- assert(ch->max_local_streams_uni == 0);
+ assert(ch->resumed || ch->max_local_streams_uni == 0);
ch->max_local_streams_uni = v;
ch->rx_init_max_streams_uni = v;
got_initial_max_streams_uni = 1;
@@ -4343,3 +4349,42 @@ uint64_t ossl_quic_channel_get_active_conn_id_limit_peer_request(const QUIC_CHAN
{
return ch->rx_active_conn_id_limit;
}
+
+/*
+ * The function updates QUIC protocol parameters from session.
+ * It complements code implemented ch_on_transport_params) where
+ * protocol parameters are saved to session.
+ *
+ * Function here is called on behalf of SSL_set_session() which
+ * populates QUIC protocol parameters from session.
+ *
+ * We will need to revisit the code which handles session and
+ * parameters. Server may want to override parameters from session because the
+ * session might be resumed on different network path. So we may want to
+ * re-think which parameters are kept along the ssession.
+ */
+void ossl_quic_update_parms_from_session(QUIC_CHANNEL *ch)
+{
+ SSL_SESSION *session;
+
+ if (ch == NULL || ch->tls == NULL)
+ return;
+
+ session = SSL_CONNECTION_FROM_SSL(ch->tls)->session;
+ if (session == NULL)
+ return;
+
+ ch->tx_init_max_stream_data_bidi_local = session->quic_params.init_max_stream_data_bidi_local;
+ ch->tx_init_max_stream_data_bidi_remote = session->quic_params.init_max_stream_data_bidi_remote;
+ ch->tx_init_max_stream_data_uni = session->quic_params.init_max_stream_data_uni;
+ ch->rx_max_udp_payload_size = session->quic_params.max_udp_payload_size;
+ ch->tx_init_max_data = session->quic_params.init_max_data;
+ ch->max_idle_timeout_local_req = /* sent in transport params */
+ session->quic_params.max_idle_timeout;
+ ch->max_idle_timeout = /* negotiated value */
+ session->quic_params.max_idle_timeout;
+ ch->rx_active_conn_id_limit = session->quic_params.active_conn_id_limit;
+ ch->max_local_streams_bidi = session->quic_params.max_local_streams_bidi;
+ ch->max_local_streams_uni = session->quic_params.max_local_streams_uni;
+ ch->resumed = 1;
+}
diff --git a/ssl/quic/quic_channel_local.h b/ssl/quic/quic_channel_local.h
index 6dea6fdf1b2..f726b9555e9 100644
--- a/ssl/quic/quic_channel_local.h
+++ b/ssl/quic/quic_channel_local.h
@@ -482,6 +482,8 @@ struct quic_channel_st {
/* Has qlog been requested? */
unsigned int is_tserver_ch : 1;
+ unsigned int resumed : 1;
+
/* Saved error stack in case permanent error was encountered */
ERR_STATE *err_state;
diff --git a/ssl/quic/quic_impl.c b/ssl/quic/quic_impl.c
index 8fb60911714..2556190ed00 100644
--- a/ssl/quic/quic_impl.c
+++ b/ssl/quic/quic_impl.c
@@ -5976,3 +5976,50 @@ int ossl_quic_set_diag_title(SSL_CTX *ctx, const char *title)
return 1;
}
+
+int ossl_quic_read_early_data(SSL *s, void *buf, size_t num, size_t *readbytes)
+{
+ return 0;
+}
+
+int ossl_quic_write_early_data(SSL *s, const void *buf, size_t num, size_t *written)
+{
+ return 0;
+}
+
+int ossl_quic_get_early_data_status(const SSL *s)
+{
+ return 0;
+}
+
+int ossl_quic_set_max_early_data(SSL *ssl, uint32_t max_early_data)
+{
+ return 0;
+}
+
+uint32_t ossl_quic_get_max_early_data(const SSL *ssl)
+{
+ return 0;
+}
+
+int ossl_quic_set_recv_max_early_data(SSL *ssl, uint32_t recv_max_early_data)
+{
+ return 0;
+}
+
+uint32_t ossl_quic_get_recv_max_early_data(const SSL *ssl)
+{
+ return 0;
+}
+
+void ossl_quic_update_params(SSL *s)
+{
+ QCTX ctx;
+
+ if (!expect_quic_conn_only(s, &ctx))
+ return;
+
+ qctx_lock(&ctx);
+ ossl_quic_update_parms_from_session(ctx.qc->ch);
+ qctx_unlock(&ctx);
+}
diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c
index be5f4a20f02..53d17dce9e7 100644
--- a/ssl/ssl_lib.c
+++ b/ssl/ssl_lib.c
@@ -2511,12 +2511,16 @@ int SSL_read_early_data(SSL *s, void *buf, size_t num, size_t *readbytes)
int ret;
SSL_CONNECTION *sc = SSL_CONNECTION_FROM_SSL_ONLY(s);
- /* TODO(QUIC 0RTT): 0-RTT support */
if (sc == NULL) {
ERR_raise(ERR_LIB_SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
return SSL_READ_EARLY_DATA_ERROR;
}
+#ifndef OPENSSL_NO_QUIC
+ if (IS_QUIC(s))
+ return ossl_quic_read_early_data(s, buf, num, readbytes);
+#endif
+
if (ssl_reset_error_state(sc) == 0)
return 0;
@@ -2576,10 +2580,14 @@ int SSL_get_early_data_status(const SSL *s)
{
const SSL_CONNECTION *sc = SSL_CONNECTION_FROM_CONST_SSL_ONLY(s);
- /* TODO(QUIC 0RTT): 0-RTT support */
if (sc == NULL)
return 0;
+#ifndef OPENSSL_NO_QUIC
+ if (IS_QUIC(s))
+ return ossl_quic_get_early_data_status(s);
+#endif
+
return sc->ext.early_data;
}
@@ -2861,7 +2869,11 @@ int SSL_write_early_data(SSL *s, const void *buf, size_t num, size_t *written)
uint32_t partialwrite;
SSL_CONNECTION *sc = SSL_CONNECTION_FROM_SSL_ONLY(s);
- /* TODO(QUIC 0RTT): This will need special handling for QUIC */
+#ifndef OPENSSL_NO_QUIC
+ if (IS_QUIC(s))
+ return ossl_quic_write_early_data(s, buf, num, written);
+#endif
+
if (sc == NULL)
return 0;
@@ -7405,6 +7417,11 @@ int SSL_set_max_early_data(SSL *s, uint32_t max_early_data)
if (sc == NULL)
return 0;
+#ifndef OPENSSL_NO_QUIC
+ if (IS_QUIC(s))
+ return ossl_quic_set_max_early_data(s, max_early_data);
+#endif
+
sc->max_early_data = max_early_data;
return 1;
@@ -7417,6 +7434,11 @@ uint32_t SSL_get_max_early_data(const SSL *s)
if (sc == NULL)
return 0;
+#ifndef OPENSSL_NO_QUIC
+ if (IS_QUIC(s))
+ return ossl_quic_get_max_early_data(s);
+#endif
+
return sc->max_early_data;
}
@@ -7439,6 +7461,11 @@ int SSL_set_recv_max_early_data(SSL *s, uint32_t recv_max_early_data)
if (sc == NULL)
return 0;
+#ifndef OPENSSL_NO_QUIC
+ if (IS_QUIC(s))
+ return ossl_quic_set_recv_max_early_data(s, recv_max_early_data);
+#endif
+
sc->recv_max_early_data = recv_max_early_data;
return 1;
@@ -7451,6 +7478,11 @@ uint32_t SSL_get_recv_max_early_data(const SSL *s)
if (sc == NULL)
return 0;
+#ifndef OPENSSL_NO_QUIC
+ if (IS_QUIC(s))
+ return ossl_quic_get_recv_max_early_data(s);
+#endif
+
return sc->recv_max_early_data;
}
diff --git a/ssl/ssl_sess.c b/ssl/ssl_sess.c
index 9a1af383798..99644930d7c 100644
--- a/ssl/ssl_sess.c
+++ b/ssl/ssl_sess.c
@@ -21,6 +21,7 @@
#include "statem/statem_local.h"
#ifndef OPENSSL_NO_QUIC
#include "internal/quic_types.h"
+#include "internal/quic_ssl.h"
#endif
static void SSL_SESSION_list_remove(SSL_CTX *ctx, SSL_SESSION *s);
@@ -937,6 +938,11 @@ int SSL_set_session(SSL *s, SSL_SESSION *session)
SSL_SESSION_free(sc->session);
sc->session = session;
+#ifndef OPENSSL_NO_QUIC
+ if (IS_QUIC(s))
+ ossl_quic_update_params(s);
+#endif
+
return 1;
}
diff --git a/test/build.info b/test/build.info
index c7a9bd28f6e..126f7d332b8 100644
--- a/test/build.info
+++ b/test/build.info
@@ -434,6 +434,10 @@ IF[{- !$disabled{tests} -}]
SOURCE[quic_radix_test]=helpers/ssltestlib.c $QUICTESTHELPERS
INCLUDE[quic_radix_test]=../include ../apps/include
DEPEND[quic_radix_test]=../libcrypto.a ../libssl.a libtestutil.a
+
+ SOURCE[quic_0rtt_test]=quic_0rtt_test.c helpers/ssltestlib.c $QUICTESTHELPERS
+ INCLUDE[quic_0rtt_test]=../include ../apps/include
+ DEPEND[quic_0rtt_test]=../libcrypto.a ../libssl.a libtestutil.a
ENDIF
IF[{- !$disabled{'qlog'} -}]
@@ -1318,7 +1322,7 @@ ENDIF
PROGRAMS{noinst}=quic_srtm_test quic_lcidm_test quic_rcidm_test
PROGRAMS{noinst}=quic_fifd_test quic_txp_test quic_tserver_test
PROGRAMS{noinst}=quic_client_test quic_cc_test quic_multistream_test
- PROGRAMS{noinst}=quic_radix_test
+ PROGRAMS{noinst}=quic_radix_test quic_0rtt_test
SOURCE[quic_ackm_test]=quic_ackm_test.c cc_dummy.c
INCLUDE[quic_ackm_test]=../include ../apps/include
diff --git a/test/helpers/quictestlib.c b/test/helpers/quictestlib.c
index 5405df6ebf3..7baa1ae7802 100644
--- a/test/helpers/quictestlib.c
+++ b/test/helpers/quictestlib.c
@@ -1374,3 +1374,315 @@ int bio_msg_copy(BIO_MSG *dst, BIO_MSG *src)
return 1;
}
+
+static const unsigned char alpn[] = {
+ 8, 'o', 's', 's', 'l', 't', 'e', 's', 't'
+};
+
+static int select_alpn(SSL *ssl, const unsigned char **out, unsigned char *out_len,
+ const unsigned char *in, unsigned int in_len, void *arg)
+{
+ int e;
+
+ e = SSL_select_next_proto((unsigned char **)out, out_len, alpn, sizeof(alpn),
+ in, in_len);
+ return (e == OPENSSL_NPN_NEGOTIATED) ? SSL_TLSEXT_ERR_OK : SSL_TLSEXT_ERR_ALERT_FATAL;
+}
+
+int create_quic_ctx_pair(OSSL_LIB_CTX *libctx, SSL_CTX **c_sctx_p, SSL_CTX **s_sctx_p,
+ const char *certfile, const char *keyfile)
+{
+ int ok = 0;
+ SSL_CTX *c_sctx, *s_sctx;
+
+ c_sctx = NULL;
+ s_sctx = NULL;
+ c_sctx = SSL_CTX_new_ex(libctx, NULL, OSSL_QUIC_client_method());
+ if (!TEST_ptr(c_sctx)) {
+ TEST_info("%s SSL_CTX_new_ex(OSSL_QUIC_client_method()) failed", __func__);
+ goto done;
+ }
+
+ s_sctx = SSL_CTX_new_ex(libctx, NULL, OSSL_QUIC_server_method());
+ if (!TEST_ptr(s_sctx)) {
+ TEST_info("%s SSL_CTX_new_ex(OSSL_QUIC_server_method()) failed", __func__);
+ goto done;
+ }
+
+ ok = SSL_CTX_use_certificate_file(s_sctx, certfile, SSL_FILETYPE_PEM);
+ if (ok != 1) {
+ TEST_info("%s SSL_CTX_use_certificate_file(%s) failed", __func__, certfile);
+ ok = 0;
+ goto done;
+ }
+
+ ok = SSL_CTX_use_PrivateKey_file(s_sctx, keyfile, SSL_FILETYPE_PEM);
+ if (ok != 1) {
+ TEST_info("%s SSL_CTX_use_PrivateKey_file(%s) failed", __func__, keyfile);
+ ok = 0;
+ goto done;
+ }
+ SSL_CTX_set_alpn_select_cb(s_sctx, select_alpn, NULL);
+
+ *c_sctx_p = c_sctx;
+ c_sctx = NULL;
+ *s_sctx_p = s_sctx;
+ s_sctx = NULL;
+
+done:
+ SSL_CTX_free(c_sctx);
+ SSL_CTX_free(s_sctx);
+
+ return ok;
+}
+
+static int create_dgram_pair(BIO **c_bio_p, BIO **s_bio_p)
+{
+ BIO *c_bio, *s_bio;
+ BIO_ADDR *localaddr = NULL;
+ struct in_addr ina = { (uint32_t)htonl(0x7f000001UL) };
+ int bio_flags = 0;
+ int ok;
+
+ bio_flags |= BIO_DGRAM_CAP_HANDLES_DST_ADDR;
+ bio_flags |= BIO_DGRAM_CAP_HANDLES_SRC_ADDR;
+ bio_flags |= BIO_DGRAM_CAP_PROVIDES_DST_ADDR;
+ bio_flags |= BIO_DGRAM_CAP_PROVIDES_SRC_ADDR;
+
+ c_bio = NULL;
+ s_bio = NULL;
+ ok = BIO_new_bio_dgram_pair(&c_bio, 1500, &s_bio, 1500);
+ if (ok == 0) {
+ TEST_info("%s BIO_new_bio_dgram_pair() error", __func__);
+ goto done;
+ }
+
+ ok = BIO_dgram_set_caps(c_bio, bio_flags);
+ if (ok == 0) {
+ TEST_info("%s BIO_dgram_set_caps(c_bio, bio_flags) failed", __func__);
+ goto done;
+ }
+
+ ok = BIO_dgram_set_caps(s_bio, bio_flags);
+ if (ok == 0) {
+ TEST_info("%s BIO_dgram_set_caps(s_bio, bio_flags) failed", __func__);
+ goto done;
+ }
+
+ ok = BIO_dgram_set_mtu(c_bio, 1500);
+ if (ok == 0) {
+ TEST_info("%s BIO_dgram_set_mtu(c_bio) error", __func__);
+ goto done;
+ }
+
+ ok = BIO_dgram_set_mtu(s_bio, 1500);
+ if (ok == 0) {
+ TEST_info("%s BIO_dgram_set_mtu(s_bio) error", __func__);
+ goto done;
+ }
+
+ localaddr = BIO_ADDR_new();
+ if (!TEST_ptr(localaddr)) {
+ TEST_info("%s BIO_ADDR_new() error", __func__);
+ goto done;
+ }
+ ok = BIO_ADDR_rawmake(localaddr, AF_INET, &ina, sizeof(ina), htons(4080));
+ if (ok == 0) {
+ TEST_info("%s BIO_ADDR_rawmake(4080) error", __func__);
+ goto done;
+ }
+ ok = BIO_dgram_set0_local_addr(c_bio, localaddr);
+ if (ok != 1) {
+ TEST_info("%s BIO_dgram_set0_local_addr(c_bio)", __func__);
+ ok = 0;
+ goto done;
+ }
+
+ localaddr = BIO_ADDR_new();
+ if (!TEST_ptr(localaddr)) {
+ TEST_info("%s BIO_ADDR_new() error", __func__);
+ goto done;
+ }
+ ok = BIO_ADDR_rawmake(localaddr, AF_INET, &ina, sizeof(ina), htons(8040));
+ if (ok == 0) {
+ TEST_info("%s BIO_ADDR_rawmake(8040) error", __func__);
+ goto done;
+ }
+ ok = BIO_dgram_set0_local_addr(s_bio, localaddr);
+ if (ok != 1) {
+ TEST_info("%s BIO_dgram_set0_local_addr(c_bio)", __func__);
+ ok = 0;
+ goto done;
+ }
+ localaddr = NULL;
+
+ ok = BIO_dgram_set_local_addr_enable(c_bio, 1);
+ if (ok == 0) {
+ TEST_info("%s BIO_dgram_set_local_addr_enable(c_bio)", __func__);
+ goto done;
+ }
+
+ ok = BIO_dgram_set_local_addr_enable(s_bio, 1);
+ if (ok == 0) {
+ TEST_info("%s BIO_dgram_set_local_addr_enable(s_bio)", __func__);
+ goto done;
+ }
+
+ *c_bio_p = c_bio;
+ c_bio = NULL;
+ *s_bio_p = s_bio;
+ s_bio = NULL;
+
+done:
+ BIO_free(c_bio);
+ BIO_free(s_bio);
+ BIO_ADDR_free(localaddr);
+
+ return ok;
+}
+
+static int init_client(SSL *c_ssl)
+{
+ BIO_ADDR *peer_addr = NULL;
+ struct in_addr ina = { (uint32_t)htonl(0x7f000001UL) };
+ int ok = 0;
+
+ ok = SSL_set_tlsext_host_name(c_ssl, "localhost");
+ if (ok == 0) {
+ TEST_info("%s SSL_set_tlsext_host_name()", __func__);
+ goto done;
+ }
+
+ ok = SSL_set1_dnsname(c_ssl, "localhost");
+ if (ok == 0) {
+ TEST_info("%s SSL_set1_dnsname()", __func__);
+ goto done;
+ }
+
+ ok = SSL_set_alpn_protos(c_ssl, alpn, sizeof(alpn));
+ if (ok != 0) {
+ TEST_info("%s SSL_set_alpn_protos() failed", __func__);
+ ok = 0;
+ goto done;
+ }
+
+ ok = SSL_set_blocking_mode(c_ssl, 0);
+ if (ok == 0) {
+ TEST_info("%s SSL_set_block_mode() failed", __func__);
+ goto done;
+ }
+
+ peer_addr = BIO_ADDR_new();
+ if (!TEST_ptr(peer_addr)) {
+ TEST_info("%s BIO_ADDR_new() failed", __func__);
+ goto done;
+ }
+ ok = BIO_ADDR_rawmake(peer_addr, AF_INET, &ina, sizeof(ina), htons(8040));
+ if (ok == 0) {
+ TEST_info("%s BIO_ADDR_rawmake() failed", __func__);
+ goto done;
+ }
+ ok = SSL_set1_initial_peer_addr(c_ssl, peer_addr);
+ if (ok == 0) {
+ TEST_info("%s SSL_set1_initial_peer_addr() failed", __func__);
+ goto done;
+ }
+
+done:
+ BIO_ADDR_free(peer_addr);
+
+ return ok;
+}
+
+int create_quic_conn_objects(SSL_CTX *c_sctx, SSL_CTX *s_sctx, SSL **c_ssl_p, SSL **s_ssl_p)
+{
+ BIO *c_bio, *s_bio;
+ SSL *c_ssl, *s_ssl;
+ int ok;
+
+ c_bio = NULL;
+ s_bio = NULL;
+ c_ssl = NULL;
+ s_ssl = NULL;
+ ok = create_dgram_pair(&c_bio, &s_bio);
+ if (ok == 0)
+ goto done;
+
+ c_ssl = SSL_new(c_sctx);
+ if (!TEST_ptr(c_ssl)) {
+ TEST_info("%s SSL_new(c_sctx) failed", __func__);
+ ok = 0;
+ goto done;
+ }
+
+ ok = init_client(c_ssl);
+ if (ok == 0)
+ goto done;
+
+ s_ssl = SSL_new_listener(s_sctx, 0);
+ if (!TEST_ptr(s_ssl)) {
+ TEST_info("%s SSL_new_listener() failed", __func__);
+ ok = 0;
+ goto done;
+ }
+
+ SSL_set_bio(c_ssl, c_bio, c_bio);
+ SSL_set_bio(s_ssl, s_bio, s_bio);
+ c_bio = NULL;
+ s_bio = NULL;
+
+ ok = SSL_set_blocking_mode(s_ssl, 0);
+ if (ok == 0) {
+ TEST_info("%s SSL_set_blocking_mode() failed", __func__);
+ ok = 0;
+ goto done;
+ }
+
+ *c_ssl_p = c_ssl;
+ c_ssl = NULL;
+ *s_ssl_p = s_ssl;
+ s_ssl = NULL;
+
+done:
+ BIO_free(c_bio);
+ BIO_free(s_bio);
+ SSL_free(c_ssl);
+ SSL_free(s_ssl);
+
+ return ok;
+}
+
+SSL *create_quic_client(SSL_CTX *c_sctx, BIO *c_bio)
+{
+ SSL *c_ssl;
+
+ if (!TEST_ptr(c_bio))
+ return NULL;
+
+ c_ssl = SSL_new(c_sctx);
+ if (!TEST_ptr(c_ssl)) {
+ TEST_info("%s SSL_new(c_sctx) failed", __func__);
+ return NULL;
+ }
+
+ if (BIO_up_ref(c_bio) == 0) {
+ TEST_info("%s BIO_up_ref() failed)", __func__);
+ goto error;
+ }
+ SSL_set_bio(c_ssl, c_bio, c_bio);
+
+ if (init_client(c_ssl) == 0)
+ goto error;
+
+ if (SSL_set_blocking_mode(c_ssl, 0) == 0) {
+ TEST_info("%s SSL_set_blocking_mode() failed", __func__);
+ goto error;
+ }
+
+ return c_ssl;
+
+error:
+ SSL_free(c_ssl);
+
+ return NULL;
+}
diff --git a/test/helpers/quictestlib.h b/test/helpers/quictestlib.h
index 63c77f90b78..a60d40ffe3c 100644
--- a/test/helpers/quictestlib.h
+++ b/test/helpers/quictestlib.h
@@ -289,3 +289,7 @@ const BIO_METHOD *bio_f_pkt_split_dgram_filter(void);
/* Free the BIO filter method object */
void bio_f_pkt_split_dgram_filter_free(void);
+
+int create_quic_ctx_pair(OSSL_LIB_CTX *libctx, SSL_CTX **c_sctx_p, SSL_CTX **s_sctx_p, const char *certfile, const char *keyfile);
+int create_quic_conn_objects(SSL_CTX *c_sctx, SSL_CTX *s_ctx, SSL **c_ssl_p, SSL **s_ssl_p);
+SSL *create_quic_client(SSL_CTX *c_sctx, BIO *c_bio);
diff --git a/test/quic_0rtt_test.c b/test/quic_0rtt_test.c
new file mode 100644
index 00000000000..e824017e95d
--- /dev/null
+++ b/test/quic_0rtt_test.c
@@ -0,0 +1,475 @@
+/*
+ * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License"). You may not use
+ * this file except in compliance with the License. You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include <openssl/ssl.h>
+#include <openssl/quic.h>
+#include <openssl/bio.h>
+#include <openssl/lhash.h>
+#include <openssl/rand.h>
+
+#include "helpers/quictestlib.h"
+#include "helpers/ssltestlib.h"
+
+#if defined(OPENSSL_THREADS)
+#include "internal/thread_arch.h"
+#endif
+
+#include "internal/numbers.h" /* UINT64_C */
+
+#include "testutil.h"
+
+static char *certfile, *keyfile;
+
+static OSSL_LIB_CTX *libctx = NULL;
+static OSSL_PROVIDER *defctxnull = NULL;
+
+#define MESSAGE "Hello There!"
+#define MESSAGE_LEN (sizeof(MESSAGE) - 1)
+
+static SSL *client_ssl;
+static SSL *server_ssl;
+
+static void handle_events(void)
+{
+ SSL_handle_events(client_ssl);
+ SSL_handle_events(server_ssl);
+}
+
+static void set_server(SSL *s)
+{
+ server_ssl = s;
+}
+static void set_client(SSL *s)
+{
+ client_ssl = s;
+}
+
+static int send_msg(SSL *s, const char *msg, int to_write, const char *print_msg)
+{
+ int rv, done;
+
+ if (to_write <= 0)
+ return 0;
+
+ done = 0;
+ rv = 0;
+ while (!done) {
+ rv = SSL_write(s, msg, to_write);
+ if (rv <= 0) {
+ switch (SSL_get_error(s, rv)) {
+ case SSL_ERROR_WANT_READ:
+ case SSL_ERROR_WANT_WRITE:
+ case SSL_ERROR_WANT_CONNECT:
+ case SSL_ERROR_WANT_ACCEPT:
+ rv = 0;
+ break;
+ default:
+ TEST_info("%s write failed\n", print_msg);
+ return 0;
+ }
+ } else {
+ done = 1;
+ }
+ /*
+ * Need to poke to handle_events() two times. Not doing so makes
+ * shutdown operation in caller later to fail. It then makes session
+ * resumption to fail, because instead of finishing shutdown we do
+ * SSL_free(). The exact failure mechanism is yet to be understood,
+ * making sure we call handle_events() two times here fixes/workarounds
+ * the problem. It's a magic!
+ */
+ handle_events();
+ handle_events();
+ }
+
+ return rv == to_write;
+}
+
+static int recv_msg(SSL *s, char *buf, int to_read, const char *print_msg)
+{
+ int read, rv, done;
+
+ if (to_read <= 0)
+ return 0;
+
+ done = 0;
+ read = 0;
+ while (!done) {
+ if (read < to_read) {
+ to_read = to_read - read;
+ rv = SSL_read(s, &buf[read], to_read);
+ if (rv <= 0) {
+ switch (SSL_get_error(s, rv)) {
+ case SSL_ERROR_WANT_READ:
+ case SSL_ERROR_WANT_WRITE:
+ case SSL_ERROR_WANT_CONNECT:
+ case SSL_ERROR_WANT_ACCEPT:
+ rv = 0;
+ break;
+ default:
+ TEST_info("%s read failed at %d\n", print_msg, read);
+ return 0;
+ }
+ break;
+ }
+ read += rv;
+ } else {
+ done = 1;
+ }
+ handle_events();
+ }
+
+ return done;
+}
+
+static int create_connection(SSL *clientssl, SSL *serverssl_listener, SSL **serverssl)
+{
+ int done = 0;
+ SSL *serverconn = NULL;
+
+ while (!done) {
+ done = SSL_connect(clientssl);
+ if (done != 1) {
+ switch (SSL_get_error(clientssl, done)) {
+ case SSL_ERROR_WANT_READ:
+ case SSL_ERROR_WANT_WRITE:
+ case SSL_ERROR_WANT_CONNECT:
+ case SSL_ERROR_WANT_ACCEPT:
+ done = 0;
+ break;
+ default:
+ TEST_info("SSL_connect() failure");
+ return 0;
+ }
+ }
+ done = SSL_handle_events(clientssl);
+ if (done == 0) {
+ break;
+ }
+ done = SSL_handle_events(serverssl_listener);
+ if (done == 0) {
+ break;
+ }
+ serverconn = SSL_accept_connection(serverssl_listener, 0);
+ done = (serverconn != NULL);
+ }
+
+ if (done != 0)
+ *serverssl = serverconn;
+
+ return done;
+}
+
+static int destroy_connection(SSL *clientssl, SSL *serverssl)
+{
+ int done = 0;
+
+ while (!done) {
+ done = SSL_shutdown(clientssl);
+ if (done != 1) {
+ switch (SSL_get_error(clientssl, done)) {
+ case SSL_ERROR_WANT_READ:
+ case SSL_ERROR_WANT_WRITE:
+ case SSL_ERROR_WANT_CONNECT:
+ case SSL_ERROR_WANT_ACCEPT:
+ done = 0;
+ break;
+ default:
+ TEST_info("SSL_shutdown() failure");
+ return 0;
+ }
+ }
+ done = SSL_handle_events(clientssl);
+ if (done == 0) {
+ break;
+ }
+ done = SSL_handle_events(serverssl);
+ if (done == 0) {
+ break;
+ }
+ }
+
+ return done;
+}
+
+/*
+ * Test QUIC connection using regular QUIC client and server methods.
+ * No quic t-server is needed. Runs non blocking variant using busy-loops.
+ */
+static int test_quic_conn(void)
+{
+ SSL_CTX *cctx = NULL, *sctx = NULL;
+ SSL *clientssl = NULL, *serverssl = NULL, *serverssl_listener = NULL;
+ char recv_buf[80];
+ int testresult = 0;
+
+ if (!TEST_true(create_quic_ctx_pair(libctx, &cctx, &sctx, certfile, keyfile)))
+ return 0;
+
+ if (!TEST_true(create_quic_conn_objects(cctx, sctx, &clientssl, &serverssl_listener)))
+ goto end;
+
+ testresult = SSL_listen(serverssl_listener);
+ if (testresult == 0) {
+ TEST_info("%s SSL_listen() failed", __func__);
+ goto end;
+ }
+
+ testresult = create_connection(clientssl, serverssl_listener, &serverssl);
+ if (testresult == 0) {
+ TEST_info("%s connect failed", __func__);
+ goto end;
+ }
+
+ set_server(serverssl);
+ set_client(clientssl);
+
+ testresult = send_msg(clientssl, MESSAGE, MESSAGE_LEN,
+ "test_quic_conn send_msg(clnt -> srv)");
+ if (testresult == 0)
+ goto end;
+
+ memset(recv_buf, 0, sizeof(recv_buf));
+ testresult = recv_msg(serverssl, recv_buf, MESSAGE_LEN,
+ "test_quic_conn recv_msg(srv -> clnt)");
+ if (testresult == 0)
+ goto end;
+ if (!TEST_str_eq(recv_buf, MESSAGE))
+ goto end;
+
+ testresult = send_msg(serverssl, MESSAGE, MESSAGE_LEN,
+ "test_quic_conn send_msg(srv -> clnt)");
+ if (testresult == 0)
+ goto end;
+
+ memset(recv_buf, 0, sizeof(recv_buf));
+ testresult = recv_msg(clientssl, recv_buf, MESSAGE_LEN,
+ "test_quic_conn recv_msg(srv -> clnt)");
+ if (testresult == 0)
+ goto end;
+ if (!TEST_str_eq(recv_buf, MESSAGE))
+ goto end;
+
+ testresult = 1;
+end:
+ set_server(NULL);
+ set_client(NULL);
+
+ SSL_free(clientssl);
+ SSL_free(serverssl);
+ SSL_free(serverssl_listener);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+
+ return testresult;
+}
+
+static int test_quic_resume(void)
+{
+ SSL_CTX *cctx = NULL, *sctx = NULL;
+ SSL *clientssl = NULL, *serverssl = NULL, *serverssl_listener = NULL, *new_clientssl;
+ SSL_SESSION *sess = NULL;
+ BIO *bio = NULL;
+ char recv_buf[80];
+ int testresult = 0;
+
+ if (!TEST_true(create_quic_ctx_pair(libctx, &cctx, &sctx, certfile, keyfile)))
+ return 0;
+ /*
+ * note: no t-server for testing is used here.
+ */
+ if (!TEST_true(create_quic_conn_objects(cctx, sctx, &clientssl, &serverssl_listener)))
+ goto end;
+
+ testresult = SSL_listen(serverssl_listener);
+ if (testresult == 0) {
+ TEST_info("%s SSL_listen() failed", __func__);
+ goto end;
+ }
+
+ testresult = create_connection(clientssl, serverssl_listener, &serverssl);
+ if (testresult == 0) {
+ TEST_info("%s connect failed", __func__);
+ goto end;
+ }
+
+ set_server(serverssl);
+ set_client(clientssl);
+
+ testresult = send_msg(clientssl, MESSAGE, MESSAGE_LEN,
+ "test_quic_resume send_msg(clnt -> srv)");
+ if (testresult == 0)
+ goto end;
+
+ memset(recv_buf, 0, sizeof(recv_buf));
+ testresult = recv_msg(serverssl, recv_buf, MESSAGE_LEN,
+ "test_quic_resume recv_msg(srv -> clnt)");
+ if (testresult == 0)
+ goto end;
+ if (!TEST_str_eq(recv_buf, MESSAGE))
+ goto end;
+
+ testresult = send_msg(serverssl, MESSAGE, MESSAGE_LEN,
+ "test_quic_resume send_msg(srv -> clnt)");
+ if (testresult == 0)
+ goto end;
+
+ memset(recv_buf, 0, sizeof(recv_buf));
+ testresult = recv_msg(clientssl, recv_buf, MESSAGE_LEN,
+ "test_quic_resume recv_msg(srv -> clnt)");
+ if (testresult == 0)
+ goto end;
+ if (!TEST_str_eq(recv_buf, MESSAGE))
+ goto end;
+
+ sess = SSL_get1_session(clientssl);
+ if (!TEST_ptr(sess)) {
+ TEST_info("%s SSL_get1_session() fails", __func__);
+ testresult = 0;
+ goto end;
+ }
+
+ /*
+ * Drop existing connection and create new client.
+ * The new client is using the same client BIO.
+ */
+ set_server(NULL);
+ set_client(NULL);
+ /*
+ * Need to call SSL_shutdown() in order to be able
+ * to resume session. Paragraph here comes from
+ * SSL_shutdown(3ossl)
+ *
+ * This approach of a single SSL_shutdown() call without
+ * waiting is preferable to simply calling SSL_free(3)
+ * or SSL_clear(3) as calling SSL_shutdown() beforehand
+ * makes an SSL session eligible for subsequent reuse and
+ * notifies the peer of connection shutdown.
+ *
+ * NOTE: session from test_quic_conn() can not be resumed,
+ * because the test uses SSL_free().
+ */
+ destroy_connection(clientssl, serverssl);
+ SSL_free(serverssl);
+ serverssl = NULL;
+ bio = SSL_get_rbio(clientssl);
+ if (bio != SSL_get_wbio(clientssl)) {
+ TEST_info("%s bio != SSL_get_wbio(clientssl)", __func__);
+ testresult = 0;
+ goto end;
+ }
+ /*
+ * NOTE: We can not free existing client yet, because
+ * c_bio is not copied/duplicated. Old client can
+ * be freed after create_quic_client() returns.
+ * The create_quic_client() duplicates the bio
+ * for new_clientssl.
+ */
+ new_clientssl = create_quic_client(cctx, bio);
+ if (new_clientssl == NULL) {
+ TEST_info("%s create_quic_client() failed", __func__);
+ testresult = 0;
+ goto end;
+ }
+ SSL_free(clientssl);
+ clientssl = new_clientssl;
+
+ testresult = SSL_set_session(clientssl, sess);
+ if (testresult == 0) {
+ TEST_info("%s SSL_set_session(clientssl) failed", __func__);
+ goto end;
+ }
+
+ testresult = create_connection(clientssl, serverssl_listener, &serverssl);
+ if (testresult == 0) {
+ TEST_info("%s re-connect failed", __func__);
+ goto end;
+ }
+
+ set_server(serverssl);
+ set_client(clientssl);
+
+ testresult = send_msg(clientssl, MESSAGE, MESSAGE_LEN,
+ "test_quic_resume send_msg(clnt -> srv) [resumed]");
+ if (testresult == 0)
+ goto end;
+
+ memset(recv_buf, 0, sizeof(recv_buf));
+ testresult = recv_msg(serverssl, recv_buf, MESSAGE_LEN,
+ "test_quic_resume recv_msg(srv -> clnt) [resumed]");
+ if (testresult == 0)
+ goto end;
+ if (!TEST_str_eq(recv_buf, MESSAGE))
+ goto end;
+
+ testresult = send_msg(serverssl, MESSAGE, MESSAGE_LEN,
+ "test_quic_resume send_msg(srv -> clnt) [resumed]");
+ if (testresult == 0)
+ goto end;
+
+ memset(recv_buf, 0, sizeof(recv_buf));
+ testresult = recv_msg(clientssl, recv_buf, MESSAGE_LEN,
+ "test_quic_resume recv_msg(srv -> clnt) [resumed]");
+ if (testresult == 0)
+ goto end;
+ if (!TEST_str_eq(recv_buf, MESSAGE))
+ goto end;
+
+ testresult = SSL_session_reused(clientssl);
+ if (testresult == 0) {
+ TEST_info("%s SSL_session_reused(): did not reuse", __func__);
+ goto end;
+ }
+
+ testresult = 1;
+end:
+ set_server(NULL);
+ set_client(NULL);
+ SSL_SESSION_free(sess);
+ SSL_free(serverssl);
+ SSL_free(clientssl);
+ SSL_free(serverssl_listener);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+
+ return testresult;
+}
+
+int setup_tests(void)
+{
+#if defined(OPENSSL_NO_QUIC)
+ return TEST_skip("QUIC is not supported by this build");
+#else
+ libctx = OSSL_LIB_CTX_new();
+ if (!TEST_ptr(libctx))
+ return 0;
+
+ defctxnull = OSSL_PROVIDER_load(NULL, "null");
+
+ if (!test_skip_common_options()) {
+ TEST_error("Error parsing test options\n");
+ return 0;
+ }
+
+ if (!TEST_ptr(certfile = test_get_argument(0))
+ || !TEST_ptr(keyfile = test_get_argument(1)))
+ return 0;
+
+ ADD_TEST(test_quic_conn);
+ ADD_TEST(test_quic_resume);
+
+ return 1;
+#endif
+}
+
+void cleanup_tests(void)
+{
+ OSSL_PROVIDER_unload(defctxnull);
+ OSSL_LIB_CTX_free(libctx);
+}
diff --git a/test/recipes/70-test_quic_0rtt.t b/test/recipes/70-test_quic_0rtt.t
new file mode 100644
index 00000000000..290915daa7c
--- /dev/null
+++ b/test/recipes/70-test_quic_0rtt.t
@@ -0,0 +1,27 @@
+#! /usr/bin/env perl
+# Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License"). You may not use
+# this file except in compliance with the License. You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+use OpenSSL::Test qw/:DEFAULT srctop_file result_dir data_file/;
+use OpenSSL::Test::Utils;
+use File::Temp qw(tempfile);
+use File::Path 2.00 qw(rmtree mkpath);
+
+setup("test_quic_0rtt");
+
+plan skip_all => "QUIC protocol is not supported by this OpenSSL build"
+ if disabled('quic');
+
+plan tests => 1;
+
+#
+# note we might need to add coverage for qlog
+#
+
+ok(run(test(["quic_0rtt_test",
+ srctop_file("test", "certs", "servercert.pem"),
+ srctop_file("test", "certs", "serverkey.pem")])));