From 65f5c86d658a80933a9e9f7a2ceea238ceb7e477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Stenberg?= <bjorn@haxx.se> Date: Mon, 10 Jun 2019 15:44:42 +0200 Subject: [PATCH] HSTS: support for HTTP Strict Transport Security using libhsts This patch adds two new configure parameters: --with-libhsts=PATH to point to libhsts --with-hsts-file=FILE to specify location of the dafsa file that contains the domain database --- configure.ac | 99 ++++++++++++++++++++++++++++++++ docs/libcurl/curl_version_info.3 | 3 + docs/libcurl/symbols-in-versions | 1 + include/curl/curl.h | 1 + lib/url.c | 49 +++++++++++++++- lib/urldata.h | 7 +++ lib/version.c | 8 +++ src/tool_help.c | 1 + tests/data/Makefile.inc | 4 +- tests/data/hsts.dafsa | 2 + tests/data/hsts.json | 9 +++ tests/data/test2077 | 46 +++++++++++++++ tests/data/test2078 | 45 +++++++++++++++ tests/runtests.pl | 8 +++ 14 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 tests/data/hsts.dafsa create mode 100644 tests/data/hsts.json create mode 100644 tests/data/test2077 create mode 100644 tests/data/test2078 diff --git a/configure.ac b/configure.ac index 1a2e237d4f..fd1094740f 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 07cdf0c47b..41c6c7a66c 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 715badf971..ef3d988361 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 e7f812daca..70209ee6f0 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 c37ce04947..2bed43f247 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 <libhsts.h> +#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 fdc185b228..22c1f9bc91 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -315,6 +315,10 @@ typedef enum { #include <iconv.h> #endif +#ifdef USE_HSTS +#include <libhsts.h> +#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 14b0531d37..958d134c7c 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 9209a13dd5..4ce11e1191 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 0c52173651..e0a998291d 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 0000000000..447f9540a8 --- /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 0000000000..f506f89534 --- /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 0000000000..c9f7e6f1d1 --- /dev/null +++ b/tests/data/test2077 @@ -0,0 +1,46 @@ +<testcase> +<info> +<keywords> +HSTS +HTTP +</keywords> +</info> + +# Client-side +<client> +<features> +HSTS +debug +</features> +<server> +http +http-proxy +</server> +<name> +Check that HSTS does not upgrade domain without record +</name> +<setenv> +CURL_HSTSFILE=data/hsts.dafsa +</setenv> +<command> +-x %HOSTIP:%PROXYPORT http://nohstsdomain.fake/ +</command> +</client> + +# Verify data after the test has been "shot" +<verify> +<strip> +^User-Agent:.* +</strip> +<proxy> +GET http://nohstsdomain.fake/ HTTP/1.1 +Host: nohstsdomain.fake +Accept: */* +Proxy-Connection: Keep-Alive + +</proxy> +<errorcode> +0 +</errorcode> +</verify> +</testcase> diff --git a/tests/data/test2078 b/tests/data/test2078 new file mode 100644 index 0000000000..569ac02f0f --- /dev/null +++ b/tests/data/test2078 @@ -0,0 +1,45 @@ +<testcase> +<info> +<keywords> +HSTS +HTTPS +</keywords> +</info> + +# Client-side +<client> +<features> +HSTS +debug +</features> +<server> +https +http-proxy +</server> +<name> +Check that HSTS upgrades http to https +</name> +<setenv> +CURL_HSTSFILE=data/hsts.dafsa +</setenv> +<command> +-x %HOSTIP:%PROXYPORT http://hstsdomain.fake/ +</command> +</client> + +# Verify data after the test has been "shot" +<verify> +<strip> +^User-Agent:.* +</strip> +<proxy> +CONNECT hstsdomain.fake:443 HTTP/1.1 +Host: hstsdomain.fake:443 +Proxy-Connection: Keep-Alive + +</proxy> +<errorcode> +56 +</errorcode> +</verify> +</testcase> diff --git a/tests/runtests.pl b/tests/runtests.pl index a6e1adde36..618823317e 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; -- GitLab