aboutsummaryrefslogtreecommitdiff
path: root/src/hsts.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/hsts.c')
-rw-r--r--src/hsts.c360
1 files changed, 360 insertions, 0 deletions
diff --git a/src/hsts.c b/src/hsts.c
new file mode 100644
index 00000000..5874e44f
--- /dev/null
+++ b/src/hsts.c
@@ -0,0 +1,360 @@
+/*
+ * File: hsts.c
+ * HTTP Strict Transport Security
+ *
+ * Copyright 2015 corvid
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ */
+
+/* To preload hosts, as of 2015, chromium is the list keeper:
+ * https://src.chromium.org/viewvc/chrome/trunk/src/net/http/transport_security_state_static.json
+ * although mozilla's is easier to work from (and they trim it based on
+ * criteria such as max-age must be at least some number of months)
+ * https://mxr.mozilla.org/mozilla-central/source/security/manager/ssl/nsSTSPreloadList.inc?raw=1
+ */
+
+#include <time.h>
+#include <errno.h>
+#include <limits.h> /* for INT_MAX */
+#include <ctype.h> /* for isspace */
+#include <stdlib.h> /* for strtol */
+
+#include "hsts.h"
+#include "msg.h"
+#include "../dlib/dlib.h"
+#include "IO/tls.h"
+
+typedef struct {
+ char *host;
+ time_t expires_at;
+ bool_t subdomains;
+} HstsData_t;
+
+/* When there is difficulty in representing future dates, use the (by far)
+ * most likely latest representable time of January 19, 2038.
+ */
+static time_t hsts_latest_representable_time;
+static Dlist *domains;
+
+static void Hsts_free_policy(HstsData_t *p)
+{
+ dFree(p->host);
+ dFree(p);
+}
+
+void a_Hsts_freeall()
+{
+ HstsData_t *policy;
+ int i, n = dList_length(domains);
+
+ for (i = 0; i < n; i++) {
+ policy = dList_nth_data(domains, i);
+ Hsts_free_policy(policy);
+ }
+ dList_free(domains);
+}
+
+/*
+ * Compare function for searching a domain node by domain string
+ */
+static int Domain_node_domain_str_cmp(const void *v1, const void *v2)
+{
+ const HstsData_t *node = v1;
+ const char *host = v2;
+
+ return dStrAsciiCasecmp(node->host, host);
+}
+
+static HstsData_t *Hsts_get_policy(const char *host)
+{
+ return dList_find_sorted(domains, host, Domain_node_domain_str_cmp);
+}
+
+static void Hsts_remove_policy(HstsData_t *policy)
+{
+ if (policy) {
+ _MSG("HSTS: removed policy for %s\n", policy->host);
+ Hsts_free_policy(policy);
+ dList_remove(domains, policy);
+ }
+}
+
+/*
+ * Return the time_t for a future time.
+ */
+static time_t Hsts_future_time(long seconds_from_now)
+{
+ time_t ret, now = time(NULL);
+ struct tm *tm = gmtime(&now);
+
+ if (seconds_from_now > INT_MAX - tm->tm_sec)
+ tm->tm_sec = INT_MAX;
+ else
+ tm->tm_sec += seconds_from_now;
+
+ ret = mktime(tm);
+ if (ret == (time_t) -1)
+ ret = hsts_latest_representable_time;
+
+ return ret;
+}
+
+/*
+ * Compare function for searching domains.
+ */
+static int Domain_node_cmp(const void *v1, const void *v2)
+{
+ const HstsData_t *node1 = v1, *node2 = v2;
+
+ return dStrAsciiCasecmp(node1->host, node2->host);
+}
+
+static void Hsts_set_policy(const char *host, long max_age, bool_t subdomains)
+{
+ time_t exp = Hsts_future_time(max_age);
+ HstsData_t *policy = Hsts_get_policy(host);
+
+ _MSG("HSTS: %s %s%s: until %s", (policy ? "modify" : "add"), host,
+ (subdomains ? " and subdomains" : ""), ctime(&exp));
+
+ if (policy == NULL) {
+ policy = dNew0(HstsData_t, 1);
+ policy->host = dStrdup(host);
+ dList_insert_sorted(domains, policy, Domain_node_cmp);
+ }
+ policy->subdomains = subdomains;
+ policy->expires_at = exp;
+}
+
+/*
+ * Read the next attribute.
+ */
+static char *Hsts_parse_attr(const char **header_str)
+{
+ const char *str;
+ uint_t len;
+
+ while (dIsspace(**header_str))
+ (*header_str)++;
+
+ str = *header_str;
+ /* find '=' at end of attr, ';' after attr/val pair, '\0' end of string */
+ len = strcspn(str, "=;");
+ *header_str += len;
+
+ while (len && (str[len - 1] == ' ' || str[len - 1] == '\t'))
+ len--;
+ return dStrndup(str, len);
+}
+
+/*
+ * Get the value in *header_str.
+ */
+static char *Hsts_parse_value(const char **header_str)
+{
+ uint_t len;
+ const char *str;
+
+ if (**header_str == '=') {
+ (*header_str)++;
+ while (dIsspace(**header_str))
+ (*header_str)++;
+
+ str = *header_str;
+ /* finds ';' after attr/val pair or '\0' at end of string */
+ len = strcspn(str, ";");
+ *header_str += len;
+
+ while (len && (str[len - 1] == ' ' || str[len - 1] == '\t'))
+ len--;
+ } else {
+ str = *header_str;
+ len = 0;
+ }
+ return dStrndup(str, len);
+}
+
+/*
+ * Advance past any value.
+ */
+static void Hsts_eat_value(const char **str)
+{
+ if (**str == '=')
+ *str += strcspn(*str, ";");
+}
+
+/*
+ * The reponse for this url had an HSTS header, so let's take action.
+ */
+void a_Hsts_set(const char *header, const DilloUrl *url)
+{
+ long max_age;
+ const char *host = URL_HOST(url);
+ bool_t max_age_valid = FALSE, subdomains = FALSE;
+
+ _MSG("HSTS header for %s: %s\n", host, header);
+
+ if (!a_Tls_certificate_is_clean(url)) {
+ /* RFC 6797 gives rationale in section 14.3. */
+ _MSG("But there were certificate warnings, so ignore it (!)\n");
+ return;
+ }
+
+ /* Iterate until there is nothing left of the string */
+ while (*header) {
+ char *attr;
+ char *value;
+
+ /* Get attribute */
+ attr = Hsts_parse_attr(&header);
+
+ /* Get the value for the attribute and store it */
+ if (dStrAsciiCasecmp(attr, "max-age") == 0) {
+ value = Hsts_parse_value(&header);
+ if (isdigit(*value)) {
+ errno = 0;
+ max_age = strtol(value, NULL, 10);
+ if (errno == ERANGE)
+ max_age = INT_MAX;
+ max_age_valid = TRUE;
+ }
+ dFree(value);
+ } else if (dStrAsciiCasecmp(attr, "includeSubDomains") == 0) {
+ subdomains = TRUE;
+ Hsts_eat_value(&header);
+ } else if (dStrAsciiCasecmp(attr, "preload") == 0) {
+ /* 'preload' is not part of the RFC, but what does google care for
+ * standards? They require that 'preload' be specified by a domain
+ * in order to be added to their preload list.
+ */
+ } else {
+ MSG("HSTS: header contains unknown attribute: '%s'\n", attr);
+ Hsts_eat_value(&header);
+ }
+
+ dFree(attr);
+
+ if (*header == ';')
+ header++;
+ }
+ if (max_age_valid) {
+ if (max_age > 0)
+ Hsts_set_policy(host, max_age, subdomains);
+ else
+ Hsts_remove_policy(Hsts_get_policy(host));
+ }
+}
+
+static bool_t Hsts_expired(HstsData_t *policy)
+{
+ time_t now = time(NULL);
+ bool_t ret = (now > policy->expires_at) ? TRUE : FALSE;
+
+ if (ret) {
+ _MSG("HSTS: expired\n");
+ }
+ return ret;
+}
+
+bool_t a_Hsts_require_https(const char *host)
+{
+ bool_t ret = FALSE;
+
+ if (host) {
+ HstsData_t *policy = Hsts_get_policy(host);
+
+ if (policy) {
+ _MSG("HSTS: matched host %s\n", host);
+ if (Hsts_expired(policy))
+ Hsts_remove_policy(policy);
+ else
+ ret = TRUE;
+ }
+ if (!ret) {
+ const char *domain_str;
+
+ for (domain_str = strchr(host+1, '.');
+ domain_str != NULL && *domain_str;
+ domain_str = strchr(domain_str+1, '.')) {
+ policy = Hsts_get_policy(domain_str+1);
+
+ if (policy && policy->subdomains) {
+ _MSG("HSTS: matched %s under %s subdomain rule\n", host,
+ policy->host);
+ if (Hsts_expired(policy)) {
+ Hsts_remove_policy(policy);
+ } else {
+ ret = TRUE;
+ break;
+ }
+ }
+ }
+ }
+ }
+ return ret;
+}
+
+static void Hsts_preload(FILE *stream)
+{
+ const int LINE_MAXLEN = 4096;
+ const long ONE_YEAR = 60 * 60 * 24 * 365;
+
+ char *rc, *subdomains;
+ char line[LINE_MAXLEN];
+ char domain[LINE_MAXLEN];
+
+ /* Get all lines in the file */
+ while (!feof(stream)) {
+ line[0] = '\0';
+ rc = fgets(line, LINE_MAXLEN, stream);
+ if (!rc && ferror(stream)) {
+ MSG_WARN("HSTS: Error while reading preload entries: %s\n",
+ dStrerror(errno));
+ return; /* bail out */
+ }
+
+ /* Remove leading and trailing whitespace */
+ dStrstrip(line);
+
+ if (line[0] != '\0' && line[0] != '#') {
+ int i = 0, j = 0;
+
+ /* Get the domain */
+ while (line[i] != '\0' && !dIsspace(line[i]))
+ domain[j++] = line[i++];
+ domain[j] = '\0';
+
+ /* Skip past whitespace */
+ while (dIsspace(line[i]))
+ i++;
+
+ subdomains = line + i;
+
+ if (dStrAsciiCasecmp(subdomains, "true") == 0)
+ Hsts_set_policy(domain, ONE_YEAR, TRUE);
+ else if (dStrAsciiCasecmp(subdomains, "false") == 0)
+ Hsts_set_policy(domain, ONE_YEAR, FALSE);
+ else {
+ MSG_WARN("HSTS: format of line not recognized. Ignoring '%s'.\n",
+ line);
+ }
+ }
+ }
+}
+
+void a_Hsts_init(FILE *preload_file)
+{
+ struct tm future_tm = {7, 14, 3, 19, 0, 138, 0, 0, 0, 0, 0};
+
+ hsts_latest_representable_time = mktime(&future_tm);
+ domains = dList_new(32);
+
+ if (preload_file)
+ Hsts_preload(preload_file);
+}
+