diff options
Diffstat (limited to 'src/utils/keymgr')
-rw-r--r-- | src/utils/keymgr/bind_privkey.c | 411 | ||||
-rw-r--r-- | src/utils/keymgr/bind_privkey.h | 72 | ||||
-rw-r--r-- | src/utils/keymgr/functions.c | 1135 | ||||
-rw-r--r-- | src/utils/keymgr/functions.h | 62 | ||||
-rw-r--r-- | src/utils/keymgr/main.c | 405 | ||||
-rw-r--r-- | src/utils/keymgr/offline_ksk.c | 549 | ||||
-rw-r--r-- | src/utils/keymgr/offline_ksk.h | 35 |
7 files changed, 2669 insertions, 0 deletions
diff --git a/src/utils/keymgr/bind_privkey.c b/src/utils/keymgr/bind_privkey.c new file mode 100644 index 0000000..70f03e3 --- /dev/null +++ b/src/utils/keymgr/bind_privkey.c @@ -0,0 +1,411 @@ +/* Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <https://www.gnu.org/licenses/>. + */ + +#include <string.h> + +#include "contrib/ctype.h" +#include "contrib/strtonum.h" +#include "libdnssec/binary.h" +#include "libdnssec/error.h" +#include "libdnssec/pem.h" +#include "libdnssec/shared/shared.h" +#include "utils/keymgr/bind_privkey.h" + +/* -- private key params conversion ---------------------------------------- */ + +/*! + * Private key attribute conversion. + */ +typedef struct param_t { + char *name; + size_t offset; + int (*parse_cb)(char *string, void *data); + void (*free_cb)(void *data); +} param_t; + +static int parse_algorithm(char *string, void *_algorithm); +static int parse_binary(char *string, void *_binary); +static int parse_time(char *string, void *_time); + +static void binary_free(void *_binary) +{ + dnssec_binary_t *binary = _binary; + dnssec_binary_free(binary); +} + +/*! + * Know attributes in private key file. + */ +const param_t PRIVKEY_CONVERSION_TABLE[] = { + #define o(field) offsetof(bind_privkey_t, field) + { "Algorithm", o(algorithm), parse_algorithm, NULL }, + { "Modulus", o(modulus), parse_binary, binary_free }, + { "PublicExponent", o(public_exponent), parse_binary, binary_free }, + { "PrivateExponent", o(private_exponent), parse_binary, binary_free }, + { "Prime1", o(prime_one), parse_binary, binary_free }, + { "Prime2", o(prime_two), parse_binary, binary_free }, + { "Exponent1", o(exponent_one), parse_binary, binary_free }, + { "Exponent2", o(exponent_two), parse_binary, binary_free }, + { "Coefficient", o(coefficient), parse_binary, binary_free }, + { "PrivateKey", o(private_key), parse_binary, binary_free }, + { "Created", o(time_created), parse_time, NULL }, + { "Publish", o(time_publish), parse_time, NULL }, + { "Activate", o(time_activate), parse_time, NULL }, + { "Revoke", o(time_revoke), parse_time, NULL }, + { "Inactive", o(time_inactive), parse_time, NULL }, + { "Delete", o(time_delete), parse_time, NULL }, + { NULL } + #undef o +}; + +/* -- attribute parsing ---------------------------------------------------- */ + +/*! + * Parse key algorithm field. + * + * Example: 7 (NSEC3RSASHA1) + * + * Only the numeric value is decoded, the rest of the value is ignored. + */ +static int parse_algorithm(char *string, void *_algorithm) +{ + char *end = string; + while (*end != '\0' && !is_space(*end)) { + end += 1; + } + *end = '\0'; + + uint8_t *algorithm = _algorithm; + int r = str_to_u8(string, algorithm); + + return (r == KNOT_EOK ? DNSSEC_EOK : DNSSEC_INVALID_KEY_ALGORITHM); +} + +/*! + * Parse binary data encoded in Base64. + * + * Example: AQAB + */ +static int parse_binary(char *string, void *_binary) +{ + dnssec_binary_t base64 = { + .data = (uint8_t *)string, + .size = strlen(string) + }; + + dnssec_binary_t *binary = _binary; + return dnssec_binary_from_base64(&base64, binary); +} + +#define LEGACY_DATE_FORMAT "%Y%m%d%H%M%S" + +/*! + * Parse timestamp in a format in \ref LEGACY_DATE_FORMAT. + * + * Example: 20140415151855 + */ +static int parse_time(char *string, void *_time) +{ + struct tm tm = { 0 }; + + char *end = strptime(string, LEGACY_DATE_FORMAT, &tm); + if (end == NULL || *end != '\0') { + return DNSSEC_MALFORMED_DATA; + } + + time_t *time = _time; + *time = timegm(&tm); + + return DNSSEC_EOK; +} + +/* -- key parsing ---------------------------------------------------------- */ + +/*! + * Strip string value of left and right whitespaces. + * + * \param[in,out] value Start of the string. + * \param[in,out] length Length of the string. + * + */ +static void strip(char **value, size_t *length) +{ + // strip from left + while (*length > 0 && is_space(**value)) { + *value += 1; + *length -= 1; + } + // strip from right + while (*length > 0 && is_space((*value)[*length - 1])) { + *length -= 1; + } +} + +/*! + * Parse one line of the private key file. + */ +static int parse_line(bind_privkey_t *params, char *line, size_t length) +{ + assert(params); + assert(line); + + strip(&line, &length); + if (length == 0) { + return KNOT_EOK; // blank line + } + + char *separator = memchr(line, ':', length); + if (!separator) { + return DNSSEC_MALFORMED_DATA; + } + + char *key = line; + size_t key_length = separator - key; + strip(&key, &key_length); + + char *value = separator + 1; + size_t value_length = (line + length) - value; + strip(&value, &value_length); + + if (key_length == 0 || value_length == 0) { + return DNSSEC_MALFORMED_DATA; + } + + key[key_length] = '\0'; + value[value_length] = '\0'; + + for (const param_t *p = PRIVKEY_CONVERSION_TABLE; p->name != NULL; p++) { + size_t name_length = strlen(p->name); + if (name_length != key_length) { + continue; + } + + if (strcasecmp(p->name, key) != 0) { + continue; + } + + return p->parse_cb(value, (void *)params + p->offset); + } + + // ignore unknown attributes + + return DNSSEC_EOK; +} + +int bind_privkey_parse(const char *filename, bind_privkey_t *params_ptr) +{ + _cleanup_fclose_ FILE *file = fopen(filename, "r"); + if (!file) { + return DNSSEC_NOT_FOUND; + } + + bind_privkey_t params = *params_ptr; + + _cleanup_free_ char *line = NULL; + size_t size = 0; + ssize_t read = 0; + while ((read = getline(&line, &size, file)) != -1) { + int r = parse_line(¶ms, line, read); + if (r != DNSSEC_EOK) { + bind_privkey_free(¶ms); + return r; + } + } + + *params_ptr = params; + + return DNSSEC_EOK; +} + +/* -- freeing -------------------------------------------------------------- */ + +/*! + * Free private key parameters. + */ +void bind_privkey_free(bind_privkey_t *params) +{ + if (!params) { + return; + } + + for (const param_t *p = PRIVKEY_CONVERSION_TABLE; p->name != NULL; p++) { + if (p->free_cb) { + p->free_cb((void *)params + p->offset); + } + } + + clear_struct(params); +} + +/* -- export to PEM -------------------------------------------------------- */ + +static int rsa_params_to_pem(const bind_privkey_t *params, dnssec_binary_t *pem) +{ + _cleanup_x509_privkey_ gnutls_x509_privkey_t key = NULL; + int result = gnutls_x509_privkey_init(&key); + if (result != GNUTLS_E_SUCCESS) { + return DNSSEC_ENOMEM; + } + + gnutls_datum_t m = binary_to_datum(¶ms->modulus); + gnutls_datum_t e = binary_to_datum(¶ms->public_exponent); + gnutls_datum_t d = binary_to_datum(¶ms->private_exponent); + gnutls_datum_t p = binary_to_datum(¶ms->prime_one); + gnutls_datum_t q = binary_to_datum(¶ms->prime_two); + gnutls_datum_t u = binary_to_datum(¶ms->coefficient); + + result = gnutls_x509_privkey_import_rsa_raw(key, &m, &e, &d, &p, &q, &u); + if (result != GNUTLS_E_SUCCESS) { + return DNSSEC_KEY_IMPORT_ERROR; + } + + return dnssec_pem_from_x509(key, pem); +} + +/*! + * \see lib/key/convert.h + */ +static gnutls_ecc_curve_t choose_ecdsa_curve(size_t pubkey_size) +{ + switch (pubkey_size) { +#ifdef HAVE_ED25519 + case 32: return GNUTLS_ECC_CURVE_ED25519; +#endif +#ifdef HAVE_ED448 + case 57: return GNUTLS_ECC_CURVE_ED448; +#endif + case 64: return GNUTLS_ECC_CURVE_SECP256R1; + case 96: return GNUTLS_ECC_CURVE_SECP384R1; + default: return GNUTLS_ECC_CURVE_INVALID; + } +} + +static void ecdsa_extract_public_params(dnssec_key_t *key, gnutls_ecc_curve_t *curve, + gnutls_datum_t *x, gnutls_datum_t *y) +{ + dnssec_binary_t pubkey = { 0 }; + dnssec_key_get_pubkey(key, &pubkey); + + *curve = choose_ecdsa_curve(pubkey.size); + + size_t param_size = pubkey.size / 2; + x->data = pubkey.data; + x->size = param_size; + y->data = pubkey.data + param_size; + y->size = param_size; +} + +static int ecdsa_params_to_pem(dnssec_key_t *dnskey, const bind_privkey_t *params, + dnssec_binary_t *pem) +{ + _cleanup_x509_privkey_ gnutls_x509_privkey_t key = NULL; + int result = gnutls_x509_privkey_init(&key); + if (result != GNUTLS_E_SUCCESS) { + return DNSSEC_ENOMEM; + } + + gnutls_ecc_curve_t curve = 0; + gnutls_datum_t x = { 0 }; + gnutls_datum_t y = { 0 }; + ecdsa_extract_public_params(dnskey, &curve, &x, &y); + + gnutls_datum_t k = binary_to_datum(¶ms->private_key); + + result = gnutls_x509_privkey_import_ecc_raw(key, curve, &x, &y, &k); + if (result != DNSSEC_EOK) { + return DNSSEC_KEY_IMPORT_ERROR; + } + + gnutls_x509_privkey_fix(key); + + return dnssec_pem_from_x509(key, pem); +} + +#if defined(HAVE_ED25519) || defined(HAVE_ED448) +static void eddsa_extract_public_params(dnssec_key_t *key, gnutls_ecc_curve_t *curve, + gnutls_datum_t *x) +{ + dnssec_binary_t pubkey = { 0 }; + dnssec_key_get_pubkey(key, &pubkey); + + *curve = choose_ecdsa_curve(pubkey.size); + + x->data = pubkey.data; + x->size = pubkey.size; +} + +static int eddsa_params_to_pem(dnssec_key_t *dnskey, const bind_privkey_t *params, + dnssec_binary_t *pem) +{ + _cleanup_x509_privkey_ gnutls_x509_privkey_t key = NULL; + int result = gnutls_x509_privkey_init(&key); + if (result != GNUTLS_E_SUCCESS) { + return DNSSEC_ENOMEM; + } + + gnutls_ecc_curve_t curve = 0; + gnutls_datum_t x = { 0 }; + eddsa_extract_public_params(dnskey, &curve, &x); + + gnutls_datum_t k = binary_to_datum(¶ms->private_key); + + result = gnutls_x509_privkey_import_ecc_raw(key, curve, &x, NULL, &k); + if (result != DNSSEC_EOK) { + return DNSSEC_KEY_IMPORT_ERROR; + } + + gnutls_x509_privkey_fix(key); + + return dnssec_pem_from_x509(key, pem); +} +#endif + +int bind_privkey_to_pem(dnssec_key_t *key, bind_privkey_t *params, dnssec_binary_t *pem) +{ + dnssec_key_algorithm_t algorithm = dnssec_key_get_algorithm(key); + switch (algorithm) { + case DNSSEC_KEY_ALGORITHM_RSA_SHA1: + case DNSSEC_KEY_ALGORITHM_RSA_SHA1_NSEC3: + case DNSSEC_KEY_ALGORITHM_RSA_SHA256: + case DNSSEC_KEY_ALGORITHM_RSA_SHA512: + return rsa_params_to_pem(params, pem); + case DNSSEC_KEY_ALGORITHM_ECDSA_P256_SHA256: + case DNSSEC_KEY_ALGORITHM_ECDSA_P384_SHA384: + return ecdsa_params_to_pem(key, params, pem); +#ifdef HAVE_ED25519 + case DNSSEC_KEY_ALGORITHM_ED25519: +#endif +#ifdef HAVE_ED448 + case DNSSEC_KEY_ALGORITHM_ED448: +#endif +#if defined(HAVE_ED25519) || defined(HAVE_ED448) + return eddsa_params_to_pem(key, params, pem); +#endif + default: + return DNSSEC_INVALID_KEY_ALGORITHM; + } +} + +void bind_privkey_to_timing(bind_privkey_t *params, knot_kasp_key_timing_t *timing) +{ + // timing->created remains "now" + timing->publish = (knot_time_t)params->time_publish; + timing->ready = 0; + timing->active = (knot_time_t)params->time_activate; + timing->retire = (knot_time_t)params->time_inactive; + timing->revoke = (knot_time_t)params->time_revoke; + timing->remove = (knot_time_t)params->time_delete; +} diff --git a/src/utils/keymgr/bind_privkey.h b/src/utils/keymgr/bind_privkey.h new file mode 100644 index 0000000..cdb4924 --- /dev/null +++ b/src/utils/keymgr/bind_privkey.h @@ -0,0 +1,72 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> +#include <time.h> + +#include "libdnssec/binary.h" +#include "knot/dnssec/kasp/policy.h" + +/*! + * Legacy private key parameters. + */ +typedef struct { + // key information + uint8_t algorithm; + + // RSA + dnssec_binary_t modulus; + dnssec_binary_t public_exponent; + dnssec_binary_t private_exponent; + dnssec_binary_t prime_one; + dnssec_binary_t prime_two; + dnssec_binary_t exponent_one; + dnssec_binary_t exponent_two; + dnssec_binary_t coefficient; + + // ECDSA + dnssec_binary_t private_key; + + // key lifetime + time_t time_created; + time_t time_publish; + time_t time_activate; + time_t time_revoke; + time_t time_inactive; + time_t time_delete; +} bind_privkey_t; + +/*! + * Extract parameters from legacy private key file. + */ +int bind_privkey_parse(const char *filename, bind_privkey_t *params); + +/*! + * Free private key parameters. + */ +void bind_privkey_free(bind_privkey_t *params); + +/*! + * Generate PEM from pub&priv key. + */ +int bind_privkey_to_pem(dnssec_key_t *key, bind_privkey_t *params, dnssec_binary_t *pem); + +/*! + * Extract timing info. + */ +void bind_privkey_to_timing(bind_privkey_t *params, knot_kasp_key_timing_t *timing); diff --git a/src/utils/keymgr/functions.c b/src/utils/keymgr/functions.c new file mode 100644 index 0000000..cd40f7f --- /dev/null +++ b/src/utils/keymgr/functions.c @@ -0,0 +1,1135 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <https://www.gnu.org/licenses/>. + */ + +#include <limits.h> +#include <string.h> +#include <strings.h> +#include <time.h> +#include <fcntl.h> + +#include "utils/keymgr/functions.h" + +#include "utils/common/msg.h" +#include "utils/keymgr/bind_privkey.h" +#include "contrib/base64.h" +#include "contrib/color.h" +#include "contrib/ctype.h" +#include "contrib/json.h" +#include "contrib/string.h" +#include "contrib/strtonum.h" +#include "contrib/tolower.h" +#include "contrib/wire_ctx.h" +#include "libdnssec/error.h" +#include "libdnssec/keyid.h" +#include "libdnssec/shared/shared.h" +#include "knot/dnssec/kasp/policy.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-events.h" +#include "knot/dnssec/zone-keys.h" +#include "knot/dnssec/zone-sign.h" +#include "libzscanner/scanner.h" + +int parse_timestamp(char *arg, knot_time_t *stamp) +{ + int ret = knot_time_parse("YMDhms|'now'+-#u|'t'+-#u|+-#u|'t'+-#|+-#|#", + arg, stamp); + if (ret < 0) { + ERR2("invalid timestamp: %s", arg); + return KNOT_EINVAL; + } + return KNOT_EOK; +} + +static bool init_timestamps(char *arg, knot_kasp_key_timing_t *timing) +{ + knot_time_t *dst = NULL; + + if (strncasecmp(arg, "created=", 8) == 0) { + dst = &timing->created; + } else if (strncasecmp(arg, "publish=", 8) == 0) { + dst = &timing->publish; + } else if (strncasecmp(arg, "ready=", 6) == 0) { + dst = &timing->ready; + } else if (strncasecmp(arg, "active=", 7) == 0) { + dst = &timing->active; + } else if (strncasecmp(arg, "retire=", 7) == 0) { + dst = &timing->retire; + } else if (strncasecmp(arg, "remove=", 7) == 0) { + dst = &timing->remove; + } else if (strncasecmp(arg, "pre_active=", 11) == 0) { + dst = &timing->pre_active; + } else if (strncasecmp(arg, "post_active=", 12) == 0) { + dst = &timing->post_active; + } else if (strncasecmp(arg, "retire_active=", 14) == 0) { + dst = &timing->retire_active; + } else if (strncasecmp(arg, "revoke=", 7) == 0) { + dst = &timing->revoke; + } else { + return false; + } + + knot_time_t stamp; + int ret = parse_timestamp(strchr(arg, '=') + 1, &stamp); + if (ret != KNOT_EOK) { + return true; + } + + *dst = stamp; + + return true; +} + +static bool str2bool(const char *s) +{ + switch (knot_tolower(s[0])) { + case '1': + case 'y': + case 't': + return true; + default: + return false; + } +} + +static void bitmap_set(kdnssec_generate_flags_t *bitmap, int flag, bool onoff) +{ + if (onoff) { + *bitmap |= flag; + } else { + *bitmap &= ~flag; + } +} + +static bool genkeyargs(int argc, char *argv[], bool just_timing, + kdnssec_generate_flags_t *flags, dnssec_key_algorithm_t *algorithm, + uint16_t *keysize, knot_kasp_key_timing_t *timing, + const char **addtopolicy) +{ + // generate algorithms field + char *algnames[256] = { 0 }; + algnames[DNSSEC_KEY_ALGORITHM_RSA_SHA1] = "rsasha1"; + algnames[DNSSEC_KEY_ALGORITHM_RSA_SHA1_NSEC3] = "rsasha1nsec3sha1"; + algnames[DNSSEC_KEY_ALGORITHM_RSA_SHA256] = "rsasha256"; + algnames[DNSSEC_KEY_ALGORITHM_RSA_SHA512] = "rsasha512"; + algnames[DNSSEC_KEY_ALGORITHM_ECDSA_P256_SHA256] = "ecdsap256sha256"; + algnames[DNSSEC_KEY_ALGORITHM_ECDSA_P384_SHA384] = "ecdsap384sha384"; + algnames[DNSSEC_KEY_ALGORITHM_ED25519] = "ed25519"; + algnames[DNSSEC_KEY_ALGORITHM_ED448] = "ed448"; + + // parse args + for (int i = 0; i < argc; i++) { + if (!just_timing && strncasecmp(argv[i], "algorithm=", 10) == 0) { + int alg = 256; // invalid value + (void)str_to_int(argv[i] + 10, &alg, 0, 255); + for (int al = 0; al < 256 && alg > 255; al++) { + if (algnames[al] != NULL && + strcasecmp(argv[i] + 10, algnames[al]) == 0) { + alg = al; + } + } + if (alg > 255) { + ERR2("unknown algorithm: %s", argv[i] + 10); + return false; + } + *algorithm = alg; + } else if (strncasecmp(argv[i], "ksk=", 4) == 0) { + bitmap_set(flags, DNSKEY_GENERATE_KSK, str2bool(argv[i] + 4)); + } else if (strncasecmp(argv[i], "zsk=", 4) == 0) { + bitmap_set(flags, DNSKEY_GENERATE_ZSK, str2bool(argv[i] + 4)); + } else if (strncasecmp(argv[i], "sep=", 4) == 0) { + bitmap_set(flags, DNSKEY_GENERATE_SEP_SPEC, true); + bitmap_set(flags, DNSKEY_GENERATE_SEP_ON, str2bool(argv[i] + 4)); + } else if (!just_timing && strncasecmp(argv[i], "size=", 5) == 0) { + if (str_to_u16(argv[i] + 5, keysize) != KNOT_EOK) { + ERR2("invalid size: '%s'", argv[i] + 5); + return false; + } + } else if (!just_timing && strncasecmp(argv[i], "addtopolicy=", 12) == 0) { + *addtopolicy = argv[i] + 12; + } else if (!init_timestamps(argv[i], timing)) { + ERR2("invalid parameter: %s", argv[i]); + return false; + } + } + + return true; +} + +static bool _check_lower(knot_time_t a, knot_time_t b, + const char *a_name, const char *b_name) +{ + if (knot_time_cmp(a, b) > 0) { + ERR2("timestamp '%s' must be before '%s'", a_name, b_name); + return false; + } + return true; +} + +#define check_lower(t, a, b) if (!_check_lower(t->a, t->b, #a, #b)) return KNOT_ESEMCHECK + +static int check_timers(const knot_kasp_key_timing_t *t) +{ + if (t->pre_active != 0) { + check_lower(t, pre_active, publish); + } + check_lower(t, publish, active); + check_lower(t, active, retire_active); + check_lower(t, active, retire); + check_lower(t, active, post_active); + if (t->post_active == 0) { + check_lower(t, retire, remove); + } + return KNOT_EOK; +} + +#undef check_lower + +// modifies ctx->policy options, so don't do anything afterwards ! +int keymgr_generate_key(kdnssec_ctx_t *ctx, int argc, char *argv[]) +{ + knot_time_t now = knot_time(), infinity = 0; + knot_kasp_key_timing_t gen_timing = { now, infinity, now, infinity, now, infinity, infinity, infinity, infinity }; + kdnssec_generate_flags_t flags = 0; + uint16_t keysize = 0; + const char *addtopolicy = NULL; + if (!genkeyargs(argc, argv, false, &flags, &ctx->policy->algorithm, + &keysize, &gen_timing, &addtopolicy)) { + return KNOT_EINVAL; + } + + int ret = check_timers(&gen_timing); + if (ret != KNOT_EOK) { + return ret; + } + + if ((flags & DNSKEY_GENERATE_KSK) && gen_timing.ready == infinity) { + gen_timing.ready = gen_timing.active; + } + + if (keysize == 0) { + keysize = dnssec_algorithm_key_size_default(ctx->policy->algorithm); + } + if ((flags & DNSKEY_GENERATE_KSK)) { + ctx->policy->ksk_size = keysize; + } else { + ctx->policy->zsk_size = keysize; + } + + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *kasp_key = &ctx->zone->keys[i]; + if ((kasp_key->is_ksk && (flags & DNSKEY_GENERATE_KSK)) && + dnssec_key_get_algorithm(kasp_key->key) != ctx->policy->algorithm) { + WARN2("creating key with different algorithm than " + "configured in the policy"); + break; + } + } + + knot_kasp_key_t *key = NULL; + ret = kdnssec_generate_key(ctx, flags, &key); + if (ret != KNOT_EOK) { + return ret; + } + + key->timing = gen_timing; + + if (addtopolicy != NULL) { + char *last_policy_last = NULL; + + knot_dname_t *unused = NULL; + ret = kasp_db_get_policy_last(ctx->kasp_db, addtopolicy, &unused, + &last_policy_last); + knot_dname_free(unused, NULL); + if (ret != KNOT_EOK && ret != KNOT_ENOENT) { + free(last_policy_last); + return ret; + } + + ret = kasp_db_set_policy_last(ctx->kasp_db, addtopolicy, last_policy_last, + ctx->zone->dname, key->id); + free(last_policy_last); + if (ret != KNOT_EOK) { + return ret; + } + } + + ret = kdnssec_ctx_commit(ctx); + + if (ret == KNOT_EOK) { + printf("%s\n", key->id); + } + + return ret; +} + +static void parse_record(zs_scanner_t *scanner) +{ + dnssec_key_t *key = scanner->process.data; + + if (dnssec_key_get_dname(key) != NULL || + scanner->r_type != KNOT_RRTYPE_DNSKEY) { + scanner->state = ZS_STATE_STOP; + return; + } + + dnssec_binary_t rdata = { + .data = scanner->r_data, + .size = scanner->r_data_length + }; + dnssec_key_set_dname(key, scanner->dname); + dnssec_key_set_rdata(key, &rdata); +} + +int bind_pubkey_parse(const char *filename, dnssec_key_t **key_ptr) +{ + dnssec_key_t *key = NULL; + int result = dnssec_key_new(&key); + if (result != DNSSEC_EOK) { + return KNOT_ENOMEM; + } + + uint16_t cls = KNOT_CLASS_IN; + uint32_t ttl = 0; + zs_scanner_t *scanner = malloc(sizeof(zs_scanner_t)); + if (scanner == NULL) { + dnssec_key_free(key); + return KNOT_ENOMEM; + } + + if (zs_init(scanner, ".", cls, ttl) != 0 || + zs_set_input_file(scanner, filename) != 0 || + zs_set_processing(scanner, parse_record, NULL, key) != 0 || + zs_parse_all(scanner) != 0) { + int ret; + switch (scanner->error.code) { + case ZS_FILE_OPEN: + case ZS_FILE_INVALID: + ret = KNOT_EFILE; + break; + default: + ret = KNOT_EPARSEFAIL; + } + zs_deinit(scanner); + free(scanner); + dnssec_key_free(key); + return ret; + } + zs_deinit(scanner); + free(scanner); + + if (dnssec_key_get_dname(key) == NULL) { + dnssec_key_free(key); + return KNOT_INVALID_PUBLIC_KEY; + } + + *key_ptr = key; + return KNOT_EOK; +} + +static char *gen_keyfilename(const char *orig, const char *wantsuff, const char *altsuff) +{ + assert(orig && wantsuff && altsuff); + + const char *dot = strrchr(orig, '.'); + + if (dot != NULL && strcmp(dot, wantsuff) == 0) { // Full match. + return strdup(orig); + } else if (dot != NULL && strcmp(dot, altsuff) == 0) { // Replace suffix. + return sprintf_alloc("%.*s%s", (int)(dot - orig), orig, wantsuff); + } else { // Add wanted suffix. + return sprintf_alloc("%s%s", orig, wantsuff); + } +} + +int keymgr_import_bind(kdnssec_ctx_t *ctx, const char *import_file, bool pub_only) +{ + if (ctx == NULL || import_file == NULL) { + return KNOT_EINVAL; + } + + knot_kasp_key_timing_t timing = { ctx->now, 0 }; + dnssec_key_t *key = NULL; + char *keyid = NULL; + + char *pubname = gen_keyfilename(import_file, ".key", ".private"); + if (pubname == NULL) { + return KNOT_EINVAL; + } + + int ret = bind_pubkey_parse(pubname, &key); + free(pubname); + if (ret != KNOT_EOK) { + goto fail; + } + + if (!pub_only) { + bind_privkey_t bpriv = { .time_publish = ctx->now, .time_activate = ctx->now }; + + char *privname = gen_keyfilename(import_file, ".private", ".key"); + if (privname == NULL) { + goto fail; + } + + ret = bind_privkey_parse(privname, &bpriv); + free(privname); + if (ret != DNSSEC_EOK) { + goto fail; + } + + dnssec_binary_t pem = { 0 }; + ret = bind_privkey_to_pem(key, &bpriv, &pem); + if (ret != DNSSEC_EOK) { + bind_privkey_free(&bpriv); + goto fail; + } + + bind_privkey_to_timing(&bpriv, &timing); + + bind_privkey_free(&bpriv); + + ret = dnssec_keystore_import(ctx->keystore, &pem, &keyid); + dnssec_binary_free(&pem); + if (ret != DNSSEC_EOK) { + goto fail; + } + } else { + timing.publish = ctx->now; + + ret = dnssec_key_get_keyid(key, &keyid); + if (ret != DNSSEC_EOK) { + goto fail; + } + } + + // allocate kasp key + knot_kasp_key_t *kkey = calloc(1, sizeof(*kkey)); + if (!kkey) { + ret = KNOT_ENOMEM; + goto fail; + } + + kkey->id = keyid; + kkey->key = key; + kkey->timing = timing; + kkey->is_pub_only = pub_only; + kkey->is_ksk = (dnssec_key_get_flags(kkey->key) == DNSKEY_FLAGS_KSK); + kkey->is_zsk = !kkey->is_ksk; + + // append to zone + ret = kasp_zone_append(ctx->zone, kkey); + free(kkey); + if (ret != KNOT_EOK) { + goto fail; + } + ret = kdnssec_ctx_commit(ctx); + if (ret == KNOT_EOK) { + printf("%s\n", keyid); + return KNOT_EOK; + } +fail: + dnssec_key_free(key); + free(keyid); + return knot_error_from_libdnssec(ret); +} + +static int import_key(kdnssec_ctx_t *ctx, unsigned backend, const char *param, + int argc, char *argv[]) +{ + if (ctx == NULL || param == NULL) { + return KNOT_EINVAL; + } + + // parse params + knot_time_t now = knot_time(); + knot_kasp_key_timing_t timing = { .publish = now, .active = now }; + kdnssec_generate_flags_t flags = 0; + uint16_t keysize = 0; + if (!genkeyargs(argc, argv, false, &flags, &ctx->policy->algorithm, + &keysize, &timing, NULL)) { + return KNOT_EINVAL; + } + + int ret = check_timers(&timing); + if (ret != KNOT_EOK) { + return ret; + } + + normalize_generate_flags(&flags); + + dnssec_key_t *key = NULL; + char *keyid = NULL; + + if (backend == KEYSTORE_BACKEND_PEM) { + // open file + int fd = open(param, O_RDONLY, 0); + if (fd == -1) { + return knot_map_errno(); + } + + // determine size + off_t fsize = lseek(fd, 0, SEEK_END); + if (fsize == -1) { + close(fd); + return knot_map_errno(); + } + if (lseek(fd, 0, SEEK_SET) == -1) { + close(fd); + return knot_map_errno(); + } + + // alloc memory + dnssec_binary_t pem = { 0 }; + ret = dnssec_binary_alloc(&pem, fsize); + if (ret != DNSSEC_EOK) { + close(fd); + goto fail; + } + + // read pem + ssize_t read_count = read(fd, pem.data, pem.size); + close(fd); + if (read_count == -1) { + dnssec_binary_free(&pem); + ret = knot_map_errno(); + goto fail; + } + + // put pem to keystore + ret = dnssec_keystore_import(ctx->keystore, &pem, &keyid); + dnssec_binary_free(&pem); + if (ret != DNSSEC_EOK) { + goto fail; + } + } else { + assert(backend == KEYSTORE_BACKEND_PKCS11); + keyid = strdup(param); + } + + // create dnssec key + ret = dnssec_key_new(&key); + if (ret != DNSSEC_EOK) { + goto fail; + } + ret = dnssec_key_set_dname(key, ctx->zone->dname); + if (ret != DNSSEC_EOK) { + goto fail; + } + dnssec_key_set_flags(key, dnskey_flags(flags & DNSKEY_GENERATE_SEP_ON)); + dnssec_key_set_algorithm(key, ctx->policy->algorithm); + + // fill key structure from keystore (incl. pubkey from privkey computation) + ret = dnssec_keystore_get_private(ctx->keystore, keyid, key); + if (ret != DNSSEC_EOK) { + goto fail; + } + + // allocate kasp key + knot_kasp_key_t *kkey = calloc(1, sizeof(*kkey)); + if (kkey == NULL) { + ret = KNOT_ENOMEM; + goto fail; + } + kkey->id = keyid; + kkey->key = key; + kkey->timing = timing; + kkey->is_ksk = (flags & DNSKEY_GENERATE_KSK); + kkey->is_zsk = (flags & DNSKEY_GENERATE_ZSK); + + // append to zone + ret = kasp_zone_append(ctx->zone, kkey); + free(kkey); + if (ret != KNOT_EOK) { + goto fail; + } + ret = kdnssec_ctx_commit(ctx); + if (ret == KNOT_EOK) { + printf("%s\n", keyid); + return KNOT_EOK; + } +fail: + dnssec_key_free(key); + free(keyid); + return knot_error_from_libdnssec(ret); +} + +int keymgr_import_pem(kdnssec_ctx_t *ctx, const char *import_file, int argc, char *argv[]) +{ + return import_key(ctx, KEYSTORE_BACKEND_PEM, import_file, argc, argv); +} + +int keymgr_import_pkcs11(kdnssec_ctx_t *ctx, char *key_id, int argc, char *argv[]) +{ + if (!dnssec_keyid_is_valid(key_id)) { + return DNSSEC_INVALID_KEY_ID; + } + dnssec_keyid_normalize(key_id); + return import_key(ctx, KEYSTORE_BACKEND_PKCS11, key_id, argc, argv); +} + +int keymgr_nsec3_salt_print(kdnssec_ctx_t *ctx) +{ + dnssec_binary_t salt_bin; + knot_time_t created; + int ret = kasp_db_load_nsec3salt(ctx->kasp_db, ctx->zone->dname, + &salt_bin, &created); + switch (ret) { + case KNOT_EOK: + printf("Current salt: "); + if (salt_bin.size == 0) { + printf("-"); + } + for (size_t i = 0; i < salt_bin.size; i++) { + printf("%02X", (unsigned)salt_bin.data[i]); + } + printf("\n"); + free(salt_bin.data); + break; + case KNOT_ENOENT: + printf("-- no salt --\n"); + ret = KNOT_EOK; + break; + } + return ret; +} + +int keymgr_nsec3_salt_set(kdnssec_ctx_t *ctx, const char *new_salt) +{ + assert(new_salt); + + dnssec_binary_t salt_bin = { 0 }; + if (strcmp(new_salt, "-") != 0) { + salt_bin.data = hex_to_bin(new_salt, &salt_bin.size); + if (salt_bin.data == NULL) { + return KNOT_EMALF; + } + } + if (salt_bin.size != ctx->policy->nsec3_salt_length) { + WARN2("specified salt doesn't match configured salt length (%d)", + (int)ctx->policy->nsec3_salt_length); + } + int ret = kasp_db_store_nsec3salt(ctx->kasp_db, ctx->zone->dname, + &salt_bin, knot_time()); + if (salt_bin.size > 0) { + free(salt_bin.data); + } + return ret; +} + +int keymgr_serial_print(kdnssec_ctx_t *ctx, kaspdb_serial_t type) +{ + uint32_t serial = 0; + int ret = kasp_db_load_serial(ctx->kasp_db, ctx->zone->dname, + type, &serial); + switch (ret) { + case KNOT_EOK: + printf("Current serial: %u\n", serial); + break; + case KNOT_ENOENT: + printf("-- no serial --\n"); + ret = KNOT_EOK; + break; + } + return ret; +} + +int keymgr_serial_set(kdnssec_ctx_t *ctx, kaspdb_serial_t type, uint32_t new_serial) +{ + return kasp_db_store_serial(ctx->kasp_db, ctx->zone->dname, + type, new_serial); +} + +static void print_tsig(dnssec_tsig_algorithm_t mac, const char *name, + const dnssec_binary_t *secret) +{ + assert(name); + assert(secret); + + const char *mac_name = dnssec_tsig_algorithm_to_name(mac); + assert(mac_name); + + // client format (as a comment) + printf("# %s:%s:%.*s\n", mac_name, name, (int)secret->size, secret->data); + + // server format + printf("key:\n"); + printf(" - id: %s\n", name); + printf(" algorithm: %s\n", mac_name); + printf(" secret: %.*s\n", (int)secret->size, secret->data); +} + +int keymgr_generate_tsig(const char *tsig_name, const char *alg_name, int bits) +{ + dnssec_tsig_algorithm_t alg = dnssec_tsig_algorithm_from_name(alg_name); + if (alg == DNSSEC_TSIG_UNKNOWN) { + return KNOT_INVALID_KEY_ALGORITHM; + } + + int optimal_bits = dnssec_tsig_optimal_key_size(alg); + if (bits == 0) { + bits = optimal_bits; + } + + // round up bits to bytes + bits = (bits + CHAR_BIT - 1) / CHAR_BIT * CHAR_BIT; + + if (bits < optimal_bits) { + WARN2("optimal key size for %s is at least %d bits", + dnssec_tsig_algorithm_to_name(alg), optimal_bits); + } + assert(bits % CHAR_BIT == 0); + + _cleanup_binary_ dnssec_binary_t key = { 0 }; + int r = dnssec_binary_alloc(&key, bits / CHAR_BIT); + if (r != DNSSEC_EOK) { + ERR2("failed to allocate memory"); + return knot_error_from_libdnssec(r); + } + + r = gnutls_rnd(GNUTLS_RND_KEY, key.data, key.size); + if (r != 0) { + ERR2("failed to generate secret the key"); + return knot_error_from_libdnssec(r); + } + + _cleanup_binary_ dnssec_binary_t key_b64 = { 0 }; + r = dnssec_binary_to_base64(&key, &key_b64); + if (r != DNSSEC_EOK) { + ERR2("failed to convert the key to Base64"); + return knot_error_from_libdnssec(r); + } + + print_tsig(alg, tsig_name, &key_b64); + + return KNOT_EOK; +} + +static bool is_hex(const char *string) +{ + for (const char *p = string; *p != '\0'; p++) { + if (!is_xdigit(*p)) { + return false; + } + } + return (*string != '\0'); +} + +int keymgr_get_key(kdnssec_ctx_t *ctx, const char *key_spec, knot_kasp_key_t **key) +{ + // Check if type of key spec is prescribed. + bool is_keytag = false, is_id = false; + if (strncasecmp(key_spec, "keytag=", 7) == 0) { + key_spec += 7; + is_keytag = true; + } else if (strncasecmp(key_spec, "id=", 3) == 0) { + key_spec += 3; + is_id = true; + } + + uint16_t keytag = 0; + bool can_be_keytag = (str_to_u16(key_spec, &keytag) == KNOT_EOK); + long spec_len = strlen(key_spec); + + // Check if input is a valid key spec. + if ((is_keytag && !can_be_keytag) || + (is_id && !is_hex(key_spec)) || + (!can_be_keytag && !is_hex(key_spec))) { + ERR2("invalid key specification"); + return KNOT_EINVAL; + } + + *key = NULL; + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *candidate = &ctx->zone->keys[i]; + + bool keyid_match = strncmp(candidate->id, key_spec, spec_len) == 0; // May be just a prefix. + bool keytag_match = can_be_keytag && + dnssec_key_get_keytag(candidate->key) == keytag; + + // Terminate if found exact key ID match. + if (keyid_match && !is_keytag && strlen(candidate->id) == spec_len) { + *key = candidate; + break; + } + // Check for key ID prefix or tag match. + if ((is_keytag && keytag_match) || // Tag is prescribed. + (is_id && keyid_match) || // Key ID is prescribed. + ((!is_keytag && !is_id) && (keyid_match || keytag_match))) { // Nothing is prescribed. + if (*key == NULL) { + *key = candidate; + } else { + ERR2("key not specified uniquely, please use id=Full_Key_ID"); + return KNOT_EINVAL; + } + } + } + if (*key == NULL) { + ERR2("key not found"); + return KNOT_ENOENT; + } + return KNOT_EOK; +} + +int keymgr_foreign_key_id(char *argv[], knot_lmdb_db_t *kaspdb, knot_dname_t **key_zone, char **key_id) +{ + *key_zone = knot_dname_from_str_alloc(argv[3]); + if (*key_zone == NULL) { + return KNOT_ENOMEM; + } + knot_dname_to_lower(*key_zone); + + kdnssec_ctx_t kctx = { 0 }; + int ret = kdnssec_ctx_init(conf(), &kctx, *key_zone, kaspdb, NULL); + if (ret != KNOT_EOK) { + ERR2("failed to initialize zone %s (%s)", argv[0], knot_strerror(ret)); + free(*key_zone); + *key_zone = NULL; + return KNOT_ENOZONE; + } + knot_kasp_key_t *key; + ret = keymgr_get_key(&kctx, argv[2], &key); + if (ret == KNOT_EOK) { + *key_id = strdup(key->id); + if (*key_id == NULL) { + ret = KNOT_ENOMEM; + } + } + kdnssec_ctx_deinit(&kctx); + return ret; +} + +int keymgr_set_timing(knot_kasp_key_t *key, int argc, char *argv[]) +{ + knot_kasp_key_timing_t temp = key->timing; + kdnssec_generate_flags_t flags = ((key->is_ksk ? DNSKEY_GENERATE_KSK : 0) | (key->is_zsk ? DNSKEY_GENERATE_ZSK : 0)); + + if (genkeyargs(argc, argv, true, &flags, NULL, NULL, &temp, NULL)) { + int ret = check_timers(&temp); + if (ret != KNOT_EOK) { + return ret; + } + key->timing = temp; + if (key->is_ksk != (bool)(flags & DNSKEY_GENERATE_KSK) || + key->is_zsk != (bool)(flags & DNSKEY_GENERATE_ZSK) || + flags & DNSKEY_GENERATE_SEP_SPEC) { + normalize_generate_flags(&flags); + key->is_ksk = (flags & DNSKEY_GENERATE_KSK); + key->is_zsk = (flags & DNSKEY_GENERATE_ZSK); + return dnssec_key_set_flags(key->key, dnskey_flags(flags & DNSKEY_GENERATE_SEP_ON)); + } + return KNOT_EOK; + } + return KNOT_EINVAL; +} + +typedef struct { + const char *name; + size_t offset; +} timer_ctx_t; + +static const timer_ctx_t timers[] = { + { "pre-active", offsetof(knot_kasp_key_timing_t, pre_active) }, + { "publish", offsetof(knot_kasp_key_timing_t, publish) }, + { "ready", offsetof(knot_kasp_key_timing_t, ready) }, + { "active", offsetof(knot_kasp_key_timing_t, active) }, + { "retire-active", offsetof(knot_kasp_key_timing_t, retire_active) }, + { "retire", offsetof(knot_kasp_key_timing_t, retire) }, + { "post-active", offsetof(knot_kasp_key_timing_t, post_active) }, + { "revoke", offsetof(knot_kasp_key_timing_t, revoke) }, + { "remove", offsetof(knot_kasp_key_timing_t, remove) }, + { NULL } +}; + +static void print_key_brief(const knot_kasp_key_t *key, keymgr_list_params_t *params) +{ + const bool c = params->color; + + printf("%s %s%5u%s ", + key->id, COL_BOLD(c), dnssec_key_get_keytag(key->key), COL_RST(c)); + + printf("%s%s%s%s ", + COL_BOLD(c), + (key->is_ksk ? (key->is_zsk ? COL_YELW(c) : COL_RED(c)) : COL_GRN(c)), + (key->is_ksk ? (key->is_zsk ? "CSK" : "KSK") : "ZSK"), + COL_RST(c)); + + uint8_t alg = dnssec_key_get_algorithm(key->key); + const knot_lookup_t *alg_info = knot_lookup_by_id(knot_dnssec_alg_names, alg); + if (alg_info != NULL) { + printf("%s", alg_info->name); + if (alg <= DNSSEC_KEY_ALGORITHM_RSA_SHA512) { + printf("%s/%u%s", COL_DIM(c), dnssec_key_get_size(key->key), COL_RST(c)); + } + } else { + printf("ALGORITHM_%u", alg); + } + + if (key->is_pub_only) { + printf(" %s%spublic-only%s", COL_BOLD(c), COL_MGNT(c), COL_RST(c)); + } + + static char buf[100]; + knot_time_t now = knot_time(); + for (const timer_ctx_t *t = &timers[0]; t->name != NULL; t++) { + knot_time_t *val = (void *)(&key->timing) + t->offset; + if (*val == 0) { + continue; + } + bool past = (knot_time_cmp(*val, now) <= 0); + const char *UNDR = past ? COL_UNDR(c) : ""; + const char *BOLD = past ? "" : COL_BOLD(c); + for (const timer_ctx_t *t2 = t + 1; past && t2->name != NULL; t2++) { + knot_time_t *val2 = (void *)(&key->timing) + t2->offset; + if (knot_time_cmp(*val2, now) <= 0) { + UNDR = ""; + break; + } + } + (void)knot_time_print(params->format, *val, buf, sizeof(buf)); + printf(" %s%s%s=%s%s%s", UNDR, t->name, COL_RST(c), BOLD, buf, COL_RST(c)); + } + printf("\n"); +} + +static void print_key_full(const knot_kasp_key_t *key, knot_time_print_t format) +{ + printf("%s ksk=%s zsk=%s tag=%05d algorithm=%-2d size=%-4u public-only=%s", key->id, + (key->is_ksk ? "yes" : "no "), (key->is_zsk ? "yes" : "no "), + dnssec_key_get_keytag(key->key), (int)dnssec_key_get_algorithm(key->key), + dnssec_key_get_size(key->key), (key->is_pub_only ? "yes" : "no ")); + + static char buf[100]; + for (const timer_ctx_t *t = &timers[0]; t->name != NULL; t++) { + knot_time_t *val = (void *)(&key->timing) + t->offset; + (void)knot_time_print(format, *val, buf, sizeof(buf)); + printf(" %s=%s", t->name, buf); + } + printf("\n"); +} + +static void print_key_json(const knot_kasp_key_t *key, knot_time_print_t format, + jsonw_t *w, const char *zone_name) +{ + jsonw_str(w, "zone", zone_name); + jsonw_str(w, "id", key->id); + jsonw_bool(w, "ksk", key->is_ksk); + jsonw_bool(w, "zsk", key->is_zsk); + jsonw_int(w, "tag", dnssec_key_get_keytag(key->key)); + jsonw_ulong(w, "algorithm", dnssec_key_get_algorithm(key->key)); + jsonw_int(w, "size", dnssec_key_get_size(key->key)); + jsonw_bool(w, "public-only", key->is_pub_only); + + static char buf[100]; + for (const timer_ctx_t *t = &timers[0]; t->name != NULL; t++) { + knot_time_t *val = (void *)(&key->timing) + t->offset; + (void)knot_time_print(format, *val, buf, sizeof(buf)); + + if (format == TIME_PRINT_UNIX) { + jsonw_int(w, t->name, *val); + } else { + jsonw_str(w, t->name, buf); + } + } +} + +typedef struct { + knot_time_t val; + const knot_kasp_key_t *key; +} key_sort_item_t; + +static int key_sort(const void *a, const void *b) +{ + const key_sort_item_t *key_a = a; + const key_sort_item_t *key_b = b; + return knot_time_cmp(key_a->val, key_b->val); +} + +int keymgr_list_keys(kdnssec_ctx_t *ctx, keymgr_list_params_t *params) +{ + if (ctx->zone->num_keys == 0) { + return KNOT_EOK; + } + + if (params->extended) { + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + print_key_full(key, params->format); + } + } else if (params->json) { + jsonw_t *w = jsonw_new(stdout, " "); + if (w == NULL) { + return KNOT_ENOMEM; + } + + knot_dname_txt_storage_t name; + (void)knot_dname_to_str(name, ctx->zone->dname, sizeof(name)); + + jsonw_list(w, NULL); + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + jsonw_object(w, NULL); + print_key_json(key, params->format, w, name); + jsonw_end(w); // object + } + jsonw_end(w); // list + jsonw_free(&w); + } else { + key_sort_item_t items[ctx->zone->num_keys]; + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + items[i].key = key; + if (knot_time_cmp(key->timing.pre_active, key->timing.publish) < 0) { + items[i].val = key->timing.pre_active; + } else { + items[i].val = key->timing.publish; + } + } + qsort(&items, ctx->zone->num_keys, sizeof(items[0]), key_sort); + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + print_key_brief(items[i].key, params); + } + } + return KNOT_EOK; +} + +static int print_ds(const knot_dname_t *dname, const dnssec_binary_t *rdata) +{ + wire_ctx_t ctx = wire_ctx_init(rdata->data, rdata->size); + if (wire_ctx_available(&ctx) < 4) { + return KNOT_EMALF; + } + + char *name = knot_dname_to_str_alloc(dname); + if (!name) { + return KNOT_ENOMEM; + } + + uint16_t keytag = wire_ctx_read_u16(&ctx); + uint8_t algorithm = wire_ctx_read_u8(&ctx); + uint8_t digest_type = wire_ctx_read_u8(&ctx); + + size_t digest_size = wire_ctx_available(&ctx); + + printf("%s DS %d %d %d ", name, keytag, algorithm, digest_type); + for (size_t i = 0; i < digest_size; i++) { + printf("%02x", ctx.position[i]); + } + printf("\n"); + + free(name); + return KNOT_EOK; +} + +static int create_and_print_ds(const knot_dname_t *zone_name, + const dnssec_key_t *key, dnssec_key_digest_t digest) +{ + _cleanup_binary_ dnssec_binary_t rdata = { 0 }; + int r = dnssec_key_create_ds(key, digest, &rdata); + if (r != DNSSEC_EOK) { + return knot_error_from_libdnssec(r); + } + + return print_ds(zone_name, &rdata); +} + +int keymgr_generate_ds(const knot_dname_t *dname, const knot_kasp_key_t *key) +{ + static const dnssec_key_digest_t digests[] = { + DNSSEC_KEY_DIGEST_SHA256, + DNSSEC_KEY_DIGEST_SHA384, + 0 + }; + + int ret = KNOT_EOK; + for (int i = 0; digests[i] != 0 && ret == KNOT_EOK; i++) { + ret = create_and_print_ds(dname, key->key, digests[i]); + } + + return ret; +} + +int keymgr_generate_dnskey(const knot_dname_t *dname, const knot_kasp_key_t *key) +{ + const dnssec_key_t *dnskey = key->key; + + char *name = knot_dname_to_str_alloc(dname); + if (!name) { + return KNOT_ENOMEM; + } + + uint16_t flags = dnssec_key_get_flags(dnskey); + uint8_t algorithm = dnssec_key_get_algorithm(dnskey); + + dnssec_binary_t pubkey = { 0 }; + int ret = dnssec_key_get_pubkey(dnskey, &pubkey); + if (ret != DNSSEC_EOK) { + free(name); + return knot_error_from_libdnssec(ret); + } + + uint8_t *base64_output = NULL; + int len = knot_base64_encode_alloc(pubkey.data, pubkey.size, &base64_output); + if (len < 0) { + free(name); + return len; + } + + printf("%s DNSKEY %u 3 %u %.*s\n", name, flags, algorithm, len, base64_output); + + free(base64_output); + free(name); + return KNOT_EOK; +} + +int keymgr_list_zones(knot_lmdb_db_t *kaspdb, bool json) +{ + jsonw_t *w; + list_t zones; + init_list(&zones); + int ret = kasp_db_list_zones(kaspdb, &zones); + if (ret != KNOT_EOK) { + ERR2("failed to initialize KASP (%s)", knot_strerror(ret)); + return ret; + } + + knot_dname_txt_storage_t name; + ptrnode_t *node; + + if (json) { + w = jsonw_new(stdout, " "); + if (w == NULL) { + ERR2("failed to allocate memory"); + ptrlist_deep_free(&zones, NULL); + return KNOT_ENOMEM; + } + jsonw_list(w, NULL); + } + WALK_LIST(node, zones) { + (void)knot_dname_to_str(name, node->d, sizeof(name)); + if (json) { + jsonw_str(w, NULL, name); + } else { + printf("%s\n", name); + } + } + if (json) { + jsonw_end(w); // list + jsonw_free(&w); + } + + ptrlist_deep_free(&zones, NULL); + return KNOT_EOK; +} diff --git a/src/utils/keymgr/functions.h b/src/utils/keymgr/functions.h new file mode 100644 index 0000000..9c16d80 --- /dev/null +++ b/src/utils/keymgr/functions.h @@ -0,0 +1,62 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdio.h> + +#include "knot/dnssec/context.h" + +typedef struct { + knot_time_print_t format; + bool extended; + bool color; + bool json; +} keymgr_list_params_t; + +int parse_timestamp(char *arg, knot_time_t *stamp); + +int keymgr_generate_key(kdnssec_ctx_t *ctx, int argc, char *argv[]); + +int keymgr_import_bind(kdnssec_ctx_t *ctx, const char *import_file, bool pub_only); + +int keymgr_import_pem(kdnssec_ctx_t *ctx, const char *import_file, int argc, char *argv[]); + +int keymgr_import_pkcs11(kdnssec_ctx_t *ctx, char *key_id, int argc, char *argv[]); + +int keymgr_nsec3_salt_print(kdnssec_ctx_t *ctx); + +int keymgr_nsec3_salt_set(kdnssec_ctx_t *ctx, const char *new_salt); + +int keymgr_serial_print(kdnssec_ctx_t *ctx, kaspdb_serial_t type); + +int keymgr_serial_set(kdnssec_ctx_t *ctx, kaspdb_serial_t type, uint32_t new_serial); + +int keymgr_generate_tsig(const char *tsig_name, const char *alg_name, int bits); + +int keymgr_get_key(kdnssec_ctx_t *ctx, const char *key_spec, knot_kasp_key_t **key); + +int keymgr_foreign_key_id(char *argv[], knot_lmdb_db_t *kaspdb, knot_dname_t **key_zone, char **key_id); + +int keymgr_set_timing(knot_kasp_key_t *key, int argc, char *argv[]); + +int keymgr_list_keys(kdnssec_ctx_t *ctx, keymgr_list_params_t *params); + +int keymgr_generate_ds(const knot_dname_t *dname, const knot_kasp_key_t *key); + +int keymgr_generate_dnskey(const knot_dname_t *dname, const knot_kasp_key_t *key); + +int keymgr_list_zones(knot_lmdb_db_t *kaspdb, bool json); diff --git a/src/utils/keymgr/main.c b/src/utils/keymgr/main.c new file mode 100644 index 0000000..cf40c59 --- /dev/null +++ b/src/utils/keymgr/main.c @@ -0,0 +1,405 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <https://www.gnu.org/licenses/>. + */ + +#include <getopt.h> +#include <stdlib.h> +#include <unistd.h> + +#include "contrib/strtonum.h" +#include "knot/dnssec/zone-keys.h" +#include "libknot/libknot.h" +#include "utils/common/msg.h" +#include "utils/common/params.h" +#include "utils/common/util_conf.h" +#include "utils/keymgr/functions.h" +#include "utils/keymgr/offline_ksk.h" + +#define PROGRAM_NAME "keymgr" + +static void print_help(void) +{ + printf("Usage:\n" + " %s [-c | -C | -D <path>] <zone_name> <command> [<argument>...]\n" + " %s [-c | -C | -D <path>] -l\n" + " %s -t <tsig_name> [<algorithm> [<bits>]]\n" + "\n" + "Parameters:\n" + " -c, --config <file> Path to a textual configuration file.\n" + " (default %s)\n" + " -C, --confdb <dir> Path to a configuration database directory.\n" + " (default %s)\n" + " -D, --dir <path> Path to a KASP database directory, use default configuration.\n" + " -t, --tsig <name> [alg] Generate a TSIG key.\n" + " -e, --extended Extended output (listing of keys with full description).\n" + " -j, --json Print the zones or keys in JSON format.\n" + " -l, --list List all zones that have at least one key in KASP database.\n" + " -x, --mono Don't color the output.\n" + " -X, --color Force output colorization in the normal mode.\n" + " -h, --help Print the program help.\n" + " -V, --version Print the program version.\n" + "\n" + "Commands:\n" + " list List all zone's DNSSEC keys.\n" + " generate Generate new DNSSEC key.\n" + " (syntax: generate <attribute_name>=<value>...)\n" + " import-bind Import BIND-style key file pair (.key + .private).\n" + " (syntax: import-bind <key_file_name>)\n" + " import-pub Import public-only key to be published in the zone (in BIND .key format).\n" + " (syntax: import-pub <key_file_name>)\n" + " import-pem Import key in PEM format. Specify its parameters manually.\n" + " (syntax: import-pem <pem_file_path> <attribute_name>=<value>...)\n" + " import-pkcs11 Import key stored in PKCS11 storage. Specify its parameters manually.\n" + " (syntax: import-pkcs11 <key_id> <attribute_name>=<value>...)\n" + " nsec3-salt Print current NSEC3 salt. If a parameter is specified, set new salt.\n" + " (syntax: nsec3-salt [<new_salt>])\n" + " local-serial Print SOA serial stored in KASP database when using on-slave signing.\n" + " If a parameter is specified, set new serial.\n" + " (syntax: local-serial <new_serial>)\n" + " master-serial Print SOA serial of the remote master stored in KASP database when using on-slave signing.\n" + " If a parameter is specified, set new master serial.\n" + " (syntax: master-serial <new_serial>)\n" + " ds Generate DS record(s) for specified key.\n" + " (syntax: ds <key_spec>)\n" + " dnskey Generate DNSKEY record for specified key.\n" + " (syntax: dnskey <key_spec>)\n" + " share Share an existing key of another zone with the specified zone.\n" + " (syntax: share <full_key_ID> <zone2share_from>\n" + " delete Remove the specified key from zone.\n" + " (syntax: delete <key_spec>)\n" + " set Set existing key's timing attribute.\n" + " (syntax: set <key_spec> <attribute_name>=<value>...)\n" + "\n" + "Commands related to Offline KSK feature:\n" + " pregenerate Pre-generate ZSKs for later rollovers with offline KSK.\n" + " (syntax: pregenerate [<from>] <to>)\n" + " show-offline Print pre-generated offline key-related records for specified time interval (possibly to infinity).\n" + " (syntax: show-offline [<from>] [<to>])\n" + " del-offline Delete pre-generated offline key-related records in specified time interval.\n" + " (syntax: del-offline <from> <to>)\n" + " del-all-old Delete old keys that are in state 'removed'.\n" + " generate-ksr Print to stdout KeySigningRequest based on pre-generated ZSKS.\n" + " (syntax: generate-ksr [<from>] <to>)\n" + " sign-ksr Read KeySigningRequest from a file, sign it and print SignedKeyResponse to stdout.\n" + " (syntax: sign-ksr <ksr_file>)\n" + " validate-skr Validate RRSIGs in a SignedKeyResponse (if not corrupt).\n" + " (syntax: validate-skr <skr_file>)\n" + " import-skr Import DNSKEY record signatures from a SignedKeyResponse.\n" + " (syntax: import-skr <skr_file>)\n" + "\n" + "Key specification:\n" + " either the key tag (number) or [a prefix of] key ID, with an optional\n" + " [id=|keytag=] prefix.\n" + "\n" + "Key attributes:\n" + " algorithm The key cryptographic algorithm: either name (e.g. RSASHA256) or\n" + " number.\n" + " size The key size in bits.\n" + " ksk Whether the generated/imported key shall be Key Signing Key.\n" + " created/publish/ready/active/retire/remove The timestamp of the key\n" + " lifetime event (e.g. published=+1d active=1499770874)\n", + PROGRAM_NAME, PROGRAM_NAME, PROGRAM_NAME, CONF_DEFAULT_FILE, CONF_DEFAULT_DBDIR); +} + +static int key_command(int argc, char *argv[], int opt_ind, knot_lmdb_db_t *kaspdb, + keymgr_list_params_t *list_params) +{ + if (argc < opt_ind + 2) { + ERR2("zone name or command not specified"); + print_help(); + return KNOT_EINVAL; + } + argc -= opt_ind; + argv += opt_ind; + + knot_dname_t *zone_name = knot_dname_from_str_alloc(argv[0]); + if (zone_name == NULL) { + return KNOT_ENOMEM; + } + knot_dname_to_lower(zone_name); + + kdnssec_ctx_t kctx = { 0 }; + + int ret = kdnssec_ctx_init(conf(), &kctx, zone_name, kaspdb, NULL); + if (ret != KNOT_EOK) { + ERR2("failed to initialize KASP (%s)", knot_strerror(ret)); + goto main_end; + } + +#define CHECK_MISSING_ARG(msg) \ + if (argc < 3) { \ + ERR2("%s", (msg)); \ + ret = KNOT_EINVAL; \ + goto main_end; \ + } + +#define CHECK_MISSING_ARG2(msg) \ + if (argc < 4) { \ + ERR2("%s", (msg)); \ + ret = KNOT_EINVAL; \ + goto main_end; \ + } + + bool print_ok_on_succes = true; + if (strcmp(argv[1], "generate") == 0) { + ret = keymgr_generate_key(&kctx, argc - 2, argv + 2); + print_ok_on_succes = false; + } else if (strcmp(argv[1], "import-bind") == 0) { + CHECK_MISSING_ARG("BIND-style key to import not specified"); + ret = keymgr_import_bind(&kctx, argv[2], false); + } else if (strcmp(argv[1], "import-pub") == 0) { + CHECK_MISSING_ARG("BIND-style key to import not specified"); + ret = keymgr_import_bind(&kctx, argv[2], true); + } else if (strcmp(argv[1], "import-pem") == 0) { + CHECK_MISSING_ARG("PEM file to import not specified"); + ret = keymgr_import_pem(&kctx, argv[2], argc - 3, argv + 3); + } else if (strcmp(argv[1], "import-pkcs11") == 0) { + CHECK_MISSING_ARG("Key ID to import not specified"); + ret = keymgr_import_pkcs11(&kctx, argv[2], argc - 3, argv + 3); + } else if (strcmp(argv[1], "nsec3-salt") == 0) { + if (argc > 2) { + ret = keymgr_nsec3_salt_set(&kctx, argv[2]); + } else { + ret = keymgr_nsec3_salt_print(&kctx); + print_ok_on_succes = false; + } + } else if (strcmp(argv[1], "local-serial") == 0 || strcmp(argv[1], "master-serial") == 0 ) { + kaspdb_serial_t type = (argv[1][0] == 'm' ? KASPDB_SERIAL_MASTER : KASPDB_SERIAL_LASTSIGNED); + if (argc > 2) { + uint32_t new_serial = 0; + if ((ret = str_to_u32(argv[2], &new_serial)) == KNOT_EOK) { + ret = keymgr_serial_set(&kctx, type, new_serial); + } + } else { + ret = keymgr_serial_print(&kctx, type); + print_ok_on_succes = false; + } + } else if (strcmp(argv[1], "set") == 0) { + CHECK_MISSING_ARG("Key is not specified"); + knot_kasp_key_t *key2set; + ret = keymgr_get_key(&kctx, argv[2], &key2set); + if (ret == KNOT_EOK) { + ret = keymgr_set_timing(key2set, argc - 3, argv + 3); + if (ret == KNOT_EOK) { + ret = kdnssec_ctx_commit(&kctx); + } + } + } else if (strcmp(argv[1], "list") == 0) { + list_params->format = TIME_PRINT_UNIX; + if (argc > 2 && strcmp(argv[2], "human") == 0) { + list_params->format = TIME_PRINT_HUMAN_MIXED; + } else if (argc > 2 && strcmp(argv[2], "iso") == 0) { + list_params->format = TIME_PRINT_ISO8601; + } + ret = keymgr_list_keys(&kctx, list_params); + print_ok_on_succes = false; + } else if (strcmp(argv[1], "ds") == 0 || strcmp(argv[1], "dnskey") == 0) { + int (*generate_rr)(const knot_dname_t *, const knot_kasp_key_t *) = keymgr_generate_dnskey; + if (strcmp(argv[1], "ds") == 0) { + generate_rr = keymgr_generate_ds; + } + if (argc < 3) { + for (int i = 0; i < kctx.zone->num_keys && ret == KNOT_EOK; i++) { + if (kctx.zone->keys[i].is_ksk) { + ret = generate_rr(zone_name, &kctx.zone->keys[i]); + } + } + } else { + knot_kasp_key_t *key2rr; + ret = keymgr_get_key(&kctx, argv[2], &key2rr); + if (ret == KNOT_EOK) { + ret = generate_rr(zone_name, key2rr); + } + } + print_ok_on_succes = false; + } else if (strcmp(argv[1], "share") == 0) { + CHECK_MISSING_ARG("Key to be shared is not specified"); + CHECK_MISSING_ARG2("Zone to be shared from not specified"); + knot_dname_t *other_zone = NULL; + char *key_to_share = NULL; + ret = keymgr_foreign_key_id(argv, kaspdb, &other_zone, &key_to_share); + if (ret == KNOT_EOK) { + ret = kasp_db_share_key(kctx.kasp_db, other_zone, kctx.zone->dname, key_to_share); + } + free(other_zone); + free(key_to_share); + } else if (strcmp(argv[1], "delete") == 0) { + CHECK_MISSING_ARG("Key is not specified"); + knot_kasp_key_t *key2del; + ret = keymgr_get_key(&kctx, argv[2], &key2del); + if (ret == KNOT_EOK) { + ret = kdnssec_delete_key(&kctx, key2del); + } + } else if (strcmp(argv[1], "pregenerate") == 0) { + CHECK_MISSING_ARG("Timestamp to not specified"); + ret = keymgr_pregenerate_zsks(&kctx, argc > 3 ? argv[2] : NULL, + argc > 3 ? argv[3] : argv[2]); + } else if (strcmp(argv[1], "show-offline") == 0) { + ret = keymgr_print_offline_records(&kctx, argc > 2 ? argv[2] : NULL, + argc > 3 ? argv[3] : NULL); + print_ok_on_succes = false; + } else if (strcmp(argv[1], "del-offline") == 0) { + CHECK_MISSING_ARG2("Timestamps from-to not specified"); + ret = keymgr_delete_offline_records(&kctx, argv[2], argv[3]); + } else if (strcmp(argv[1], "del-all-old") == 0) { + ret = keymgr_del_all_old(&kctx); + } else if (strcmp(argv[1], "generate-ksr") == 0) { + CHECK_MISSING_ARG("Timestamps to not specified"); + ret = keymgr_print_ksr(&kctx, argc > 3 ? argv[2] : NULL, + argc > 3 ? argv[3] : argv[2]); + print_ok_on_succes = false; + } else if (strcmp(argv[1], "sign-ksr") == 0) { + CHECK_MISSING_ARG("Input file not specified"); + ret = keymgr_sign_ksr(&kctx, argv[2]); + print_ok_on_succes = false; + } else if (strcmp(argv[1], "validate-skr") == 0) { + CHECK_MISSING_ARG("Input file not specified"); + ret = keymgr_validate_skr(&kctx, argv[2]); + } else if (strcmp(argv[1], "import-skr") == 0) { + CHECK_MISSING_ARG("Input file not specified"); + ret = keymgr_import_skr(&kctx, argv[2]); + } else { + ERR2("invalid command '%s'", argv[1]); + goto main_end; + } + +#undef CHECK_MISSING_ARG + + if (ret == KNOT_EOK) { + printf("%s", print_ok_on_succes ? "OK\n" : ""); + } else { + ERR2("%s", knot_strerror(ret)); + } + +main_end: + kdnssec_ctx_deinit(&kctx); + free(zone_name); + + return ret; +} + +int main(int argc, char *argv[]) +{ + struct option opts[] = { + { "config", required_argument, NULL, 'c' }, + { "confdb", required_argument, NULL, 'C' }, + { "dir", required_argument, NULL, 'D' }, + { "tsig", required_argument, NULL, 't' }, + { "extended", no_argument, NULL, 'e' }, + { "list", no_argument, NULL, 'l' }, + { "brief", no_argument, NULL, 'b' }, // Legacy. + { "mono", no_argument, NULL, 'x' }, + { "color", no_argument, NULL, 'X' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "json", no_argument, NULL, 'j' }, + { NULL } + }; + + tzset(); + + int ret; + bool just_list = false; + keymgr_list_params_t list_params = { 0 }; + + list_params.color = isatty(STDOUT_FILENO); + + int opt = 0, parm = 0; + while ((opt = getopt_long(argc, argv, "c:C:D:t:ejlbxXhV", opts, NULL)) != -1) { + switch (opt) { + case 'c': + if (util_conf_init_file(optarg) != KNOT_EOK) { + goto failure; + } + break; + case 'C': + if (util_conf_init_confdb(optarg) != KNOT_EOK) { + goto failure; + } + break; + case 'D': + if (util_conf_init_justdb("kasp-db", optarg) != KNOT_EOK) { + goto failure; + } + break; + case 't': + if (argc > optind + 1) { + (void)str_to_int(argv[optind + 1], &parm, 0, 65536); + } + ret = keymgr_generate_tsig(optarg, (argc > optind ? argv[optind] : "hmac-sha256"), parm); + if (ret != KNOT_EOK) { + ERR2("failed to generate TSIG (%s)", knot_strerror(ret)); + goto failure; + } + goto success; + case 'e': + list_params.extended = true; + break; + case 'j': + list_params.json = true; + break; + case 'l': + just_list = true; + break; + case 'b': + WARN2("option '--brief' is deprecated and enabled by default"); + break; + case 'x': + list_params.color = false; + break; + case 'X': + list_params.color = true; + break; + case 'h': + print_help(); + goto success; + case 'V': + print_version(PROGRAM_NAME); + goto success; + default: + print_help(); + goto failure; + } + } + + if (util_conf_init_default(true) != KNOT_EOK) { + goto failure; + } + + util_update_privileges(); + + knot_lmdb_db_t kaspdb = { 0 }; + conf_val_t mapsize = conf_db_param(conf(), C_KASP_DB_MAX_SIZE); + char *kasp_dir = conf_db(conf(), C_KASP_DB); + knot_lmdb_init(&kaspdb, kasp_dir, conf_int(&mapsize), 0, "keys_db"); + free(kasp_dir); + + if (just_list) { + ret = keymgr_list_zones(&kaspdb, list_params.json); + } else { + ret = key_command(argc, argv, optind, &kaspdb, &list_params); + } + knot_lmdb_deinit(&kaspdb); + if (ret != KNOT_EOK) { + goto failure; + } + +success: + util_conf_deinit(); + return EXIT_SUCCESS; +failure: + util_conf_deinit(); + return EXIT_FAILURE; +} diff --git a/src/utils/keymgr/offline_ksk.c b/src/utils/keymgr/offline_ksk.c new file mode 100644 index 0000000..1f1110e --- /dev/null +++ b/src/utils/keymgr/offline_ksk.c @@ -0,0 +1,549 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <https://www.gnu.org/licenses/>. + */ + +#include <string.h> +#include <stdio.h> +#include <time.h> + +#include "utils/keymgr/offline_ksk.h" +#include "knot/dnssec/kasp/policy.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/key_records.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-events.h" +#include "knot/dnssec/zone-keys.h" +#include "knot/dnssec/zone-sign.h" +#include "libzscanner/scanner.h" +#include "utils/common/msg.h" +#include "utils/keymgr/functions.h" + +#define KSR_SKR_VER "1.0" + +static int pregenerate_once(kdnssec_ctx_t *ctx, knot_time_t *next) +{ + zone_sign_reschedule_t resch = { 0 }; + + // generate ZSKs + int ret = knot_dnssec_key_rollover(ctx, KEY_ROLL_ALLOW_ZSK_ROLL, &resch); + if (ret != KNOT_EOK) { + ERR2("key rollover failed"); + return ret; + } + // we don't need to do anything explicitly with the generated ZSKs + // they're simply stored in KASP db + + *next = resch.next_rollover; + return KNOT_EOK; +} + +// please free *_dnskey and keyset even if returned error +static int load_dnskey_rrset(kdnssec_ctx_t *ctx, knot_rrset_t **_dnskey, zone_keyset_t *keyset) +{ + // prepare the DNSKEY rrset to be signed + knot_rrset_t *dnskey = knot_rrset_new(ctx->zone->dname, KNOT_RRTYPE_DNSKEY, + KNOT_CLASS_IN, ctx->policy->dnskey_ttl, NULL); + if (dnskey == NULL) { + return KNOT_ENOMEM; + } + *_dnskey = dnskey; + + int ret = load_zone_keys(ctx, keyset, false); + if (ret != KNOT_EOK) { + ERR2("failed to load keys"); + return ret; + } + + for (int i = 0; i < keyset->count; i++) { + zone_key_t *key = &keyset->keys[i]; + if (key->is_public) { + ret = rrset_add_zone_key(dnskey, key); + if (ret != KNOT_EOK) { + ERR2("failed to add zone key"); + return ret; + } + } + } + + return KNOT_EOK; +} + +int keymgr_pregenerate_zsks(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to) +{ + knot_time_t from = 0, to; + int ret = parse_timestamp(arg_to, &to); + if (ret != KNOT_EOK) { + return ret; + } + if (arg_from != NULL) { + ret = parse_timestamp(arg_from, &from); + if (ret != KNOT_EOK) { + return ret; + } + } + + knot_time_t next = (from == 0 ? ctx->now : from); + ret = KNOT_EOK; + + ctx->keep_deleted_keys = true; + ctx->policy->manual = false; + + if (ctx->policy->dnskey_ttl == UINT32_MAX || + ctx->policy->zone_maximal_ttl == UINT32_MAX) { + ERR2("dnskey-ttl or zone-max-ttl not configured"); + return KNOT_ESEMCHECK; + } + + while (ret == KNOT_EOK && knot_time_cmp(next, to) <= 0) { + ctx->now = next; + ret = pregenerate_once(ctx, &next); + } + + return ret; +} + +static int dump_rrset_to_buf(const knot_rrset_t *rrset, char **buf, size_t *buf_size) +{ + if (*buf == NULL) { + *buf = malloc(*buf_size); + if (*buf == NULL) { + return KNOT_ENOMEM; + } + } + + knot_dump_style_t style = { + .wrap = true, + .show_ttl = true, + .verbose = true, + .original_ttl = true, + .human_timestamp = true + }; + return knot_rrset_txt_dump(rrset, buf, buf_size, &style); +} + +static void print_header(const char *of_what, knot_time_t timestamp, const char *contents) +{ + char date[64] = { 0 }; + (void)knot_time_print(TIME_PRINT_ISO8601, timestamp, date, sizeof(date)); + printf(";; %s %"PRIu64" (%s) =========\n%s", of_what, + timestamp, date, contents); +} + +int keymgr_print_offline_records(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to) +{ + knot_time_t from = 0, to = 0; + if (arg_from != NULL) { + int ret = parse_timestamp(arg_from, &from); + if (ret != KNOT_EOK) { + return ret; + } + } + if (arg_to != NULL) { + int ret = parse_timestamp(arg_to, &to); + if (ret != KNOT_EOK) { + return ret; + } + } + + bool empty = true; + char *buf = NULL; + size_t buf_size = 512; + while (true) { + if (arg_to != NULL && knot_time_cmp(from, to) > 0) { + break; + } + knot_time_t next; + key_records_t r = { { 0 } }; + int ret = kasp_db_load_offline_records(ctx->kasp_db, ctx->zone->dname, + &from, &next, &r); + if (ret == KNOT_ENOENT) { + break; + } else if (ret != KNOT_EOK) { + free(buf); + return ret; + } + + ret = key_records_dump(&buf, &buf_size, &r, true); + key_records_clear(&r); + if (ret != KNOT_EOK) { + free(buf); + return ret; + } + print_header("Offline records for", from, buf); + empty = false; + + if (next == 0) { + break; + } + from = next; + } + free(buf); + + /* If from is lower than the first record's timestamp, try to start + from the first one's instead of empty output. */ + if (empty && from > 0) { + knot_time_t last = 0; + int ret = key_records_last_timestamp(ctx, &last); + if (ret == KNOT_EOK && knot_time_cmp(last, from) > 0) { + return keymgr_print_offline_records(ctx, 0, arg_to); + } + } + return KNOT_EOK; +} + +int keymgr_delete_offline_records(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to) +{ + knot_time_t from, to; + int ret = parse_timestamp(arg_from, &from); + if (ret != KNOT_EOK) { + return ret; + } + ret = parse_timestamp(arg_to, &to); + if (ret != KNOT_EOK) { + return ret; + } + return kasp_db_delete_offline_records(ctx->kasp_db, ctx->zone->dname, from, to); +} + +int keymgr_del_all_old(kdnssec_ctx_t *ctx) +{ + for (size_t i = 0; i < ctx->zone->num_keys; ) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + if (knot_time_cmp(key->timing.remove, ctx->now) < 0) { + int ret = kdnssec_delete_key(ctx, key); + if (ret != KNOT_EOK) { + return ret; + } + } else { + i++; + } + } + return kdnssec_ctx_commit(ctx); +} + +static void print_generated_message(void) +{ + char buf[64] = { 0 }; + knot_time_print(TIME_PRINT_ISO8601, knot_time(), buf, sizeof(buf)); + printf("generated at %s by Knot DNS %s\n", buf, VERSION); +} + +static int ksr_once(kdnssec_ctx_t *ctx, char **buf, size_t *buf_size, knot_time_t *next_ksr) +{ + knot_rrset_t *dnskey = NULL; + zone_keyset_t keyset = { 0 }; + int ret = load_dnskey_rrset(ctx, &dnskey, &keyset); + if (ret != KNOT_EOK) { + goto done; + } + ret = dump_rrset_to_buf(dnskey, buf, buf_size); + if (ret >= 0) { + print_header("KeySigningRequest "KSR_SKR_VER, ctx->now, *buf); + ret = KNOT_EOK; + } + +done: + if (ret == KNOT_EOK && next_ksr != NULL) { + *next_ksr = knot_get_next_zone_key_event(&keyset); + } + knot_rrset_free(dnskey, NULL); + free_zone_keys(&keyset); + return ret; +} + +#define OFFLINE_KSK_CONF_CHECK \ + if (!ctx->policy->offline_ksk || !ctx->policy->manual) { \ + ERR2("offline-ksk and manual must be enabled in configuration"); \ + return KNOT_ESEMCHECK; \ + } + +int keymgr_print_ksr(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to) +{ + OFFLINE_KSK_CONF_CHECK + + knot_time_t from, to; + int ret = parse_timestamp(arg_to, &to); + if (ret != KNOT_EOK) { + return ret; + } + if (arg_from == NULL) { + ret = key_records_last_timestamp(ctx, &from); + } else { + ret = parse_timestamp(arg_from, &from); + } + if (ret != KNOT_EOK) { + return ret; + } + + char *buf = NULL; + size_t buf_size = 4096; + while (ret == KNOT_EOK && knot_time_cmp(from, to) < 0) { + ctx->now = from; + ret = ksr_once(ctx, &buf, &buf_size, &from); + } + if (ret != KNOT_EOK) { + free(buf); + return ret; + } + ctx->now = to; + // force end of period as a KSR timestamp + ret = ksr_once(ctx, &buf, &buf_size, NULL); + + printf(";; KeySigningRequest %s ", KSR_SKR_VER); + print_generated_message(); + + free(buf); + return ret; +} + +typedef struct { + int ret; + key_records_t r; + knot_time_t timestamp; + kdnssec_ctx_t *kctx; +} ksr_sign_ctx_t; + +static int ksr_sign_dnskey(kdnssec_ctx_t *ctx, knot_rrset_t *zsk, knot_time_t now, + knot_time_t *next_sign) +{ + zone_keyset_t keyset = { 0 }; + char *buf = NULL; + size_t buf_size = 4096; + knot_time_t rrsigs_expire = 0; + + ctx->now = now; + ctx->policy->dnskey_ttl = zsk->ttl; + + int ret = load_zone_keys(ctx, &keyset, false); + if (ret != KNOT_EOK) { + return ret; + } + + key_records_t r; + key_records_init(ctx, &r); + + ret = knot_zone_sign_add_dnskeys(&keyset, ctx, &r, NULL, NULL); + if (ret != KNOT_EOK) { + goto done; + } + + ret = knot_rdataset_merge(&r.dnskey.rrs, &zsk->rrs, NULL); + if (ret != KNOT_EOK) { + goto done; + } + + // no check if the KSK used for signing (in keyset) is contained in DNSKEY record being signed (in KSR) ! + for (int i = 0; i < keyset.count; i++) { + ret = key_records_sign(&keyset.keys[i], &r, ctx, &rrsigs_expire); + if (ret != KNOT_EOK) { + goto done; + } + } + ret = key_records_dump(&buf, &buf_size, &r, true); + if (ret == KNOT_EOK) { + print_header("SignedKeyResponse "KSR_SKR_VER, ctx->now, buf); + *next_sign = knot_time_min( + knot_get_next_zone_key_event(&keyset), + knot_time_add(rrsigs_expire, -(knot_timediff_t)ctx->policy->rrsig_refresh_before) + ); + } + +done: + free(buf); + key_records_clear(&r); + free_zone_keys(&keyset); + return ret; +} + +static int process_skr_between_ksrs(ksr_sign_ctx_t *ctx, knot_time_t from, knot_time_t to) +{ + for (knot_time_t t = from; t < to /* if (t == infinity) stop */; ) { + int ret = ksr_sign_dnskey(ctx->kctx, &ctx->r.dnskey, t, &t); + if (ret != KNOT_EOK) { + return ret; + } + } + return KNOT_EOK; +} + +static void ksr_sign_header(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + + // parse header + float header_ver; + knot_time_t next_timestamp = 0; + if (sc->error.code != 0 || ctx->ret != KNOT_EOK || + sscanf((const char *)sc->buffer, "; KeySigningRequest %f %"PRIu64, + &header_ver, &next_timestamp) < 1) { + return; + } + (void)header_ver; + + // sign previous KSR and inbetween KSK changes + if (ctx->timestamp > 0) { + knot_time_t inbetween_from; + ctx->ret = ksr_sign_dnskey(ctx->kctx, &ctx->r.dnskey, ctx->timestamp, + &inbetween_from); + if (next_timestamp > 0 && ctx->ret == KNOT_EOK) { + ctx->ret = process_skr_between_ksrs(ctx, inbetween_from, + next_timestamp); + } + key_records_clear_rdatasets(&ctx->r); + } + + // start new KSR + ctx->timestamp = next_timestamp; +} + +static void ksr_sign_once(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + if (sc->error.code == 0 && ctx->ret == KNOT_EOK) { + ctx->ret = knot_rrset_add_rdata(&ctx->r.dnskey, sc->r_data, sc->r_data_length, NULL); + ctx->r.dnskey.ttl = sc->r_ttl; + } +} + +static void skr_import_header(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + + // parse header + float header_ver; + knot_time_t next_timestamp; + if (sc->error.code != 0 || ctx->ret != KNOT_EOK || + sscanf((const char *)sc->buffer, "; SignedKeyResponse %f %"PRIu64, + &header_ver, &next_timestamp) < 1) { + return; + } + (void)header_ver; + + // delete possibly existing conflicting offline records + ctx->ret = kasp_db_delete_offline_records( + ctx->kctx->kasp_db, ctx->kctx->zone->dname, next_timestamp, 0 + ); + + // store previous SKR + if (ctx->timestamp > 0 && ctx->ret == KNOT_EOK) { + ctx->ret = key_records_verify(&ctx->r, ctx->kctx, ctx->timestamp); + if (ctx->ret != KNOT_EOK) { + return; + } + + ctx->ret = kasp_db_store_offline_records(ctx->kctx->kasp_db, + ctx->timestamp, &ctx->r); + key_records_clear_rdatasets(&ctx->r); + } + + // start new SKR + ctx->timestamp = next_timestamp; +} + +static void skr_validate_header(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + + float header_ver; + knot_time_t next_timestamp; + if (sc->error.code != 0 || ctx->ret != KNOT_EOK || + sscanf((const char *)sc->buffer, "; SignedKeyResponse %f %"PRIu64, + &header_ver, &next_timestamp) < 1) { + return; + } + (void)header_ver; + + if (ctx->timestamp > 0 && ctx->ret == KNOT_EOK) { + int ret = key_records_verify(&ctx->r, ctx->kctx, ctx->timestamp); + if (ret != KNOT_EOK) { // ctx->ret untouched + ERR2("invalid SignedKeyResponse for %"KNOT_TIME_PRINTF" (%s)", + ctx->timestamp, knot_strerror(ret)); + } + key_records_clear_rdatasets(&ctx->r); + } + + ctx->timestamp = next_timestamp; +} + +static void skr_import_once(zs_scanner_t *sc) +{ + ksr_sign_ctx_t *ctx = sc->process.data; + if (sc->error.code == 0 && ctx->ret == KNOT_EOK) { + ctx->ret = key_records_add_rdata(&ctx->r, sc->r_type, sc->r_data, + sc->r_data_length, sc->r_ttl); + } +} + +static int read_ksr_skr(kdnssec_ctx_t *ctx, const char *infile, + void (*cb_header)(zs_scanner_t *), void (*cb_record)(zs_scanner_t *)) +{ + zs_scanner_t sc = { 0 }; + int ret = zs_init(&sc, "", KNOT_CLASS_IN, 0); + if (ret < 0) { + return KNOT_ERROR; + } + + ret = zs_set_input_file(&sc, infile); + if (ret < 0) { + zs_deinit(&sc); + return (sc.error.code == ZS_FILE_ACCESS) ? KNOT_EACCES : KNOT_EFILE; + } + + ksr_sign_ctx_t pctx = { 0 }; + key_records_init(ctx, &pctx.r); + pctx.kctx = ctx; + ret = zs_set_processing(&sc, cb_record, NULL, &pctx); + if (ret < 0) { + zs_deinit(&sc); + return KNOT_EBUSY; + } + sc.process.comment = cb_header; + + ret = zs_parse_all(&sc); + + if (sc.error.code != 0) { + ret = KNOT_EMALF; + } else if (pctx.ret != KNOT_EOK) { + ret = pctx.ret; + } else if (ret < 0 || pctx.r.dnskey.rrs.count > 0 || pctx.r.cdnskey.rrs.count > 0 || + pctx.r.cds.rrs.count > 0 || pctx.r.rrsig.rrs.count > 0) { + ret = KNOT_EMALF; + } + key_records_clear(&pctx.r); + zs_deinit(&sc); + return ret; +} + +int keymgr_sign_ksr(kdnssec_ctx_t *ctx, const char *ksr_file) +{ + OFFLINE_KSK_CONF_CHECK + + int ret = read_ksr_skr(ctx, ksr_file, ksr_sign_header, ksr_sign_once); + printf(";; SignedKeyResponse %s ", KSR_SKR_VER); + print_generated_message(); + return ret; +} + +int keymgr_import_skr(kdnssec_ctx_t *ctx, const char *skr_file) +{ + OFFLINE_KSK_CONF_CHECK + + return read_ksr_skr(ctx, skr_file, skr_import_header, skr_import_once); +} + +int keymgr_validate_skr(kdnssec_ctx_t *ctx, const char *skr_file) +{ + return read_ksr_skr(ctx, skr_file, skr_validate_header, skr_import_once); +} diff --git a/src/utils/keymgr/offline_ksk.h b/src/utils/keymgr/offline_ksk.h new file mode 100644 index 0000000..bf0e085 --- /dev/null +++ b/src/utils/keymgr/offline_ksk.h @@ -0,0 +1,35 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + 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 <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/dnssec/context.h" + +int keymgr_pregenerate_zsks(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to); + +int keymgr_print_offline_records(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to); + +int keymgr_delete_offline_records(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to); + +int keymgr_del_all_old(kdnssec_ctx_t *ctx); + +int keymgr_print_ksr(kdnssec_ctx_t *ctx, char *arg_from, char *arg_to); + +int keymgr_sign_ksr(kdnssec_ctx_t *ctx, const char *ksr_file); + +int keymgr_import_skr(kdnssec_ctx_t *ctx, const char *skr_file); + +int keymgr_validate_skr(kdnssec_ctx_t *ctx, const char *skr_file); |