diff --git a/configure.ac b/configure.ac index 1a2e237d4fe7dd3e8239d355bc943e21be2e124b..fd1094740f25cedcd7c2f44038edeffd46fe7982 100755 --- a/configure.ac +++ b/configure.ac @@ -175,6 +175,7 @@ curl_verbose_msg="enabled (--disable-verbose)" curl_rtmp_msg="no (--with-librtmp)" curl_mtlnk_msg="no (--with-libmetalink)" curl_psl_msg="no (--with-libpsl)" + curl_hsts_msg="no (--with-libhsts)" ssl_backends= @@ -3341,6 +3342,102 @@ if test X"$want_h2" != Xno; then fi +dnl ********************************************************************** +dnl Check for libhsts +dnl ********************************************************************** +dnl libhsts project home page: https://gitlab.com/rockdaboot/libhsts +OPT_HSTS=off +AC_ARG_WITH(libhsts,dnl +AC_HELP_STRING([--with-libhsts=PATH],[Where to look for libhsts, PATH points to the libhsts installation; when possible, set the PKG_CONFIG_PATH environment variable instead of using this option]) +AC_HELP_STRING([--without-libhsts], [disable HSTS]), + OPT_HSTS=$withval) + +if test X"$OPT_HSTS" != Xno; then + dnl backup the pre-hsts variables + CLEANLDFLAGS="$LDFLAGS" + CLEANCPPFLAGS="$CPPFLAGS" + CLEANLIBS="$LIBS" + + case "$OPT_HSTS" in + yes) + dnl --with-hsts (without path) used + CURL_CHECK_PKGCONFIG(libhsts) + + if test "$PKGCONFIG" != "no" ; then + LIB_HSTS=`$PKGCONFIG --libs-only-l libhsts` + LD_HSTS=`$PKGCONFIG --libs-only-L libhsts` + CPP_HSTS=`$PKGCONFIG --cflags-only-I libhsts` + version=`$PKGCONFIG --modversion libhsts` + DIR_HSTS=`echo $LD_HSTS | $SED -e 's/-L//'` + fi + + ;; + off) + dnl no --with-hsts option given, just check default places + ;; + *) + dnl use the given --with-hsts spot + PREFIX_HSTS=$OPT_HSTS + ;; + esac + + dnl if given with a prefix, we set -L and -I based on that + if test -n "$PREFIX_HSTS"; then + LIB_HSTS="-lhsts" + LD_HSTS=-L${PREFIX_HSTS}/lib$libsuff + CPP_HSTS=-I${PREFIX_HSTS}/include + DIR_HSTS=${PREFIX_HSTS}/lib$libsuff + fi + + LDFLAGS="$LDFLAGS $LD_HSTS" + CPPFLAGS="$CPPFLAGS $CPP_HSTS" + LIBS="$LIB_HSTS $LIBS" + + AC_CHECK_LIB(hsts, hsts_search) + + AC_CHECK_HEADERS(libhsts.h, + curl_hsts_msg="enabled (libhsts)" + HAVE_HSTS=1 + AC_DEFINE(USE_HSTS, 1, [if HSTS is in use]) + AC_SUBST(USE_HSTS, [1]) + ) + + if test X"$OPT_HSTS" != Xoff && + test "$HAVE_HSTS" != "1"; then + AC_MSG_ERROR([HSTS libs and/or directories were not found where specified!]) + fi + + if test "$HAVE_HSTS" = "1"; then + if test -n "$DIR_HSTS"; then + dnl when the hsts shared libs were found in a path that the run-time + dnl linker doesn't search through, we need to add it to LD_LIBRARY_PATH + dnl to prevent further configure tests to fail due to this + + if test "x$cross_compiling" != "xyes"; then + LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$DIR_HSTS" + export LD_LIBRARY_PATH + AC_MSG_NOTICE([Added $DIR_HSTS to LD_LIBRARY_PATH]) + fi + fi + + AC_ARG_WITH(hsts-file, + AC_HELP_STRING([--with-hsts-file=FILE], + [Path to hsts.dafsa file]), + [ HSTS_FILE="$withval" ] ) + if test -n "$HSTS_FILE" ; then + AC_DEFINE_UNQUOTED(HSTS_FILE, "$HSTS_FILE", + [Path to hsts.dafsa file] ) + else + AC_MSG_ERROR([When enabling HSTS, you also need to specify --with-hsts-file pointing to the hsts.dafsa file]) + fi + else + dnl no hsts, revert back to clean variables + LDFLAGS=$CLEANLDFLAGS + CPPFLAGS=$CLEANCPPFLAGS + LIBS=$CLEANLIBS + fi +fi + dnl ********************************************************************** dnl Check for zsh completion path dnl ********************************************************************** @@ -4350,6 +4447,8 @@ AC_MSG_NOTICE([Configured to build curl/libcurl: PSL: ${curl_psl_msg} Alt-svc: ${curl_altsvc_msg} HTTP2: ${curl_h2_msg} + HSTS: ${curl_hsts_msg} (using ${HSTS_FILE}) + HSTS : ${curl_hsts_msg} (using ${HSTS_FILE}) Protocols: ${SUPPORT_PROTOCOLS} Features: ${SUPPORT_FEATURES} ]) diff --git a/docs/libcurl/curl_version_info.3 b/docs/libcurl/curl_version_info.3 index 07cdf0c47bc002d92738f9fde32543304c33e8d1..41c6c7a66c7f1e563cc17a6582475cbf32fdde83 100644 --- a/docs/libcurl/curl_version_info.3 +++ b/docs/libcurl/curl_version_info.3 @@ -149,6 +149,9 @@ libcurl was built with support for TLS-SRP. (Added in 7.21.4) .IP CURL_VERSION_NTLM_WB libcurl was built with support for NTLM delegation to a winbind helper. (Added in 7.22.0) +.IP CURL_VERSION_HSTS +libcurl was built with support for HSTS +(Added in 7.62.0) .IP CURL_VERSION_HTTP2 libcurl was built with support for HTTP2. (Added in 7.33.0) diff --git a/docs/libcurl/symbols-in-versions b/docs/libcurl/symbols-in-versions index 715badf9718114e8850ca3f3bfdcafd7158eb1f3..ef3d98836111ba33fafce2a8ad9fa6e535e22ada 100644 --- a/docs/libcurl/symbols-in-versions +++ b/docs/libcurl/symbols-in-versions @@ -922,6 +922,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.62.0 CURL_VERSION_HTTP2 7.33.0 CURL_VERSION_HTTPS_PROXY 7.52.0 CURL_VERSION_IDN 7.12.0 diff --git a/include/curl/curl.h b/include/curl/curl.h index e7f812daca50d03a5d9f443737bb58df1f6ba175..70209ee6f08e4920b0f3435dd9163483465015da 100644 --- a/include/curl/curl.h +++ b/include/curl/curl.h @@ -2787,6 +2787,7 @@ typedef struct { #define CURL_VERSION_MULTI_SSL (1<<22) /* Multiple SSL backends available */ #define CURL_VERSION_BROTLI (1<<23) /* Brotli features are present. */ #define CURL_VERSION_ALTSVC (1<<24) /* Alt-Svc handling built-in */ +#define CURL_VERSION_HSTS (1<<25) /* HSTS features are present */ /* * NAME curl_version_info() diff --git a/lib/url.c b/lib/url.c index c37ce04947c010b1a795af0bd06aad6aef130498..2bed43f2474a0210dad2c8937f4541b7a3e3eddf 100644 --- a/lib/url.c +++ b/lib/url.c @@ -122,6 +122,9 @@ bool curl_win32_idn_to_ascii(const char *in, char **out); #include "strdup.h" #include "setopt.h" #include "altsvc.h" +#ifdef USE_HSTS +#include +#endif /* The last 3 #include files should be in this order */ #include "curl_printf.h" @@ -291,6 +294,10 @@ void Curl_freeset(struct Curl_easy *data) data->change.url = NULL; Curl_mime_cleanpart(&data->set.mimepost); + +#ifdef USE_HSTS + hsts_free(data->set.hsts); +#endif } /* free the URL pieces */ @@ -617,6 +624,27 @@ CURLcode Curl_open(struct Curl_easy **curl) Curl_http2_init_state(&data->state); } + +#ifdef USE_HSTS + { + hsts_status_t hsts_status = HSTS_ERR_INPUT_FAILURE; +#if DEBUGBUILD + char *debug_hsts_file = curl_getenv("CURL_HSTSFILE"); + if(debug_hsts_file) { + DEBUGF(fprintf(stderr, "DEBUG: HSTS file override: %s\n", + debug_hsts_file)); + hsts_status = hsts_load_file(debug_hsts_file, &data->set.hsts); + free(debug_hsts_file); + } + else +#endif + hsts_status = hsts_load_file(HSTS_FILE, &data->set.hsts); + if(hsts_status < HSTS_SUCCESS) { + fprintf(stderr, "Failed loading HSTS database, error code %d!\n", + hsts_status); + } + } +#endif } if(result) { @@ -1821,22 +1849,37 @@ const struct Curl_handler *Curl_builtin_scheme(const char *scheme) { const struct Curl_handler * const *pp; const struct Curl_handler *p; - /* Scan protocol handler table and match against 'scheme'. The handler may - be changed later when the protocol specific setup function is called. */ + + /* Scan protocol handler table and match against 'protostr' to set a few + variables based on the URL. Now that the handler may be changed later + when the protocol specific setup function is called. */ for(pp = protocols; (p = *pp) != NULL; pp++) if(strcasecompare(p->scheme, scheme)) /* Protocol found in table. Check if allowed */ return p; + return NULL; /* not found */ } - static CURLcode findprotocol(struct Curl_easy *data, struct connectdata *conn, const char *protostr) { const struct Curl_handler *p = Curl_builtin_scheme(protostr); +#ifdef USE_HSTS + /* HSTS means we override any http access with https if the domain + is listed in the HSTS database */ + if(data->set.hsts && strcasecompare(protostr, "http")) { + hsts_status_t hsts_status = hsts_search(data->set.hsts, + conn->host.name, 0, NULL); + if(hsts_status == HSTS_SUCCESS) { + infof(data, "Domain found in HSTS database, upgrading to https\n"); + p = Curl_builtin_scheme("https"); + } + } +#endif + if(p && /* Protocol found in table. Check if allowed */ (data->set.allowed_protocols & p->protocol)) { diff --git a/lib/urldata.h b/lib/urldata.h index fdc185b2289261f1554b28182855a87112e9b6a0..22c1f9bc91e8618670fdf0ceeea0cb30e720add1 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -315,6 +315,10 @@ typedef enum { #include #endif +#ifdef USE_HSTS +#include +#endif + /* Struct used for GSSAPI (Kerberos V5) authentication */ #if defined(USE_KERBEROS5) struct kerberos5data { @@ -1750,6 +1754,9 @@ struct UserDefined { bit doh:1; /* DNS-over-HTTPS enabled */ bit doh_get:1; /* use GET for DoH requests, instead of POST */ bit http09_allowed:1; /* allow HTTP/0.9 responses */ +#ifdef USE_HSTS + hsts_t *hsts; /* libhsts handle */ +#endif }; struct Names { diff --git a/lib/version.c b/lib/version.c index 14b0531d37caf19ba52670f42a014ab115155a5e..958d134c7cd03ef1a885b8bd41e463bae039adb3 100644 --- a/lib/version.c +++ b/lib/version.c @@ -211,6 +211,11 @@ char *curl_version(void) */ } #endif +#ifdef USE_HSTS + len = msnprintf(ptr, left, " libhsts/%s", hsts_get_version()); + left -= len; + ptr += len; +#endif /* Silent scan-build even if librtmp is not enabled. */ (void) left; @@ -372,6 +377,9 @@ static curl_version_info_data version_info = { #endif #if defined(USE_ALTSVC) | CURL_VERSION_ALTSVC +#endif +#ifdef USE_HSTS + | CURL_VERSION_HSTS #endif , NULL, /* ssl_version */ diff --git a/src/tool_help.c b/src/tool_help.c index 9209a13dd500592d8095c3cbd79bf9d35c09ee9f..4ce11e1191e4adc5a19f9a589f2847c9e72b1f1e 100644 --- a/src/tool_help.c +++ b/src/tool_help.c @@ -531,6 +531,7 @@ static const struct feat feats[] = { {"MultiSSL", CURL_VERSION_MULTI_SSL}, {"PSL", CURL_VERSION_PSL}, {"alt-svc", CURL_VERSION_ALTSVC}, + {"HSTS", CURL_VERSION_HSTS}, }; void tool_help(void) diff --git a/tests/data/Makefile.inc b/tests/data/Makefile.inc index 0c52173651dab8043e15430dc89bad3ee4dfd56c..e0a998291d04552bfba25c10c95ee6b436c44128 100644 --- a/tests/data/Makefile.inc +++ b/tests/data/Makefile.inc @@ -200,8 +200,10 @@ test2040 test2041 test2042 test2043 test2044 test2045 test2046 test2047 \ test2048 test2049 test2050 test2051 test2052 test2053 test2054 test2055 \ test2056 test2057 test2058 test2059 test2060 test2061 test2062 test2063 \ test2064 test2065 test2066 test2067 test2068 test2069 \ - test2071 test2072 test2073 test2074 test2075 test2076 \ + test2071 test2072 test2073 test2074 test2075 test2076 test2077 \ +test2078 \ test2080 \ +\ test2100 \ \ test3000 test3001 diff --git a/tests/data/hsts.dafsa b/tests/data/hsts.dafsa new file mode 100644 index 0000000000000000000000000000000000000000..447f9540a8f786e5fb9928be8b20955e8d9a6d7f --- /dev/null +++ b/tests/data/hsts.dafsa @@ -0,0 +1,2 @@ +.DAFSA@HSTS_0 +hstsdomain.fake \ No newline at end of file diff --git a/tests/data/hsts.json b/tests/data/hsts.json new file mode 100644 index 0000000000000000000000000000000000000000..f506f89534ba524eb5f6609063ca8790e43e8152 --- /dev/null +++ b/tests/data/hsts.json @@ -0,0 +1,9 @@ +{ + "entries": [ + { "name": "hstsdomain.fake", + "policy": "test", + "include_subdomains": true, + "mode": "force-https" + } + ] +} diff --git a/tests/data/test2077 b/tests/data/test2077 new file mode 100644 index 0000000000000000000000000000000000000000..c9f7e6f1d1e34f4b3473f88ec75a139ca8c9c381 --- /dev/null +++ b/tests/data/test2077 @@ -0,0 +1,46 @@ + + + +HSTS +HTTP + + + +# Client-side + + +HSTS +debug + + +http +http-proxy + + +Check that HSTS does not upgrade domain without record + + +CURL_HSTSFILE=data/hsts.dafsa + + +-x %HOSTIP:%PROXYPORT http://nohstsdomain.fake/ + + + +# Verify data after the test has been "shot" + + +^User-Agent:.* + + +GET http://nohstsdomain.fake/ HTTP/1.1 +Host: nohstsdomain.fake +Accept: */* +Proxy-Connection: Keep-Alive + + + +0 + + + diff --git a/tests/data/test2078 b/tests/data/test2078 new file mode 100644 index 0000000000000000000000000000000000000000..569ac02f0f69b90362720cbca6e4fe18cd57962d --- /dev/null +++ b/tests/data/test2078 @@ -0,0 +1,45 @@ + + + +HSTS +HTTPS + + + +# Client-side + + +HSTS +debug + + +https +http-proxy + + +Check that HSTS upgrades http to https + + +CURL_HSTSFILE=data/hsts.dafsa + + +-x %HOSTIP:%PROXYPORT http://hstsdomain.fake/ + + + +# Verify data after the test has been "shot" + + +^User-Agent:.* + + +CONNECT hstsdomain.fake:443 HTTP/1.1 +Host: hstsdomain.fake:443 +Proxy-Connection: Keep-Alive + + + +56 + + + diff --git a/tests/runtests.pl b/tests/runtests.pl index a6e1adde36f6ddd3cf584839dccab0c198cdc4b6..618823317e1919538fea2291ab7bc842d3d79dbf 100755 --- a/tests/runtests.pl +++ b/tests/runtests.pl @@ -237,6 +237,7 @@ my $has_altsvc; # set if libcurl is built with alt-svc 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 +my $has_hsts; # set if curl is built with HSTS support # this version is decided by the particular nghttp2 library that is being used my $h2cver = "h2c"; @@ -2774,6 +2775,10 @@ sub checksystem { push @protocols, 'http/2'; } + if($feat =~ /HSTS/) { + # HSTS enabled + $has_hsts=1; + } } # # Test harness currently uses a non-stunnel server in order to @@ -3326,6 +3331,9 @@ sub singletest { elsif($1 eq "unix-sockets") { next if $has_unix; } + elsif($1 eq "HSTS") { + next if $has_hsts; + } # See if this "feature" is in the list of supported protocols elsif (grep /^\Q$1\E$/i, @protocols) { next;