summaryrefslogtreecommitdiff
path: root/lib/hsts.c
diff options
context:
space:
mode:
authorDaniel Stenberg <daniel@haxx.se>2020-11-02 23:17:01 +0100
committerDaniel Stenberg <daniel@haxx.se>2020-11-03 16:08:42 +0100
commit7385610d0c74c6a254fea5e4cd6e1d559d848c8c (patch)
tree3b572bcf972062b7cc1315ac23fdb547e7216463 /lib/hsts.c
parent9f43b28f783cc8f7464492a0b5b9dd35c1625fde (diff)
downloadcurl-7385610d0c74c6a254fea5e4cd6e1d559d848c8c.tar.gz
hsts: add support for Strict-Transport-Security
- enable in the build (configure) - header parsing - host name lookup - unit tests for the above - CI build - CURL_VERSION_HSTS bit - curl_version_info support - curl -V output - curl-config --features - CURLOPT_HSTS_CTRL - man page for CURLOPT_HSTS_CTRL - curl --hsts (sets CURLOPT_HSTS_CTRL and works with --libcurl) - man page for --hsts - save cache to disk - load cache from disk - CURLOPT_HSTS - man page for CURLOPT_HSTS - added docs/HSTS.md - fixed --version docs - adjusted curl_easy_duphandle Closes #5896
Diffstat (limited to 'lib/hsts.c')
-rw-r--r--lib/hsts.c430
1 files changed, 430 insertions, 0 deletions
diff --git a/lib/hsts.c b/lib/hsts.c
new file mode 100644
index 000000000..7eb3cda03
--- /dev/null
+++ b/lib/hsts.c
@@ -0,0 +1,430 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) 2020, Daniel Stenberg, <daniel@haxx.se>, 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 <curl/curl.h>
+#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 */