From 68bd759c2bfe74799c3355ad29265b795a7e6c62 Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Mon, 2 Dec 2024 12:50:15 +0100 Subject: [PATCH] QUIC: 0RTT for gnutls via CURLSSLOPT_EARLYDATA When a QUIC TLS session announced early data support and 'CURLSSLOPT_EARLYDATA' is set for the transfer, send initial request and body (up to the 128k we buffer) as 0RTT when curl is built with ngtcp2+gnutls. QUIC 0RTT needs not only the TLS session but the QUIC transport paramters as well. Store those and the earlydata max value together with the session in the cache. Add test case for h3 use of this. Enable quic early data in nghttpx for testing. Closes #15667 --- lib/vquic/curl_ngtcp2.c | 247 +++++++++++++++++++++++++-------- lib/vquic/curl_osslq.c | 2 +- lib/vquic/curl_quiche.c | 2 +- lib/vquic/vquic-tls.c | 11 +- lib/vquic/vquic-tls.h | 12 +- lib/vtls/bearssl.c | 2 +- lib/vtls/gtls.c | 160 ++++++++++++++------- lib/vtls/gtls.h | 14 +- lib/vtls/mbedtls.c | 2 +- lib/vtls/openssl.c | 2 +- lib/vtls/vtls_scache.c | 45 +++++- lib/vtls/vtls_scache.h | 14 ++ lib/vtls/wolfssl.c | 2 +- tests/http/test_02_download.py | 3 +- tests/http/test_07_upload.py | 7 +- tests/http/test_08_caddy.py | 11 +- tests/http/testenv/nghttpx.py | 1 + 17 files changed, 395 insertions(+), 142 deletions(-) diff --git a/lib/vquic/curl_ngtcp2.c b/lib/vquic/curl_ngtcp2.c index 9b2c66e705..0449acdab1 100644 --- a/lib/vquic/curl_ngtcp2.c +++ b/lib/vquic/curl_ngtcp2.c @@ -66,6 +66,7 @@ #include "vquic-tls.h" #include "vtls/keylog.h" #include "vtls/vtls.h" +#include "vtls/vtls_scache.h" #include "curl_ngtcp2.h" #include "warnless.h" @@ -137,8 +138,16 @@ struct cf_ngtcp2_ctx { uint64_t max_idle_ms; /* max idle time for QUIC connection */ uint64_t used_bidi_streams; /* bidi streams we have opened */ uint64_t max_bidi_streams; /* max bidi streams we can open */ + size_t earlydata_max; /* max amount of early data supported by + server on session reuse */ + size_t earlydata_skip; /* sending bytes to skip when earlydata + * is accepted by peer */ + CURLcode tls_vrfy_result; /* result of TLS peer verification */ int qlogfd; BIT(initialized); + BIT(tls_handshake_complete); /* TLS handshake is done */ + BIT(use_earlydata); /* Using 0RTT data */ + BIT(earlydata_accepted); /* 0RTT was acceptd by server */ BIT(shutdown_started); /* queued shutdown packets */ }; @@ -442,12 +451,42 @@ static void quic_settings(struct cf_ngtcp2_ctx *ctx, } } -static CURLcode init_ngh3_conn(struct Curl_cfilter *cf); +static CURLcode init_ngh3_conn(struct Curl_cfilter *cf, + struct Curl_easy *data); -static int cb_handshake_completed(ngtcp2_conn *tconn, void *user_data) +static int cf_ngtcp2_handshake_completed(ngtcp2_conn *tconn, void *user_data) { - (void)user_data; + struct Curl_cfilter *cf = user_data; + struct cf_ngtcp2_ctx *ctx = cf ? cf->ctx : NULL; + struct Curl_easy *data; + (void)tconn; + DEBUGASSERT(ctx); + data = CF_DATA_CURRENT(cf); + DEBUGASSERT(data); + if(!ctx || !data) + return NGHTTP3_ERR_CALLBACK_FAILURE; + + ctx->handshake_at = Curl_now(); + ctx->tls_handshake_complete = TRUE; + cf->conn->bits.multiplex = TRUE; /* at least potentially multiplexed */ + cf->conn->httpversion = 30; + + ctx->tls_vrfy_result = Curl_vquic_tls_verify_peer(&ctx->tls, cf, + data, &ctx->peer); + CURL_TRC_CF(data, cf, "handshake complete after %dms", + (int)Curl_timediff(ctx->handshake_at, ctx->started_at)); +#ifdef USE_GNUTLS + if(ctx->use_earlydata) { + int flags = gnutls_session_get_flags(ctx->tls.gtls.session); + ctx->earlydata_accepted = !!(flags & GNUTLS_SFLAGS_EARLY_DATA); + CURL_TRC_CF(data, cf, "server did%s accept %zu bytes of early data", + ctx->earlydata_accepted ? "" : " not", ctx->earlydata_skip); + Curl_pgrsEarlyData(data, ctx->earlydata_accepted ? + (curl_off_t)ctx->earlydata_skip : + -(curl_off_t)ctx->earlydata_skip); + } +#endif return 0; } @@ -717,16 +756,19 @@ static int cb_recv_rx_key(ngtcp2_conn *tconn, ngtcp2_encryption_level level, void *user_data) { struct Curl_cfilter *cf = user_data; + struct cf_ngtcp2_ctx *ctx = cf ? cf->ctx : NULL; + struct Curl_easy *data = CF_DATA_CURRENT(cf); (void)tconn; - if(level != NGTCP2_ENCRYPTION_LEVEL_1RTT) { + if(level != NGTCP2_ENCRYPTION_LEVEL_1RTT) return 0; - } - if(init_ngh3_conn(cf) != CURLE_OK) { - return NGTCP2_ERR_CALLBACK_FAILURE; + DEBUGASSERT(ctx); + DEBUGASSERT(data); + if(ctx && data && !ctx->h3conn) { + if(init_ngh3_conn(cf, data)) + return NGTCP2_ERR_CALLBACK_FAILURE; } - return 0; } @@ -739,7 +781,7 @@ static ngtcp2_callbacks ng_callbacks = { ngtcp2_crypto_client_initial_cb, NULL, /* recv_client_initial */ ngtcp2_crypto_recv_crypto_data_cb, - cb_handshake_completed, + cf_ngtcp2_handshake_completed, NULL, /* recv_version_negotiation */ ngtcp2_crypto_encrypt_cb, ngtcp2_crypto_decrypt_cb, @@ -1128,14 +1170,15 @@ static nghttp3_callbacks ngh3_callbacks = { NULL /* recv_settings */ }; -static CURLcode init_ngh3_conn(struct Curl_cfilter *cf) +static CURLcode init_ngh3_conn(struct Curl_cfilter *cf, + struct Curl_easy *data) { struct cf_ngtcp2_ctx *ctx = cf->ctx; - CURLcode result; - int rc; int64_t ctrl_stream_id, qpack_enc_stream_id, qpack_dec_stream_id; + int rc; if(ngtcp2_conn_get_streams_uni_left(ctx->qconn) < 3) { + failf(data, "QUIC connection lacks 3 uni streams to run HTTP/3"); return CURLE_QUIC_CONNECT_ERROR; } @@ -1147,45 +1190,47 @@ static CURLcode init_ngh3_conn(struct Curl_cfilter *cf) nghttp3_mem_default(), cf); if(rc) { - result = CURLE_OUT_OF_MEMORY; - goto fail; + failf(data, "error creating nghttp3 connection instance"); + return CURLE_OUT_OF_MEMORY; } rc = ngtcp2_conn_open_uni_stream(ctx->qconn, &ctrl_stream_id, NULL); if(rc) { - result = CURLE_QUIC_CONNECT_ERROR; - goto fail; + failf(data, "error creating HTTP/3 control stream: %s", + ngtcp2_strerror(rc)); + return CURLE_QUIC_CONNECT_ERROR; } rc = nghttp3_conn_bind_control_stream(ctx->h3conn, ctrl_stream_id); if(rc) { - result = CURLE_QUIC_CONNECT_ERROR; - goto fail; + failf(data, "error binding HTTP/3 control stream: %s", + ngtcp2_strerror(rc)); + return CURLE_QUIC_CONNECT_ERROR; } rc = ngtcp2_conn_open_uni_stream(ctx->qconn, &qpack_enc_stream_id, NULL); if(rc) { - result = CURLE_QUIC_CONNECT_ERROR; - goto fail; + failf(data, "error creating HTTP/3 qpack encoding stream: %s", + ngtcp2_strerror(rc)); + return CURLE_QUIC_CONNECT_ERROR; } rc = ngtcp2_conn_open_uni_stream(ctx->qconn, &qpack_dec_stream_id, NULL); if(rc) { - result = CURLE_QUIC_CONNECT_ERROR; - goto fail; + failf(data, "error creating HTTP/3 qpack decoding stream: %s", + ngtcp2_strerror(rc)); + return CURLE_QUIC_CONNECT_ERROR; } rc = nghttp3_conn_bind_qpack_streams(ctx->h3conn, qpack_enc_stream_id, qpack_dec_stream_id); if(rc) { - result = CURLE_QUIC_CONNECT_ERROR; - goto fail; + failf(data, "error binding HTTP/3 qpack streams: %s", + ngtcp2_strerror(rc)); + return CURLE_QUIC_CONNECT_ERROR; } return CURLE_OK; -fail: - - return result; } static ssize_t recv_closed_stream(struct Curl_cfilter *cf, @@ -1236,6 +1281,10 @@ static ssize_t cf_ngtcp2_recv(struct Curl_cfilter *cf, struct Curl_easy *data, DEBUGASSERT(ctx->h3conn); *err = CURLE_OK; + /* handshake verification failed in callback, do not recv anything */ + if(ctx->tls_vrfy_result) + return ctx->tls_vrfy_result; + pktx_init(&pktx, cf, data); if(!stream || ctx->shutdown_started) { @@ -1533,6 +1582,10 @@ static ssize_t cf_ngtcp2_send(struct Curl_cfilter *cf, struct Curl_easy *data, pktx_init(&pktx, cf, data); *err = CURLE_OK; + /* handshake verification failed in callback, do not send anything */ + if(ctx->tls_vrfy_result) + return ctx->tls_vrfy_result; + (void)eos; /* TODO: use for stream EOF and block handling */ result = cf_progress_ingress(cf, data, &pktx); if(result) { @@ -1594,6 +1647,9 @@ static ssize_t cf_ngtcp2_send(struct Curl_cfilter *cf, struct Curl_easy *data, (void)nghttp3_conn_resume_stream(ctx->h3conn, stream->id); } + if(sent > 0 && !ctx->tls_handshake_complete && ctx->use_earlydata) + ctx->earlydata_skip += sent; + result = cf_progress_egress(cf, data, &pktx); if(result) { *err = result; @@ -1612,17 +1668,6 @@ out: return sent; } -static CURLcode qng_verify_peer(struct Curl_cfilter *cf, - struct Curl_easy *data) -{ - struct cf_ngtcp2_ctx *ctx = cf->ctx; - - cf->conn->bits.multiplex = TRUE; /* at least potentially multiplexed */ - cf->conn->httpversion = 30; - - return Curl_vquic_tls_verify_peer(&ctx->tls, cf, data, &ctx->peer); -} - static CURLcode recv_pkt(const unsigned char *pkt, size_t pktlen, struct sockaddr_storage *remote_addr, socklen_t remote_addrlen, int ecn, @@ -2135,6 +2180,24 @@ static int quic_ossl_new_session_cb(SSL *ssl, SSL_SESSION *ssl_sessionid) #endif /* USE_OPENSSL */ #ifdef USE_GNUTLS + +static const char *gtls_hs_msg_name(int mtype) +{ + switch(mtype) { + case 1: return "ClientHello"; + case 2: return "ServerHello"; + case 4: return "SessionTicket"; + case 8: return "EncryptedExtensions"; + case 11: return "Certificate"; + case 13: return "CertificateRequest"; + case 15: return "CertificateVerify"; + case 20: return "Finished"; + case 24: return "KeyUpdate"; + case 254: return "MessageHash"; + } + return "Unknown"; +} + static int quic_gtls_handshake_cb(gnutls_session_t session, unsigned int htype, unsigned when, unsigned int incoming, const gnutls_datum_t *msg) @@ -2148,14 +2211,28 @@ static int quic_gtls_handshake_cb(gnutls_session_t session, unsigned int htype, if(when && cf && ctx) { /* after message has been processed */ struct Curl_easy *data = CF_DATA_CURRENT(cf); DEBUGASSERT(data); - if(data) { - CURL_TRC_CF(data, cf, "handshake: %s message type %d", - incoming ? "incoming" : "outgoing", htype); - } + if(!data) + return 0; + CURL_TRC_CF(data, cf, "SSL message: %s %s [%d]", + incoming ? "<-" : "->", gtls_hs_msg_name(htype), htype); switch(htype) { case GNUTLS_HANDSHAKE_NEW_SESSION_TICKET: { + ngtcp2_ssize tplen; + uint8_t tpbuf[256]; + unsigned char *quic_tp = NULL; + size_t quic_tp_len = 0; + + tplen = ngtcp2_conn_encode_0rtt_transport_params(ctx->qconn, tpbuf, + sizeof(tpbuf)); + if(tplen < 0) + CURL_TRC_CF(data, cf, "error encoding 0RTT transport data: %s", + ngtcp2_strerror((int)tplen)); + else { + quic_tp = (unsigned char *)tpbuf; + quic_tp_len = (size_t)tplen; + } (void)Curl_gtls_cache_session(cf, data, ctx->peer.scache_key, - session, -1, "h3"); + session, -1, "h3", quic_tp, quic_tp_len); break; } default: @@ -2186,9 +2263,9 @@ static int wssl_quic_new_session_cb(WOLFSSL *ssl, WOLFSSL_SESSION *session) } #endif /* USE_WOLFSSL */ -static CURLcode tls_ctx_setup(struct Curl_cfilter *cf, - struct Curl_easy *data, - void *user_data) +static CURLcode cf_ngtcp2_tls_ctx_setup(struct Curl_cfilter *cf, + struct Curl_easy *data, + void *user_data) { struct curl_tls_ctx *ctx = user_data; struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data); @@ -2241,6 +2318,53 @@ static CURLcode tls_ctx_setup(struct Curl_cfilter *cf, return CURLE_OK; } +static CURLcode cf_ngtcp2_on_session_reuse(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct Curl_ssl_session *scs, + bool *do_early_data) +{ + struct cf_ngtcp2_ctx *ctx = cf->ctx; + CURLcode result = CURLE_OK; + + *do_early_data = FALSE; +#ifdef USE_GNUTLS + ctx->earlydata_max = + gnutls_record_get_max_early_data_size(ctx->tls.gtls.session); + if((!ctx->earlydata_max)) { + CURL_TRC_CF(data, cf, "SSL session does not allow earlydata"); + } + else if(strcmp("h3", scs->alpn)) { + CURL_TRC_CF(data, cf, "SSL session from different ALPN, no early data"); + } + else if(!scs->quic_tp || !scs->quic_tp_len) { + CURL_TRC_CF(data, cf, "no 0RTT transport parameters, no early data, "); + } + else { + int rv; + rv = ngtcp2_conn_decode_and_set_0rtt_transport_params( + ctx->qconn, (uint8_t *)scs->quic_tp, scs->quic_tp_len); + if(rv) + CURL_TRC_CF(data, cf, "no early data, failed to set 0RTT transport " + "parameters: %s", ngtcp2_strerror(rv)); + else { + infof(data, "SSL session allows %zu bytes of early data, " + "reusing ALPN '%s'", ctx->earlydata_max, scs->alpn); + result = init_ngh3_conn(cf, data); + if(!result) { + ctx->use_earlydata = TRUE; + cf->connected = TRUE; + *do_early_data = TRUE; + } + } + } +#else /* USE_GNUTLS */ + (void)data; + (void)ctx; + (void)scs; +#endif + return result; +} + /* * Might be called twice for happy eyeballs. */ @@ -2256,17 +2380,6 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf, int qfd; DEBUGASSERT(ctx->initialized); -#define H3_ALPN "\x2h3\x5h3-29" - result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer, - H3_ALPN, sizeof(H3_ALPN) - 1, - tls_ctx_setup, &ctx->tls, &ctx->conn_ref); - if(result) - return result; - -#ifdef USE_OPENSSL - SSL_set_quic_use_legacy_codepoint(ctx->tls.ossl.ssl, 0); -#endif - ctx->dcid.datalen = NGTCP2_MAX_CIDLEN; result = Curl_rand(data, ctx->dcid.data, NGTCP2_MAX_CIDLEN); if(result) @@ -2308,7 +2421,17 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf, if(rc) return CURLE_QUIC_CONNECT_ERROR; +#define H3_ALPN "\x2h3\x5h3-29" + result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer, + H3_ALPN, sizeof(H3_ALPN) - 1, + cf_ngtcp2_tls_ctx_setup, &ctx->tls, + &ctx->conn_ref, + cf_ngtcp2_on_session_reuse); + if(result) + return result; + #ifdef USE_OPENSSL + SSL_set_quic_use_legacy_codepoint(ctx->tls.ossl.ssl, 0); ngtcp2_conn_set_tls_native_handle(ctx->qconn, ctx->tls.ossl.ssl); #elif defined(USE_GNUTLS) ngtcp2_conn_set_tls_native_handle(ctx->qconn, ctx->tls.gtls.session); @@ -2359,6 +2482,11 @@ static CURLcode cf_ngtcp2_connect(struct Curl_cfilter *cf, result = cf_connect_start(cf, data, &pktx); if(result) goto out; + if(cf->connected) { + cf->conn->alpn = CURL_HTTP_VERSION_3; + *done = TRUE; + goto out; + } result = cf_progress_egress(cf, data, &pktx); /* we do not expect to be able to recv anything yet */ goto out; @@ -2373,10 +2501,7 @@ static CURLcode cf_ngtcp2_connect(struct Curl_cfilter *cf, goto out; if(ngtcp2_conn_get_handshake_completed(ctx->qconn)) { - ctx->handshake_at = now; - CURL_TRC_CF(data, cf, "handshake complete after %dms", - (int)Curl_timediff(now, ctx->started_at)); - result = qng_verify_peer(cf, data); + result = ctx->tls_vrfy_result; if(!result) { CURL_TRC_CF(data, cf, "peer verified"); cf->connected = TRUE; diff --git a/lib/vquic/curl_osslq.c b/lib/vquic/curl_osslq.c index d9e5e1da3a..cf743d1972 100644 --- a/lib/vquic/curl_osslq.c +++ b/lib/vquic/curl_osslq.c @@ -1166,7 +1166,7 @@ static CURLcode cf_osslq_ctx_start(struct Curl_cfilter *cf, #define H3_ALPN "\x2h3" result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer, H3_ALPN, sizeof(H3_ALPN) - 1, - NULL, NULL, NULL); + NULL, NULL, NULL, NULL); if(result) goto out; diff --git a/lib/vquic/curl_quiche.c b/lib/vquic/curl_quiche.c index 0c294e2bfc..17025d9906 100644 --- a/lib/vquic/curl_quiche.c +++ b/lib/vquic/curl_quiche.c @@ -1309,7 +1309,7 @@ static CURLcode cf_quiche_ctx_open(struct Curl_cfilter *cf, result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer, QUICHE_H3_APPLICATION_PROTOCOL, sizeof(QUICHE_H3_APPLICATION_PROTOCOL) - 1, - NULL, NULL, cf); + NULL, NULL, cf, NULL); if(result) return result; diff --git a/lib/vquic/vquic-tls.c b/lib/vquic/vquic-tls.c index e9fda86481..6e1ace2e3c 100644 --- a/lib/vquic/vquic-tls.c +++ b/lib/vquic/vquic-tls.c @@ -235,7 +235,8 @@ CURLcode Curl_vquic_tls_init(struct curl_tls_ctx *ctx, struct ssl_peer *peer, const char *alpn, size_t alpn_len, Curl_vquic_tls_ctx_setup *cb_setup, - void *cb_user_data, void *ssl_user_data) + void *cb_user_data, void *ssl_user_data, + Curl_vquic_session_reuse_cb *session_reuse_cb) { char tls_id[80]; CURLcode result; @@ -250,6 +251,7 @@ CURLcode Curl_vquic_tls_init(struct curl_tls_ctx *ctx, #error "no TLS lib in used, should not happen" return CURLE_FAILED_INIT; #endif + (void)session_reuse_cb; result = Curl_ssl_peer_init(peer, cf, tls_id, TRNSPRT_QUIC); if(result) return result; @@ -260,15 +262,16 @@ CURLcode Curl_vquic_tls_init(struct curl_tls_ctx *ctx, (const unsigned char *)alpn, alpn_len, cb_setup, cb_user_data, NULL, ssl_user_data); #elif defined(USE_GNUTLS) - (void)result; return Curl_gtls_ctx_init(&ctx->gtls, cf, data, peer, - (const unsigned char *)alpn, alpn_len, NULL, - cb_setup, cb_user_data, ssl_user_data); + (const unsigned char *)alpn, alpn_len, + cb_setup, cb_user_data, ssl_user_data, + session_reuse_cb); #elif defined(USE_WOLFSSL) result = wssl_init_ctx(ctx, cf, data, cb_setup, cb_user_data); if(result) return result; + (void)session_reuse_cb; return wssl_init_ssl(ctx, cf, data, peer, alpn, alpn_len, ssl_user_data); #else #error "no TLS lib in used, should not happen" diff --git a/lib/vquic/vquic-tls.h b/lib/vquic/vquic-tls.h index 8ee6904a54..c0706b0eb8 100644 --- a/lib/vquic/vquic-tls.h +++ b/lib/vquic/vquic-tls.h @@ -35,6 +35,7 @@ #include "vtls/wolfssl.h" struct ssl_peer; +struct Curl_ssl_session; struct curl_tls_ctx { #ifdef USE_OPENSSL @@ -57,6 +58,11 @@ typedef CURLcode Curl_vquic_tls_ctx_setup(struct Curl_cfilter *cf, struct Curl_easy *data, void *cb_user_data); +typedef CURLcode Curl_vquic_session_reuse_cb(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct Curl_ssl_session *scs, + bool *do_early_data); + /** * Initialize the QUIC TLS instances based of the SSL configurations * for the connection filter, transfer and peer. @@ -68,8 +74,9 @@ typedef CURLcode Curl_vquic_tls_ctx_setup(struct Curl_cfilter *cf, * may be NULL * @param alpn_len the overall number of bytes in `alpn` * @param cb_setup optional callback for early TLS config - ± @param cb_user_data user_data param for callback + * @param cb_user_data user_data param for callback * @param ssl_user_data optional pointer to set in TLS application context + * @param session_reuse_cb callback to handle session reuse, signal early data */ CURLcode Curl_vquic_tls_init(struct curl_tls_ctx *ctx, struct Curl_cfilter *cf, @@ -78,7 +85,8 @@ CURLcode Curl_vquic_tls_init(struct curl_tls_ctx *ctx, const char *alpn, size_t alpn_len, Curl_vquic_tls_ctx_setup *cb_setup, void *cb_user_data, - void *ssl_user_data); + void *ssl_user_data, + Curl_vquic_session_reuse_cb *session_reuse_cb); /** * Cleanup all data that has been initialized. diff --git a/lib/vtls/bearssl.c b/lib/vtls/bearssl.c index acd158f027..d3326bb45c 100644 --- a/lib/vtls/bearssl.c +++ b/lib/vtls/bearssl.c @@ -836,7 +836,7 @@ static CURLcode bearssl_connect_step3(struct Curl_cfilter *cf, ret = Curl_ssl_session_create((unsigned char *)session, sizeof(*session), (int)session->version, connssl->negotiated.alpn, - 0, -1, &sc_session); + 0, -1, 0, &sc_session); if(!ret) { ret = Curl_ssl_scache_put(cf, data, connssl->peer.scache_key, sc_session); diff --git a/lib/vtls/gtls.c b/lib/vtls/gtls.c index 451bc92cbe..879c6f3455 100644 --- a/lib/vtls/gtls.c +++ b/lib/vtls/gtls.c @@ -54,6 +54,7 @@ #include "progress.h" #include "select.h" #include "strcase.h" +#include "strdup.h" #include "warnless.h" #include "x509asn1.h" #include "multiif.h" @@ -720,12 +721,15 @@ CURLcode Curl_gtls_cache_session(struct Curl_cfilter *cf, const char *ssl_peer_key, gnutls_session_t session, int lifetime_secs, - const char *alpn) + const char *alpn, + unsigned char *quic_tp, + size_t quic_tp_len) { struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data); struct Curl_ssl_session *sc_session; - unsigned char *sdata; + unsigned char *sdata, *qtp_clone = NULL; size_t sdata_len = 0; + size_t earlydata_max = 0; CURLcode result = CURLE_OK; if(!ssl_config->primary.cache_session) @@ -750,11 +754,21 @@ CURLcode Curl_gtls_cache_session(struct Curl_cfilter *cf, CURL_TRC_CF(data, cf, "get session id (len=%zu, alpn=%s) and store in cache", sdata_len, alpn ? alpn : "-"); - result = Curl_ssl_session_create(sdata, sdata_len, - Curl_glts_get_ietf_proto(session), - alpn, 0, lifetime_secs, - &sc_session); - /* call took ownership of `sdata`*/ + earlydata_max = gnutls_record_get_max_early_data_size(session); + if(quic_tp && quic_tp_len) { + qtp_clone = Curl_memdup0((char *)quic_tp, quic_tp_len); + if(!qtp_clone) { + free(sdata); + return CURLE_OUT_OF_MEMORY; + } + } + + result = Curl_ssl_session_create2(sdata, sdata_len, + Curl_glts_get_ietf_proto(session), + alpn, 0, lifetime_secs, earlydata_max, + qtp_clone, quic_tp_len, + &sc_session); + /* call took ownership of `sdata` and `qtp_clone` */ if(!result) { result = Curl_ssl_scache_put(cf, data, ssl_peer_key, sc_session); /* took ownership of `sc_session` */ @@ -787,7 +801,7 @@ static CURLcode cf_gtls_update_session_id(struct Curl_cfilter *cf, struct ssl_connect_data *connssl = cf->ctx; return Curl_gtls_cache_session(cf, data, connssl->peer.scache_key, session, -1, - connssl->negotiated.alpn); + connssl->negotiated.alpn, NULL, 0); } static int gtls_handshake_cb(gnutls_session_t session, unsigned int htype, @@ -819,6 +833,7 @@ static int gtls_handshake_cb(gnutls_session_t session, unsigned int htype, static CURLcode gtls_client_init(struct Curl_cfilter *cf, struct Curl_easy *data, struct ssl_peer *peer, + size_t earlydata_max, struct gtls_ctx *gtls) { struct ssl_primary_config *config = Curl_ssl_cf_get_primary_config(cf); @@ -872,6 +887,14 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf, /* Initialize TLS session as a client */ init_flags = GNUTLS_CLIENT; + if(peer->transport == TRNSPRT_QUIC && earlydata_max > 0) + init_flags |= GNUTLS_ENABLE_EARLY_DATA | GNUTLS_NO_END_OF_EARLY_DATA; + else if(earlydata_max > 0 && earlydata_max != 0xFFFFFFFFUL) + /* See https://gitlab.com/gnutls/gnutls/-/issues/1619 + * We cannot differentiate between a session announcing no earldata + * and one announcing 0xFFFFFFFFUL. On TCP+TLS, this is unlikely, but + * on QUIC this is common. */ + init_flags |= GNUTLS_ENABLE_EARLY_DATA; #if defined(GNUTLS_FORCE_CLIENT_CERT) init_flags |= GNUTLS_FORCE_CLIENT_CERT; @@ -893,6 +916,8 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf, init_flags |= GNUTLS_NO_STATUS_REQUEST; #endif + CURL_TRC_CF(data, cf, "gnutls_init(flags=%x), earlydata=%zu", + init_flags, earlydata_max); rc = gnutls_init(>ls->session, init_flags); if(rc != GNUTLS_E_SUCCESS) { failf(data, "gnutls_init() failed: %d", rc); @@ -1065,46 +1090,62 @@ static int keylog_callback(gnutls_session_t session, const char *label, return 0; } +static CURLcode gtls_on_session_reuse(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct Curl_ssl_session *scs, + bool *do_early_data) +{ + struct ssl_connect_data *connssl = cf->ctx; + struct gtls_ssl_backend_data *backend = + (struct gtls_ssl_backend_data *)connssl->backend; + CURLcode result = CURLE_OK; + + *do_early_data = FALSE; + connssl->earlydata_max = + gnutls_record_get_max_early_data_size(backend->gtls.session); + if((!connssl->earlydata_max || connssl->earlydata_max == 0xFFFFFFFFUL)) { + /* Seems to be GnuTLS way to signal no EarlyData in session */ + CURL_TRC_CF(data, cf, "SSL session does not allow earlydata"); + } + else if(!Curl_alpn_contains_proto(connssl->alpn, scs->alpn)) { + CURL_TRC_CF(data, cf, "SSL session has different ALPN, no early data"); + } + else { + infof(data, "SSL session allows %zu bytes of early data, " + "reusing ALPN '%s'", connssl->earlydata_max, scs->alpn); + connssl->earlydata_state = ssl_earlydata_use; + connssl->state = ssl_connection_deferred; + result = Curl_alpn_set_negotiated(cf, data, connssl, + (const unsigned char *)scs->alpn, + scs->alpn ? strlen(scs->alpn) : 0); + *do_early_data = !result; + } + return result; +} + CURLcode Curl_gtls_ctx_init(struct gtls_ctx *gctx, struct Curl_cfilter *cf, struct Curl_easy *data, struct ssl_peer *peer, const unsigned char *alpn, size_t alpn_len, - struct ssl_connect_data *connssl, Curl_gtls_ctx_setup_cb *cb_setup, void *cb_user_data, - void *ssl_user_data) + void *ssl_user_data, + Curl_gtls_init_session_reuse_cb *sess_reuse_cb) { struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf); struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data); struct Curl_ssl_session *scs = NULL; gnutls_datum_t gtls_alpns[5]; size_t gtls_alpns_count = 0; + bool gtls_session_setup = FALSE; CURLcode result; int rc; DEBUGASSERT(gctx); - - result = gtls_client_init(cf, data, peer, gctx); - if(result) - return result; - - gnutls_session_set_ptr(gctx->session, ssl_user_data); - - if(cb_setup) { - result = cb_setup(cf, data, cb_user_data); - if(result) - return result; - } - - /* Open the file if a TLS or QUIC backend has not done this before. */ - Curl_tls_keylog_open(); - if(Curl_tls_keylog_enabled()) { - gnutls_session_set_keylog_function(gctx->session, keylog_callback); - } - /* This might be a reconnect, so we check for a session ID in the cache - to speed up things */ + to speed up things. We need to do this before constructing the gnutls + session since we need to set flags depending on the kind of reuse. */ if(conn_config->cache_session) { result = Curl_ssl_scache_take(cf, data, peer->scache_key, &scs); if(result) @@ -1112,35 +1153,28 @@ CURLcode Curl_gtls_ctx_init(struct gtls_ctx *gctx, if(scs && scs->sdata && scs->sdata_len) { /* we got a cached session, use it! */ + + result = gtls_client_init(cf, data, peer, scs->earlydata_max, gctx); + if(result) + goto out; + gtls_session_setup = TRUE; + rc = gnutls_session_set_data(gctx->session, scs->sdata, scs->sdata_len); - if(rc < 0) { + if(rc < 0) infof(data, "SSL session not accepted by GnuTLS, continuing without"); - } else { infof(data, "SSL reusing session with ALPN '%s'", scs->alpn ? scs->alpn : "-"); if(ssl_config->earlydata && - !cf->conn->connect_only && connssl && - (gnutls_protocol_get_version(gctx->session) == GNUTLS_TLS1_3) && - Curl_alpn_contains_proto(connssl->alpn, scs->alpn)) { - connssl->earlydata_max = - gnutls_record_get_max_early_data_size(gctx->session); - if((!connssl->earlydata_max || - connssl->earlydata_max == 0xFFFFFFFFUL)) { - /* Seems to be GnuTLS way to signal no EarlyData in session */ - CURL_TRC_CF(data, cf, "TLS session does not allow earlydata"); - } - else { - CURL_TRC_CF(data, cf, "TLS session allows %zu earlydata bytes, " - "reusing ALPN '%s'", - connssl->earlydata_max, scs->alpn); - connssl->earlydata_state = ssl_earlydata_use; - connssl->state = ssl_connection_deferred; - result = Curl_alpn_set_negotiated(cf, data, connssl, - (const unsigned char *)scs->alpn, - scs->alpn ? strlen(scs->alpn) : 0); + !cf->conn->connect_only && + (gnutls_protocol_get_version(gctx->session) == GNUTLS_TLS1_3)) { + bool do_early_data = FALSE; + if(sess_reuse_cb) { + result = sess_reuse_cb(cf, data, scs, &do_early_data); if(result) - goto out; + goto out; + } + if(do_early_data) { /* We only try the ALPN protocol the session used before, * otherwise we might send early data for the wrong protocol */ gtls_alpns[0].data = (unsigned char *)scs->alpn; @@ -1161,6 +1195,26 @@ CURLcode Curl_gtls_ctx_init(struct gtls_ctx *gctx, } } + if(!gtls_session_setup) { + result = gtls_client_init(cf, data, peer, 0, gctx); + if(result) + goto out; + } + + gnutls_session_set_ptr(gctx->session, ssl_user_data); + + if(cb_setup) { + result = cb_setup(cf, data, cb_user_data); + if(result) + goto out; + } + + /* Open the file if a TLS or QUIC backend has not done this before. */ + Curl_tls_keylog_open(); + if(Curl_tls_keylog_enabled()) { + gnutls_session_set_keylog_function(gctx->session, keylog_callback); + } + /* convert the ALPN string from our arguments to a list of strings that * gnutls wants and will convert internally back to this string for sending * to the server. nice. */ @@ -1225,7 +1279,7 @@ gtls_connect_step1(struct Curl_cfilter *cf, struct Curl_easy *data) result = Curl_gtls_ctx_init(&backend->gtls, cf, data, &connssl->peer, proto.data, proto.len, - connssl, NULL, NULL, cf); + NULL, NULL, cf, gtls_on_session_reuse); if(result) return result; diff --git a/lib/vtls/gtls.h b/lib/vtls/gtls.h index 1b7c098cf0..a40e68097c 100644 --- a/lib/vtls/gtls.h +++ b/lib/vtls/gtls.h @@ -46,6 +46,7 @@ struct ssl_primary_config; struct ssl_config_data; struct ssl_peer; struct ssl_connect_data; +struct Curl_ssl_session; int Curl_glts_get_ietf_proto(gnutls_session_t session); @@ -78,15 +79,20 @@ typedef CURLcode Curl_gtls_ctx_setup_cb(struct Curl_cfilter *cf, struct Curl_easy *data, void *user_data); +typedef CURLcode Curl_gtls_init_session_reuse_cb(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct Curl_ssl_session *scs, + bool *do_early_data); + CURLcode Curl_gtls_ctx_init(struct gtls_ctx *gctx, struct Curl_cfilter *cf, struct Curl_easy *data, struct ssl_peer *peer, const unsigned char *alpn, size_t alpn_len, - struct ssl_connect_data *connssl, Curl_gtls_ctx_setup_cb *cb_setup, void *cb_user_data, - void *ssl_user_data); + void *ssl_user_data, + Curl_gtls_init_session_reuse_cb *sess_reuse_cb); CURLcode Curl_gtls_client_trust_setup(struct Curl_cfilter *cf, struct Curl_easy *data, @@ -105,7 +111,9 @@ CURLcode Curl_gtls_cache_session(struct Curl_cfilter *cf, const char *ssl_peer_key, gnutls_session_t session, int lifetime_secs, - const char *alpn); + const char *alpn, + unsigned char *quic_tp, + size_t quic_tp_len); extern const struct Curl_ssl Curl_ssl_gnutls; diff --git a/lib/vtls/mbedtls.c b/lib/vtls/mbedtls.c index 1739084c9a..e5d162df83 100644 --- a/lib/vtls/mbedtls.c +++ b/lib/vtls/mbedtls.c @@ -1171,7 +1171,7 @@ mbed_new_session(struct Curl_cfilter *cf, struct Curl_easy *data) #endif result = Curl_ssl_session_create(sdata, slen, ietf_tls_id, - connssl->negotiated.alpn, 0, -1, + connssl->negotiated.alpn, 0, -1, 0, &sc_session); sdata = NULL; /* call took ownership */ if(!result) diff --git a/lib/vtls/openssl.c b/lib/vtls/openssl.c index 955f2bc743..a6549d272f 100644 --- a/lib/vtls/openssl.c +++ b/lib/vtls/openssl.c @@ -2903,7 +2903,7 @@ CURLcode Curl_ossl_add_session(struct Curl_cfilter *cf, result = Curl_ssl_session_create(der_session_buf, der_session_size, ietf_tls_id, alpn, 0, - SSL_SESSION_get_timeout(session), + SSL_SESSION_get_timeout(session), 0, &sc_session); der_session_buf = NULL; /* took ownership of sdata */ if(!result) { diff --git a/lib/vtls/vtls_scache.c b/lib/vtls/vtls_scache.c index a9215223e7..b06a4ca805 100644 --- a/lib/vtls/vtls_scache.c +++ b/lib/vtls/vtls_scache.c @@ -103,6 +103,11 @@ static void cf_ssl_scache_clear_session(struct Curl_ssl_session *s) s->sdata = NULL; } s->sdata_len = 0; + if(s->quic_tp) { + free((void *)s->quic_tp); + s->quic_tp = NULL; + } + s->quic_tp_len = 0; s->ietf_tls_id = 0; s->time_received = 0; s->lifetime_secs = 0; @@ -120,7 +125,21 @@ CURLcode Curl_ssl_session_create(unsigned char *sdata, size_t sdata_len, int ietf_tls_id, const char *alpn, curl_off_t time_received, long lifetime_secs, + size_t earlydata_max, struct Curl_ssl_session **psession) +{ + return Curl_ssl_session_create2(sdata, sdata_len, ietf_tls_id, alpn, + time_received, lifetime_secs, + earlydata_max, NULL, 0, psession); +} + +CURLcode +Curl_ssl_session_create2(unsigned char *sdata, size_t sdata_len, + int ietf_tls_id, const char *alpn, + curl_off_t time_received, long lifetime_secs, + size_t earlydata_max, + unsigned char *quic_tp, size_t quic_tp_len, + struct Curl_ssl_session **psession) { struct Curl_ssl_session *s; @@ -133,6 +152,7 @@ Curl_ssl_session_create(unsigned char *sdata, size_t sdata_len, s = calloc(1, sizeof(*s)); if(!s) { free(sdata); + free(quic_tp); return CURLE_OUT_OF_MEMORY; } @@ -147,8 +167,11 @@ Curl_ssl_session_create(unsigned char *sdata, size_t sdata_len, lifetime_secs = CURL_SCACHE_MAX_12_LIFETIME_SEC; s->lifetime_secs = (int)lifetime_secs; + s->earlydata_max = earlydata_max; s->sdata = sdata; s->sdata_len = sdata_len; + s->quic_tp = quic_tp; + s->quic_tp_len = quic_tp_len; if(alpn) { s->alpn = strdup(alpn); if(!s->alpn) { @@ -738,8 +761,10 @@ out: } else CURL_TRC_CF(data, cf, "[SCACHE] added session for %s [proto=0x%x, " - "lifetime=%d, alpn=%s], peer has %zu sessions now", + "lifetime=%d, alpn=%s, earlydata=%zu, quic_tp=%s], " + "peer has %zu sessions now", ssl_peer_key, s->ietf_tls_id, s->lifetime_secs, s->alpn, + s->earlydata_max, s->quic_tp ? "yes" : "no", Curl_llist_count(&peer->sessions)); return result; } @@ -779,6 +804,7 @@ CURLcode Curl_ssl_scache_take(struct Curl_cfilter *cf, struct Curl_ssl_scache *scache = data->state.ssl_scache; struct Curl_ssl_scache_peer *peer = NULL; struct Curl_llist_node *n; + struct Curl_ssl_session *s = NULL; CURLcode result; *ps = NULL; @@ -791,15 +817,24 @@ CURLcode Curl_ssl_scache_take(struct Curl_cfilter *cf, cf_scache_peer_remove_expired(peer, (curl_off_t)time(NULL)); n = Curl_llist_head(&peer->sessions); if(n) { - *ps = Curl_node_take_elem(n); + s = Curl_node_take_elem(n); (scache->age)++; /* increase general age */ peer->age = scache->age; /* set this as used in this age */ } } Curl_ssl_scache_unlock(data); - - CURL_TRC_CF(data, cf, "[SCACHE] %s cached session for '%s'", - *ps ? "Found" : "No", ssl_peer_key); + if(s) { + *ps = s; + CURL_TRC_CF(data, cf, "[SCACHE] took session for %s [proto=0x%x, " + "lifetime=%d, alpn=%s, earlydata=%zu, quic_tp=%s], " + "%zu sessions remain", + ssl_peer_key, s->ietf_tls_id, s->lifetime_secs, s->alpn, + s->earlydata_max, s->quic_tp ? "yes" : "no", + Curl_llist_count(&peer->sessions)); + } + else { + CURL_TRC_CF(data, cf, "[SCACHE] no cached session for %s", ssl_peer_key); + } return result; } diff --git a/lib/vtls/vtls_scache.h b/lib/vtls/vtls_scache.h index a3e3cac3d2..33d426a38e 100644 --- a/lib/vtls/vtls_scache.h +++ b/lib/vtls/vtls_scache.h @@ -122,6 +122,9 @@ struct Curl_ssl_session { int lifetime_secs; /* ticket lifetime (-1 unknown) */ int ietf_tls_id; /* TLS protocol identifier negotiated */ char *alpn; /* APLN TLS negotiated protocol string */ + size_t earlydata_max; /* max 0-RTT data supported by peer */ + const unsigned char *quic_tp; /* Optional QUIC transport param bytes */ + size_t quic_tp_len; /* number of bytes in quic_tp */ struct Curl_llist_node list; /* internal storage handling */ }; @@ -142,8 +145,19 @@ CURLcode Curl_ssl_session_create(unsigned char *sdata, size_t sdata_len, int ietf_tls_id, const char *alpn, curl_off_t time_received, long lifetime_secs, + size_t earlydata_max, struct Curl_ssl_session **psession); +/* Variation of session creation with quic transport parameter bytes, + * Takes ownership of `quic_tp` regardless of return code. */ +CURLcode +Curl_ssl_session_create2(unsigned char *sdata, size_t sdata_len, + int ietf_tls_id, const char *alpn, + curl_off_t time_received, long lifetime_secs, + size_t earlydata_max, + unsigned char *quic_tp, size_t quic_tp_len, + struct Curl_ssl_session **psession); + /* Destroy a `session` instance. Can be called with NULL. * Does NOT need locking. */ void Curl_ssl_session_destroy(struct Curl_ssl_session *s); diff --git a/lib/vtls/wolfssl.c b/lib/vtls/wolfssl.c index 800aa2ecb4..a17f1cfdde 100644 --- a/lib/vtls/wolfssl.c +++ b/lib/vtls/wolfssl.c @@ -432,7 +432,7 @@ CURLcode Curl_wssl_cache_session(struct Curl_cfilter *cf, result = Curl_ssl_session_create(sdata, sdata_len, ietf_tls_id, alpn, 0, - wolfSSL_SESSION_get_timeout(session), + wolfSSL_SESSION_get_timeout(session), 0, &sc_session); sdata = NULL; /* took ownership of sdata */ if(!result) { diff --git a/tests/http/test_02_download.py b/tests/http/test_02_download.py index b2acaca7b6..21908c1882 100644 --- a/tests/http/test_02_download.py +++ b/tests/http/test_02_download.py @@ -631,8 +631,7 @@ class TestDownload: elif proto == 'h2': assert earlydata[1] == 107, f'{earlydata}' elif proto == 'h3': - # not implemented - assert earlydata[1] == 0, f'{earlydata}' + assert earlydata[1] == 67, f'{earlydata}' @pytest.mark.parametrize("proto", ['http/1.1', 'h2']) @pytest.mark.parametrize("max_host_conns", [0, 1, 5]) diff --git a/tests/http/test_07_upload.py b/tests/http/test_07_upload.py index dcafe7c064..a814bf043a 100644 --- a/tests/http/test_07_upload.py +++ b/tests/http/test_07_upload.py @@ -697,7 +697,10 @@ class TestUpload: ['http/1.1', 32*1024, 16384], # headers+body, limited by server max ['h2', 10*1024, 10378], # headers+body ['h2', 32*1024, 16384], # headers+body, limited by server max - ['h3', 1024, 0], # earlydata not supported + ['h3', 1024, 1126], # headers+body (app data) + ['h3', 1024 * 1024, 131177], # headers+body (long app data). The 0RTT + # size is limited by our sendbuf size + # of 128K. ]) def test_07_70_put_earlydata(self, env: Env, httpd, nghttpx, proto, upload_size, exp_early): if not env.curl_uses_lib('gnutls'): @@ -727,7 +730,7 @@ class TestUpload: self.check_downloads(client, [f"{upload_size}"], count) earlydata = {} for line in r.trace_lines: - m = re.match(r'^\[t-(\d+)] EarlyData: (\d+)', line) + m = re.match(r'^\[t-(\d+)] EarlyData: (-?\d+)', line) if m: earlydata[int(m.group(1))] = int(m.group(2)) assert earlydata[0] == 0, f'{earlydata}' diff --git a/tests/http/test_08_caddy.py b/tests/http/test_08_caddy.py index 335f76e6fc..2f7f649c08 100644 --- a/tests/http/test_08_caddy.py +++ b/tests/http/test_08_caddy.py @@ -210,7 +210,7 @@ class TestCaddy: respdata = open(curl.response_file(i)).readlines() assert respdata == exp_data - @pytest.mark.parametrize("proto", ['http/1.1', 'h2']) + @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3']) def test_08_08_earlydata(self, env: Env, httpd, caddy, proto): count = 2 docname = 'data10k.data' @@ -230,12 +230,15 @@ class TestCaddy: self.check_downloads(client, srcfile, count) earlydata = {} for line in r.trace_lines: - m = re.match(r'^\[t-(\d+)] EarlyData: (\d+)', line) + m = re.match(r'^\[t-(\d+)] EarlyData: (-?\d+)', line) if m: earlydata[int(m.group(1))] = int(m.group(2)) - # Caddy does not support early data assert earlydata[0] == 0, f'{earlydata}' - assert earlydata[1] == 0, f'{earlydata}' + if proto == 'h3': + assert earlydata[1] == 71, f'{earlydata}' + else: + # Caddy does not support early data on TCP + assert earlydata[1] == 0, f'{earlydata}' def check_downloads(self, client, srcfile: str, count: int, complete: bool = True): diff --git a/tests/http/testenv/nghttpx.py b/tests/http/testenv/nghttpx.py index 801d9a63ad..03200beba9 100644 --- a/tests/http/testenv/nghttpx.py +++ b/tests/http/testenv/nghttpx.py @@ -205,6 +205,7 @@ class NghttpxQuic(Nghttpx): args = [ self._cmd, f'--frontend=*,{self.env.h3_port};quic', + '--frontend-quic-early-data', f'--frontend=*,{self.env.nghttpx_https_port};tls', f'--backend=127.0.0.1,{self.env.https_port};{self.env.domain1};sni={self.env.domain1};proto=h2;tls', f'--backend=127.0.0.1,{self.env.http_port}',