diff options
author | Adam Ehlers Nyholm Thomsen <adament@adament.net> | 2012-12-11 00:18:41 +0100 |
---|---|---|
committer | Adam Ehlers Nyholm Thomsen <adament@adament.net> | 2012-12-11 00:18:41 +0100 |
commit | 307c558801ba06c3a36f88e2ae9dbe208f6a0059 (patch) | |
tree | 793044d3791aec79a782ee5f4ef5aeed672b8771 /src/hsts.c | |
parent | 944fc426af70bb926749d0273c082e5feb7b5306 (diff) | |
download | dwb-307c558801ba06c3a36f88e2ae9dbe208f6a0059.zip |
First version of HSTS implementation.
Diffstat (limited to 'src/hsts.c')
-rw-r--r-- | src/hsts.c | 697 |
1 files changed, 697 insertions, 0 deletions
diff --git a/src/hsts.c b/src/hsts.c new file mode 100644 index 00000000..f622bb54 --- /dev/null +++ b/src/hsts.c @@ -0,0 +1,697 @@ +/* + * Copyright (c) 2010-2012 Stefan Bolte <portix@gmx.net> + * + * 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. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#include <stdio.h> +#include <string.h> +#include <glib-object.h> +#include <glib/gstdio.h> +#include "dwb.h" +#include "util.h" +#include "hsts.h" + +/* + * This file contains an HSTS (HTTP Strict Transport Security) implementation + * for the dwb browser. It works by registering a session interface with soup + * and rewriting relevant requests when they are queued, and listening for + * hsts headers. The approach was inspired by the HSTS implementation in thje + * midori browser. + * + * Current Features: + * + Enforces HSTS as specified in [RFC6797] + * + Loading and saving of the cache + * + * TODO: + * + Handle UTF-8 BOM in loading code + * + Bootstrap whitelist (steal from chromium) + * + Enforce strict ssl verification on known hsts hosts + * + Add support for certificate pining a la Chromium + * + Periodic saving of database to mitigate loss of information in event of crash + * + * Problems: + * 1. The implementation doesn't consider mixed content, which should be + * blocked according to RFC 6797 12.4 + * + * Sites that support HSTS to test the system with: + * + crypto.cat + */ + +#define HSTS_HEADER_NAME "Strict-Transport-Security" + +/* The HSTSEntry data structure represents a known host in the HSTS database + * + * Members: + * expiry - the expiry of the rule, represented as microseconds since January 1, 1970 UTC. + * sub_domains - whether the rule applies to sub_domains + */ +typedef struct _HSTSEntry { + gint64 expiry; + gboolean sub_domains; +} HSTSEntry; + +/* Allocate a new HSTSEntry and initialise it. It is initialised to have + * maximum expiry (effectively indefinite life) and not to apply to sub + * domains. + */ +static HSTSEntry * +hsts_entry_new() +{ + HSTSEntry *entry = dwb_malloc(sizeof(HSTSEntry)); + entry->expiry = G_MAXINT64; + entry->sub_domains = false; + return entry; +} + +/* Allocates and initialises a new HSTSEntry to the given values. + * Params: + * max_age - number of seconds the rule should live. + * sub_domains - whether the rule applies to sub_domains + */ +static HSTSEntry * +hsts_entry_new_from_val(gint64 max_age, gboolean sub_domains) +{ + HSTSEntry *entry = hsts_entry_new(); + entry->expiry = g_get_real_time(); + if(max_age > (G_MAXINT64 - entry->expiry)/G_USEC_PER_SEC) + entry->expiry = G_MAXINT64; + else + entry->expiry += max_age*G_USEC_PER_SEC; + entry->sub_domains = sub_domains; + return entry; +} + +/* Frees the HSTSEntry + */ +static void +hsts_entry_free(HSTSEntry *entry) +{ + g_free(entry); +} + +/* + * HSTSProvider works by registering as a SoupSessionFeature and rewriting all + * http requests into https requests for known hosts. However this means that + * HSTSProvider has to be implement the SoupSessionFeatureInterface and hence + * all the boilerplate gobject code in the following. + * + */ + +/* + * Type macros. + */ +#define HSTS_TYPE_PROVIDER (hsts_provider_get_type ()) +#define HSTS_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), HSTS_TYPE_PROVIDER, HSTSProvider)) +#define HSTS_IS_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), HSTS_TYPE_PROVIDER)) +#define HSTS_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), HSTS_TYPE_PROVIDER, HSTSProviderClass)) +#define HSTS_IS_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), HSTS_TYPE_PROVIDER)) +#define HSTS_PROVIDER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), HSTS_TYPE_PROVIDER, HSTSProviderClass)) +#define HSTS_PROVIDER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), HSTS_TYPE_PROVIDER, HSTSProviderPrivate)) + +/* The HSTSProvider public interface + */ +typedef struct _HSTSProvider +{ + GObject parent_instance; +} HSTSProvider; + +/* The private members of the HSTSProvider + */ +typedef struct _HSTSProviderPrivate +{ + GHashTable *domains; +} HSTSProviderPrivate; + +/* The class members of the HSTSProvider + */ +typedef struct _HSTSProviderClass +{ + GObjectClass parent_class; + + /* The following static variables are used to do case insensitive comparisons + * of directive names, as specified in RFC 6797 6.1 2. + */ + gchar *directive_max_age; + gchar *directive_sub_domains; +} HSTSProviderClass; + +/* Prototypes of various functions, some are needed for glib magic. This is not an exhaustive + * list of the hsts_provider functions. + */ +static void hsts_provider_init (HSTSProvider *self); +static void hsts_provider_class_init (HSTSProviderClass *klass); +static void hsts_provider_base_class_init (HSTSProviderClass *klass); +static void hsts_provider_base_class_finalize (HSTSProviderClass *klass); +static gpointer hsts_provider_parent_class = NULL; +static void hsts_provider_session_feature_init(SoupSessionFeatureInterface *feature_interface, gpointer interface_data); +static void hsts_provider_finalize (GObject *object); + +/* GLib essential function. This basically declares the existence of the + * HSTSProvider class to GLib and gives it various information about it. This + * rather cumbersome function is needed to get dynamic class members(ie. + * setting the base_* options). + */ +GType +hsts_provider_get_type (void) +{ + static volatile gsize g_define_type_id__volatile = 0; + if (g_once_init_enter (&g_define_type_id__volatile)) + { + GTypeInfo info; + info.class_size = sizeof(HSTSProviderClass); + info.base_init = (GBaseInitFunc) hsts_provider_base_class_init; + info.base_finalize = (GBaseFinalizeFunc) hsts_provider_base_class_finalize; + info.class_init = (GClassInitFunc) hsts_provider_class_init; + info.class_finalize = NULL; + info.class_data = NULL; + info.instance_size = sizeof(HSTSProvider); + info.n_preallocs = 0; + info.instance_init = (GInstanceInitFunc) hsts_provider_init; + info.value_table = NULL; + + GType g_define_type_id = g_type_register_static (G_TYPE_OBJECT, g_intern_static_string ("HSTSProvider"), &info, 0); + + const GInterfaceInfo g_implement_interface_info = { + (GInterfaceInitFunc) hsts_provider_session_feature_init, NULL, NULL + }; + g_type_add_interface_static (g_define_type_id, SOUP_TYPE_SESSION_FEATURE, &g_implement_interface_info); + g_once_init_leave (&g_define_type_id__volatile, g_define_type_id); + } + return g_define_type_id__volatile; +} + +/* Initialise the dynamic class members of HSTSProvider + */ +static void +hsts_provider_base_class_init (HSTSProviderClass *klass) +{ + klass->directive_max_age = g_utf8_casefold("max-age", -1); + klass->directive_sub_domains = g_utf8_casefold("includeSubDomains", -1); +} + +/* Finalise(free) the dynamic class members of HSTSProvider + */ +static void +hsts_provider_base_class_finalize (HSTSProviderClass *klass) +{ + g_free(klass->directive_max_age); + g_free(klass->directive_sub_domains); +} + +/* Initialise the HSTSProvider class + */ +static void +hsts_provider_class_init (HSTSProviderClass *klass) +{ + hsts_provider_parent_class = g_type_class_peek_parent (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + g_type_class_add_private (klass, sizeof (HSTSProviderPrivate)); + + object_class->finalize = hsts_provider_finalize; +} + +/* Initialise an HSTSProvider instance + */ +static void +hsts_provider_init (HSTSProvider *provider) +{ + HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE (provider); + + priv->domains = g_hash_table_new_full((GHashFunc)g_str_hash, (GEqualFunc)g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)hsts_entry_free); +} + +/* Finalise an HSTSProvider instance + */ +static void +hsts_provider_finalize (GObject *object) +{ + HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE (object); + + g_hash_table_destroy(priv->domains); + + G_OBJECT_CLASS (hsts_provider_parent_class)->finalize (object); +} + +/* Remove an entry from the known hosts, this doesn't remove superdomains of + * host with the includeSubDomains directive. So the host might still be + * affected by the HSTS code + */ +static void +hsts_provider_remove_entry(HSTSProvider *provider, const char *host) +{ + HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(provider); + + gchar *canonical = g_hostname_to_unicode(host); + g_hash_table_remove(priv->domains, canonical); + g_free(canonical); +} + +/* Adds the host to the known host, if it already exists it replaces it with + * the information contained in entry. As specified in 8.1 [RFC6797] it won't + * add ip addresses as hosts. + */ +static void +hsts_provider_add_entry(HSTSProvider *provider, const char *host, HSTSEntry *entry) +{ + if(g_hostname_is_ip_address(host)) + return; + + HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(provider); + + g_hash_table_replace(priv->domains, g_hostname_to_unicode(host), entry); +} + +/* Checks whether host is currently a known host or it is a sub domain of a + * known host which covers sub domains. + * + * Beware: An ip address will return false, as specified in 8.3 [RFC6797] + */ +static gboolean +hsts_provider_should_secure_host(HSTSProvider *provider, const char *host) +{ + HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(provider); + + if(g_hostname_is_ip_address(host)) + return false; + + gchar *canonical = g_hostname_to_unicode(host); + gboolean result = false; + if(strlen(canonical) > 0) /* Don't match empty strings as per. 8.3 [RFC6797] */ + { + gchar *cur = canonical; + gboolean sub_domain = false; /* Indicates whether host is a proper sub domain of cur */ + gunichar dot = g_utf8_get_char("."); + while(cur != NULL) + { + HSTSEntry *entry = g_hash_table_lookup(priv->domains, cur); + if(entry != NULL) + { + if(g_get_real_time() > entry->expiry) /* Remove expired entries */ + hsts_provider_remove_entry(provider, cur); + else if(!sub_domain || entry->sub_domains) + { /* If either host == cur or host is a proper sub domain of + cur and the cur entry covers sub domains. */ + result = true; + break; + } + } + + sub_domain = true; + cur = g_utf8_strchr(cur, -1, dot); + /* Since canonical is in canonical form, it doesn't end with a . + * and hence there's no problem with the following: */ + if(cur != NULL) + cur = g_utf8_next_char(cur); + } + } + g_free(canonical); + + return result; +} + +/* Parse an HSTS header and add it to the known hosts. + * Returns whether or not the header was valid. + */ +static gboolean +hsts_provider_parse_header(HSTSProvider *provider, const char *host, const char *header) +{ + GHashTable *directives = soup_header_parse_semi_param_list(header); + + HSTSProviderClass *klass = g_type_class_ref(HSTS_TYPE_PROVIDER); + gint64 max_age = -1; + gboolean sub_domains = false; + gboolean success = true; + + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init(&iter, directives); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + /* We have to jump through hoops here to be able to do the + * comparison in a case-insensitive manner, as specified in + * RFC 6797 6.1 + */ + gchar *key_ci = g_utf8_casefold(key, -1); + if (g_utf8_collate(key_ci, klass->directive_max_age) == 0) + { + if(value == NULL) + { + success = false; + break; + } + else + { + gchar *endptr; + max_age = g_ascii_strtoll(value, &endptr, 10); + if(endptr == value || max_age < 0) + { + success = false; + break; + } + } + } + else if (g_utf8_collate(key_ci, klass->directive_sub_domains) == 0) + { + if(value != NULL) + { + success = false; + break; + } + else + sub_domains = true; + } + g_free(key_ci); + } + g_type_class_unref(klass); + if(success) + { + if(max_age != 0) + hsts_provider_add_entry(provider, host, hsts_entry_new_from_val(max_age, sub_domains)); + else /* max_age = 0 indicates remove header */ + hsts_provider_remove_entry(provider, host); + } + + soup_header_free_param_list(directives); + return success; +} + +/* Processes the headers of msg and looks for a valid HSTS, if found it adds it + * as a known host according to the information specified in the header. + */ +static void +hsts_process_hsts_header (SoupMessage *msg, gpointer user_data) +{ + GTlsCertificate *certificate; + GTlsCertificateFlags errors; + /* Only read HSTS headers sent over a properly validated https connection + * as specified in 8.1 [RFC6797] + */ + SoupURI *uri = soup_message_get_uri(msg); + const char *host = soup_uri_get_host(uri); + if(!g_hostname_is_ip_address(host) && + soup_message_get_https_status(msg, &certificate, &errors) && + errors == 0){ + HSTSProvider *provider = user_data; + + SoupMessageHeaders *hdrs; + { + /* Get the headers from msg */ + GValue hdrs_val = G_VALUE_INIT; + g_value_init(&hdrs_val, SOUP_TYPE_MESSAGE_HEADERS); + g_object_get_property(G_OBJECT(msg), SOUP_MESSAGE_RESPONSE_HEADERS, &hdrs_val); + hdrs = g_value_get_boxed(&hdrs_val); + g_value_unset(&hdrs_val); + } + + SoupMessageHeadersIter iter; + soup_message_headers_iter_init(&iter, hdrs); + const char *name, *value; + while(soup_message_headers_iter_next(&iter, &name, &value)) + { + if(strcmp(name, HSTS_HEADER_NAME) == 0) + { + /* It is not exactly clear to me what the correct behavior is + * if multiple headers are present. There seems to be some + * relevant information in 8.1 [RFC6797]. + */ + if(hsts_provider_parse_header(provider, host, value)) + break; + } + } + } +} + +/* Contains case folded versions of true and false used for comparisons in + * parse_line */ +static char *parser_true, *parser_false; + +/* Parses a line from a known hosts file and if it is correctly parsed it is + * added to the known hosts in provider */ +static void +parse_line(HSTSProvider *provider, const char *line, gint64 now) +{ + /* Ignore comments */ + if(g_utf8_get_char(line) == g_utf8_get_char("#")) + return; + + char **split = g_strsplit(line, "\t", -1); + if(g_strv_length(split) == 3) + { + char *host = split[0], *sub_domains = split[1], *expires = split[2]; + HSTSEntry *entry = hsts_entry_new(); + gboolean success = true; + + if(g_utf8_collate(parser_true, sub_domains) == 0) + entry->sub_domains = true; + else if(g_utf8_collate(parser_false, sub_domains) == 0) + entry->sub_domains = false; + else + success = false; + + char *end; + entry->expiry = g_ascii_strtoll(expires, &end, 10); + if(expires == end || entry->expiry < now) + success = false; + + if(success) + hsts_provider_add_entry(provider, host, entry); + else + hsts_entry_free(entry); + } + + g_strfreev(split); +} + +/* Loads the default database built into dwb + */ +static void +load_default_database(HSTSProvider *provider) +{ +} + +/* Reads a database of known hosts from filename. filename is a utf-8 encoded + * file, which on each line contains the following tab separated fields: + * + * host - is the known host + * sub domains - is either true or false compared case-insensitively and + * indicates whether the entry applies to sub domains of the + * given host + * expiry - Expiry time given as the number of microseconds since + * January 1, 1970 UTF. Encoded as a decimal. + * + * Lines which start with a '#' are treated as comments. Only \n and \r are + * recognised as line separators. + */ +static gboolean +hsts_provider_load(HSTSProvider *provider, const char *filename) +{ + + load_default_database(provider); + + gchar *contents; + gsize length = 0; + if(!g_file_get_contents(filename, &contents, &length, NULL)) + return false; + + gboolean success = false; + if(g_utf8_validate(contents, length, NULL)) + { + parser_true = g_utf8_casefold("true", -1); + parser_false = g_utf8_casefold("false", -1); + + gint64 now = g_get_real_time(); + /* TODO: Handle UTF-8 BOM */ + gchar *line = contents, *p = contents; + gunichar r = g_utf8_get_char("\r"), n = g_utf8_get_char("\n"); + while(*p) + { + gunichar c = g_utf8_get_char(p); + if(c == r || c == n) + { + /* \r\n is treated as two lines but it doesn't since empty + * lines are ignored */ + gchar *next = g_utf8_next_char(p); + *p = '\0'; /* null terminate line */ + parse_line(provider, line, now); + line = next; + p = next; + } + else + p = g_utf8_next_char(p); + } + + success = true; + g_free(parser_true); + g_free(parser_false); + } + g_free(contents); + return success; +} + +/* Saves the database of known hosts to filename in the format specified for + * hsts_provider_load */ +static void +hsts_provider_save(HSTSProvider *provider, const char *filename) +{ + HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(provider); + FILE *file = g_fopen(filename, "w"); + fprintf(file, "# dwb hsts database\n"); + + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init(&iter, priv->domains); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + const char *host = (const char *)key; + const HSTSEntry *entry = (HSTSEntry *)value; + /* TODO: assert MAX_LONG_LONG > G_MAXINT64 */ + long long expiry = entry->expiry; + fprintf(file, "%s\t%s\t%lld\n", host, entry->sub_domains ? "true" : "false", expiry); + } + fclose(file); +} + +/* This callback is called when a new message is put on the session queue. It + * investigates whether the message is intended for a known host and if so it + * switches URI scheme to HTTPS. + */ +static void +hsts_provider_request_queued (SoupSessionFeature *feature, + SoupSession *session, + SoupMessage *msg) +{ + HSTSProvider *provider = HSTS_PROVIDER (feature); + + SoupURI *uri = soup_message_get_uri(msg); + if(soup_uri_get_scheme(uri) == SOUP_URI_SCHEME_HTTP && + hsts_provider_should_secure_host(provider, soup_uri_get_host(uri))) + { + soup_uri_set_scheme(uri, SOUP_URI_SCHEME_HTTPS); + /* Only change port if it explicitly references port 80 as specified in + * 8.3 [RFC6797]. */ + if(soup_uri_get_port(uri) == 80) + soup_uri_set_port(uri, 443); + soup_session_requeue_message(session, msg); + + /* TODO: Ensure strict ssl handling of known HSTS hosts. */ + } + + /* Only look for HSTS headers sent over https */ + if(soup_uri_get_scheme(uri) == SOUP_URI_SCHEME_HTTPS) + { + soup_message_add_header_handler (msg, "got-headers", + HSTS_HEADER_NAME, + G_CALLBACK (hsts_process_hsts_header), + feature); + } +} + +static void +hsts_provider_request_started (SoupSessionFeature *feature, + SoupSession *session, + SoupMessage *msg, + SoupSocket *socket) +{ +} + +/* Removes added callbacks on message unqueue + */ +static void +hsts_provider_request_unqueued (SoupSessionFeature *feature, + SoupSession *session, + SoupMessage *msg) +{ + g_signal_handlers_disconnect_by_func (msg, hsts_process_hsts_header, feature); +} + +/* Initialise the SoupSessionFeature interface. + */ +static void +hsts_provider_session_feature_init (SoupSessionFeatureInterface *feature_interface, + gpointer interface_data) +{ + feature_interface->request_queued = hsts_provider_request_queued; + feature_interface->request_started = hsts_provider_request_started; + feature_interface->request_unqueued = hsts_provider_request_unqueued; +} + +/* Indicates whether hsts has been initialised */ +static gboolean s_init = false; +static HSTSProvider *s_provider; + +gboolean +hsts_running() +{ + return s_init && GET_BOOL("hsts"); +} + +/* Activates hsts */ +void +hsts_activate() +{ + if(!hsts_init()) + return; + soup_session_add_feature(dwb.misc.soupsession, SOUP_SESSION_FEATURE(s_provider)); +} + +/* Deactivates hsts */ +void +hsts_deactivate() +{ + if(!s_init) + return; + soup_session_remove_feature(dwb.misc.soupsession, SOUP_SESSION_FEATURE(s_provider)); +} + +/* Save current hsts lists */ +void +hsts_save() +{ + if(hsts_running()) + hsts_provider_save(s_provider, dwb.files[FILES_HSTS]); +} + +/* Initialises the hsts implementation */ +gboolean +hsts_init() +{ + if(s_init) + return true; + if(!GET_BOOL("hsts")) + return false; + + s_provider = g_object_new(HSTS_TYPE_PROVIDER, NULL); + s_init = true; + + hsts_provider_load(s_provider, dwb.files[FILES_HSTS]); + hsts_activate(); + + return true; +} + +/* Finalises the hsts implementation */ +void +hsts_end() +{ + hsts_save(); + hsts_deactivate(); + + if(s_init) + { + g_object_unref(s_provider); + s_init = false; + } +} |