From 515a21f350b89f0676e5df7f904c62c8f67be377 Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Tue, 7 Jan 2025 12:41:26 +0100 Subject: [PATCH] vtls: feature ssls-export for SSL session im-/export Adds the experimental feature `ssls-export` to libcurl and curl for importing and exporting SSL sessions from/to a file. * add functions to libcurl API * add command line option `--ssl-sessions ` to curl * add documenation * add support in configure * add support in cmake + add pytest case Closes #15924 --- .github/scripts/spellcheck.words | 1 + CMakeLists.txt | 10 + configure.ac | 26 ++ docs/EXPERIMENTAL.md | 12 + docs/INSTALL-CMAKE.md | 1 + docs/cmdline-opts/Makefile.inc | 1 + docs/cmdline-opts/ssl-sessions.md | 35 ++ docs/cmdline-opts/tls-earlydata.md | 1 + docs/libcurl/Makefile.inc | 2 + docs/libcurl/curl_easy_ssls_export.md | 172 +++++++++ docs/libcurl/curl_easy_ssls_import.md | 84 +++++ docs/libcurl/curl_global_trace.md | 4 + docs/libcurl/curl_version_info.md | 7 + docs/options-in-versions | 1 + include/curl/curl.h | 44 +++ lib/Makefile.inc | 2 + lib/curl_get_line.c | 2 + lib/curl_get_line.h | 6 + lib/curl_trc.c | 21 ++ lib/curl_trc.h | 13 + lib/easy.c | 39 +++ lib/libcurl.def | 2 + lib/version.c | 3 + lib/vquic/curl_ngtcp2.c | 4 + lib/vtls/gtls.c | 7 +- lib/vtls/vtls.c | 2 +- lib/vtls/vtls_scache.c | 487 ++++++++++++++++++++------ lib/vtls/vtls_scache.h | 12 + lib/vtls/vtls_spack.c | 345 ++++++++++++++++++ lib/vtls/vtls_spack.h | 43 +++ m4/curl-confopts.m4 | 38 ++ projects/generate.bat | 2 + scripts/singleuse.pl | 2 + src/Makefile.inc | 7 +- src/tool_cfgable.h | 1 + src/tool_getparam.c | 7 + src/tool_getparam.h | 1 + src/tool_libinfo.c | 2 + src/tool_libinfo.h | 1 + src/tool_listhelp.c | 3 + src/tool_operate.c | 32 +- src/tool_ssls.c | 222 ++++++++++++ src/tool_ssls.h | 37 ++ tests/data/test1135 | 2 + tests/http/test_17_ssl_use.py | 34 ++ tests/server/Makefile.inc | 2 + tests/unit/unit3200.c | 2 + winbuild/MakefileBuild.vc | 3 + 48 files changed, 1662 insertions(+), 125 deletions(-) create mode 100644 docs/cmdline-opts/ssl-sessions.md create mode 100644 docs/libcurl/curl_easy_ssls_export.md create mode 100644 docs/libcurl/curl_easy_ssls_import.md create mode 100644 lib/vtls/vtls_spack.c create mode 100644 lib/vtls/vtls_spack.h create mode 100644 src/tool_ssls.c create mode 100644 src/tool_ssls.h diff --git a/.github/scripts/spellcheck.words b/.github/scripts/spellcheck.words index 2e90bd0640..85714c771d 100644 --- a/.github/scripts/spellcheck.words +++ b/.github/scripts/spellcheck.words @@ -779,6 +779,7 @@ src SRP SRWLOCK SSL +SSLS ssl SSLeay SSLKEYLOGFILE diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ede5306fe..6de5cb7fe6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -978,6 +978,15 @@ if(USE_ECH) endif() endif() +option(USE_SSLS_EXPORT "Enable SSL session export support" OFF) +if(USE_SSLS_EXPORT) + if(_ssl_enabled) + message(STATUS "SSL export enabled.") + else() + message(FATAL_ERROR "SSL session export requires SSL enabled") + endif() +endif() + option(USE_NGHTTP2 "Use nghttp2 library" ON) if(USE_NGHTTP2) find_package(NGHTTP2) @@ -2086,6 +2095,7 @@ curl_add_if("TrackMemory" ENABLE_CURLDEBUG) curl_add_if("ECH" _ssl_enabled AND HAVE_ECH) curl_add_if("PSL" USE_LIBPSL) curl_add_if("CAcert" CURL_CA_EMBED_SET) +curl_add_if("SSLS-EXPORT" _ssl_enabled AND USE_SSLS_EXPORT) if(_items) if(NOT CMAKE_VERSION VERSION_LESS 3.13) list(SORT _items CASE INSENSITIVE) diff --git a/configure.ac b/configure.ac index ecb58a924c..8c05f70d95 100644 --- a/configure.ac +++ b/configure.ac @@ -54,6 +54,7 @@ CURL_CHECK_OPTION_ARES CURL_CHECK_OPTION_RT CURL_CHECK_OPTION_HTTPSRR CURL_CHECK_OPTION_ECH +CURL_CHECK_OPTION_SSLS_EXPORT XC_CHECK_PATH_SEPARATOR @@ -4946,6 +4947,26 @@ else CURL_DISABLE_WEBSOCKETS=1 fi +dnl ************************************************************* +dnl check whether experimental SSL Session Im-/Export is enabled +dnl +if test "x$want_ssls_export" != "xno"; then + AC_MSG_CHECKING([whether SSL session export support is available]) + + dnl assume NOT and look for sufficient condition + SSLS_EXPORT_ENABLED=0 + SSLS_EXPORT_SUPPORT='' + + if test "x$SSL_ENABLED" != "x1"; then + AC_MSG_ERROR([--enable-ssls-export ignored: No SSL support]) + else + SSLS_EXPORT_ENABLED=1 + AC_DEFINE(USE_SSLS_EXPORT, 1, [if SSL session export support is available]) + AC_MSG_RESULT("SSL session im-/export enabled") + experimental="$experimental SSLS-EXPORT" + fi +fi + dnl ************************************************************ dnl hiding of library internal symbols dnl @@ -5148,6 +5169,10 @@ if test "x$OPENSSL_ENABLED" = "x1" -o -n "$SSL_ENABLED"; then fi fi +if test "x$SSLS_EXPORT_ENABLED" = "x1"; then + SUPPORT_FEATURES="$SUPPORT_FEATURES SSLS-EXPORT" +fi + if test ${ac_cv_sizeof_curl_off_t} -gt 4; then if test ${ac_cv_sizeof_off_t} -gt 4 -o \ "$curl_win32_file_api" = "win32_large_files"; then @@ -5413,6 +5438,7 @@ AC_MSG_NOTICE([Configured to build curl/libcurl: HTTP2: ${curl_h2_msg} HTTP3: ${curl_h3_msg} ECH: ${curl_ech_msg} + SSLS-EXPORT: ${curl_ssls_export_msg} Protocols: ${SUPPORT_PROTOCOLS_LOWER} Features: ${SUPPORT_FEATURES} ]) diff --git a/docs/EXPERIMENTAL.md b/docs/EXPERIMENTAL.md index 00e267a960..a8eab659bc 100644 --- a/docs/EXPERIMENTAL.md +++ b/docs/EXPERIMENTAL.md @@ -63,3 +63,15 @@ Graduation requirements: - it has been given time to mature, so no earlier than April 2025 (twelve months after being added here) + +## SSL session import/export + +Import/Export of SSL sessions tickets in libcurl and curl command line +option '--ssl-session ' for faster TLS handshakes and use +of TLSv1.3/QUIC Early Data (0-RTT). + +Graduation requirements: + +- the implementation is considered safe + +- feedback from users saying that session export works for their use cases diff --git a/docs/INSTALL-CMAKE.md b/docs/INSTALL-CMAKE.md index 1cdde97c2c..33622a3b2d 100644 --- a/docs/INSTALL-CMAKE.md +++ b/docs/INSTALL-CMAKE.md @@ -191,6 +191,7 @@ assumes that CMake generates `Makefile`: - `USE_ECH`: Enable ECH support. Default: `OFF` - `USE_HTTPSRR`: Enable HTTPS RR support. Default: `OFF` - `USE_OPENSSL_QUIC`: Use OpenSSL and nghttp3 libraries for HTTP/3 support. Default: `OFF` +- `USE_SSLS_EXPORT`: Enable experimental SSL session import/export. Default: `OFF` ## Disabling features diff --git a/docs/cmdline-opts/Makefile.inc b/docs/cmdline-opts/Makefile.inc index 3bcffa49fc..be6fadd26b 100644 --- a/docs/cmdline-opts/Makefile.inc +++ b/docs/cmdline-opts/Makefile.inc @@ -269,6 +269,7 @@ DPAGES = \ ssl-no-revoke.md \ ssl-reqd.md \ ssl-revoke-best-effort.md \ + ssl-sessions.md \ ssl.md \ sslv2.md \ sslv3.md \ diff --git a/docs/cmdline-opts/ssl-sessions.md b/docs/cmdline-opts/ssl-sessions.md new file mode 100644 index 0000000000..aefc20a102 --- /dev/null +++ b/docs/cmdline-opts/ssl-sessions.md @@ -0,0 +1,35 @@ +--- +c: Copyright (C) Daniel Stenberg, , et al. +SPDX-License-Identifier: curl +Long: ssl-sessions +Arg: +Protocols: TLS +Help: Load/save SSL session tickets from/to this file +Added: 8.12.0 +Category: tls +Multi: single +See-also: + - tls-earlydata +Example: + - --ssl-sessions sessions.txt $URL +--- + +# `--ssl-sessions` + +Use the given file to load SSL session tickets into curl's cache before +starting any transfers. At the end of a successful curl run, the cached +SSL sessions tickets are save to the file, replacing any previous content. + +The file does not have to exist, but curl reports an error if it is +unable to create it. Unused loaded tickets are saved again, unless they +get replaced or purged from the cache for space reasons. + +Using a session file allows `--tls-earlydata` to send the first request +in "0-RTT" mode, should an SSL session with the feature be found. Note that +a server may not support early data. Also note that early data does +not provide forward secrecy, e.g. is not as secure. + +The SSL session tickets are stored as base64 encoded text, each ticket on +its own line. The hostnames are cryptographically salted and hashed. While +this prevents someone to easily see the hosts you contacted, they could still +check if a specific hostname matches one of the values. diff --git a/docs/cmdline-opts/tls-earlydata.md b/docs/cmdline-opts/tls-earlydata.md index 8482f809ec..de815b4956 100644 --- a/docs/cmdline-opts/tls-earlydata.md +++ b/docs/cmdline-opts/tls-earlydata.md @@ -10,6 +10,7 @@ Multi: boolean See-also: - tlsv1.3 - tls-max + - ssl-sessions Example: - --tls-earlydata $URL --- diff --git a/docs/libcurl/Makefile.inc b/docs/libcurl/Makefile.inc index fe990cc1ec..0ae1a05389 100644 --- a/docs/libcurl/Makefile.inc +++ b/docs/libcurl/Makefile.inc @@ -41,6 +41,8 @@ man_MANS = \ curl_easy_reset.3 \ curl_easy_send.3 \ curl_easy_setopt.3 \ + curl_easy_ssls_import.3 \ + curl_easy_ssls_export.3 \ curl_easy_strerror.3 \ curl_easy_unescape.3 \ curl_easy_upkeep.3 \ diff --git a/docs/libcurl/curl_easy_ssls_export.md b/docs/libcurl/curl_easy_ssls_export.md new file mode 100644 index 0000000000..d0cb94391d --- /dev/null +++ b/docs/libcurl/curl_easy_ssls_export.md @@ -0,0 +1,172 @@ +--- +c: Copyright (C) Daniel Stenberg, , et al. +SPDX-License-Identifier: curl +Title: curl_easy_ssls_export +Section: 3 +Source: libcurl +See-also: + - CURLOPT_SHARE (3) + - curl_share_setopt (3) + - curl_easy_ssls_import (3) +Protocol: + - All +TLS-backend: + - GnuTLS + - OpenSSL + - BearSSL + - wolfSSL + - mbedTLS +Added-in: 8.12.0 +--- + +# NAME + +curl_easy_ssls_export - export SSL sessions + +# SYNOPSIS + +~~~c +#include + +typedef CURLcode curl_ssls_export_function(CURL *handle, + void *userptr, + const char *session_key, + const unsigned char *shmac, + size_t shmac_len, + const unsigned char *sdata, + size_t sdata_len, + curl_off_t valid_until, + int ietf_tls_id, + const char *alpn, + size_t earlydata_max); + +CURLcode curl_easy_ssls_export(CURL *handle, + curl_ssls_export_function *export_fn, + void *userptr); +~~~ + +# DESCRIPTION + +This function iterates over all SSL session tickets that belong to the +easy handle and invokes the **export_fn** callback on each of them, as +long as the callback returns **CURLE_OK**. + +The callback may then store this information and use curl_easy_ssls_import(3) +in another libcurl instance to add SSL session tickets again. Reuse of +SSL session tickets may result in faster handshakes and some connections +might be able to send request data in the initial packets (0-RTT). + +From all the parameters passed to the **export_fn** only two need to be +persisted: either **session_key** or **shamc** and always **sdata**. All +other parameters are informative, e.g. allow the callback to act only +on specific session tickets. + +Note that SSL sessions that involve a client certificate or SRP +username/password are not exported. + +# Export Function Parameter + +## Session Key + +This is a printable, 0-terminated string that starts with **hostname:port** +the session ticket is originating from and also contains all relevant +SSL parameters used in the connection. The key also carries the name +and version number of the TLS backend used. + +It is recommended to only persist **session_key** when it can be protected +from outside access. Since the hostname appears in plain text, it would +allow any third party to see how curl has been used for. + +## Salted Hash + +A binary blob of **shmac_len** bytes that contains a random salt and +a cryptographic hash of the salt and **session_key**. The salt is generated +for every session individually. Storing **shmac** is recommended when +placing session tickets in a file, for example. + +A third party may brute-force known hostnames, but cannot just "grep" for +them. + +## Session Data + +A binary blob of **sdata_len** bytes, **sdata** contains all relevant +SSL session ticket information for a later import - apart from **session_key** +and **shmac**. + +## valid_until + +Seconds since EPOCH (1970-01-01) until the session ticket is considered +valid. + +## TLS Version + +The IETF assigned number for the TLS version the session ticket originates +from. This is **0x0304** for TLSv1.3, **0x0303** for 1.2, etc. Session +tickets from version 1.3 have better security properties, so an export +might store only those. + +## ALPN + +The ALPN protocol that had been negotiated with the host. This may be +**NULL** if negotiation gave no result or had not been attempted. + +## Early Data + +The maximum amount of bytes the server supports to receive in early data +(0-RTT). This is 0 unless the server explicitly indicates support. + +# %PROTOCOLS% + +# EXAMPLE + +~~~c +CURLcode my_export_cb(CURL *handle, + void *userptr, + const char *session_key, + const unsigned char *shmac, + size_t shmac_len, + const unsigned char *sdata, + size_t sdata_len, + curl_off_t valid_until, + int ietf_tls_id, + const char *alpn, + size_t earlydata_max) +{ + /* persist sdata */ + return CURLE_OK; +} + +int main(void) +{ + CURLSHcode sh; + CURLSH *share = curl_share_init(); + CURLcode rc; + CURL *curl; + + sh = curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); + if(sh) + printf("Error: %s\n", curl_share_strerror(sh)); + + curl = curl_easy_init(); + if(curl) { + curl_easy_setopt(curl, CURLOPT_SHARE, share); + + rc = curl_easy_ssls_export(curl, my_export_cb, NULL); + + /* always cleanup */ + curl_easy_cleanup(curl); + } + curl_share_cleanup(share); +} +~~~ + +# %AVAILABILITY% + +# RETURN VALUE + +This function returns a CURLcode indicating success or error. + +CURLE_OK (0) means everything was OK, non-zero means an error occurred, see +libcurl-errors(3). If CURLOPT_ERRORBUFFER(3) was set with curl_easy_setopt(3) +there can be an error message stored in the error buffer when non-zero is +returned. diff --git a/docs/libcurl/curl_easy_ssls_import.md b/docs/libcurl/curl_easy_ssls_import.md new file mode 100644 index 0000000000..30d0b4df65 --- /dev/null +++ b/docs/libcurl/curl_easy_ssls_import.md @@ -0,0 +1,84 @@ +--- +c: Copyright (C) Daniel Stenberg, , et al. +SPDX-License-Identifier: curl +Title: curl_easy_ssls_import +Section: 3 +Source: libcurl +See-also: + - CURLOPT_SHARE (3) + - curl_share_setopt (3) + - curl_easy_ssls_export (3) +Protocol: + - All +Added-in: 8.12.0 +--- + +# NAME + +curl_easy_ssls_export - export SSL sessions + +# SYNOPSIS + +~~~c +#include + +CURLcode curl_easy_ssls_import(CURL *handle, + const char *session_key, + const unsigned char *shmac, size_t shmac_len, + const unsigned char *sdata, size_t sdata_len); +~~~ + +# DESCRIPTION + +This function imports a previously exported SSL session ticket. **sdata** and +**sdata_len** must always be provided. If **session_key** is **NULL**, then +**shmac** and **shmac_len** must be given as received during the export. +See curl_easy_ssls_export(3) for a description of those. + +Import of session tickets from other curl versions may fail due to changes +in the handling of **shmac** or **sdata**. A session ticket which has +already expired is silently discarded. + +# %PROTOCOLS% + +# EXAMPLE + +~~~c +int main(void) +{ + CURLSHcode sh; + CURLSH *share = curl_share_init(); + CURLcode rc; + CURL *curl; + + sh = curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); + if(sh) + printf("Error: %s\n", curl_share_strerror(sh)); + + curl = curl_easy_init(); + if(curl) { + unsigned char *shmac, *sdata; + size_t hlen, slen; + + curl_easy_setopt(curl, CURLOPT_SHARE, share); + + /* read shmac and sdata from storage */ + rc = curl_easy_ssls_import(curl, NULL, shmac, hlen, sdata, slen); + + /* always cleanup */ + curl_easy_cleanup(curl); + } + curl_share_cleanup(share); +} +~~~ + +# %AVAILABILITY% + +# RETURN VALUE + +This function returns a CURLcode indicating success or error. + +CURLE_OK (0) means everything was OK, non-zero means an error occurred, see +libcurl-errors(3). If CURLOPT_ERRORBUFFER(3) was set with curl_easy_setopt(3) +there can be an error message stored in the error buffer when non-zero is +returned. diff --git a/docs/libcurl/curl_global_trace.md b/docs/libcurl/curl_global_trace.md index bf722ae6f9..87edb8ecf0 100644 --- a/docs/libcurl/curl_global_trace.md +++ b/docs/libcurl/curl_global_trace.md @@ -105,6 +105,10 @@ Tracing of DNS-over-HTTP operations to resolve hostnames. Traces reading of upload data from the application in order to send it to the server. +## `ssls` + +Tracing of SSL Session handling, e.g. caching/import/export. + ## `smtp` Tracing of SMTP operations when this protocol is enabled in your build. diff --git a/docs/libcurl/curl_version_info.md b/docs/libcurl/curl_version_info.md index 271e935caf..a112058a6d 100644 --- a/docs/libcurl/curl_version_info.md +++ b/docs/libcurl/curl_version_info.md @@ -300,6 +300,13 @@ GSS-API Negotiation Mechanism, defined in RFC 2478.) (added in 7.10.8) supports SSL (HTTPS/FTPS) (Added in 7.10) +## SSLS-EXPORT + +*features* mask bit: non-existent + +libcurl was built with SSL session import/export support +(experimental, added in 8.12.0) + ## SSPI *features* mask bit: CURL_VERSION_SSPI diff --git a/docs/options-in-versions b/docs/options-in-versions index a7a11630d3..057cbafccd 100644 --- a/docs/options-in-versions +++ b/docs/options-in-versions @@ -235,6 +235,7 @@ --ssl-no-revoke 7.44.0 --ssl-reqd 7.20.0 --ssl-revoke-best-effort 7.70.0 +--ssl-sessions 8.12.0 --sslv2 (-2) 5.9 --sslv3 (-3) 5.9 --stderr 6.2 diff --git a/include/curl/curl.h b/include/curl/curl.h index 353a3b1926..84cf5f2f52 100644 --- a/include/curl/curl.h +++ b/include/curl/curl.h @@ -3232,6 +3232,50 @@ CURL_EXTERN CURLcode curl_easy_pause(CURL *handle, int bitmask); #define CURLPAUSE_ALL (CURLPAUSE_RECV|CURLPAUSE_SEND) #define CURLPAUSE_CONT (CURLPAUSE_RECV_CONT|CURLPAUSE_SEND_CONT) +/* + * NAME curl_easy_ssls_import() + * + * DESCRIPTION + * + * The curl_easy_ssls_import function adds a previously exported SSL session + * to the SSL session cache of the easy handle (or the underlying share). + */ +CURL_EXTERN CURLcode curl_easy_ssls_import(CURL *handle, + const char *session_key, + const unsigned char *shmac, + size_t shmac_len, + const unsigned char *sdata, + size_t sdata_len); + +/* This is the curl_ssls_export_cb callback prototype. It + * is passed to curl_easy_ssls_export() to extract SSL sessions/tickets. */ +typedef CURLcode curl_ssls_export_cb(CURL *handle, + void *userptr, + const char *session_key, + const unsigned char *shmac, + size_t shmac_len, + const unsigned char *sdata, + size_t sdata_len, + curl_off_t valid_until, + int ietf_tls_id, + const char *alpn, + size_t earlydata_max); + +/* + * NAME curl_easy_ssls_export() + * + * DESCRIPTION + * + * The curl_easy_ssls_export function iterates over all SSL sessions stored + * in the easy handle (or underlying share) and invokes the passed + * callback. + * + */ +CURL_EXTERN CURLcode curl_easy_ssls_export(CURL *handle, + curl_ssls_export_cb *export_fn, + void *userptr); + + #ifdef __cplusplus } /* end of extern "C" */ #endif diff --git a/lib/Makefile.inc b/lib/Makefile.inc index 12bf70e9e1..d2c7b6e888 100644 --- a/lib/Makefile.inc +++ b/lib/Makefile.inc @@ -57,6 +57,7 @@ LIB_VTLS_CFILES = \ vtls/sectransp.c \ vtls/vtls.c \ vtls/vtls_scache.c \ + vtls/vtls_spack.c \ vtls/wolfssl.c \ vtls/x509asn1.c @@ -76,6 +77,7 @@ LIB_VTLS_HFILES = \ vtls/vtls.h \ vtls/vtls_int.h \ vtls/vtls_scache.h \ + vtls/vtls_spack.h \ vtls/wolfssl.h \ vtls/x509asn1.h diff --git a/lib/curl_get_line.c b/lib/curl_get_line.c index 100207331d..7ee3b65af7 100644 --- a/lib/curl_get_line.c +++ b/lib/curl_get_line.c @@ -28,7 +28,9 @@ !defined(CURL_DISABLE_HSTS) || !defined(CURL_DISABLE_NETRC) #include "curl_get_line.h" +#ifdef BUILDING_LIBCURL #include "curl_memory.h" +#endif /* The last #include file should be: */ #include "memdebug.h" diff --git a/lib/curl_get_line.h b/lib/curl_get_line.h index 7907cde880..1e3b0f0357 100644 --- a/lib/curl_get_line.h +++ b/lib/curl_get_line.h @@ -26,6 +26,12 @@ #include "dynbuf.h" +#ifndef BUILDING_LIBCURL +/* this renames functions so that the tool code can use the same code + without getting symbol collisions */ +#define Curl_get_line(a,b) curlx_get_line(a,b) +#endif + /* Curl_get_line() returns complete lines that end with a newline. */ int Curl_get_line(struct dynbuf *buf, FILE *input); diff --git a/lib/curl_trc.c b/lib/curl_trc.c index 2776d0d119..dcb7813f6d 100644 --- a/lib/curl_trc.c +++ b/lib/curl_trc.c @@ -239,6 +239,24 @@ void Curl_trc_smtp(struct Curl_easy *data, const char *fmt, ...) } #endif /* !CURL_DISABLE_SMTP */ +#ifdef USE_SSL +struct curl_trc_feat Curl_trc_feat_ssls = { + "SSLS", + CURL_LOG_LVL_NONE, +}; + +void Curl_trc_ssls(struct Curl_easy *data, const char *fmt, ...) +{ + DEBUGASSERT(!strchr(fmt, '\n')); + if(Curl_trc_ft_is_verbose(data, &Curl_trc_feat_ssls)) { + va_list ap; + va_start(ap, fmt); + trc_infof(data, &Curl_trc_feat_ssls, fmt, ap); + va_end(ap); + } +} +#endif /* USE_SSL */ + #if !defined(CURL_DISABLE_WEBSOCKETS) && !defined(CURL_DISABLE_HTTP) struct curl_trc_feat Curl_trc_feat_ws = { "WS", @@ -279,6 +297,9 @@ static struct trc_feat_def trc_feats[] = { #ifndef CURL_DISABLE_SMTP { &Curl_trc_feat_smtp, TRC_CT_PROTOCOL }, #endif +#ifdef USE_SSL + { &Curl_trc_feat_ssls, TRC_CT_NETWORK }, +#endif #if !defined(CURL_DISABLE_WEBSOCKETS) && !defined(CURL_DISABLE_HTTP) { &Curl_trc_feat_ws, TRC_CT_PROTOCOL }, #endif diff --git a/lib/curl_trc.h b/lib/curl_trc.h index 67a7f4aa10..c796d671d0 100644 --- a/lib/curl_trc.h +++ b/lib/curl_trc.h @@ -94,6 +94,11 @@ void Curl_failf(struct Curl_easy *data, do { if(Curl_trc_ft_is_verbose(data, &Curl_trc_feat_smtp)) \ Curl_trc_smtp(data, __VA_ARGS__); } while(0) #endif /* !CURL_DISABLE_SMTP */ +#ifdef USE_SSL +#define CURL_TRC_SSLS(data, ...) \ + do { if(Curl_trc_ft_is_verbose(data, &Curl_trc_feat_ssls)) \ + Curl_trc_ssls(data, __VA_ARGS__); } while(0) +#endif /* USE_SSL */ #if !defined(CURL_DISABLE_WEBSOCKETS) && !defined(CURL_DISABLE_HTTP) #define CURL_TRC_WS(data, ...) \ do { if(Curl_trc_ft_is_verbose(data, &Curl_trc_feat_ws)) \ @@ -113,6 +118,9 @@ void Curl_failf(struct Curl_easy *data, #ifndef CURL_DISABLE_SMTP #define CURL_TRC_SMTP Curl_trc_smtp #endif +#ifdef USE_SSL +#define CURL_TRC_SSLS Curl_trc_ssls +#endif #if !defined(CURL_DISABLE_WEBSOCKETS) && !defined(CURL_DISABLE_HTTP) #define CURL_TRC_WS Curl_trc_ws #endif @@ -167,6 +175,11 @@ extern struct curl_trc_feat Curl_trc_feat_smtp; void Curl_trc_smtp(struct Curl_easy *data, const char *fmt, ...) CURL_PRINTF(2, 3); #endif +#ifdef USE_SSL +extern struct curl_trc_feat Curl_trc_feat_ssls; +void Curl_trc_ssls(struct Curl_easy *data, + const char *fmt, ...) CURL_PRINTF(2, 3); +#endif #if !defined(CURL_DISABLE_WEBSOCKETS) && !defined(CURL_DISABLE_HTTP) extern struct curl_trc_feat Curl_trc_feat_ws; void Curl_trc_ws(struct Curl_easy *data, diff --git a/lib/easy.c b/lib/easy.c index ac8fab3422..d1888b7fe4 100644 --- a/lib/easy.c +++ b/lib/easy.c @@ -48,6 +48,7 @@ #include #include "transfer.h" #include "vtls/vtls.h" +#include "vtls/vtls_scache.h" #include "url.h" #include "getinfo.h" #include "hostip.h" @@ -1336,3 +1337,41 @@ CURLcode curl_easy_upkeep(CURL *d) /* Use the common function to keep connections alive. */ return Curl_cpool_upkeep(data); } + +CURLcode curl_easy_ssls_import(CURL *d, const char *session_key, + const unsigned char *shmac, size_t shmac_len, + const unsigned char *sdata, size_t sdata_len) +{ +#ifdef USE_SSLS_EXPORT + struct Curl_easy *data = d; + if(!GOOD_EASY_HANDLE(data)) + return CURLE_BAD_FUNCTION_ARGUMENT; + return Curl_ssl_session_import(data, session_key, + shmac, shmac_len, sdata, sdata_len); +#else + (void)d; + (void)session_key; + (void)shmac; + (void)shmac_len; + (void)sdata; + (void)sdata_len; + return CURLE_NOT_BUILT_IN; +#endif +} + +CURLcode curl_easy_ssls_export(CURL *d, + curl_ssls_export_cb *export_fn, + void *userptr) +{ +#ifdef USE_SSLS_EXPORT + struct Curl_easy *data = d; + if(!GOOD_EASY_HANDLE(data)) + return CURLE_BAD_FUNCTION_ARGUMENT; + return Curl_ssl_session_export(data, export_fn, userptr); +#else + (void)d; + (void)export_fn; + (void)userptr; + return CURLE_NOT_BUILT_IN; +#endif +} diff --git a/lib/libcurl.def b/lib/libcurl.def index 9bf9fcc958..43e26f655c 100644 --- a/lib/libcurl.def +++ b/lib/libcurl.def @@ -15,6 +15,8 @@ curl_easy_recv curl_easy_reset curl_easy_send curl_easy_setopt +curl_easy_ssls_export +curl_easy_ssls_import curl_easy_strerror curl_easy_unescape curl_easy_upkeep diff --git a/lib/version.c b/lib/version.c index 6898b4255c..c5275495bc 100644 --- a/lib/version.c +++ b/lib/version.c @@ -523,6 +523,9 @@ static const struct feat features_table[] = { #ifdef USE_SSL FEATURE("SSL", NULL, CURL_VERSION_SSL), #endif +#if defined(USE_SSLS_EXPORT) + FEATURE("SSLS-EXPORT", NULL, 0), +#endif #ifdef USE_WINDOWS_SSPI FEATURE("SSPI", NULL, CURL_VERSION_SSPI), #endif diff --git a/lib/vquic/curl_ngtcp2.c b/lib/vquic/curl_ngtcp2.c index 79535d6e09..e891e75973 100644 --- a/lib/vquic/curl_ngtcp2.c +++ b/lib/vquic/curl_ngtcp2.c @@ -476,6 +476,10 @@ static int cf_ngtcp2_handshake_completed(ngtcp2_conn *tconn, void *user_data) data, &ctx->peer); CURL_TRC_CF(data, cf, "handshake complete after %dms", (int)Curl_timediff(ctx->handshake_at, ctx->started_at)); + /* In case of earlydata, where we simulate being connected, update + * the handshake time when we really did connect */ + if(ctx->use_earlydata) + Curl_pgrsTimeWas(data, TIMER_APPCONNECT, ctx->handshake_at); #ifdef USE_GNUTLS if(ctx->use_earlydata) { int flags = gnutls_session_get_flags(ctx->tls.gtls.session); diff --git a/lib/vtls/gtls.c b/lib/vtls/gtls.c index b87da2c82d..77b1d23493 100644 --- a/lib/vtls/gtls.c +++ b/lib/vtls/gtls.c @@ -751,10 +751,11 @@ CURLcode Curl_gtls_cache_session(struct Curl_cfilter *cf, /* extract session ID to the allocated buffer */ gnutls_session_get_data(session, sdata, &sdata_len); - - CURL_TRC_CF(data, cf, "get session id (len=%zu, alpn=%s) and store in cache", - sdata_len, alpn ? alpn : "-"); earlydata_max = gnutls_record_get_max_early_data_size(session); + + CURL_TRC_CF(data, cf, "get session id (len=%zu, alpn=%s, earlymax=%zu) " + "and store in cache", sdata_len, alpn ? alpn : "-", + earlydata_max); if(quic_tp && quic_tp_len) { qtp_clone = Curl_memdup0((char *)quic_tp, quic_tp_len); if(!qtp_clone) { diff --git a/lib/vtls/vtls.c b/lib/vtls/vtls.c index 02085f412a..1a7f362f86 100644 --- a/lib/vtls/vtls.c +++ b/lib/vtls/vtls.c @@ -667,7 +667,7 @@ CURLcode Curl_ssl_push_certinfo_len(struct Curl_easy *data, return result; } -/* get 32 bits of random */ +/* get length bytes of randomness */ CURLcode Curl_ssl_random(struct Curl_easy *data, unsigned char *entropy, size_t length) diff --git a/lib/vtls/vtls_scache.c b/lib/vtls/vtls_scache.c index 2c661a61c1..5f4ec4604b 100644 --- a/lib/vtls/vtls_scache.c +++ b/lib/vtls/vtls_scache.c @@ -22,22 +22,6 @@ * ***************************************************************************/ -/* This file is for implementing all "generic" SSL functions that all libcurl - internals should use. It is then responsible for calling the proper - "backend" function. - - SSL-functions in libcurl should call functions in this source file, and not - to any specific SSL-layer. - - Curl_ssl_ - prefix for generic ones - - Note that this source code uses the functions of the configured SSL - backend via the global Curl_ssl instance. - - "SSL/TLS Strong Encryption: An Introduction" - https://httpd.apache.org/docs/2.0/ssl/ssl_intro.html -*/ - #include "curl_setup.h" #ifdef USE_SSL @@ -58,6 +42,7 @@ #include "vtls.h" /* generic SSL protos etc */ #include "vtls_int.h" #include "vtls_scache.h" +#include "vtls_spack.h" #include "strcase.h" #include "url.h" @@ -65,6 +50,7 @@ #include "share.h" #include "curl_trc.h" #include "curl_sha256.h" +#include "rand.h" #include "warnless.h" #include "curl_printf.h" #include "strdup.h" @@ -215,18 +201,33 @@ static void cf_ssl_scache_peer_set_obj(struct Curl_ssl_scache_peer *peer, peer->sobj_free = sobj_free; } -static CURLcode cf_ssl_scache_peer_init(struct Curl_ssl_scache_peer *peer, - const char *ssl_peer_key, - const char *clientcert, - const char *srp_username, - const char *srp_password) +static CURLcode +cf_ssl_scache_peer_init(struct Curl_ssl_scache_peer *peer, + const char *ssl_peer_key, + const char *clientcert, + const char *srp_username, + const char *srp_password, + const unsigned char *salt, + const unsigned char *hmac) { CURLcode result = CURLE_OUT_OF_MEMORY; DEBUGASSERT(!peer->ssl_peer_key); - peer->ssl_peer_key = strdup(ssl_peer_key); - if(!peer->ssl_peer_key) + if(ssl_peer_key) { + peer->ssl_peer_key = strdup(ssl_peer_key); + if(!peer->ssl_peer_key) + goto out; + peer->hmac_set = FALSE; + } + else if(salt && hmac) { + memcpy(peer->key_salt, salt, sizeof(peer->key_salt)); + memcpy(peer->key_hmac, hmac, sizeof(peer->key_hmac)); + peer->hmac_set = TRUE; + } + else { + result = CURLE_BAD_FUNCTION_ARGUMENT; goto out; + } if(clientcert) { peer->clientcert = strdup(clientcert); if(!peer->clientcert) @@ -557,7 +558,16 @@ out: static bool cf_ssl_scache_match_auth(struct Curl_ssl_scache_peer *peer, struct ssl_primary_config *conn_config) { - if(!Curl_safecmp(peer->clientcert, conn_config->clientcert)) + if(!conn_config) { + if(peer->clientcert) + return FALSE; +#ifdef USE_TLS_SRP + if(peer->srp_username || peer->srp_password) + return FALSE; +#endif + return TRUE; + } + else if(!Curl_safecmp(peer->clientcert, conn_config->clientcert)) return FALSE; #ifdef USE_TLS_SRP if(Curl_timestrcmp(peer->srp_username, conn_config->username) || @@ -567,21 +577,17 @@ static bool cf_ssl_scache_match_auth(struct Curl_ssl_scache_peer *peer, return TRUE; } -static CURLcode cf_ssl_find_peer(struct Curl_cfilter *cf, - struct Curl_easy *data, - struct Curl_ssl_scache *scache, - const char *ssl_peer_key, - struct Curl_ssl_scache_peer **ppeer) +static CURLcode +cf_ssl_find_peer_by_key(struct Curl_easy *data, + struct Curl_ssl_scache *scache, + const char *ssl_peer_key, + struct ssl_primary_config *conn_config, + struct Curl_ssl_scache_peer **ppeer) { - 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); size_t i, peer_key_len = 0; CURLcode result = CURLE_OK; *ppeer = NULL; - if(!ssl_config || !ssl_config->primary.cache_session) - goto out; - /* check for entries with known peer_key */ for(i = 0; scache && i < scache->peer_count; i++) { if(scache->peers[i].ssl_peer_key && @@ -611,6 +617,8 @@ static CURLcode cf_ssl_find_peer(struct Curl_cfilter *cf, goto out; if(!memcmp(scache->peers[i].key_hmac, my_hmac, sizeof(my_hmac))) { /* remember peer_key for future lookups */ + CURL_TRC_SSLS(data, "peer entry %zu key recovered: %s", + i, ssl_peer_key); scache->peers[i].ssl_peer_key = strdup(ssl_peer_key); if(!scache->peers[i].ssl_peer_key) { result = CURLE_OUT_OF_MEMORY; @@ -621,34 +629,18 @@ static CURLcode cf_ssl_find_peer(struct Curl_cfilter *cf, } } } + CURL_TRC_SSLS(data, "peer not found for %s", ssl_peer_key); out: - if(result) - CURL_TRC_CF(data, cf, "[SACHE] failure finding scache peer: %d", result); return result; } -static CURLcode cf_ssl_add_peer(struct Curl_cfilter *cf, - struct Curl_easy *data, - struct Curl_ssl_scache *scache, - const char *ssl_peer_key, - struct Curl_ssl_scache_peer **ppeer) +static struct Curl_ssl_scache_peer * +cf_ssl_get_free_peer(struct Curl_ssl_scache *scache) { - struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf); struct Curl_ssl_scache_peer *peer = NULL; size_t i; - CURLcode result; - *ppeer = NULL; - result = cf_ssl_find_peer(cf, data, scache, ssl_peer_key, &peer); - if(result || !scache->peer_count) - return result; - - if(peer) { - *ppeer = peer; - return CURLE_OK; - } - - /* not there, find empty or oldest peer */ + /* find empty or oldest peer */ for(i = 0; i < scache->peer_count; ++i) { /* free peer entry? */ if(!scache->peers[i].ssl_peer_key && !scache->peers[i].hmac_set) { @@ -667,39 +659,86 @@ static CURLcode cf_ssl_add_peer(struct Curl_cfilter *cf, } } DEBUGASSERT(peer); - if(!peer) + if(peer) + cf_ssl_scache_clear_peer(peer); + return peer; +} + +static CURLcode +cf_ssl_add_peer(struct Curl_easy *data, + struct Curl_ssl_scache *scache, + const char *ssl_peer_key, + struct ssl_primary_config *conn_config, + struct Curl_ssl_scache_peer **ppeer) +{ + struct Curl_ssl_scache_peer *peer = NULL; + CURLcode result = CURLE_OK; + + *ppeer = NULL; + if(ssl_peer_key) { + result = cf_ssl_find_peer_by_key(data, scache, ssl_peer_key, conn_config, + &peer); + if(result || !scache->peer_count) + return result; + } + + if(peer) { + *ppeer = peer; return CURLE_OK; - /* clear previous peer and reinit */ - cf_ssl_scache_clear_peer(peer); - result = cf_ssl_scache_peer_init(peer, ssl_peer_key, - conn_config->clientcert, + } + + peer = cf_ssl_get_free_peer(scache); + if(peer) { + const char *ccert = conn_config ? conn_config->clientcert : NULL; + const char *username = NULL, *password = NULL; #ifdef USE_TLS_SRP - conn_config->username, - conn_config->password); -#else - NULL, NULL); + username = conn_config ? conn_config->username : NULL; + password = conn_config ? conn_config->password : NULL; #endif - if(result) - goto out; - /* all ready */ - *ppeer = peer; - result = CURLE_OK; + result = cf_ssl_scache_peer_init(peer, ssl_peer_key, ccert, + username, password, NULL, NULL); + if(result) + goto out; + /* all ready */ + *ppeer = peer; + result = CURLE_OK; + } out: if(result) { cf_ssl_scache_clear_peer(peer); - CURL_TRC_CF(data, cf, "[SACHE] failure adding peer: %d", result); } return result; } -static CURLcode cf_scache_peer_add_session(struct Curl_cfilter *cf, - struct Curl_easy *data, - struct Curl_ssl_scache *scache, - const char *ssl_peer_key, - struct Curl_ssl_session *s) +static void cf_scache_peer_add_session(struct Curl_ssl_scache_peer *peer, + struct Curl_ssl_session *s, + curl_off_t now) +{ + /* A session not from TLSv1.3 replaces all other. */ + if(s->ietf_tls_id != CURL_IETF_PROTO_TLS1_3) { + Curl_llist_destroy(&peer->sessions, NULL); + Curl_llist_append(&peer->sessions, s, &s->list); + } + else { + /* Expire existing, append, trim from head to obey max_sessions */ + cf_scache_peer_remove_expired(peer, now); + cf_scache_peer_remove_non13(peer); + Curl_llist_append(&peer->sessions, s, &s->list); + while(Curl_llist_count(&peer->sessions) > peer->max_sessions) { + Curl_node_remove(Curl_llist_head(&peer->sessions)); + } + } +} + +static CURLcode cf_scache_add_session(struct Curl_cfilter *cf, + struct Curl_easy *data, + struct Curl_ssl_scache *scache, + const char *ssl_peer_key, + struct Curl_ssl_session *s) { struct Curl_ssl_scache_peer *peer = NULL; + struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf); CURLcode result = CURLE_OUT_OF_MEMORY; curl_off_t now = (curl_off_t)time(NULL); curl_off_t max_lifetime; @@ -719,32 +758,19 @@ static CURLcode cf_scache_peer_add_session(struct Curl_cfilter *cf, s->valid_until = now + max_lifetime; if(cf_scache_session_expired(s, now)) { - CURL_TRC_CF(data, cf, "[SCACHE] add, session already expired"); + CURL_TRC_SSLS(data, "add, session already expired"); Curl_ssl_session_destroy(s); return CURLE_OK; } - result = cf_ssl_add_peer(cf, data, scache, ssl_peer_key, &peer); + result = cf_ssl_add_peer(data, scache, ssl_peer_key, conn_config, &peer); if(result || !peer) { - CURL_TRC_CF(data, cf, "[SCACHE] unable to add scache peer: %d", result); + CURL_TRC_SSLS(data, "unable to add scache peer: %d", result); Curl_ssl_session_destroy(s); goto out; } - /* A session not from TLSv1.3 replaces all other. */ - if(s->ietf_tls_id != CURL_IETF_PROTO_TLS1_3) { - Curl_llist_destroy(&peer->sessions, NULL); - Curl_llist_append(&peer->sessions, s, &s->list); - } - else { - /* Expire existing, append, trim from head to obey max_sessions */ - cf_scache_peer_remove_expired(peer, now); - cf_scache_peer_remove_non13(peer); - Curl_llist_append(&peer->sessions, s, &s->list); - while(Curl_llist_count(&peer->sessions) > peer->max_sessions) { - Curl_node_remove(Curl_llist_head(&peer->sessions)); - } - } + cf_scache_peer_add_session(peer, s, now); out: if(result) { @@ -752,12 +778,12 @@ out: ssl_peer_key, result); } else - CURL_TRC_CF(data, cf, "[SCACHE] added session for %s [proto=0x%x, " - "valid_secs=%" FMT_OFF_T ", alpn=%s, earlydata=%zu, " - "quic_tp=%s], peer has %zu sessions now", - ssl_peer_key, s->ietf_tls_id, s->valid_until - now, s->alpn, - s->earlydata_max, s->quic_tp ? "yes" : "no", - Curl_llist_count(&peer->sessions)); + CURL_TRC_SSLS(data, "added session for %s [proto=0x%x, " + "valid_secs=%" FMT_OFF_T ", alpn=%s, earlydata=%zu, " + "quic_tp=%s], peer has %zu sessions now", + ssl_peer_key, s->ietf_tls_id, s->valid_until - now, + s->alpn, s->earlydata_max, s->quic_tp ? "yes" : "no", + Curl_llist_count(&peer->sessions)); return result; } @@ -767,10 +793,16 @@ CURLcode Curl_ssl_scache_put(struct Curl_cfilter *cf, struct Curl_ssl_session *s) { struct Curl_ssl_scache *scache = data->state.ssl_scache; + struct ssl_config_data *ssl_config = Curl_ssl_cf_get_config(cf, data); CURLcode result; + if(!ssl_config || !scache || !ssl_config->primary.cache_session) { + Curl_ssl_session_destroy(s); + return CURLE_OK; + } + Curl_ssl_scache_lock(data); - result = cf_scache_peer_add_session(cf, data, scache, ssl_peer_key, s); + result = cf_scache_add_session(cf, data, scache, ssl_peer_key, s); Curl_ssl_scache_unlock(data); return result; } @@ -794,6 +826,7 @@ CURLcode Curl_ssl_scache_take(struct Curl_cfilter *cf, struct Curl_ssl_session **ps) { struct Curl_ssl_scache *scache = data->state.ssl_scache; + struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf); struct Curl_ssl_scache_peer *peer = NULL; struct Curl_llist_node *n; struct Curl_ssl_session *s = NULL; @@ -804,7 +837,8 @@ CURLcode Curl_ssl_scache_take(struct Curl_cfilter *cf, return CURLE_OK; Curl_ssl_scache_lock(data); - result = cf_ssl_find_peer(cf, data, scache, ssl_peer_key, &peer); + result = cf_ssl_find_peer_by_key(data, scache, ssl_peer_key, conn_config, + &peer); if(!result && peer) { cf_scache_peer_remove_expired(peer, (curl_off_t)time(NULL)); n = Curl_llist_head(&peer->sessions); @@ -817,14 +851,14 @@ CURLcode Curl_ssl_scache_take(struct Curl_cfilter *cf, Curl_ssl_scache_unlock(data); if(s) { *ps = s; - CURL_TRC_CF(data, cf, "[SCACHE] took session for %s [proto=0x%x, " - "alpn=%s, earlydata=%zu, quic_tp=%s], %zu sessions remain", - ssl_peer_key, s->ietf_tls_id, s->alpn, - s->earlydata_max, s->quic_tp ? "yes" : "no", - Curl_llist_count(&peer->sessions)); + CURL_TRC_SSLS(data, "took session for %s [proto=0x%x, " + "alpn=%s, earlydata=%zu, quic_tp=%s], %zu sessions remain", + ssl_peer_key, s->ietf_tls_id, 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); + CURL_TRC_SSLS(data, "no cached session for %s", ssl_peer_key); } return result; } @@ -836,15 +870,16 @@ CURLcode Curl_ssl_scache_add_obj(struct Curl_cfilter *cf, Curl_ssl_scache_obj_dtor *sobj_free) { struct Curl_ssl_scache *scache = data->state.ssl_scache; + struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf); struct Curl_ssl_scache_peer *peer = NULL; CURLcode result; DEBUGASSERT(sobj); DEBUGASSERT(sobj_free); - result = cf_ssl_add_peer(cf, data, scache, ssl_peer_key, &peer); + result = cf_ssl_add_peer(data, scache, ssl_peer_key, conn_config, &peer); if(result || !peer) { - CURL_TRC_CF(data, cf, "[SCACHE] unable to add scache peer: %d", result); + CURL_TRC_SSLS(data, "unable to add scache peer: %d", result); goto out; } @@ -863,6 +898,7 @@ bool Curl_ssl_scache_get_obj(struct Curl_cfilter *cf, void **sobj) { struct Curl_ssl_scache *scache = data->state.ssl_scache; + struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf); struct Curl_ssl_scache_peer *peer = NULL; CURLcode result; @@ -870,15 +906,16 @@ bool Curl_ssl_scache_get_obj(struct Curl_cfilter *cf, if(!scache) return FALSE; - result = cf_ssl_find_peer(cf, data, scache, ssl_peer_key, &peer); + result = cf_ssl_find_peer_by_key(data, scache, ssl_peer_key, conn_config, + &peer); if(result) return FALSE; if(peer) *sobj = peer->sobj; - CURL_TRC_CF(data, cf, "[SACHE] %s cached session for '%s'", - *sobj ? "Found" : "No", ssl_peer_key); + CURL_TRC_SSLS(data, "%s cached session for '%s'", + *sobj ? "Found" : "No", ssl_peer_key); return !!*sobj; } @@ -887,6 +924,7 @@ void Curl_ssl_scache_remove_all(struct Curl_cfilter *cf, const char *ssl_peer_key) { struct Curl_ssl_scache *scache = data->state.ssl_scache; + struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf); struct Curl_ssl_scache_peer *peer = NULL; CURLcode result; @@ -895,10 +933,237 @@ void Curl_ssl_scache_remove_all(struct Curl_cfilter *cf, return; Curl_ssl_scache_lock(data); - result = cf_ssl_find_peer(cf, data, scache, ssl_peer_key, &peer); + result = cf_ssl_find_peer_by_key(data, scache, ssl_peer_key, conn_config, + &peer); if(!result && peer) cf_ssl_scache_clear_peer(peer); Curl_ssl_scache_unlock(data); } +#ifdef USE_SSLS_EXPORT + +#define CURL_SSL_TICKET_MAX (16*1024) + +static CURLcode cf_ssl_scache_peer_set_hmac(struct Curl_ssl_scache_peer *peer) +{ + CURLcode result; + + DEBUGASSERT(peer); + if(!peer->ssl_peer_key) + return CURLE_BAD_FUNCTION_ARGUMENT; + + result = Curl_rand(NULL, peer->key_salt, sizeof(peer->key_salt)); + if(result) + return result; + + result = Curl_hmacit(&Curl_HMAC_SHA256, + peer->key_salt, sizeof(peer->key_salt), + (const unsigned char *)peer->ssl_peer_key, + strlen(peer->ssl_peer_key), + peer->key_hmac); + if(!result) + peer->hmac_set = TRUE; + return result; +} + +static CURLcode +cf_ssl_find_peer_by_hmac(struct Curl_ssl_scache *scache, + const unsigned char *salt, + const unsigned char *hmac, + struct Curl_ssl_scache_peer **ppeer) +{ + size_t i; + CURLcode result = CURLE_OK; + + *ppeer = NULL; + /* look for an entry that matches salt+hmac exactly or has a known + * ssl_peer_key which salt+hmac's to the same. */ + for(i = 0; scache && i < scache->peer_count; i++) { + struct Curl_ssl_scache_peer *peer = &scache->peers[i]; + if(!cf_ssl_scache_match_auth(peer, NULL)) + continue; + if(scache->peers[i].hmac_set && + !memcmp(peer->key_salt, salt, sizeof(peer->key_salt)) && + !memcmp(peer->key_hmac, hmac, sizeof(peer->key_hmac))) { + /* found exact match, return */ + *ppeer = peer; + goto out; + } + else if(peer->ssl_peer_key) { + unsigned char my_hmac[CURL_SHA256_DIGEST_LENGTH]; + /* compute hmac for the passed salt */ + result = Curl_hmacit(&Curl_HMAC_SHA256, + salt, sizeof(peer->key_salt), + (const unsigned char *)peer->ssl_peer_key, + strlen(peer->ssl_peer_key), + my_hmac); + if(result) + goto out; + if(!memcmp(my_hmac, hmac, sizeof(my_hmac))) { + /* cryptohash match, take over salt+hmac if no set and return */ + if(!peer->hmac_set) { + memcpy(peer->key_salt, salt, sizeof(peer->key_salt)); + memcpy(peer->key_hmac, hmac, sizeof(peer->key_hmac)); + peer->hmac_set = TRUE; + } + *ppeer = peer; + goto out; + } + } + } +out: + return result; +} + +CURLcode Curl_ssl_session_import(struct Curl_easy *data, + const char *ssl_peer_key, + const unsigned char *shmac, size_t shmac_len, + const unsigned char *sdata, size_t sdata_len) +{ + struct Curl_ssl_scache *scache = data->state.ssl_scache; + struct Curl_ssl_scache_peer *peer = NULL; + struct Curl_ssl_session *s = NULL; + bool locked = FALSE; + CURLcode r; + + if(!scache) { + r = CURLE_BAD_FUNCTION_ARGUMENT; + goto out; + } + if(!ssl_peer_key && (!shmac || !shmac_len)) { + r = CURLE_BAD_FUNCTION_ARGUMENT; + goto out; + } + + r = Curl_ssl_session_unpack(data, sdata, sdata_len, &s); + if(r) + goto out; + + Curl_ssl_scache_lock(data); + locked = TRUE; + + if(ssl_peer_key) { + r = cf_ssl_add_peer(data, scache, ssl_peer_key, NULL, &peer); + if(r) + goto out; + } + else if(shmac_len != (sizeof(peer->key_salt) + sizeof(peer->key_hmac))) { + /* Either salt+hmac was garbled by caller or is from a curl version + * that does things differently */ + r = CURLE_BAD_FUNCTION_ARGUMENT; + goto out; + } + else { + const unsigned char *salt = shmac; + const unsigned char *hmac = shmac + sizeof(peer->key_salt); + + r = cf_ssl_find_peer_by_hmac(scache, salt, hmac, &peer); + if(r) + goto out; + if(!peer) { + peer = cf_ssl_get_free_peer(scache); + if(peer) { + r = cf_ssl_scache_peer_init(peer, ssl_peer_key, NULL, + NULL, NULL, salt, hmac); + if(r) + goto out; + } + } + } + + if(peer) { + cf_scache_peer_add_session(peer, s, time(NULL)); + s = NULL; /* peer is now owner */ + CURL_TRC_SSLS(data, "successfully imported ticket for peer %s, now " + "with %zu tickets", + peer->ssl_peer_key ? peer->ssl_peer_key : "without key", + Curl_llist_count(&peer->sessions)); + } + +out: + if(locked) + Curl_ssl_scache_unlock(data); + Curl_ssl_session_destroy(s); + return r; +} + +CURLcode Curl_ssl_session_export(struct Curl_easy *data, + curl_ssls_export_cb *export_fn, + void *userptr) +{ + struct Curl_ssl_scache *scache = data->state.ssl_scache; + struct Curl_ssl_scache_peer *peer; + struct dynbuf sbuf, hbuf; + struct Curl_llist_node *n; + size_t i, npeers = 0, ntickets = 0; + curl_off_t now = time(NULL); + CURLcode r = CURLE_OK; + + if(!export_fn) + return CURLE_BAD_FUNCTION_ARGUMENT; + if(!scache) + return CURLE_OK; + + Curl_ssl_scache_lock(data); + + Curl_dyn_init(&hbuf, (CURL_SHA256_DIGEST_LENGTH * 2) + 1); + Curl_dyn_init(&sbuf, CURL_SSL_TICKET_MAX); + + for(i = 0; scache && i < scache->peer_count; i++) { + peer = &scache->peers[i]; + if(!peer->ssl_peer_key && !peer->hmac_set) + continue; /* skip free entry */ + if(peer->clientcert || peer->srp_username || peer->srp_password) + continue; /* not exporting those */ + + Curl_dyn_reset(&hbuf); + cf_scache_peer_remove_expired(peer, now); + n = Curl_llist_head(&peer->sessions); + if(n) + ++npeers; + while(n) { + struct Curl_ssl_session *s = Curl_node_elem(n); + if(!peer->hmac_set) { + r = cf_ssl_scache_peer_set_hmac(peer); + if(r) + goto out; + } + if(!Curl_dyn_len(&hbuf)) { + r = Curl_dyn_addn(&hbuf, peer->key_salt, sizeof(peer->key_salt)); + if(r) + goto out; + r = Curl_dyn_addn(&hbuf, peer->key_hmac, sizeof(peer->key_hmac)); + if(r) + goto out; + } + Curl_dyn_reset(&sbuf); + r = Curl_ssl_session_pack(data, s, &sbuf); + if(r) + goto out; + + r = export_fn(data, userptr, peer->ssl_peer_key, + Curl_dyn_uptr(&hbuf), Curl_dyn_len(&hbuf), + Curl_dyn_uptr(&sbuf), Curl_dyn_len(&sbuf), + s->valid_until, s->ietf_tls_id, + s->alpn, s->earlydata_max); + if(r) + goto out; + ++ntickets; + n = Curl_node_next(n); + } + + } + r = CURLE_OK; + CURL_TRC_SSLS(data, "exported %zu session tickets for %zu peers", + ntickets, npeers); + +out: + Curl_ssl_scache_unlock(data); + Curl_dyn_free(&hbuf); + Curl_dyn_free(&sbuf); + return r; +} + +#endif /* USE_SSLS_EXPORT */ + #endif /* USE_SSL */ diff --git a/lib/vtls/vtls_scache.h b/lib/vtls/vtls_scache.h index 14b011241f..a81f57e393 100644 --- a/lib/vtls/vtls_scache.h +++ b/lib/vtls/vtls_scache.h @@ -195,6 +195,18 @@ void Curl_ssl_scache_remove_all(struct Curl_cfilter *cf, struct Curl_easy *data, const char *ssl_peer_key); +#ifdef USE_SSLS_EXPORT + +CURLcode Curl_ssl_session_import(struct Curl_easy *data, + const char *ssl_peer_key, + const unsigned char *shmac, size_t shmac_len, + const unsigned char *sdata, size_t sdata_len); + +CURLcode Curl_ssl_session_export(struct Curl_easy *data, + curl_ssls_export_cb *export_fn, + void *userptr); + +#endif /* USE_SSLS_EXPORT */ #else /* USE_SSL */ diff --git a/lib/vtls/vtls_spack.c b/lib/vtls/vtls_spack.c new file mode 100644 index 0000000000..6dec8e567b --- /dev/null +++ b/lib/vtls/vtls_spack.c @@ -0,0 +1,345 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + * SPDX-License-Identifier: curl + * + ***************************************************************************/ + +#include "curl_setup.h" + +#ifdef USE_SSLS_EXPORT + +#include "urldata.h" +#include "curl_trc.h" +#include "vtls_scache.h" +#include "vtls_spack.h" +#include "strdup.h" + +/* The last #include files should be: */ +#include "curl_memory.h" +#include "memdebug.h" + +#ifdef _MSC_VER +#if _MSC_VER >= 1600 +#include +#else +typedef unsigned char uint8_t; +typedef unsigned __int16 uint16_t; +typedef unsigned __int32 uint32_t; +typedef unsigned __int64 uint64_t; +#endif +#endif /* _MSC_VER */ + +#ifndef UINT16_MAX +#define UINT16_MAX 0xffff +#endif +#ifndef UINT32_MAX +#define UINT32_MAX 0xffffffff +#endif + +#define CURL_SPACK_VERSION 0x01 +#define CURL_SPACK_IETF_ID 0x02 +#define CURL_SPACK_VALID_UNTIL 0x03 +#define CURL_SPACK_TICKET 0x04 +#define CURL_SPACK_ALPN 0x05 +#define CURL_SPACK_EARLYDATA 0x06 +#define CURL_SPACK_QUICTP 0x07 + +static CURLcode spack_enc8(struct dynbuf *buf, uint8_t b) +{ + return Curl_dyn_addn(buf, &b, 1); +} + +static CURLcode +spack_dec8(uint8_t *val, const uint8_t **src, const uint8_t *end) +{ + if(end - *src < 1) + return CURLE_READ_ERROR; + *val = **src; + *src += 1; + return CURLE_OK; +} + +static CURLcode spack_enc16(struct dynbuf *buf, uint16_t val) +{ + uint8_t nval[2]; + nval[0] = (uint8_t)(val >> 8); + nval[1] = (uint8_t)val; + return Curl_dyn_addn(buf, nval, sizeof(nval)); +} + +static CURLcode +spack_dec16(uint16_t *val, const uint8_t **src, const uint8_t *end) +{ + if(end - *src < 2) + return CURLE_READ_ERROR; + *val = (uint16_t)((*src)[0] << 8 | (*src)[1]); + *src += 2; + return CURLE_OK; +} + +static CURLcode spack_enc32(struct dynbuf *buf, uint32_t val) +{ + uint8_t nval[4]; + nval[0] = (uint8_t)(val >> 24); + nval[1] = (uint8_t)(val >> 16); + nval[2] = (uint8_t)(val >> 8); + nval[3] = (uint8_t)val; + return Curl_dyn_addn(buf, nval, sizeof(nval)); +} + +static CURLcode +spack_dec32(uint32_t *val, const uint8_t **src, const uint8_t *end) +{ + if(end - *src < 4) + return CURLE_READ_ERROR; + *val = (uint32_t)(*src)[0] << 24 | (uint32_t)(*src)[1] << 16 | + (uint32_t)(*src)[2] << 8 | (*src)[3]; + *src += 4; + return CURLE_OK; +} + +static CURLcode spack_enc64(struct dynbuf *buf, uint64_t val) +{ + uint8_t nval[8]; + nval[0] = (uint8_t)(val >> 56); + nval[1] = (uint8_t)(val >> 48); + nval[2] = (uint8_t)(val >> 40); + nval[3] = (uint8_t)(val >> 32); \ + nval[4] = (uint8_t)(val >> 24); + nval[5] = (uint8_t)(val >> 16); + nval[6] = (uint8_t)(val >> 8); + nval[7] = (uint8_t)val; + return Curl_dyn_addn(buf, nval, sizeof(nval)); +} + +static CURLcode +spack_dec64(uint64_t *val, const uint8_t **src, const uint8_t *end) +{ + if(end - *src < 8) + return CURLE_READ_ERROR; + *val = (uint64_t)(*src)[0] << 56 | (uint64_t)(*src)[1] << 48 | + (uint64_t)(*src)[2] << 40 | (uint64_t)(*src)[3] << 32 | + (uint64_t)(*src)[4] << 24 | (uint64_t)(*src)[5] << 16 | + (uint64_t)(*src)[6] << 8 | (*src)[7]; + *src += 8; + return CURLE_OK; +} + +static CURLcode spack_encstr16(struct dynbuf *buf, const char *s) +{ + size_t slen = strlen(s); + CURLcode r; + if(slen > UINT16_MAX) + return CURLE_BAD_FUNCTION_ARGUMENT; + r = spack_enc16(buf, (uint16_t)slen); + if(!r) { + r = Curl_dyn_addn(buf, s, slen); + } + return r; +} + +static CURLcode +spack_decstr16(char **val, const uint8_t **src, const uint8_t *end) +{ + uint16_t slen; + CURLcode r; + + *val = NULL; + r = spack_dec16(&slen, src, end); + if(r) + return r; + if(end - *src < slen) + return CURLE_READ_ERROR; + *val = Curl_memdup0((const char *)(*src), slen); + *src += slen; + return *val ? CURLE_OK : CURLE_OUT_OF_MEMORY; +} + +static CURLcode spack_encdata16(struct dynbuf *buf, + const uint8_t *data, size_t data_len) +{ + CURLcode r; + if(data_len > UINT16_MAX) + return CURLE_BAD_FUNCTION_ARGUMENT; + r = spack_enc16(buf, (uint16_t)data_len); + if(!r) { + r = Curl_dyn_addn(buf, data, data_len); + } + return r; +} + +static CURLcode +spack_decdata16(uint8_t **val, size_t *val_len, + const uint8_t **src, const uint8_t *end) +{ + uint16_t data_len; + CURLcode r; + + *val = NULL; + r = spack_dec16(&data_len, src, end); + if(r) + return r; + if(end - *src < data_len) + return CURLE_READ_ERROR; + *val = Curl_memdup0((const char *)(*src), data_len); + *val_len = data_len; + *src += data_len; + return *val ? CURLE_OK : CURLE_OUT_OF_MEMORY; +} + +CURLcode Curl_ssl_session_pack(struct Curl_easy *data, + struct Curl_ssl_session *s, + struct dynbuf *buf) +{ + CURLcode r; + DEBUGASSERT(s->sdata); + DEBUGASSERT(s->sdata_len); + + if(s->valid_until < 0) + return CURLE_BAD_FUNCTION_ARGUMENT; + + r = spack_enc8(buf, CURL_SPACK_VERSION); + if(!r) + r = spack_enc8(buf, CURL_SPACK_TICKET); + if(!r) + r = spack_encdata16(buf, s->sdata, s->sdata_len); + if(!r) + r = spack_enc8(buf, CURL_SPACK_IETF_ID); + if(!r) + r = spack_enc16(buf, (uint16_t)s->ietf_tls_id); + if(!r) + r = spack_enc8(buf, CURL_SPACK_VALID_UNTIL); + if(!r) + r = spack_enc64(buf, (uint64_t)s->valid_until); + if(!r && s->alpn) { + r = spack_enc8(buf, CURL_SPACK_ALPN); + if(!r) + r = spack_encstr16(buf, s->alpn); + } + if(!r && s->earlydata_max) { + if(s->earlydata_max > UINT32_MAX) + r = CURLE_BAD_FUNCTION_ARGUMENT; + if(!r) + r = spack_enc8(buf, CURL_SPACK_EARLYDATA); + if(!r) + r = spack_enc32(buf, (uint32_t)s->earlydata_max); + } + if(!r && s->quic_tp && s->quic_tp_len) { + r = spack_enc8(buf, CURL_SPACK_QUICTP); + if(!r) + r = spack_encdata16(buf, s->quic_tp, s->quic_tp_len); + } + + if(r) + CURL_TRC_SSLS(data, "error packing data: %d", r); + return r; +} + +CURLcode Curl_ssl_session_unpack(struct Curl_easy *data, + const unsigned char *buf, size_t buflen, + struct Curl_ssl_session **ps) +{ + struct Curl_ssl_session *s = NULL; + const unsigned char *end = buf + buflen; + uint8_t val8, *pval8; + uint16_t val16; + uint32_t val32; + uint64_t val64; + CURLcode r; + + DEBUGASSERT(buf); + DEBUGASSERT(buflen); + *ps = NULL; + + r = spack_dec8(&val8, &buf, end); + if(r) + goto out; + if(val8 != CURL_SPACK_VERSION) { + r = CURLE_READ_ERROR; + goto out; + } + + s = calloc(1, sizeof(*s)); + if(!s) { + r = CURLE_OUT_OF_MEMORY; + goto out; + } + + while(buf < end) { + r = spack_dec8(&val8, &buf, end); + if(r) + goto out; + + switch(val8) { + case CURL_SPACK_ALPN: + r = spack_decstr16(&s->alpn, &buf, end); + if(r) + goto out; + break; + case CURL_SPACK_EARLYDATA: + r = spack_dec32(&val32, &buf, end); + if(r) + goto out; + s->earlydata_max = val32; + break; + case CURL_SPACK_IETF_ID: + r = spack_dec16(&val16, &buf, end); + if(r) + goto out; + s->ietf_tls_id = val16; + break; + case CURL_SPACK_QUICTP: { + r = spack_decdata16(&pval8, &s->quic_tp_len, &buf, end); + if(r) + goto out; + s->quic_tp = pval8; + break; + } + case CURL_SPACK_TICKET: { + r = spack_decdata16(&pval8, &s->sdata_len, &buf, end); + if(r) + goto out; + s->sdata = pval8; + break; + } + case CURL_SPACK_VALID_UNTIL: + r = spack_dec64(&val64, &buf, end); + if(r) + goto out; + s->valid_until = (curl_off_t)val64; + break; + default: /* unknown tag */ + r = CURLE_READ_ERROR; + goto out; + } + } + +out: + if(r) { + CURL_TRC_SSLS(data, "error unpacking data: %d", r); + Curl_ssl_session_destroy(s); + } + else + *ps = s; + return r; +} + +#endif /* USE_SSLS_EXPORT */ diff --git a/lib/vtls/vtls_spack.h b/lib/vtls/vtls_spack.h new file mode 100644 index 0000000000..8905d7febf --- /dev/null +++ b/lib/vtls/vtls_spack.h @@ -0,0 +1,43 @@ +#ifndef HEADER_CURL_VTLS_SPACK_H +#define HEADER_CURL_VTLS_SPACK_H +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + * SPDX-License-Identifier: curl + * + ***************************************************************************/ +#include "curl_setup.h" + +#ifdef USE_SSLS_EXPORT + +struct dynbuf; +struct Curl_ssl_session; + +CURLcode Curl_ssl_session_pack(struct Curl_easy *data, + struct Curl_ssl_session *s, + struct dynbuf *buf); + +CURLcode Curl_ssl_session_unpack(struct Curl_easy *data, + const unsigned char *buf, size_t buflen, + struct Curl_ssl_session **ps); + +#endif /* USE_SSLS_EXPORT */ + +#endif /* HEADER_CURL_VTLS_SPACK_H */ diff --git a/m4/curl-confopts.m4 b/m4/curl-confopts.m4 index f11a320eb9..f7b99dffcc 100644 --- a/m4/curl-confopts.m4 +++ b/m4/curl-confopts.m4 @@ -653,3 +653,41 @@ AS_HELP_STRING([--disable-ech],[Disable ECH support]), esac ]) ]) + +dnl CURL_CHECK_OPTION_SSLS_EXPORT +dnl ----------------------------------------------------- +dnl Verify whether configure has been invoked with option +dnl --enable-ssl-session-export or --disable-ssl-session-export, and set +dnl shell variable want_ech as appropriate. + +AC_DEFUN([CURL_CHECK_OPTION_SSLS_EXPORT], [ + AC_MSG_CHECKING([whether to enable SSL session export support]) + OPT_SSLS_EXPORT="default" + AC_ARG_ENABLE(ssls-export, +AS_HELP_STRING([--enable-ssls-export], + [Enable SSL session export support]) +AS_HELP_STRING([--disable-ssls-export], + [Disable SSL session export support]), + OPT_SSLS_EXPORT=$enableval) + case "$OPT_SSLS_EXPORT" in + no) + dnl --disable-ssls-export option used + want_ssls_export="no" + curl_ssls_export_msg="no (--enable-ssls-export)" + AC_MSG_RESULT([no]) + ;; + default) + dnl configure option not specified + want_ssls_export="no" + curl_ssls_export_msg="no (--enable-ssls-export)" + AC_MSG_RESULT([no]) + ;; + *) + dnl --enable-ssls-export option used + want_ssls_export="yes" + curl_ssls_export_msg="enabled (--disable-ssls-export)" + AC_MSG_RESULT([yes]) + ;; + esac +]) +]) diff --git a/projects/generate.bat b/projects/generate.bat index 8145883f38..d217848acf 100644 --- a/projects/generate.bat +++ b/projects/generate.bat @@ -164,6 +164,7 @@ rem call :element %1 lib "timediff.c" %3 call :element %1 lib "nonblock.c" %3 call :element %1 lib "warnless.c" %3 + call :element %1 lib "curl_get_line.c" %3 call :element %1 lib "curl_multibyte.c" %3 call :element %1 lib "version_win32.c" %3 call :element %1 lib "dynbuf.c" %3 @@ -176,6 +177,7 @@ rem call :element %1 lib "nonblock.h" %3 call :element %1 lib "warnless.h" %3 call :element %1 lib "curl_ctype.h" %3 + call :element %1 lib "curl_get_line.h" %3 call :element %1 lib "curl_multibyte.h" %3 call :element %1 lib "version_win32.h" %3 call :element %1 lib "dynbuf.h" %3 diff --git a/scripts/singleuse.pl b/scripts/singleuse.pl index 93a8fd7272..06cf8b56e0 100755 --- a/scripts/singleuse.pl +++ b/scripts/singleuse.pl @@ -61,6 +61,8 @@ my %api = ( 'curl_easy_reset' => 'API', 'curl_easy_send' => 'API', 'curl_easy_setopt' => 'API', + 'curl_easy_ssls_export' => 'API', + 'curl_easy_ssls_import' => 'API', 'curl_easy_strerror' => 'API', 'curl_easy_unescape' => 'API', 'curl_easy_upkeep' => 'API', diff --git a/src/Makefile.inc b/src/Makefile.inc index cbf6d81c42..28275fee93 100644 --- a/src/Makefile.inc +++ b/src/Makefile.inc @@ -32,12 +32,14 @@ # libcurl sources to include in curltool lib we use for test binaries CURLTOOL_LIBCURL_CFILES = \ ../lib/base64.c \ - ../lib/dynbuf.c + ../lib/dynbuf.c \ + ../lib/curl_get_line.c # libcurl has sources that provide functions named curlx_* that are not part of # the official API, but we reuse the code here to avoid duplication. CURLX_CFILES = \ ../lib/base64.c \ + ../lib/curl_get_line.c \ ../lib/curl_multibyte.c \ ../lib/dynbuf.c \ ../lib/nonblock.c \ @@ -48,6 +50,7 @@ CURLX_CFILES = \ CURLX_HFILES = \ ../lib/curl_ctype.h \ + ../lib/curl_get_line.h \ ../lib/curl_multibyte.h \ ../lib/curl_setup.h \ ../lib/dynbuf.h \ @@ -92,6 +95,7 @@ CURL_CFILES = \ tool_progress.c \ tool_setopt.c \ tool_sleep.c \ + tool_ssls.c \ tool_stderr.c \ tool_strdup.c \ tool_urlglob.c \ @@ -139,6 +143,7 @@ CURL_HFILES = \ tool_setopt.h \ tool_setup.h \ tool_sleep.h \ + tool_ssls.h \ tool_stderr.h \ tool_strdup.h \ tool_urlglob.h \ diff --git a/src/tool_cfgable.h b/src/tool_cfgable.h index 62f0e58cc4..e1799f8833 100644 --- a/src/tool_cfgable.h +++ b/src/tool_cfgable.h @@ -330,6 +330,7 @@ struct GlobalConfig { bool styled_output; /* enable fancy output style detection */ long ms_per_transfer; /* start next transfer after (at least) this many milliseconds */ + char *ssl_sessions; /* file to load/save SSL session tickets */ #ifdef DEBUGBUILD bool test_duphandle; bool test_event_based; diff --git a/src/tool_getparam.c b/src/tool_getparam.c index 81dbbb8835..5eb3794519 100644 --- a/src/tool_getparam.c +++ b/src/tool_getparam.c @@ -300,6 +300,7 @@ static const struct LongShort aliases[]= { {"ssl-no-revoke", ARG_BOOL, ' ', C_SSL_NO_REVOKE}, {"ssl-reqd", ARG_BOOL, ' ', C_SSL_REQD}, {"ssl-revoke-best-effort", ARG_BOOL, ' ', C_SSL_REVOKE_BEST_EFFORT}, + {"ssl-sessions", ARG_FILE, ' ', C_SSL_SESSIONS}, {"sslv2", ARG_NONE, '2', C_SSLV2}, {"sslv3", ARG_NONE, '3', C_SSLV3}, {"stderr", ARG_FILE, ' ', C_STDERR}, @@ -2470,6 +2471,12 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */ if(feature_ssl) config->ssl_revoke_best_effort = TRUE; break; + case C_SSL_SESSIONS: /* --ssl-sessions */ + if(feature_ssls_export) + err = getstr(&global->ssl_sessions, nextarg, DENY_BLANK); + else + err = PARAM_LIBCURL_DOESNT_SUPPORT; + break; case C_TCP_FASTOPEN: /* --tcp-fastopen */ config->tcp_fastopen = TRUE; break; diff --git a/src/tool_getparam.h b/src/tool_getparam.h index 90708e001c..af083d06c7 100644 --- a/src/tool_getparam.h +++ b/src/tool_getparam.h @@ -259,6 +259,7 @@ typedef enum { C_SSL_NO_REVOKE, C_SSL_REQD, C_SSL_REVOKE_BEST_EFFORT, + C_SSL_SESSIONS, C_SSLV2, C_SSLV3, C_STDERR, diff --git a/src/tool_libinfo.c b/src/tool_libinfo.c index f6053e8400..ff3117a038 100644 --- a/src/tool_libinfo.c +++ b/src/tool_libinfo.c @@ -84,6 +84,7 @@ bool feature_ssl = FALSE; bool feature_tls_srp = FALSE; bool feature_zstd = FALSE; bool feature_ech = FALSE; +bool feature_ssls_export = FALSE; static struct feature_name_presentp { const char *feature_name; @@ -115,6 +116,7 @@ static struct feature_name_presentp { {"SPNEGO", &feature_spnego, CURL_VERSION_SPNEGO}, {"SSL", &feature_ssl, CURL_VERSION_SSL}, {"SSPI", NULL, CURL_VERSION_SSPI}, + {"SSLS-EXPORT", &feature_ssls_export, 0}, {"threadsafe", NULL, CURL_VERSION_THREADSAFE}, {"TLS-SRP", &feature_tls_srp, CURL_VERSION_TLSAUTH_SRP}, {"TrackMemory", NULL, CURL_VERSION_CURLDEBUG}, diff --git a/src/tool_libinfo.h b/src/tool_libinfo.h index 0d176699e8..003ed0140c 100644 --- a/src/tool_libinfo.h +++ b/src/tool_libinfo.h @@ -62,6 +62,7 @@ extern bool feature_ssl; extern bool feature_tls_srp; extern bool feature_zstd; extern bool feature_ech; +extern bool feature_ssls_export; CURLcode get_libcurl_info(void); const char *proto_token(const char *proto); diff --git a/src/tool_listhelp.c b/src/tool_listhelp.c index 3a4b096d1d..70b5e37817 100644 --- a/src/tool_listhelp.c +++ b/src/tool_listhelp.c @@ -715,6 +715,9 @@ const struct helptxt helptext[] = { {" --ssl-revoke-best-effort", "Ignore missing cert CRL dist points", CURLHELP_TLS}, + {" --ssl-sessions ", + "Load/save SSL session tickets from/to this file", + CURLHELP_TLS}, {"-2, --sslv2", "SSLv2", CURLHELP_DEPRECATED}, diff --git a/src/tool_operate.c b/src/tool_operate.c index 1bba71f829..da807ab857 100644 --- a/src/tool_operate.c +++ b/src/tool_operate.c @@ -87,6 +87,7 @@ #include "tool_parsecfg.h" #include "tool_setopt.h" #include "tool_sleep.h" +#include "tool_ssls.h" #include "tool_urlglob.h" #include "tool_util.h" #include "tool_writeout.h" @@ -3232,18 +3233,31 @@ CURLcode operate(struct GlobalConfig *global, int argc, argv_item_t argv[]) curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_PSL); curl_share_setopt(share, CURLSHOPT_SHARE, CURL_LOCK_DATA_HSTS); - /* Get the required arguments for each operation */ - do { - result = get_args(operation, count++); + if(global->ssl_sessions && feature_ssls_export) + result = tool_ssls_load(global, global->first, share, + global->ssl_sessions); - operation = operation->next; - } while(!result && operation); + if(!result) { + /* Get the required arguments for each operation */ + do { + result = get_args(operation, count++); - /* Set the current operation pointer */ - global->current = global->first; + operation = operation->next; + } while(!result && operation); - /* now run! */ - result = run_all_transfers(global, share, result); + /* Set the current operation pointer */ + global->current = global->first; + + /* now run! */ + result = run_all_transfers(global, share, result); + + if(global->ssl_sessions && feature_ssls_export) { + CURLcode r2 = tool_ssls_save(global, global->first, share, + global->ssl_sessions); + if(r2 && !result) + result = r2; + } + } curl_share_cleanup(share); if(global->libcurl) { diff --git a/src/tool_ssls.c b/src/tool_ssls.c new file mode 100644 index 0000000000..48b20de50b --- /dev/null +++ b/src/tool_ssls.c @@ -0,0 +1,222 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + * SPDX-License-Identifier: curl + * + ***************************************************************************/ +#include "tool_setup.h" + +#include "curlx.h" +#include "tool_cfgable.h" +#include "tool_cb_dbg.h" +#include "tool_msgs.h" +#include "tool_setopt.h" +#include "tool_ssls.h" +#include "dynbuf.h" +#include "curl_base64.h" +#include "curl_get_line.h" + +/* The maximum line length for an ecoded session ticket */ +#define MAX_SSLS_LINE (64 * 1024) + + +static CURLcode tool_ssls_easy(struct GlobalConfig *global, + struct OperationConfig *config, + CURLSH *share, CURL **peasy) +{ + CURLcode result = CURLE_OK; + + *peasy = curl_easy_init(); + if(!*peasy) + return CURLE_OUT_OF_MEMORY; + + result = curl_easy_setopt(*peasy, CURLOPT_SHARE, share); + if(global->tracetype != TRACE_NONE) { + my_setopt(*peasy, CURLOPT_DEBUGFUNCTION, tool_debug_cb); + my_setopt(*peasy, CURLOPT_DEBUGDATA, config); + my_setopt(*peasy, CURLOPT_VERBOSE, 1L); + } + return result; +} + +CURLcode tool_ssls_load(struct GlobalConfig *global, + struct OperationConfig *config, + CURLSH *share, const char *filename) +{ + FILE *fp; + CURL *easy = NULL; + struct dynbuf buf; + unsigned char *shmac = NULL, *sdata = NULL; + char *c, *line, *end; + size_t shmac_len, sdata_len; + CURLcode r = CURLE_OK; + int i, imported; + + curlx_dyn_init(&buf, MAX_SSLS_LINE); + fp = fopen(filename, FOPEN_READTEXT); + if(!fp) { /* ok if it does not exist */ + notef(global, "SSL session file does not exist (yet?): %s", filename); + goto out; + } + + r = tool_ssls_easy(global, config, share, &easy); + if(r) + goto out; + + i = imported = 0; + while(Curl_get_line(&buf, fp)) { + ++i; + curl_free(shmac); + curl_free(sdata); + line = Curl_dyn_ptr(&buf); + while(*line && ISBLANK(*line)) + line++; + if(*line == '#') + /* skip commented lines */ + continue; + + c = memchr(line, ':', strlen(line)); + if(!c) { + warnf(global, "unrecognized line %d in ssl session file %s", + i, filename); + continue; + } + *c = '\0'; + r = curlx_base64_decode(line, &shmac, &shmac_len); + if(r) { + warnf(global, "invalid shmax base64 encoding in line %d", i); + continue; + } + line = c + 1; + end = line + strlen(line) - 1; + while((end > line) && (*end == '\n' || *end == '\r' || ISBLANK(*line))) { + *end = '\0'; + --end; + } + r = curlx_base64_decode(line, &sdata, &sdata_len); + if(r) { + warnf(global, "invalid sdata base64 encoding in line %d: %s", i, line); + continue; + } + + r = curl_easy_ssls_import(easy, NULL, shmac, shmac_len, sdata, sdata_len); + if(r) { + warnf(global, "import of session from line %d rejected(%d)", i, r); + continue; + } + ++imported; + } + r = CURLE_OK; + +out: + if(easy) + curl_easy_cleanup(easy); + if(fp) + fclose(fp); + curlx_dyn_free(&buf); + curl_free(shmac); + curl_free(sdata); + return r; +} + +struct tool_ssls_ctx { + struct GlobalConfig *global; + FILE *fp; + int exported; +}; + +static CURLcode tool_ssls_exp(CURL *easy, void *userptr, + const char *session_key, + const unsigned char *shmac, size_t shmac_len, + const unsigned char *sdata, size_t sdata_len, + curl_off_t valid_until, int ietf_tls_id, + const char *alpn, size_t earlydata_max) +{ + struct tool_ssls_ctx *ctx = userptr; + char *enc = NULL; + size_t enc_len; + CURLcode r; + + (void)easy; + (void)valid_until; + (void)ietf_tls_id; + (void)alpn; + (void)earlydata_max; + if(!ctx->exported) + fputs("# Your SSL session cache. https://curl.se/docs/ssl-sessions.html\n" + "# This file was generated by libcurl! Edit at your own risk.\n", + ctx->fp); + + r = curlx_base64_encode((const char *)shmac, shmac_len, &enc, &enc_len); + if(r) + goto out; + r = CURLE_WRITE_ERROR; + if(enc_len != fwrite(enc, 1, enc_len, ctx->fp)) + goto out; + if(EOF == fputc(':', ctx->fp)) + goto out; + curl_free(enc); + r = curlx_base64_encode((const char *)sdata, sdata_len, &enc, &enc_len); + if(r) + goto out; + r = CURLE_WRITE_ERROR; + if(enc_len != fwrite(enc, 1, enc_len, ctx->fp)) + goto out; + if(EOF == fputc('\n', ctx->fp)) + goto out; + r = CURLE_OK; + ctx->exported++; +out: + if(r) + warnf(ctx->global, "Warning: error saving SSL session for '%s': %d", + session_key, r); + curl_free(enc); + return r; +} + +CURLcode tool_ssls_save(struct GlobalConfig *global, + struct OperationConfig *config, + CURLSH *share, const char *filename) +{ + struct tool_ssls_ctx ctx; + CURL *easy = NULL; + CURLcode r = CURLE_OK; + + ctx.global = global; + ctx.exported = 0; + ctx.fp = fopen(filename, FOPEN_WRITETEXT); + if(!ctx.fp) { + warnf(global, "Warning: Failed to create SSL session file %s", filename); + goto out; + } + + r = tool_ssls_easy(global, config, share, &easy); + if(r) + goto out; + + r = curl_easy_ssls_export(easy, tool_ssls_exp, &ctx); + +out: + if(easy) + curl_easy_cleanup(easy); + if(ctx.fp) + fclose(ctx.fp); + return r; +} diff --git a/src/tool_ssls.h b/src/tool_ssls.h new file mode 100644 index 0000000000..b8a6a5e053 --- /dev/null +++ b/src/tool_ssls.h @@ -0,0 +1,37 @@ +#ifndef HEADER_CURL_TOOL_SSLS_H +#define HEADER_CURL_TOOL_SSLS_H +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + * SPDX-License-Identifier: curl + * + ***************************************************************************/ +#include "tool_setup.h" +#include "tool_operate.h" + + +CURLcode tool_ssls_load(struct GlobalConfig *global, + struct OperationConfig *config, + CURLSH *share, const char *filename); +CURLcode tool_ssls_save(struct GlobalConfig *global, + struct OperationConfig *config, + CURLSH *share, const char *filename); + +#endif /* HEADER_CURL_TOOL_SSLS_H */ diff --git a/tests/data/test1135 b/tests/data/test1135 index e1e74752ad..070d8dff4c 100644 --- a/tests/data/test1135 +++ b/tests/data/test1135 @@ -67,6 +67,8 @@ curl_version_info curl_easy_strerror curl_share_strerror curl_easy_pause +curl_easy_ssls_import +curl_easy_ssls_export curl_easy_init curl_easy_setopt curl_easy_perform diff --git a/tests/http/test_17_ssl_use.py b/tests/http/test_17_ssl_use.py index 883d4a5f6f..dfea92ef96 100644 --- a/tests/http/test_17_ssl_use.py +++ b/tests/http/test_17_ssl_use.py @@ -390,3 +390,37 @@ class TestSSLUse: match_trace = line break assert match_trace, f'Did not find "{exp_trace}" in trace\n{r.dump_logs()}' + + @pytest.mark.skipif(condition=not Env.curl_has_feature('SSLS-EXPORT'), + reason='curl lacks SSL session export support') + def test_17_15_session_export(self, env: Env, httpd): + proto = 'http/1.1' + if env.curl_uses_lib('libressl'): + pytest.skip('Libressl resumption does not work inTLSv1.3') + if env.curl_uses_lib('rustls-ffi'): + pytest.skip('rustsls does not expose sessions') + if env.curl_uses_lib('bearssl'): + pytest.skip('BearSSL does not support TLSv1.3') + if env.curl_uses_lib('mbedtls') and \ + not env.curl_lib_version_at_least('mbedtls', '3.6.0'): + pytest.skip('mbedtls TLSv1.3 session resume not working before 3.6.0') + run_env = os.environ.copy() + run_env['CURL_DEBUG'] = 'ssl,scache' + # clean session file first, then reuse + session_file = os.path.join(env.gen_dir, 'test_17_15.sessions') + if os.path.exists(session_file): + return os.remove(session_file) + xargs = ['--tls-max', '1.3', '--tlsv1.3', '--ssl-sessions', session_file] + curl = CurlClient(env=env, run_env=run_env) + # tell the server to close the connection after each request + url = f'https://{env.authority_for(env.domain1, proto)}/curltest/sslinfo' + r = curl.http_get(url=url, alpn_proto=proto, extra_args=xargs) + assert r.exit_code == 0, f'{r}' + assert r.json['HTTPS'] == 'on', f'{r.json}' + assert r.json['SSL_SESSION_RESUMED'] == 'Initial', f'{r.json}\n{r.dump_logs()}' + # ok, run again, sessions should be imported + run_dir2 = os.path.join(env.gen_dir, 'curl2') + curl = CurlClient(env=env, run_env=run_env, run_dir=run_dir2) + r = curl.http_get(url=url, alpn_proto=proto, extra_args=xargs) + assert r.exit_code == 0, f'{r}' + assert r.json['SSL_SESSION_RESUMED'] == 'Resumed', f'{r.json}\n{r.dump_logs()}' diff --git a/tests/server/Makefile.inc b/tests/server/Makefile.inc index 575a4d121c..bcd8cae5e8 100644 --- a/tests/server/Makefile.inc +++ b/tests/server/Makefile.inc @@ -34,6 +34,7 @@ CURLX_SRCS = \ ../../lib/dynbuf.c \ ../../lib/strdup.c \ ../../lib/strcase.c \ + ../../lib/curl_get_line.c \ ../../lib/curl_multibyte.c CURLX_HDRS = \ @@ -45,6 +46,7 @@ CURLX_HDRS = \ ../../lib/curl_ctype.h \ ../../lib/dynbuf.h \ ../../lib/strdup.h \ + ../../lib/curl_get_line.h \ ../../lib/curl_multibyte.h USEFUL = \ diff --git a/tests/unit/unit3200.c b/tests/unit/unit3200.c index 92e179fc79..03d37a555c 100644 --- a/tests/unit/unit3200.c +++ b/tests/unit/unit3200.c @@ -22,6 +22,8 @@ * ***************************************************************************/ #include "curlcheck.h" +/* disable the curlx_get_line redefinitions for this unit test */ +#define BUILDING_LIBCURL #include "curl_get_line.h" #include "memdebug.h" diff --git a/winbuild/MakefileBuild.vc b/winbuild/MakefileBuild.vc index aee03d5640..41fe4d3dc9 100644 --- a/winbuild/MakefileBuild.vc +++ b/winbuild/MakefileBuild.vc @@ -688,6 +688,7 @@ CURL_FROM_LIBCURL=$(CURL_DIROBJ)\tool_hugehelp.obj \ $(CURL_DIROBJ)\nonblock.obj \ $(CURL_DIROBJ)\strtoofft.obj \ $(CURL_DIROBJ)\warnless.obj \ + $(CURL_DIROBJ)\curl_get_line.obj \ $(CURL_DIROBJ)\curl_multibyte.obj \ $(CURL_DIROBJ)\version_win32.obj \ $(CURL_DIROBJ)\dynbuf.obj \ @@ -708,6 +709,8 @@ $(CURL_DIROBJ)\strtoofft.obj: ../lib/strtoofft.c $(CURL_CC) $(CURL_CFLAGS) /Fo"$@" ../lib/strtoofft.c $(CURL_DIROBJ)\warnless.obj: ../lib/warnless.c $(CURL_CC) $(CURL_CFLAGS) /Fo"$@" ../lib/warnless.c +$(CURL_DIROBJ)\curl_get_line.obj: ../lib/curl_get_line.c + $(CURL_CC) $(CURL_CFLAGS) /Fo"$@" ../lib/curl_get_line.c $(CURL_DIROBJ)\curl_multibyte.obj: ../lib/curl_multibyte.c $(CURL_CC) $(CURL_CFLAGS) /Fo"$@" ../lib/curl_multibyte.c $(CURL_DIROBJ)\version_win32.obj: ../lib/version_win32.c