/* * Copyright (c) 2012 Adam Ehlers Nyholm Thomsen * Copyright (c) 2013 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. */ #define _XOPEN_SOURCE 700 #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include /* 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; }