/* * Copyright (c) 2010-2012 Stefan Bolte * * 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 #include #include #include #include "dwb.h" #include "util.h" #include "hsts.h" #include "gnutls/gnutls.h" #include "gnutls/x509.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 * + Enforce strict ssl verification on known hsts hosts * + Bootstrap whitelist (automatically converted from the chromium project) * + Add support for certificate pinning a la Chromium * * TODO: * + Handle UTF-8 BOM in loading code * + 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 */ #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); } /* The HSTSPinEntry data structure represents a host with a static set of * allowed and forbidden SPKIs hashes. */ typedef struct _HSTSPinEntry { GHashTable *good_certs; GHashTable *bad_certs; gboolean sub_domains; } HSTSPinEntry; /* Allocates and initialises a new HSTSPinEntry */ static HSTSPinEntry * hsts_pin_entry_new() { HSTSPinEntry *entry = dwb_malloc(sizeof(HSTSPinEntry)); entry->good_certs = NULL; entry->bad_certs = NULL; entry->sub_domains = false; return entry; } /* Frees the HSTSPinEntry, it is safe to pass NULL */ static void hsts_pin_entry_free(HSTSPinEntry *entry) { if(entry == NULL) return; if(entry->good_certs != NULL) g_hash_table_destroy(entry->good_certs); if(entry->bad_certs != NULL) g_hash_table_destroy(entry->bad_certs); 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, *pin_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); priv->pin_domains = g_hash_table_new_full((GHashFunc)g_str_hash, (GEqualFunc)g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)hsts_pin_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_hash_table_destroy(priv->pin_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); } /* Adds the host to hosts for which a certificate black or whitelist has been * specified. */ static void hsts_provider_add_pin_entry(HSTSProvider *provider, const char *host, HSTSPinEntry *entry) { if(g_hostname_is_ip_address(host)) return; HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(provider); g_hash_table_replace(priv->pin_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; } /* Checks whether there is relevant information for host in the certificate * white- and blacklist, if so it returns the relevant entry. Else it returns * NULL. */ static HSTSPinEntry * hsts_provider_has_cert_pin(HSTSProvider *provider, const char *host) { HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(provider); if(g_hostname_is_ip_address(host)) return NULL; HSTSPinEntry *result = NULL; gchar *canonical = g_hostname_to_unicode(host); 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) { result = g_hash_table_lookup(priv->pin_domains, cur); if(result != NULL && (!sub_domain || result->sub_domains)) /* If either host == cur or host is a proper sub domain of cur and the cur entry covers sub domains. */ break; result = NULL; 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; g_object_get(G_OBJECT(msg), SOUP_MESSAGE_RESPONSE_HEADERS, &hdrs, NULL); 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; } } /* FIXME: Possible memory leak, Investigate whether hdrs should be * cleaned up? * g_object_unref(hdrs); <-- This makes GLib complain so that clearly * isn't the right approach. */ } } /* 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); } /* Represents an entry in the preloaded HSTS database. * * Members: * host - the host of the entry * good_certs - a null terminated array of base64 encoded key ids of the good certificates, if NULL it is treated as the empty array * bad_certs - a null terminated array of base64 encoded key ids of the bad certificates, if NULL it is treated as the empty array * hsts - if true the host is added to the database of known HSTS hosts * sub_domains - indicates whether this entry applies to sub_domains * */ typedef struct _HSTSPreloadEntry { const char *host; const char * const *good_certs; const char * const *bad_certs; gboolean hsts; gboolean sub_domains; } HSTSPreloadEntry; #include "hsts_preload.h" /* Allocates and fills a hash set of certificates */ static void fill_cert_set(GHashTable **cert_set, const char * const *certs) { if(certs == NULL) return; if(*cert_set == NULL) *cert_set = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); GHashTable *hash_set = *cert_set; while(*certs != NULL) { g_hash_table_add(hash_set, g_strdup(*certs)); certs++; } } /* Loads the default database built into dwb */ static void load_default_database(HSTSProvider *provider) { const HSTSPreloadEntry *entry = s_hsts_preload; size_t i; for(i=0; i < s_hsts_preload_length; i++) { if(entry->hsts) { HSTSEntry *hsts_entry = hsts_entry_new(); hsts_entry->sub_domains = entry->sub_domains; hsts_provider_add_entry(provider, entry->host, hsts_entry); } if(entry->good_certs != NULL || entry->bad_certs != NULL) { HSTSPinEntry *hsts_pin_entry = hsts_pin_entry_new(); hsts_pin_entry->sub_domains = entry->sub_domains; fill_cert_set(&hsts_pin_entry->good_certs, entry->good_certs); fill_cert_set(&hsts_pin_entry->bad_certs, entry->bad_certs); hsts_provider_add_pin_entry(provider, entry->host, hsts_pin_entry); } entry++; } } /* 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); } /* 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); } } /* This callback is called when a new message is started, that is right before * data is sent but after a connection has been made. This callback might be * called multiple times for the same message. It is used to check the HTTPS * certificates according to the relevant HSTS directives and certificate * pinnings.*/ static void hsts_provider_request_started (SoupSessionFeature *feature, SoupSession *session, SoupMessage *msg, SoupSocket *socket) { HSTSProvider *provider = HSTS_PROVIDER (feature); const char *host = soup_uri_get_host(soup_message_get_uri(msg)); gboolean cancel = false; if(hsts_provider_should_secure_host(provider, host)) { GTlsCertificate *certificate; GTlsCertificateFlags errors; if(!(soup_message_get_https_status(msg, &certificate, &errors) && errors == 0)) /* If host is known HSTS host the standard specifies that we should ensure strict ssl handling */ cancel = true; } HSTSPinEntry *entry; GTlsCertificate *certificate; GTlsCertificateFlags errors; if(!cancel && soup_message_get_https_status(msg, &certificate, &errors) && (entry = hsts_provider_has_cert_pin(provider, host)) != NULL) { /* If we are connecting over HTTPS to a host with a certificate black/whitelist */ /* If there is no whitelist assume the certificate chain is good */ gboolean is_good = entry->good_certs != NULL ? false : true; /* Whether a certificate on the chain is found in the whitelist */ gboolean is_bad = false; /* Whether a certificate in the chain is on the blacklist */ GTlsCertificate *cur = certificate; while(cur != NULL) { /* Check each certificate in the chain */ /* First import the certificate into gnutls */ GByteArray *cert_bytes; g_object_get(G_OBJECT(cur), "certificate", &cert_bytes, NULL); gnutls_datum_t data; data.data = cert_bytes->data; data.size = cert_bytes->len; gnutls_x509_crt_t cert; gnutls_x509_crt_init(&cert); /* Then try to get the key_id and check that against the black/white lists */ int err; unsigned char key_id[1024]; size_t key_id_size = 1024; if((err = gnutls_x509_crt_import(cert, &data, GNUTLS_X509_FMT_DER)) == GNUTLS_E_SUCCESS && (err = gnutls_x509_crt_get_key_id(cert, 0, key_id, &key_id_size)) == GNUTLS_E_SUCCESS ) { char *key_id_base64 = g_base64_encode(key_id, key_id_size); is_good = is_good || (entry->good_certs != NULL && g_hash_table_lookup(entry->good_certs, key_id_base64)); is_bad = is_bad || (entry->bad_certs != NULL && g_hash_table_lookup(entry->bad_certs, key_id_base64)); g_free(key_id_base64); } else { printf("HSTS: Warning: Problems getting certificate key id for a certificate of %s\n", host); } /* Cleanup */ gnutls_x509_crt_deinit(cert); g_byte_array_unref(cert_bytes); cur = g_tls_certificate_get_issuer(cur); } /* If we aren't explicitly on the whitelist or a certificate is on the * blacklist, cancel the message. Said simpler a certificate is * accepted only if it has at least one certificate in it's chain on * the whitelist and none on the blacklist */ if(!is_good || is_bad) cancel = true; } if(cancel) soup_session_cancel_message(session, msg, SOUP_STATUS_SSL_FAILED); } /* 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; } }