Subject: Add functions for key manipulation Origin: v7-6-g6090505 Upstream-Author: Sergio Correia Date: Fri Nov 27 09:30:49 2020 -0300 We currently rely on the tangd-update script to read the keys and generate signed advertisements as well as JWKs for key derivation. Whenever there is a change in the directory containing the actual keys, we run tangd-update through a systemd file watching mechanism, so that we can have a cache directory with updated advertisements + JWKs. As reported in #23 and #24, this mechanism can be unreliable in certain situations, and having up-to-date information on the keys that are available is critical to tang, so the idea here is to remove this dependency on external scripts (e.g. tangd-update) and move this computation to tang itself. In this commit we add the related functions for key manipulation so that in a next step we can start using it in tang. --- /dev/null +++ b/src/keys.c @@ -0,0 +1,455 @@ +/* vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: */ +/* + * Copyright (c) 2020 Red Hat, Inc. + * Author: Sergio Correia + * + * 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 . + */ + +#include +#include +#include +#include + +#include +#include +#include + +#include "keys.h" + +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + +static const char** +supported_hashes(void) +{ + /* TODO: check if jose has a way to export the hash algorithms it + * supports. */ + static const char* hashes[] = {"S1", "S224", "S256", "S384", "S512", NULL}; + return hashes; +} + +static int +is_hash(const char* alg) +{ + if (!alg) { + return 0; + } + + const char** algs = supported_hashes(); + for (size_t a = 0; algs[a]; a++) { + if (strcmp(alg, algs[a]) == 0) { + return 1; + } + } + return 0; +} + +static json_t* +jwk_generate(const char* alg) +{ + json_auto_t* jalg = json_pack("{s:s}", "alg", alg); + if (!jalg) { + fprintf(stderr, "Error packing JSON with alg %s\n", alg); + return NULL; + } + + if (!jose_jwk_gen(NULL, jalg)) { + fprintf(stderr, "Error generating JWK with alg %s\n", alg); + return NULL; + } + + return json_incref(jalg); +} + +static char* +jwk_thumbprint(const json_t* jwk, const char* alg) +{ + size_t elen = 0; + size_t dlen = 0; + + if (!jwk) { + fprintf(stderr, "Invalid JWK\n"); + return NULL; + } + + if (!alg || !is_hash(alg)) { + fprintf(stderr, "Invalid hash algorithm (%s)\n", alg); + return NULL; + } + + dlen = jose_jwk_thp_buf(NULL, NULL, alg, NULL, 0); + if (dlen == SIZE_MAX) { + fprintf(stderr, "Error determining hash size for %s\n", alg); + return NULL; + } + + elen = jose_b64_enc_buf(NULL, dlen, NULL, 0); + if (elen == SIZE_MAX) { + fprintf(stderr, "Error determining encoded size for %s\n", alg); + return NULL; + } + + uint8_t dec[dlen]; + char enc[elen]; + + if (!jose_jwk_thp_buf(NULL, jwk, alg, dec, sizeof(dec))) { + fprintf(stderr, "Error making thumbprint\n"); + return NULL; + } + + if (jose_b64_enc_buf(dec, dlen, enc, sizeof(enc)) != elen) { + fprintf(stderr, "Error encoding data Base64\n"); + return NULL; + } + + return strndup(enc, elen); +} + +void +free_tang_keys_info(struct tang_keys_info* tki) +{ + if (!tki) { + return; + } + + json_t* to_free[] = {tki->m_keys, tki->m_rotated_keys, + tki->m_payload, tki->m_sign + }; + size_t len = sizeof(to_free) / sizeof(to_free[0]); + + for (size_t i = 0; i < len; i++) { + if (to_free[i] == NULL) { + continue; + } + json_decref(to_free[i]); + } + free(tki); +} + +void +cleanup_tang_keys_info(struct tang_keys_info** tki) +{ + if (!tki || !*tki) { + return; + } + free_tang_keys_info(*tki); + *tki = NULL; +} + +static struct tang_keys_info* +new_tang_keys_info(void) +{ + struct tang_keys_info* tki = calloc(1, sizeof(*tki)); + if (!tki) { + return NULL; + } + + tki->m_keys = json_array(); + tki->m_rotated_keys = json_array(); + tki->m_payload = json_array(); + tki->m_sign = json_array(); + + if (!tki->m_keys || !tki->m_rotated_keys || + !tki->m_payload || !tki->m_sign) { + free_tang_keys_info(tki); + return NULL; + } + tki->m_keys_count = 0; + return tki; +} + +static int +jwk_valid_for(const json_t* jwk, const char* use) +{ + if (!jwk || !use) { + return 0; + } + return jose_jwk_prm(NULL, jwk, false, use); +} + +static int +jwk_valid_for_signing_and_verifying(const json_t* jwk) +{ + const char* uses[] = {"sign", "verify", NULL}; + int ret = 1; + for (int i = 0; uses[i]; i++) { + if (!jwk_valid_for(jwk, uses[i])) { + ret = 0; + break; + } + } + return ret; +} + +static int +jwk_valid_for_signing(const json_t* jwk) +{ + return jwk_valid_for(jwk, "sign"); +} + +static int +jwk_valid_for_deriving_keys(const json_t* jwk) +{ + return jwk_valid_for(jwk, "deriveKey"); +} + +static void +cleanup_str(char** str) +{ + if (!str || !*str) { + return; + } + free(*str); + *str = NULL; +} + +static json_t* +jwk_sign(const json_t* to_sign, const json_t* sig_keys) +{ + if (!sig_keys || !json_is_array(sig_keys) || !json_is_array(to_sign)) { + return NULL; + } + + json_auto_t* to_sign_copy = json_deep_copy(to_sign); + if (!jose_jwk_pub(NULL, to_sign_copy)) { + fprintf(stderr, "Error removing private material from data to sign\n"); + } + + json_auto_t* payload = json_pack("{s:O}", "keys", to_sign_copy); + json_auto_t* sig_template = json_pack("{s:{s:s}}", + "protected", "cty", "jwk-set+json"); + + /* Use the template with the signing keys. */ + json_auto_t* sig_template_arr = json_array(); + size_t arr_size = json_array_size(sig_keys); + for (size_t i = 0; i < arr_size; i++) { + if (json_array_append(sig_template_arr, sig_template) == -1) { + fprintf(stderr, "Unable to append sig template to array\n"); + return NULL; + } + } + + __attribute__ ((__cleanup__(cleanup_str))) char* data_to_sign = json_dumps(payload, 0); + json_auto_t* jws = json_pack("{s:o}", "payload", + jose_b64_enc(data_to_sign, strlen(data_to_sign))); + + if (!jose_jws_sig(NULL, jws, sig_template_arr, sig_keys)) { + fprintf(stderr, "Error trying to jose_jws_sign\n"); + return NULL; + } + return json_incref(jws); +} + +static json_t* +find_by_thp(struct tang_keys_info* tki, const char* target) +{ + if (!tki) { + return NULL; + } + + json_auto_t* keys = json_deep_copy(tki->m_keys); + json_array_extend(keys, tki->m_rotated_keys); + + size_t idx; + json_t* jwk; + const char** hashes = supported_hashes(); + json_array_foreach(keys, idx, jwk) { + for (int i = 0; hashes[i]; i++) { + __attribute__ ((__cleanup__(cleanup_str))) char* thumbprint = jwk_thumbprint(jwk, hashes[i]); + if (strcmp(thumbprint, target) != 0) { + continue; + } + + if (jwk_valid_for_deriving_keys(jwk)) { + return json_incref(jwk); + } else if (jwk_valid_for_signing(jwk)) { + json_auto_t* sign = json_deep_copy(tki->m_sign); + if (json_array_append(sign, jwk) == -1) { + return NULL; + } + json_auto_t* jws = jwk_sign(tki->m_payload, sign); + if (!jws) { + return NULL; + } + return json_incref(jws); + } + } + } + return NULL; +} + +static int +prepare_payload_and_sign(struct tang_keys_info* tki) +{ + if (!tki) { + return 0; + } + + size_t idx; + json_t* jwk; + json_array_foreach(tki->m_keys, idx, jwk) { + if (jwk_valid_for_signing_and_verifying(jwk)) { + if (json_array_append(tki->m_sign, jwk) == -1) { + continue; + } + if (json_array_append(tki->m_payload, jwk) == -1) { + continue; + } + } else if (jwk_valid_for_deriving_keys(jwk)) { + if (json_array_append(tki->m_payload, jwk) == -1) { + continue; + } + } + } + if (json_array_size(tki->m_sign) == 0 || json_array_size(tki->m_payload) == 0) { + return 0; + } + return 1; +} + +static int +create_new_keys(const char* jwkdir) +{ + const char** hashes = supported_hashes(); + const char* alg[] = {"ES512", "ECMR", NULL}; + char path[PATH_MAX]; + for (int i = 0; alg[i] != NULL; i++) { + json_auto_t* jwk = jwk_generate(alg[i]); + if (!jwk) { + return 0; + } + __attribute__ ((__cleanup__(cleanup_str))) char* thp = jwk_thumbprint(jwk, hashes[0]); + if (!thp) { + return 0; + } + if (snprintf(path, PATH_MAX, "%s/%s.jwk", jwkdir, thp) < 0) { + fprintf(stderr, "Unable to prepare variable with file full path (%s)\n", thp); + return 0; + } + path[sizeof(path) - 1] = '\0'; + if (json_dump_file(jwk, path, 0) == -1) { + fprintf(stderr, "Error saving JWK to file (%s)\n", path); + return 0; + } + } + return 1; +} + +static struct tang_keys_info* +load_keys(const char* jwkdir) +{ + struct tang_keys_info* tki = new_tang_keys_info(); + if (!tki) { + return NULL; + } + + struct dirent* d; + DIR* dir = opendir(jwkdir); + if (!dir) { + free_tang_keys_info(tki); + return NULL; + } + + char filepath[PATH_MAX]; + const char* pattern = ".jwk"; + while ((d = readdir(dir)) != NULL) { + if (strcmp(d->d_name, ".") == 0 || strcmp(d->d_name, "..") == 0) { + continue; + } + + char* dot = strrchr(d->d_name, '.'); + if (!dot) { + continue; + } + + if (strcmp(dot, pattern) == 0) { + /* Found a file with .jwk extension. */ + if (snprintf(filepath, PATH_MAX, "%s/%s", jwkdir, d->d_name) < 0) { + fprintf(stderr, "Unable to prepare variable with file full path (%s); skipping\n", d->d_name); + continue; + } + filepath[sizeof(filepath) - 1] = '\0'; + json_auto_t* json = json_load_file(filepath, 0, NULL); + if (!json) { + fprintf(stderr, "Invalid JSON file (%s); skipping\n", filepath); + continue; + } + + json_t* arr = tki->m_keys; + if (d->d_name[0] == '.') { + arr = tki->m_rotated_keys; + } + if (json_array_append(arr, json) == -1) { + fprintf(stderr, "Unable to append JSON (%s) to array; skipping\n", d->d_name); + continue; + } + tki->m_keys_count++; + } + } + closedir(dir); + return tki; +} + +struct tang_keys_info* +read_keys(const char* jwkdir) +{ + struct tang_keys_info* tki = load_keys(jwkdir); + if (!tki) { + return NULL; + } + + if (tki->m_keys_count == 0) { + /* Let's attempt to create a new pair of keys. */ + free_tang_keys_info(tki); + if (!create_new_keys(jwkdir)) { + return NULL; + } + tki = load_keys(jwkdir); + } + + if (!prepare_payload_and_sign(tki)) { + free_tang_keys_info(tki); + return NULL; + } + return tki; +} + +json_t* +find_jws(struct tang_keys_info* tki, const char* thp) +{ + if (!tki) { + return NULL; + } + + if (thp == NULL) { + /* Default advertisement. */ + json_auto_t* jws = jwk_sign(tki->m_payload, tki->m_sign); + if (!jws) { + return NULL; + } + return json_incref(jws); + } + return find_by_thp(tki, thp); +} + +json_t* +find_jwk(struct tang_keys_info* tki, const char* thp) +{ + if (!tki || !thp) { + return NULL; + } + return find_by_thp(tki, thp); +} --- /dev/null +++ b/src/keys.h @@ -0,0 +1,45 @@ +/* vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: */ +/* + * Copyright (c) 2020 Red Hat, Inc. + * Author: Sergio Correia + * + * 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 . + */ + +#pragma once + +#include +#include + +struct tang_keys_info { + /* Arrays. */ + json_t* m_keys; /* Regular keys. */ + json_t* m_rotated_keys; /* Rotated keys. */ + + json_t* m_payload; /* Payload made of regular keys capable of + * either signing+verifying or deriving new + * keys. */ + + json_t* m_sign; /* Set of signing keys made from regular + keys. */ + + size_t m_keys_count; /* Number of keys (regular + rotated). */ + +}; + +void cleanup_tang_keys_info(struct tang_keys_info**); +void free_tang_keys_info(struct tang_keys_info*); +struct tang_keys_info* read_keys(const char* /* jwkdir */); +json_t* find_jws(struct tang_keys_info* /* tki */, const char* /* thp */); +json_t* find_jwk(struct tang_keys_info* /* tki */, const char* /* thp */);