#include "vquic-tls.h"
#include "vtls/keylog.h"
#include "vtls/vtls.h"
+#include "vtls/vtls_scache.h"
#include "curl_ngtcp2.h"
#include "warnless.h"
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 */
};
}
}
-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;
}
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;
}
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,
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;
}
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,
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) {
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) {
(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;
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,
#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)
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:
}
#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);
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.
*/
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)
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);
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;
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;
#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;
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;
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;
#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;
(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"
#include "vtls/wolfssl.h"
struct ssl_peer;
+struct Curl_ssl_session;
struct curl_tls_ctx {
#ifdef USE_OPENSSL
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.
* 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,
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.
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);
#include "progress.h"
#include "select.h"
#include "strcase.h"
+#include "strdup.h"
#include "warnless.h"
#include "x509asn1.h"
#include "multiif.h"
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)
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` */
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,
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);
/* 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;
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);
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)
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;
}
}
+ 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. */
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;
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);
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,
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;
#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)
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) {
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;
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;
s = calloc(1, sizeof(*s));
if(!s) {
free(sdata);
+ free(quic_tp);
return CURLE_OUT_OF_MEMORY;
}
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) {
}
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;
}
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;
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;
}
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 */
};
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);
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) {
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])
['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'):
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}'
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'
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):
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}',