summaryrefslogtreecommitdiffstats
path: root/lib/dns/keymgr.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 07:24:22 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 07:24:22 +0000
commit45d6379135504814ab723b57f0eb8be23393a51d (patch)
treed4f2ec4acca824a8446387a758b0ce4238a4dffa /lib/dns/keymgr.c
parentInitial commit. (diff)
downloadbind9-45d6379135504814ab723b57f0eb8be23393a51d.tar.xz
bind9-45d6379135504814ab723b57f0eb8be23393a51d.zip
Adding upstream version 1:9.16.44.upstream/1%9.16.44
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib/dns/keymgr.c')
-rw-r--r--lib/dns/keymgr.c2628
1 files changed, 2628 insertions, 0 deletions
diff --git a/lib/dns/keymgr.c b/lib/dns/keymgr.c
new file mode 100644
index 0000000..fe0af0e
--- /dev/null
+++ b/lib/dns/keymgr.c
@@ -0,0 +1,2628 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+/*! \file */
+
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <isc/buffer.h>
+#include <isc/dir.h>
+#include <isc/mem.h>
+#include <isc/print.h>
+#include <isc/string.h>
+#include <isc/util.h>
+
+#include <dns/dnssec.h>
+#include <dns/kasp.h>
+#include <dns/keymgr.h>
+#include <dns/keyvalues.h>
+#include <dns/log.h>
+#include <dns/result.h>
+
+#include <dst/dst.h>
+#include <dst/result.h>
+
+#define RETERR(x) \
+ do { \
+ result = (x); \
+ if (result != ISC_R_SUCCESS) \
+ goto failure; \
+ } while (0)
+
+/*
+ * Set key state to `target` state and change last changed
+ * to `time`, only if key state has not been set before.
+ */
+#define INITIALIZE_STATE(key, state, timing, target, time) \
+ do { \
+ dst_key_state_t s; \
+ if (dst_key_getstate((key), (state), &s) == ISC_R_NOTFOUND) { \
+ dst_key_setstate((key), (state), (target)); \
+ dst_key_settime((key), (timing), time); \
+ } \
+ } while (0)
+
+/* Shorter keywords for better readability. */
+#define HIDDEN DST_KEY_STATE_HIDDEN
+#define RUMOURED DST_KEY_STATE_RUMOURED
+#define OMNIPRESENT DST_KEY_STATE_OMNIPRESENT
+#define UNRETENTIVE DST_KEY_STATE_UNRETENTIVE
+#define NA DST_KEY_STATE_NA
+
+/* Quickly get key state timing metadata. */
+#define NUM_KEYSTATES (DST_MAX_KEYSTATES)
+static int keystatetimes[NUM_KEYSTATES] = { DST_TIME_DNSKEY, DST_TIME_ZRRSIG,
+ DST_TIME_KRRSIG, DST_TIME_DS };
+/* Readable key state types and values. */
+static const char *keystatetags[NUM_KEYSTATES] = { "DNSKEY", "ZRRSIG", "KRRSIG",
+ "DS" };
+static const char *keystatestrings[4] = { "HIDDEN", "RUMOURED", "OMNIPRESENT",
+ "UNRETENTIVE" };
+
+/*
+ * Print key role.
+ *
+ */
+static const char *
+keymgr_keyrole(dst_key_t *key) {
+ bool ksk = false, zsk = false;
+ isc_result_t ret;
+ ret = dst_key_getbool(key, DST_BOOL_KSK, &ksk);
+ if (ret != ISC_R_SUCCESS) {
+ return ("UNKNOWN");
+ }
+ ret = dst_key_getbool(key, DST_BOOL_ZSK, &zsk);
+ if (ret != ISC_R_SUCCESS) {
+ return ("UNKNOWN");
+ }
+ if (ksk && zsk) {
+ return ("CSK");
+ } else if (ksk) {
+ return ("KSK");
+ } else if (zsk) {
+ return ("ZSK");
+ }
+ return ("NOSIGN");
+}
+
+/*
+ * Set the remove time on key given its retire time.
+ *
+ */
+static void
+keymgr_settime_remove(dns_dnsseckey_t *key, dns_kasp_t *kasp) {
+ isc_stdtime_t retire = 0, remove = 0, ksk_remove = 0, zsk_remove = 0;
+ bool zsk = false, ksk = false;
+ isc_result_t ret;
+
+ REQUIRE(key != NULL);
+ REQUIRE(key->key != NULL);
+
+ ret = dst_key_gettime(key->key, DST_TIME_INACTIVE, &retire);
+ if (ret != ISC_R_SUCCESS) {
+ return;
+ }
+
+ ret = dst_key_getbool(key->key, DST_BOOL_ZSK, &zsk);
+ if (ret == ISC_R_SUCCESS && zsk) {
+ /* ZSK: Iret = Dsgn + Dprp + TTLsig */
+ zsk_remove = retire + dns_kasp_zonemaxttl(kasp) +
+ dns_kasp_zonepropagationdelay(kasp) +
+ dns_kasp_retiresafety(kasp) +
+ dns_kasp_signdelay(kasp);
+ }
+ ret = dst_key_getbool(key->key, DST_BOOL_KSK, &ksk);
+ if (ret == ISC_R_SUCCESS && ksk) {
+ /* KSK: Iret = DprpP + TTLds */
+ ksk_remove = retire + dns_kasp_dsttl(kasp) +
+ dns_kasp_parentpropagationdelay(kasp) +
+ dns_kasp_retiresafety(kasp);
+ }
+
+ remove = ksk_remove > zsk_remove ? ksk_remove : zsk_remove;
+ dst_key_settime(key->key, DST_TIME_DELETE, remove);
+}
+
+/*
+ * Set the SyncPublish time (when the DS may be submitted to the parent)
+ *
+ */
+static void
+keymgr_settime_syncpublish(dns_dnsseckey_t *key, dns_kasp_t *kasp, bool first) {
+ isc_stdtime_t published, syncpublish;
+ bool ksk = false;
+ isc_result_t ret;
+
+ REQUIRE(key != NULL);
+ REQUIRE(key->key != NULL);
+
+ ret = dst_key_gettime(key->key, DST_TIME_PUBLISH, &published);
+ if (ret != ISC_R_SUCCESS) {
+ return;
+ }
+
+ ret = dst_key_getbool(key->key, DST_BOOL_KSK, &ksk);
+ if (ret != ISC_R_SUCCESS || !ksk) {
+ return;
+ }
+
+ syncpublish = published + dst_key_getttl(key->key) +
+ dns_kasp_zonepropagationdelay(kasp) +
+ dns_kasp_publishsafety(kasp);
+ if (first) {
+ /* Also need to wait until the signatures are omnipresent. */
+ isc_stdtime_t zrrsig_present;
+ zrrsig_present = published + dns_kasp_zonemaxttl(kasp) +
+ dns_kasp_zonepropagationdelay(kasp) +
+ dns_kasp_publishsafety(kasp);
+ if (zrrsig_present > syncpublish) {
+ syncpublish = zrrsig_present;
+ }
+ }
+ dst_key_settime(key->key, DST_TIME_SYNCPUBLISH, syncpublish);
+}
+
+/*
+ * Calculate prepublication time of a successor key of 'key'.
+ * This function can have side effects:
+ * 1. If there is no active time set, which would be super weird, set it now.
+ * 2. If there is no published time set, also super weird, set it now.
+ * 3. If there is no syncpublished time set, set it now.
+ * 4. If the lifetime is not set, it will be set now.
+ * 5. If there should be a retire time and it is not set, it will be set now.
+ * 6. The removed time is adjusted accordingly.
+ *
+ * This returns when the successor key needs to be published in the zone.
+ * A special value of 0 means there is no need for a successor.
+ *
+ */
+static isc_stdtime_t
+keymgr_prepublication_time(dns_dnsseckey_t *key, dns_kasp_t *kasp,
+ uint32_t lifetime, isc_stdtime_t now) {
+ isc_result_t ret;
+ isc_stdtime_t active, retire, pub, prepub;
+ bool zsk = false, ksk = false;
+
+ REQUIRE(key != NULL);
+ REQUIRE(key->key != NULL);
+
+ active = 0;
+ pub = 0;
+ retire = 0;
+
+ /*
+ * An active key must have publish and activate timing
+ * metadata.
+ */
+ ret = dst_key_gettime(key->key, DST_TIME_ACTIVATE, &active);
+ if (ret != ISC_R_SUCCESS) {
+ /* Super weird, but if it happens, set it to now. */
+ dst_key_settime(key->key, DST_TIME_ACTIVATE, now);
+ active = now;
+ }
+ ret = dst_key_gettime(key->key, DST_TIME_PUBLISH, &pub);
+ if (ret != ISC_R_SUCCESS) {
+ /* Super weird, but if it happens, set it to now. */
+ dst_key_settime(key->key, DST_TIME_PUBLISH, now);
+ pub = now;
+ }
+
+ /*
+ * Calculate prepublication time.
+ */
+ prepub = dst_key_getttl(key->key) + dns_kasp_publishsafety(kasp) +
+ dns_kasp_zonepropagationdelay(kasp);
+ ret = dst_key_getbool(key->key, DST_BOOL_KSK, &ksk);
+ if (ret == ISC_R_SUCCESS && ksk) {
+ isc_stdtime_t syncpub;
+
+ /*
+ * Set PublishCDS if not set.
+ */
+ ret = dst_key_gettime(key->key, DST_TIME_SYNCPUBLISH, &syncpub);
+ if (ret != ISC_R_SUCCESS) {
+ uint32_t tag;
+ isc_stdtime_t syncpub1, syncpub2;
+
+ syncpub1 = pub + prepub;
+ syncpub2 = 0;
+ ret = dst_key_getnum(key->key, DST_NUM_PREDECESSOR,
+ &tag);
+ if (ret != ISC_R_SUCCESS) {
+ /*
+ * No predecessor, wait for zone to be
+ * completely signed.
+ */
+ syncpub2 = pub + dns_kasp_zonemaxttl(kasp) +
+ dns_kasp_publishsafety(kasp) +
+ dns_kasp_zonepropagationdelay(kasp);
+ }
+
+ syncpub = syncpub1 > syncpub2 ? syncpub1 : syncpub2;
+ dst_key_settime(key->key, DST_TIME_SYNCPUBLISH,
+ syncpub);
+ }
+ }
+
+ /*
+ * Not sure what to do when dst_key_getbool() fails here. Extending
+ * the prepublication time anyway is arguably the safest thing to do,
+ * so ignore the result code.
+ */
+ (void)dst_key_getbool(key->key, DST_BOOL_ZSK, &zsk);
+
+ ret = dst_key_gettime(key->key, DST_TIME_INACTIVE, &retire);
+ if (ret != ISC_R_SUCCESS) {
+ uint32_t klifetime = 0;
+
+ ret = dst_key_getnum(key->key, DST_NUM_LIFETIME, &klifetime);
+ if (ret != ISC_R_SUCCESS) {
+ dst_key_setnum(key->key, DST_NUM_LIFETIME, lifetime);
+ klifetime = lifetime;
+ }
+ if (klifetime == 0) {
+ /*
+ * No inactive time and no lifetime,
+ * so no need to start a rollover.
+ */
+ return (0);
+ }
+
+ retire = active + klifetime;
+ dst_key_settime(key->key, DST_TIME_INACTIVE, retire);
+ }
+
+ /*
+ * Update remove time.
+ */
+ keymgr_settime_remove(key, kasp);
+
+ /*
+ * Publish successor 'prepub' time before the 'retire' time of 'key'.
+ */
+ if (prepub > retire) {
+ /* We should have already prepublished the new key. */
+ return (now);
+ }
+ return (retire - prepub);
+}
+
+static void
+keymgr_key_retire(dns_dnsseckey_t *key, dns_kasp_t *kasp, isc_stdtime_t now) {
+ char keystr[DST_KEY_FORMATSIZE];
+ isc_result_t ret;
+ isc_stdtime_t retire;
+ dst_key_state_t s;
+ bool ksk = false, zsk = false;
+
+ REQUIRE(key != NULL);
+ REQUIRE(key->key != NULL);
+
+ /* This key wants to retire and hide in a corner. */
+ ret = dst_key_gettime(key->key, DST_TIME_INACTIVE, &retire);
+ if (ret != ISC_R_SUCCESS || (retire > now)) {
+ dst_key_settime(key->key, DST_TIME_INACTIVE, now);
+ }
+ dst_key_setstate(key->key, DST_KEY_GOAL, HIDDEN);
+ keymgr_settime_remove(key, kasp);
+
+ /* This key may not have key states set yet. Pretend as if they are
+ * in the OMNIPRESENT state.
+ */
+ if (dst_key_getstate(key->key, DST_KEY_DNSKEY, &s) != ISC_R_SUCCESS) {
+ dst_key_setstate(key->key, DST_KEY_DNSKEY, OMNIPRESENT);
+ dst_key_settime(key->key, DST_TIME_DNSKEY, now);
+ }
+
+ ret = dst_key_getbool(key->key, DST_BOOL_KSK, &ksk);
+ if (ret == ISC_R_SUCCESS && ksk) {
+ if (dst_key_getstate(key->key, DST_KEY_KRRSIG, &s) !=
+ ISC_R_SUCCESS)
+ {
+ dst_key_setstate(key->key, DST_KEY_KRRSIG, OMNIPRESENT);
+ dst_key_settime(key->key, DST_TIME_KRRSIG, now);
+ }
+ if (dst_key_getstate(key->key, DST_KEY_DS, &s) != ISC_R_SUCCESS)
+ {
+ dst_key_setstate(key->key, DST_KEY_DS, OMNIPRESENT);
+ dst_key_settime(key->key, DST_TIME_DS, now);
+ }
+ }
+ ret = dst_key_getbool(key->key, DST_BOOL_ZSK, &zsk);
+ if (ret == ISC_R_SUCCESS && zsk) {
+ if (dst_key_getstate(key->key, DST_KEY_ZRRSIG, &s) !=
+ ISC_R_SUCCESS)
+ {
+ dst_key_setstate(key->key, DST_KEY_ZRRSIG, OMNIPRESENT);
+ dst_key_settime(key->key, DST_TIME_ZRRSIG, now);
+ }
+ }
+
+ dst_key_format(key->key, keystr, sizeof(keystr));
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC, DNS_LOGMODULE_DNSSEC,
+ ISC_LOG_INFO, "keymgr: retire DNSKEY %s (%s)", keystr,
+ keymgr_keyrole(key->key));
+}
+
+/*
+ * Check if a dnsseckey matches kasp key configuration. A dnsseckey matches
+ * if it has the same algorithm and size, and if it has the same role as the
+ * kasp key configuration.
+ *
+ */
+static bool
+keymgr_dnsseckey_kaspkey_match(dns_dnsseckey_t *dkey, dns_kasp_key_t *kkey) {
+ dst_key_t *key;
+ isc_result_t ret;
+ bool role = false;
+
+ REQUIRE(dkey != NULL);
+ REQUIRE(kkey != NULL);
+
+ key = dkey->key;
+
+ /* Matching algorithms? */
+ if (dst_key_alg(key) != dns_kasp_key_algorithm(kkey)) {
+ return (false);
+ }
+ /* Matching length? */
+ if (dst_key_size(key) != dns_kasp_key_size(kkey)) {
+ return (false);
+ }
+ /* Matching role? */
+ ret = dst_key_getbool(key, DST_BOOL_KSK, &role);
+ if (ret != ISC_R_SUCCESS || role != dns_kasp_key_ksk(kkey)) {
+ return (false);
+ }
+ ret = dst_key_getbool(key, DST_BOOL_ZSK, &role);
+ if (ret != ISC_R_SUCCESS || role != dns_kasp_key_zsk(kkey)) {
+ return (false);
+ }
+
+ /* Found a match. */
+ return (true);
+}
+
+static bool
+keymgr_keyid_conflict(dst_key_t *newkey, dns_dnsseckeylist_t *keys) {
+ uint16_t id = dst_key_id(newkey);
+ uint32_t rid = dst_key_rid(newkey);
+ uint32_t alg = dst_key_alg(newkey);
+
+ for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*keys); dkey != NULL;
+ dkey = ISC_LIST_NEXT(dkey, link))
+ {
+ if (dst_key_alg(dkey->key) != alg) {
+ continue;
+ }
+ if (dst_key_id(dkey->key) == id ||
+ dst_key_rid(dkey->key) == id ||
+ dst_key_id(dkey->key) == rid ||
+ dst_key_rid(dkey->key) == rid)
+ {
+ return (true);
+ }
+ }
+ return (false);
+}
+
+/*
+ * Create a new key for 'origin' given the kasp key configuration 'kkey'.
+ * This will check for key id collisions with keys in 'keylist'.
+ * The created key will be stored in 'dst_key'.
+ *
+ */
+static isc_result_t
+keymgr_createkey(dns_kasp_key_t *kkey, const dns_name_t *origin,
+ dns_rdataclass_t rdclass, isc_mem_t *mctx,
+ dns_dnsseckeylist_t *keylist, dns_dnsseckeylist_t *newkeys,
+ dst_key_t **dst_key) {
+ bool conflict = false;
+ int keyflags = DNS_KEYOWNER_ZONE;
+ isc_result_t result = ISC_R_SUCCESS;
+ dst_key_t *newkey = NULL;
+
+ do {
+ uint32_t algo = dns_kasp_key_algorithm(kkey);
+ int size = dns_kasp_key_size(kkey);
+
+ if (dns_kasp_key_ksk(kkey)) {
+ keyflags |= DNS_KEYFLAG_KSK;
+ }
+ RETERR(dst_key_generate(origin, algo, size, 0, keyflags,
+ DNS_KEYPROTO_DNSSEC, rdclass, mctx,
+ &newkey, NULL));
+
+ /* Key collision? */
+ conflict = keymgr_keyid_conflict(newkey, keylist);
+ if (!conflict) {
+ conflict = keymgr_keyid_conflict(newkey, newkeys);
+ }
+ if (conflict) {
+ /* Try again. */
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_WARNING,
+ "keymgr: key collision id %d",
+ dst_key_id(newkey));
+ dst_key_free(&newkey);
+ }
+ } while (conflict);
+
+ INSIST(!conflict);
+ dst_key_setnum(newkey, DST_NUM_LIFETIME, dns_kasp_key_lifetime(kkey));
+ dst_key_setbool(newkey, DST_BOOL_KSK, dns_kasp_key_ksk(kkey));
+ dst_key_setbool(newkey, DST_BOOL_ZSK, dns_kasp_key_zsk(kkey));
+ *dst_key = newkey;
+ return (ISC_R_SUCCESS);
+
+failure:
+ return (result);
+}
+
+/*
+ * Return the desired state for this record 'type'. The desired state depends
+ * on whether the key wants to be active, or wants to retire. This implements
+ * the edges of our state machine:
+ *
+ * ----> OMNIPRESENT ----
+ * | |
+ * | \|/
+ *
+ * RUMOURED <----> UNRETENTIVE
+ *
+ * /|\ |
+ * | |
+ * ---- HIDDEN <----
+ *
+ * A key that wants to be active eventually wants to have its record types
+ * in the OMNIPRESENT state (that is, all resolvers that know about these
+ * type of records know about these records specifically).
+ *
+ * A key that wants to be retired eventually wants to have its record types
+ * in the HIDDEN state (that is, all resolvers that know about these type
+ * of records specifically don't know about these records).
+ *
+ */
+static dst_key_state_t
+keymgr_desiredstate(dns_dnsseckey_t *key, dst_key_state_t state) {
+ dst_key_state_t goal;
+
+ if (dst_key_getstate(key->key, DST_KEY_GOAL, &goal) != ISC_R_SUCCESS) {
+ /* No goal? No movement. */
+ return (state);
+ }
+
+ if (goal == HIDDEN) {
+ switch (state) {
+ case RUMOURED:
+ case OMNIPRESENT:
+ return (UNRETENTIVE);
+ case HIDDEN:
+ case UNRETENTIVE:
+ return (HIDDEN);
+ default:
+ return (state);
+ }
+ } else if (goal == OMNIPRESENT) {
+ switch (state) {
+ case RUMOURED:
+ case OMNIPRESENT:
+ return (OMNIPRESENT);
+ case HIDDEN:
+ case UNRETENTIVE:
+ return (RUMOURED);
+ default:
+ return (state);
+ }
+ }
+
+ /* Unknown goal. */
+ return (state);
+}
+
+/*
+ * Check if 'key' matches specific 'states'.
+ * A state in 'states' that is NA matches any state.
+ * A state in 'states' that is HIDDEN also matches if the state is not set.
+ * If 'next_state' is set (not NA), we are pretending as if record 'type' of
+ * 'subject' key already transitioned to the 'next state'.
+ *
+ */
+static bool
+keymgr_key_match_state(dst_key_t *key, dst_key_t *subject, int type,
+ dst_key_state_t next_state,
+ dst_key_state_t states[NUM_KEYSTATES]) {
+ REQUIRE(key != NULL);
+
+ for (int i = 0; i < NUM_KEYSTATES; i++) {
+ dst_key_state_t state;
+ if (states[i] == NA) {
+ continue;
+ }
+ if (next_state != NA && i == type &&
+ dst_key_id(key) == dst_key_id(subject))
+ {
+ /* Check next state rather than current state. */
+ state = next_state;
+ } else if (dst_key_getstate(key, i, &state) != ISC_R_SUCCESS) {
+ /* This is fine only if expected state is HIDDEN. */
+ if (states[i] != HIDDEN) {
+ return (false);
+ }
+ continue;
+ }
+ if (state != states[i]) {
+ return (false);
+ }
+ }
+ /* Match. */
+ return (true);
+}
+
+/*
+ * Key d directly depends on k if d is the direct predecessor of k.
+ */
+static bool
+keymgr_direct_dep(dst_key_t *d, dst_key_t *k) {
+ uint32_t s, p;
+
+ if (dst_key_getnum(d, DST_NUM_SUCCESSOR, &s) != ISC_R_SUCCESS) {
+ return (false);
+ }
+ if (dst_key_getnum(k, DST_NUM_PREDECESSOR, &p) != ISC_R_SUCCESS) {
+ return (false);
+ }
+ return (dst_key_id(d) == p && dst_key_id(k) == s);
+}
+
+/*
+ * Determine which key (if any) has a dependency on k.
+ */
+static bool
+keymgr_dep(dst_key_t *k, dns_dnsseckeylist_t *keyring, uint32_t *dep) {
+ for (dns_dnsseckey_t *d = ISC_LIST_HEAD(*keyring); d != NULL;
+ d = ISC_LIST_NEXT(d, link))
+ {
+ /*
+ * Check if k is a direct successor of d, e.g. d depends on k.
+ */
+ if (keymgr_direct_dep(d->key, k)) {
+ if (dep != NULL) {
+ *dep = dst_key_id(d->key);
+ }
+ return (true);
+ }
+ }
+ return (false);
+}
+
+/*
+ * Check if a 'z' is a successor of 'x'.
+ * This implements Equation(2) of "Flexible and Robust Key Rollover".
+ */
+static bool
+keymgr_key_is_successor(dst_key_t *x, dst_key_t *z, dst_key_t *key, int type,
+ dst_key_state_t next_state,
+ dns_dnsseckeylist_t *keyring) {
+ uint32_t dep_x;
+ uint32_t dep_z;
+
+ /*
+ * The successor relation requires that the predecessor key must not
+ * have any other keys relying on it. In other words, there must be
+ * nothing depending on x.
+ */
+ if (keymgr_dep(x, keyring, &dep_x)) {
+ return (false);
+ }
+
+ /*
+ * If there is no keys relying on key z, then z is not a successor.
+ */
+ if (!keymgr_dep(z, keyring, &dep_z)) {
+ return (false);
+ }
+
+ /*
+ * x depends on z, thus key z is a direct successor of key x.
+ */
+ if (dst_key_id(x) == dep_z) {
+ return (true);
+ }
+
+ /*
+ * It is possible to roll keys faster than the time required to finish
+ * the rollover procedure. For example, consider the keys x, y, z.
+ * Key x is currently published and is going to be replaced by y. The
+ * DNSKEY for x is removed from the zone and at the same moment the
+ * DNSKEY for y is introduced. Key y is a direct dependency for key x
+ * and is therefore the successor of x. However, before the new DNSKEY
+ * has been propagated, key z will replace key y. The DNSKEY for y is
+ * removed and moves into the same state as key x. Key y now directly
+ * depends on key z, and key z will be a new successor key for x.
+ */
+ dst_key_state_t zst[NUM_KEYSTATES] = { NA, NA, NA, NA };
+ for (int i = 0; i < NUM_KEYSTATES; i++) {
+ dst_key_state_t state;
+ if (dst_key_getstate(z, i, &state) != ISC_R_SUCCESS) {
+ continue;
+ }
+ zst[i] = state;
+ }
+
+ for (dns_dnsseckey_t *y = ISC_LIST_HEAD(*keyring); y != NULL;
+ y = ISC_LIST_NEXT(y, link))
+ {
+ if (dst_key_id(y->key) == dst_key_id(z)) {
+ continue;
+ }
+
+ if (dst_key_id(y->key) != dep_z) {
+ continue;
+ }
+ /*
+ * This is another key y, that depends on key z. It may be
+ * part of the successor relation if the key states match
+ * those of key z.
+ */
+
+ if (keymgr_key_match_state(y->key, key, type, next_state, zst))
+ {
+ /*
+ * If y is a successor of x, then z is also a
+ * successor of x.
+ */
+ return (keymgr_key_is_successor(x, y->key, key, type,
+ next_state, keyring));
+ }
+ }
+
+ return (false);
+}
+
+/*
+ * Check if a key exists in 'keyring' that matches 'states'.
+ *
+ * If 'match_algorithms', the key must also match the algorithm of 'key'.
+ * If 'next_state' is not NA, we are actually looking for a key as if
+ * 'key' already transitioned to the next state.
+ * If 'check_successor', we also want to make sure there is a successor
+ * relationship with the found key that matches 'states2'.
+ */
+static bool
+keymgr_key_exists_with_state(dns_dnsseckeylist_t *keyring, dns_dnsseckey_t *key,
+ int type, dst_key_state_t next_state,
+ dst_key_state_t states[NUM_KEYSTATES],
+ dst_key_state_t states2[NUM_KEYSTATES],
+ bool check_successor, bool match_algorithms) {
+ for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*keyring); dkey != NULL;
+ dkey = ISC_LIST_NEXT(dkey, link))
+ {
+ if (match_algorithms &&
+ (dst_key_alg(dkey->key) != dst_key_alg(key->key)))
+ {
+ continue;
+ }
+
+ if (!keymgr_key_match_state(dkey->key, key->key, type,
+ next_state, states))
+ {
+ continue;
+ }
+
+ /* Found a match. */
+ if (!check_successor) {
+ return (true);
+ }
+
+ /*
+ * We have to make sure that the key we are checking, also
+ * has a successor relationship with another key.
+ */
+ for (dns_dnsseckey_t *skey = ISC_LIST_HEAD(*keyring);
+ skey != NULL; skey = ISC_LIST_NEXT(skey, link))
+ {
+ if (skey == dkey) {
+ continue;
+ }
+
+ if (!keymgr_key_match_state(skey->key, key->key, type,
+ next_state, states2))
+ {
+ continue;
+ }
+
+ /*
+ * Found a possible successor, check.
+ */
+ if (keymgr_key_is_successor(dkey->key, skey->key,
+ key->key, type, next_state,
+ keyring))
+ {
+ return (true);
+ }
+ }
+ }
+ /* No match. */
+ return (false);
+}
+
+/*
+ * Check if a key has a successor.
+ */
+static bool
+keymgr_key_has_successor(dns_dnsseckey_t *predecessor,
+ dns_dnsseckeylist_t *keyring) {
+ for (dns_dnsseckey_t *successor = ISC_LIST_HEAD(*keyring);
+ successor != NULL; successor = ISC_LIST_NEXT(successor, link))
+ {
+ if (keymgr_direct_dep(predecessor->key, successor->key)) {
+ return (true);
+ }
+ }
+ return (false);
+}
+
+/*
+ * Check if all keys have their DS hidden. If not, then there must be at
+ * least one key with an OMNIPRESENT DNSKEY.
+ *
+ * If 'next_state' is not NA, we are actually looking for a key as if
+ * 'key' already transitioned to the next state.
+ * If 'match_algorithms', only consider keys with same algorithm of 'key'.
+ *
+ */
+static bool
+keymgr_ds_hidden_or_chained(dns_dnsseckeylist_t *keyring, dns_dnsseckey_t *key,
+ int type, dst_key_state_t next_state,
+ bool match_algorithms, bool must_be_hidden) {
+ /* (3e) */
+ dst_key_state_t dnskey_chained[NUM_KEYSTATES] = { OMNIPRESENT, NA,
+ OMNIPRESENT, NA };
+ dst_key_state_t ds_hidden[NUM_KEYSTATES] = { NA, NA, NA, HIDDEN };
+ /* successor n/a */
+ dst_key_state_t na[NUM_KEYSTATES] = { NA, NA, NA, NA };
+
+ for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*keyring); dkey != NULL;
+ dkey = ISC_LIST_NEXT(dkey, link))
+ {
+ if (match_algorithms &&
+ (dst_key_alg(dkey->key) != dst_key_alg(key->key)))
+ {
+ continue;
+ }
+
+ if (keymgr_key_match_state(dkey->key, key->key, type,
+ next_state, ds_hidden))
+ {
+ /* This key has its DS hidden. */
+ continue;
+ }
+
+ if (must_be_hidden) {
+ return (false);
+ }
+
+ /*
+ * This key does not have its DS hidden. There must be at
+ * least one key with the same algorithm that provides a
+ * chain of trust (can be this key).
+ */
+ if (keymgr_key_match_state(dkey->key, key->key, type,
+ next_state, dnskey_chained))
+ {
+ /* This DNSKEY and KRRSIG are OMNIPRESENT. */
+ continue;
+ }
+
+ /*
+ * Perhaps another key provides a chain of trust.
+ */
+ dnskey_chained[DST_KEY_DS] = OMNIPRESENT;
+ if (!keymgr_key_exists_with_state(keyring, key, type,
+ next_state, dnskey_chained,
+ na, false, match_algorithms))
+ {
+ /* There is no chain of trust. */
+ return (false);
+ }
+ }
+ /* All good. */
+ return (true);
+}
+
+/*
+ * Check if all keys have their DNSKEY hidden. If not, then there must be at
+ * least one key with an OMNIPRESENT ZRRSIG.
+ *
+ * If 'next_state' is not NA, we are actually looking for a key as if
+ * 'key' already transitioned to the next state.
+ * If 'match_algorithms', only consider keys with same algorithm of 'key'.
+ *
+ */
+static bool
+keymgr_dnskey_hidden_or_chained(dns_dnsseckeylist_t *keyring,
+ dns_dnsseckey_t *key, int type,
+ dst_key_state_t next_state,
+ bool match_algorithms) {
+ /* (3i) */
+ dst_key_state_t rrsig_chained[NUM_KEYSTATES] = { OMNIPRESENT,
+ OMNIPRESENT, NA, NA };
+ dst_key_state_t dnskey_hidden[NUM_KEYSTATES] = { HIDDEN, NA, NA, NA };
+ /* successor n/a */
+ dst_key_state_t na[NUM_KEYSTATES] = { NA, NA, NA, NA };
+
+ for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*keyring); dkey != NULL;
+ dkey = ISC_LIST_NEXT(dkey, link))
+ {
+ if (match_algorithms &&
+ (dst_key_alg(dkey->key) != dst_key_alg(key->key)))
+ {
+ continue;
+ }
+
+ if (keymgr_key_match_state(dkey->key, key->key, type,
+ next_state, dnskey_hidden))
+ {
+ /* This key has its DNSKEY hidden. */
+ continue;
+ }
+
+ /*
+ * This key does not have its DNSKEY hidden. There must be at
+ * least one key with the same algorithm that has its RRSIG
+ * records OMNIPRESENT.
+ */
+ (void)dst_key_getstate(dkey->key, DST_KEY_DNSKEY,
+ &rrsig_chained[DST_KEY_DNSKEY]);
+ if (!keymgr_key_exists_with_state(keyring, key, type,
+ next_state, rrsig_chained, na,
+ false, match_algorithms))
+ {
+ /* There is no chain of trust. */
+ return (false);
+ }
+ }
+ /* All good. */
+ return (true);
+}
+
+/*
+ * Check for existence of DS.
+ *
+ */
+static bool
+keymgr_have_ds(dns_dnsseckeylist_t *keyring, dns_dnsseckey_t *key, int type,
+ dst_key_state_t next_state, bool secure_to_insecure) {
+ /* (3a) */
+ dst_key_state_t states[2][NUM_KEYSTATES] = {
+ /* DNSKEY, ZRRSIG, KRRSIG, DS */
+ { NA, NA, NA, OMNIPRESENT }, /* DS present */
+ { NA, NA, NA, RUMOURED } /* DS introducing */
+ };
+ /* successor n/a */
+ dst_key_state_t na[NUM_KEYSTATES] = { NA, NA, NA, NA };
+
+ /*
+ * Equation (3a):
+ * There is a key with the DS in either RUMOURD or OMNIPRESENT state.
+ */
+ return (keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[0], na, false, false) ||
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[1], na, false, false) ||
+ (secure_to_insecure &&
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ na, na, false, false)));
+}
+
+/*
+ * Check for existence of DNSKEY, or at least a good DNSKEY state.
+ * See equations what are good DNSKEY states.
+ *
+ */
+static bool
+keymgr_have_dnskey(dns_dnsseckeylist_t *keyring, dns_dnsseckey_t *key, int type,
+ dst_key_state_t next_state) {
+ dst_key_state_t states[9][NUM_KEYSTATES] = {
+ /* DNSKEY, ZRRSIG, KRRSIG, DS */
+ { OMNIPRESENT, NA, OMNIPRESENT, OMNIPRESENT }, /* (3b) */
+
+ { OMNIPRESENT, NA, OMNIPRESENT, UNRETENTIVE }, /* (3c)p */
+ { OMNIPRESENT, NA, OMNIPRESENT, RUMOURED }, /* (3c)s */
+
+ { UNRETENTIVE, NA, UNRETENTIVE, OMNIPRESENT }, /* (3d)p */
+ { OMNIPRESENT, NA, UNRETENTIVE, OMNIPRESENT }, /* (3d)p */
+ { UNRETENTIVE, NA, OMNIPRESENT, OMNIPRESENT }, /* (3d)p */
+ { RUMOURED, NA, RUMOURED, OMNIPRESENT }, /* (3d)s */
+ { OMNIPRESENT, NA, RUMOURED, OMNIPRESENT }, /* (3d)s */
+ { RUMOURED, NA, OMNIPRESENT, OMNIPRESENT }, /* (3d)s */
+ };
+ /* successor n/a */
+ dst_key_state_t na[NUM_KEYSTATES] = { NA, NA, NA, NA };
+
+ return (
+ /*
+ * Equation (3b):
+ * There is a key with the same algorithm with its DNSKEY,
+ * KRRSIG and DS records in OMNIPRESENT state.
+ */
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[0], na, false, true) ||
+ /*
+ * Equation (3c):
+ * There are two or more keys with an OMNIPRESENT DNSKEY and
+ * the DS records get swapped. These keys must be in a
+ * successor relation.
+ */
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[1], states[2], true,
+ true) ||
+ /*
+ * Equation (3d):
+ * There are two or more keys with an OMNIPRESENT DS and
+ * the DNSKEY records and its KRRSIG records get swapped.
+ * These keys must be in a successor relation. Since the
+ * state for DNSKEY and KRRSIG move independently, we have
+ * to check all combinations for DNSKEY and KRRSIG in
+ * OMNIPRESENT/UNRETENTIVE state for the predecessor, and
+ * OMNIPRESENT/RUMOURED state for the successor.
+ */
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[3], states[6], true,
+ true) ||
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[3], states[7], true,
+ true) ||
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[3], states[8], true,
+ true) ||
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[4], states[6], true,
+ true) ||
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[4], states[7], true,
+ true) ||
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[4], states[8], true,
+ true) ||
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[5], states[6], true,
+ true) ||
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[5], states[7], true,
+ true) ||
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[5], states[8], true,
+ true) ||
+ /*
+ * Equation (3e):
+ * The key may be in any state as long as all keys have their
+ * DS HIDDEN, or when their DS is not HIDDEN, there must be a
+ * key with its DS in the same state and its DNSKEY omnipresent.
+ * In other words, if a DS record for the same algorithm is
+ * is still available to some validators, there must be a
+ * chain of trust for those validators.
+ */
+ keymgr_ds_hidden_or_chained(keyring, key, type, next_state,
+ true, false));
+}
+
+/*
+ * Check for existence of RRSIG (zsk), or a good RRSIG state.
+ * See equations what are good RRSIG states.
+ *
+ */
+static bool
+keymgr_have_rrsig(dns_dnsseckeylist_t *keyring, dns_dnsseckey_t *key, int type,
+ dst_key_state_t next_state) {
+ dst_key_state_t states[11][NUM_KEYSTATES] = {
+ /* DNSKEY, ZRRSIG, KRRSIG, DS */
+ { OMNIPRESENT, OMNIPRESENT, NA, NA }, /* (3f) */
+ { UNRETENTIVE, OMNIPRESENT, NA, NA }, /* (3g)p */
+ { RUMOURED, OMNIPRESENT, NA, NA }, /* (3g)s */
+ { OMNIPRESENT, UNRETENTIVE, NA, NA }, /* (3h)p */
+ { OMNIPRESENT, RUMOURED, NA, NA }, /* (3h)s */
+ };
+ /* successor n/a */
+ dst_key_state_t na[NUM_KEYSTATES] = { NA, NA, NA, NA };
+
+ return (
+ /*
+ * If all DS records are hidden than this rule can be ignored.
+ */
+ keymgr_ds_hidden_or_chained(keyring, key, type, next_state,
+ true, true) ||
+ /*
+ * Equation (3f):
+ * There is a key with the same algorithm with its DNSKEY and
+ * ZRRSIG records in OMNIPRESENT state.
+ */
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[0], na, false, true) ||
+ /*
+ * Equation (3g):
+ * There are two or more keys with OMNIPRESENT ZRRSIG
+ * records and the DNSKEY records get swapped. These keys
+ * must be in a successor relation.
+ */
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[1], states[2], true,
+ true) ||
+ /*
+ * Equation (3h):
+ * There are two or more keys with an OMNIPRESENT DNSKEY
+ * and the ZRRSIG records get swapped. These keys must be in
+ * a successor relation.
+ */
+ keymgr_key_exists_with_state(keyring, key, type, next_state,
+ states[3], states[4], true,
+ true) ||
+ /*
+ * Equation (3i):
+ * If no DNSKEYs are published, the state of the signatures is
+ * irrelevant. In case a DNSKEY is published however, there
+ * must be a path that can be validated from there.
+ */
+ keymgr_dnskey_hidden_or_chained(keyring, key, type, next_state,
+ true));
+}
+
+/*
+ * Check if a transition in the state machine is allowed by the policy.
+ * This means when we do rollovers, we want to follow the rules of the
+ * 1. Pre-publish rollover method (in case of a ZSK)
+ * - First introduce the DNSKEY record.
+ * - Only if the DNSKEY record is OMNIPRESENT, introduce ZRRSIG records.
+ *
+ * 2. Double-KSK rollover method (in case of a KSK)
+ * - First introduce the DNSKEY record, as well as the KRRSIG records.
+ * - Only if the DNSKEY record is OMNIPRESENT, suggest to introduce the DS.
+ */
+static bool
+keymgr_policy_approval(dns_dnsseckeylist_t *keyring, dns_dnsseckey_t *key,
+ int type, dst_key_state_t next) {
+ dst_key_state_t dnskeystate = HIDDEN;
+ dst_key_state_t ksk_present[NUM_KEYSTATES] = { OMNIPRESENT, NA,
+ OMNIPRESENT,
+ OMNIPRESENT };
+ dst_key_state_t ds_rumoured[NUM_KEYSTATES] = { OMNIPRESENT, NA,
+ OMNIPRESENT, RUMOURED };
+ dst_key_state_t ds_retired[NUM_KEYSTATES] = { OMNIPRESENT, NA,
+ OMNIPRESENT,
+ UNRETENTIVE };
+ dst_key_state_t ksk_rumoured[NUM_KEYSTATES] = { RUMOURED, NA, NA,
+ OMNIPRESENT };
+ dst_key_state_t ksk_retired[NUM_KEYSTATES] = { UNRETENTIVE, NA, NA,
+ OMNIPRESENT };
+ /* successor n/a */
+ dst_key_state_t na[NUM_KEYSTATES] = { NA, NA, NA, NA };
+
+ if (next != RUMOURED) {
+ /*
+ * Local policy only adds an extra barrier on transitions to
+ * the RUMOURED state.
+ */
+ return (true);
+ }
+
+ switch (type) {
+ case DST_KEY_DNSKEY:
+ /* No restrictions. */
+ return (true);
+ case DST_KEY_ZRRSIG:
+ /* Make sure the DNSKEY record is OMNIPRESENT. */
+ (void)dst_key_getstate(key->key, DST_KEY_DNSKEY, &dnskeystate);
+ if (dnskeystate == OMNIPRESENT) {
+ return (true);
+ }
+ /*
+ * Or are we introducing a new key for this algorithm? Because
+ * in that case allow publishing the RRSIG records before the
+ * DNSKEY.
+ */
+ return (!(keymgr_key_exists_with_state(keyring, key, type, next,
+ ksk_present, na, false,
+ true) ||
+ keymgr_key_exists_with_state(keyring, key, type, next,
+ ds_retired, ds_rumoured,
+ true, true) ||
+ keymgr_key_exists_with_state(
+ keyring, key, type, next, ksk_retired,
+ ksk_rumoured, true, true)));
+ case DST_KEY_KRRSIG:
+ /* Only introduce if the DNSKEY is also introduced. */
+ (void)dst_key_getstate(key->key, DST_KEY_DNSKEY, &dnskeystate);
+ return (dnskeystate != HIDDEN);
+ case DST_KEY_DS:
+ /* Make sure the DNSKEY record is OMNIPRESENT. */
+ (void)dst_key_getstate(key->key, DST_KEY_DNSKEY, &dnskeystate);
+ return (dnskeystate == OMNIPRESENT);
+ default:
+ return (false);
+ }
+}
+
+/*
+ * Check if a transition in the state machine is DNSSEC safe.
+ * This implements Equation(1) of "Flexible and Robust Key Rollover".
+ *
+ */
+static bool
+keymgr_transition_allowed(dns_dnsseckeylist_t *keyring, dns_dnsseckey_t *key,
+ int type, dst_key_state_t next_state,
+ bool secure_to_insecure) {
+ /* Debug logging. */
+ if (isc_log_wouldlog(dns_lctx, ISC_LOG_DEBUG(1))) {
+ bool rule1a, rule1b, rule2a, rule2b, rule3a, rule3b;
+ char keystr[DST_KEY_FORMATSIZE];
+ dst_key_format(key->key, keystr, sizeof(keystr));
+ rule1a = keymgr_have_ds(keyring, key, type, NA,
+ secure_to_insecure);
+ rule1b = keymgr_have_ds(keyring, key, type, next_state,
+ secure_to_insecure);
+ rule2a = keymgr_have_dnskey(keyring, key, type, NA);
+ rule2b = keymgr_have_dnskey(keyring, key, type, next_state);
+ rule3a = keymgr_have_rrsig(keyring, key, type, NA);
+ rule3b = keymgr_have_rrsig(keyring, key, type, next_state);
+ isc_log_write(
+ dns_lctx, DNS_LOGCATEGORY_DNSSEC, DNS_LOGMODULE_DNSSEC,
+ ISC_LOG_DEBUG(1),
+ "keymgr: dnssec evaluation of %s %s record %s: "
+ "rule1=(~%s or %s) rule2=(~%s or %s) "
+ "rule3=(~%s or %s)",
+ keymgr_keyrole(key->key), keystr, keystatetags[type],
+ rule1a ? "true" : "false", rule1b ? "true" : "false",
+ rule2a ? "true" : "false", rule2b ? "true" : "false",
+ rule3a ? "true" : "false", rule3b ? "true" : "false");
+ }
+
+ return (
+ /*
+ * Rule 1: There must be a DS at all times.
+ * First check the current situation: if the rule check fails,
+ * we allow the transition to attempt to move us out of the
+ * invalid state. If the rule check passes, also check if
+ * the next state is also still a valid situation.
+ */
+ (!keymgr_have_ds(keyring, key, type, NA, secure_to_insecure) ||
+ keymgr_have_ds(keyring, key, type, next_state,
+ secure_to_insecure)) &&
+ /*
+ * Rule 2: There must be a DNSKEY at all times. Again, first
+ * check the current situation, then assess the next state.
+ */
+ (!keymgr_have_dnskey(keyring, key, type, NA) ||
+ keymgr_have_dnskey(keyring, key, type, next_state)) &&
+ /*
+ * Rule 3: There must be RRSIG records at all times. Again,
+ * first check the current situation, then assess the next
+ * state.
+ */
+ (!keymgr_have_rrsig(keyring, key, type, NA) ||
+ keymgr_have_rrsig(keyring, key, type, next_state)));
+}
+
+/*
+ * Calculate the time when it is safe to do the next transition.
+ *
+ */
+static void
+keymgr_transition_time(dns_dnsseckey_t *key, int type,
+ dst_key_state_t next_state, dns_kasp_t *kasp,
+ isc_stdtime_t now, isc_stdtime_t *when) {
+ isc_result_t ret;
+ isc_stdtime_t lastchange, dstime, nexttime = now;
+
+ /*
+ * No need to wait if we move things into an uncertain state.
+ */
+ if (next_state == RUMOURED || next_state == UNRETENTIVE) {
+ *when = now;
+ return;
+ }
+
+ ret = dst_key_gettime(key->key, keystatetimes[type], &lastchange);
+ if (ret != ISC_R_SUCCESS) {
+ /* No last change, for safety purposes let's set it to now. */
+ dst_key_settime(key->key, keystatetimes[type], now);
+ lastchange = now;
+ }
+
+ switch (type) {
+ case DST_KEY_DNSKEY:
+ case DST_KEY_KRRSIG:
+ switch (next_state) {
+ case OMNIPRESENT:
+ /*
+ * RFC 7583: The publication interval (Ipub) is the
+ * amount of time that must elapse after the
+ * publication of a DNSKEY (plus RRSIG (KSK)) before
+ * it can be assumed that any resolvers that have the
+ * relevant RRset cached have a copy of the new
+ * information. This is the sum of the propagation
+ * delay (Dprp) and the DNSKEY TTL (TTLkey). This
+ * translates to zone-propagation-delay + dnskey-ttl.
+ * We will also add the publish-safety interval.
+ */
+ nexttime = lastchange + dst_key_getttl(key->key) +
+ dns_kasp_zonepropagationdelay(kasp) +
+ dns_kasp_publishsafety(kasp);
+ break;
+ case HIDDEN:
+ /*
+ * Same as OMNIPRESENT but without the publish-safety
+ * interval.
+ */
+ nexttime = lastchange + dst_key_getttl(key->key) +
+ dns_kasp_zonepropagationdelay(kasp);
+ break;
+ default:
+ nexttime = now;
+ break;
+ }
+ break;
+ case DST_KEY_ZRRSIG:
+ switch (next_state) {
+ case OMNIPRESENT:
+ case HIDDEN:
+ /*
+ * RFC 7583: The retire interval (Iret) is the amount
+ * of time that must elapse after a DNSKEY or
+ * associated data enters the retire state for any
+ * dependent information (RRSIG ZSK) to be purged from
+ * validating resolver caches. This is defined as:
+ *
+ * Iret = Dsgn + Dprp + TTLsig
+ *
+ * Where Dsgn is the Dsgn is the delay needed to
+ * ensure that all existing RRsets have been re-signed
+ * with the new key, Dprp is the propagation delay and
+ * TTLsig is the maximum TTL of all zone RRSIG
+ * records. This translates to:
+ *
+ * Dsgn + zone-propagation-delay + max-zone-ttl.
+ *
+ * We will also add the retire-safety interval.
+ */
+ nexttime = lastchange + dns_kasp_zonemaxttl(kasp) +
+ dns_kasp_zonepropagationdelay(kasp) +
+ dns_kasp_retiresafety(kasp);
+ /*
+ * Only add the sign delay Dsgn if there is an actual
+ * predecessor or successor key.
+ */
+ uint32_t tag;
+ ret = dst_key_getnum(key->key, DST_NUM_PREDECESSOR,
+ &tag);
+ if (ret != ISC_R_SUCCESS) {
+ ret = dst_key_getnum(key->key,
+ DST_NUM_SUCCESSOR, &tag);
+ }
+ if (ret == ISC_R_SUCCESS) {
+ nexttime += dns_kasp_signdelay(kasp);
+ }
+ break;
+ default:
+ nexttime = now;
+ break;
+ }
+ break;
+ case DST_KEY_DS:
+ switch (next_state) {
+ /*
+ * RFC 7583: The successor DS record is published in
+ * the parent zone and after the registration delay
+ * (Dreg), the time taken after the DS record has been
+ * submitted to the parent zone manager for it to be
+ * placed in the zone. Key N (the predecessor) must
+ * remain in the zone until any caches that contain a
+ * copy of the DS RRset have a copy containing the new
+ * DS record. This interval is the retire interval
+ * (Iret), given by:
+ *
+ * Iret = DprpP + TTLds
+ *
+ * This translates to:
+ *
+ * parent-propagation-delay + parent-ds-ttl.
+ *
+ * We will also add the retire-safety interval.
+ */
+ case OMNIPRESENT:
+ /* Make sure DS has been seen in the parent. */
+ ret = dst_key_gettime(key->key, DST_TIME_DSPUBLISH,
+ &dstime);
+ if (ret != ISC_R_SUCCESS || dstime > now) {
+ /* Not yet, try again in an hour. */
+ nexttime = now + 3600;
+ } else {
+ nexttime =
+ dstime + dns_kasp_dsttl(kasp) +
+ dns_kasp_parentpropagationdelay(kasp) +
+ dns_kasp_retiresafety(kasp);
+ }
+ break;
+ case HIDDEN:
+ /* Make sure DS has been withdrawn from the parent. */
+ ret = dst_key_gettime(key->key, DST_TIME_DSDELETE,
+ &dstime);
+ if (ret != ISC_R_SUCCESS || dstime > now) {
+ /* Not yet, try again in an hour. */
+ nexttime = now + 3600;
+ } else {
+ nexttime =
+ dstime + dns_kasp_dsttl(kasp) +
+ dns_kasp_parentpropagationdelay(kasp) +
+ dns_kasp_retiresafety(kasp);
+ }
+ break;
+ default:
+ nexttime = now;
+ break;
+ }
+ break;
+ default:
+ UNREACHABLE();
+ break;
+ }
+
+ *when = nexttime;
+}
+
+/*
+ * Update keys.
+ * This implements Algorithm (1) of "Flexible and Robust Key Rollover".
+ *
+ */
+static isc_result_t
+keymgr_update(dns_dnsseckeylist_t *keyring, dns_kasp_t *kasp, isc_stdtime_t now,
+ isc_stdtime_t *nexttime, bool secure_to_insecure) {
+ bool changed;
+
+ /* Repeat until nothing changed. */
+transition:
+ changed = false;
+
+ /* For all keys in the zone. */
+ for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*keyring); dkey != NULL;
+ dkey = ISC_LIST_NEXT(dkey, link))
+ {
+ char keystr[DST_KEY_FORMATSIZE];
+ dst_key_format(dkey->key, keystr, sizeof(keystr));
+
+ /* For all records related to this key. */
+ for (int i = 0; i < NUM_KEYSTATES; i++) {
+ isc_result_t ret;
+ isc_stdtime_t when;
+ dst_key_state_t state, next_state;
+
+ ret = dst_key_getstate(dkey->key, i, &state);
+ if (ret == ISC_R_NOTFOUND) {
+ /*
+ * This record type is not applicable for this
+ * key, continue to the next record type.
+ */
+ continue;
+ }
+
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(1),
+ "keymgr: examine %s %s type %s "
+ "in state %s",
+ keymgr_keyrole(dkey->key), keystr,
+ keystatetags[i], keystatestrings[state]);
+
+ /* Get the desired next state. */
+ next_state = keymgr_desiredstate(dkey, state);
+ if (state == next_state) {
+ /*
+ * This record is in a stable state.
+ * No change needed, continue with the next
+ * record type.
+ */
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC,
+ ISC_LOG_DEBUG(1),
+ "keymgr: %s %s type %s in "
+ "stable state %s",
+ keymgr_keyrole(dkey->key), keystr,
+ keystatetags[i],
+ keystatestrings[state]);
+ continue;
+ }
+
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(1),
+ "keymgr: can we transition %s %s type %s "
+ "state %s to state %s?",
+ keymgr_keyrole(dkey->key), keystr,
+ keystatetags[i], keystatestrings[state],
+ keystatestrings[next_state]);
+
+ /* Is the transition allowed according to policy? */
+ if (!keymgr_policy_approval(keyring, dkey, i,
+ next_state))
+ {
+ /* No, please respect rollover methods. */
+ isc_log_write(
+ dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(1),
+ "keymgr: policy says no to %s %s type "
+ "%s "
+ "state %s to state %s",
+ keymgr_keyrole(dkey->key), keystr,
+ keystatetags[i], keystatestrings[state],
+ keystatestrings[next_state]);
+
+ continue;
+ }
+
+ /* Is the transition DNSSEC safe? */
+ if (!keymgr_transition_allowed(keyring, dkey, i,
+ next_state,
+ secure_to_insecure))
+ {
+ /* No, this would make the zone bogus. */
+ isc_log_write(
+ dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(1),
+ "keymgr: dnssec says no to %s %s type "
+ "%s "
+ "state %s to state %s",
+ keymgr_keyrole(dkey->key), keystr,
+ keystatetags[i], keystatestrings[state],
+ keystatestrings[next_state]);
+ continue;
+ }
+
+ /* Is it time to make the transition? */
+ when = now;
+ keymgr_transition_time(dkey, i, next_state, kasp, now,
+ &when);
+ if (when > now) {
+ /* Not yet. */
+ isc_log_write(
+ dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(1),
+ "keymgr: time says no to %s %s type %s "
+ "state %s to state %s (wait %u "
+ "seconds)",
+ keymgr_keyrole(dkey->key), keystr,
+ keystatetags[i], keystatestrings[state],
+ keystatestrings[next_state],
+ when - now);
+ if (*nexttime == 0 || *nexttime > when) {
+ *nexttime = when;
+ }
+ continue;
+ }
+
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(1),
+ "keymgr: transition %s %s type %s "
+ "state %s to state %s!",
+ keymgr_keyrole(dkey->key), keystr,
+ keystatetags[i], keystatestrings[state],
+ keystatestrings[next_state]);
+
+ /* It is safe to make the transition. */
+ dst_key_setstate(dkey->key, i, next_state);
+ dst_key_settime(dkey->key, keystatetimes[i], now);
+ INSIST(dst_key_ismodified(dkey->key));
+ changed = true;
+ }
+ }
+
+ /* We changed something, continue processing. */
+ if (changed) {
+ goto transition;
+ }
+
+ return (ISC_R_SUCCESS);
+}
+
+/*
+ * See if this key needs to be initialized with properties. A key created
+ * and derived from a dnssec-policy will have the required metadata available,
+ * otherwise these may be missing and need to be initialized. The key states
+ * will be initialized according to existing timing metadata.
+ *
+ */
+static void
+keymgr_key_init(dns_dnsseckey_t *key, dns_kasp_t *kasp, isc_stdtime_t now,
+ bool csk) {
+ bool ksk, zsk;
+ isc_result_t ret;
+ isc_stdtime_t active = 0, pub = 0, syncpub = 0, retire = 0, remove = 0;
+ dst_key_state_t dnskey_state = HIDDEN;
+ dst_key_state_t ds_state = HIDDEN;
+ dst_key_state_t zrrsig_state = HIDDEN;
+ dst_key_state_t goal_state = HIDDEN;
+
+ REQUIRE(key != NULL);
+ REQUIRE(key->key != NULL);
+
+ /* Initialize role. */
+ ret = dst_key_getbool(key->key, DST_BOOL_KSK, &ksk);
+ if (ret != ISC_R_SUCCESS) {
+ ksk = ((dst_key_flags(key->key) & DNS_KEYFLAG_KSK) != 0);
+ dst_key_setbool(key->key, DST_BOOL_KSK, (ksk || csk));
+ }
+ ret = dst_key_getbool(key->key, DST_BOOL_ZSK, &zsk);
+ if (ret != ISC_R_SUCCESS) {
+ zsk = ((dst_key_flags(key->key) & DNS_KEYFLAG_KSK) == 0);
+ dst_key_setbool(key->key, DST_BOOL_ZSK, (zsk || csk));
+ }
+
+ /* Get time metadata. */
+ ret = dst_key_gettime(key->key, DST_TIME_ACTIVATE, &active);
+ if (active <= now && ret == ISC_R_SUCCESS) {
+ dns_ttl_t zone_ttl = dns_kasp_zonemaxttl(kasp);
+ zone_ttl += dns_kasp_zonepropagationdelay(kasp);
+ if ((active + zone_ttl) <= now) {
+ zrrsig_state = OMNIPRESENT;
+ } else {
+ zrrsig_state = RUMOURED;
+ }
+ goal_state = OMNIPRESENT;
+ }
+ ret = dst_key_gettime(key->key, DST_TIME_PUBLISH, &pub);
+ if (pub <= now && ret == ISC_R_SUCCESS) {
+ dns_ttl_t key_ttl = dst_key_getttl(key->key);
+ key_ttl += dns_kasp_zonepropagationdelay(kasp);
+ if ((pub + key_ttl) <= now) {
+ dnskey_state = OMNIPRESENT;
+ } else {
+ dnskey_state = RUMOURED;
+ }
+ goal_state = OMNIPRESENT;
+ }
+ ret = dst_key_gettime(key->key, DST_TIME_SYNCPUBLISH, &syncpub);
+ if (syncpub <= now && ret == ISC_R_SUCCESS) {
+ dns_ttl_t ds_ttl = dns_kasp_dsttl(kasp);
+ ds_ttl += dns_kasp_parentpropagationdelay(kasp);
+ if ((syncpub + ds_ttl) <= now) {
+ ds_state = OMNIPRESENT;
+ } else {
+ ds_state = RUMOURED;
+ }
+ goal_state = OMNIPRESENT;
+ }
+ ret = dst_key_gettime(key->key, DST_TIME_INACTIVE, &retire);
+ if (retire <= now && ret == ISC_R_SUCCESS) {
+ dns_ttl_t zone_ttl = dns_kasp_zonemaxttl(kasp);
+ zone_ttl += dns_kasp_zonepropagationdelay(kasp);
+ if ((retire + zone_ttl) <= now) {
+ zrrsig_state = HIDDEN;
+ } else {
+ zrrsig_state = UNRETENTIVE;
+ }
+ ds_state = UNRETENTIVE;
+ goal_state = HIDDEN;
+ }
+ ret = dst_key_gettime(key->key, DST_TIME_DELETE, &remove);
+ if (remove <= now && ret == ISC_R_SUCCESS) {
+ dns_ttl_t key_ttl = dst_key_getttl(key->key);
+ key_ttl += dns_kasp_zonepropagationdelay(kasp);
+ if ((remove + key_ttl) <= now) {
+ dnskey_state = HIDDEN;
+ } else {
+ dnskey_state = UNRETENTIVE;
+ }
+ zrrsig_state = HIDDEN;
+ ds_state = HIDDEN;
+ goal_state = HIDDEN;
+ }
+
+ /* Set goal if not already set. */
+ if (dst_key_getstate(key->key, DST_KEY_GOAL, &goal_state) !=
+ ISC_R_SUCCESS)
+ {
+ dst_key_setstate(key->key, DST_KEY_GOAL, goal_state);
+ }
+
+ /* Set key states for all keys that do not have them. */
+ INITIALIZE_STATE(key->key, DST_KEY_DNSKEY, DST_TIME_DNSKEY,
+ dnskey_state, now);
+ if (ksk || csk) {
+ INITIALIZE_STATE(key->key, DST_KEY_KRRSIG, DST_TIME_KRRSIG,
+ dnskey_state, now);
+ INITIALIZE_STATE(key->key, DST_KEY_DS, DST_TIME_DS, ds_state,
+ now);
+ }
+ if (zsk || csk) {
+ INITIALIZE_STATE(key->key, DST_KEY_ZRRSIG, DST_TIME_ZRRSIG,
+ zrrsig_state, now);
+ }
+}
+
+static isc_result_t
+keymgr_key_rollover(dns_kasp_key_t *kaspkey, dns_dnsseckey_t *active_key,
+ dns_dnsseckeylist_t *keyring, dns_dnsseckeylist_t *newkeys,
+ const dns_name_t *origin, dns_rdataclass_t rdclass,
+ dns_kasp_t *kasp, uint32_t lifetime, bool rollover,
+ isc_stdtime_t now, isc_stdtime_t *nexttime,
+ isc_mem_t *mctx) {
+ char keystr[DST_KEY_FORMATSIZE];
+ isc_stdtime_t retire = 0, active = 0, prepub = 0;
+ dns_dnsseckey_t *new_key = NULL;
+ dns_dnsseckey_t *candidate = NULL;
+ dst_key_t *dst_key = NULL;
+
+ /* Do we need to create a successor for the active key? */
+ if (active_key != NULL) {
+ if (isc_log_wouldlog(dns_lctx, ISC_LOG_DEBUG(1))) {
+ dst_key_format(active_key->key, keystr, sizeof(keystr));
+ isc_log_write(
+ dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(1),
+ "keymgr: DNSKEY %s (%s) is active in policy %s",
+ keystr, keymgr_keyrole(active_key->key),
+ dns_kasp_getname(kasp));
+ }
+
+ /*
+ * Calculate when the successor needs to be published
+ * in the zone.
+ */
+ prepub = keymgr_prepublication_time(active_key, kasp, lifetime,
+ now);
+ if (prepub == 0 || prepub > now) {
+ if (isc_log_wouldlog(dns_lctx, ISC_LOG_DEBUG(1))) {
+ dst_key_format(active_key->key, keystr,
+ sizeof(keystr));
+ isc_log_write(
+ dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(1),
+ "keymgr: new successor needed for "
+ "DNSKEY %s (%s) (policy %s) in %u "
+ "seconds",
+ keystr, keymgr_keyrole(active_key->key),
+ dns_kasp_getname(kasp), (prepub - now));
+ }
+
+ /* No need to start rollover now. */
+ if (*nexttime == 0 || prepub < *nexttime) {
+ *nexttime = prepub;
+ }
+ return (ISC_R_SUCCESS);
+ }
+
+ if (keymgr_key_has_successor(active_key, keyring)) {
+ /* Key already has successor. */
+ if (isc_log_wouldlog(dns_lctx, ISC_LOG_DEBUG(1))) {
+ dst_key_format(active_key->key, keystr,
+ sizeof(keystr));
+ isc_log_write(
+ dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(1),
+ "keymgr: key DNSKEY %s (%s) (policy "
+ "%s) already has successor",
+ keystr, keymgr_keyrole(active_key->key),
+ dns_kasp_getname(kasp));
+ }
+ return (ISC_R_SUCCESS);
+ }
+
+ if (isc_log_wouldlog(dns_lctx, ISC_LOG_DEBUG(1))) {
+ dst_key_format(active_key->key, keystr, sizeof(keystr));
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(1),
+ "keymgr: need successor for DNSKEY %s "
+ "(%s) (policy %s)",
+ keystr, keymgr_keyrole(active_key->key),
+ dns_kasp_getname(kasp));
+ }
+
+ /*
+ * If rollover is not allowed, warn.
+ */
+ if (!rollover) {
+ dst_key_format(active_key->key, keystr, sizeof(keystr));
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_WARNING,
+ "keymgr: DNSKEY %s (%s) is offline in "
+ "policy %s, cannot start rollover",
+ keystr, keymgr_keyrole(active_key->key),
+ dns_kasp_getname(kasp));
+ return (ISC_R_SUCCESS);
+ }
+ } else if (isc_log_wouldlog(dns_lctx, ISC_LOG_DEBUG(1))) {
+ char namestr[DNS_NAME_FORMATSIZE];
+ dns_name_format(origin, namestr, sizeof(namestr));
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(1),
+ "keymgr: no active key found for %s (policy %s)",
+ namestr, dns_kasp_getname(kasp));
+ }
+
+ /* It is time to do key rollover, we need a new key. */
+
+ /*
+ * Check if there is a key available in pool because keys
+ * may have been pregenerated with dnssec-keygen.
+ */
+ for (candidate = ISC_LIST_HEAD(*keyring); candidate != NULL;
+ candidate = ISC_LIST_NEXT(candidate, link))
+ {
+ if (keymgr_dnsseckey_kaspkey_match(candidate, kaspkey) &&
+ dst_key_is_unused(candidate->key))
+ {
+ /* Found a candidate in keyring. */
+ break;
+ }
+ }
+
+ if (candidate == NULL) {
+ /* No key available in keyring, create a new one. */
+ bool csk = (dns_kasp_key_ksk(kaspkey) &&
+ dns_kasp_key_zsk(kaspkey));
+
+ isc_result_t result = keymgr_createkey(kaspkey, origin, rdclass,
+ mctx, keyring, newkeys,
+ &dst_key);
+ if (result != ISC_R_SUCCESS) {
+ return (result);
+ }
+ dst_key_setttl(dst_key, dns_kasp_dnskeyttl(kasp));
+ dst_key_settime(dst_key, DST_TIME_CREATED, now);
+ result = dns_dnsseckey_create(mctx, &dst_key, &new_key);
+ if (result != ISC_R_SUCCESS) {
+ return (result);
+ }
+ keymgr_key_init(new_key, kasp, now, csk);
+ } else {
+ new_key = candidate;
+ }
+ dst_key_setnum(new_key->key, DST_NUM_LIFETIME, lifetime);
+
+ /* Got a key. */
+ if (active_key == NULL) {
+ /*
+ * If there is no active key found yet for this kasp
+ * key configuration, immediately make this key active.
+ */
+ dst_key_settime(new_key->key, DST_TIME_PUBLISH, now);
+ dst_key_settime(new_key->key, DST_TIME_ACTIVATE, now);
+ keymgr_settime_syncpublish(new_key, kasp, true);
+ active = now;
+ } else {
+ /*
+ * This is a successor. Mark the relationship.
+ */
+ isc_stdtime_t created;
+ (void)dst_key_gettime(new_key->key, DST_TIME_CREATED, &created);
+
+ dst_key_setnum(new_key->key, DST_NUM_PREDECESSOR,
+ dst_key_id(active_key->key));
+ dst_key_setnum(active_key->key, DST_NUM_SUCCESSOR,
+ dst_key_id(new_key->key));
+ (void)dst_key_gettime(active_key->key, DST_TIME_INACTIVE,
+ &retire);
+ active = retire;
+
+ /*
+ * If prepublication time and/or retire time are
+ * in the past (before the new key was created), use
+ * creation time as published and active time,
+ * effectively immediately making the key active.
+ */
+ if (prepub < created) {
+ active += (created - prepub);
+ prepub = created;
+ }
+ if (active < created) {
+ active = created;
+ }
+ dst_key_settime(new_key->key, DST_TIME_PUBLISH, prepub);
+ dst_key_settime(new_key->key, DST_TIME_ACTIVATE, active);
+ keymgr_settime_syncpublish(new_key, kasp, false);
+
+ /*
+ * Retire predecessor.
+ */
+ dst_key_setstate(active_key->key, DST_KEY_GOAL, HIDDEN);
+ }
+
+ /* This key wants to be present. */
+ dst_key_setstate(new_key->key, DST_KEY_GOAL, OMNIPRESENT);
+
+ /* Do we need to set retire time? */
+ if (lifetime > 0) {
+ dst_key_settime(new_key->key, DST_TIME_INACTIVE,
+ (active + lifetime));
+ keymgr_settime_remove(new_key, kasp);
+ }
+
+ /* Append dnsseckey to list of new keys. */
+ dns_dnssec_get_hints(new_key, now);
+ new_key->source = dns_keysource_repository;
+ INSIST(!new_key->legacy);
+ if (candidate == NULL) {
+ ISC_LIST_APPEND(*newkeys, new_key, link);
+ }
+
+ /* Logging. */
+ dst_key_format(new_key->key, keystr, sizeof(keystr));
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC, DNS_LOGMODULE_DNSSEC,
+ ISC_LOG_INFO, "keymgr: DNSKEY %s (%s) %s for policy %s",
+ keystr, keymgr_keyrole(new_key->key),
+ (candidate != NULL) ? "selected" : "created",
+ dns_kasp_getname(kasp));
+ return (ISC_R_SUCCESS);
+}
+
+static bool
+keymgr_key_may_be_purged(dst_key_t *key, uint32_t after, isc_stdtime_t now) {
+ bool ksk = false;
+ bool zsk = false;
+ dst_key_state_t hidden[NUM_KEYSTATES] = { HIDDEN, NA, NA, NA };
+ isc_stdtime_t lastchange = 0;
+
+ char keystr[DST_KEY_FORMATSIZE];
+ dst_key_format(key, keystr, sizeof(keystr));
+
+ /* If 'purge-keys' is disabled, always retain keys. */
+ if (after == 0) {
+ return (false);
+ }
+
+ /* Don't purge keys with goal OMNIPRESENT */
+ if (dst_key_goal(key) == OMNIPRESENT) {
+ return (false);
+ }
+
+ /* Don't purge unused keys. */
+ if (dst_key_is_unused(key)) {
+ return (false);
+ }
+
+ /* If this key is completely HIDDEN it may be purged. */
+ (void)dst_key_getbool(key, DST_BOOL_KSK, &ksk);
+ (void)dst_key_getbool(key, DST_BOOL_ZSK, &zsk);
+ if (ksk) {
+ hidden[DST_KEY_KRRSIG] = HIDDEN;
+ hidden[DST_KEY_DS] = HIDDEN;
+ }
+ if (zsk) {
+ hidden[DST_KEY_ZRRSIG] = HIDDEN;
+ }
+ if (!keymgr_key_match_state(key, key, 0, NA, hidden)) {
+ return (false);
+ }
+
+ /*
+ * Check 'purge-keys' interval. If the interval has passed since
+ * the last key change, it may be purged.
+ */
+ for (int i = 0; i < NUM_KEYSTATES; i++) {
+ isc_stdtime_t change = 0;
+ (void)dst_key_gettime(key, keystatetimes[i], &change);
+ if (change > lastchange) {
+ lastchange = change;
+ }
+ }
+
+ return ((lastchange + after) < now);
+}
+
+static void
+keymgr_purge_keyfile(dst_key_t *key, const char *dir, int type) {
+ isc_result_t ret;
+ isc_buffer_t fileb;
+ char filename[NAME_MAX];
+
+ /*
+ * Make the filename.
+ */
+ isc_buffer_init(&fileb, filename, sizeof(filename));
+ ret = dst_key_buildfilename(key, type, dir, &fileb);
+ if (ret != ISC_R_SUCCESS) {
+ char keystr[DST_KEY_FORMATSIZE];
+ dst_key_format(key, keystr, sizeof(keystr));
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_WARNING,
+ "keymgr: failed to purge DNSKEY %s (%s): cannot "
+ "build filename (%s)",
+ keystr, keymgr_keyrole(key),
+ isc_result_totext(ret));
+ return;
+ }
+
+ if (unlink(filename) < 0) {
+ char keystr[DST_KEY_FORMATSIZE];
+ dst_key_format(key, keystr, sizeof(keystr));
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_WARNING,
+ "keymgr: failed to purge DNSKEY %s (%s): unlink "
+ "'%s' failed",
+ keystr, keymgr_keyrole(key), filename);
+ }
+}
+
+/*
+ * Examine 'keys' and match 'kasp' policy.
+ *
+ */
+isc_result_t
+dns_keymgr_run(const dns_name_t *origin, dns_rdataclass_t rdclass,
+ const char *directory, isc_mem_t *mctx,
+ dns_dnsseckeylist_t *keyring, dns_dnsseckeylist_t *dnskeys,
+ dns_kasp_t *kasp, isc_stdtime_t now, isc_stdtime_t *nexttime) {
+ isc_result_t result = ISC_R_SUCCESS;
+ dns_dnsseckeylist_t newkeys;
+ dns_kasp_key_t *kkey;
+ dns_dnsseckey_t *newkey = NULL;
+ isc_dir_t dir;
+ bool dir_open = false;
+ bool secure_to_insecure = false;
+ int numkeys = 0;
+ int options = (DST_TYPE_PRIVATE | DST_TYPE_PUBLIC | DST_TYPE_STATE);
+ char keystr[DST_KEY_FORMATSIZE];
+
+ REQUIRE(DNS_KASP_VALID(kasp));
+ REQUIRE(keyring != NULL);
+
+ ISC_LIST_INIT(newkeys);
+
+ isc_dir_init(&dir);
+ if (directory == NULL) {
+ directory = ".";
+ }
+
+ RETERR(isc_dir_open(&dir, directory));
+ dir_open = true;
+
+ *nexttime = 0;
+
+ /* Debug logging: what keys are available in the keyring? */
+ if (isc_log_wouldlog(dns_lctx, ISC_LOG_DEBUG(1))) {
+ if (ISC_LIST_EMPTY(*keyring)) {
+ char namebuf[DNS_NAME_FORMATSIZE];
+ dns_name_format(origin, namebuf, sizeof(namebuf));
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(1),
+ "keymgr: keyring empty (zone %s policy "
+ "%s)",
+ namebuf, dns_kasp_getname(kasp));
+ }
+
+ for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*keyring);
+ dkey != NULL; dkey = ISC_LIST_NEXT(dkey, link))
+ {
+ dst_key_format(dkey->key, keystr, sizeof(keystr));
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(1),
+ "keymgr: keyring: %s (policy %s)", keystr,
+ dns_kasp_getname(kasp));
+ }
+ for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*dnskeys);
+ dkey != NULL; dkey = ISC_LIST_NEXT(dkey, link))
+ {
+ dst_key_format(dkey->key, keystr, sizeof(keystr));
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(1),
+ "keymgr: dnskeys: %s (policy %s)", keystr,
+ dns_kasp_getname(kasp));
+ }
+ }
+
+ for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*dnskeys); dkey != NULL;
+ dkey = ISC_LIST_NEXT(dkey, link))
+ {
+ numkeys++;
+ }
+
+ /* Do we need to remove keys? */
+ for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*keyring); dkey != NULL;
+ dkey = ISC_LIST_NEXT(dkey, link))
+ {
+ bool found_match = false;
+
+ keymgr_key_init(dkey, kasp, now, (numkeys == 1));
+
+ for (kkey = ISC_LIST_HEAD(dns_kasp_keys(kasp)); kkey != NULL;
+ kkey = ISC_LIST_NEXT(kkey, link))
+ {
+ if (keymgr_dnsseckey_kaspkey_match(dkey, kkey)) {
+ found_match = true;
+ break;
+ }
+ }
+
+ /* No match, so retire unwanted retire key. */
+ if (!found_match) {
+ keymgr_key_retire(dkey, kasp, now);
+ }
+
+ /* Check purge-keys interval. */
+ if (keymgr_key_may_be_purged(dkey->key,
+ dns_kasp_purgekeys(kasp), now))
+ {
+ dst_key_format(dkey->key, keystr, sizeof(keystr));
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_INFO,
+ "keymgr: purge DNSKEY %s (%s) according "
+ "to policy %s",
+ keystr, keymgr_keyrole(dkey->key),
+ dns_kasp_getname(kasp));
+
+ keymgr_purge_keyfile(dkey->key, directory,
+ DST_TYPE_PUBLIC);
+ keymgr_purge_keyfile(dkey->key, directory,
+ DST_TYPE_PRIVATE);
+ keymgr_purge_keyfile(dkey->key, directory,
+ DST_TYPE_STATE);
+
+ dkey->purge = true;
+ }
+ }
+
+ /* Create keys according to the policy, if come in short. */
+ for (kkey = ISC_LIST_HEAD(dns_kasp_keys(kasp)); kkey != NULL;
+ kkey = ISC_LIST_NEXT(kkey, link))
+ {
+ uint32_t lifetime = dns_kasp_key_lifetime(kkey);
+ dns_dnsseckey_t *active_key = NULL;
+ bool rollover_allowed = true;
+
+ /* Do we have keys available for this kasp key? */
+ for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*keyring);
+ dkey != NULL; dkey = ISC_LIST_NEXT(dkey, link))
+ {
+ if (keymgr_dnsseckey_kaspkey_match(dkey, kkey)) {
+ /* Found a match. */
+ dst_key_format(dkey->key, keystr,
+ sizeof(keystr));
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC,
+ ISC_LOG_DEBUG(1),
+ "keymgr: DNSKEY %s (%s) matches "
+ "policy %s",
+ keystr, keymgr_keyrole(dkey->key),
+ dns_kasp_getname(kasp));
+
+ /* Initialize lifetime if not set. */
+ uint32_t l;
+ if (dst_key_getnum(dkey->key, DST_NUM_LIFETIME,
+ &l) != ISC_R_SUCCESS)
+ {
+ dst_key_setnum(dkey->key,
+ DST_NUM_LIFETIME,
+ lifetime);
+ }
+
+ if (active_key) {
+ /* We already have an active key that
+ * matches the kasp policy.
+ */
+ if (!dst_key_is_unused(dkey->key) &&
+ (dst_key_goal(dkey->key) ==
+ OMNIPRESENT) &&
+ !keymgr_dep(dkey->key, keyring,
+ NULL) &&
+ !keymgr_dep(active_key->key,
+ keyring, NULL))
+ {
+ /*
+ * Multiple signing keys match
+ * the kasp key configuration.
+ * Retire excess keys in use.
+ */
+ keymgr_key_retire(dkey, kasp,
+ now);
+ }
+ continue;
+ }
+
+ /*
+ * Save the matched key only if it is active
+ * or desires to be active.
+ */
+ if (dst_key_goal(dkey->key) == OMNIPRESENT ||
+ dst_key_is_active(dkey->key, now))
+ {
+ active_key = dkey;
+ }
+ }
+ }
+
+ if (active_key == NULL) {
+ /*
+ * We didn't found an active key, perhaps the .private
+ * key file is offline. If so, we don't want to create
+ * a successor key. Check if we have an appropriate
+ * state file.
+ */
+ for (dns_dnsseckey_t *dnskey = ISC_LIST_HEAD(*dnskeys);
+ dnskey != NULL;
+ dnskey = ISC_LIST_NEXT(dnskey, link))
+ {
+ if (keymgr_dnsseckey_kaspkey_match(dnskey,
+ kkey))
+ {
+ /* Found a match. */
+ dst_key_format(dnskey->key, keystr,
+ sizeof(keystr));
+ isc_log_write(
+ dns_lctx,
+ DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC,
+ ISC_LOG_DEBUG(1),
+ "keymgr: DNSKEY %s (%s) "
+ "offline, policy %s",
+ keystr,
+ keymgr_keyrole(dnskey->key),
+ dns_kasp_getname(kasp));
+ rollover_allowed = false;
+ active_key = dnskey;
+ break;
+ }
+ }
+ }
+
+ /* See if this key requires a rollover. */
+ RETERR(keymgr_key_rollover(
+ kkey, active_key, keyring, &newkeys, origin, rdclass,
+ kasp, lifetime, rollover_allowed, now, nexttime, mctx));
+ }
+
+ /* Walked all kasp key configurations. Append new keys. */
+ if (!ISC_LIST_EMPTY(newkeys)) {
+ ISC_LIST_APPENDLIST(*keyring, newkeys, link);
+ }
+
+ /*
+ * If the policy has an empty key list, this means the zone is going
+ * back to unsigned.
+ */
+ secure_to_insecure = dns_kasp_keylist_empty(kasp);
+
+ /* Read to update key states. */
+ keymgr_update(keyring, kasp, now, nexttime, secure_to_insecure);
+
+ /* Store key states and update hints. */
+ for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*keyring); dkey != NULL;
+ dkey = ISC_LIST_NEXT(dkey, link))
+ {
+ if (dst_key_ismodified(dkey->key) && !dkey->purge) {
+ dns_dnssec_get_hints(dkey, now);
+ RETERR(dst_key_tofile(dkey->key, options, directory));
+ dst_key_setmodified(dkey->key, false);
+ }
+ }
+
+ result = ISC_R_SUCCESS;
+
+failure:
+ if (dir_open) {
+ isc_dir_close(&dir);
+ }
+
+ if (result != ISC_R_SUCCESS) {
+ while ((newkey = ISC_LIST_HEAD(newkeys)) != NULL) {
+ ISC_LIST_UNLINK(newkeys, newkey, link);
+ INSIST(newkey->key != NULL);
+ dst_key_free(&newkey->key);
+ dns_dnsseckey_destroy(mctx, &newkey);
+ }
+ }
+
+ if (isc_log_wouldlog(dns_lctx, ISC_LOG_DEBUG(3))) {
+ char namebuf[DNS_NAME_FORMATSIZE];
+ dns_name_format(origin, namebuf, sizeof(namebuf));
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_DEBUG(3),
+ "keymgr: %s done", namebuf);
+ }
+ return (result);
+}
+
+static isc_result_t
+keymgr_checkds(dns_kasp_t *kasp, dns_dnsseckeylist_t *keyring,
+ const char *directory, isc_stdtime_t now, isc_stdtime_t when,
+ bool dspublish, dns_keytag_t id, unsigned int alg,
+ bool check_id) {
+ int options = (DST_TYPE_PRIVATE | DST_TYPE_PUBLIC | DST_TYPE_STATE);
+ isc_dir_t dir;
+ isc_result_t result;
+ dns_dnsseckey_t *ksk_key = NULL;
+
+ REQUIRE(DNS_KASP_VALID(kasp));
+ REQUIRE(keyring != NULL);
+
+ for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*keyring); dkey != NULL;
+ dkey = ISC_LIST_NEXT(dkey, link))
+ {
+ isc_result_t ret;
+ bool ksk = false;
+
+ ret = dst_key_getbool(dkey->key, DST_BOOL_KSK, &ksk);
+ if (ret == ISC_R_SUCCESS && ksk) {
+ if (check_id && dst_key_id(dkey->key) != id) {
+ continue;
+ }
+ if (alg > 0 && dst_key_alg(dkey->key) != alg) {
+ continue;
+ }
+
+ if (ksk_key != NULL) {
+ /*
+ * Only checkds for one key at a time.
+ */
+ return (DNS_R_TOOMANYKEYS);
+ }
+
+ ksk_key = dkey;
+ }
+ }
+
+ if (ksk_key == NULL) {
+ return (DNS_R_NOKEYMATCH);
+ }
+
+ if (dspublish) {
+ dst_key_state_t s;
+ dst_key_settime(ksk_key->key, DST_TIME_DSPUBLISH, when);
+ result = dst_key_getstate(ksk_key->key, DST_KEY_DS, &s);
+ if (result != ISC_R_SUCCESS || s != RUMOURED) {
+ dst_key_setstate(ksk_key->key, DST_KEY_DS, RUMOURED);
+ }
+ } else {
+ dst_key_state_t s;
+ dst_key_settime(ksk_key->key, DST_TIME_DSDELETE, when);
+ result = dst_key_getstate(ksk_key->key, DST_KEY_DS, &s);
+ if (result != ISC_R_SUCCESS || s != UNRETENTIVE) {
+ dst_key_setstate(ksk_key->key, DST_KEY_DS, UNRETENTIVE);
+ }
+ }
+
+ if (isc_log_wouldlog(dns_lctx, ISC_LOG_NOTICE)) {
+ char keystr[DST_KEY_FORMATSIZE];
+ char timestr[26]; /* Minimal buf as per ctime_r() spec. */
+
+ dst_key_format(ksk_key->key, keystr, sizeof(keystr));
+ isc_stdtime_tostring(when, timestr, sizeof(timestr));
+ isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC,
+ DNS_LOGMODULE_DNSSEC, ISC_LOG_NOTICE,
+ "keymgr: checkds DS for key %s seen %s at %s",
+ keystr, dspublish ? "published" : "withdrawn",
+ timestr);
+ }
+
+ /* Store key state and update hints. */
+ isc_dir_init(&dir);
+ if (directory == NULL) {
+ directory = ".";
+ }
+ result = isc_dir_open(&dir, directory);
+ if (result != ISC_R_SUCCESS) {
+ return (result);
+ }
+
+ dns_dnssec_get_hints(ksk_key, now);
+ result = dst_key_tofile(ksk_key->key, options, directory);
+ if (result == ISC_R_SUCCESS) {
+ dst_key_setmodified(ksk_key->key, false);
+ }
+ isc_dir_close(&dir);
+
+ return (result);
+}
+
+isc_result_t
+dns_keymgr_checkds(dns_kasp_t *kasp, dns_dnsseckeylist_t *keyring,
+ const char *directory, isc_stdtime_t now, isc_stdtime_t when,
+ bool dspublish) {
+ return (keymgr_checkds(kasp, keyring, directory, now, when, dspublish,
+ 0, 0, false));
+}
+
+isc_result_t
+dns_keymgr_checkds_id(dns_kasp_t *kasp, dns_dnsseckeylist_t *keyring,
+ const char *directory, isc_stdtime_t now,
+ isc_stdtime_t when, bool dspublish, dns_keytag_t id,
+ unsigned int alg) {
+ return (keymgr_checkds(kasp, keyring, directory, now, when, dspublish,
+ id, alg, true));
+}
+
+static void
+keytime_status(dst_key_t *key, isc_stdtime_t now, isc_buffer_t *buf,
+ const char *pre, int ks, int kt) {
+ char timestr[26]; /* Minimal buf as per ctime_r() spec. */
+ isc_result_t ret;
+ isc_stdtime_t when = 0;
+ dst_key_state_t state = NA;
+
+ isc_buffer_printf(buf, "%s", pre);
+ (void)dst_key_getstate(key, ks, &state);
+ ret = dst_key_gettime(key, kt, &when);
+ if (state == RUMOURED || state == OMNIPRESENT) {
+ isc_buffer_printf(buf, "yes - since ");
+ } else if (now < when) {
+ isc_buffer_printf(buf, "no - scheduled ");
+ } else {
+ isc_buffer_printf(buf, "no\n");
+ return;
+ }
+ if (ret == ISC_R_SUCCESS) {
+ isc_stdtime_tostring(when, timestr, sizeof(timestr));
+ isc_buffer_printf(buf, "%s\n", timestr);
+ }
+}
+
+static void
+rollover_status(dns_dnsseckey_t *dkey, dns_kasp_t *kasp, isc_stdtime_t now,
+ isc_buffer_t *buf, bool zsk) {
+ char timestr[26]; /* Minimal buf as per ctime_r() spec. */
+ isc_result_t ret = ISC_R_SUCCESS;
+ isc_stdtime_t active_time = 0;
+ dst_key_state_t state = NA, goal = NA;
+ int rrsig, active, retire;
+ dst_key_t *key = dkey->key;
+
+ if (zsk) {
+ rrsig = DST_KEY_ZRRSIG;
+ active = DST_TIME_ACTIVATE;
+ retire = DST_TIME_INACTIVE;
+ } else {
+ rrsig = DST_KEY_KRRSIG;
+ active = DST_TIME_PUBLISH;
+ retire = DST_TIME_DELETE;
+ }
+
+ isc_buffer_printf(buf, "\n");
+
+ (void)dst_key_getstate(key, DST_KEY_GOAL, &goal);
+ (void)dst_key_getstate(key, rrsig, &state);
+ (void)dst_key_gettime(key, active, &active_time);
+ if (active_time == 0) {
+ // only interested in keys that were once active.
+ return;
+ }
+
+ if (goal == HIDDEN && (state == UNRETENTIVE || state == HIDDEN)) {
+ isc_stdtime_t remove_time = 0;
+ // is the key removed yet?
+ state = NA;
+ (void)dst_key_getstate(key, DST_KEY_DNSKEY, &state);
+ if (state == RUMOURED || state == OMNIPRESENT) {
+ ret = dst_key_gettime(key, DST_TIME_DELETE,
+ &remove_time);
+ if (ret == ISC_R_SUCCESS) {
+ isc_buffer_printf(buf, " Key is retired, will "
+ "be removed on ");
+ isc_stdtime_tostring(remove_time, timestr,
+ sizeof(timestr));
+ isc_buffer_printf(buf, "%s", timestr);
+ }
+ } else {
+ isc_buffer_printf(
+ buf, " Key has been removed from the zone");
+ }
+ } else {
+ isc_stdtime_t retire_time = 0;
+ uint32_t lifetime = 0;
+ (void)dst_key_getnum(key, DST_NUM_LIFETIME, &lifetime);
+ ret = dst_key_gettime(key, retire, &retire_time);
+ if (ret == ISC_R_SUCCESS) {
+ if (now < retire_time) {
+ if (goal == OMNIPRESENT) {
+ isc_buffer_printf(buf,
+ " Next rollover "
+ "scheduled on ");
+ retire_time = keymgr_prepublication_time(
+ dkey, kasp, lifetime, now);
+ } else {
+ isc_buffer_printf(
+ buf, " Key will retire on ");
+ }
+ } else {
+ isc_buffer_printf(buf,
+ " Rollover is due since ");
+ }
+ isc_stdtime_tostring(retire_time, timestr,
+ sizeof(timestr));
+ isc_buffer_printf(buf, "%s", timestr);
+ } else {
+ isc_buffer_printf(buf, " No rollover scheduled");
+ }
+ }
+ isc_buffer_printf(buf, "\n");
+}
+
+static void
+keystate_status(dst_key_t *key, isc_buffer_t *buf, const char *pre, int ks) {
+ dst_key_state_t state = NA;
+
+ (void)dst_key_getstate(key, ks, &state);
+ switch (state) {
+ case HIDDEN:
+ isc_buffer_printf(buf, " - %shidden\n", pre);
+ break;
+ case RUMOURED:
+ isc_buffer_printf(buf, " - %srumoured\n", pre);
+ break;
+ case OMNIPRESENT:
+ isc_buffer_printf(buf, " - %somnipresent\n", pre);
+ break;
+ case UNRETENTIVE:
+ isc_buffer_printf(buf, " - %sunretentive\n", pre);
+ break;
+ case NA:
+ default:
+ /* print nothing */
+ break;
+ }
+}
+
+void
+dns_keymgr_status(dns_kasp_t *kasp, dns_dnsseckeylist_t *keyring,
+ isc_stdtime_t now, char *out, size_t out_len) {
+ isc_buffer_t buf;
+ char timestr[26]; /* Minimal buf as per ctime_r() spec. */
+
+ REQUIRE(DNS_KASP_VALID(kasp));
+ REQUIRE(keyring != NULL);
+ REQUIRE(out != NULL);
+
+ isc_buffer_init(&buf, out, out_len);
+
+ // policy name
+ isc_buffer_printf(&buf, "dnssec-policy: %s\n", dns_kasp_getname(kasp));
+ isc_buffer_printf(&buf, "current time: ");
+ isc_stdtime_tostring(now, timestr, sizeof(timestr));
+ isc_buffer_printf(&buf, "%s\n", timestr);
+
+ for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*keyring); dkey != NULL;
+ dkey = ISC_LIST_NEXT(dkey, link))
+ {
+ char algstr[DNS_NAME_FORMATSIZE];
+ bool ksk = false, zsk = false;
+ isc_result_t ret;
+
+ if (dst_key_is_unused(dkey->key)) {
+ continue;
+ }
+
+ // key data
+ dns_secalg_format((dns_secalg_t)dst_key_alg(dkey->key), algstr,
+ sizeof(algstr));
+ isc_buffer_printf(&buf, "\nkey: %d (%s), %s\n",
+ dst_key_id(dkey->key), algstr,
+ keymgr_keyrole(dkey->key));
+
+ // publish status
+ keytime_status(dkey->key, now, &buf,
+ " published: ", DST_KEY_DNSKEY,
+ DST_TIME_PUBLISH);
+
+ // signing status
+ ret = dst_key_getbool(dkey->key, DST_BOOL_KSK, &ksk);
+ if (ret == ISC_R_SUCCESS && ksk) {
+ keytime_status(dkey->key, now, &buf,
+ " key signing: ", DST_KEY_KRRSIG,
+ DST_TIME_PUBLISH);
+ }
+ ret = dst_key_getbool(dkey->key, DST_BOOL_ZSK, &zsk);
+ if (ret == ISC_R_SUCCESS && zsk) {
+ keytime_status(dkey->key, now, &buf,
+ " zone signing: ", DST_KEY_ZRRSIG,
+ DST_TIME_ACTIVATE);
+ }
+
+ // rollover status
+ rollover_status(dkey, kasp, now, &buf, zsk);
+
+ // key states
+ keystate_status(dkey->key, &buf,
+ "goal: ", DST_KEY_GOAL);
+ keystate_status(dkey->key, &buf,
+ "dnskey: ", DST_KEY_DNSKEY);
+ keystate_status(dkey->key, &buf,
+ "ds: ", DST_KEY_DS);
+ keystate_status(dkey->key, &buf,
+ "zone rrsig: ", DST_KEY_ZRRSIG);
+ keystate_status(dkey->key, &buf,
+ "key rrsig: ", DST_KEY_KRRSIG);
+ }
+}
+
+isc_result_t
+dns_keymgr_rollover(dns_kasp_t *kasp, dns_dnsseckeylist_t *keyring,
+ const char *directory, isc_stdtime_t now,
+ isc_stdtime_t when, dns_keytag_t id,
+ unsigned int algorithm) {
+ int options = (DST_TYPE_PRIVATE | DST_TYPE_PUBLIC | DST_TYPE_STATE);
+ isc_dir_t dir;
+ isc_result_t result;
+ dns_dnsseckey_t *key = NULL;
+ isc_stdtime_t active, retire, prepub;
+
+ REQUIRE(DNS_KASP_VALID(kasp));
+ REQUIRE(keyring != NULL);
+
+ for (dns_dnsseckey_t *dkey = ISC_LIST_HEAD(*keyring); dkey != NULL;
+ dkey = ISC_LIST_NEXT(dkey, link))
+ {
+ if (dst_key_id(dkey->key) != id) {
+ continue;
+ }
+ if (algorithm > 0 && dst_key_alg(dkey->key) != algorithm) {
+ continue;
+ }
+ if (key != NULL) {
+ /*
+ * Only rollover for one key at a time.
+ */
+ return (DNS_R_TOOMANYKEYS);
+ }
+ key = dkey;
+ }
+
+ if (key == NULL) {
+ return (DNS_R_NOKEYMATCH);
+ }
+
+ result = dst_key_gettime(key->key, DST_TIME_ACTIVATE, &active);
+ if (result != ISC_R_SUCCESS || active > now) {
+ return (DNS_R_KEYNOTACTIVE);
+ }
+
+ result = dst_key_gettime(key->key, DST_TIME_INACTIVE, &retire);
+ if (result != ISC_R_SUCCESS) {
+ /**
+ * Default to as if this key was not scheduled to
+ * become retired, as if it had unlimited lifetime.
+ */
+ retire = 0;
+ }
+
+ /**
+ * Usually when is set to now, which is before the scheduled
+ * prepublication time, meaning we reduce the lifetime of the
+ * key. But in some cases, the lifetime can also be extended.
+ * We accept it, but we can return an error here if that
+ * turns out to be unintuitive behavior.
+ */
+ prepub = dst_key_getttl(key->key) + dns_kasp_publishsafety(kasp) +
+ dns_kasp_zonepropagationdelay(kasp);
+ retire = when + prepub;
+
+ dst_key_settime(key->key, DST_TIME_INACTIVE, retire);
+ dst_key_setnum(key->key, DST_NUM_LIFETIME, (retire - active));
+
+ /* Store key state and update hints. */
+ isc_dir_init(&dir);
+ if (directory == NULL) {
+ directory = ".";
+ }
+ result = isc_dir_open(&dir, directory);
+ if (result != ISC_R_SUCCESS) {
+ return (result);
+ }
+
+ dns_dnssec_get_hints(key, now);
+ result = dst_key_tofile(key->key, options, directory);
+ if (result == ISC_R_SUCCESS) {
+ dst_key_setmodified(key->key, false);
+ }
+ isc_dir_close(&dir);
+
+ return (result);
+}