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