]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
dnssec/multi-keystore: implemented ksk-only keystore...
authorLibor Peltan <libor.peltan@nic.cz>
Wed, 28 May 2025 12:57:19 +0000 (14:57 +0200)
committerDaniel Salzman <daniel.salzman@nic.cz>
Tue, 1 Jul 2025 08:51:18 +0000 (10:51 +0200)
...so that KSKs and ZSKs can be in distinct keystores

19 files changed:
doc/operation.rst
doc/reference.rst
src/knot/conf/schema.c
src/knot/conf/schema.h
src/knot/dnssec/kasp/kasp_zone.c
src/knot/dnssec/kasp/policy.h
src/knot/dnssec/zone-keys.c
src/knot/dnssec/zone-keys.h
src/libknot/errcode.h
src/libknot/error.c
src/utils/keymgr/functions.c
tests-extra/tests/dnssec/keystores/data/8329a00d5dceefdcbbf7b8a3cdf61fe944c51d6f.pem [new file with mode: 0644]
tests-extra/tests/dnssec/keystores/data/894d4240398f459f59f4a99cd4c5b658c9a62d54.pem [new file with mode: 0644]
tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+07147.key [new file with mode: 0644]
tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+07147.private [new file with mode: 0644]
tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+18635.key [new file with mode: 0644]
tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+18635.private [new file with mode: 0644]
tests-extra/tests/dnssec/keystores/test.py
tests-extra/tools/dnstest/server.py

index 5d5c6d50e45e9b43c698767f4b70b7a8a21d1f1e..8a2b6d8fd238aa69e87c98e7a4e1d88c29b68c79 100644 (file)
@@ -1214,6 +1214,46 @@ or module used for communication with the HSM.
      --write-object c4eae5dea3ee8c15395680085c515f2ad41941b6.pub.der --type pubkey \
      --usage-sign --id c4eae5dea3ee8c15395680085c515f2ad41941b6
 
+.. _DNSSEC multiple keystores:
+
+DNSSEC multiple keystores
+=========================
+
+Private keys for signing a single zone can be stored in more than one keystore
+with two main use-cases:
+
+Keystore migration
+------------------
+
+Some keystores (HSMs) do not allow exporting private key material, so it is not
+possible to migrate away from them by copying the private keys to a new keystore
+(another HSM or a directory with PEM files). In such cases, the migration must
+be performed by key rollover(s), where new keys are generated in the target
+keystore, while the old keys remain in the original one and are eventually
+retired and removed.
+
+During the transition period (i.e. rollovers), both keystores must be
+configured simultaneously. This is achieved by configuring them both in the
+:ref:`keystore section` and referencing them in the :ref:`policy_keystore`
+option in :ref:`policy section`. The order matters: new keys are generated
+in the first referenced keystore, so in this case, the target keystore must be
+listed first.
+
+KSK more secured than ZSK
+-------------------------
+
+When desirable, the Key Signing Key (KSK) can be stored in an HSM, while the Zone
+Signing Key (ZSK) is kept in a simple PEM file. This enhances the security of
+the KSK while preserving zone signing performance (as HSM signing operations are
+usually much slower).
+
+To achieve this, both keystores must be configured in the
+:ref:`keystore section`, while the PKCS#11 keystore (the HSM) has the
+:ref:`keystore_ksk-only` option enabled and being listed first in the
+:ref:`policy_keystore` option in :ref:`policy section`. With this configuration,
+new KSKs are generated in the HSM, while ZSKs are generated in the second
+keystore.
+
 .. _Controlling a running daemon:
 
 Daemon controls
index d660293d636075f26a5f38b4d55ad456f17b4555..19f0d47091d32a7eb194978207c2c60e8b05022a 100644 (file)
@@ -1347,6 +1347,7 @@ DNSSEC keystore configuration.
    - id: STR
      backend: pem | pkcs11
      config: STR
+     ksk-only: BOOL
      key-label: BOOL
 
 .. _keystore_id:
@@ -1388,6 +1389,16 @@ The PKCS #11 URI Scheme is defined in :rfc:`7512`.
 
 *Default:* :ref:`kasp-db<database_kasp-db>`\ ``/keys``
 
+.. _keystore_ksk-only:
+
+ksk-only
+--------
+
+Newly generated keys sre stored in this keystore only if they are KSKs or CSKs.
+Zone signing keys will be stored in subsequent keystore without this option enabled.
+
+*Default:* ``off``
+
 .. _keystore_key-label:
 
 key-label
@@ -2056,7 +2067,9 @@ A :ref:`reference<keystore_id>` to a keystore holding private key material
 for zones.
 
 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.
+all of them. But newly generated keys are stored in the first one (or in the
+first one without enabled :ref:`keystore_ksk-only` in the case of a new ZSK)
+in the specified order.
 
 .. NOTE::
    If multiple keystores are configured and a zone is being restored
index 1db67b716fd4feead9e616da113e6724994dce21..0c4e8e954daa6718ac56373424a4368ac82d0057 100644 (file)
@@ -321,6 +321,7 @@ static const yp_item_t desc_keystore[] = {
        { C_BACKEND,   YP_TOPT,  YP_VOPT = { keystore_backends, KEYSTORE_BACKEND_PEM },
                                 CONF_IO_FRLD_ZONES },
        { C_CONFIG,    YP_TSTR,  YP_VSTR = { "keys" }, CONF_IO_FRLD_ZONES },
+       { C_KSK_ONLY,  YP_TBOOL, YP_VNONE },
        { C_KEY_LABEL, YP_TBOOL, YP_VNONE },
        { C_COMMENT,   YP_TSTR,  YP_VNONE },
        { NULL }
index 87984df91ad009c5234e47c23e72ab54a5f101d4..9ba63f542d827032e4e75f990aabc34af8c211fe 100644 (file)
@@ -81,6 +81,7 @@
 #define C_KEY_FILE             "\x08""key-file"
 #define C_KEY_LABEL            "\x09""key-label"
 #define C_KSK_LIFETIME         "\x0C""ksk-lifetime"
+#define C_KSK_ONLY             "\x08""ksk-only"
 #define C_KSK_SBM              "\x0E""ksk-submission"
 #define C_KSK_SHARED           "\x0a""ksk-shared"
 #define C_KSK_SIZE             "\x08""ksk-size"
index 808c2c663a288bf60fb65e8fb26729a0b073dc8e..390bc3c3f2bf0300b652925bb1a96baece0ab200 100644 (file)
@@ -351,6 +351,8 @@ int zone_init_keystore(conf_t *conf, conf_val_t *policy_id, conf_val_t *keystore
 
                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_KSK_ONLY, keystore_id);
+               ks->ksk_only = conf_bool(&val);
                val = conf_id_get(conf, C_KEYSTORE, C_KEY_LABEL, keystore_id);
                ks->key_label = conf_bool(&val);
                ks->count = ks_count;
index 7f9db32609990b634c207067eda553dcd03e0c92..f7bc932306d3013fcdea031d52183fadc14bc04c 100644 (file)
@@ -57,6 +57,7 @@ typedef struct {
 typedef struct {
        dnssec_keystore_t *keystore;
        unsigned backend;
+       bool ksk_only;
        bool key_label;
        size_t count;                   /*!< Number of keystores configured. */
 } knot_kasp_keystore_t;
index 2a4b305e31b3978d9044ed5c6c3c04ddf67efddf..779386c0335694b7ec3b67eaeda95a7f9cfae99f 100644 (file)
@@ -96,6 +96,7 @@ static bool keytag_in_use(kdnssec_ctx_t *ctx, uint16_t keytag)
 #define GENERATE_KEYTAG_ATTEMPTS (40)
 
 static int generate_keytag_unconflict(kdnssec_ctx_t *ctx,
+                                      knot_kasp_keystore_t *keystore,
                                       kdnssec_generate_flags_t flags,
                                       char **id,
                                       dnssec_key_t **key)
@@ -106,17 +107,15 @@ static int generate_keytag_unconflict(kdnssec_ctx_t *ctx,
        const char *label = NULL;
 
        char label_buf[sizeof(knot_dname_txt_storage_t) + 16];
-       if (ctx->keystores[0].key_label &&
+       if (keystore->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->keystores[0].keystore, ctx->zone->dname, label,
+               int ret = generate_dnssec_key(keystore->keystore, ctx->zone->dname, label,
                                              ctx->policy->algorithm, size, flags,
                                              id, key);
                if (ret != KNOT_EOK) {
@@ -128,7 +127,7 @@ static int generate_keytag_unconflict(kdnssec_ctx_t *ctx,
                        return KNOT_EOK;
                }
 
-               (void)dnssec_keystore_remove(ctx->keystores[0].keystore, *id);
+               (void)dnssec_keystore_remove(keystore->keystore, *id);
                dnssec_key_free(*key);
                free(*id);
        }
@@ -153,7 +152,14 @@ int kdnssec_generate_key(kdnssec_ctx_t *ctx, kdnssec_generate_flags_t flags,
        char *id = NULL;
        dnssec_key_t *dnskey = NULL;
 
-       int r = generate_keytag_unconflict(ctx, flags, &id, &dnskey);
+       assert(ctx->keystores != NULL && ctx->keystores[0].count > 0 && ctx->keystores[0].keystore != NULL);
+
+       knot_kasp_keystore_t *keystore = knot_store_for_key(ctx->keystores, (flags & DNSKEY_GENERATE_KSK));
+       if (keystore == NULL) {
+               return KNOT_DNSSEC_ENOKEYSTORE;
+       }
+
+       int r = generate_keytag_unconflict(ctx, keystore, flags, &id, &dnskey);
        if (r != KNOT_EOK) {
                return r;
        }
@@ -174,7 +180,7 @@ int kdnssec_generate_key(kdnssec_ctx_t *ctx, kdnssec_generate_flags_t flags,
        r = kasp_zone_append(ctx->zone, key);
        free(key);
        if (r != KNOT_EOK) {
-               (void)dnssec_keystore_remove(ctx->keystores[0].keystore, id);
+               (void)dnssec_keystore_remove(keystore->keystore, id);
                dnssec_key_free(dnskey);
                free(id);
                return r;
@@ -466,6 +472,16 @@ int kdnssec_load_private(knot_kasp_keystore_t *keystores, const char *id,
        return ret;
 }
 
+knot_kasp_keystore_t *knot_store_for_key(knot_kasp_keystore_t *keystores, bool ksk)
+{
+       for (size_t i = 0; i < keystores[0].count; i++) {
+               if (ksk || !keystores[i].ksk_only) {
+                       return &keystores[i];
+               }
+       }
+       return NULL;
+}
+
 /*!
  * \brief Load private keys for active keys.
  */
index 0474d527fd53a2cadcb6e06b160af2c094cb1a6c..cca60ed3af6061dc3acfbda1fc6e9a275873266b 100644 (file)
@@ -125,6 +125,16 @@ int kdnssec_delete_key(kdnssec_ctx_t *ctx, knot_kasp_key_t *key_ptr);
 int kdnssec_load_private(knot_kasp_keystore_t *keystores, const char *id,
                          dnssec_key_t *key, unsigned *backend);
 
+/*!
+ * \brief Find configured keystore for newly generated key.
+ *
+ * \param keystores    Array of keystores.
+ * \param ksk          If the generated key is a KSK or CSK.
+ *
+ * \return Suitable keystore or NULL.
+ */
+knot_kasp_keystore_t *knot_store_for_key(knot_kasp_keystore_t *keystores, bool ksk);
+
 /*!
  * \brief Load zone keys and init cryptographic context.
  *
index c9b07198922ecb1260da2c598546f78f4496aae8..6c789f3605eace597fb52d604f7fa75a970e1e4f 100644 (file)
@@ -164,6 +164,7 @@ enum knot_error {
        KNOT_NO_READY_KEY,
        KNOT_ESOON_EXPIRE,
        KNOT_DNSSEC_ENOKEY,
+       KNOT_DNSSEC_ENOKEYSTORE,
        KNOT_DNSSEC_EMISSINGKEYTYPE,
        KNOT_DNSSEC_ENOSIG,
        KNOT_DNSSEC_ENONSEC,
index f1eb1f00876f80d40f7eaf339337c431a3ddfcc9..21f34a246cce2b643ff2e3125b216b5a0d7ed305 100644 (file)
@@ -163,6 +163,7 @@ static const struct error errors[] = {
        { KNOT_NO_READY_KEY,           "no key ready for submission" },
        { KNOT_ESOON_EXPIRE,           "oncoming RRSIG expiration" },
        { KNOT_DNSSEC_ENOKEY,          "no keys for signing" },
+       { KNOT_DNSSEC_ENOKEYSTORE,     "no suitable keystore" },
        { KNOT_DNSSEC_EMISSINGKEYTYPE, "missing active KSK or ZSK" },
        { KNOT_DNSSEC_ENOSIG,          "no valid signature for a record" },
        { KNOT_DNSSEC_ENONSEC,         "missing NSEC(3) record" },
index 760114e67d7e9f9d2d7a13ceb45d40741183c9cb..9f2cfac7595086d68623e16bb2f397875d2833c3 100644 (file)
@@ -181,6 +181,16 @@ static bool genkeyargs(int argc, char *argv[], bool just_timing,
        return true;
 }
 
+static bool genkeyargs_ksk(int argc, char *argv[])
+{
+       for (int i = 0; i < argc; i++) {
+               if (same_command(argv[i], "ksk=", true) && str2bool(argv[i] + 4)) {
+                       return true;
+               }
+       }
+       return false;
+}
+
 static bool _check_lower(knot_time_t a, knot_time_t b,
                         const char *a_name, const char *b_name)
 {
@@ -394,6 +404,13 @@ int keymgr_import_bind(kdnssec_ctx_t *ctx, const char *import_file, bool pub_onl
        if (!pub_only) {
                bind_privkey_t bpriv = { .time_publish = ctx->now, .time_activate = ctx->now };
 
+               knot_kasp_keystore_t *keystore = knot_store_for_key(ctx->keystores,
+                                                (dnssec_key_get_flags(key) == DNSKEY_FLAGS_KSK));
+               if (keystore == NULL) {
+                       ret = KNOT_DNSSEC_ENOKEYSTORE;
+                       goto fail;
+               }
+
                char *privname = gen_keyfilename(import_file, ".private", ".key");
                if (privname == NULL) {
                        goto fail;
@@ -416,7 +433,7 @@ int keymgr_import_bind(kdnssec_ctx_t *ctx, const char *import_file, bool pub_onl
 
                bind_privkey_free(&bpriv);
 
-               ret = dnssec_keystore_import(ctx->keystores[0].keystore, &pem, &keyid);
+               ret = dnssec_keystore_import(keystore->keystore, &pem, &keyid);
                dnssec_binary_free(&pem);
                if (ret != DNSSEC_EOK) {
                        goto fail;
@@ -485,6 +502,12 @@ static int import_key(kdnssec_ctx_t *ctx, unsigned backend, const char *param,
                return KNOT_EINVAL;
        }
 
+       knot_kasp_keystore_t *keystore = knot_store_for_key(ctx->keystores,
+                                        (flags & DNSKEY_GENERATE_KSK));
+       if (keystore == NULL) {
+               return KNOT_DNSSEC_ENOKEYSTORE;
+       }
+
        int ret = check_timers(&timing);
        if (ret != KNOT_EOK) {
                return ret;
@@ -536,7 +559,7 @@ static int import_key(kdnssec_ctx_t *ctx, unsigned backend, const char *param,
                }
 
                // put pem to keystore
-               ret = dnssec_keystore_import(ctx->keystores[0].keystore, &pem, &keyid);
+               ret = dnssec_keystore_import(keystore->keystore, &pem, &keyid);
                dnssec_binary_free(&pem);
                if (ret != DNSSEC_EOK) {
                        err_import_key(keyid, param);
@@ -606,7 +629,9 @@ int keymgr_import_pkcs11(kdnssec_ctx_t *ctx, char *key_id, int argc, char *argv[
                return DNSSEC_INVALID_KEY_ID;
        }
 
-       if (ctx->keystores[0].backend != KEYSTORE_BACKEND_PKCS11) {
+       knot_kasp_keystore_t *keystore = knot_store_for_key(ctx->keystores, genkeyargs_ksk(argc, argv));
+
+       if (keystore == NULL || keystore->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);
diff --git a/tests-extra/tests/dnssec/keystores/data/8329a00d5dceefdcbbf7b8a3cdf61fe944c51d6f.pem b/tests-extra/tests/dnssec/keystores/data/8329a00d5dceefdcbbf7b8a3cdf61fe944c51d6f.pem
new file mode 100644 (file)
index 0000000..7572a1e
--- /dev/null
@@ -0,0 +1,6 @@
+-----BEGIN PRIVATE KEY-----
+MIGUAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHoweAIBAQQhANLVbiBJW8syA61m
+PXESOpWImE9PhzBJqvL2BEADJMdMoAoGCCqGSM49AwEHoUQDQgAE4b3dkmeulAJz
+HtMK9QLoNSYn0CnHhkEG22QhMBJNnmV33wdAiqEsOEYd0b6sRhfrrzVWkIqLJ7S5
+jT7y+PgAGw==
+-----END PRIVATE KEY-----
diff --git a/tests-extra/tests/dnssec/keystores/data/894d4240398f459f59f4a99cd4c5b658c9a62d54.pem b/tests-extra/tests/dnssec/keystores/data/894d4240398f459f59f4a99cd4c5b658c9a62d54.pem
new file mode 100644 (file)
index 0000000..5c197d8
--- /dev/null
@@ -0,0 +1,6 @@
+-----BEGIN PRIVATE KEY-----
+MIGUAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHoweAIBAQQhAPL+7ZuUTW7+KnAS
+AofVmmtuWVwMfMaa7ja+hEmZhrZIoAoGCCqGSM49AwEHoUQDQgAEeZIL+RsErvtI
+2M8V6ZITPaeCY0bkhbtVdkHOvVKe0hjyN9E2F7TY+x9dbwDbHVnUFxufLW0VxSUM
+tNHhp5BEkA==
+-----END PRIVATE KEY-----
diff --git a/tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+07147.key b/tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+07147.key
new file mode 100644 (file)
index 0000000..46bc589
--- /dev/null
@@ -0,0 +1,7 @@
+; This is a zone-signing key, keyid 7147, for catalog.
+; Created: 20250529114250 (Thu May 29 13:42:50 2025)
+; Publish: 20250529114250 (Thu May 29 13:42:50 2025)
+; Activate: 20250529114250 (Thu May 29 13:42:50 2025)
+; Inactive: 20450524114250 (Wed May 24 13:42:50 2045)
+; Delete: 20450524114250 (Wed May 24 13:42:50 2045)
+catalog. IN DNSKEY 256 3 13 xWrgaMrU3QPNYuXDsvMjv3paMHZ+9JwT08oP3E6hg2X7CCkNGz8YDUh/ hxRO0h3q99HhDvInTU2VokCsL9QSpg==
diff --git a/tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+07147.private b/tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+07147.private
new file mode 100644 (file)
index 0000000..d2d8c4a
--- /dev/null
@@ -0,0 +1,8 @@
+Private-key-format: v1.3
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: L9SQtoHKL+YqpsXhZu0spCmKtxopxRPU3e2RW48BcKY=
+Created: 20250529114250
+Publish: 20250529114250
+Activate: 20250529114250
+Inactive: 20450524114250
+Delete: 20450524114250
diff --git a/tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+18635.key b/tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+18635.key
new file mode 100644 (file)
index 0000000..0b72461
--- /dev/null
@@ -0,0 +1,7 @@
+; This is a key-signing key, keyid 18635, for catalog.
+; Created: 20250529114244 (Thu May 29 13:42:44 2025)
+; Publish: 20250529114244 (Thu May 29 13:42:44 2025)
+; Activate: 20250529114244 (Thu May 29 13:42:44 2025)
+; Inactive: 20450524114244 (Wed May 24 13:42:44 2045)
+; Delete: 20450524114244 (Wed May 24 13:42:44 2045)
+catalog. IN DNSKEY 257 3 13 4Eaujq8zZyDhYj9P+8b2o7I02hjwcNGrBGqzHFOnPJO6W4ex5biUKuEr M1m7RTw6ZBiBgYqXMvDSplrBeuLi6A==
diff --git a/tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+18635.private b/tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+18635.private
new file mode 100644 (file)
index 0000000..a9a8284
--- /dev/null
@@ -0,0 +1,8 @@
+Private-key-format: v1.3
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: PZuKAv38zUt4g13PAtTK7yWSdAv8FJA3QBGt6AlKdi8=
+Created: 20250529114244
+Publish: 20250529114244
+Activate: 20250529114244
+Inactive: 20450524114244
+Delete: 20450524114244
index 664a399d9596f30a32d875569bd8298bcaa8ac18..4dea41dda058ed951d335d0b3bb578e1c131eddf 100644 (file)
@@ -61,4 +61,52 @@ serial = server.zone_wait(zone, serial)
 server.flush(zone[0], wait=True)
 server.zone_verify(zone[0])
 
+server.dnssec(zone).keystores = [ "keys0ksk", "keys1", "keys2" ]
+server.gen_confile()
+server.reload()
+
+server.ctl("zone-key-rollover %s ksk" % zone[0].name)
+serial = server.zone_wait(zone, serial)
+check_key_count(server, "keys0ksk", 1)
+check_key_count(server, "keys1", 0)
+check_key_count(server, "keys2", 2)
+
+server.ctl("zone-ksk-submitted %s" % zone[0].name)
+serial = server.zone_wait(zone, serial)
+check_key_count(server, "keys0ksk", 1)
+check_key_count(server, "keys1", 0)
+check_key_count(server, "keys2", 1)
+
+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, "keys0ksk", 1)
+check_key_count(server, "keys1", 1)
+check_key_count(server, "keys2", 0)
+
+Keymgr.run_check(server.confile, zone[0].name, "generate", "ksk=yes")
+check_key_count(server, "keys0ksk", 2)
+check_key_count(server, "keys1", 1)
+
+Keymgr.run_check(server.confile, zone[0].name, "generate", "ksk=no")
+check_key_count(server, "keys0ksk", 2)
+check_key_count(server, "keys1", 2)
+
+Keymgr.run_check(server.confile, zone[0].name, "import-bind", os.path.join(t.data_dir, "Kcatalog.+013+07147.key"))
+check_key_count(server, "keys0ksk", 2)
+check_key_count(server, "keys1", 3)
+
+Keymgr.run_check(server.confile, zone[0].name, "import-bind", os.path.join(t.data_dir, "Kcatalog.+013+18635.key"))
+check_key_count(server, "keys0ksk", 3)
+check_key_count(server, "keys1", 3)
+
+Keymgr.run_check(server.confile, zone[0].name, "import-pem", os.path.join(t.data_dir, "8329a00d5dceefdcbbf7b8a3cdf61fe944c51d6f.pem"), "ksk=yes")
+check_key_count(server, "keys0ksk", 4)
+check_key_count(server, "keys1", 3)
+
+Keymgr.run_check(server.confile, zone[0].name, "import-pem", os.path.join(t.data_dir, "894d4240398f459f59f4a99cd4c5b658c9a62d54.pem"), "ksk=no")
+check_key_count(server, "keys0ksk", 4)
+check_key_count(server, "keys1", 4)
+check_key_count(server, "keys2", 0)
+
 t.end()
index 1e1f4710ab699e40b28f712f6d7f6d2e6d19a989..9fb5aedde1a86978688c4c0b031fd672351e9053 100644 (file)
@@ -1798,6 +1798,8 @@ class Knot(Server):
             for ks in z.dnssec.keystores:
                 s.id_item("id", ks)
                 s.item("config", ks)
+                if ks.endswith("ksk"):
+                    s.item("ksk-only", "on")
         if have_keystore:
             s.end()