From: Daniel Carl Date: Wed, 23 Apr 2014 21:47:43 +0000 (+0200) Subject: Added new session feature for HSTS (#79). X-Git-Url: https://git.owens.tech/assets/me.png/assets/me.png/git?a=commitdiff_plain;h=ee219d819ce0c32604a1fddb15cee359765ad867;p=vimb.git Added new session feature for HSTS (#79). The main stuff for the new session feature to handle HTTP strict Transport Security is added. --- diff --git a/config.mk b/config.mk index ed2e551..bb4a95d 100644 --- a/config.mk +++ b/config.mk @@ -40,8 +40,8 @@ LIBFLAGS = $(shell pkg-config --libs $(LIBS)) # normal compiler flags CFLAGS += $(shell pkg-config --cflags $(LIBS)) -CFLAGS += -Wall -pipe -ansi -std=c99 -pedantic -CFLAGS += -Wno-overlength-strings +CFLAGS += -Wall -pipe -ansi -std=c99 +CFLAGS += -Wno-overlength-strings -Werror=format-security CFLAGS += ${CPPFLAGS} LDFLAGS += ${LIBFLAGS} diff --git a/src/hsts.c b/src/hsts.c new file mode 100644 index 0000000..3207c26 --- /dev/null +++ b/src/hsts.c @@ -0,0 +1,317 @@ +/** + * vimb - a webkit based vim like browser. + * + * Copyright (C) 2012-2014 Daniel Carl + * + * 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, see http://www.gnu.org/licenses/. + */ + +#include "config.h" +#include "hsts.h" +#include +#include +#include + +#define HSTS_HEADER_NAME "Strict-Transport-Security" +#define HSTS_PROVIDER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE((o), HSTS_TYPE_PROVIDER, HSTSProviderPrivate)) + +/* private interface of the provider */ +typedef struct _HSTSProviderPrivate { + GHashTable* whitelist; +} HSTSProviderPrivate; + +typedef struct { + gint64 expires_at; + gboolean include_sub_domains; +} HSTSEntry; + +static void hsts_provider_class_init(HSTSProviderClass *klass); +static void hsts_provider_init(HSTSProvider *self); +static void hsts_provider_finalize(GObject* obj); +static gboolean should_secure_host(HSTSProvider *provider, + const char *host); +static void process_hsts_header(SoupMessage *msg, gpointer data); +static void parse_hsts_header(HSTSProvider *provider, + const char *host, const char *header); +static HSTSEntry *get_new_entry(gint64 max_age, gboolean include_sub_domains); +static void add_host_entry(HSTSProvider *provider, const char *host, + HSTSEntry *entry); +static void remove_host_entry(HSTSProvider *provider, const char *host); +/* session feature related functions */ +static void session_feature_init( + SoupSessionFeatureInterface *inteface, gpointer data); +static void request_queued(SoupSessionFeature *feature, + SoupSession *session, SoupMessage *msg); +static void request_started(SoupSessionFeature *feature, + SoupSession *session, SoupMessage *msg, SoupSocket *socket); +static void request_unqueued(SoupSessionFeature *feature, + SoupSession *session, SoupMessage *msg); + + +/** + * Generates a new hsts provider instance. + * Unref the instance with g_object_unref if no more used. + */ +HSTSProvider *hsts_provider_new(void) +{ + return g_object_new(HSTS_TYPE_PROVIDER, NULL); +} + +G_DEFINE_TYPE_WITH_CODE( + HSTSProvider, hsts_provider, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(SOUP_TYPE_SESSION_FEATURE, session_feature_init) +) + +static void hsts_provider_class_init(HSTSProviderClass *klass) +{ + hsts_provider_parent_class = g_type_class_peek_parent(klass); + g_type_class_add_private(klass, sizeof(HSTSProviderPrivate)); + G_OBJECT_CLASS(klass)->finalize = hsts_provider_finalize; +} + +static void hsts_provider_init(HSTSProvider *self) +{ + /* Initialize private fields */ + HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(self); + priv->whitelist = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); +} + +static void hsts_provider_finalize(GObject* obj) +{ + HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE (obj); + + g_hash_table_destroy(priv->whitelist); + G_OBJECT_CLASS(hsts_provider_parent_class)->finalize(obj); +} + +/** + * Checks if given host is a known https host according to RFC 6797 8.3 + */ +static gboolean should_secure_host(HSTSProvider *provider, + const char *host) +{ + HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(provider); + char *canonical, *p; + gboolean result = false, is_subdomain = false; + + /* ip is not allowed for hsts */ + if (g_hostname_is_ip_address(host)) { + return false; + } + + canonical = g_hostname_to_ascii(host); + /* don't match empty host */ + if (*canonical) { + p = canonical; + while (p != NULL) { + HSTSEntry *entry = g_hash_table_lookup(priv->whitelist, p); + if (entry != NULL) { + /* remove expired entries */ + if (g_get_real_time() > entry->expires_at) { + remove_host_entry(provider, p); + } else if(!is_subdomain || entry->include_sub_domains) { + result = true; + break; + } + } + + is_subdomain = true; + /* test without the first domain part */ + if ((p = strchr(p, '.'))) { + p++; + } + } + } + g_free(canonical); + + return result; +} + +static void process_hsts_header(SoupMessage *msg, gpointer data) +{ + HSTSProvider *provider = (HSTSProvider*)data; + SoupURI *uri = soup_message_get_uri(msg); + const char *header, *host = soup_uri_get_host(uri); + SoupMessageHeaders *hdrs; + + if (!g_hostname_is_ip_address(host) + && (soup_message_get_flags(msg) & SOUP_MESSAGE_CERTIFICATE_TRUSTED) + ){ + g_object_get(G_OBJECT(msg), SOUP_MESSAGE_RESPONSE_HEADERS, &hdrs, NULL); + + /* TODO according to RFC 6797 8.1 we must only use the first header */ + header = soup_message_headers_get_one(hdrs, HSTS_HEADER_NAME); + if (header) { + parse_hsts_header(provider, host, header); + } + } +} + +/** + * Parses the hsts directives from given header like specified in RFC 6797 6.1 + */ +static void parse_hsts_header(HSTSProvider *provider, + const char *host, const char *header) +{ + GHashTable *directives = soup_header_parse_semi_param_list(header); + gint64 max_age = 0; + gboolean include_sub_domains = false; + GHashTableIter iter; + gpointer key, value; + gboolean success = true; + + HSTSProviderClass *klass = g_type_class_ref(HSTS_TYPE_PROVIDER); + + g_hash_table_iter_init(&iter, directives); + while (g_hash_table_iter_next(&iter, &key, &value)) { + /* parse the max-age directive */ + if (!g_ascii_strncasecmp(key, "max-age", 7)) { + /* max age needs a value */ + if (value) { + max_age = g_ascii_strtoll(value, NULL, 10); + if (max_age < 0) { + success = false; + break; + } + } else { + success = false; + break; + } + } else if (g_ascii_strncasecmp(key, "includeSubDomains", 17)) { + /* includeSubDomains must not have a value */ + if (!value) { + include_sub_domains = true; + } else { + success = false; + break; + } + } + } + soup_header_free_param_list(directives); + g_type_class_unref(klass); + + if (success) { + /* remove host if max-age = 0 RFC 6797 6.1.1 */ + if (max_age == 0) { + remove_host_entry(provider, host); + } else { + add_host_entry(provider, host, get_new_entry(max_age, include_sub_domains)); + } + } +} + +/** + * Create a new hsts entry for given data. + * Returned entry have to be freed if no more used. + */ +static HSTSEntry *get_new_entry(gint64 max_age, gboolean include_sub_domains) +{ + HSTSEntry *entry = g_new(HSTSEntry, 1); + entry->expires_at = g_get_real_time(); + if (max_age > (G_MAXINT64 - entry->expires_at)/G_USEC_PER_SEC) { + entry->expires_at = G_MAXINT64; + } else { + entry->expires_at += max_age * G_USEC_PER_SEC; + } + entry->include_sub_domains = include_sub_domains; + + return entry; +} + + +/** + * Adds the host to the known host, if it already exists it replaces it with + * the information contained in entry according to RFC 6797 8.1. + */ +static void add_host_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->whitelist, g_hostname_to_unicode(host), entry); +} + +/** + * Removes stored entry for given host. + */ +static void remove_host_entry(HSTSProvider *provider, const char *host) +{ + HSTSProviderPrivate *priv = HSTS_PROVIDER_GET_PRIVATE(provider); + char *canonical = g_hostname_to_unicode(host); + + g_hash_table_remove(priv->whitelist, canonical); + g_free(canonical); +} + +/** + * Initialise the SoupSessionFeature interface. + */ +static void session_feature_init( + SoupSessionFeatureInterface *inteface, gpointer data) +{ + inteface->request_queued = request_queued; + inteface->request_started = request_started; + inteface->request_unqueued = request_unqueued; +} + +/** + * Check if the host is known and switch the URI scheme to https. + */ +static void 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 + && should_secure_host(provider, soup_uri_get_host(uri)) + ) { + soup_uri_set_scheme(uri, SOUP_URI_SCHEME_HTTPS); + /* change port if the is explicitly set */ + 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(process_hsts_header), feature + ); + } +} + +static void request_started(SoupSessionFeature *feature, + SoupSession *session, SoupMessage *msg, SoupSocket *socket) +{ + HSTSProvider *provider = HSTS_PROVIDER(feature); + SoupURI *uri = soup_message_get_uri(msg); + const char *host = soup_uri_get_host(uri); + if (should_secure_host(provider, host)) { + if (soup_uri_get_scheme(uri) != SOUP_URI_SCHEME_HTTPS + || !(soup_message_get_flags(msg) & SOUP_MESSAGE_CERTIFICATE_TRUSTED) + ) { + soup_session_cancel_message(session, msg, SOUP_STATUS_SSL_FAILED); + } + } +} + +static void request_unqueued(SoupSessionFeature *feature, + SoupSession *session, SoupMessage *msg) +{ + g_signal_handlers_disconnect_by_func(msg, process_hsts_header, feature); +} diff --git a/src/hsts.h b/src/hsts.h new file mode 100644 index 0000000..3bb4030 --- /dev/null +++ b/src/hsts.h @@ -0,0 +1,53 @@ +/** + * vimb - a webkit based vim like browser. + * + * Copyright (C) 2012-2014 Daniel Carl + * + * 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, see http://www.gnu.org/licenses/. + */ + +#ifndef _HSTS_H +#define _HSTS_H + +#include + +#define HSTS_TYPE_PROVIDER (hsts_provider_get_type()) +#define HSTS_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), HSTS_TYPE_PROVIDER, HSTSProvider)) +#define HSTS_PROVIDER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), HSTS_TYPE_PROVIDER, HSTSProviderClass)) +#define HSTS_IS_PROVIDER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), HSTS_TYPE_PROVIDER)) +#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)) + +#ifndef false + #define false FALSE +#endif +#ifndef true + #define true TRUE +#endif + +/* public interface of the provider */ +typedef struct { + GObject parent_instance; +} HSTSProvider; + +/* class members of the provider */ +typedef struct { + GObjectClass parent_class; +} HSTSProviderClass; + + +GType hsts_provider_get_type(void); +HSTSProvider *hsts_provider_new(void); + +#endif /* end of include guard: _HSTS_H */ diff --git a/src/main.c b/src/main.c index 6149d5d..9ea0f8f 100644 --- a/src/main.c +++ b/src/main.c @@ -371,6 +371,7 @@ void vb_quit(void) mode_cleanup(); setting_cleanup(); history_cleanup(); + session_cleanup(); for (int i = 0; i < FILES_LAST; i++) { g_free(vb.files[i]); diff --git a/src/main.h b/src/main.h index 3e2eef9..83779ac 100644 --- a/src/main.h +++ b/src/main.h @@ -201,6 +201,7 @@ typedef enum { FILES_QUEUE, #endif FILES_USER_STYLE, + FILES_HSTS, FILES_LAST } VbFile; diff --git a/src/session.c b/src/session.c index 8b217c5..b56cda1 100644 --- a/src/session.c +++ b/src/session.c @@ -21,6 +21,7 @@ #include #include "main.h" #include "session.h" +#include "hsts.h" #ifdef FEATURE_COOKIE @@ -49,6 +50,7 @@ static void cookiejar_set_property(GObject *self, guint prop_id, const GValue *value, GParamSpec *pspec); #endif +static HSTSProvider *hsts; extern VbCore vb; @@ -66,6 +68,16 @@ void session_init(void) SOUP_SESSION_FEATURE(cookiejar_new(vb.files[FILES_COOKIE], false)) ); #endif + hsts = hsts_provider_new(); + soup_session_add_feature( + vb.session, + SOUP_SESSION_FEATURE(hsts) + ); +} + +void session_cleanup(void) +{ + g_object_unref(hsts); } #ifdef FEATURE_COOKIE diff --git a/src/session.h b/src/session.h index 5721bea..dcc4cf7 100644 --- a/src/session.h +++ b/src/session.h @@ -21,5 +21,6 @@ #define _SESSION_H void session_init(void); +void session_cleanup(void); #endif /* end of include guard: _SESSION_H */