From: Libor Peltan Date: Wed, 28 May 2025 12:57:19 +0000 (+0200) Subject: dnssec/multi-keystore: implemented ksk-only keystore... X-Git-Tag: v3.5.0~53^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=34cb1aef7c5e3401f8af0add13b7b560f4e5022f;p=thirdparty%2Fknot-dns.git dnssec/multi-keystore: implemented ksk-only keystore... ...so that KSKs and ZSKs can be in distinct keystores --- diff --git a/doc/operation.rst b/doc/operation.rst index 5d5c6d50e4..8a2b6d8fd2 100644 --- a/doc/operation.rst +++ b/doc/operation.rst @@ -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 diff --git a/doc/reference.rst b/doc/reference.rst index d660293d63..19f0d47091 100644 --- a/doc/reference.rst +++ b/doc/reference.rst @@ -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`\ ``/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` 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 diff --git a/src/knot/conf/schema.c b/src/knot/conf/schema.c index 1db67b716f..0c4e8e954d 100644 --- a/src/knot/conf/schema.c +++ b/src/knot/conf/schema.c @@ -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 } diff --git a/src/knot/conf/schema.h b/src/knot/conf/schema.h index 87984df91a..9ba63f542d 100644 --- a/src/knot/conf/schema.h +++ b/src/knot/conf/schema.h @@ -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" diff --git a/src/knot/dnssec/kasp/kasp_zone.c b/src/knot/dnssec/kasp/kasp_zone.c index 808c2c663a..390bc3c3f2 100644 --- a/src/knot/dnssec/kasp/kasp_zone.c +++ b/src/knot/dnssec/kasp/kasp_zone.c @@ -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; diff --git a/src/knot/dnssec/kasp/policy.h b/src/knot/dnssec/kasp/policy.h index 7f9db32609..f7bc932306 100644 --- a/src/knot/dnssec/kasp/policy.h +++ b/src/knot/dnssec/kasp/policy.h @@ -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; diff --git a/src/knot/dnssec/zone-keys.c b/src/knot/dnssec/zone-keys.c index 2a4b305e31..779386c033 100644 --- a/src/knot/dnssec/zone-keys.c +++ b/src/knot/dnssec/zone-keys.c @@ -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. */ diff --git a/src/knot/dnssec/zone-keys.h b/src/knot/dnssec/zone-keys.h index 0474d527fd..cca60ed3af 100644 --- a/src/knot/dnssec/zone-keys.h +++ b/src/knot/dnssec/zone-keys.h @@ -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. * diff --git a/src/libknot/errcode.h b/src/libknot/errcode.h index c9b0719892..6c789f3605 100644 --- a/src/libknot/errcode.h +++ b/src/libknot/errcode.h @@ -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, diff --git a/src/libknot/error.c b/src/libknot/error.c index f1eb1f0087..21f34a246c 100644 --- a/src/libknot/error.c +++ b/src/libknot/error.c @@ -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" }, diff --git a/src/utils/keymgr/functions.c b/src/utils/keymgr/functions.c index 760114e67d..9f2cfac759 100644 --- a/src/utils/keymgr/functions.c +++ b/src/utils/keymgr/functions.c @@ -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 index 0000000000..7572a1e61f --- /dev/null +++ b/tests-extra/tests/dnssec/keystores/data/8329a00d5dceefdcbbf7b8a3cdf61fe944c51d6f.pem @@ -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 index 0000000000..5c197d8336 --- /dev/null +++ b/tests-extra/tests/dnssec/keystores/data/894d4240398f459f59f4a99cd4c5b658c9a62d54.pem @@ -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 index 0000000000..46bc5890ce --- /dev/null +++ b/tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+07147.key @@ -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 index 0000000000..d2d8c4aa30 --- /dev/null +++ b/tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+07147.private @@ -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 index 0000000000..0b724614cf --- /dev/null +++ b/tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+18635.key @@ -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 index 0000000000..a9a8284737 --- /dev/null +++ b/tests-extra/tests/dnssec/keystores/data/Kcatalog.+013+18635.private @@ -0,0 +1,8 @@ +Private-key-format: v1.3 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: PZuKAv38zUt4g13PAtTK7yWSdAv8FJA3QBGt6AlKdi8= +Created: 20250529114244 +Publish: 20250529114244 +Activate: 20250529114244 +Inactive: 20450524114244 +Delete: 20450524114244 diff --git a/tests-extra/tests/dnssec/keystores/test.py b/tests-extra/tests/dnssec/keystores/test.py index 664a399d95..4dea41dda0 100644 --- a/tests-extra/tests/dnssec/keystores/test.py +++ b/tests-extra/tests/dnssec/keystores/test.py @@ -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() diff --git a/tests-extra/tools/dnstest/server.py b/tests-extra/tools/dnstest/server.py index 1e1f4710ab..9fb5aedde1 100644 --- a/tests-extra/tools/dnstest/server.py +++ b/tests-extra/tools/dnstest/server.py @@ -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()