summaryrefslogtreecommitdiffstats
path: root/src/utils/keymgr
diff options
context:
space:
mode:
Diffstat (limited to 'src/utils/keymgr')
-rw-r--r--src/utils/keymgr/bind_privkey.c411
-rw-r--r--src/utils/keymgr/bind_privkey.h72
-rw-r--r--src/utils/keymgr/functions.c951
-rw-r--r--src/utils/keymgr/functions.h55
-rw-r--r--src/utils/keymgr/main.c439
-rw-r--r--src/utils/keymgr/offline_ksk.c502
-rw-r--r--src/utils/keymgr/offline_ksk.h35
7 files changed, 2465 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..9879f6b
--- /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 = { 0 };
+
+ _cleanup_free_ char *line = NULL;
+ size_t size = 0;
+ ssize_t read = 0;
+ while ((read = getline(&line, &size, file)) != -1) {
+ int r = parse_line(&params, line, read);
+ if (r != DNSSEC_EOK) {
+ bind_privkey_free(&params);
+ 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(&params->modulus);
+ gnutls_datum_t e = binary_to_datum(&params->public_exponent);
+ gnutls_datum_t d = binary_to_datum(&params->private_exponent);
+ gnutls_datum_t p = binary_to_datum(&params->prime_one);
+ gnutls_datum_t q = binary_to_datum(&params->prime_two);
+ gnutls_datum_t u = binary_to_datum(&params->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(&params->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(&params->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..0a5bbb4
--- /dev/null
+++ b/src/utils/keymgr/functions.c
@@ -0,0 +1,951 @@
+/* Copyright (C) 2020 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/keymgr/bind_privkey.h"
+#include "contrib/base64.h"
+#include "contrib/ctype.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) {
+ ERROR("invalid timestamp: %s\n", 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";
+
+ // 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) {
+ ERROR("unknown algorithm: %s\n", 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) {
+ ERROR("invalid size: '%s'\n", 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)) {
+ ERROR("invalid parameter: %s\n", 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) {
+ ERROR("semantic error: expected '%s' before '%s'\n", 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(), infty = 0;
+ knot_kasp_key_timing_t gen_timing = { now, infty, now, infty, now, infty, infty, infty, infty };
+ 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 == infty) {
+ gen_timing.ready = gen_timing.active;
+ }
+
+ if (keysize > 0) {
+ 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) {
+ printf("warning: creating key with different algorithm than "
+ "configured in the policy\n");
+ 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) {
+ 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 = { 0 };
+
+ 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 kesytore
+ 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) {
+ printf("Warning: specified salt doesn't match configured "
+ "salt length (%d).\n",
+ (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)
+{
+ uint32_t serial = 0;
+ int ret = kasp_db_load_serial(ctx->kasp_db, ctx->zone->dname,
+ KASPDB_SERIAL_LASTSIGNED, &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, uint32_t new_serial)
+{
+ return kasp_db_store_serial(ctx->kasp_db, ctx->zone->dname,
+ KASPDB_SERIAL_LASTSIGNED, 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; // TODO review
+ }
+
+ // round up bits to bytes
+ bits = (bits + CHAR_BIT - 1) / CHAR_BIT * CHAR_BIT;
+
+ if (bits != optimal_bits) {
+ printf("Notice: Optimal key size for %s is %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) {
+ ERROR("failed to allocate memory\n");
+ return knot_error_from_libdnssec(r);
+ }
+
+ r = gnutls_rnd(GNUTLS_RND_KEY, key.data, key.size);
+ if (r != 0) {
+ ERROR("failed to generate secret the key\n");
+ 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) {
+ ERROR("failed to convert the key to Base64\n");
+ 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))) {
+ ERROR("invalid key specification\n");
+ 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 {
+ ERROR("key is not specified uniquely. Please use id=Full_Key_ID\n");
+ return KNOT_EINVAL;
+ }
+ }
+ }
+ if (*key == NULL) {
+ ERROR("key not found\n");
+ 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) {
+ ERROR("failed to initialize zone %s (%s)\n", 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;
+}
+
+static void print_timer(const char *name, knot_time_t t, knot_time_print_t format,
+ char separator)
+{
+ static char buff[100];
+ if (knot_time_print(format, t, buff, sizeof(buff)) < 0) {
+ printf("%s=(error)%c", name, separator); // shall not happen
+ } else {
+ printf("%s=%s%c", name, buff, separator);
+ }
+}
+
+int keymgr_list_keys(kdnssec_ctx_t *ctx, knot_time_print_t format)
+{
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *key = &ctx->zone->keys[i];
+ 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 "));
+ print_timer("pre-active", key->timing.pre_active, format, ' ');
+ print_timer("publish", key->timing.publish, format, ' ');
+ print_timer("ready", key->timing.ready, format, ' ');
+ print_timer("active", key->timing.active, format, ' ');
+ print_timer("retire-active", key->timing.retire_active, format, ' ');
+ print_timer("retire", key->timing.retire, format, ' ');
+ print_timer("post-active", key->timing.post_active, format, ' ');
+ print_timer("revoke", key->timing.revoke, format, ' ');
+ print_timer("remove", key->timing.remove, format, '\n');
+ }
+ 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;
+}
diff --git a/src/utils/keymgr/functions.h b/src/utils/keymgr/functions.h
new file mode 100644
index 0000000..9a46140
--- /dev/null
+++ b/src/utils/keymgr/functions.h
@@ -0,0 +1,55 @@
+/* Copyright (C) 2020 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"
+
+#define ERROR(msg, ...) { fprintf(stderr, "Error: " msg, ##__VA_ARGS__); fflush(stderr); }
+
+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);
+
+int keymgr_serial_set(kdnssec_ctx_t *ctx, 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, knot_time_print_t format);
+
+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);
diff --git a/src/utils/keymgr/main.c b/src/utils/keymgr/main.c
new file mode 100644
index 0000000..133ebb7
--- /dev/null
+++ b/src/utils/keymgr/main.c
@@ -0,0 +1,439 @@
+/* Copyright (C) 2020 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 <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "contrib/string.h"
+#include "contrib/strtonum.h"
+#include "knot/conf/conf.h"
+#include "knot/dnssec/zone-keys.h"
+#include "libknot/libknot.h"
+#include "utils/common/params.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 -h | -V\n"
+ " %s -t <tsig_name> [<algorithm>] [<bits>]\n"
+ " %s [-c | -C | -d <path>] <zone> <command> [<argument>...]\n"
+ "\n"
+ "Parameters:\n"
+ " -c, --config <file> Use a textual configuration file.\n"
+ " (default %s)\n"
+ " -C, --confdb <dir> Use a binary configuration database directory.\n"
+ " (default %s)\n"
+ " -d, --dir <path> Use specified KASP database path and default configuration.\n"
+ " -t, --tsig <name> [alg] Generate a TSIG key.\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: nsec3salt [<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: 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 <timestamp>)\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)
+{
+ if (argc < opt_ind + 2) {
+ ERROR("zone name and/or command not specified\n");
+ 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);
+
+ knot_lmdb_db_t kaspdb = { 0 };
+ kdnssec_ctx_t kctx = { 0 };
+
+ conf_val_t mapsize = conf_db_param(conf(), C_KASP_DB_MAX_SIZE, C_MAX_KASP_DB_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);
+
+ int ret = kdnssec_ctx_init(conf(), &kctx, zone_name, &kaspdb, NULL);
+ if (ret != KNOT_EOK) {
+ ERROR("failed to initialize KASP (%s)\n", knot_strerror(ret));
+ goto main_end;
+ }
+
+#define CHECK_MISSING_ARG(msg) \
+ if (argc < 3) { \
+ ERROR("%s\n", (msg)); \
+ ret = KNOT_EINVAL; \
+ goto main_end; \
+ }
+
+#define CHECK_MISSING_ARG2(msg) \
+ if (argc < 4) { \
+ ERROR("%s\n", (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) {
+ if (argc > 2) {
+ uint32_t new_serial = 0;
+ if ((ret = str_to_u32(argv[2], &new_serial)) == KNOT_EOK) {
+ ret = keymgr_serial_set(&kctx, new_serial);
+ }
+ } else {
+ ret = keymgr_serial_print(&kctx);
+ 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) {
+ knot_time_print_t format = TIME_PRINT_UNIX;
+ if (argc > 2 && strcmp(argv[2], "human") == 0) {
+ format = TIME_PRINT_HUMAN_MIXED;
+ } else if (argc > 2 && strcmp(argv[2], "iso") == 0) {
+ format = TIME_PRINT_ISO8601;
+ }
+ ret = keymgr_list_keys(&kctx, format);
+ 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("Period not specified");
+ ret = keymgr_pregenerate_zsks(&kctx, argv[2]);
+ } else if (strcmp(argv[1], "show-offline") == 0) {
+ CHECK_MISSING_ARG("Timestamp not specified");
+ ret = keymgr_print_offline_records(&kctx, argv[2], argc > 3 ? argv[3] : NULL);
+ } 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_ARG2("Timestamps from-to not specified");
+ ret = keymgr_print_ksr(&kctx, argv[2], argv[3]);
+ 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 {
+ ERROR("wrong zone-key command: %s\n", argv[1]);
+ goto main_end;
+ }
+
+#undef CHECK_MISSING_ARG
+
+ if (ret == KNOT_EOK) {
+ printf("%s", print_ok_on_succes ? "OK\n" : "");
+ } else {
+ ERROR("%s\n", knot_strerror(ret));
+ }
+
+main_end:
+ kdnssec_ctx_deinit(&kctx);
+ knot_lmdb_deinit(&kaspdb);
+ free(zone_name);
+
+ return ret;
+}
+
+static bool init_conf(const char *confdb)
+{
+ size_t max_conf_size = (size_t)CONF_MAPSIZE * 1024 * 1024;
+
+ conf_flag_t flags = CONF_FNOHOSTNAME | CONF_FOPTMODULES;
+ if (confdb != NULL) {
+ flags |= CONF_FREADONLY;
+ }
+
+ conf_t *new_conf = NULL;
+ int ret = conf_new(&new_conf, conf_schema, confdb, max_conf_size, flags);
+ if (ret != KNOT_EOK) {
+ ERROR("failed opening configuration database %s (%s)\n",
+ (confdb == NULL ? "" : confdb), knot_strerror(ret));
+ return false;
+ }
+ conf_update(new_conf, CONF_UPD_FNONE);
+ return true;
+}
+
+static bool init_confile(const char *confile)
+{
+ int ret = conf_import(conf(), confile, true, false);
+ if (ret != KNOT_EOK) {
+ ERROR("failed opening configuration file %s (%s)\n",
+ confile, knot_strerror(ret));
+ return false;
+ }
+ return true;
+}
+
+static bool init_conf_blank(const char *kasp_dir)
+{
+ char *confstr = sprintf_alloc("database:\n"
+ " storage: .\n"
+ " kasp-db: \"%s\"\n", kasp_dir);
+ int ret = conf_import(conf(), confstr, false, false);
+ free(confstr);
+ if (ret != KNOT_EOK) {
+ ERROR("failed creating fake configuration (%s)\n",
+ knot_strerror(ret));
+ return false;
+ }
+ return true;
+}
+
+static void update_privileges(void)
+{
+ int uid, gid;
+ if (conf_user(conf(), &uid, &gid) != KNOT_EOK) {
+ return;
+ }
+
+ // Just try to alter process privileges if different from configured.
+ int unused __attribute__((unused));
+ if ((gid_t)gid != getgid()) {
+ unused = setregid(gid, gid);
+ }
+ if ((uid_t)uid != getuid()) {
+ unused = setreuid(uid, uid);
+ }
+}
+
+static bool conf_initialized = false; // This is a singleton as well as conf() is.
+
+#define CHECK_CONF_UNINIT \
+ if ((conf_initialized = !conf_initialized) == false) { \
+ ERROR("multiple arguments attempting configuration initializatioin\n"); \
+ return EXIT_FAILURE; \
+ }
+
+int main(int argc, char *argv[])
+{
+ int ret;
+
+ struct option opts[] = {
+ { "config", required_argument, NULL, 'c' },
+ { "confdb", required_argument, NULL, 'C' },
+ { "dir", required_argument, NULL, 'd' },
+ { "tsig", required_argument, NULL, 't' },
+ { "help", no_argument, NULL, 'h' },
+ { "version", no_argument, NULL, 'V' },
+ { NULL }
+ };
+
+ tzset();
+
+ int opt = 0, parm = 0;
+ while ((opt = getopt_long(argc, argv, "hVd:c:C:t:", opts, NULL)) != -1) {
+ switch (opt) {
+ case 'h':
+ print_help();
+ return EXIT_SUCCESS;
+ case 'V':
+ print_version(PROGRAM_NAME);
+ return EXIT_SUCCESS;
+ case 'd':
+ CHECK_CONF_UNINIT
+ if (!init_conf(NULL) || !init_conf_blank(optarg)) {
+ return EXIT_FAILURE;
+ }
+ break;
+ case 'c':
+ CHECK_CONF_UNINIT
+ if (!init_conf(NULL) || !init_confile(optarg)) {
+ return EXIT_FAILURE;
+ }
+ break;
+ case 'C':
+ CHECK_CONF_UNINIT
+ if (!init_conf(argv[2])) {
+ return EXIT_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) {
+ ERROR("failed to generate TSIG (%s)\n", knot_strerror(ret));
+ }
+ return (ret == KNOT_EOK ? EXIT_SUCCESS : EXIT_FAILURE);
+ default:
+ print_help();
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (!conf_initialized) {
+ struct stat st;
+ if (conf_db_exists(CONF_DEFAULT_DBDIR) && init_conf(CONF_DEFAULT_DBDIR)) {
+ // initialized conf from default DB location
+ } else if (stat(CONF_DEFAULT_FILE, &st) == 0 &&
+ init_conf(NULL) && init_confile(CONF_DEFAULT_FILE)) {
+ // initialized conf from default confile
+ } else {
+ ERROR("couldn't initialize configuration, please provide -c, -C, or -d option\n");
+ return EXIT_FAILURE;
+ }
+ }
+
+ update_privileges();
+
+ ret = key_command(argc, argv, optind);
+
+ conf_free(conf());
+
+ return (ret == KNOT_EOK ? EXIT_SUCCESS : EXIT_FAILURE);
+}
diff --git a/src/utils/keymgr/offline_ksk.c b/src/utils/keymgr/offline_ksk.c
new file mode 100644
index 0000000..f4fc1dd
--- /dev/null
+++ b/src/utils/keymgr/offline_ksk.c
@@ -0,0 +1,502 @@
+/* Copyright (C) 2020 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/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) {
+ ERROR("key rollover failed\n");
+ 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) {
+ ERROR("failed to load keys\n");
+ 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) {
+ ERROR("failed to add zone key\n");
+ return ret;
+ }
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+int keymgr_pregenerate_zsks(kdnssec_ctx_t *ctx, char *arg)
+{
+ knot_time_t upto;
+ int ret = parse_timestamp(arg, &upto);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ knot_time_t next = ctx->now;
+ 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) {
+ ERROR("dnskey-ttl or zone-max-ttl not configured\n");
+ return KNOT_ESEMCHECK;
+ }
+
+ while (ret == KNOT_EOK && knot_time_cmp(next, upto) <= 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_tmstamp = 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, next = 0;
+ int ret = parse_timestamp(arg_from, &from);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ if (arg_to != NULL) {
+ ret = parse_timestamp(arg_to, &to);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+ char *buf = NULL;
+ size_t buf_size = 512;
+ for (knot_time_t i = from; ret == KNOT_EOK && i != 0 && (arg_to == NULL || knot_time_cmp(i, to) < 0); i = next) {
+ key_records_t r = { { 0 } };
+ ret = kasp_db_load_offline_records(ctx->kasp_db, ctx->zone->dname, i, &next, &r);
+ if (ret == KNOT_EOK) {
+ ret = key_records_dump(&buf, &buf_size, &r, true);
+ }
+ if (ret == KNOT_EOK) {
+ print_header("Offline records for", i, buf);
+ }
+ key_records_clear(&r);
+ }
+ free(buf);
+
+ return ret;
+}
+
+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; i++) {
+ 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);
+ printf("- %s\n", knot_strerror(ret));
+ }
+ }
+ 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) { \
+ ERROR("offline-ksk and manual must be enabled in configuration\n"); \
+ 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_from, &from);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ ret = parse_timestamp(arg_to, &to);
+ 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;
+
+ 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);
+ 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
+ ERROR("invalid SignedKeyResponse for %"KNOT_TIME_PRINTF" (%s)\n",
+ 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 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..21b0eb8
--- /dev/null
+++ b/src/utils/keymgr/offline_ksk.h
@@ -0,0 +1,35 @@
+/* Copyright (C) 2020 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);
+
+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);