diff options
Diffstat (limited to 'src/util/convert_transport_security.c')
-rw-r--r-- | src/util/convert_transport_security.c | 397 |
1 files changed, 397 insertions, 0 deletions
diff --git a/src/util/convert_transport_security.c b/src/util/convert_transport_security.c new file mode 100644 index 00000000..34dd849a --- /dev/null +++ b/src/util/convert_transport_security.c @@ -0,0 +1,397 @@ +/* + * Copyright (c) 2012 Adam Ehlers Nyholm Thomsen + * Copyright (c) 2013 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. + */ +#define _XOPEN_SOURCE 700 +#define _POSIX_C_SOURCE 200809L +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <gnutls/gnutls.h> +#include <gnutls/x509.h> +#include <glib.h> +#include <glib/gstdio.h> +#include <json.h> + +/* Converts the static .certs and .json whitelist to a header file of the + * apropriate type. + * + * Warning: This file is slightly non portable as it uses getline. */ + + +/* Indicates whether a given pinset included a list of good certificates and/or + * a list of bad certificates. */ +typedef enum _has_certs { + HAS_GOOD_CERTS = 1, + HAS_BAD_CERTS = 2, +} has_certs; + +/* Whether a certificate is a good certificate or a bad certificate */ +typedef enum _cert_type { + GOOD_CERT, + BAD_CERT, +} cert_type; + +/* Maps pinset name to has_certs. + */ +GHashTable *pins; + +#define cert_filename "transport_security_state_static.certs" +#define json_filename "transport_security_state_static.json" +#define certificate_begin "-----BEGIN CERTIFICATE-----" +#define certificate_end "-----END CERTIFICATE-----" +#define sha1_prefix "sha1/" +#define cert_template "static const char s_hsts_cert_hash_%s[] = \"%s\";\n" + +#define cert_list_template_begin "static const char * const s_hsts_cert_list_%s_%s[] = {\n" +#define cert_list_template_entry " s_hsts_cert_hash_%s,\n" +#define cert_list_template_end " NULL,\n};\n\n" + +#define entry_list_begin "static const HSTSPreloadEntry s_hsts_preload[] = {\n" +#define entry_list_end "};\n" +#define entry_list_length "static const size_t s_hsts_preload_length = %zu\n;" + +const char *gboolean_to_string(gboolean val){ + return val ? "true" : "false"; +} +const char *cert_type_to_string(cert_type val){ + return val == GOOD_CERT ? "good" : "bad"; +} +void print_has_certs(const char *name, has_certs cert_status, cert_type val){ + if(cert_status & ((val == GOOD_CERT) ? HAS_GOOD_CERTS : HAS_BAD_CERTS)) { + printf("s_hsts_cert_list_%s_%s", cert_type_to_string(val), name); + } else + printf("NULL"); +} +void print_entry_list_entry(const char *host, const char *pin_name, gboolean hsts, gboolean sub_domains){ + has_certs cert_status = pin_name != NULL ? *((has_certs *)g_hash_table_lookup(pins, pin_name)) : 0; + char *host_safe = g_strescape(host, ""); + printf(" {\"%s\", ", host); + g_free(host_safe); + print_has_certs(pin_name, cert_status, GOOD_CERT); + printf(", "); + print_has_certs(pin_name, cert_status, BAD_CERT); + printf(", "); + printf("%s, %s},\n", gboolean_to_string(hsts), gboolean_to_string(sub_domains)); +} + +/* The ID size should be 20, but give some room for changes */ +#define MAX_ID_SIZE 4096 + +/* Parse the certificate file and for each certificate print the base64 encoded + * certificate key id, to be used in pinsets */ +gboolean parse_certs(const char *filename) +{ + FILE *file = g_fopen(filename, "r"); + char *line = NULL; + size_t line_size = 0; + size_t buffer_size = 4096; + size_t buffer_used = 0; + unsigned char *buffer = g_malloc(sizeof(unsigned char)*buffer_size); + while(getline(&line, &line_size, file) >= 0) + { + g_strstrip(line); + size_t len = strlen(line); + if(len == 0 || line[0] == '#') + continue; /* Ignore comments and pure whitespace lines */ + char *name = g_strdup(line); + char *key_id_base64; + + if(getline(&line, &line_size, file) < 0) + { + fprintf(stderr, "Unexpected end of file while parsing %s\n", name); + return FALSE; + } + g_strstrip(line); + if(g_str_has_prefix(line, certificate_begin)) + { + /* If it is a certificate entry: base64 decode the certificate, + * load it using gnutls, and compute the base64 encoded key id. + */ + gint state = 0; + guint save = 0; + buffer_used = 0; + ssize_t read; + while((read = getline(&line, &line_size, file)) >= 0 && !g_str_has_prefix(line, certificate_end)) + { + /* Read certificate line by line and base64 decode */ + g_strstrip(line); + size_t len = strlen(line); + gboolean to_realloc = FALSE; + while(len > buffer_size - buffer_used - 3) + { + to_realloc = TRUE; + buffer_size *= 2; + } + if(to_realloc) + { + /* Increase buffer_size if it is a long certificate -- this + * should never really happen */ + fprintf(stderr, "Warning: Increasing buffer size to %zd\n", buffer_size); + buffer = g_realloc(buffer, buffer_size); + } + buffer_used += g_base64_decode_step(line, len, &buffer[buffer_used], &state, &save); + } + if(read < 0) + { + fprintf(stderr, "Unexpected end of file while parsing base64 certificate of %s\n", name); + return FALSE; + } + gnutls_datum_t binary; + binary.data = buffer; + binary.size = buffer_used; + + /* Load the certificate and compute the key id */ + gnutls_x509_crt_t cert; + gnutls_x509_crt_init(&cert); + int err; + if((err = gnutls_x509_crt_import(cert, &binary, GNUTLS_X509_FMT_DER)) != GNUTLS_E_SUCCESS) + { + fprintf(stderr, "Error while decoding certificate of %s, error was %d, is it perhaps PEM encoded?\n", name, err); + return FALSE; + } + unsigned char key_id[MAX_ID_SIZE]; + size_t key_id_size = MAX_ID_SIZE; + if((err = gnutls_x509_crt_get_key_id(cert, 0, key_id, &key_id_size)) != GNUTLS_E_SUCCESS) + { + fprintf(stderr, "Couldn't retrieve the key id for the certificate of %s, error was %d\n", name, err); + return FALSE; + } + if(key_id_size != 20) + { + /* This might be problematic, I don't know */ + fprintf(stderr, "Warning: Key id for %s isn't 20 bytes long, this means it probably isn't a sha-1 hash...\n", name); + } + key_id_base64 = g_base64_encode(key_id, key_id_size); + gnutls_x509_crt_deinit(cert); + } + else if(g_str_has_prefix(line, sha1_prefix)) + { + /* If it is given as a sha-1 hash directly */ + key_id_base64 = g_strdup(&line[strlen(sha1_prefix)]); + } + else + { + fprintf(stderr, "Unrecognised line: %s\n", line); + return FALSE; + } + + printf(cert_template, name, key_id_base64); + + g_free(name); + g_free(key_id_base64); + } + printf("\n"); + g_free(buffer); + free(line); + fclose(file); + return TRUE; +} + + +/* Writes a list of certificate id names + * Params: + * name - The name of the pinset + * type - Whether it is a list of good or bad certificates + * list - The json_array of certificate id names + */ +gboolean write_cert_list(const char *name, cert_type type, has_certs *certs, json_object *list) +{ + if(list == NULL) + return TRUE; + if(!json_object_is_type(list, json_type_array)) + return FALSE; + int len = json_object_array_length(list); + printf(cert_list_template_begin, cert_type_to_string(type), name); + int i; + for(i = 0; i < len; i++) + { + printf(cert_list_template_entry, json_object_get_string(json_object_array_get_idx(list, i))); + } + printf(cert_list_template_end); + *certs |= (type == GOOD_CERT) ? HAS_GOOD_CERTS : HAS_BAD_CERTS; + return TRUE; +} + +/* Allocates a new has_certs enum and initializes it to 0(No certificates) */ +has_certs *has_certs_new() +{ + has_certs *var = g_malloc(sizeof(has_certs)); + *var = 0; + return var; +} + +/* For each pinset check whether it has a list of good certificates and if so + * print, and do likewise for the bad certificates */ +gboolean handle_pinsets(json_object *pinsets) +{ + int len = json_object_array_length(pinsets), i; + for(i = 0; i < len; i++) + { + json_object *pin_list = json_object_array_get_idx(pinsets, i); + if(pin_list == NULL || !json_object_is_type(pin_list, json_type_object)) + { + fprintf(stderr, "pinset %d is not of type object\n", i); + return FALSE; + } + json_object *name_obj, *good_hashes, *bad_hashes; + if((name_obj = json_object_object_get(pin_list, "name")) == NULL || !json_object_is_type(name_obj, json_type_string)) + { + fprintf(stderr, "Couldn't get name from pinset %d\n", i); + return FALSE; + } + const char *name = json_object_get_string(name_obj); + + good_hashes = json_object_object_get(pin_list, "static_spki_hashes"); + bad_hashes = json_object_object_get(pin_list, "bad_static_spki_hashes"); + has_certs *certs = has_certs_new(); + if(!write_cert_list(name, GOOD_CERT, certs, good_hashes) || + !write_cert_list(name, BAD_CERT, certs, bad_hashes)) + { + fprintf(stderr, "Couldn't parse hash lists for pinset %s\n", name); + return FALSE; + } + + g_hash_table_insert(pins, g_strdup(name), certs); + } + return TRUE; +} + +/* For each entry convert it into the structure of an HSTSPreloadEntry and + * print it as c code on stdout. + */ +gboolean handle_entries(json_object *entries) +{ + int len = json_object_array_length(entries); + printf(entry_list_begin); + int i; + for(i = 0; i < len; i++) + { + json_object *entry = json_object_array_get_idx(entries, i); + if(entry == NULL || !json_object_is_type(entry, json_type_object)) + { + fprintf(stderr, "Entry %d wasn't a json object\n", i); + return FALSE; + } + + /* Get hostname */ + json_object *name_obj; + if((name_obj = json_object_object_get(entry, "name")) == NULL || + !json_object_is_type(name_obj, json_type_string)) + { + fprintf(stderr, "Couldn't process name from entry %d\n", i); + return FALSE; + } + const char *name = json_object_get_string(name_obj); + char *host = g_hostname_to_unicode(name); + + /* Get whether to enable hsts for host */ + json_object *mode = json_object_object_get(entry, "mode"); + gboolean hsts = mode != NULL; + if(hsts && strcmp(json_object_get_string(mode), "force-https") != 0) + { + fprintf(stderr, "Unknown mode for entry %s: %s", name, json_object_get_string(mode)); + } + + /* Get sub domains directive */ + json_object *include_subdomains = json_object_object_get(entry, "include_subdomains"); + gboolean sub_domains = include_subdomains != NULL && + json_object_get_boolean(include_subdomains); + if(include_subdomains != NULL && !json_object_is_type(include_subdomains, json_type_boolean)) + { + fprintf(stderr, "include_subdomains for entry %s wasn't of type boolean\n", name); + return FALSE; + } + + /* Get pins directive */ + json_object *entry_pins; + const char *pin_name = NULL; + if((entry_pins = json_object_object_get(entry, "pins")) != NULL) + { + if(!json_object_is_type(entry_pins, json_type_string)) + { + fprintf(stderr, "non string pins entry for %s\n", name); + return FALSE; + } + pin_name = json_object_get_string(entry_pins); + if(g_hash_table_lookup(pins, pin_name) == NULL) + { + fprintf(stderr, "unrecognised pin name in entry for %s\n", name); + } + } + + print_entry_list_entry(host, pin_name, hsts, sub_domains); + g_free(host); + } + size_t length = len; + printf(entry_list_end); + printf(entry_list_length, length); + return TRUE; +} + +/* Parse the json file and print the relevant c code */ +gboolean parse_json(const char *filename) +{ + /* Read and parse the file */ + char *file; + if(!g_file_get_contents(filename, &file, NULL, NULL)) + { + fprintf(stderr, "Couldn't read JSON file: %s\n", filename); + return FALSE; + } + + json_object *json = json_tokener_parse(file); + if(json == NULL) + { + fprintf(stderr, "There was an error while parsing %s\n", filename); + return FALSE; + } + + /* Parse and handle the pinsets entry */ + json_object *pinsets; + pins = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); + if((pinsets = json_object_object_get(json, "pinsets")) == NULL || !json_object_is_type(pinsets, json_type_array) || + !handle_pinsets(pinsets)) + { + fprintf(stderr, "Error while handling pinsets\n"); + return FALSE; + } + + /* Parse and handle the list of hostnames */ + json_object *entries; + if((entries = json_object_object_get(json, "entries")) == NULL || !json_object_is_type(entries, json_type_array) || + !handle_entries(entries)) + { + fprintf(stderr, "Error while handling entries\n"); + return FALSE; + } + + g_free(file); + g_hash_table_destroy(pins); + json_object_put(json); + return TRUE; +} + +int main(){ + gnutls_global_init(); + if(!parse_certs(cert_filename)) + return -1; + if(!parse_json(json_filename)) + return -1; + gnutls_global_deinit(); + return 0; +} |