diff --git a/configure.ac b/configure.ac index a81d96a39b9f39..d60ccc14e79d39 100755 --- a/configure.ac +++ b/configure.ac @@ -4882,6 +4882,31 @@ AC_HELP_STRING([--disable-alt-svc],[Disable alt-svc support]), AC_MSG_RESULT(no) ) +dnl ************************************************************ +dnl switch on/off hsts +dnl +curl_hsts_msg="no (--enable-hsts)"; +AC_MSG_CHECKING([whether to support HSTS]) +AC_ARG_ENABLE(hsts, +AC_HELP_STRING([--enable-hsts],[Enable HSTS support]) +AC_HELP_STRING([--disable-hsts],[Disable HSTS support]), +[ case "$enableval" in + no) + AC_MSG_RESULT(no) + ;; + *) AC_MSG_RESULT(yes) + curl_hsts_msg="enabled"; + enable_hsts="yes" + ;; + esac ], + AC_MSG_RESULT(no) +) + +if test "$enable_hsts" = "yes"; then + AC_DEFINE(USE_HSTS, 1, [to enable HSTS]) + experimental="$experimental HSTS" +fi + dnl ************************************************************* dnl check whether ECH support, if desired, is actually available dnl @@ -4998,6 +5023,9 @@ fi if test "x$enable_altsvc" = "xyes"; then SUPPORT_FEATURES="$SUPPORT_FEATURES alt-svc" fi +if test "x$enable_hsts" = "xyes"; then + SUPPORT_FEATURES="$SUPPORT_FEATURES HSTS" +fi if test "x$CURL_DISABLE_CRYPTO_AUTH" != "x1" -a \ \( "x$HAVE_GSSAPI" = "x1" -o "x$USE_WINDOWS_SSPI" = "x1" \); then diff --git a/docs/EXPERIMENTAL.md b/docs/EXPERIMENTAL.md index ee5898944ba717..5b2d36c145d3fa 100644 --- a/docs/EXPERIMENTAL.md +++ b/docs/EXPERIMENTAL.md @@ -20,3 +20,5 @@ Experimental support in curl means: - HTTP/3 support and options - CURLSSLOPT_NATIVE_CA (No configure option, feature built in when supported) + - HSTS support and options + diff --git a/docs/HSTS.md b/docs/HSTS.md new file mode 100644 index 00000000000000..c3f08393c421fa --- /dev/null +++ b/docs/HSTS.md @@ -0,0 +1,44 @@ +# HSTS support + +curl features **EXPERIMENTAL** support for the Strict-Transport-Security: HTTP +header. Added in curl 7.74.0 + +## Standard + +[HTTP Strict Transport Security](https://tools.ietf.org/html/rfc6797) + +## Behavior + +libcurl features an in-memory cache for HSTS hosts, so that subsequent +HTTP-only requests to a host name present in the cache will get internally +"redirected" to the HTTPS version. + +## `curl_easy_setopt()` options: + + - `CURLOPT_HSTS_CTRL` - enable HSTS for this easy handle + - `CURLOPT_HSTS` - specify file name where to store the HSTS cache on close + (and possibly read from at startup) + +## curl cmdline options + + - `--hsts [filename]` - enable HSTS, use the file as HSTS cache. If filename + is `""` (no length) then no file will be used, only in-memory cache. + +## HSTS cache file format + +Lines starting with `#` are ignored. + +For each hsts entry: + + [host name] "YYYYMMDD HH:MM:SS" + +The `[host name]` is dot-prefixed if it is a includeSubDomain. + +The time stamp is when the entry expires. + +I considered using wget's file format for the HSTS cache. However, they store the time stamp as the epoch (number of seconds since 1970) and I strongly disagree with using that format. Instead I opted to use a format similar to the curl alt-svc cache file format. + +## Possible future additions + + - `CURLOPT_HSTS_PRELOAD` - provide a set of preloaded HSTS host names + - ability to save to something else than a file diff --git a/docs/Makefile.am b/docs/Makefile.am index 9d67084d99e8a0..4a1984010c6306 100644 --- a/docs/Makefile.am +++ b/docs/Makefile.am @@ -63,6 +63,7 @@ EXTRA_DIST = \ GOVERNANCE.md \ HELP-US.md \ HISTORY.md \ + HSTS.md \ HTTP-COOKIES.md \ HTTP2.md \ HTTP3.md \ diff --git a/docs/cmdline-opts/Makefile.inc b/docs/cmdline-opts/Makefile.inc index 792cadb3c3ccc5..7e8529c1a4c23f 100644 --- a/docs/cmdline-opts/Makefile.inc +++ b/docs/cmdline-opts/Makefile.inc @@ -85,6 +85,7 @@ DPAGES = \ head.d header.d \ help.d \ hostpubmd5.d \ + hsts.d \ http0.9.d \ http1.0.d \ http1.1.d http2.d \ diff --git a/docs/cmdline-opts/hsts.d b/docs/cmdline-opts/hsts.d new file mode 100644 index 00000000000000..2399084bfcd367 --- /dev/null +++ b/docs/cmdline-opts/hsts.d @@ -0,0 +1,18 @@ +Long: hsts +Arg: +Protocols: HTTPS +Help: Enable HSTS with this cache file +Added: 7.74.0 +Category: http +--- +WARNING: this option is experimental. Do not use in production. + +This option enables HSTS for the transfer. If the file name points to an +existing HSTS cache file, that will be used. After a completed transfer, the +cache will be saved to the file name again if it has been modified. + +Specify a "" file name (zero length) to avoid loading/saving and make curl +just handle HSTS in memory. + +If this option is used several times, curl will load contents from all the +files but the last one will be used for saving. diff --git a/docs/cmdline-opts/version.d b/docs/cmdline-opts/version.d index 52c29f177c8f39..f6c091707404f6 100644 --- a/docs/cmdline-opts/version.d +++ b/docs/cmdline-opts/version.d @@ -28,6 +28,8 @@ This curl uses a libcurl built with Debug. This enables more error-tracking and memory debugging etc. For curl-developers only! .IP "GSS-API" GSS-API is supported. +.IP "HSTS" +HSTS support is present. .IP "HTTP2" HTTP/2 support has been built-in. .IP "HTTP3" diff --git a/docs/libcurl/curl_easy_setopt.3 b/docs/libcurl/curl_easy_setopt.3 index 362cfef0a8d6e4..3434b158ca2f2a 100644 --- a/docs/libcurl/curl_easy_setopt.3 +++ b/docs/libcurl/curl_easy_setopt.3 @@ -319,6 +319,10 @@ Add or control cookies. See \fICURLOPT_COOKIELIST(3)\fP Specify the Alt-Svc: cache file name. See \fICURLOPT_ALTSVC(3)\fP .IP CURLOPT_ALTSVC_CTRL Enable and configure Alt-Svc: treatment. See \fICURLOPT_ALTSVC_CTRL(3)\fP +.IP CURLOPT_HSTS +Set HSTS cache file. See \fICURLOPT_HSTS(3)\fP +.IP CURLOPT_HSTS_CTRL +Enable HSTS. See \fICURLOPT_HSTS_CTRL(3)\fP .IP CURLOPT_HTTPGET Do an HTTP GET request. See \fICURLOPT_HTTPGET(3)\fP .IP CURLOPT_REQUEST_TARGET diff --git a/docs/libcurl/curl_version_info.3 b/docs/libcurl/curl_version_info.3 index 5c5f16a3d57264..a6fa5e3b97b96c 100644 --- a/docs/libcurl/curl_version_info.3 +++ b/docs/libcurl/curl_version_info.3 @@ -143,6 +143,9 @@ to use the current user credentials without the app having to pass them on. (Added in 7.38.0) .IP CURL_VERSION_GSSNEGOTIATE supports HTTP GSS-Negotiate (added in 7.10.6) +.IP CURL_VERSION_HSTS +libcurl was built with support for HSTS (HTTP Strict Transport Security) +(Added in 7.74.0) .IP CURL_VERSION_HTTPS_PROXY libcurl was built with support for HTTPS-proxy. (Added in 7.52.0) diff --git a/docs/libcurl/opts/CURLOPT_HSTS.3 b/docs/libcurl/opts/CURLOPT_HSTS.3 new file mode 100644 index 00000000000000..b90dbff8bf21f3 --- /dev/null +++ b/docs/libcurl/opts/CURLOPT_HSTS.3 @@ -0,0 +1,66 @@ +.\" ************************************************************************** +.\" * _ _ ____ _ +.\" * Project ___| | | | _ \| | +.\" * / __| | | | |_) | | +.\" * | (__| |_| | _ <| |___ +.\" * \___|\___/|_| \_\_____| +.\" * +.\" * Copyright (C) 2020, 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.haxx.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. +.\" * +.\" ************************************************************************** +.\" +.TH CURLOPT_HSTS 3 "5 Feb 2019" "libcurl 7.74.0" "curl_easy_setopt options" +.SH NAME +CURLOPT_HSTS \- set HSTS cache file name +.SH SYNOPSIS +.nf +#include + +CURLcode curl_easy_setopt(CURL *handle, CURLOPT_HSTS, char *filename); +.fi +.SH EXPERIMENTAL +Warning: this feature is early code and is marked as experimental. It can only +be enabled by explicitly telling configure with \fB--enable-hsts\fP. You are +advised to not ship this in production before the experimental label is +removed. +.SH DESCRIPTION +Make the \fIfilename\fP point to a file name to load an existing HSTS cache +from, and to store the cache in when the easy handle is closed. Setting a file +name with this option will also enable HSTS for this handle (the equivalent of +setting \fICURLHSTS_ENABLE\fP with \fICURLOPT_HSTS_CTRL(3)\fP). + +If the given file does not exist or contains no HSTS entries at startup, the +HSTS cache will simply start empty. Setting the file name to NULL or "" will +only enable HSTS without reading from or writing to any file. + +If this option is set multiple times, libcurl will load cache entries from +each given file but will only store the last used name for later writing. +.SH DEFAULT +NULL, no file name +.SH PROTOCOLS +HTTPS and HTTP +.SH EXAMPLE +.nf +CURL *curl = curl_easy_init(); +if(curl) { + curl_easy_setopt(curl, CURLOPT_HSTS, "/home/user/.hsts-cache"); + curl_easy_perform(curl); +} +.fi +.SH AVAILABILITY +Added in 7.74.0 +.SH RETURN VALUE +Returns CURLE_OK if the option is supported, and CURLE_UNKNOWN_OPTION if not. +.SH "SEE ALSO" +.BR CURLOPT_HSTS_CTRL "(3), " CURLOPT_ALTSVC "(3), " CURLOPT_RESOLVE "(3), " diff --git a/docs/libcurl/opts/CURLOPT_HSTS_CTRL.3 b/docs/libcurl/opts/CURLOPT_HSTS_CTRL.3 new file mode 100644 index 00000000000000..ffdfee93f88d38 --- /dev/null +++ b/docs/libcurl/opts/CURLOPT_HSTS_CTRL.3 @@ -0,0 +1,73 @@ +.\" ************************************************************************** +.\" * _ _ ____ _ +.\" * Project ___| | | | _ \| | +.\" * / __| | | | |_) | | +.\" * | (__| |_| | _ <| |___ +.\" * \___|\___/|_| \_\_____| +.\" * +.\" * Copyright (C) 2020, 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.haxx.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. +.\" * +.\" ************************************************************************** +.\" +.TH CURLOPT_HSTS_CTRL 3 "4 Sep 2020" "libcurl 7.74.0" "curl_easy_setopt options" +.SH NAME +CURLOPT_HSTS_CTRL \- control HSTS behavior +.SH SYNOPSIS +.nf +#include + +#define CURLHSTS_ENABLE (1<<0) +#define CURLHSTS_READONLYFILE (1<<1) + +CURLcode curl_easy_setopt(CURL *handle, CURLOPT_HSTS_CTRL, long bitmask); +.fi +.SH EXPERIMENTAL +Warning: this feature is early code and is marked as experimental. It can only +be enabled by explicitly telling configure with \fB--enable-hsts\fP. You are +advised to not ship this in production before the experimental label is +removed. +.SH DESCRIPTION +HSTS (HTTP Strict Transport Security) means that an HTTPS server can instruct +the client to not contact it again over clear-text HTTP for a certain period +into the future. libcurl will then automatically redirect HTTP attempts to +such hosts to instead use HTTPS. This is done by libcurl retaining this +knowledge in an in-memory cache. + +Populate the long \fIbitmask\fP with the correct set of features to instruct +libcurl how to handle HSTS for the transfers using this handle. +.SH BITS +.IP "CURLHSTS_ENABLE" +Enable the in-memory HSTS cache for this handle. +.IP "CURLHSTS_READONLYFILE" +Make the HSTS file (if specified) read-only - makes libcurl not save the cache +to the file when closing the handle. +.SH DEFAULT +0. HSTS is disabled by default. +.SH PROTOCOLS +HTTPS and HTTP +.SH EXAMPLE +.nf +CURL *curl = curl_easy_init(); +if(curl) { + curl_easy_setopt(curl, CURLOPT_HSTS_CTRL, CURLHSTS_ENABLE); + curl_easy_perform(curl); +} +.fi +.SH AVAILABILITY +Added in 7.74.0 +.SH RETURN VALUE +Returns CURLE_OK if the option is supported, and CURLE_UNKNOWN_OPTION if not. +.SH "SEE ALSO" +.BR CURLOPT_HSTS "(3), " CURLOPT_CONNECT_TO "(3), " CURLOPT_RESOLVE "(3), " +.BR CURLOPT_ALTSVC "(3), " diff --git a/docs/libcurl/opts/Makefile.inc b/docs/libcurl/opts/Makefile.inc index fe417757968ef5..9d1eb2bba767a0 100644 --- a/docs/libcurl/opts/Makefile.inc +++ b/docs/libcurl/opts/Makefile.inc @@ -180,6 +180,8 @@ man_MANS = \ CURLOPT_HEADERDATA.3 \ CURLOPT_HEADERFUNCTION.3 \ CURLOPT_HEADEROPT.3 \ + CURLOPT_HSTS.3 \ + CURLOPT_HSTS_CTRL.3 \ CURLOPT_HTTP09_ALLOWED.3 \ CURLOPT_HTTP200ALIASES.3 \ CURLOPT_HTTPAUTH.3 \ diff --git a/docs/libcurl/symbols-in-versions b/docs/libcurl/symbols-in-versions index 1b37d13d176a84..cc35fc57ba7aba 100644 --- a/docs/libcurl/symbols-in-versions +++ b/docs/libcurl/symbols-in-versions @@ -213,6 +213,8 @@ CURLGSSAPI_DELEGATION_NONE 7.22.0 CURLGSSAPI_DELEGATION_POLICY_FLAG 7.22.0 CURLHEADER_SEPARATE 7.37.0 CURLHEADER_UNIFIED 7.37.0 +CURLHSTS_ENABLE 7.74.0 +CURLHSTS_READONLYFILE 7.74.0 CURLINFO_ACTIVESOCKET 7.45.0 CURLINFO_APPCONNECT_TIME 7.19.0 CURLINFO_APPCONNECT_TIME_T 7.61.0 @@ -443,6 +445,8 @@ CURLOPT_HEADER 7.1 CURLOPT_HEADERDATA 7.10 CURLOPT_HEADERFUNCTION 7.7.2 CURLOPT_HEADEROPT 7.37.0 +CURLOPT_HSTS 7.74.0 +CURLOPT_HSTS_CTRL 7.74.0 CURLOPT_HTTP09_ALLOWED 7.64.0 CURLOPT_HTTP200ALIASES 7.10.3 CURLOPT_HTTPAUTH 7.10.6 @@ -1001,6 +1005,7 @@ CURL_VERSION_CURLDEBUG 7.19.6 CURL_VERSION_DEBUG 7.10.6 CURL_VERSION_GSSAPI 7.38.0 CURL_VERSION_GSSNEGOTIATE 7.10.6 7.38.0 +CURL_VERSION_HSTS 7.74.0 CURL_VERSION_HTTP2 7.33.0 CURL_VERSION_HTTP3 7.66.0 CURL_VERSION_HTTPS_PROXY 7.52.0 diff --git a/docs/options-in-versions b/docs/options-in-versions index 683363239447e2..97c416d830f36e 100644 --- a/docs/options-in-versions +++ b/docs/options-in-versions @@ -79,6 +79,7 @@ --header (-H) 5.0 --help (-h) 4.0 --hostpubmd5 7.17.1 +--hsts 7.74.0 --http0.9 7.64.0 --http1.0 (-0) 7.9.1 --http1.1 7.33.0 diff --git a/include/curl/curl.h b/include/curl/curl.h index ad1a2097b6f108..56840894291ffa 100644 --- a/include/curl/curl.h +++ b/include/curl/curl.h @@ -954,6 +954,10 @@ typedef enum { #define CURLALTSVC_H2 (1<<4) #define CURLALTSVC_H3 (1<<5) +/* CURLHSTS_* are bits for the CURLOPT_HSTS option */ +#define CURLHSTS_ENABLE (long)(1<<0) +#define CURLHSTS_READONLYFILE (long)(1<<1) + /* CURLPROTO_ defines are for the CURLOPT_*PROTOCOLS options */ #define CURLPROTO_HTTP (1<<0) #define CURLPROTO_HTTPS (1<<1) @@ -2029,6 +2033,11 @@ typedef enum { */ CURLOPT(CURLOPT_SSL_EC_CURVES, CURLOPTTYPE_STRINGPOINT, 298), + /* HSTS bitmask */ + CURLOPT(CURLOPT_HSTS_CTRL, CURLOPTTYPE_LONG, 299), + /* HSTS file name */ + CURLOPT(CURLOPT_HSTS, CURLOPTTYPE_STRINGPOINT, 300), + CURLOPT_LASTENTRY /* the last unused */ } CURLoption; @@ -2900,6 +2909,7 @@ typedef struct curl_version_info_data curl_version_info_data; #define CURL_VERSION_HTTP3 (1<<25) /* HTTP3 support built-in */ #define CURL_VERSION_ZSTD (1<<26) /* zstd features are present */ #define CURL_VERSION_UNICODE (1<<27) /* Unicode support on Windows */ +#define CURL_VERSION_HSTS (1<<28) /* HSTS is supported */ /* * NAME curl_version_info() diff --git a/lib/Makefile.inc b/lib/Makefile.inc index a2fd57a831104d..ea7a374968c3b0 100644 --- a/lib/Makefile.inc +++ b/lib/Makefile.inc @@ -61,7 +61,7 @@ LIB_CFILES = altsvc.c amigaos.c asyn-ares.c asyn-thread.c base64.c \ socks_gssapi.c socks_sspi.c speedcheck.c splay.c strcase.c strdup.c \ strerror.c strtok.c strtoofft.c system_win32.c telnet.c tftp.c timeval.c \ transfer.c urlapi.c version.c warnless.c wildcard.c x509asn1.c dynbuf.c \ - version_win32.c easyoptions.c easygetopt.c + version_win32.c easyoptions.c easygetopt.c hsts.c LIB_HFILES = altsvc.h amigaos.h arpa_telnet.h asyn.h conncache.h connect.h \ content_encoding.h cookie.h curl_addrinfo.h curl_base64.h curl_ctype.h \ @@ -80,7 +80,7 @@ LIB_HFILES = altsvc.h amigaos.h arpa_telnet.h asyn.h conncache.h connect.h \ smb.h smtp.h sockaddr.h socketpair.h socks.h speedcheck.h splay.h strcase.h \ strdup.h strerror.h strtok.h strtoofft.h system_win32.h telnet.h tftp.h \ timeval.h transfer.h urlapi-int.h urldata.h warnless.h wildcard.h \ - x509asn1.h dynbuf.h version_win32.h easyoptions.h + x509asn1.h dynbuf.h version_win32.h easyoptions.h hsts.h LIB_RCFILES = libcurl.rc diff --git a/lib/curl_get_line.c b/lib/curl_get_line.c index ffe4256baeb021..aa524d8fea6971 100644 --- a/lib/curl_get_line.c +++ b/lib/curl_get_line.c @@ -22,7 +22,8 @@ #include "curl_setup.h" -#if !defined(CURL_DISABLE_COOKIES) || !defined(CURL_DISABLE_ALTSVC) +#if !defined(CURL_DISABLE_COOKIES) || !defined(CURL_DISABLE_ALTSVC)) || \ + defined(USE_HSTS) #include "curl_get_line.h" #include "curl_memory.h" diff --git a/lib/easy.c b/lib/easy.c index 60e2befd764cdf..ca1117a467c9db 100644 --- a/lib/easy.c +++ b/lib/easy.c @@ -79,6 +79,7 @@ #include "http2.h" #include "dynbuf.h" #include "altsvc.h" +#include "hsts.h" /* The last 3 #include files should be in this order */ #include "curl_printf.h" @@ -880,6 +881,15 @@ struct Curl_easy *curl_easy_duphandle(struct Curl_easy *data) if(outcurl->set.str[STRING_ALTSVC]) (void)Curl_altsvc_load(outcurl->asi, outcurl->set.str[STRING_ALTSVC]); } +#endif +#ifdef USE_HSTS + if(data->hsts) { + outcurl->hsts = Curl_hsts_init(); + if(!outcurl->hsts) + goto fail; + if(outcurl->set.str[STRING_HSTS]) + (void)Curl_hsts_load(outcurl->hsts, outcurl->set.str[STRING_HSTS]); + } #endif /* Clone the resolver handle, if present, for the new handle */ if(Curl_resolver_duphandle(outcurl, @@ -929,6 +939,7 @@ struct Curl_easy *curl_easy_duphandle(struct Curl_easy *data) Curl_safefree(outcurl->change.url); Curl_safefree(outcurl->change.referer); Curl_altsvc_cleanup(&outcurl->asi); + Curl_hsts_cleanup(&outcurl->hsts); Curl_freeset(outcurl); free(outcurl); } diff --git a/lib/easyoptions.c b/lib/easyoptions.c index 0ab6a3fc6d3599..e5b9ffb70ac986 100644 --- a/lib/easyoptions.c +++ b/lib/easyoptions.c @@ -115,6 +115,8 @@ struct curl_easyoption Curl_easyopts[] = { {"HEADERDATA", CURLOPT_HEADERDATA, CURLOT_CBPTR, 0}, {"HEADERFUNCTION", CURLOPT_HEADERFUNCTION, CURLOT_FUNCTION, 0}, {"HEADEROPT", CURLOPT_HEADEROPT, CURLOT_VALUES, 0}, + {"HSTS", CURLOPT_HSTS, CURLOT_STRING, 0}, + {"HSTS_CTRL", CURLOPT_HSTS_CTRL, CURLOT_LONG, 0}, {"HTTP09_ALLOWED", CURLOPT_HTTP09_ALLOWED, CURLOT_LONG, 0}, {"HTTP200ALIASES", CURLOPT_HTTP200ALIASES, CURLOT_SLIST, 0}, {"HTTPAUTH", CURLOPT_HTTPAUTH, CURLOT_VALUES, 0}, @@ -342,6 +344,6 @@ struct curl_easyoption Curl_easyopts[] = { */ int Curl_easyopts_check(void) { - return (CURLOPT_LASTENTRY != (298 + 1)); + return (CURLOPT_LASTENTRY != (300 + 1)); } #endif diff --git a/lib/hsts.c b/lib/hsts.c new file mode 100644 index 00000000000000..7eb3cda03fdeca --- /dev/null +++ b/lib/hsts.c @@ -0,0 +1,430 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) 2020, 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.haxx.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. + * + ***************************************************************************/ +/* + * The Strict-Transport-Security header is defined in RFC 6797: + * https://tools.ietf.org/html/rfc6797 + */ +#include "curl_setup.h" + +#if !defined(CURL_DISABLE_HTTP) && defined(USE_HSTS) +#include +#include "urldata.h" +#include "llist.h" +#include "hsts.h" +#include "curl_get_line.h" +#include "strcase.h" +#include "sendf.h" +#include "strtoofft.h" +#include "parsedate.h" +#include "rand.h" +#include "rename.h" + +/* The last 3 #include files should be in this order */ +#include "curl_printf.h" +#include "curl_memory.h" +#include "memdebug.h" + +#define MAX_HSTS_LINE 4095 +#define MAX_HSTS_HOSTLEN 256 +#define MAX_HSTS_HOSTLENSTR "256" +#define MAX_HSTS_SUBLEN 4 +#define MAX_HSTS_SUBLENSTR "4" +#define MAX_HSTS_DATELEN 64 +#define MAX_HSTS_DATELENSTR "64" + +#ifdef DEBUGBUILD +/* to play well with debug builds, we can *set* a fixed time this will + return */ +time_t deltatime; /* allow for "adjustments" for unit test purposes */ +static time_t debugtime(void *unused) +{ + char *timestr = getenv("CURL_TIME"); + (void)unused; + if(timestr) { + unsigned long val = strtol(timestr, NULL, 10) + deltatime; + return (time_t)val; + } + return time(NULL); +} +#define time(x) debugtime(x) +#endif + +struct hsts *Curl_hsts_init(void) +{ + struct hsts *h = calloc(sizeof(struct hsts), 1); + if(h) { + Curl_llist_init(&h->list, NULL); + } + return h; +} + +static void hsts_free(struct stsentry *e) +{ + free((char *)e->host); + free(e); +} + +void Curl_hsts_cleanup(struct hsts **hp) +{ + struct hsts *h = *hp; + if(h) { + struct Curl_llist_element *e; + struct Curl_llist_element *n; + for(e = h->list.head; e; e = n) { + struct stsentry *sts = e->ptr; + n = e->next; + hsts_free(sts); + } + free(h->filename); + free(h); + *hp = NULL; + } +} + +static struct stsentry *hsts_entry(void) +{ + return calloc(sizeof(struct stsentry), 1); +} + +static CURLcode hsts_create(struct hsts *h, + const char *hostname, + bool subdomains, + curl_off_t expires) +{ + struct stsentry *sts = hsts_entry(); + if(!sts) + return CURLE_OUT_OF_MEMORY; + + sts->expires = expires; + sts->includeSubDomains = subdomains; + sts->host = strdup(hostname); + if(!sts->host) { + free(sts); + return CURLE_OUT_OF_MEMORY; + } + fprintf(stderr, "*** Add %s %s %d\n", hostname, + subdomains?"SUB":"-", (int)expires); + Curl_llist_insert_next(&h->list, h->list.tail, sts, &sts->node); + return CURLE_OK; +} + +CURLcode Curl_hsts_parse(struct hsts *h, const char *hostname, + const char *header) +{ + const char *p = header; + curl_off_t expires = 0; + bool gotma = FALSE; + bool gotinc = FALSE; + bool subdomains = FALSE; + struct stsentry *sts; + time_t now = time(NULL); + + do { + while(*p && ISSPACE(*p)) + p++; + if(Curl_strncasecompare("max-age=", p, 8)) { + bool quoted = FALSE; + CURLofft offt; + char *endp; + + if(gotma) + return CURLE_BAD_FUNCTION_ARGUMENT; + + p += 8; + while(*p && ISSPACE(*p)) + p++; + if(*p == '\"') { + p++; + quoted = TRUE; + } + offt = curlx_strtoofft(p, &endp, 10, &expires); + if(offt == CURL_OFFT_FLOW) + expires = CURL_OFF_T_MAX; + else if(offt) + /* invalid max-age */ + return CURLE_BAD_FUNCTION_ARGUMENT; + p = endp; + if(quoted) { + if(*p != '\"') + return CURLE_BAD_FUNCTION_ARGUMENT; + p++; + } + gotma = TRUE; + } + else if(Curl_strncasecompare("includesubdomains", p, 17)) { + if(gotinc) + return CURLE_BAD_FUNCTION_ARGUMENT; + subdomains = TRUE; + p += 17; + gotinc = TRUE; + } + else { + /* unknown directive, do a lame attempt to skip */ + while(*p && (*p != ';')) + p++; + } + + while(*p && ISSPACE(*p)) + p++; + if(*p == ';') + p++; + } while (*p); + + if(!gotma) + /* max-age is mandatory */ + return CURLE_BAD_FUNCTION_ARGUMENT; + + if(!expires) { + /* remove the entry if present verbatim (without subdomain match) */ + sts = Curl_hsts(h, hostname, FALSE); + if(sts) { + Curl_llist_remove(&h->list, &sts->node, NULL); + hsts_free(sts); + } + return CURLE_OK; + } + + if(CURL_OFF_T_MAX - now < expires) + /* would overflow, use maximum value */ + expires = CURL_OFF_T_MAX; + else + expires += now; + + /* check if it already exists */ + sts = Curl_hsts(h, hostname, FALSE); + if(sts) { + /* just update these fields */ + sts->expires = expires; + sts->includeSubDomains = subdomains; + } + else + return hsts_create(h, hostname, subdomains, expires); + + return CURLE_OK; +} + +/* + * Return TRUE if the given host name is currently an HSTS one. + * + * The 'subdomain' argument tells the function if subdomain matching should be + * attempted. + */ +struct stsentry *Curl_hsts(struct hsts *h, const char *hostname, + bool subdomain) +{ + if(h) { + time_t now = time(NULL); + size_t hlen = strlen(hostname); + struct Curl_llist_element *e; + struct Curl_llist_element *n; + for(e = h->list.head; e; e = n) { + struct stsentry *sts = e->ptr; + n = e->next; + if(sts->expires <= now) { + /* remove expired entries */ + Curl_llist_remove(&h->list, &sts->node, NULL); + hsts_free(sts); + continue; + } + if(subdomain && sts->includeSubDomains) { + size_t ntail = strlen(sts->host); + if(ntail < hlen) { + size_t offs = hlen - ntail; + if((hostname[offs-1] == '.') && + Curl_strncasecompare(&hostname[offs], sts->host, ntail)) + return sts; + } + } + if(Curl_strcasecompare(hostname, sts->host)) + return sts; + } + } + return NULL; /* no match */ +} + +/* + * Write this single hsts entry to a single output line + */ +static CURLcode hsts_out(struct stsentry *sts, FILE *fp) +{ + struct tm stamp; + CURLcode result = Curl_gmtime(sts->expires, &stamp); + if(result) + return result; + + fprintf(fp, "%s%s \"%d%02d%02d %02d:%02d:%02d\"\n", + sts->includeSubDomains ? ".": "", sts->host, + stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday, + stamp.tm_hour, stamp.tm_min, stamp.tm_sec); + return CURLE_OK; +} + + +/* + * Curl_https_save() writes the HSTS cache to a file. + */ +CURLcode Curl_hsts_save(struct Curl_easy *data, struct hsts *h, + const char *file) +{ + struct Curl_llist_element *e; + struct Curl_llist_element *n; + CURLcode result = CURLE_OK; + FILE *out; + char *tempstore; + unsigned char randsuffix[9]; + + if(!h) + /* no cache activated */ + return CURLE_OK; + + /* if not new name is given, use the one we stored from the load */ + if(!file && h->filename) + file = h->filename; + + if((h->flags & CURLHSTS_READONLYFILE) || !file || !file[0]) + /* marked as read-only, no file or zero length file name */ + return CURLE_OK; + + if(Curl_rand_hex(data, randsuffix, sizeof(randsuffix))) + return CURLE_FAILED_INIT; + + tempstore = aprintf("%s.%s.tmp", file, randsuffix); + if(!tempstore) + return CURLE_OUT_OF_MEMORY; + + out = fopen(tempstore, FOPEN_WRITETEXT); + if(!out) + result = CURLE_WRITE_ERROR; + else { + fputs("# Your HSTS cache. https://curl.haxx.se/docs/hsts.html\n" + "# This file was generated by libcurl! Edit at your own risk.\n", + out); + for(e = h->list.head; e; e = n) { + struct stsentry *sts = e->ptr; + n = e->next; + result = hsts_out(sts, out); + if(result) + break; + } + fclose(out); + if(!result && Curl_rename(tempstore, file)) + result = CURLE_WRITE_ERROR; + + if(result) + unlink(tempstore); + } + free(tempstore); + return result; +} + +/* only returns SERIOUS errors */ +static CURLcode hsts_add(struct hsts *h, char *line) +{ + /* Example lines: + example.com "20191231 10:00:00" + .example.net "20191231 10:00:00" + */ + char host[MAX_HSTS_HOSTLEN + 1]; + char date[MAX_HSTS_DATELEN + 1]; + int rc; + + rc = sscanf(line, + "%" MAX_HSTS_HOSTLENSTR "s \"%" MAX_HSTS_DATELENSTR "[^\"]\"", + host, date); + if(2 == rc) { + time_t expires = Curl_getdate_capped(date); + CURLcode result; + char *p = host; + bool subdomain = FALSE; + if(p[0] == '.') { + p++; + subdomain = TRUE; + } + result = hsts_create(h, p, subdomain, expires); + if(result) + return result; + } + + return CURLE_OK; +} + +/* + * Load the HSTS cache from the given file. The text based line-oriented file + * format is documented here: + * https://github.com/curl/curl/wiki/HSTS + * + * This function only returns error on major problems that prevents hsts + * handling to work completely. It will ignore individual syntactical errors + * etc. + */ +static CURLcode hsts_load(struct hsts *h, const char *file) +{ + CURLcode result = CURLE_OK; + char *line = NULL; + FILE *fp; + + /* we need a private copy of the file name so that the hsts cache file + name survives an easy handle reset */ + free(h->filename); + h->filename = strdup(file); + if(!h->filename) + return CURLE_OUT_OF_MEMORY; + + fp = fopen(file, FOPEN_READTEXT); + if(fp) { + line = malloc(MAX_HSTS_LINE); + if(!line) + goto fail; + while(Curl_get_line(line, MAX_HSTS_LINE, fp)) { + char *lineptr = line; + while(*lineptr && ISBLANK(*lineptr)) + lineptr++; + if(*lineptr == '#') + /* skip commented lines */ + continue; + + hsts_add(h, lineptr); + } + free(line); /* free the line buffer */ + fclose(fp); + } + return result; + + fail: + Curl_safefree(h->filename); + free(line); + fclose(fp); + return CURLE_OUT_OF_MEMORY; +} + +/* + * Curl_hsts_load() loads HSTS from file. + */ +CURLcode Curl_hsts_load(struct hsts *h, const char *file) +{ + CURLcode result; + DEBUGASSERT(h); + result = hsts_load(h, file); + return result; +} + +#endif /* CURL_DISABLE_HTTP || USE_HSTS */ diff --git a/lib/hsts.h b/lib/hsts.h new file mode 100644 index 00000000000000..60b3c2df7b3c39 --- /dev/null +++ b/lib/hsts.h @@ -0,0 +1,60 @@ +#ifndef HEADER_CURL_HSTS_H +#define HEADER_CURL_HSTS_H +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) 2020, 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.haxx.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. + * + ***************************************************************************/ +#include "curl_setup.h" + +#if !defined(CURL_DISABLE_HTTP) && defined(USE_HSTS) +#include +#include "llist.h" + +#ifdef DEBUGBUILD +extern time_t deltatime; +#endif + +struct stsentry { + struct Curl_llist_element node; + const char *host; + bool includeSubDomains; + time_t expires; /* the timestamp of this entry's expiry */ +}; + +/* The HSTS cache. Needs to be able to tailmatch host names. */ +struct hsts { + struct Curl_llist list; + char *filename; + unsigned int flags; +}; + +struct hsts *Curl_hsts_init(void); +void Curl_hsts_cleanup(struct hsts **hp); +CURLcode Curl_hsts_parse(struct hsts *h, const char *hostname, + const char *sts); +struct stsentry *Curl_hsts(struct hsts *h, const char *hostname, + bool subdomain); +CURLcode Curl_hsts_save(struct Curl_easy *data, struct hsts *h, + const char *file); +CURLcode Curl_hsts_load(struct hsts *h, const char *file); +#else +#define Curl_hsts_cleanup(x) +#endif /* CURL_DISABLE_HTTP || USE_HSTS */ +#endif /* HEADER_CURL_HSTS_H */ diff --git a/lib/http.c b/lib/http.c index 3a0a32df430fb0..4db1c958968fd7 100644 --- a/lib/http.c +++ b/lib/http.c @@ -77,6 +77,7 @@ #include "connect.h" #include "strdup.h" #include "altsvc.h" +#include "hsts.h" /* The last 3 #include files should be in this order */ #include "curl_printf.h" @@ -3990,6 +3991,23 @@ CURLcode Curl_http_readwrite_headers(struct Curl_easy *data, } } } + +#ifdef USE_HSTS + /* If enabled, the header is incoming and this is over HTTPS */ + else if(data->hsts && checkprefix("Strict-Transport-Security:", headp) && + (conn->handler->flags & PROTOPT_SSL)) { + CURLcode check = + Curl_hsts_parse(data->hsts, data->state.up.hostname, + &headp[ sizeof("Strict-Transport-Security:") -1 ]); + if(check) + infof(data, "Illegal STS header skipped\n"); +#ifdef DEBUGBUILD + else + infof(data, "Parsed STS header fine (%d entries)\n", + data->hsts->list.size); +#endif + } +#endif #ifndef CURL_DISABLE_ALTSVC /* If enabled, the header is incoming and this is over HTTPS */ else if(data->asi && checkprefix("Alt-Svc:", headp) && diff --git a/lib/setopt.c b/lib/setopt.c index 3b96289c253fe9..4aa31bb39a0c94 100644 --- a/lib/setopt.c +++ b/lib/setopt.c @@ -45,6 +45,7 @@ #include "setopt.h" #include "multiif.h" #include "altsvc.h" +#include "hsts.h" /* The last 3 #include files should be in this order */ #include "curl_printf.h" @@ -2839,6 +2840,33 @@ CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param) data->set.trailer_data = va_arg(param, void *); #endif break; +#ifdef USE_HSTS + case CURLOPT_HSTS: + if(!data->hsts) { + data->hsts = Curl_hsts_init(); + if(!data->hsts) + return CURLE_OUT_OF_MEMORY; + } + argptr = va_arg(param, char *); + result = Curl_setstropt(&data->set.str[STRING_HSTS], argptr); + if(result) + return result; + if(argptr) + (void)Curl_hsts_load(data->hsts, argptr); + break; + case CURLOPT_HSTS_CTRL: + arg = va_arg(param, long); + if(arg & CURLHSTS_ENABLE) { + if(!data->hsts) { + data->hsts = Curl_hsts_init(); + if(!data->hsts) + return CURLE_OUT_OF_MEMORY; + } + } + else + Curl_hsts_cleanup(&data->hsts); + break; +#endif #ifndef CURL_DISABLE_ALTSVC case CURLOPT_ALTSVC: if(!data->asi) { diff --git a/lib/url.c b/lib/url.c index 0176517d9efa16..1e3f025ada9e08 100644 --- a/lib/url.c +++ b/lib/url.c @@ -96,6 +96,7 @@ bool curl_win32_idn_to_ascii(const char *in, char **out); #include "getinfo.h" #include "urlapi-int.h" #include "system_win32.h" +#include "hsts.h" /* And now for the protocols */ #include "ftp.h" @@ -411,6 +412,7 @@ CURLcode Curl_close(struct Curl_easy **datap) Curl_flush_cookies(data, TRUE); Curl_altsvc_save(data, data->asi, data->set.str[STRING_ALTSVC]); Curl_altsvc_cleanup(&data->asi); + Curl_hsts_cleanup(&data->hsts); #if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_CRYPTO_AUTH) Curl_http_auth_cleanup_digest(data); #endif @@ -1911,6 +1913,19 @@ static CURLcode parseurlandfillconn(struct Curl_easy *data, if(uc) return Curl_uc_to_curlcode(uc); + uc = curl_url_get(uh, CURLUPART_HOST, &data->state.up.hostname, 0); + if(uc) { + if(!strcasecompare("file", data->state.up.scheme)) + return CURLE_OUT_OF_MEMORY; + } + +#ifdef USE_HSTS + if(data->hsts && strcasecompare("http", data->state.up.scheme)) { + if(Curl_hsts(data->hsts, data->state.up.hostname, TRUE)) + infof(data, "Switch from HTTP to HTTPS due to HSTS!\n"); + } +#endif + result = findprotocol(data, conn, data->state.up.scheme); if(result) return result; @@ -1956,12 +1971,6 @@ static CURLcode parseurlandfillconn(struct Curl_easy *data, else if(uc != CURLUE_NO_OPTIONS) return Curl_uc_to_curlcode(uc); - uc = curl_url_get(uh, CURLUPART_HOST, &data->state.up.hostname, 0); - if(uc) { - if(!strcasecompare("file", data->state.up.scheme)) - return CURLE_OUT_OF_MEMORY; - } - uc = curl_url_get(uh, CURLUPART_PATH, &data->state.up.path, 0); if(uc) return Curl_uc_to_curlcode(uc); diff --git a/lib/urldata.h b/lib/urldata.h index e8b54aa3072295..ea7060ec56ca38 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -1531,35 +1531,26 @@ enum dupstring { STRING_RTSP_SESSION_ID, /* Session ID to use */ STRING_RTSP_STREAM_URI, /* Stream URI for this request */ STRING_RTSP_TRANSPORT, /* Transport for this session */ - STRING_SSH_PRIVATE_KEY, /* path to the private key file for auth */ STRING_SSH_PUBLIC_KEY, /* path to the public key file for auth */ STRING_SSH_HOST_PUBLIC_KEY_MD5, /* md5 of host public key in ascii hex */ STRING_SSH_KNOWNHOSTS, /* file name of knownhosts file */ - STRING_PROXY_SERVICE_NAME, /* Proxy service name */ STRING_SERVICE_NAME, /* Service name */ STRING_MAIL_FROM, STRING_MAIL_AUTH, - STRING_TLSAUTH_USERNAME_ORIG, /* TLS auth */ STRING_TLSAUTH_USERNAME_PROXY, /* TLS auth */ STRING_TLSAUTH_PASSWORD_ORIG, /* TLS auth */ STRING_TLSAUTH_PASSWORD_PROXY, /* TLS auth */ - STRING_BEARER, /* , if used */ - STRING_UNIX_SOCKET_PATH, /* path to Unix socket, if used */ - STRING_TARGET, /* CURLOPT_REQUEST_TARGET */ STRING_DOH, /* CURLOPT_DOH_URL */ - STRING_ALTSVC, /* CURLOPT_ALTSVC */ - + STRING_HSTS, /* CURLOPT_HSTS */ STRING_SASL_AUTHZID, /* CURLOPT_SASL_AUTHZID */ - STRING_TEMP_URL, /* temp URL storage for proxy use */ - STRING_DNS_SERVERS, STRING_DNS_INTERFACE, STRING_DNS_LOCAL_IP4, @@ -1899,6 +1890,9 @@ struct Curl_easy { NOTE that the 'cookie' field in the UserDefined struct defines if the "engine" is to be used or not. */ +#ifdef USE_HSTS + struct hsts *hsts; +#endif #ifndef CURL_DISABLE_ALTSVC struct altsvcinfo *asi; /* the alt-svc cache */ #endif diff --git a/lib/version.c b/lib/version.c index 70e45665866730..5b759be3834b08 100644 --- a/lib/version.c +++ b/lib/version.c @@ -417,6 +417,9 @@ static curl_version_info_data version_info = { #endif #ifndef CURL_DISABLE_ALTSVC | CURL_VERSION_ALTSVC +#endif +#if defined(USE_HSTS) + | CURL_VERSION_HSTS #endif , NULL, /* ssl_version */ diff --git a/src/tool_cfgable.c b/src/tool_cfgable.c index e99602c4f3f213..3c0bbfa64ba5bb 100644 --- a/src/tool_cfgable.c +++ b/src/tool_cfgable.c @@ -54,6 +54,7 @@ static void free_config_fields(struct OperationConfig *config) Curl_safefree(config->egd_file); Curl_safefree(config->useragent); Curl_safefree(config->altsvc); + Curl_safefree(config->hsts); Curl_safefree(config->cookie); Curl_safefree(config->cookiejar); Curl_safefree(config->cookiefile); diff --git a/src/tool_cfgable.h b/src/tool_cfgable.h index 489f9ca0e43d31..ac4c7fadc49d21 100644 --- a/src/tool_cfgable.h +++ b/src/tool_cfgable.h @@ -58,6 +58,7 @@ struct OperationConfig { char *cookiejar; /* write to this file */ char *cookiefile; /* read from this file */ char *altsvc; /* alt-svc cache file name */ + char *hsts; /* HSTS cache file name */ bool cookiesession; /* new session? */ bool encoding; /* Accept-Encoding please */ bool tr_encoding; /* Transfer-Encoding please */ diff --git a/src/tool_getparam.c b/src/tool_getparam.c index 910a5a2f9a6350..d2e4eb4981ea13 100644 --- a/src/tool_getparam.c +++ b/src/tool_getparam.c @@ -219,6 +219,7 @@ static const struct LongShort aliases[]= { {"A", "user-agent", ARG_STRING}, {"b", "cookie", ARG_STRING}, {"ba", "alt-svc", ARG_STRING}, + {"bb", "hsts", ARG_STRING}, {"B", "use-ascii", ARG_BOOL}, {"c", "cookie-jar", ARG_STRING}, {"C", "continue-at", ARG_STRING}, @@ -1291,6 +1292,12 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */ else return PARAM_LIBCURL_DOESNT_SUPPORT; break; + case 'b': /* --hsts */ + if(curlinfo->features & CURL_VERSION_HSTS) + GetStr(&config->hsts, nextarg); + else + return PARAM_LIBCURL_DOESNT_SUPPORT; + break; default: /* --cookie string coming up: */ if(nextarg[0] == '@') { nextarg++; diff --git a/src/tool_help.c b/src/tool_help.c index 544dbbab08613c..0833a0d23e158e 100644 --- a/src/tool_help.c +++ b/src/tool_help.c @@ -328,6 +328,9 @@ static const struct helptxt helptext[] = { {" --hostpubmd5 ", "Acceptable MD5 hash of the host public key", CURLHELP_SFTP | CURLHELP_SCP}, + {" --hsts ", + "Enable HSTS with this cache file", + CURLHELP_HTTP}, {" --http0.9", "Allow HTTP 0.9 responses", CURLHELP_HTTP}, @@ -862,6 +865,7 @@ static const struct feat feats[] = { {"MultiSSL", CURL_VERSION_MULTI_SSL}, {"PSL", CURL_VERSION_PSL}, {"alt-svc", CURL_VERSION_ALTSVC}, + {"HSTS", CURL_VERSION_HSTS}, }; static void print_category(curlhelp_t category) diff --git a/src/tool_operate.c b/src/tool_operate.c index 4ad5052ffa7e63..e0fde724bd03db 100644 --- a/src/tool_operate.c +++ b/src/tool_operate.c @@ -2072,6 +2072,9 @@ static CURLcode single_transfer(struct GlobalConfig *global, if(config->altsvc) my_setopt_str(curl, CURLOPT_ALTSVC, config->altsvc); + if(config->hsts) + my_setopt_bitmask(curl, CURLOPT_HSTS_CTRL, CURLHSTS_ENABLE); + #ifdef USE_METALINK if(!metalink && config->use_metalink) { outs->metalink_parser = metalink_parser_context_new(); diff --git a/src/tool_setopt.c b/src/tool_setopt.c index 0dd7a57a2d041d..2159db6cdfdd3b 100644 --- a/src/tool_setopt.c +++ b/src/tool_setopt.c @@ -62,6 +62,11 @@ const struct NameValue setopt_nv_CURL_SOCKS_PROXY[] = { NVEND, }; +const struct NameValueUnsigned setopt_nv_CURLHSTS[] = { + NV(CURLHSTS_ENABLE), + NVEND, +}; + const struct NameValueUnsigned setopt_nv_CURLAUTH[] = { NV(CURLAUTH_ANY), /* combination */ NV(CURLAUTH_ANYSAFE), /* combination */ diff --git a/src/tool_setopt.h b/src/tool_setopt.h index 3db88c6bf3f6d4..f8d3320d3fbf18 100644 --- a/src/tool_setopt.h +++ b/src/tool_setopt.h @@ -64,8 +64,10 @@ extern const struct NameValueUnsigned setopt_nv_CURLSSLOPT[]; extern const struct NameValue setopt_nv_CURL_NETRC[]; extern const struct NameValue setopt_nv_CURLPROTO[]; extern const struct NameValueUnsigned setopt_nv_CURLAUTH[]; +extern const struct NameValueUnsigned setopt_nv_CURLHSTS[]; /* Map options to NameValue sets */ +#define setopt_nv_CURLOPT_HSTS_CTRL setopt_nv_CURLHSTS #define setopt_nv_CURLOPT_HTTP_VERSION setopt_nv_CURL_HTTP_VERSION #define setopt_nv_CURLOPT_HTTPAUTH setopt_nv_CURLAUTH #define setopt_nv_CURLOPT_SSLVERSION setopt_nv_CURL_SSLVERSION diff --git a/tests/data/Makefile.inc b/tests/data/Makefile.inc index 2e0c092adb9657..04e23c6fc5bf80 100644 --- a/tests/data/Makefile.inc +++ b/tests/data/Makefile.inc @@ -197,6 +197,7 @@ test1620 test1621 \ test1630 test1631 test1632 test1633 \ \ test1650 test1651 test1652 test1653 test1654 test1655 \ +test1660 \ \ test1700 test1701 test1702 \ \ diff --git a/tests/data/test1660 b/tests/data/test1660 new file mode 100644 index 00000000000000..f64765c3f457d2 --- /dev/null +++ b/tests/data/test1660 @@ -0,0 +1,81 @@ + + + +unittest +HSTS + + + + + +none + + +unittest +HSTS + + + +# Your HSTS cache. https://curl.haxx.se/docs/hsts.html +# This file was generated by libcurl! Edit at your own risk. +.readfrom.example "20211001 04:47:41" +.old.example "20161001 04:47:41" + + +# This date is exactly "20190124 22:34:21" UTC + +CURL_TIME=1548369261 + + +HSTS + + +- + + + + + +readfrom.example [readfrom.example]: 1633063661 includeSubDomains +'old.example' is not HSTS +'readfrom.example' is not HSTS +example.com [example.com]: 1579905261 +example.com [example.com]: 1569905261 +example.com [example.com]: 1569905261 +example.com [example.com]: 1569905261 includeSubDomains +example.org [example.org]: 1579905261 +Input 8: error 43 +Input 9: error 43 +this.example [this.example]: 1548400797 +'this.example' is not HSTS +Input 12: error 43 +Input 13: error 43 +Input 14: error 43 +3.example.com [example.com]: 1569905261 includeSubDomains +3.example.com [example.com]: 1569905261 includeSubDomains +foo.example.com [example.com]: 1569905261 includeSubDomains +'foo.xample.com' is not HSTS +'forexample.net' is not HSTS +'forexample.net' is not HSTS +'example.net' is not HSTS +expire.example [expire.example]: 1548369268 +Number of entries: 3 +expire.example [expire.example]: 1548369268 +expire.example [expire.example]: 1548369268 +expire.example [expire.example]: 1548369268 +expire.example [expire.example]: 1548369268 +expire.example [expire.example]: 1548369268 +expire.example [expire.example]: 1548369268 +expire.example [expire.example]: 1548369268 +'expire.example' is not HSTS +'expire.example' is not HSTS +'expire.example' is not HSTS + + +# Your HSTS cache. https://curl.haxx.se/docs/hsts.html +# This file was generated by libcurl! Edit at your own risk. +.example.com "20191001 04:47:41" +example.org "20200124 22:34:21" + + + diff --git a/tests/runtests.pl b/tests/runtests.pl index 4bcf61bbf012bb..a4e3306619babd 100755 --- a/tests/runtests.pl +++ b/tests/runtests.pl @@ -256,6 +256,7 @@ BEGIN my $has_threadedres;# set if built with threaded resolver my $has_psl; # set if libcurl is built with PSL support my $has_altsvc; # set if libcurl is built with alt-svc support +my $has_hsts; # set if libcurl is built with HSTS support my $has_ldpreload; # set if curl is built for systems supporting LD_PRELOAD my $has_multissl; # set if curl is build with MultiSSL support my $has_manual; # set if curl is built with built-in manual @@ -2762,6 +2763,7 @@ sub compare { sub setupfeatures { $feature{"alt-svc"} = $has_altsvc; + $feature{"HSTS"} = $has_hsts; $feature{"brotli"} = $has_brotli; $feature{"crypto"} = $has_crypto; $feature{"debug"} = $debug_build; @@ -3035,6 +3037,9 @@ sub checksystem { # alt-svc enabled $has_altsvc=1; } + if($feat =~ /HSTS/i) { + $has_hsts=1; + } if($feat =~ /AsynchDNS/i) { if(!$has_cares) { # this means threaded resolver diff --git a/tests/unit/Makefile.inc b/tests/unit/Makefile.inc index f63724f913ca1c..ee6816823981b9 100644 --- a/tests/unit/Makefile.inc +++ b/tests/unit/Makefile.inc @@ -34,7 +34,8 @@ UNITPROGS = unit1300 unit1301 unit1302 unit1303 unit1304 unit1305 unit1307 \ unit1600 unit1601 unit1602 unit1603 unit1604 unit1605 unit1606 unit1607 \ unit1608 unit1609 unit1610 unit1611 unit1612 \ unit1620 unit1621 \ - unit1650 unit1651 unit1652 unit1653 unit1654 unit1655 + unit1650 unit1651 unit1652 unit1653 unit1654 unit1655 \ + unit1660 unit1300_SOURCES = unit1300.c $(UNITFILES) unit1300_CPPFLAGS = $(AM_CPPFLAGS) @@ -154,3 +155,5 @@ unit1654_CPPFLAGS = $(AM_CPPFLAGS) unit1655_SOURCES = unit1655.c $(UNITFILES) unit1655_CPPFLAGS = $(AM_CPPFLAGS) +unit1660_SOURCES = unit1660.c $(UNITFILES) +unit1660_CPPFLAGS = $(AM_CPPFLAGS) diff --git a/tests/unit/unit1660.c b/tests/unit/unit1660.c new file mode 100644 index 00000000000000..1687cafa19fc46 --- /dev/null +++ b/tests/unit/unit1660.c @@ -0,0 +1,172 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) 2020, 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.haxx.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. + * + ***************************************************************************/ +#include "curlcheck.h" + +#include "urldata.h" +#include "hsts.h" + +static CURLcode +unit_setup(void) +{ + return CURLE_OK; +} + +static void +unit_stop(void) +{ + curl_global_cleanup(); +} + +#if defined(CURL_DISABLE_HTTP) || !defined(USE_HSTS) +UNITTEST_START +{ + return 0; /* nothing to do when HTTP or HSTS are disabled */ +} +UNITTEST_STOP +#else + +struct testit { + const char *host; + const char *chost; /* if non-NULL, use to lookup with */ + const char *hdr; /* if NULL, just do the lookup */ + const CURLcode result; /* parse result */ +}; + +static const struct testit headers[] = { + /* two entries read from disk cache, verify first */ + { "-", "readfrom.example", NULL, CURLE_OK}, + { "-", "old.example", NULL, CURLE_OK}, + /* delete the remaining one read from disk */ + { "readfrom.example", NULL, "max-age=\"0\"", CURLE_OK}, + + { "example.com", NULL, "max-age=\"31536000\"\r\n", CURLE_OK }, + { "example.com", NULL, "max-age=\"21536000\"\r\n", CURLE_OK }, + { "example.com", NULL, "max-age=\"21536000\"; \r\n", CURLE_OK }, + { "example.com", NULL, "max-age=\"21536000\"; includeSubDomains\r\n", + CURLE_OK }, + { "example.org", NULL, "max-age=\"31536000\"\r\n", CURLE_OK }, + { "this.example", NULL, "max=\"31536\";", CURLE_BAD_FUNCTION_ARGUMENT }, + { "this.example", NULL, "max-age=\"31536", CURLE_BAD_FUNCTION_ARGUMENT }, + { "this.example", NULL, "max-age=31536\"", CURLE_OK }, + /* max-age=0 removes the entry */ + { "this.example", NULL, "max-age=0", CURLE_OK }, + { "another.example", NULL, "includeSubDomains; ", + CURLE_BAD_FUNCTION_ARGUMENT }, + + /* Two max-age is illegal */ + { "example.com", NULL, + "max-age=\"21536000\"; includeSubDomains; max-age=\"3\";", + CURLE_BAD_FUNCTION_ARGUMENT }, + /* Two includeSubDomains is illegal */ + { "2.example.com", NULL, + "max-age=\"21536000\"; includeSubDomains; includeSubDomains;", + CURLE_BAD_FUNCTION_ARGUMENT }, + /* use a unknown directive "include" that should be ignored */ + { "3.example.com", NULL, "max-age=\"21536000\"; include; includeSubDomains;", + CURLE_OK }, + /* remove the "3.example.com" one, should still match the example.com */ + { "3.example.com", NULL, "max-age=\"0\"; includeSubDomains;", + CURLE_OK }, + { "-", "foo.example.com", NULL, CURLE_OK}, + { "-", "foo.xample.com", NULL, CURLE_OK}, + + /* should not match */ + { "example.net", "forexample.net", "max-age=\"31536000\"\r\n", CURLE_OK }, + + /* should not match either, since forexample.net is not in the example.net + domain */ + { "example.net", "forexample.net", + "max-age=\"31536000\"; includeSubDomains\r\n", CURLE_OK }, + /* remove example.net again */ + { "example.net", NULL, "max-age=\"0\"; includeSubDomains\r\n", CURLE_OK }, + + /* make this live for 7 seconds */ + { "expire.example", NULL, "max-age=\"7\"\r\n", CURLE_OK }, + { NULL, NULL, NULL, 0 } +}; + +static void showsts(struct stsentry *e, const char *chost) +{ + if(!e) + printf("'%s' is not HSTS\n", chost); + else { + printf("%s [%s]: %" CURL_FORMAT_CURL_OFF_T "%s\n", + chost, e->host, e->expires, + e->includeSubDomains ? " includeSubDomains" : ""); + } +} + +UNITTEST_START +{ + CURLcode result; + struct stsentry *e; + struct hsts *h = Curl_hsts_init(); + int i; + const char *chost; + CURL *easy; + if(!h) + return 1; + + Curl_hsts_load(h, "log/input1660"); + + for(i = 0; headers[i].host ; i++) { + if(headers[i].hdr) { + result = Curl_hsts_parse(h, headers[i].host, headers[i].hdr); + + if(result != headers[i].result) { + fprintf(stderr, "Curl_hsts_parse(%s) failed: %d\n", + headers[i].hdr, result); + unitfail++; + continue; + } + else if(result) { + printf("Input %u: error %d\n", i, (int) result); + continue; + } + } + + chost = headers[i].chost ? headers[i].chost : headers[i].host; + e = Curl_hsts(h, chost, TRUE); + showsts(e, chost); + } + + printf("Number of entries: %d\n", h->list.size); + + /* verify that it is exists for 7 seconds */ + chost = "expire.example"; + for(i = 100; i < 110; i++) { + e = Curl_hsts(h, chost, TRUE); + showsts(e, chost); + deltatime++; /* another second passed */ + } + + easy = curl_easy_init(); + if(easy) { + (void)Curl_hsts_save(easy, h, "log/hsts1660"); + curl_easy_cleanup(easy); + } + + Curl_hsts_cleanup(&h); + return unitfail; +} +UNITTEST_STOP +#endif