diff options
-rw-r--r-- | src/IO/tls.c | 25 | ||||
-rw-r--r-- | src/IO/tls.h | 2 | ||||
-rw-r--r-- | src/Makefile.am | 2 | ||||
-rw-r--r-- | src/cache.c | 10 | ||||
-rw-r--r-- | src/dillo.cc | 3 | ||||
-rw-r--r-- | src/hsts.c | 360 | ||||
-rw-r--r-- | src/hsts.h | 19 | ||||
-rw-r--r-- | src/nav.c | 1 | ||||
-rw-r--r-- | src/paths.hh | 1 | ||||
-rw-r--r-- | src/url.c | 38 |
10 files changed, 451 insertions, 10 deletions
diff --git a/src/IO/tls.c b/src/IO/tls.c index 89ad7989..dfe76744 100644 --- a/src/IO/tls.c +++ b/src/IO/tls.c @@ -64,7 +64,7 @@ void a_Tls_init() #define CERT_STATUS_NONE 0 #define CERT_STATUS_RECEIVING 1 -#define CERT_STATUS_GOOD 2 +#define CERT_STATUS_CLEAN 2 #define CERT_STATUS_BAD 3 #define CERT_STATUS_USER_ACCEPTED 4 @@ -402,18 +402,29 @@ int a_Tls_connect_ready(const DilloUrl *url) return ret; } +static int Tls_cert_status(const DilloUrl *url) +{ + Server_t *s = dList_find_sorted(servers, url, Tls_servers_by_url_cmp); + + return s ? s->cert_status : CERT_STATUS_NONE; +} + /* * Did we find problems with the certificate, and did the user proceed to * reject the connection? */ static int Tls_user_said_no(const DilloUrl *url) { - Server_t *s = dList_find_sorted(servers, url, Tls_servers_by_url_cmp); - - if (!s) - return FALSE; + return Tls_cert_status(url) == CERT_STATUS_BAD; +} - return s->cert_status == CERT_STATUS_BAD; +/* + * Did everything seem proper with the certificate -- no warnings to + * click through? + */ +int a_Tls_certificate_is_clean(const DilloUrl *url) +{ + return Tls_cert_status(url) == CERT_STATUS_CLEAN; } /******************** BEGINNING OF STUFF DERIVED FROM wget-1.16.3 */ @@ -894,7 +905,7 @@ static int Tls_examine_certificate(SSL *ssl, Server_t *srv,const char *host) if (choice == 2) srv->cert_status = CERT_STATUS_BAD; else if (choice == -1) - srv->cert_status = CERT_STATUS_GOOD; + srv->cert_status = CERT_STATUS_CLEAN; else srv->cert_status = CERT_STATUS_USER_ACCEPTED; diff --git a/src/IO/tls.h b/src/IO/tls.h index e3892cb2..9bc89de5 100644 --- a/src/IO/tls.h +++ b/src/IO/tls.h @@ -15,6 +15,7 @@ void a_Tls_init(); #ifdef ENABLE_SSL +int a_Tls_certificate_is_clean(const DilloUrl *url); int a_Tls_connect_ready(const DilloUrl *url); void a_Tls_reset_server_state(const DilloUrl *url); @@ -30,6 +31,7 @@ int a_Tls_read(void *conn, void *buf, size_t len); int a_Tls_write(void *conn, void *buf, size_t len); #else +#define a_Tls_certificate_is_clean(host) 0 #define a_Tls_connect_ready(url) TLS_CONNECT_NEVER #define a_Tls_reset_server_state(url) ; #define a_Tls_handshake(fd, url) ; diff --git a/src/Makefile.am b/src/Makefile.am index 57a68148..fe557bb4 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -37,6 +37,8 @@ dillo_SOURCES = \ bw.c \ cookies.c \ cookies.h \ + hsts.c \ + hsts.h \ auth.c \ auth.h \ md5.c \ diff --git a/src/cache.c b/src/cache.c index d8f1a123..cc33db9c 100644 --- a/src/cache.c +++ b/src/cache.c @@ -26,6 +26,7 @@ #include "dicache.h" #include "nav.h" #include "cookies.h" +#include "hsts.h" #include "misc.h" #include "capi.h" #include "decode.h" @@ -653,7 +654,7 @@ static void Cache_parse_header(CacheEntry_t *entry) { char *header = entry->Header->str; bool_t server1point0 = !strncmp(entry->Header->str, "HTTP/1.0", 8); - char *Length, *Type, *location_str, *encoding, *connection; + char *Length, *Type, *location_str, *encoding, *connection, *hsts; #ifndef DISABLE_COOKIES Dlist *Cookies; #endif @@ -721,6 +722,13 @@ static void Cache_parse_header(CacheEntry_t *entry) dFree(connection); } + if (!dStrAsciiCasecmp(URL_SCHEME(entry->Url), "https") && + !a_Url_host_is_ip(URL_HOST(entry->Url)) && + (hsts = Cache_parse_field(header, "Strict-Transport-Security"))) { + a_Hsts_set(hsts, entry->Url); + dFree(hsts); + } + /* * Get Transfer-Encoding and initialize decoder */ diff --git a/src/dillo.cc b/src/dillo.cc index 6e28f155..24271103 100644 --- a/src/dillo.cc +++ b/src/dillo.cc @@ -51,6 +51,7 @@ #include "capi.h" #include "dicache.h" #include "cookies.h" +#include "hsts.h" #include "domain.h" #include "auth.h" #include "styleengine.hh" @@ -483,6 +484,7 @@ int main(int argc, char **argv) a_Dicache_init(); a_Bw_init(); a_Cookies_init(); + a_Hsts_init(Paths::getPrefsFP(PATHS_HSTS_PRELOAD)); a_Auth_init(); a_UIcmd_init(); StyleEngine::init(); @@ -596,6 +598,7 @@ int main(int argc, char **argv) */ a_Domain_freeall(); a_Cookies_freeall(); + a_Hsts_freeall(); a_Cache_freeall(); a_Dicache_freeall(); a_Http_freeall(); 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); +} + diff --git a/src/hsts.h b/src/hsts.h new file mode 100644 index 00000000..693aec10 --- /dev/null +++ b/src/hsts.h @@ -0,0 +1,19 @@ +#ifndef __HSTS_H__ +#define __HSTS_H__ + +#include "d_size.h" +#include "url.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +void a_Hsts_init(FILE *fp); +void a_Hsts_set(const char *header, const DilloUrl *url); +bool_t a_Hsts_require_https(const char *host); +void a_Hsts_freeall( void ); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* !__HSTS_H__ */ @@ -353,6 +353,7 @@ void a_Nav_push(BrowserWindow *bw, const DilloUrl *url, a_Nav_cancel_expect(bw); a_Bw_expect(bw, url); Nav_open_url(bw, url, requester, 0); + a_UIcmd_set_location_text(bw, URL_STR(url)); } /* diff --git a/src/paths.hh b/src/paths.hh index 8f52cd86..ecc02f8b 100644 --- a/src/paths.hh +++ b/src/paths.hh @@ -15,6 +15,7 @@ #define PATHS_RC_PREFS "dillorc" #define PATHS_RC_KEYS "keysrc" #define PATHS_RC_DOMAIN "domainrc" +#define PATHS_HSTS_PRELOAD "hsts_preload" class Paths { public: @@ -46,6 +46,7 @@ #include <ctype.h> #include "url.h" +#include "hsts.h" #include "msg.h" static const char *HEX = "0123456789ABCDEF"; @@ -140,10 +141,17 @@ static DilloUrl *Url_object_new(const char *uri_str) url = dNew0(DilloUrl, 1); + /* url->buffer is given a little extra room in case HSTS needs to transform + * a URL string ending in ":80" to ":443". + */ + int len = strlen(uri_str)+2; + s = dNew(char, len); + memcpy(s, uri_str, len-1); + s = dStrstrip(s); + /* remove leading & trailing space from buffer */ - url->buffer = dStrstrip(dStrdup(uri_str)); + url->buffer = s; - s = (char *) url->buffer; p = strpbrk(s, ":/?#"); if (p && p[0] == ':' && p > s) { /* scheme */ *p = 0; @@ -412,6 +420,32 @@ DilloUrl* a_Url_new(const char *url_str, const char *base_url) dFree(str1); dFree(str2); + + /* + * A site's HTTP Strict Transport Security policy may direct us to transform + * URLs like "http://en.wikipedia.org:80" to "https://en.wikipedia.org:443". + */ + if (url->scheme && !dStrAsciiCasecmp(url->scheme, "http") && + a_Hsts_require_https(a_Url_hostname(url))) { + const char *const scheme = "https"; + + MSG("url: HSTS transformation for %s.\n", url->url_string->str); + url->scheme = scheme; + if (url->port == URL_HTTP_PORT) + url->port = URL_HTTPS_PORT; + + if (url->authority) { + int len = strlen(url->authority); + + if (len >= 3 && !strcmp(url->authority + len-3, ":80")) { + strcpy((char *)url->authority + len-2, "443"); + } + } + + dStr_free(url->url_string, TRUE); + url->url_string = NULL; + } + return url; } |