policy:
- id: STR
- keystore: keystore_id
+ keystore: keystore_id ...
manual: BOOL
single-type-signing: BOOL
algorithm: rsasha1 | rsasha1-nsec3-sha1 | rsasha256 | rsasha512 | ecdsap256sha256 | ecdsap384sha384 | ed25519 | ed448
A :ref:`reference<keystore_id>` to a keystore holding private key material
for zones.
-*Default:* an imaginary keystore with all default values
+If multiple keystores are specified, private keys for signing are looked up in
+all of them. But newly generated keys are stored in the first one in the specified order.
+
+.. NOTE::
+ If multiple keystores are configured and a zone is being restored
+ with the :ref:`back up<Data and metadata backup>` feature, all restored
+ private keys are stored into the first referenced keystore.
.. NOTE::
A configured keystore called "default" won't be used unless explicitly referenced.
+*Default:* an imaginary keystore with all default values
+
.. _policy_manual:
manual
static const yp_item_t desc_policy[] = {
{ C_ID, YP_TSTR, YP_VNONE, CONF_IO_FREF },
- { C_KEYSTORE, YP_TREF, YP_VREF = { C_KEYSTORE }, CONF_IO_FRLD_ZONES,
+ { C_KEYSTORE, YP_TREF, YP_VREF = { C_KEYSTORE }, YP_FMULTI | CONF_IO_FRLD_ZONES,
{ check_ref_dflt } },
{ C_MANUAL, YP_TBOOL, YP_VNONE, CONF_IO_FRLD_ZONES },
{ C_SINGLE_TYPE_SIGNING, YP_TBOOL, YP_VNONE, CONF_IO_FRLD_ZONES },
conf_id_fix_default(&policy_id);
policy_load(ctx->policy, conf, &policy_id, ctx->zone->dname);
- ret = zone_init_keystore(conf, &policy_id, NULL, &ctx->keystore,
- &ctx->keystore_type, &ctx->policy->key_label);
+ ret = zone_init_keystore(conf, &policy_id, NULL, &ctx->keystores);
if (ret != KNOT_EOK) {
goto init_error;
}
free(ctx->policy);
}
key_records_clear(&ctx->offline_records);
- dnssec_keystore_deinit(ctx->keystore);
+ zone_deinit_keystore(&ctx->keystores);
kasp_zone_free(&ctx->zone);
free(ctx->kasp_zone_path);
knot_lmdb_db_t *kasp_db;
knot_kasp_zone_t *zone;
knot_kasp_policy_t *policy;
- dnssec_keystore_t *keystore;
- unsigned keystore_type;
+ knot_kasp_keystore_t *keystores;
char *kasp_zone_path;
}
}
+void zone_deinit_keystore(knot_kasp_keystore_t **keystores)
+{
+ if (keystores != NULL && *keystores != NULL) {
+ for (size_t i = 0; i < (*keystores)[0].count; i++) {
+ dnssec_keystore_deinit((*keystores)[i].keystore);
+ }
+ free(*keystores);
+ *keystores = NULL;
+ }
+}
+
int zone_init_keystore(conf_t *conf, conf_val_t *policy_id, conf_val_t *keystore_id,
- dnssec_keystore_t **keystore, unsigned *backend, bool *key_label)
+ knot_kasp_keystore_t **keystores)
{
+ if (keystores == NULL || *keystores != NULL ||
+ (bool)(policy_id == NULL) == (bool)(keystore_id == NULL)) {
+ return KNOT_EINVAL;
+ }
+
char *zone_path = conf_db(conf, C_KASP_DB);
if (zone_path == NULL) {
return KNOT_ENOMEM;
conf_id_fix_default(keystore_id);
}
- conf_val_t val = conf_id_get(conf, C_KEYSTORE, C_BACKEND, keystore_id);
- unsigned _backend = conf_opt(&val);
+ size_t ks_count = conf_val_count(keystore_id);
+ *keystores = calloc(ks_count, sizeof(**keystores));
+ if (*keystores == NULL) {
+ free(zone_path);
+ return KNOT_ENOMEM;
+ }
- val = conf_id_get(conf, C_KEYSTORE, C_CONFIG, keystore_id);
- const char *config = conf_str(&val);
+ int ret = KNOT_EOK;
+ for (size_t i = 0; i < ks_count && ret == KNOT_EOK; i++) {
+ knot_kasp_keystore_t *ks = *keystores + i;
- if (key_label != NULL) {
+ conf_val_t val = conf_id_get(conf, C_KEYSTORE, C_BACKEND, keystore_id);
+ ks->backend = conf_opt(&val);
val = conf_id_get(conf, C_KEYSTORE, C_KEY_LABEL, keystore_id);
- *key_label = conf_bool(&val);
- }
+ ks->key_label = conf_bool(&val);
+ ks->count = ks_count;
- int ret = keystore_load(config, _backend, zone_path, keystore);
+ val = conf_id_get(conf, C_KEYSTORE, C_CONFIG, keystore_id);
+ const char *config = conf_str(&val);
+ ret = keystore_load(config, ks->backend, zone_path, &ks->keystore);
- if (backend != NULL) {
- *backend = _backend;
+ if (ks_count > 1) { // Don't try to iterate if not multivalued.
+ conf_val_next(keystore_id);
+ }
+ }
+
+ if (ret != KNOT_EOK) {
+ zone_deinit_keystore(keystores);
}
free(zone_path);
void free_key_params(key_params_t *parm);
+void zone_deinit_keystore(knot_kasp_keystore_t **keystores);
+
int zone_init_keystore(conf_t *conf, conf_val_t *policy_id, conf_val_t *keystore_id,
- dnssec_keystore_t **keystore, unsigned *backend, bool *key_label);
+ knot_kasp_keystore_t **keystores);
int kasp_zone_keys_from_rr(knot_kasp_zone_t *zone,
const knot_rdataset_t *zone_dnskey,
#include "contrib/time.h"
#include "libdnssec/key.h"
+#include "libdnssec/keystore.h"
#include "knot/conf/conf.h"
/*!
bool is_zsk;
} knot_kasp_key_t;
+typedef struct {
+ dnssec_keystore_t *keystore;
+ unsigned backend;
+ bool key_label;
+ size_t count; /*!< Number of keystores configured. */
+} knot_kasp_keystore_t;
+
/*!
* Parent for DS checks.
*/
bool has_dnskey_sync;
bool offline_ksk;
bool incremental;
- bool key_label;
unsigned unsafe;
uint32_t keytag_remain;
uint32_t keytag_modulo;
const char *label = NULL;
char label_buf[sizeof(knot_dname_txt_storage_t) + 16];
- if (ctx->policy->key_label &&
+ if (ctx->keystores[0].key_label &&
knot_dname_to_str(label_buf, ctx->zone->dname, sizeof(label_buf)) != NULL) {
const char *key_type = (flags & DNSKEY_GENERATE_KSK) ? " KSK" : " ZSK" ;
strlcat(label_buf, key_type, sizeof(label_buf));
label = label_buf;
}
+ assert(ctx->keystores != NULL && ctx->keystores[0].count > 0 && ctx->keystores[0].keystore != NULL);
+
for (size_t i = 0; i < GENERATE_KEYTAG_ATTEMPTS; i++) {
- int ret = generate_dnssec_key(ctx->keystore, ctx->zone->dname, label,
+ int ret = generate_dnssec_key(ctx->keystores[0].keystore, ctx->zone->dname, label,
ctx->policy->algorithm, size, flags,
id, key);
if (ret != KNOT_EOK) {
return KNOT_EOK;
}
- (void)dnssec_keystore_remove(ctx->keystore, *id);
+ (void)dnssec_keystore_remove(ctx->keystores[0].keystore, *id);
dnssec_key_free(*key);
free(*id);
}
{
assert(ctx);
assert(ctx->zone);
- assert(ctx->keystore);
+ assert(ctx->keystores);
assert(ctx->policy);
normalize_generate_flags(&flags);
r = kasp_zone_append(ctx->zone, key);
free(key);
if (r != KNOT_EOK) {
- (void)dnssec_keystore_remove(ctx->keystore, id);
+ (void)dnssec_keystore_remove(ctx->keystores[0].keystore, id);
dnssec_key_free(dnskey);
free(id);
return r;
{
assert(ctx);
assert(ctx->zone);
- assert(ctx->keystore);
+ assert(ctx->keystores);
+ assert(ctx->keystores[0].count > 0);
assert(ctx->policy);
ssize_t key_index = key_ptr - ctx->zone->keys;
}
if (!key_still_used_in_keystore && !key_ptr->is_pub_only) {
- ret = dnssec_keystore_remove(ctx->keystore, key_ptr->id);
+ ret = DNSSEC_ENOENT;
+ for (size_t i = 0; i < ctx->keystores[0].count && ret == DNSSEC_ENOENT; i++) {
+ ret = dnssec_keystore_remove(ctx->keystores[i].keystore, key_ptr->id);
+ }
if (ret != KNOT_EOK) {
return ret;
}
return KNOT_EOK;
}
+int kdnssec_load_private(knot_kasp_keystore_t *keystores, const char *id,
+ dnssec_key_t *key, unsigned *backend)
+{
+ int ret = DNSSEC_ENOENT;
+ for (size_t i = 0; i < keystores[0].count && ret == DNSSEC_ENOENT; i++) {
+ if (backend != NULL) {
+ *backend = keystores[i].backend;
+ }
+ ret = dnssec_keystore_get_private(keystores[i].keystore, id, key);
+ }
+ return ret;
+}
+
/*!
* \brief Load private keys for active keys.
*/
if (!key->is_active && !key->is_ksk_active_plus && !key->is_zsk_active_plus) {
continue;
}
- int ret = dnssec_keystore_get_private(ctx->keystore, key->id, key->key);
+ int ret = kdnssec_load_private(ctx->keystores, key->id, key->key, NULL);
switch (ret) {
case DNSSEC_EOK:
case DNSSEC_KEY_ALREADY_PRESENT:
*/
int kdnssec_delete_key(kdnssec_ctx_t *ctx, knot_kasp_key_t *key_ptr);
+/*!
+ * \brief Load private key for given ID by searching all configured keystores.
+ *
+ * \param keystores Array of keystores.
+ * \param id Key ID to search.
+ * \param key Libdnssec key structure to be filled with private material.
+ * \param backend Optional: backend of the keystore where found.
+ *
+ * \return DNSSEC_E*
+ */
+int kdnssec_load_private(knot_kasp_keystore_t *keystores, const char *id,
+ dnssec_key_t *key, unsigned *backend);
+
/*!
* \brief Load zone keys and init cryptographic context.
*
#include "knot/ctl/commands.h"
#include "knot/dnssec/kasp/kasp_zone.h"
#include "knot/dnssec/kasp/keystore.h"
+#include "knot/dnssec/zone-keys.h"
#include "knot/journal/journal_metadata.h"
#include "knot/server/server.h"
#include "knot/zone/backup_dir.h"
return sprintf_alloc("%s/%s", dir_name, basename);
}
-static int backup_key(key_params_t *parm, dnssec_keystore_t *from, dnssec_keystore_t *to)
+static int backup_key(key_params_t *parm, const knot_dname_t *zname,
+ knot_kasp_keystore_t *from, knot_kasp_keystore_t *to)
{
dnssec_key_t *key = NULL;
int ret = dnssec_key_new(&key);
}
dnssec_key_set_algorithm(key, parm->algorithm);
- ret = dnssec_keystore_get_private(from, parm->id, key);
+ dnssec_keystore_t *to_pem = NULL;
+ for (size_t i = 0; i < to[0].count; i++) {
+ if (to[i].backend == KEYSTORE_BACKEND_PEM) {
+ to_pem = to[i].keystore;
+ break;
+ }
+ }
+
+ unsigned backend;
+ ret = kdnssec_load_private(from, parm->id, key, &backend);
if (ret == DNSSEC_EOK) {
- ret = dnssec_keystore_set_private(to, key);
+ if (backend == KEYSTORE_BACKEND_PKCS11 || to_pem == NULL) {
+ log_zone_notice(zname, "private keys from PKCS #11 are not subject of backup/restore, skipping them");
+ } else {
+ ret = dnssec_keystore_set_private(to[0].keystore, key);
+ }
}
dnssec_key_free(key);
static int backup_keystore(conf_t *conf, zone_t *zone, zone_backup_ctx_t *ctx)
{
- dnssec_keystore_t *from = NULL, *to = NULL;
+ knot_kasp_keystore_t *from = NULL, *to = NULL;
conf_val_t policy_id = get_zone_policy(conf, zone->name);
- unsigned backend_type = 0;
- int ret = zone_init_keystore(conf, &policy_id, NULL, &from, &backend_type, NULL);
+ int ret = zone_init_keystore(conf, &policy_id, NULL, &from);
if (ret != KNOT_EOK) {
LOG_FAIL("keystore init");
return ret;
}
- if (backend_type == KEYSTORE_BACKEND_PKCS11) {
- log_zone_notice(zone->name, "private keys from PKCS #11 are not subject of backup/restore");
- (void)dnssec_keystore_deinit(from);
- return KNOT_EOK;
+
+ to = calloc(1, sizeof(*to));
+ if (to == NULL) {
+ LOG_FAIL("out of memory");
+ zone_deinit_keystore(&from);
+ return KNOT_ENOMEM;
}
+ dnssec_keystore_t *to_ks;
char kasp_dir[strlen(ctx->backup_dir) + 6];
(void)snprintf(kasp_dir, sizeof(kasp_dir), "%s/keys", ctx->backup_dir);
- ret = keystore_load("keys", KEYSTORE_BACKEND_PEM, kasp_dir, &to);
+ ret = keystore_load("keys", KEYSTORE_BACKEND_PEM, kasp_dir, &to_ks);
if (ret != KNOT_EOK) {
LOG_FAIL("keystore load");
goto done;
}
+ to->keystore = to_ks;
+ to->count = 1;
+ to->backend = KEYSTORE_BACKEND_PEM;
BACKUP_SWAP(ctx, from, to);
WALK_LIST(n, key_params) {
key_params_t *parm = n->d;
if (ret == KNOT_EOK && !parm->is_pub_only) {
- ret = backup_key(parm, from, to);
+ ret = backup_key(parm, zone->name, from, to);
}
free_key_params(parm);
}
ptrlist_deep_free(&key_params, NULL);
done:
- (void)dnssec_keystore_deinit(to);
- (void)dnssec_keystore_deinit(from);
+ zone_deinit_keystore(&to);
+ zone_deinit_keystore(&from);
return ret;
}
bind_privkey_free(&bpriv);
- ret = dnssec_keystore_import(ctx->keystore, &pem, &keyid);
+ ret = dnssec_keystore_import(ctx->keystores[0].keystore, &pem, &keyid);
dnssec_binary_free(&pem);
if (ret != DNSSEC_EOK) {
goto fail;
}
// put pem to keystore
- ret = dnssec_keystore_import(ctx->keystore, &pem, &keyid);
+ ret = dnssec_keystore_import(ctx->keystores[0].keystore, &pem, &keyid);
dnssec_binary_free(&pem);
if (ret != DNSSEC_EOK) {
err_import_key(keyid, param);
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);
+ ret = kdnssec_load_private(ctx->keystores, keyid, key, NULL);
if (ret != DNSSEC_EOK) {
err_import_key(keyid, "");
goto fail;
return DNSSEC_INVALID_KEY_ID;
}
- if (ctx->keystore_type != KEYSTORE_BACKEND_PKCS11) {
+ if (ctx->keystores[0].backend != KEYSTORE_BACKEND_PKCS11) {
knot_dname_txt_storage_t dname_str;
(void)knot_dname_to_str(dname_str, ctx->zone->dname, sizeof(dname_str));
ERR2("not a PKCS #11 keystore for zone %s", dname_str);
static bool key_missing(kdnssec_ctx_t *ctx, const knot_kasp_key_t *key)
{
return !key->is_pub_only && DNSSEC_EOK !=
- dnssec_keystore_get_private(ctx->keystore, key->id, key->key);
+ kdnssec_load_private(ctx->keystores, key->id, key->key, NULL);
}
int keymgr_list_keys(kdnssec_ctx_t *ctx, keymgr_list_params_t *params)
id.blob = (const uint8_t *)keystore_id;
id.blob_len = len;
- unsigned backend;
- bool key_label;
+ knot_kasp_keystore_t *keystores = NULL;
- int ret = zone_init_keystore(conf(), NULL, &id, store, &backend, &key_label);
+ int ret = zone_init_keystore(conf(), NULL, &id, &keystores);
if (ret != KNOT_EOK) {
ERR2("failed to open '%s' keystore (%s)", keystore_id, knot_strerror(ret));
return ret;
}
+ assert(keystores[0].count == 1);
+ *store = keystores[0].keystore;
if (strcmp(keystore_id, DFLT_ID) == 0) {
printf("Keystore default");
} else {
printf("Keystore id '%s'", keystore_id);
};
- printf(", type %s", (backend == KEYSTORE_BACKEND_PEM ? "PEM" : "PKCS #11"));
+ printf(", type %s", (keystores[0].backend == KEYSTORE_BACKEND_PEM ? "PEM" : "PKCS #11"));
if (threads > 0) {
printf(", threads %u", threads);
}
printf("\n\n");
+ free(keystores);
return KNOT_EOK;
}
--- /dev/null
+#!/usr/bin/env python3
+
+"""
+Check of multi-keystore operation.
+"""
+
+import os
+import random
+import shutil
+from dnstest.utils import *
+from dnstest.keys import Keymgr
+from dnstest.test import Test
+
+def check_key_count(server, keystore, expected):
+ ksdir = os.path.join(server.keydir, keystore)
+ try:
+ files = len([name for name in os.listdir(ksdir)])
+ except FileNotFoundError:
+ files = 0
+ compare(files, expected, "privkey count in %s" % ksdir)
+
+t = Test()
+
+server = t.server("knot")
+zone = t.zone("catalog.") # has zero TTL => faster key rollovers
+t.link(zone, server)
+
+server.dnssec(zone).enable = True
+server.dnssec(zone).propagation_delay = 1
+server.dnssec(zone).keystores = [ "keys1", "keys2" ]
+
+t.start()
+serial = server.zone_wait(zone)
+
+check_key_count(server, "keys1", 2)
+check_key_count(server, "keys2", 0)
+
+server.dnssec(zone).keystores = [ "keys2", "keys1" ]
+server.gen_confile()
+server.reload()
+server.ctl("zone-key-rollover %s zsk" % zone[0].name)
+
+serial += 2 # wait for three increments which is whole ZSK rollover
+serial = server.zone_wait(zone, serial)
+
+check_key_count(server, "keys1", 1)
+check_key_count(server, "keys2", 1)
+
+backup_dir = os.path.join(server.dir, "backup1")
+server.ctl("zone-backup +backupdir %s %s" % (backup_dir, zone[0].name), wait=True)
+shutil.rmtree(os.path.join(server.keydir, "keys1"))
+shutil.rmtree(os.path.join(server.keydir, "keys2"))
+server.ctl("zone-restore +backupdir %s %s" % (backup_dir, zone[0].name), wait=True)
+
+check_key_count(server, "keys1", 0)
+check_key_count(server, "keys2", 2) # restore puts all keys to first configured keystore no matter where they were at backup
+
+server.ctl("zone-sign %s" % zone[0].name, wait=True) # check that signing still works after restore
+serial = server.zone_wait(zone, serial)
+
+server.flush(zone[0], wait=True)
+server.zone_verify(zone[0])
+
+t.end()
self.validate = None
self.disable = None # create the policy in config, but set dnssec-signing: off
self.manual = None
+ self.keystores = []
self.single_type_signing = None
self.alg = None
self.ksk_size = None
if have_dnskeysync:
s.end()
+ have_keystore = False
+ for zone in sorted(self.zones):
+ z = self.zones[zone]
+ if not z.dnssec.keystores:
+ continue
+ if not have_keystore:
+ s.begin("keystore")
+ have_keystore = True
+ for ks in z.dnssec.keystores:
+ s.id_item("id", ks)
+ s.item("config", ks)
+ if have_keystore:
+ s.end()
+
have_policy = False
for zone in sorted(self.zones):
z = self.zones[zone]
s.id_item("id", z.name)
self._bool(s, "manual", z.dnssec.manual)
self._bool(s, "single-type-signing", z.dnssec.single_type_signing)
+ if z.dnssec.keystores:
+ s.item("keystore", "[ %s ]" % ", ".join(z.dnssec.keystores))
self._str(s, "algorithm", z.dnssec.alg)
self._str(s, "ksk-size", z.dnssec.ksk_size)
self._str(s, "zsk-size", z.dnssec.zsk_size)