diff --git a/docs/cmdline-opts/Makefile.inc b/docs/cmdline-opts/Makefile.inc index a7f635d8d9..3bcffa49fc 100644 --- a/docs/cmdline-opts/Makefile.inc +++ b/docs/cmdline-opts/Makefile.inc @@ -281,6 +281,7 @@ DPAGES = \ tftp-blksize.md \ tftp-no-options.md \ time-cond.md \ + tls-earlydata.md \ tls-max.md \ tls13-ciphers.md \ tlsauthtype.md \ diff --git a/docs/cmdline-opts/tls-earlydata.md b/docs/cmdline-opts/tls-earlydata.md new file mode 100644 index 0000000000..8482f809ec --- /dev/null +++ b/docs/cmdline-opts/tls-earlydata.md @@ -0,0 +1,41 @@ +--- +c: Copyright (C) Daniel Stenberg, , et al. +SPDX-License-Identifier: curl +Long: tls-earlydata +Help: Allow use of TLSv1.3 early data (0RTT) +Protocols: TLS +Added: 8.11.0 +Category: tls +Multi: boolean +See-also: + - tlsv1.3 + - tls-max +Example: + - --tls-earlydata $URL +--- + +# `--tls-earlydata` + +Enable the use of TLSv1.3 early data, also known as '0RTT' where possible. +This has security implications for the requests sent that way. + +This option is used when curl is built to use GnuTLS. + +If a server supports this TLSv1.3 feature, and to what extent, is announced +as part of the TLS "session" sent back to curl. Until curl has seen such +a session in a previous request, early data cannot be used. + +When a new connection is initiated with a known TLSv1.3 session, and that +session announced early data support, the first request on this connection is +sent *before* the TLS handshake is complete. While the early data is also +encrypted, it is not protected against replays. An attacker can send +your early data to the server again and the server would accept it. + +If your request contacts a public server and only retrieves a file, there +may be no harm in that. If the first request orders a refrigerator +for you, it is probably not a good idea to use early data for it. curl +cannot deduce what the security implications of your requests actually +are and make this decision for you. + +**WARNING**: this option has security implications. See above for more +details. diff --git a/docs/libcurl/curl_easy_getinfo.md b/docs/libcurl/curl_easy_getinfo.md index 7157cd2113..396fb17e87 100644 --- a/docs/libcurl/curl_easy_getinfo.md +++ b/docs/libcurl/curl_easy_getinfo.md @@ -112,6 +112,11 @@ curl_easy_header(3) instead. See CURLINFO_CONTENT_TYPE(3) List of all known cookies. See CURLINFO_COOKIELIST(3) +## CURLINFO_EARLYDATA_SENT_T + +Amount of TLS early data sent (in number of bytes) when +CURLSSLOPT_EARLYDATA is enabled. + ## CURLINFO_EFFECTIVE_METHOD Last used HTTP method. See CURLINFO_EFFECTIVE_METHOD(3) diff --git a/docs/libcurl/opts/CURLINFO_EARLYDATA_SENT_T.md b/docs/libcurl/opts/CURLINFO_EARLYDATA_SENT_T.md new file mode 100644 index 0000000000..427746077e --- /dev/null +++ b/docs/libcurl/opts/CURLINFO_EARLYDATA_SENT_T.md @@ -0,0 +1,75 @@ +--- +c: Copyright (C) Daniel Stenberg, , et al. +SPDX-License-Identifier: curl +Title: CURLINFO_EARLYDATA_SENT_T +Section: 3 +Source: libcurl +See-also: + - curl_easy_getinfo (3) + - curl_easy_setopt (3) +Protocol: + - TLS +TLS-backend: + - GnuTLS +Added-in: 8.11.0 +--- + +# NAME + +CURLINFO_EARLYDATA_SENT_T - get the number of bytes sent as TLS early data + +# SYNOPSIS + +~~~c +#include + +CURLcode curl_easy_getinfo(CURL *handle, CURLINFO_EARLYDATA_SENT_T, + curl_off_t *amount); +~~~ + +# DESCRIPTION + +Pass a pointer to an *curl_off_t* to receive the total amount of bytes that +were sent to the server as TLSv1.3 early data. When no TLS early +data is used, this reports 0. + +TLS early data is only attempted when CURLSSLOPT_EARLYDATA is set for the +transfer. In addition, it is only used by libcurl when a TLS session exists +that announces support. + +The amount is **negative** when the sent data was rejected +by the server. TLS allows a server that announces support for early data to +reject any attempt to use it at its own discretion. When for example 127 +bytes had been sent, but were rejected, it reports -127 as the amount "sent". + +# %PROTOCOLS% + +# EXAMPLE + +~~~c +int main(void) +{ + CURL *curl = curl_easy_init(); + if(curl) { + CURLcode res; + curl_easy_setopt(curl, CURLOPT_URL, "https://example.com"); + + /* Perform the request */ + res = curl_easy_perform(curl); + + if(!res) { + curl_off_t amount; + res = curl_easy_getinfo(curl, CURLINFO_EARLYDATA_SENT_T, &amount); + if(!res) { + printf("TLS earlydata: %" CURL_FORMAT_CURL_OFF_T " bytes\n", amount); + } + } + } +} +~~~ + +# %AVAILABILITY% + +# RETURN VALUE + +Returns CURLE_OK if the option is supported, and CURLE_UNKNOWN_OPTION if not. diff --git a/docs/libcurl/opts/CURLOPT_SSL_OPTIONS.md b/docs/libcurl/opts/CURLOPT_SSL_OPTIONS.md index 21e0d75e34..f789a8b529 100644 --- a/docs/libcurl/opts/CURLOPT_SSL_OPTIONS.md +++ b/docs/libcurl/opts/CURLOPT_SSL_OPTIONS.md @@ -86,6 +86,15 @@ certificate that supports client authentication in the OS certificate store it could be a privacy violation and unexpected. (Added in 7.77.0) +## CURLSSLOPT_EARLYDATA + +Tell libcurl to try sending application data as TLS1.3 early data. This option +is only supported for GnuTLS. This option works on a best effort basis, +in cases when it wasn't possible to send early data the request is resent +normally post-handshake. +This option does not work when using QUIC. +(Added in 8.11.0) + # DEFAULT 0 diff --git a/docs/libcurl/opts/Makefile.inc b/docs/libcurl/opts/Makefile.inc index edabe37a75..c8bd76b641 100644 --- a/docs/libcurl/opts/Makefile.inc +++ b/docs/libcurl/opts/Makefile.inc @@ -167,6 +167,7 @@ man_MANS = \ CURLOPT_DOH_SSL_VERIFYPEER.3 \ CURLOPT_DOH_SSL_VERIFYSTATUS.3 \ CURLOPT_DOH_URL.3 \ + CURLINFO_EARLYDATA_SENT_T.3 \ CURLOPT_ECH.3 \ CURLOPT_EGDSOCKET.3 \ CURLOPT_ERRORBUFFER.3 \ diff --git a/docs/libcurl/symbols-in-versions b/docs/libcurl/symbols-in-versions index cbabc48bf1..ddda26d832 100644 --- a/docs/libcurl/symbols-in-versions +++ b/docs/libcurl/symbols-in-versions @@ -435,6 +435,7 @@ CURLINFO_COOKIELIST 7.14.1 CURLINFO_DATA_IN 7.9.6 CURLINFO_DATA_OUT 7.9.6 CURLINFO_DOUBLE 7.4.1 +CURLINFO_EARLYDATA_SENT_T 8.11.0 CURLINFO_EFFECTIVE_METHOD 7.72.0 CURLINFO_EFFECTIVE_URL 7.4 CURLINFO_END 7.9.6 @@ -1054,6 +1055,7 @@ CURLSSLOPT_NATIVE_CA 7.71.0 CURLSSLOPT_NO_PARTIALCHAIN 7.68.0 CURLSSLOPT_NO_REVOKE 7.44.0 CURLSSLOPT_REVOKE_BEST_EFFORT 7.70.0 +CURLSSLOPT_EARLYDATA 8.11.0 CURLSSLSET_NO_BACKENDS 7.56.0 CURLSSLSET_OK 7.56.0 CURLSSLSET_TOO_LATE 7.56.0 diff --git a/docs/options-in-versions b/docs/options-in-versions index 62b61d94a8..a7a11630d3 100644 --- a/docs/options-in-versions +++ b/docs/options-in-versions @@ -246,6 +246,7 @@ --tftp-blksize 7.20.0 --tftp-no-options 7.48.0 --time-cond (-z) 5.8 +--tls-earlydata 8.11.0 --tls-max 7.54.0 --tls13-ciphers 7.61.0 --tlsauthtype 7.21.4 diff --git a/include/curl/curl.h b/include/curl/curl.h index 043b7103e8..3c2e91e517 100644 --- a/include/curl/curl.h +++ b/include/curl/curl.h @@ -943,6 +943,9 @@ typedef enum { a client certificate for authentication. (Schannel) */ #define CURLSSLOPT_AUTO_CLIENT_CERT (1<<5) +/* If possible, send data using TLS 1.3 early data */ +#define CURLSSLOPT_EARLYDATA (1<<6) + /* The default connection attempt delay in milliseconds for happy eyeballs. CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS.3 and happy-eyeballs-timeout-ms.d document this value, keep them in sync. */ @@ -2954,7 +2957,8 @@ typedef enum { CURLINFO_QUEUE_TIME_T = CURLINFO_OFF_T + 65, CURLINFO_USED_PROXY = CURLINFO_LONG + 66, CURLINFO_POSTTRANSFER_TIME_T = CURLINFO_OFF_T + 67, - CURLINFO_LASTONE = 67 + CURLINFO_EARLYDATA_SENT_T = CURLINFO_OFF_T + 68, + CURLINFO_LASTONE = 68 } CURLINFO; /* CURLINFO_RESPONSE_CODE is the new name for the option previously known as diff --git a/lib/cf-https-connect.c b/lib/cf-https-connect.c index 95c105e338..dd7cdcb051 100644 --- a/lib/cf-https-connect.c +++ b/lib/cf-https-connect.c @@ -174,6 +174,7 @@ static CURLcode baller_connected(struct Curl_cfilter *cf, { struct cf_hc_ctx *ctx = cf->ctx; CURLcode result = CURLE_OK; + int reply_ms; DEBUGASSERT(winner->cf); if(winner != &ctx->h3_baller) @@ -181,9 +182,15 @@ static CURLcode baller_connected(struct Curl_cfilter *cf, if(winner != &ctx->h21_baller) cf_hc_baller_reset(&ctx->h21_baller, data); - CURL_TRC_CF(data, cf, "connect+handshake %s: %dms, 1st data: %dms", - winner->name, (int)Curl_timediff(Curl_now(), winner->started), - cf_hc_baller_reply_ms(winner, data)); + reply_ms = cf_hc_baller_reply_ms(winner, data); + if(reply_ms >= 0) + CURL_TRC_CF(data, cf, "connect+handshake %s: %dms, 1st data: %dms", + winner->name, (int)Curl_timediff(Curl_now(), winner->started), + reply_ms); + else + CURL_TRC_CF(data, cf, "deferred handshake %s: %dms", + winner->name, (int)Curl_timediff(Curl_now(), winner->started)); + cf->next = winner->cf; winner->cf = NULL; diff --git a/lib/getinfo.c b/lib/getinfo.c index 6e64733d48..9144ad715d 100644 --- a/lib/getinfo.c +++ b/lib/getinfo.c @@ -449,6 +449,9 @@ static CURLcode getinfo_offt(struct Curl_easy *data, CURLINFO info, *param_offt = data->conn ? data->conn->connection_id : data->state.recent_conn_id; break; + case CURLINFO_EARLYDATA_SENT_T: + *param_offt = data->progress.earlydata_sent; + break; default: return CURLE_UNKNOWN_OPTION; } diff --git a/lib/http2.c b/lib/http2.c index 42abe1daae..451fa90de6 100644 --- a/lib/http2.c +++ b/lib/http2.c @@ -789,8 +789,11 @@ static ssize_t send_callback(nghttp2_session *h2, (void)flags; DEBUGASSERT(data); - nwritten = Curl_bufq_write_pass(&ctx->outbufq, buf, blen, - nw_out_writer, cf, &result); + if(!cf->connected) + nwritten = Curl_bufq_write(&ctx->outbufq, buf, blen, &result); + else + nwritten = Curl_bufq_write_pass(&ctx->outbufq, buf, blen, + nw_out_writer, cf, &result); if(nwritten < 0) { if(result == CURLE_AGAIN) { ctx->nw_out_blocked = 1; @@ -1898,6 +1901,11 @@ out: nghttp2_strerror(rv), rv); return CURLE_SEND_ERROR; } + /* Defer flushing during the connect phase so that the SETTINGS and + * other initial frames are sent together with the first request. + * Unless we are 'connect_only' where the request will never come. */ + if(!cf->connected && !cf->conn->connect_only) + return CURLE_OK; return nw_out_flush(cf, data); } @@ -2439,6 +2447,7 @@ static CURLcode cf_h2_connect(struct Curl_cfilter *cf, struct cf_h2_ctx *ctx = cf->ctx; CURLcode result = CURLE_OK; struct cf_call_data save; + bool first_time = FALSE; if(cf->connected) { *done = TRUE; @@ -2460,11 +2469,14 @@ static CURLcode cf_h2_connect(struct Curl_cfilter *cf, result = cf_h2_ctx_open(cf, data); if(result) goto out; + first_time = TRUE; } - result = h2_progress_ingress(cf, data, H2_CHUNK_SIZE); - if(result) - goto out; + if(!first_time) { + result = h2_progress_ingress(cf, data, H2_CHUNK_SIZE); + if(result) + goto out; + } /* Send out our SETTINGS and ACKs and such. If that blocks, we * have it buffered and can count this filter as being connected */ diff --git a/lib/progress.c b/lib/progress.c index 265abb1b57..d3a1b9ac9a 100644 --- a/lib/progress.c +++ b/lib/progress.c @@ -381,6 +381,11 @@ void Curl_pgrsSetUploadSize(struct Curl_easy *data, curl_off_t size) } } +void Curl_pgrsEarlyData(struct Curl_easy *data, curl_off_t sent) +{ + data->progress.earlydata_sent = sent; +} + /* returns the average speed in bytes / second */ static curl_off_t trspeed(curl_off_t size, /* number of bytes */ curl_off_t us) /* microseconds */ diff --git a/lib/progress.h b/lib/progress.h index 04a8f5bce9..326271ef1e 100644 --- a/lib/progress.h +++ b/lib/progress.h @@ -69,6 +69,8 @@ timediff_t Curl_pgrsLimitWaitTime(struct pgrs_dir *d, void Curl_pgrsTimeWas(struct Curl_easy *data, timerid timer, struct curltime timestamp); +void Curl_pgrsEarlyData(struct Curl_easy *data, curl_off_t sent); + #define PGRS_HIDE (1<<4) #define PGRS_UL_SIZE_KNOWN (1<<5) #define PGRS_DL_SIZE_KNOWN (1<<6) diff --git a/lib/setopt.c b/lib/setopt.c index 396caaeb1a..0f59710ead 100644 --- a/lib/setopt.c +++ b/lib/setopt.c @@ -2369,6 +2369,7 @@ CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param) data->set.ssl.revoke_best_effort = !!(arg & CURLSSLOPT_REVOKE_BEST_EFFORT); data->set.ssl.native_ca_store = !!(arg & CURLSSLOPT_NATIVE_CA); data->set.ssl.auto_client_cert = !!(arg & CURLSSLOPT_AUTO_CLIENT_CERT); + data->set.ssl.earlydata = !!(arg & CURLSSLOPT_EARLYDATA); /* If a setting is added here it should also be added in dohprobe() which sets its own CURLOPT_SSL_OPTIONS based on these settings. */ break; diff --git a/lib/urldata.h b/lib/urldata.h index 028ac0aff5..4e0d6ef988 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -324,6 +324,7 @@ struct ssl_config_data { char *key_passwd; /* plain text private key password */ BIT(certinfo); /* gather lots of certificate info */ BIT(falsestart); + BIT(earlydata); /* use tls1.3 early data */ BIT(enable_beast); /* allow this flaw for interoperability's sake */ BIT(no_revoke); /* disable SSL certificate revocation checks */ BIT(no_partialchain); /* do not accept partial certificate chains */ @@ -346,6 +347,7 @@ struct Curl_ssl_session { char *name; /* hostname for which this ID was used */ char *conn_to_host; /* hostname for the connection (may be NULL) */ const char *scheme; /* protocol scheme used */ + char *alpn; /* APLN TLS negotiated protocol string */ void *sessionid; /* as returned from the SSL layer */ size_t idsize; /* if known, otherwise 0 */ Curl_ssl_sessionid_dtor *sessionid_free; /* free `sessionid` callback */ @@ -1069,6 +1071,7 @@ struct Progress { struct pgrs_dir dl; curl_off_t current_speed; /* uses the currently fastest transfer */ + curl_off_t earlydata_sent; int width; /* screen width at download start */ int flags; /* see progress.h */ diff --git a/lib/vtls/bearssl.c b/lib/vtls/bearssl.c index daf98d958f..c7291b49f8 100644 --- a/lib/vtls/bearssl.c +++ b/lib/vtls/bearssl.c @@ -613,7 +613,8 @@ static CURLcode bearssl_connect_step1(struct Curl_cfilter *cf, CURL_TRC_CF(data, cf, "connect_step1, check session cache"); Curl_ssl_sessionid_lock(data); - if(!Curl_ssl_getsessionid(cf, data, &connssl->peer, &session, NULL)) { + if(!Curl_ssl_getsessionid(cf, data, &connssl->peer, + &session, NULL, NULL)) { br_ssl_engine_set_session_parameters(&backend->ctx.eng, session); session_set = 1; infof(data, "BearSSL: reusing session ID"); @@ -823,7 +824,7 @@ static CURLcode bearssl_connect_step3(struct Curl_cfilter *cf, const char *proto; proto = br_ssl_engine_get_selected_protocol(&backend->ctx.eng); - Curl_alpn_set_negotiated(cf, data, (const unsigned char *)proto, + Curl_alpn_set_negotiated(cf, data, connssl, (const unsigned char *)proto, proto ? strlen(proto) : 0); } @@ -835,7 +836,7 @@ static CURLcode bearssl_connect_step3(struct Curl_cfilter *cf, return CURLE_OUT_OF_MEMORY; br_ssl_engine_get_session_parameters(&backend->ctx.eng, session); Curl_ssl_sessionid_lock(data); - ret = Curl_ssl_set_sessionid(cf, data, &connssl->peer, session, 0, + ret = Curl_ssl_set_sessionid(cf, data, &connssl->peer, NULL, session, 0, bearssl_session_free); Curl_ssl_sessionid_unlock(data); if(ret) diff --git a/lib/vtls/gtls.c b/lib/vtls/gtls.c index d380350145..1b9bcd7a54 100644 --- a/lib/vtls/gtls.c +++ b/lib/vtls/gtls.c @@ -50,6 +50,7 @@ #include "vauth/vauth.h" #include "parsedate.h" #include "connect.h" /* for the connect timeout */ +#include "progress.h" #include "select.h" #include "strcase.h" #include "warnless.h" @@ -284,7 +285,7 @@ static CURLcode handshake(struct Curl_cfilter *cf, } else if(0 == what) { if(nonblocking) - return CURLE_OK; + return CURLE_AGAIN; else if(timeout_ms) { /* timeout */ failf(data, "SSL connection timeout at %ld", (long)timeout_ms); @@ -737,6 +738,9 @@ static CURLcode gtls_update_session_id(struct Curl_cfilter *cf, /* get the session ID data size */ gnutls_session_get_data(session, NULL, &connect_idsize); + if(!connect_idsize) /* gnutls does this for some version combinations */ + return CURLE_OK; + connect_sessionid = malloc(connect_idsize); /* get a buffer for it */ if(!connect_sessionid) { return CURLE_OUT_OF_MEMORY; @@ -750,6 +754,7 @@ static CURLcode gtls_update_session_id(struct Curl_cfilter *cf, Curl_ssl_sessionid_lock(data); /* store this session id, takes ownership */ result = Curl_ssl_set_sessionid(cf, data, &connssl->peer, + connssl->alpn_negotiated, connect_sessionid, connect_idsize, gtls_sessionid_free); Curl_ssl_sessionid_unlock(data); @@ -845,9 +850,13 @@ static CURLcode gtls_client_init(struct Curl_cfilter *cf, init_flags |= GNUTLS_FORCE_CLIENT_CERT; #endif -#if defined(GNUTLS_NO_TICKETS) - /* Disable TLS session tickets */ - init_flags |= GNUTLS_NO_TICKETS; +#if defined(GNUTLS_NO_TICKETS_TLS12) + init_flags |= GNUTLS_NO_TICKETS_TLS12; +#elif defined(GNUTLS_NO_TICKETS) + /* Disable TLS session tickets for non 1.3 connections */ + if((config->version != CURL_SSLVERSION_TLSv1_3) && + (config->version != CURL_SSLVERSION_DEFAULT)) + init_flags |= GNUTLS_NO_TICKETS; #endif #if defined(GNUTLS_NO_STATUS_REQUEST) @@ -1039,6 +1048,10 @@ CURLcode Curl_gtls_ctx_init(struct gtls_ctx *gctx, void *ssl_user_data) { 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 ssl_connect_data *connssl = cf->ctx; + gnutls_datum_t gtls_alpns[5]; + size_t gtls_alpns_count = 0; CURLcode result; DEBUGASSERT(gctx); @@ -1061,52 +1074,90 @@ CURLcode Curl_gtls_ctx_init(struct gtls_ctx *gctx, 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 very - * string for sending to the server. nice. */ - if(alpn && alpn_len) { - gnutls_datum_t alpns[5]; - size_t i, alen = alpn_len; - unsigned char *s = (unsigned char *)alpn; - unsigned char slen; - for(i = 0; (i < ARRAYSIZE(alpns)) && alen; ++i) { - slen = s[0]; - if(slen >= alen) - return CURLE_FAILED_INIT; - alpns[i].data = s + 1; - alpns[i].size = slen; - s += slen + 1; - alen -= (size_t)slen + 1; - } - if(alen) /* not all alpn chars used, wrong format or too many */ - return CURLE_FAILED_INIT; - if(i && gnutls_alpn_set_protocols(gctx->session, - alpns, (unsigned int)i, - GNUTLS_ALPN_MANDATORY)) { - failf(data, "failed setting ALPN"); - return CURLE_SSL_CONNECT_ERROR; - } - } - /* This might be a reconnect, so we check for a session ID in the cache to speed up things */ if(conn_config->cache_session) { void *ssl_sessionid; size_t ssl_idsize; - + char *session_alpn; Curl_ssl_sessionid_lock(data); - if(!Curl_ssl_getsessionid(cf, data, peer, &ssl_sessionid, &ssl_idsize)) { + if(!Curl_ssl_getsessionid(cf, data, peer, + &ssl_sessionid, &ssl_idsize, &session_alpn)) { /* we got a session id, use it! */ int rc; rc = gnutls_session_set_data(gctx->session, ssl_sessionid, ssl_idsize); if(rc < 0) infof(data, "SSL failed to set session ID"); - else + else { infof(data, "SSL reusing session ID (size=%zu)", ssl_idsize); +#ifdef DEBUGBUILD + if((ssl_config->earlydata || !!getenv("CURL_USE_EARLYDATA")) && +#else + if(ssl_config->earlydata && +#endif + !cf->conn->connect_only && + (gnutls_protocol_get_version(gctx->session) == GNUTLS_TLS1_3) && + Curl_alpn_contains_proto(connssl->alpn, session_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, session_alpn); + connssl->earlydata_state = ssl_earlydata_use; + connssl->state = ssl_connection_deferred; + result = Curl_alpn_set_negotiated(cf, data, connssl, + (const unsigned char *)session_alpn, + session_alpn ? strlen(session_alpn) : 0); + if(result) + return result; + /* 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 *)session_alpn; + gtls_alpns[0].size = (unsigned)strlen(session_alpn); + gtls_alpns_count = 1; + } + } + } } Curl_ssl_sessionid_unlock(data); } + + /* convert the ALPN string from our arguments to a list of strings + * that gnutls wants and will convert internally back to this very + * string for sending to the server. nice. */ + if(!gtls_alpns_count && alpn && alpn_len) { + size_t i, alen = alpn_len; + unsigned char *s = (unsigned char *)alpn; + unsigned char slen; + for(i = 0; (i < ARRAYSIZE(gtls_alpns)) && alen; ++i) { + slen = s[0]; + if(slen >= alen) + return CURLE_FAILED_INIT; + gtls_alpns[i].data = s + 1; + gtls_alpns[i].size = slen; + s += slen + 1; + alen -= (size_t)slen + 1; + } + if(alen) /* not all alpn chars used, wrong format or too many */ + return CURLE_FAILED_INIT; + gtls_alpns_count = i; + } + + if(gtls_alpns_count && + gnutls_alpn_set_protocols(gctx->session, + gtls_alpns, (unsigned int)gtls_alpns_count, + GNUTLS_ALPN_MANDATORY)) { + failf(data, "failed setting ALPN"); + return CURLE_SSL_CONNECT_ERROR; + } + return CURLE_OK; } @@ -1141,6 +1192,11 @@ gtls_connect_step1(struct Curl_cfilter *cf, struct Curl_easy *data) if(result) return result; + if(connssl->alpn && (connssl->state != ssl_connection_deferred)) { + Curl_alpn_to_proto_str(&proto, connssl->alpn); + infof(data, VTLS_INFOF_ALPN_OFFER_1STR, proto.data); + } + gnutls_handshake_set_hook_function(backend->gtls.session, GNUTLS_HANDSHAKE_ANY, GNUTLS_HOOK_POST, gtls_handshake_cb); @@ -1675,17 +1731,6 @@ static CURLcode gtls_verifyserver(struct Curl_cfilter *cf, if(result) goto out; - if(connssl->alpn) { - gnutls_datum_t proto; - int rc; - - rc = gnutls_alpn_get_selected_protocol(session, &proto); - if(rc == 0) - Curl_alpn_set_negotiated(cf, data, proto.data, proto.size); - else - Curl_alpn_set_negotiated(cf, data, NULL, 0); - } - /* Only on TLSv1.2 or lower do we have the session id now. For * TLSv1.3 we get it via a SESSION_TICKET message that arrives later. */ if(gnutls_protocol_get_version(session) < GNUTLS_TLS1_3) @@ -1695,6 +1740,70 @@ out: return result; } +static CURLcode gtls_set_earlydata(struct Curl_cfilter *cf, + struct Curl_easy *data, + const void *buf, size_t blen) +{ + struct ssl_connect_data *connssl = cf->ctx; + ssize_t nwritten = 0; + CURLcode result = CURLE_OK; + + DEBUGASSERT(connssl->earlydata_state == ssl_earlydata_use); + DEBUGASSERT(Curl_bufq_is_empty(&connssl->earlydata)); + if(blen) { + if(blen > connssl->earlydata_max) + blen = connssl->earlydata_max; + nwritten = Curl_bufq_write(&connssl->earlydata, buf, blen, &result); + CURL_TRC_CF(data, cf, "gtls_set_earlydata(len=%zu) -> %zd", + blen, nwritten); + if(nwritten < 0) + return result; + } + connssl->earlydata_state = ssl_earlydata_sending; + connssl->earlydata_skip = Curl_bufq_len(&connssl->earlydata); + return CURLE_OK; +} + +static CURLcode gtls_send_earlydata(struct Curl_cfilter *cf, + struct Curl_easy *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; + const unsigned char *buf; + size_t blen; + ssize_t n; + + DEBUGASSERT(connssl->earlydata_state == ssl_earlydata_sending); + backend->gtls.io_result = CURLE_OK; + while(Curl_bufq_peek(&connssl->earlydata, &buf, &blen)) { + n = gnutls_record_send_early_data(backend->gtls.session, buf, blen); + CURL_TRC_CF(data, cf, "gtls_send_earlydata(len=%zu) -> %zd", + blen, n); + if(n < 0) { + if(n == GNUTLS_E_AGAIN) + result = CURLE_AGAIN; + else + result = backend->gtls.io_result ? + backend->gtls.io_result : CURLE_SEND_ERROR; + goto out; + } + else if(!n) { + /* gnutls is buggy, it *SHOULD* return the amount of bytes it took in. + * Instead it returns 0 if everything was written. */ + n = (ssize_t)blen; + } + + Curl_bufq_skip(&connssl->earlydata, (size_t)n); + } + /* sent everything there was */ + infof(data, "SSL sending %" FMT_OFF_T " bytes of early data", + connssl->earlydata_skip); +out: + return result; +} + /* * This function is called after the TCP connect has completed. Setup the TLS * layer and do all necessary magic. @@ -1708,46 +1817,89 @@ static CURLcode gtls_connect_common(struct Curl_cfilter *cf, struct Curl_easy *data, bool nonblocking, - bool *done) -{ + bool *done) { struct ssl_connect_data *connssl = cf->ctx; - CURLcode rc; + struct gtls_ssl_backend_data *backend = + (struct gtls_ssl_backend_data *)connssl->backend; CURLcode result = CURLE_OK; + DEBUGASSERT(backend); + /* Initiate the connection, if not already done */ - if(ssl_connect_1 == connssl->connecting_state) { - rc = gtls_connect_step1(cf, data); - if(rc) { - result = rc; + if(connssl->connecting_state == ssl_connect_1) { + result = gtls_connect_step1(cf, data); + if(result) goto out; - } + connssl->connecting_state = ssl_connect_2; } - rc = handshake(cf, data, TRUE, nonblocking); - if(rc) { - /* handshake() sets its own error message with failf() */ - result = rc; - goto out; + if(connssl->connecting_state == ssl_connect_2) { + if(connssl->earlydata_state == ssl_earlydata_use) { + goto out; + } + else if(connssl->earlydata_state == ssl_earlydata_sending) { + result = gtls_send_earlydata(cf, data); + if(result) + goto out; + connssl->earlydata_state = ssl_earlydata_sent; + if(!Curl_ssl_cf_is_proxy(cf)) + Curl_pgrsEarlyData(data, (curl_off_t)connssl->earlydata_skip); + } + DEBUGASSERT((connssl->earlydata_state == ssl_earlydata_none) || + (connssl->earlydata_state == ssl_earlydata_sent)); + + result = handshake(cf, data, TRUE, nonblocking); + if(result) + goto out; + connssl->connecting_state = ssl_connect_3; } /* Finish connecting once the handshake is done */ - if(ssl_connect_1 == connssl->connecting_state) { - struct gtls_ssl_backend_data *backend = - (struct gtls_ssl_backend_data *)connssl->backend; - gnutls_session_t session; - DEBUGASSERT(backend); - session = backend->gtls.session; - rc = gtls_verifyserver(cf, data, session); - if(rc) { - result = rc; + if(connssl->connecting_state == ssl_connect_3) { + gnutls_datum_t proto; + int rc; + result = gtls_verifyserver(cf, data, backend->gtls.session); + if(result) goto out; - } + connssl->state = ssl_connection_complete; + connssl->connecting_state = ssl_connect_1; + + rc = gnutls_alpn_get_selected_protocol(backend->gtls.session, &proto); + if(rc) { /* No ALPN from server */ + proto.data = NULL; + proto.size = 0; + } + + result = Curl_alpn_set_negotiated(cf, data, connssl, + proto.data, proto.size); + if(result) + goto out; + + if(connssl->earlydata_state == ssl_earlydata_sent) { + if(gnutls_session_get_flags(backend->gtls.session) & + GNUTLS_SFLAGS_EARLY_DATA) { + connssl->earlydata_state = ssl_earlydata_accepted; + infof(data, "Server accepted %zu bytes of TLS early data.", + connssl->earlydata_skip); + } + else { + connssl->earlydata_state = ssl_earlydata_rejected; + if(!Curl_ssl_cf_is_proxy(cf)) + Curl_pgrsEarlyData(data, -(curl_off_t)connssl->earlydata_skip); + infof(data, "Server rejected TLS early data."); + connssl->earlydata_skip = 0; + } + } } out: - *done = ssl_connect_1 == connssl->connecting_state; - + if(result == CURLE_AGAIN) { + *done = FALSE; + return CURLE_OK; + } + *done = ((connssl->connecting_state == ssl_connect_1) || + (connssl->state == ssl_connection_deferred)); return result; } @@ -1755,6 +1907,12 @@ static CURLcode gtls_connect_nonblocking(struct Curl_cfilter *cf, struct Curl_easy *data, bool *done) { + struct ssl_connect_data *connssl = cf->ctx; + if(connssl->state == ssl_connection_deferred) { + /* We refuse to be pushed, we are waiting for someone to send/recv. */ + *done = TRUE; + return CURLE_OK; + } return gtls_connect_common(cf, data, TRUE, done); } @@ -1773,6 +1931,26 @@ static CURLcode gtls_connect(struct Curl_cfilter *cf, return CURLE_OK; } +static CURLcode gtls_connect_deferred(struct Curl_cfilter *cf, + struct Curl_easy *data, + const void *buf, + size_t blen, + bool *done) +{ + struct ssl_connect_data *connssl = cf->ctx; + CURLcode result = CURLE_OK; + + DEBUGASSERT(connssl->state == ssl_connection_deferred); + *done = FALSE; + if(connssl->earlydata_state == ssl_earlydata_use) { + result = gtls_set_earlydata(cf, data, buf, blen); + if(result) + return result; + } + + return gtls_connect_common(cf, data, TRUE, done); +} + static bool gtls_data_pending(struct Curl_cfilter *cf, const struct Curl_easy *data) { @@ -1800,8 +1978,38 @@ static ssize_t gtls_send(struct Curl_cfilter *cf, ssize_t rc; size_t nwritten, total_written = 0; - (void)data; DEBUGASSERT(backend); + + if(connssl->state == ssl_connection_deferred) { + bool done = FALSE; + *curlcode = gtls_connect_deferred(cf, data, buf, blen, &done); + if(*curlcode) { + rc = -1; + goto out; + } + else if(!done) { + *curlcode = CURLE_AGAIN; + rc = -1; + goto out; + } + DEBUGASSERT(connssl->state == ssl_connection_complete); + } + + if(connssl->earlydata_skip) { + if(connssl->earlydata_skip >= blen) { + connssl->earlydata_skip -= blen; + *curlcode = CURLE_OK; + rc = (ssize_t)blen; + goto out; + } + else { + total_written += connssl->earlydata_skip; + buf = ((const char *)buf) + connssl->earlydata_skip; + blen -= connssl->earlydata_skip; + connssl->earlydata_skip = 0; + } + } + while(blen) { backend->gtls.io_result = CURLE_OK; rc = gnutls_record_send(backend->gtls.session, buf, blen); @@ -1848,7 +2056,9 @@ static CURLcode gtls_shutdown(struct Curl_cfilter *cf, size_t i; DEBUGASSERT(backend); - if(!backend->gtls.session || cf->shutdown) { + /* If we have no handshaked connection or already shut down */ + if(!backend->gtls.session || cf->shutdown || + connssl->state != ssl_connection_complete) { *done = TRUE; goto out; } @@ -1946,7 +2156,21 @@ static ssize_t gtls_recv(struct Curl_cfilter *cf, (void)data; DEBUGASSERT(backend); - backend->gtls.io_result = CURLE_OK; + if(connssl->state == ssl_connection_deferred) { + bool done = FALSE; + *curlcode = gtls_connect_deferred(cf, data, NULL, 0, &done); + if(*curlcode) { + ret = -1; + goto out; + } + else if(!done) { + *curlcode = CURLE_AGAIN; + ret = -1; + goto out; + } + DEBUGASSERT(connssl->state == ssl_connection_complete); + } + ret = gnutls_record_recv(backend->gtls.session, buf, buffersize); if((ret == GNUTLS_E_AGAIN) || (ret == GNUTLS_E_INTERRUPTED)) { *curlcode = CURLE_AGAIN; diff --git a/lib/vtls/mbedtls.c b/lib/vtls/mbedtls.c index 65171a1a47..e89312ed28 100644 --- a/lib/vtls/mbedtls.c +++ b/lib/vtls/mbedtls.c @@ -877,7 +877,8 @@ mbed_connect_step1(struct Curl_cfilter *cf, struct Curl_easy *data) void *old_session = NULL; Curl_ssl_sessionid_lock(data); - if(!Curl_ssl_getsessionid(cf, data, &connssl->peer, &old_session, NULL)) { + if(!Curl_ssl_getsessionid(cf, data, &connssl->peer, + &old_session, NULL, NULL)) { ret = mbedtls_ssl_set_session(&backend->ssl, old_session); if(ret) { Curl_ssl_sessionid_unlock(data); @@ -1093,7 +1094,7 @@ pinnedpubkey_error: if(connssl->alpn) { const char *proto = mbedtls_ssl_get_alpn_protocol(&backend->ssl); - Curl_alpn_set_negotiated(cf, data, (const unsigned char *)proto, + Curl_alpn_set_negotiated(cf, data, connssl, (const unsigned char *)proto, proto ? strlen(proto) : 0); } #endif @@ -1144,7 +1145,7 @@ mbed_connect_step3(struct Curl_cfilter *cf, struct Curl_easy *data) /* If there is already a matching session in the cache, delete it */ Curl_ssl_sessionid_lock(data); - retcode = Curl_ssl_set_sessionid(cf, data, &connssl->peer, + retcode = Curl_ssl_set_sessionid(cf, data, &connssl->peer, NULL, our_ssl_sessionid, 0, mbedtls_session_free); Curl_ssl_sessionid_unlock(data); diff --git a/lib/vtls/openssl.c b/lib/vtls/openssl.c index 4d39d38dca..43c478eca2 100644 --- a/lib/vtls/openssl.c +++ b/lib/vtls/openssl.c @@ -2914,7 +2914,7 @@ CURLcode Curl_ossl_add_session(struct Curl_cfilter *cf, } Curl_ssl_sessionid_lock(data); - result = Curl_ssl_set_sessionid(cf, data, peer, der_session_buf, + result = Curl_ssl_set_sessionid(cf, data, peer, NULL, der_session_buf, der_session_size, ossl_session_free); Curl_ssl_sessionid_unlock(data); } @@ -3973,7 +3973,7 @@ CURLcode Curl_ossl_ctx_init(struct ossl_ctx *octx, if(ssl_config->primary.cache_session && transport == TRNSPRT_TCP) { Curl_ssl_sessionid_lock(data); if(!Curl_ssl_getsessionid(cf, data, peer, (void **)&der_sessionid, - &der_sessionid_size)) { + &der_sessionid_size, NULL)) { /* we got a session id, use it! */ ssl_session = d2i_SSL_SESSION(NULL, &der_sessionid, (long)der_sessionid_size); @@ -4377,7 +4377,7 @@ static CURLcode ossl_connect_step2(struct Curl_cfilter *cf, unsigned int len; SSL_get0_alpn_selected(octx->ssl, &neg_protocol, &len); - return Curl_alpn_set_negotiated(cf, data, neg_protocol, len); + return Curl_alpn_set_negotiated(cf, data, connssl, neg_protocol, len); } #endif @@ -4710,7 +4710,7 @@ CURLcode Curl_oss_check_peer_cert(struct Curl_cfilter *cf, bool incache; Curl_ssl_sessionid_lock(data); incache = !(Curl_ssl_getsessionid(cf, data, peer, - &old_ssl_sessionid, NULL)); + &old_ssl_sessionid, NULL, NULL)); if(incache) { infof(data, "Remove session ID again from cache"); Curl_ssl_delsessionid(data, old_ssl_sessionid); diff --git a/lib/vtls/rustls.c b/lib/vtls/rustls.c index 2529a5b8bb..c9409d12aa 100644 --- a/lib/vtls/rustls.c +++ b/lib/vtls/rustls.c @@ -768,11 +768,12 @@ static void cr_set_negotiated_alpn(struct Curl_cfilter *cf, struct Curl_easy *data, const struct rustls_connection *rconn) { + struct ssl_connect_data *const connssl = cf->ctx; const uint8_t *protocol = NULL; size_t len = 0; rustls_connection_get_alpn_protocol(rconn, &protocol, &len); - Curl_alpn_set_negotiated(cf, data, protocol, len); + Curl_alpn_set_negotiated(cf, data, connssl, protocol, len); } /* Given an established network connection, do a TLS handshake. diff --git a/lib/vtls/schannel.c b/lib/vtls/schannel.c index f294a1ae6a..1fc3381a78 100644 --- a/lib/vtls/schannel.c +++ b/lib/vtls/schannel.c @@ -1130,7 +1130,7 @@ schannel_connect_step1(struct Curl_cfilter *cf, struct Curl_easy *data) if(ssl_config->primary.cache_session) { Curl_ssl_sessionid_lock(data); if(!Curl_ssl_getsessionid(cf, data, &connssl->peer, - (void **)&old_cred, NULL)) { + (void **)&old_cred, NULL, NULL)) { backend->cred = old_cred; DEBUGF(infof(data, "schannel: reusing existing credential handle")); @@ -1752,7 +1752,7 @@ schannel_connect_step3(struct Curl_cfilter *cf, struct Curl_easy *data) SecApplicationProtocolNegotiationStatus_Success) { unsigned char prev_alpn = cf->conn->alpn; - Curl_alpn_set_negotiated(cf, data, alpn_result.ProtocolId, + Curl_alpn_set_negotiated(cf, data, connssl, alpn_result.ProtocolId, alpn_result.ProtocolIdSize); if(backend->recv_renegotiating) { if(prev_alpn != cf->conn->alpn && @@ -1766,7 +1766,7 @@ schannel_connect_step3(struct Curl_cfilter *cf, struct Curl_easy *data) } else { if(!backend->recv_renegotiating) - Curl_alpn_set_negotiated(cf, data, NULL, 0); + Curl_alpn_set_negotiated(cf, data, connssl, NULL, 0); } } #endif @@ -1776,7 +1776,8 @@ schannel_connect_step3(struct Curl_cfilter *cf, struct Curl_easy *data) Curl_ssl_sessionid_lock(data); /* Up ref count since call takes ownership */ backend->cred->refcount++; - result = Curl_ssl_set_sessionid(cf, data, &connssl->peer, backend->cred, + result = Curl_ssl_set_sessionid(cf, data, &connssl->peer, NULL, + backend->cred, sizeof(struct Curl_schannel_cred), schannel_session_free); Curl_ssl_sessionid_unlock(data); diff --git a/lib/vtls/sectransp.c b/lib/vtls/sectransp.c index 3632643c70..022c467f01 100644 --- a/lib/vtls/sectransp.c +++ b/lib/vtls/sectransp.c @@ -1334,7 +1334,8 @@ static CURLcode sectransp_connect_step1(struct Curl_cfilter *cf, Curl_ssl_sessionid_lock(data); if(!Curl_ssl_getsessionid(cf, data, &connssl->peer, - (void **)&ssl_sessionid, &ssl_sessionid_len)) { + (void **)&ssl_sessionid, &ssl_sessionid_len, + NULL)) { /* we got a session id, use it! */ err = SSLSetPeerID(backend->ssl_ctx, ssl_sessionid, ssl_sessionid_len); Curl_ssl_sessionid_unlock(data); @@ -1362,8 +1363,8 @@ static CURLcode sectransp_connect_step1(struct Curl_cfilter *cf, return CURLE_SSL_CONNECT_ERROR; } - result = Curl_ssl_set_sessionid(cf, data, &connssl->peer, ssl_sessionid, - ssl_sessionid_len, + result = Curl_ssl_set_sessionid(cf, data, &connssl->peer, NULL, + ssl_sessionid, ssl_sessionid_len, sectransp_session_free); Curl_ssl_sessionid_unlock(data); if(result) diff --git a/lib/vtls/vtls.c b/lib/vtls/vtls.c index 57dba55527..2a9922b744 100644 --- a/lib/vtls/vtls.c +++ b/lib/vtls/vtls.c @@ -454,6 +454,7 @@ static struct ssl_connect_data *cf_ctx_new(struct Curl_easy *data, return NULL; ctx->alpn = alpn; + Curl_bufq_init2(&ctx->earlydata, CURL_SSL_EARLY_MAX, 1, BUFQ_OPT_NO_SPARES); ctx->backend = calloc(1, Curl_ssl->sizeof_ssl_backend_data); if(!ctx->backend) { free(ctx); @@ -465,6 +466,8 @@ static struct ssl_connect_data *cf_ctx_new(struct Curl_easy *data, static void cf_ctx_free(struct ssl_connect_data *ctx) { if(ctx) { + Curl_safefree(ctx->alpn_negotiated); + Curl_bufq_free(&ctx->earlydata); free(ctx->backend); free(ctx); } @@ -527,7 +530,8 @@ bool Curl_ssl_getsessionid(struct Curl_cfilter *cf, struct Curl_easy *data, const struct ssl_peer *peer, void **ssl_sessionid, - size_t *idsize) /* set 0 if unknown */ + size_t *idsize, /* set 0 if unknown */ + char **palpn) { 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); @@ -537,6 +541,8 @@ bool Curl_ssl_getsessionid(struct Curl_cfilter *cf, bool no_match = TRUE; *ssl_sessionid = NULL; + if(palpn) + *palpn = NULL; if(!ssl_config) return TRUE; @@ -575,6 +581,8 @@ bool Curl_ssl_getsessionid(struct Curl_cfilter *cf, *ssl_sessionid = check->sessionid; if(idsize) *idsize = check->idsize; + if(palpn) + *palpn = check->alpn; no_match = FALSE; break; } @@ -605,6 +613,7 @@ void Curl_ssl_kill_session(struct Curl_ssl_session *session) Curl_safefree(session->name); Curl_safefree(session->conn_to_host); + Curl_safefree(session->alpn); } } @@ -628,6 +637,7 @@ void Curl_ssl_delsessionid(struct Curl_easy *data, void *ssl_sessionid) CURLcode Curl_ssl_set_sessionid(struct Curl_cfilter *cf, struct Curl_easy *data, const struct ssl_peer *peer, + const char *alpn, void *ssl_sessionid, size_t idsize, Curl_ssl_sessionid_dtor *sessionid_free_cb) @@ -639,6 +649,7 @@ CURLcode Curl_ssl_set_sessionid(struct Curl_cfilter *cf, long oldest_age; char *clone_host = NULL; char *clone_conn_to_host = NULL; + char *clone_alpn = NULL; int conn_to_port; long *general_age; void *old_sessionid; @@ -653,7 +664,7 @@ CURLcode Curl_ssl_set_sessionid(struct Curl_cfilter *cf, return CURLE_OK; } - if(!Curl_ssl_getsessionid(cf, data, peer, &old_sessionid, &old_size)) { + if(!Curl_ssl_getsessionid(cf, data, peer, &old_sessionid, &old_size, NULL)) { if((old_size == idsize) && ((old_sessionid == ssl_sessionid) || (idsize && !memcmp(old_sessionid, ssl_sessionid, idsize)))) { @@ -679,6 +690,10 @@ CURLcode Curl_ssl_set_sessionid(struct Curl_cfilter *cf, goto out; } + clone_alpn = alpn ? strdup(alpn) : NULL; + if(alpn && !clone_alpn) + goto out; + if(cf->conn->bits.conn_to_port) conn_to_port = cf->conn->conn_to_port; else @@ -727,6 +742,8 @@ CURLcode Curl_ssl_set_sessionid(struct Curl_cfilter *cf, store->conn_to_host = clone_conn_to_host; /* clone connect to hostname */ clone_conn_to_host = NULL; store->conn_to_port = conn_to_port; /* connect to port number */ + store->alpn = clone_alpn; + clone_alpn = NULL; /* port number */ store->remote_port = peer->port; store->scheme = cf->conn->handler->scheme; @@ -737,6 +754,7 @@ CURLcode Curl_ssl_set_sessionid(struct Curl_cfilter *cf, out: free(clone_host); free(clone_conn_to_host); + free(clone_alpn); if(result) { failf(data, "Failed to add Session ID to cache for %s://%s:%d [%s]", store->scheme, store->name, store->remote_port, @@ -1716,7 +1734,9 @@ static CURLcode ssl_cf_connect(struct Curl_cfilter *cf, if(!result && *done) { cf->connected = TRUE; connssl->handshake_done = Curl_now(); - DEBUGASSERT(connssl->state == ssl_connection_complete); + /* Connection can be deferred when sending early data */ + DEBUGASSERT(connssl->state == ssl_connection_complete || + connssl->state == ssl_connection_deferred); } out: CURL_TRC_CF(data, cf, "cf_connect() -> %d, done=%d", result, *done); @@ -2219,11 +2239,25 @@ CURLcode Curl_alpn_to_proto_str(struct alpn_proto_buf *buf, return CURLE_OK; } +bool Curl_alpn_contains_proto(const struct alpn_spec *spec, + const char *proto) +{ + size_t i, plen = proto ? strlen(proto) : 0; + for(i = 0; spec && plen && i < spec->count; ++i) { + size_t slen = strlen(spec->entries[i]); + if((slen == plen) && !memcmp(proto, spec->entries[i], plen)) + return TRUE; + } + return FALSE; +} + CURLcode Curl_alpn_set_negotiated(struct Curl_cfilter *cf, struct Curl_easy *data, + struct ssl_connect_data *connssl, const unsigned char *proto, size_t proto_len) { + CURLcode result = CURLE_OK; unsigned char *palpn = #ifndef CURL_DISABLE_PROXY (cf->conn->bits.tunnel_proxy && Curl_ssl_cf_is_proxy(cf)) ? @@ -2233,6 +2267,45 @@ CURLcode Curl_alpn_set_negotiated(struct Curl_cfilter *cf, #endif ; + if(connssl->alpn_negotiated) { + /* When we ask for a specific ALPN protocol, we need the confirmation + * of it by the server, as we have installed protocol handler and + * connection filter chain for exactly this protocol. */ + if(!proto_len) { + failf(data, "ALPN: asked for '%s' from previous session, " + "but server did not confirm it. Refusing to continue.", + connssl->alpn_negotiated); + result = CURLE_SSL_CONNECT_ERROR; + goto out; + } + else if((strlen(connssl->alpn_negotiated) != proto_len) || + memcmp(connssl->alpn_negotiated, proto, proto_len)) { + failf(data, "ALPN: asked for '%s' from previous session, but server " + "selected '%.*s'. Refusing to continue.", + connssl->alpn_negotiated, (int)proto_len, proto); + result = CURLE_SSL_CONNECT_ERROR; + goto out; + } + /* ALPN is exactly what we asked for, done. */ + infof(data, "ALPN: server confirmed to use '%s'", + connssl->alpn_negotiated); + goto out; + } + + if(proto && proto_len) { + if(memchr(proto, '\0', proto_len)) { + failf(data, "ALPN: server selected protocol contains NUL. " + "Refusing to continue."); + result = CURLE_SSL_CONNECT_ERROR; + goto out; + } + connssl->alpn_negotiated = malloc(proto_len + 1); + if(!connssl->alpn_negotiated) + return CURLE_OUT_OF_MEMORY; + memcpy(connssl->alpn_negotiated, proto, proto_len); + connssl->alpn_negotiated[proto_len] = 0; + } + if(proto && proto_len) { if(proto_len == ALPN_HTTP_1_1_LENGTH && !memcmp(ALPN_HTTP_1_1, proto, ALPN_HTTP_1_1_LENGTH)) { @@ -2258,15 +2331,22 @@ CURLcode Curl_alpn_set_negotiated(struct Curl_cfilter *cf, /* return CURLE_NOT_BUILT_IN; */ goto out; } - infof(data, VTLS_INFOF_ALPN_ACCEPTED_LEN_1STR, (int)proto_len, proto); + + if(connssl->state == ssl_connection_deferred) + infof(data, VTLS_INFOF_ALPN_DEFERRED, (int)proto_len, proto); + else + infof(data, VTLS_INFOF_ALPN_ACCEPTED, (int)proto_len, proto); } else { *palpn = CURL_HTTP_VERSION_NONE; - infof(data, VTLS_INFOF_NO_ALPN); + if(connssl->state == ssl_connection_deferred) + infof(data, VTLS_INFOF_NO_ALPN_DEFERRED); + else + infof(data, VTLS_INFOF_NO_ALPN); } out: - return CURLE_OK; + return result; } #endif /* USE_SSL */ diff --git a/lib/vtls/vtls.h b/lib/vtls/vtls.h index fce1e00183..7a223f6fea 100644 --- a/lib/vtls/vtls.h +++ b/lib/vtls/vtls.h @@ -43,15 +43,18 @@ struct Curl_ssl_session; #define ALPN_ACCEPTED "ALPN: server accepted " -#define VTLS_INFOF_NO_ALPN \ +#define VTLS_INFOF_NO_ALPN \ "ALPN: server did not agree on a protocol. Uses default." -#define VTLS_INFOF_ALPN_OFFER_1STR \ +#define VTLS_INFOF_ALPN_OFFER_1STR \ "ALPN: curl offers %s" -#define VTLS_INFOF_ALPN_ACCEPTED_1STR \ - ALPN_ACCEPTED "%s" -#define VTLS_INFOF_ALPN_ACCEPTED_LEN_1STR \ +#define VTLS_INFOF_ALPN_ACCEPTED \ ALPN_ACCEPTED "%.*s" +#define VTLS_INFOF_NO_ALPN_DEFERRED \ + "ALPN: deferred handshake for early data without specific protocol." +#define VTLS_INFOF_ALPN_DEFERRED \ + "ALPN: deferred handshake for early data using '%.*s'." + /* Curl_multi SSL backend-specific data; declared differently by each SSL backend */ struct Curl_cfilter; diff --git a/lib/vtls/vtls_int.h b/lib/vtls/vtls_int.h index ce5e7cf396..eb7e7a513c 100644 --- a/lib/vtls/vtls_int.h +++ b/lib/vtls/vtls_int.h @@ -29,6 +29,8 @@ #ifdef USE_SSL +struct ssl_connect_data; + /* see https://www.iana.org/assignments/tls-extensiontype-values/ */ #define ALPN_HTTP_1_1_LENGTH 8 #define ALPN_HTTP_1_1 "http/1.1" @@ -61,9 +63,13 @@ CURLcode Curl_alpn_to_proto_str(struct alpn_proto_buf *buf, CURLcode Curl_alpn_set_negotiated(struct Curl_cfilter *cf, struct Curl_easy *data, + struct ssl_connect_data *connssl, const unsigned char *proto, size_t proto_len); +bool Curl_alpn_contains_proto(const struct alpn_spec *spec, + const char *proto); + /* enum for the nonblocking SSL connection state machine */ typedef enum { ssl_connect_1, @@ -74,14 +80,27 @@ typedef enum { typedef enum { ssl_connection_none, + ssl_connection_deferred, ssl_connection_negotiating, ssl_connection_complete } ssl_connection_state; +typedef enum { + ssl_earlydata_none, + ssl_earlydata_use, + ssl_earlydata_sending, + ssl_earlydata_sent, + ssl_earlydata_accepted, + ssl_earlydata_rejected +} ssl_earlydata_state; + #define CURL_SSL_IO_NEED_NONE (0) #define CURL_SSL_IO_NEED_RECV (1<<0) #define CURL_SSL_IO_NEED_SEND (1<<1) +/* Max earlydata payload we want to send */ +#define CURL_SSL_EARLY_MAX (64*1024) + /* Information in each SSL cfilter context: cf->ctx */ struct ssl_connect_data { struct ssl_peer peer; @@ -89,8 +108,14 @@ struct ssl_connect_data { void *backend; /* vtls backend specific props */ struct cf_call_data call_data; /* data handle used in current call */ struct curltime handshake_done; /* time when handshake finished */ + char *alpn_negotiated; /* negotiated ALPN value or NULL */ + struct bufq earlydata; /* earlydata to be send to peer */ + size_t earlydata_max; /* max earlydata allowed by peer */ + size_t earlydata_skip; /* sending bytes to skip when earlydata + * is accepted by peer */ ssl_connection_state state; ssl_connect_state connecting_state; + ssl_earlydata_state earlydata_state; int io_need; /* TLS signals special SEND/RECV needs */ BIT(use_alpn); /* if ALPN shall be used in handshake */ BIT(peer_closed); /* peer has closed connection */ @@ -193,12 +218,20 @@ bool Curl_ssl_cf_is_proxy(struct Curl_cfilter *cf); * Caller must make sure that the ownership of returned sessionid object * is properly taken (e.g. its refcount is incremented * under sessionid mutex). + * @param cf the connection filter wanting to use it + * @param data the transfer involved + * @param peer the peer the filter wants to talk to + * @param sessionid on return the TLS session + * @param idsize on return the size of the TLS session data + * @param palpn on return the ALPN string used by the session, + * set to NULL when not interested */ bool Curl_ssl_getsessionid(struct Curl_cfilter *cf, struct Curl_easy *data, const struct ssl_peer *peer, void **ssl_sessionid, - size_t *idsize); /* set 0 if unknown */ + size_t *idsize, /* set 0 if unknown */ + char **palpn); /* Set a TLS session ID for `peer`. Replaces an existing session ID if * not already the very same. @@ -212,6 +245,7 @@ bool Curl_ssl_getsessionid(struct Curl_cfilter *cf, CURLcode Curl_ssl_set_sessionid(struct Curl_cfilter *cf, struct Curl_easy *data, const struct ssl_peer *peer, + const char *alpn, void *sessionid, size_t sessionid_size, Curl_ssl_sessionid_dtor *sessionid_free_cb); diff --git a/lib/vtls/wolfssl.c b/lib/vtls/wolfssl.c index 5be005b912..c36306c6a8 100644 --- a/lib/vtls/wolfssl.c +++ b/lib/vtls/wolfssl.c @@ -1059,7 +1059,7 @@ wolfssl_connect_step1(struct Curl_cfilter *cf, struct Curl_easy *data) Curl_ssl_sessionid_lock(data); if(!Curl_ssl_getsessionid(cf, data, &connssl->peer, - &ssl_sessionid, NULL)) { + &ssl_sessionid, NULL, NULL)) { /* we got a session id, use it! */ if(!SSL_set_session(backend->handle, ssl_sessionid)) { Curl_ssl_delsessionid(data, ssl_sessionid); @@ -1406,11 +1406,11 @@ wolfssl_connect_step2(struct Curl_cfilter *cf, struct Curl_easy *data) rc = wolfSSL_ALPN_GetProtocol(backend->handle, &protocol, &protocol_len); if(rc == SSL_SUCCESS) { - Curl_alpn_set_negotiated(cf, data, (const unsigned char *)protocol, - protocol_len); + Curl_alpn_set_negotiated(cf, data, connssl, + (const unsigned char *)protocol, protocol_len); } else if(rc == SSL_ALPN_NOT_FOUND) - Curl_alpn_set_negotiated(cf, data, NULL, 0); + Curl_alpn_set_negotiated(cf, data, connssl, NULL, 0); else { failf(data, "ALPN, failure getting protocol, error %d", rc); return CURLE_SSL_CONNECT_ERROR; @@ -1457,7 +1457,7 @@ wolfssl_connect_step3(struct Curl_cfilter *cf, struct Curl_easy *data) if(our_ssl_sessionid) { Curl_ssl_sessionid_lock(data); /* call takes ownership of `our_ssl_sessionid` */ - result = Curl_ssl_set_sessionid(cf, data, &connssl->peer, + result = Curl_ssl_set_sessionid(cf, data, &connssl->peer, NULL, our_ssl_sessionid, 0, wolfssl_session_free); Curl_ssl_sessionid_unlock(data); diff --git a/src/tool_cfgable.h b/src/tool_cfgable.h index d9099c329d..257a8d2c66 100644 --- a/src/tool_cfgable.h +++ b/src/tool_cfgable.h @@ -261,6 +261,7 @@ struct OperationConfig { bool xattr; /* store metadata in extended attributes */ long gssapi_delegation; bool ssl_allow_beast; /* allow this SSL vulnerability */ + bool ssl_allow_earlydata; /* allow use of TLSv1.3 early data */ bool proxy_ssl_allow_beast; /* allow this SSL vulnerability for proxy */ bool ssl_no_revoke; /* disable SSL certificate revocation checks */ bool ssl_revoke_best_effort; /* ignore SSL revocation offline/missing diff --git a/src/tool_getparam.c b/src/tool_getparam.c index 7b6aea70a9..da42a4f62d 100644 --- a/src/tool_getparam.c +++ b/src/tool_getparam.c @@ -316,6 +316,7 @@ static const struct LongShort aliases[]= { {"tftp-blksize", ARG_STRG, ' ', C_TFTP_BLKSIZE}, {"tftp-no-options", ARG_BOOL, ' ', C_TFTP_NO_OPTIONS}, {"time-cond", ARG_STRG, 'z', C_TIME_COND}, + {"tls-earlydata", ARG_BOOL, ' ', C_TLS_EARLYDATA}, {"tls-max", ARG_STRG, ' ', C_TLS_MAX}, {"tls13-ciphers", ARG_STRG, ' ', C_TLS13_CIPHERS}, {"tlsauthtype", ARG_STRG, ' ', C_TLSAUTHTYPE}, @@ -1691,6 +1692,10 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */ config->abstract_unix_socket = TRUE; err = getstr(&config->unix_socket_path, nextarg, DENY_BLANK); break; + case C_TLS_EARLYDATA: /* --tls-earlydata */ + if(feature_ssl) + config->ssl_allow_earlydata = toggle; + break; case C_TLS_MAX: /* --tls-max */ err = str2tls_max(&config->ssl_version_max, nextarg); break; diff --git a/src/tool_getparam.h b/src/tool_getparam.h index b22e60b7b3..c42f686a48 100644 --- a/src/tool_getparam.h +++ b/src/tool_getparam.h @@ -271,6 +271,7 @@ typedef enum { C_TFTP_BLKSIZE, C_TFTP_NO_OPTIONS, C_TIME_COND, + C_TLS_EARLYDATA, C_TLS_MAX, C_TLS13_CIPHERS, C_TLSAUTHTYPE, diff --git a/src/tool_listhelp.c b/src/tool_listhelp.c index fa29a51c1f..2d5f2b3ab9 100644 --- a/src/tool_listhelp.c +++ b/src/tool_listhelp.c @@ -748,6 +748,9 @@ const struct helptxt helptext[] = { {"-z, --time-cond