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}',