summaryrefslogtreecommitdiffstats
path: root/src/knot/dnssec
diff options
context:
space:
mode:
Diffstat (limited to 'src/knot/dnssec')
-rw-r--r--src/knot/dnssec/context.c351
-rw-r--r--src/knot/dnssec/context.h82
-rw-r--r--src/knot/dnssec/ds_query.c288
-rw-r--r--src/knot/dnssec/ds_query.h22
-rw-r--r--src/knot/dnssec/kasp/kasp_db.c610
-rw-r--r--src/knot/dnssec/kasp/kasp_db.h296
-rw-r--r--src/knot/dnssec/kasp/kasp_zone.c447
-rw-r--r--src/knot/dnssec/kasp/kasp_zone.h63
-rw-r--r--src/knot/dnssec/kasp/keystate.c74
-rw-r--r--src/knot/dnssec/kasp/keystate.h35
-rw-r--r--src/knot/dnssec/kasp/keystore.c89
-rw-r--r--src/knot/dnssec/kasp/keystore.h22
-rw-r--r--src/knot/dnssec/kasp/policy.h135
-rw-r--r--src/knot/dnssec/key-events.c863
-rw-r--r--src/knot/dnssec/key-events.h69
-rw-r--r--src/knot/dnssec/key_records.c300
-rw-r--r--src/knot/dnssec/key_records.h54
-rw-r--r--src/knot/dnssec/nsec-chain.c797
-rw-r--r--src/knot/dnssec/nsec-chain.h174
-rw-r--r--src/knot/dnssec/nsec3-chain.c733
-rw-r--r--src/knot/dnssec/nsec3-chain.h69
-rw-r--r--src/knot/dnssec/policy.c51
-rw-r--r--src/knot/dnssec/policy.h26
-rw-r--r--src/knot/dnssec/rrset-sign.c425
-rw-r--r--src/knot/dnssec/rrset-sign.h123
-rw-r--r--src/knot/dnssec/zone-events.c470
-rw-r--r--src/knot/dnssec/zone-events.h134
-rw-r--r--src/knot/dnssec/zone-keys.c767
-rw-r--r--src/knot/dnssec/zone-keys.h213
-rw-r--r--src/knot/dnssec/zone-nsec.c429
-rw-r--r--src/knot/dnssec/zone-nsec.h163
-rw-r--r--src/knot/dnssec/zone-sign.c1081
-rw-r--r--src/knot/dnssec/zone-sign.h162
33 files changed, 9617 insertions, 0 deletions
diff --git a/src/knot/dnssec/context.c b/src/knot/dnssec/context.c
new file mode 100644
index 0000000..f2a3685
--- /dev/null
+++ b/src/knot/dnssec/context.c
@@ -0,0 +1,351 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "contrib/macros.h"
+#include "contrib/time.h"
+#include "libknot/libknot.h"
+#include "knot/dnssec/context.h"
+#include "knot/dnssec/kasp/keystore.h"
+#include "knot/dnssec/key_records.h"
+#include "knot/server/dthreads.h"
+
+knot_dynarray_define(parent, knot_kasp_parent_t, DYNARRAY_VISIBILITY_NORMAL)
+
+static void policy_load(knot_kasp_policy_t *policy, conf_t *conf, conf_val_t *id,
+ const knot_dname_t *zone_name)
+{
+ if (conf_str(id) == NULL) {
+ policy->string = strdup("default");
+ } else {
+ policy->string = strdup(conf_str(id));
+ }
+
+ conf_val_t val = conf_id_get(conf, C_POLICY, C_MANUAL, id);
+ policy->manual = conf_bool(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_SINGLE_TYPE_SIGNING, id);
+ policy->single_type_signing = conf_bool(&val);
+ policy->sts_default = (val.code != KNOT_EOK);
+
+ val = conf_id_get(conf, C_POLICY, C_ALG, id);
+ policy->algorithm = conf_opt(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_KSK_SHARED, id);
+ policy->ksk_shared = conf_bool(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_KSK_SIZE, id);
+ int64_t num = conf_int(&val);
+ policy->ksk_size = (num != YP_NIL) ? num :
+ dnssec_algorithm_key_size_default(policy->algorithm);
+
+ val = conf_id_get(conf, C_POLICY, C_ZSK_SIZE, id);
+ num = conf_int(&val);
+ policy->zsk_size = (num != YP_NIL) ? num :
+ dnssec_algorithm_key_size_default(policy->algorithm);
+
+ val = conf_id_get(conf, C_POLICY, C_DNSKEY_TTL, id);
+ int64_t ttl = conf_int(&val);
+ policy->dnskey_ttl = (ttl != YP_NIL) ? ttl : UINT32_MAX;
+
+ val = conf_id_get(conf, C_POLICY, C_ZONE_MAX_TTL, id);
+ ttl = conf_int(&val);
+ policy->zone_maximal_ttl = (ttl != YP_NIL) ? ttl : UINT32_MAX;
+
+ val = conf_id_get(conf, C_POLICY, C_ZSK_LIFETIME, id);
+ policy->zsk_lifetime = conf_int(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_KSK_LIFETIME, id);
+ policy->ksk_lifetime = conf_int(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_DELETE_DELAY, id);
+ policy->delete_delay = conf_int(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_PROPAG_DELAY, id);
+ policy->propagation_delay = conf_int(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_RRSIG_LIFETIME, id);
+ policy->rrsig_lifetime = conf_int(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_RRSIG_REFRESH, id);
+ num = conf_int(&val);
+ policy->rrsig_refresh_before = (num != YP_NIL) ? num : UINT32_MAX;
+ if (policy->rrsig_refresh_before == UINT32_MAX && policy->zone_maximal_ttl != UINT32_MAX) {
+ policy->rrsig_refresh_before = policy->propagation_delay + policy->zone_maximal_ttl;
+ }
+
+ val = conf_id_get(conf, C_POLICY, C_RRSIG_PREREFRESH, id);
+ policy->rrsig_prerefresh = conf_int(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_REPRO_SIGNING, id);
+ policy->reproducible_sign = conf_bool(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_NSEC3, id);
+ policy->nsec3_enabled = conf_bool(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_NSEC3_OPT_OUT, id);
+ policy->nsec3_opt_out = conf_bool(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_NSEC3_ITER, id);
+ policy->nsec3_iterations = conf_int(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_NSEC3_SALT_LEN, id);
+ policy->nsec3_salt_length = conf_int(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_NSEC3_SALT_LIFETIME, id);
+ policy->nsec3_salt_lifetime = conf_int(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_CDS_CDNSKEY, id);
+ policy->cds_cdnskey_publish = conf_opt(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_CDS_DIGESTTYPE, id);
+ policy->cds_dt = conf_opt(&val);
+
+ val = conf_id_get(conf, C_POLICY, C_DNSKEY_MGMT, id);
+ policy->incremental = (conf_opt(&val) == DNSKEY_MGMT_INCREMENTAL);
+
+ conf_val_t ksk_sbm = conf_id_get(conf, C_POLICY, C_KSK_SBM, id);
+ if (ksk_sbm.code == KNOT_EOK) {
+ val = conf_id_get(conf, C_SBM, C_CHK_INTERVAL, &ksk_sbm);
+ policy->ksk_sbm_check_interval = conf_int(&val);
+
+ val = conf_id_get(conf, C_SBM, C_TIMEOUT, &ksk_sbm);
+ policy->ksk_sbm_timeout = conf_int(&val);
+
+ val = conf_id_get(conf, C_SBM, C_PARENT, &ksk_sbm);
+ conf_mix_iter_t iter;
+ conf_mix_iter_init(conf, &val, &iter);
+ while (iter.id->code == KNOT_EOK) {
+ conf_val_t addr = conf_id_get(conf, C_RMT, C_ADDR, iter.id);
+ knot_kasp_parent_t p = { .addrs = conf_val_count(&addr) };
+ p.addr = p.addrs ? malloc(p.addrs * sizeof(*p.addr)) : NULL;
+ if (p.addr != NULL) {
+ for (size_t i = 0; i < p.addrs; i++) {
+ p.addr[i] = conf_remote(conf, iter.id, i);
+ }
+ parent_dynarray_add(&policy->parents, &p);
+ }
+ conf_mix_iter_next(&iter);
+ }
+
+ val = conf_id_get(conf, C_SBM, C_PARENT_DELAY, &ksk_sbm);
+ policy->ksk_sbm_delay = conf_int(&val);
+ }
+
+ val = conf_id_get(conf, C_POLICY, C_SIGNING_THREADS, id);
+ policy->signing_threads = conf_int(&val);
+
+ val = conf_zone_get(conf, C_DS_PUSH, zone_name);
+ if (val.code != KNOT_EOK) {
+ val = conf_id_get(conf, C_POLICY, C_DS_PUSH, id);
+ }
+ policy->ds_push = conf_val_count(&val) > 0;
+
+ val = conf_id_get(conf, C_POLICY, C_OFFLINE_KSK, id);
+ policy->offline_ksk = conf_bool(&val);
+
+ policy->unsafe = 0;
+ val = conf_id_get(conf, C_POLICY, C_UNSAFE_OPERATION, id);
+ while (val.code == KNOT_EOK) {
+ policy->unsafe |= conf_opt(&val);
+ conf_val_next(&val);
+ }
+}
+
+int kdnssec_ctx_init(conf_t *conf, kdnssec_ctx_t *ctx, const knot_dname_t *zone_name,
+ knot_lmdb_db_t *kaspdb, const conf_mod_id_t *from_module)
+{
+ if (ctx == NULL || zone_name == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ int ret;
+
+ memset(ctx, 0, sizeof(*ctx));
+
+ ctx->zone = calloc(1, sizeof(*ctx->zone));
+ if (ctx->zone == NULL) {
+ ret = KNOT_ENOMEM;
+ goto init_error;
+ }
+
+ ctx->kasp_db = kaspdb;
+ ret = knot_lmdb_open(ctx->kasp_db);
+ if (ret != KNOT_EOK) {
+ goto init_error;
+ }
+
+ ret = kasp_zone_load(ctx->zone, zone_name, ctx->kasp_db,
+ &ctx->keytag_conflict);
+ if (ret != KNOT_EOK) {
+ goto init_error;
+ }
+
+ ctx->kasp_zone_path = conf_db(conf, C_KASP_DB);
+ if (ctx->kasp_zone_path == NULL) {
+ ret = KNOT_ENOMEM;
+ goto init_error;
+ }
+
+ ctx->policy = calloc(1, sizeof(*ctx->policy));
+ if (ctx->policy == NULL) {
+ ret = KNOT_ENOMEM;
+ goto init_error;
+ }
+
+ ret = kasp_db_get_saved_ttls(ctx->kasp_db, zone_name,
+ &ctx->policy->saved_max_ttl,
+ &ctx->policy->saved_key_ttl);
+ if (ret != KNOT_EOK && ret != KNOT_ENOENT) {
+ return ret;
+ }
+
+ conf_val_t policy_id;
+ if (from_module == NULL) {
+ policy_id = conf_zone_get(conf, C_DNSSEC_POLICY, zone_name);
+ } else {
+ policy_id = conf_mod_get(conf, C_POLICY, from_module);
+ }
+ conf_id_fix_default(&policy_id);
+ policy_load(ctx->policy, conf, &policy_id, ctx->zone->dname);
+
+ ret = zone_init_keystore(conf, &policy_id, &ctx->keystore, NULL,
+ &ctx->policy->key_label);
+ if (ret != KNOT_EOK) {
+ goto init_error;
+ }
+
+ ctx->dbus_event = conf->cache.srv_dbus_event;
+
+ ctx->now = knot_time();
+
+ key_records_init(ctx, &ctx->offline_records);
+ if (ctx->policy->offline_ksk) {
+ ret = kasp_db_load_offline_records(ctx->kasp_db, ctx->zone->dname,
+ &ctx->now, &ctx->offline_next_time,
+ &ctx->offline_records);
+ if (ret != KNOT_EOK && ret != KNOT_ENOENT) {
+ goto init_error;
+ }
+ }
+
+ return KNOT_EOK;
+init_error:
+ kdnssec_ctx_deinit(ctx);
+ return ret;
+}
+
+int kdnssec_ctx_commit(kdnssec_ctx_t *ctx)
+{
+ if (ctx == NULL || ctx->kasp_zone_path == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ if (ctx->policy->dnskey_ttl != UINT32_MAX &&
+ ctx->policy->zone_maximal_ttl != UINT32_MAX) {
+ int ret = kasp_db_set_saved_ttls(ctx->kasp_db, ctx->zone->dname,
+ ctx->policy->zone_maximal_ttl,
+ ctx->policy->dnskey_ttl);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ return kasp_zone_save(ctx->zone, ctx->zone->dname, ctx->kasp_db);
+}
+
+void kdnssec_ctx_deinit(kdnssec_ctx_t *ctx)
+{
+ if (ctx == NULL) {
+ return;
+ }
+
+ if (ctx->policy != NULL) {
+ free(ctx->policy->string);
+ knot_dynarray_foreach(parent, knot_kasp_parent_t, i, ctx->policy->parents) {
+ free(i->addr);
+ }
+ free(ctx->policy);
+ }
+ key_records_clear(&ctx->offline_records);
+ dnssec_keystore_deinit(ctx->keystore);
+ kasp_zone_free(&ctx->zone);
+ free(ctx->kasp_zone_path);
+
+ memset(ctx, 0, sizeof(*ctx));
+}
+
+// expects policy struct to be zeroed
+static void policy_from_zone(knot_kasp_policy_t *policy, const zone_contents_t *zone)
+{
+ knot_rdataset_t *dnskey = node_rdataset(zone->apex, KNOT_RRTYPE_DNSKEY);
+ knot_rdataset_t *n3p = node_rdataset(zone->apex, KNOT_RRTYPE_NSEC3PARAM);
+
+ policy->manual = true;
+ policy->single_type_signing = (dnskey != NULL && dnskey->count == 1);
+
+ if (n3p != NULL) {
+ policy->nsec3_enabled = true;
+ policy->nsec3_iterations = knot_nsec3param_iters(n3p->rdata);
+ policy->nsec3_salt_length = knot_nsec3param_salt_len(n3p->rdata);
+ }
+ policy->signing_threads = 1;
+}
+
+int kdnssec_validation_ctx(conf_t *conf, kdnssec_ctx_t *ctx, const zone_contents_t *zone)
+{
+ if (ctx == NULL || zone == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ memset(ctx, 0, sizeof(*ctx));
+
+ ctx->zone = calloc(1, sizeof(*ctx->zone));
+ if (ctx->zone == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ ctx->policy = calloc(1, sizeof(*ctx->policy));
+ if (ctx->policy == NULL) {
+ free(ctx->zone);
+ return KNOT_ENOMEM;
+ }
+
+ policy_from_zone(ctx->policy, zone);
+ if (conf != NULL) {
+ conf_val_t policy_id = conf_zone_get(conf, C_DNSSEC_POLICY, zone->apex->owner);
+ conf_id_fix_default(&policy_id);
+ conf_val_t num_threads = conf_id_get(conf, C_POLICY, C_SIGNING_THREADS, &policy_id);
+ ctx->policy->signing_threads = conf_int(&num_threads);
+ } else {
+ ctx->policy->signing_threads = MAX(dt_optimal_size(), 1);
+ }
+
+ int ret = kasp_zone_from_contents(ctx->zone, zone, ctx->policy->single_type_signing,
+ ctx->policy->nsec3_enabled, &ctx->policy->nsec3_iterations,
+ &ctx->keytag_conflict);
+ if (ret != KNOT_EOK) {
+ memset(ctx->zone, 0, sizeof(*ctx->zone));
+ kdnssec_ctx_deinit(ctx);
+ return ret;
+ }
+
+ ctx->now = knot_time();
+ ctx->validation_mode = true;
+ return KNOT_EOK;
+}
diff --git a/src/knot/dnssec/context.h b/src/knot/dnssec/context.h
new file mode 100644
index 0000000..55a2f3c
--- /dev/null
+++ b/src/knot/dnssec/context.h
@@ -0,0 +1,82 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <time.h>
+
+#include "libdnssec/keystore.h"
+
+#include "knot/conf/conf.h"
+#include "knot/dnssec/kasp/kasp_zone.h"
+#include "knot/dnssec/kasp/policy.h"
+
+/*!
+ * \brief DNSSEC signing context.
+ */
+typedef struct {
+ knot_time_t now;
+
+ knot_lmdb_db_t *kasp_db;
+ knot_kasp_zone_t *zone;
+ knot_kasp_policy_t *policy;
+ dnssec_keystore_t *keystore;
+
+ char *kasp_zone_path;
+
+ bool rrsig_drop_existing;
+ bool keep_deleted_keys;
+ bool keytag_conflict;
+ bool validation_mode;
+
+ unsigned dbus_event;
+
+ key_records_t offline_records;
+ knot_time_t offline_next_time;
+} kdnssec_ctx_t;
+
+/*!
+ * \brief Initialize DNSSEC signing context.
+ *
+ * \param conf Configuration.
+ * \param ctx Signing context to be initialized.
+ * \param zone_name Name of the zone.
+ * \param kaspdb Key and signature policy database.
+ * \param from_module Module identifier if initialized from a module.
+ */
+int kdnssec_ctx_init(conf_t *conf, kdnssec_ctx_t *ctx, const knot_dname_t *zone_name,
+ knot_lmdb_db_t *kaspdb, const conf_mod_id_t *from_module);
+
+/*!
+ * \brief Initialize DNSSEC validating context.
+ *
+ * \param conf Configuration.
+ * \param ctx Signing context to be initialized.
+ * \param zone Zone contents to be validated.
+ *
+ * \return KNOT_E*
+ */
+int kdnssec_validation_ctx(conf_t *conf, kdnssec_ctx_t *ctx, const zone_contents_t *zone);
+
+/*!
+ * \brief Save the changes in ctx (in kasp zone).
+ */
+int kdnssec_ctx_commit(kdnssec_ctx_t *ctx);
+
+/*!
+ * \brief Cleanup DNSSEC signing context.
+ */
+void kdnssec_ctx_deinit(kdnssec_ctx_t *ctx);
diff --git a/src/knot/dnssec/ds_query.c b/src/knot/dnssec/ds_query.c
new file mode 100644
index 0000000..918ae5d
--- /dev/null
+++ b/src/knot/dnssec/ds_query.c
@@ -0,0 +1,288 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include "contrib/macros.h"
+#include "knot/common/log.h"
+#include "knot/conf/conf.h"
+#include "knot/dnssec/ds_query.h"
+#include "knot/dnssec/key-events.h"
+#include "knot/query/layer.h"
+#include "knot/query/query.h"
+#include "knot/query/requestor.h"
+
+static bool match_key_ds(knot_kasp_key_t *key, knot_rdata_t *ds)
+{
+ assert(key);
+ assert(ds);
+
+ dnssec_binary_t ds_rdata = {
+ .size = ds->len,
+ .data = ds->data,
+ };
+
+ dnssec_binary_t cds_rdata = { 0 };
+
+ int ret = dnssec_key_create_ds(key->key, knot_ds_digest_type(ds), &cds_rdata);
+ if (ret != KNOT_EOK) {
+ return false;
+ }
+
+ ret = (dnssec_binary_cmp(&cds_rdata, &ds_rdata) == 0);
+ dnssec_binary_free(&cds_rdata);
+ return ret;
+}
+
+static bool match_key_ds_rrset(knot_kasp_key_t *key, const knot_rrset_t *rr)
+{
+ if (key == NULL) {
+ return false;
+ }
+ knot_rdata_t *rd = rr->rrs.rdata;
+ for (int i = 0; i < rr->rrs.count; i++) {
+ if (match_key_ds(key, rd)) {
+ return true;
+ }
+ rd = knot_rdataset_next(rd);
+ }
+ return false;
+}
+
+struct ds_query_data {
+ conf_t *conf;
+
+ const knot_dname_t *zone_name;
+ const struct sockaddr *remote;
+
+ knot_kasp_key_t *key;
+ knot_kasp_key_t *not_key;
+
+ query_edns_data_t edns;
+
+ bool ds_ok;
+ bool result_logged;
+
+ uint32_t ttl;
+};
+
+static int ds_query_begin(knot_layer_t *layer, void *params)
+{
+ layer->data = params;
+
+ return KNOT_STATE_PRODUCE;
+}
+
+static int ds_query_produce(knot_layer_t *layer, knot_pkt_t *pkt)
+{
+ struct ds_query_data *data = layer->data;
+
+ query_init_pkt(pkt);
+
+ int r = knot_pkt_put_question(pkt, data->zone_name, KNOT_CLASS_IN, KNOT_RRTYPE_DS);
+ if (r != KNOT_EOK) {
+ return KNOT_STATE_FAIL;
+ }
+
+ r = query_put_edns(pkt, &data->edns);
+ if (r != KNOT_EOK) {
+ return KNOT_STATE_FAIL;
+ }
+
+ knot_wire_set_rd(pkt->wire);
+
+ return KNOT_STATE_CONSUME;
+}
+
+static int ds_query_consume(knot_layer_t *layer, knot_pkt_t *pkt)
+{
+ struct ds_query_data *data = layer->data;
+ data->result_logged = true;
+
+ uint16_t rcode = knot_pkt_ext_rcode(pkt);
+ if (rcode != KNOT_RCODE_NOERROR) {
+ ns_log((rcode == KNOT_RCODE_NXDOMAIN ? LOG_NOTICE : LOG_WARNING),
+ data->zone_name, LOG_OPERATION_DS_CHECK,
+ LOG_DIRECTION_OUT, data->remote,
+ layer->flags & KNOT_REQUESTOR_REUSED,
+ "failed (%s)", knot_pkt_ext_rcode_name(pkt));
+ return KNOT_STATE_FAIL;
+ }
+
+ const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
+
+ bool match = false, match_not = false;
+
+ for (size_t j = 0; j < answer->count; j++) {
+ const knot_rrset_t *rr = knot_pkt_rr(answer, j);
+ switch ((rr && rr->rrs.count > 0) ? rr->type : 0) {
+ case KNOT_RRTYPE_DS:
+ if (match_key_ds_rrset(data->key, rr)) {
+ match = true;
+ if (data->ttl == 0) { // fallback: if there is no RRSIG
+ data->ttl = rr->ttl;
+ }
+ }
+ if (match_key_ds_rrset(data->not_key, rr)) {
+ match_not = true;
+ }
+ break;
+ case KNOT_RRTYPE_RRSIG:
+ data->ttl = knot_rrsig_original_ttl(rr->rrs.rdata);
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (match_not) {
+ match = false;
+ }
+
+ ns_log(LOG_INFO, data->zone_name, LOG_OPERATION_DS_CHECK,
+ LOG_DIRECTION_OUT, data->remote, layer->flags & KNOT_REQUESTOR_REUSED,
+ "KSK submission check: %s", (match ? "positive" : "negative"));
+
+ if (match) {
+ data->ds_ok = true;
+ }
+ return KNOT_STATE_DONE;
+}
+
+static const knot_layer_api_t ds_query_api = {
+ .begin = ds_query_begin,
+ .produce = ds_query_produce,
+ .consume = ds_query_consume,
+ .reset = NULL,
+ .finish = NULL,
+};
+
+static int try_ds(conf_t *conf, const knot_dname_t *zone_name, const conf_remote_t *parent,
+ knot_kasp_key_t *key, knot_kasp_key_t *not_key, size_t timeout, uint32_t *ds_ttl)
+{
+ // TODO: Abstract interface to issue DNS queries. This is almost copy-pasted.
+
+ assert(zone_name);
+ assert(parent);
+
+ struct ds_query_data data = {
+ .zone_name = zone_name,
+ .remote = (struct sockaddr *)&parent->addr,
+ .key = key,
+ .not_key = not_key,
+ .edns = query_edns_data_init(conf, parent->addr.ss_family,
+ QUERY_EDNS_OPT_DO),
+ .ds_ok = false,
+ .result_logged = false,
+ .ttl = 0,
+ };
+
+ knot_requestor_t requestor;
+ knot_requestor_init(&requestor, &ds_query_api, &data, NULL);
+
+ knot_pkt_t *pkt = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, NULL);
+ if (!pkt) {
+ knot_requestor_clear(&requestor);
+ return KNOT_ENOMEM;
+ }
+
+ const struct sockaddr_storage *dst = &parent->addr;
+ const struct sockaddr_storage *src = &parent->via;
+ knot_request_t *req = knot_request_make(NULL, dst, src, pkt, &parent->key, 0);
+ if (!req) {
+ knot_request_free(req, NULL);
+ knot_requestor_clear(&requestor);
+ return KNOT_ENOMEM;
+ }
+
+ int ret = knot_requestor_exec(&requestor, req, timeout);
+ knot_request_free(req, NULL);
+ knot_requestor_clear(&requestor);
+
+ // alternative: we could put answer back through ctx instead of errcode
+ if (ret == KNOT_EOK && !data.ds_ok) {
+ ret = KNOT_ENORECORD;
+ }
+
+ if (ret != KNOT_EOK && !data.result_logged) {
+ ns_log(LOG_WARNING, zone_name, LOG_OPERATION_DS_CHECK,
+ LOG_DIRECTION_OUT, data.remote,
+ requestor.layer.flags & KNOT_REQUESTOR_REUSED,
+ "failed (%s)", knot_strerror(ret));
+ }
+
+ *ds_ttl = data.ttl;
+
+ return ret;
+}
+
+static knot_kasp_key_t *get_not_key(kdnssec_ctx_t *kctx, knot_kasp_key_t *key)
+{
+ knot_kasp_key_t *not_key = knot_dnssec_key2retire(kctx, key);
+
+ if (not_key == NULL || dnssec_key_get_algorithm(not_key->key) == dnssec_key_get_algorithm(key->key)) {
+ return NULL;
+ }
+
+ return not_key;
+}
+
+static bool parents_have_ds(conf_t *conf, kdnssec_ctx_t *kctx, knot_kasp_key_t *key,
+ size_t timeout, uint32_t *max_ds_ttl)
+{
+ bool success = false;
+ knot_dynarray_foreach(parent, knot_kasp_parent_t, i, kctx->policy->parents) {
+ success = false;
+ for (size_t j = 0; j < i->addrs; j++) {
+ uint32_t ds_ttl = 0;
+ int ret = try_ds(conf, kctx->zone->dname, &i->addr[j], key,
+ get_not_key(kctx, key), timeout, &ds_ttl);
+ if (ret == KNOT_EOK) {
+ *max_ds_ttl = MAX(*max_ds_ttl, ds_ttl);
+ success = true;
+ break;
+ } else if (ret == KNOT_ENORECORD) {
+ // parent was queried successfully, answer was negative
+ break;
+ }
+ }
+ // Each parent must succeed.
+ if (!success) {
+ return false;
+ }
+ }
+ return success;
+}
+
+int knot_parent_ds_query(conf_t *conf, kdnssec_ctx_t *kctx, size_t timeout)
+{
+ uint32_t max_ds_ttl = 0;
+
+ for (size_t i = 0; i < kctx->zone->num_keys; i++) {
+ knot_kasp_key_t *key = &kctx->zone->keys[i];
+ if (!key->is_pub_only &&
+ knot_time_cmp(key->timing.ready, kctx->now) <= 0 &&
+ knot_time_cmp(key->timing.active, kctx->now) > 0) {
+ assert(key->is_ksk);
+ if (parents_have_ds(conf, kctx, key, timeout, &max_ds_ttl)) {
+ return knot_dnssec_ksk_sbm_confirm(kctx, max_ds_ttl + kctx->policy->ksk_sbm_delay);
+ } else {
+ return KNOT_ENOENT;
+ }
+ }
+ }
+ return KNOT_NO_READY_KEY;
+}
diff --git a/src/knot/dnssec/ds_query.h b/src/knot/dnssec/ds_query.h
new file mode 100644
index 0000000..1144d21
--- /dev/null
+++ b/src/knot/dnssec/ds_query.h
@@ -0,0 +1,22 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/dnssec/zone-keys.h"
+#include "knot/dnssec/context.h"
+
+int knot_parent_ds_query(conf_t *conf, kdnssec_ctx_t *kctx, size_t timeout);
diff --git a/src/knot/dnssec/kasp/kasp_db.c b/src/knot/dnssec/kasp/kasp_db.c
new file mode 100644
index 0000000..29c6a7d
--- /dev/null
+++ b/src/knot/dnssec/kasp/kasp_db.c
@@ -0,0 +1,610 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "knot/dnssec/kasp/kasp_db.h"
+
+#include <inttypes.h>
+#include <stdio.h>
+
+#include "contrib/strtonum.h"
+#include "contrib/wire_ctx.h"
+#include "knot/dnssec/key_records.h"
+
+typedef enum {
+ KASPDBKEY_PARAMS = 0x1,
+ KASPDBKEY_POLICYLAST = 0x2,
+ KASPDBKEY_NSEC3SALT = 0x3,
+ KASPDBKEY_NSEC3TIME = 0x4,
+ KASPDBKEY_MASTERSERIAL = 0x5,
+ KASPDBKEY_LASTSIGNEDSERIAL = 0x6,
+ KASPDBKEY_OFFLINE_RECORDS = 0x7,
+ KASPDBKEY_SAVED_TTLS = 0x8,
+} keyclass_t;
+
+static const keyclass_t zone_related_classes[] = {
+ KASPDBKEY_PARAMS,
+ KASPDBKEY_NSEC3SALT,
+ KASPDBKEY_NSEC3TIME,
+ KASPDBKEY_MASTERSERIAL,
+ KASPDBKEY_LASTSIGNEDSERIAL,
+ KASPDBKEY_OFFLINE_RECORDS,
+ KASPDBKEY_SAVED_TTLS,
+};
+static const size_t zone_related_classes_size = sizeof(zone_related_classes) / sizeof(*zone_related_classes);
+
+static bool is_zone_related_class(uint8_t class)
+{
+ for (size_t i = 0; i < zone_related_classes_size; i++) {
+ if (zone_related_classes[i] == class) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool is_zone_related(const MDB_val *key)
+{
+ return is_zone_related_class(*(uint8_t *)key->mv_data);
+}
+
+static MDB_val make_key_str(keyclass_t kclass, const knot_dname_t *dname, const char *str)
+{
+ switch (kclass) {
+ case KASPDBKEY_POLICYLAST:
+ assert(dname == NULL && str != NULL);
+ return knot_lmdb_make_key("BS", (int)kclass, str);
+ case KASPDBKEY_NSEC3SALT:
+ case KASPDBKEY_NSEC3TIME:
+ case KASPDBKEY_LASTSIGNEDSERIAL:
+ case KASPDBKEY_MASTERSERIAL:
+ case KASPDBKEY_SAVED_TTLS:
+ assert(dname != NULL && str == NULL);
+ return knot_lmdb_make_key("BN", (int)kclass, dname);
+ case KASPDBKEY_PARAMS:
+ case KASPDBKEY_OFFLINE_RECORDS:
+ assert(dname != NULL);
+ if (str == NULL) {
+ return knot_lmdb_make_key("BN", (int)kclass, dname);
+ } else {
+ return knot_lmdb_make_key("BNS", (int)kclass, dname, str);
+ }
+ default:
+ assert(0);
+ MDB_val empty = { 0 };
+ return empty;
+ }
+}
+
+static MDB_val make_key_time(keyclass_t kclass, const knot_dname_t *dname, knot_time_t time)
+{
+ char tmp[21];
+ (void)snprintf(tmp, sizeof(tmp), "%0*"PRIu64, (int)(sizeof(tmp) - 1), time);
+ return make_key_str(kclass, dname, tmp);
+}
+
+static bool unmake_key_str(const MDB_val *keyv, char **str)
+{
+ uint8_t kclass;
+ const knot_dname_t *dname;
+ const char *s;
+ return (knot_lmdb_unmake_key(keyv->mv_data, keyv->mv_size, "BNS", &kclass, &dname, &s) &&
+ ((*str = strdup(s)) != NULL));
+}
+
+static bool unmake_key_time(const MDB_val *keyv, knot_time_t *time)
+{
+ uint8_t kclass;
+ const knot_dname_t *dname;
+ const char *s;
+ return (knot_lmdb_unmake_key(keyv->mv_data, keyv->mv_size, "BNS", &kclass, &dname, &s) &&
+ str_to_u64(s, time) == KNOT_EOK);
+}
+
+static MDB_val params_serialize(const key_params_t *params)
+{
+ uint8_t flags = 0x02;
+ flags |= (params->is_ksk ? 0x01 : 0);
+ flags |= (params->is_pub_only ? 0x04 : 0);
+ flags |= (params->is_csk ? 0x08 : 0);
+
+ return knot_lmdb_make_key("LLHBBLLLLLLLLLDL", (uint64_t)params->public_key.size,
+ (uint64_t)sizeof(params->timing.revoke), params->keytag, params->algorithm, flags,
+ params->timing.created, params->timing.pre_active, params->timing.publish,
+ params->timing.ready, params->timing.active, params->timing.retire_active,
+ params->timing.retire, params->timing.post_active, params->timing.remove,
+ params->public_key.data, params->public_key.size, params->timing.revoke);
+}
+
+// this is no longer compatible with keys created by Knot 2.5.x (and unmodified since)
+static bool params_deserialize(const MDB_val *val, key_params_t *params)
+{
+ if (val->mv_size < 2 * sizeof(uint64_t)) {
+ return false;
+ }
+ uint64_t keylen = knot_wire_read_u64(val->mv_data);
+ uint64_t future = knot_wire_read_u64(val->mv_data + sizeof(keylen));
+ uint8_t flags;
+
+ if ((params->public_key.data = malloc(keylen)) == NULL) {
+ return false;
+ }
+
+ if (knot_lmdb_unmake_key(val->mv_data, val->mv_size - future, "LLHBBLLLLLLLLLD",
+ &keylen, &future, &params->keytag, &params->algorithm, &flags,
+ &params->timing.created, &params->timing.pre_active, &params->timing.publish,
+ &params->timing.ready, &params->timing.active, &params->timing.retire_active,
+ &params->timing.retire, &params->timing.post_active, &params->timing.remove,
+ params->public_key.data, (size_t)keylen)) {
+
+ params->public_key.size = keylen;
+ params->is_ksk = ((flags & 0x01) ? true : false);
+ params->is_pub_only = ((flags & 0x04) ? true : false);
+ params->is_csk = ((flags & 0x08) ? true : false);
+
+ if (future > 0) {
+ if (future < sizeof(params->timing.revoke)) {
+ free(params->public_key.data);
+ params->public_key.data = NULL;
+ return false;
+ }
+ // 'revoked' timer is part of 'future' section since it was added later
+ params->timing.revoke = knot_wire_read_u64(val->mv_data + val->mv_size - future);
+ }
+
+ if ((flags & 0x02) && (params->is_ksk || !params->is_csk)) {
+ return true;
+ }
+ }
+ free(params->public_key.data);
+ params->public_key.data = NULL;
+ return false;
+}
+
+static key_params_t *txn2params(knot_lmdb_txn_t *txn)
+{
+ key_params_t *p = calloc(1, sizeof(*p));
+ if (p == NULL) {
+ txn->ret = KNOT_ENOMEM;
+ } else {
+ if (!params_deserialize(&txn->cur_val, p) ||
+ !unmake_key_str(&txn->cur_key, &p->id)) {
+ txn->ret = KNOT_EMALF;
+ free(p);
+ p = NULL;
+ }
+ }
+ return p;
+}
+
+int kasp_db_list_keys(knot_lmdb_db_t *db, const knot_dname_t *zone_name, list_t *dst)
+{
+ init_list(dst);
+ knot_lmdb_txn_t txn = { 0 };
+ MDB_val prefix = make_key_str(KASPDBKEY_PARAMS, zone_name, NULL);
+ knot_lmdb_begin(db, &txn, false);
+ knot_lmdb_foreach(&txn, &prefix) {
+ key_params_t *p = txn2params(&txn);
+ if (p != NULL) {
+ ptrlist_add(dst, p, NULL);
+ }
+ }
+ knot_lmdb_abort(&txn);
+ free(prefix.mv_data);
+ if (txn.ret != KNOT_EOK) {
+ ptrlist_deep_free(dst, NULL);
+ return txn.ret;
+ }
+ return (EMPTY_LIST(*dst) ? KNOT_ENOENT : KNOT_EOK);
+}
+
+int kasp_db_get_key_algorithm(knot_lmdb_db_t *db, const knot_dname_t *zone_name,
+ const char *key_id)
+{
+ knot_lmdb_txn_t txn = { 0 };
+ MDB_val search = make_key_str(KASPDBKEY_PARAMS, zone_name, key_id);
+ knot_lmdb_begin(db, &txn, false);
+ int ret = txn.ret == KNOT_EOK ? KNOT_ENOENT : txn.ret;
+ if (knot_lmdb_find(&txn, &search, KNOT_LMDB_EXACT)) {
+ key_params_t p = { 0 };
+ ret = params_deserialize(&txn.cur_val, &p) ? p.algorithm : KNOT_EMALF;
+ free(p.public_key.data);
+ }
+ knot_lmdb_abort(&txn);
+ free(search.mv_data);
+ return ret;
+}
+
+static bool keyid_inuse(knot_lmdb_txn_t *txn, const char *key_id, key_params_t **params)
+{
+ uint8_t pf = KASPDBKEY_PARAMS;
+ MDB_val prefix = { sizeof(pf), &pf };
+ knot_lmdb_foreach(txn, &prefix) {
+ char *found_id = NULL;
+ if (unmake_key_str(&txn->cur_key, &found_id) &&
+ strcmp(found_id, key_id) == 0) {
+ if (params != NULL) {
+ *params = txn2params(txn);
+ }
+ free(found_id);
+ return true;
+ }
+ free(found_id);
+ }
+ return false;
+}
+
+
+int kasp_db_delete_key(knot_lmdb_db_t *db, const knot_dname_t *zone_name, const char *key_id, bool *still_used)
+{
+ MDB_val search = make_key_str(KASPDBKEY_PARAMS, zone_name, key_id);
+ knot_lmdb_txn_t txn = { 0 };
+ knot_lmdb_begin(db, &txn, true);
+ knot_lmdb_del_prefix(&txn, &search);
+ if (still_used != NULL) {
+ *still_used = keyid_inuse(&txn, key_id, NULL);
+ }
+ knot_lmdb_commit(&txn);
+ free(search.mv_data);
+ return txn.ret;
+}
+
+
+int kasp_db_delete_all(knot_lmdb_db_t *db, const knot_dname_t *zone)
+{
+ MDB_val prefix = make_key_str(KASPDBKEY_PARAMS, zone, NULL);
+ knot_lmdb_txn_t txn = { 0 };
+ knot_lmdb_begin(db, &txn, true);
+ for (size_t i = 0; i < zone_related_classes_size && prefix.mv_data != NULL; i++) {
+ *(uint8_t *)prefix.mv_data = zone_related_classes[i];
+ knot_lmdb_del_prefix(&txn, &prefix);
+ }
+ knot_lmdb_commit(&txn);
+ free(prefix.mv_data);
+ return txn.ret;
+}
+
+int kasp_db_sweep(knot_lmdb_db_t *db, sweep_cb keep_zone, void *cb_data)
+{
+ if (knot_lmdb_exists(db) == KNOT_ENODB) {
+ return KNOT_EOK;
+ }
+ int ret = knot_lmdb_open(db);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ knot_lmdb_txn_t txn = { 0 };
+ knot_lmdb_begin(db, &txn, true);
+ knot_lmdb_forwhole(&txn) {
+ if (is_zone_related(&txn.cur_key) &&
+ !keep_zone((const knot_dname_t *)txn.cur_key.mv_data + 1, cb_data)) {
+ knot_lmdb_del_cur(&txn);
+ }
+ }
+ knot_lmdb_commit(&txn);
+ return txn.ret;
+}
+
+int kasp_db_list_zones(knot_lmdb_db_t *db, list_t *zones)
+{
+ if (knot_lmdb_exists(db) == KNOT_ENODB) {
+ return KNOT_EOK;
+ }
+ int ret = knot_lmdb_open(db);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ knot_lmdb_txn_t txn = { 0 };
+ knot_lmdb_begin(db, &txn, false);
+
+ uint8_t prefix_data = KASPDBKEY_PARAMS;
+ MDB_val prefix = { sizeof(prefix_data), &prefix_data };
+ knot_lmdb_foreach(&txn, &prefix) {
+ const knot_dname_t *found = txn.cur_key.mv_data + sizeof(prefix_data);
+ if (!knot_dname_is_equal(found, ((ptrnode_t *)TAIL(*zones))->d)) {
+ knot_dname_t *copy = knot_dname_copy(found, NULL);
+ if (copy == NULL || ptrlist_add(zones, copy, NULL) == NULL) {
+ free(copy);
+ ptrlist_deep_free(zones, NULL);
+ return KNOT_ENOMEM;
+ }
+ }
+ }
+ knot_lmdb_abort(&txn);
+ if (txn.ret != KNOT_EOK) {
+ ptrlist_deep_free(zones, NULL);
+ }
+ return txn.ret;
+}
+
+int kasp_db_add_key(knot_lmdb_db_t *db, const knot_dname_t *zone_name, const key_params_t *params)
+{
+ MDB_val v = params_serialize(params);
+ MDB_val k = make_key_str(KASPDBKEY_PARAMS, zone_name, params->id);
+ return knot_lmdb_quick_insert(db, k, v);
+}
+
+int kasp_db_share_key(knot_lmdb_db_t *db, const knot_dname_t *zone_from,
+ const knot_dname_t *zone_to, const char *key_id)
+{
+ MDB_val from = make_key_str(KASPDBKEY_PARAMS, zone_from, key_id);
+ MDB_val to = make_key_str(KASPDBKEY_PARAMS, zone_to, key_id);
+ knot_lmdb_txn_t txn = { 0 };
+ knot_lmdb_begin(db, &txn, true);
+ if (knot_lmdb_find(&txn, &from, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE)) {
+ knot_lmdb_insert(&txn, &to, &txn.cur_val);
+ }
+ knot_lmdb_commit(&txn);
+ free(from.mv_data);
+ free(to.mv_data);
+ return txn.ret;
+}
+
+int kasp_db_store_nsec3salt(knot_lmdb_db_t *db, const knot_dname_t *zone_name,
+ const dnssec_binary_t *nsec3salt, knot_time_t salt_created)
+{
+ MDB_val key = make_key_str(KASPDBKEY_NSEC3SALT, zone_name, NULL);
+ MDB_val val1 = { nsec3salt->size, nsec3salt->data };
+ uint64_t tmp = htobe64(salt_created);
+ MDB_val val2 = { sizeof(tmp), &tmp };
+ knot_lmdb_txn_t txn = { 0 };
+ knot_lmdb_begin(db, &txn, true);
+ knot_lmdb_insert(&txn, &key, &val1);
+ if (key.mv_data != NULL) {
+ *(uint8_t *)key.mv_data = KASPDBKEY_NSEC3TIME;
+ }
+ knot_lmdb_insert(&txn, &key, &val2);
+ knot_lmdb_commit(&txn);
+ free(key.mv_data);
+ return txn.ret;
+}
+
+int kasp_db_load_nsec3salt(knot_lmdb_db_t *db, const knot_dname_t *zone_name,
+ dnssec_binary_t *nsec3salt, knot_time_t *salt_created)
+{
+ MDB_val key = make_key_str(KASPDBKEY_NSEC3SALT, zone_name, NULL);
+ knot_lmdb_txn_t txn = { 0 };
+ knot_lmdb_begin(db, &txn, false);
+ if (nsec3salt != NULL) {
+ memset(nsec3salt, 0, sizeof(*nsec3salt));
+ if (knot_lmdb_find(&txn, &key, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE)) {
+ nsec3salt->size = txn.cur_val.mv_size;
+ nsec3salt->data = malloc(txn.cur_val.mv_size + 1); // +1 because it can be zero
+ if (nsec3salt->data == NULL) {
+ txn.ret = KNOT_ENOMEM;
+ } else {
+ memcpy(nsec3salt->data, txn.cur_val.mv_data, txn.cur_val.mv_size);
+ }
+ }
+ }
+ *(uint8_t *)key.mv_data = KASPDBKEY_NSEC3TIME;
+ if (knot_lmdb_find(&txn, &key, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE)) {
+ knot_lmdb_unmake_curval(&txn, "L", salt_created);
+ }
+ knot_lmdb_abort(&txn);
+ free(key.mv_data);
+ if (txn.ret != KNOT_EOK && nsec3salt != NULL) {
+ free(nsec3salt->data);
+ }
+ return txn.ret;
+}
+
+int kasp_db_store_serial(knot_lmdb_db_t *db, const knot_dname_t *zone_name,
+ kaspdb_serial_t serial_type, uint32_t serial)
+{
+ int ret = knot_lmdb_open(db);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ MDB_val k = make_key_str((keyclass_t)serial_type, zone_name, NULL);
+ MDB_val v = knot_lmdb_make_key("I", serial);
+ return knot_lmdb_quick_insert(db, k, v);
+}
+
+int kasp_db_load_serial(knot_lmdb_db_t *db, const knot_dname_t *zone_name,
+ kaspdb_serial_t serial_type, uint32_t *serial)
+{
+ if (knot_lmdb_exists(db) == KNOT_ENODB) {
+ return KNOT_ENOENT;
+ }
+ int ret = knot_lmdb_open(db);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ MDB_val k = make_key_str((keyclass_t)serial_type, zone_name, NULL);
+ knot_lmdb_txn_t txn = { 0 };
+ knot_lmdb_begin(db, &txn, false);
+ if (knot_lmdb_find(&txn, &k, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE)) {
+ knot_lmdb_unmake_curval(&txn, "I", serial);
+ }
+ knot_lmdb_abort(&txn);
+ free(k.mv_data);
+ return txn.ret;
+}
+
+int kasp_db_get_policy_last(knot_lmdb_db_t *db, const char *policy_string,
+ knot_dname_t **lp_zone, char **lp_keyid)
+{
+ MDB_val k = make_key_str(KASPDBKEY_POLICYLAST, NULL, policy_string);
+ uint8_t kclass = 0;
+ knot_lmdb_txn_t txn = { 0 };
+ *lp_zone = NULL;
+ *lp_keyid = NULL;
+ knot_lmdb_begin(db, &txn, false);
+ if (knot_lmdb_find(&txn, &k, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE) &&
+ knot_lmdb_unmake_curval(&txn, "BNS", &kclass, lp_zone, lp_keyid)) {
+ assert(*lp_zone != NULL && *lp_keyid != NULL);
+ *lp_zone = knot_dname_copy(*lp_zone, NULL);
+ *lp_keyid = strdup(*lp_keyid);
+ if (kclass != KASPDBKEY_PARAMS) {
+ txn.ret = KNOT_EMALF;
+ } else if (*lp_keyid == NULL || *lp_zone == NULL) {
+ txn.ret = KNOT_ENOMEM;
+ } else {
+ // check that the referenced key really exists
+ knot_lmdb_find(&txn, &txn.cur_val, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE);
+ }
+ }
+ knot_lmdb_abort(&txn);
+ free(k.mv_data);
+
+ return txn.ret;
+}
+
+int kasp_db_set_policy_last(knot_lmdb_db_t *db, const char *policy_string, const char *last_lp_keyid,
+ const knot_dname_t *new_lp_zone, const char *new_lp_keyid)
+{
+ MDB_val k = make_key_str(KASPDBKEY_POLICYLAST, NULL, policy_string);
+ knot_lmdb_txn_t txn = { 0 };
+ knot_lmdb_begin(db, &txn, true);
+ if (knot_lmdb_find(&txn, &k, KNOT_LMDB_EXACT)) {
+ // check that the last_lp_keyid matches
+ uint8_t unuse1, *unuse2;
+ const char *real_last_keyid;
+ if (knot_lmdb_unmake_curval(&txn, "BNS", &unuse1, &unuse2, &real_last_keyid) &&
+ (last_lp_keyid == NULL || strcmp(last_lp_keyid, real_last_keyid) != 0)) {
+ txn.ret = KNOT_ESEMCHECK;
+ }
+ }
+ MDB_val v = make_key_str(KASPDBKEY_PARAMS, new_lp_zone, new_lp_keyid);
+ knot_lmdb_insert(&txn, &k, &v);
+ free(k.mv_data);
+ free(v.mv_data);
+ knot_lmdb_commit(&txn);
+ return txn.ret;
+}
+
+int kasp_db_store_offline_records(knot_lmdb_db_t *db, knot_time_t for_time, const key_records_t *r)
+{
+ MDB_val k = make_key_time(KASPDBKEY_OFFLINE_RECORDS, r->rrsig.owner, for_time);
+ MDB_val v = { key_records_serialized_size(r), NULL };
+ knot_lmdb_txn_t txn = { 0 };
+ knot_lmdb_begin(db, &txn, true);
+ if (knot_lmdb_insert(&txn, &k, &v)) {
+ wire_ctx_t wire = wire_ctx_init(v.mv_data, v.mv_size);
+ txn.ret = key_records_serialize(&wire, r);
+ }
+ knot_lmdb_commit(&txn);
+ free(k.mv_data);
+ return txn.ret;
+}
+
+int kasp_db_load_offline_records(knot_lmdb_db_t *db, const knot_dname_t *for_dname,
+ knot_time_t *for_time, knot_time_t *next_time,
+ key_records_t *r)
+{
+ MDB_val prefix = make_key_str(KASPDBKEY_OFFLINE_RECORDS, for_dname, NULL);
+ if (prefix.mv_data == NULL) {
+ return KNOT_ENOMEM;
+ }
+ unsigned operator = KNOT_LMDB_GEQ;
+ MDB_val search = prefix;
+ bool zero_for_time = (*for_time == 0);
+ if (!zero_for_time) {
+ operator = KNOT_LMDB_LEQ;
+ search = make_key_time(KASPDBKEY_OFFLINE_RECORDS, for_dname, *for_time);
+ }
+ knot_lmdb_txn_t txn = { 0 };
+ knot_lmdb_begin(db, &txn, false);
+ if (knot_lmdb_find(&txn, &search, operator) &&
+ knot_lmdb_is_prefix_of(&prefix, &txn.cur_key)) {
+ wire_ctx_t wire = wire_ctx_init(txn.cur_val.mv_data, txn.cur_val.mv_size);
+ txn.ret = key_records_deserialize(&wire, r);
+ if (zero_for_time) {
+ unmake_key_time(&txn.cur_key, for_time);
+ }
+ if (!knot_lmdb_next(&txn) || !knot_lmdb_is_prefix_of(&prefix, &txn.cur_key) ||
+ !unmake_key_time(&txn.cur_key, next_time)) {
+ *next_time = 0;
+ }
+ } else if (txn.ret == KNOT_EOK) {
+ txn.ret = KNOT_ENOENT;
+ }
+ knot_lmdb_abort(&txn);
+ if (!zero_for_time) {
+ free(search.mv_data);
+ }
+ free(prefix.mv_data);
+ return txn.ret;
+}
+
+int kasp_db_delete_offline_records(knot_lmdb_db_t *db, const knot_dname_t *zone,
+ knot_time_t from_time, knot_time_t to_time)
+{
+ MDB_val prefix = make_key_str(KASPDBKEY_OFFLINE_RECORDS, zone, NULL);
+ knot_lmdb_txn_t txn = { 0 };
+ knot_lmdb_begin(db, &txn, true);
+ knot_lmdb_foreach(&txn, &prefix) {
+ knot_time_t found;
+ if (unmake_key_time(&txn.cur_key, &found) &&
+ knot_time_cmp(found, from_time) >= 0 &&
+ knot_time_cmp(found, to_time) <= 0) {
+ knot_lmdb_del_cur(&txn);
+ }
+ }
+ knot_lmdb_commit(&txn);
+ free(prefix.mv_data);
+ return txn.ret;
+}
+
+int kasp_db_get_saved_ttls(knot_lmdb_db_t *db, const knot_dname_t *zone,
+ uint32_t *max_ttl, uint32_t *key_ttl)
+{
+ MDB_val key = make_key_str(KASPDBKEY_SAVED_TTLS, zone, NULL);
+ knot_lmdb_txn_t txn = { 0 };
+ knot_lmdb_begin(db, &txn, false);
+ if (knot_lmdb_find(&txn, &key, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE)) {
+ knot_lmdb_unmake_curval(&txn, "II", max_ttl, key_ttl);
+ }
+ knot_lmdb_abort(&txn);
+ free(key.mv_data);
+ return txn.ret;
+}
+
+int kasp_db_set_saved_ttls(knot_lmdb_db_t *db, const knot_dname_t *zone,
+ uint32_t max_ttl, uint32_t key_ttl)
+{
+ MDB_val key = make_key_str(KASPDBKEY_SAVED_TTLS, zone, NULL);
+ MDB_val val = knot_lmdb_make_key("II", max_ttl, key_ttl);
+ return knot_lmdb_quick_insert(db, key, val);
+}
+
+void kasp_db_ensure_init(knot_lmdb_db_t *db, conf_t *conf)
+{
+ if (db->path == NULL) {
+ char *kasp_dir = conf_db(conf, C_KASP_DB);
+ conf_val_t kasp_size = conf_db_param(conf, C_KASP_DB_MAX_SIZE);
+ knot_lmdb_init(db, kasp_dir, conf_int(&kasp_size), 0, "keys_db");
+ free(kasp_dir);
+ assert(db->path != NULL);
+ }
+}
+
+int kasp_db_backup(const knot_dname_t *zone, knot_lmdb_db_t *db, knot_lmdb_db_t *backup_db)
+{
+ size_t n_prefs = zone_related_classes_size + 1; // NOTE: this and following must match number of record types
+ MDB_val prefixes[n_prefs];
+ prefixes[0] = knot_lmdb_make_key("B", KASPDBKEY_POLICYLAST); // we copy all policy-last records, that doesn't harm
+ for (size_t i = 1; i < n_prefs; i++) {
+ prefixes[i] = make_key_str(zone_related_classes[i - 1], zone, NULL);
+ }
+
+ int ret = knot_lmdb_copy_prefixes(db, backup_db, prefixes, n_prefs);
+
+ for (int i = 0; i < n_prefs; i++) {
+ free(prefixes[i].mv_data);
+ }
+ return ret;
+}
diff --git a/src/knot/dnssec/kasp/kasp_db.h b/src/knot/dnssec/kasp/kasp_db.h
new file mode 100644
index 0000000..e9eea4f
--- /dev/null
+++ b/src/knot/dnssec/kasp/kasp_db.h
@@ -0,0 +1,296 @@
+/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <time.h>
+
+#include "contrib/time.h"
+#include "contrib/ucw/lists.h"
+#include "libknot/db/db_lmdb.h"
+#include "libknot/dname.h"
+#include "knot/dnssec/kasp/policy.h"
+#include "knot/journal/knot_lmdb.h"
+
+typedef struct kasp_db kasp_db_t;
+
+typedef enum { // the enum values MUST match those from keyclass_t !!
+ KASPDB_SERIAL_MASTER = 0x5,
+ KASPDB_SERIAL_LASTSIGNED = 0x6,
+} kaspdb_serial_t;
+
+/*!
+ * \brief For given zone, list all keys (their IDs) belonging to it.
+ *
+ * \param db KASP db
+ * \param zone_name name of the zone in question
+ * \param dst output if KNOT_EOK: ptrlist of keys' params
+ *
+ * \return KNOT_E* (KNOT_ENOENT if no keys)
+ */
+int kasp_db_list_keys(knot_lmdb_db_t *db, const knot_dname_t *zone_name, list_t *dst);
+
+/*!
+ * \brief Obtain the algorithm of a key.
+ *
+ * \param db KASP db.
+ * \param zone_name name of the zone
+ * \param key_id ID of the key in question
+ *
+ * \retval KNOT_E* if error
+ * \return >0 The algorithm of the key.
+ */
+int kasp_db_get_key_algorithm(knot_lmdb_db_t *db, const knot_dname_t *zone_name,
+ const char *key_id);
+
+/*!
+ * \brief Remove a key from zone. Delete the key if no zone has it anymore.
+ *
+ * \param db KASP db
+ * \param zone_name zone to be removed from
+ * \param key_id ID of key to be removed
+ * \param still_used output if KNOT_EOK: is the key still in use by other zones?
+ *
+ * \return KNOT_E*
+ */
+int kasp_db_delete_key(knot_lmdb_db_t *db, const knot_dname_t *zone_name, const char *key_id, bool *still_used);
+
+/*!
+ * \brief Remove all zone's keys from DB, including nsec3param
+ *
+ * \param db KASP db
+ * \param zone_name zone to be removed
+ *
+ * \return KNOT_E*
+ */
+int kasp_db_delete_all(knot_lmdb_db_t *db, const knot_dname_t *zone_name);
+
+/*!
+ * \brief Selectively delete zones from the database.
+ *
+ * \param db KASP database.
+ * \param keep_zone Filtering callback.
+ * \param cb_data Data passed to callback function.
+ *
+ * \return KNOT_E*
+ */
+int kasp_db_sweep(knot_lmdb_db_t *db, sweep_cb keep_zone, void *cb_data);
+
+/*!
+ * \brief List all zones that have at least one key in KASP db.
+ *
+ * \param db KASP database.
+ * \param zones Output: ptrlist with zone names.
+ *
+ * \return KNOT_E*
+ */
+int kasp_db_list_zones(knot_lmdb_db_t *db, list_t *zones);
+
+/*!
+ * \brief Add a key to the DB (possibly overwrite) and link it to a zone.
+ *
+ * Stores new key with given params into KASP db. If a key with the same ID had been present
+ * in KASP db already, its params get silently overwritten by those new params.
+ * Moreover, the key ID is linked to the zone.
+ *
+ * \param db KASP db
+ * \param zone_name name of the zone the new key shall belong to
+ * \param params key params, incl. ID
+ *
+ * \return KNOT_E*
+ */
+int kasp_db_add_key(knot_lmdb_db_t *db, const knot_dname_t *zone_name, const key_params_t *params);
+
+/*!
+ * \brief Link a key from another zone.
+ *
+ * \param db KASP db
+ * \param zone_from name of the zone the key belongs to
+ * \param zone_to name of the zone the key shall belong to as well
+ * \param key_id ID of the key in question
+ *
+ * \return KNOT_E*
+ */
+int kasp_db_share_key(knot_lmdb_db_t *db, const knot_dname_t *zone_from,
+ const knot_dname_t *zone_to, const char *key_id);
+
+/*!
+ * \brief Store NSEC3 salt for given zone (possibly overwrites old salt).
+ *
+ * \param db KASP db
+ * \param zone_name zone name
+ * \param nsec3salt new NSEC3 salt
+ * \param salt_created timestamp when the salt was created
+ *
+ * \return KNOT_E*
+ */
+int kasp_db_store_nsec3salt(knot_lmdb_db_t *db, const knot_dname_t *zone_name,
+ const dnssec_binary_t *nsec3salt, knot_time_t salt_created);
+
+/*!
+ * \brief Load NSEC3 salt for given zone.
+ *
+ * \param db KASP db
+ * \param zone_name zone name
+ * \param nsec3salt output if KNOT_EOK: the zone's NSEC3 salt
+ * \param salt_created output if KNOT_EOK: timestamp when the salt was created
+ *
+ * \return KNOT_E* (KNOT_ENOENT if not stored before)
+ */
+int kasp_db_load_nsec3salt(knot_lmdb_db_t *db, const knot_dname_t *zone_name,
+ dnssec_binary_t *nsec3salt, knot_time_t *salt_created);
+
+/*!
+ * \brief Store SOA serial number of master or last signed serial.
+ *
+ * \param db KASP db
+ * \param zone_name zone name
+ * \param serial_type kind of serial to be stored
+ * \param serial new serial to be stored
+ *
+ * \return KNOT_E*
+ */
+int kasp_db_store_serial(knot_lmdb_db_t *db, const knot_dname_t *zone_name,
+ kaspdb_serial_t serial_type, uint32_t serial);
+
+/*!
+ * \brief Load saved SOA serial number of master or last signed serial.
+ *
+ * \param db KASP db
+ * \param zone_name zone name
+ * \param serial_type kind of serial to be loaded
+ * \param serial output if KNOT_EOK: desired serial number
+ *
+ * \return KNOT_E* (KNOT_ENOENT if not stored before)
+ */
+int kasp_db_load_serial(knot_lmdb_db_t *db, const knot_dname_t *zone_name,
+ kaspdb_serial_t serial_type, uint32_t *serial);
+
+/*!
+ * \brief For given policy name, obtain last generated key.
+ *
+ * \param db KASP db
+ * \param policy_string a name identifying the signing policy with shared keys
+ * \param lp_zone out: the zone owning the last generated key
+ * \param lp_keyid out: the ID of the last generated key
+ *
+ * \note lp_zone and lp_keyid must be freed even when an error is returned
+ *
+ * \return KNOT_E*
+ */
+int kasp_db_get_policy_last(knot_lmdb_db_t *db, const char *policy_string,
+ knot_dname_t **lp_zone, char **lp_keyid);
+
+/*!
+ * \brief For given policy name, try to reset last generated key.
+ *
+ * \param db KASP db
+ * \param policy_string a name identifying the signing policy with shared keys
+ * \param last_lp_keyid just for check: ID of the key the caller thinks is the policy-last
+ * \param new_lp_zone zone name of the new policy-last key
+ * \param new_lp_keyid ID of the new policy-last key
+ *
+ * \retval KNOT_ESEMCHECK lasp_lp_keyid does not correspond to real last key. Probably another zone
+ * changed policy-last key in the meantime. Re-run kasp_db_get_policy_last()
+ * \retval KNOT_EOK policy-last key set up successfully to given zone/ID
+ * \return KNOT_E* common error
+ */
+int kasp_db_set_policy_last(knot_lmdb_db_t *db, const char *policy_string, const char *last_lp_keyid,
+ const knot_dname_t *new_lp_zone, const char *new_lp_keyid);
+
+/*!
+ * \brief Store pre-generated records for offline KSK usage.
+ *
+ * \param db KASP db.
+ * \param for_time Timestamp in future in which the RRSIG shall be used.
+ * \param r Records to be stored.
+ *
+ * \return KNOT_E*
+ */
+int kasp_db_store_offline_records(knot_lmdb_db_t *db, knot_time_t for_time, const key_records_t *r);
+
+/*!
+ * \brief Load pregenerated records for offline signing.
+ *
+ * \param db KASP db.
+ * \param for_dname Name of the related zone.
+ * \param for_time Now. Closest RRSIG (timestamp equals or is closest lower).
+ * If zero, the first record is returned and its time is stored.
+ * \param next_time Out: timestamp of next saved RRSIG (for easy "iteration").
+ * \param r Out: offline records.
+ *
+ * \return KNOT_E*
+ */
+int kasp_db_load_offline_records(knot_lmdb_db_t *db, const knot_dname_t *for_dname,
+ knot_time_t *for_time, knot_time_t *next_time,
+ key_records_t *r);
+
+/*!
+ * \brief Delete pregenerated records for specified time interval.
+ *
+ * \param db KASP db.
+ * \param zone Zone in question.
+ * \param from_time Lower bound of the time interval (0 = infinity).
+ * \param to_time Upper bound of the time interval (0 = infinity).
+ *
+ * \return KNOT_E*
+ */
+int kasp_db_delete_offline_records(knot_lmdb_db_t *db, const knot_dname_t *zone,
+ knot_time_t from_time, knot_time_t to_time);
+
+/*!
+ * \brief Load saved zone-max-TTL and DNSKEY-TTL.
+ *
+ * \param db KASP db.
+ * \param max_ttl Out: saved zone max TTL.
+ * \param key_ttl Out: saved DNSKEY TTL.
+ *
+ * \retval KNOT_ENOENT If not saved yet.
+ * \return KNOT_E*
+ */
+int kasp_db_get_saved_ttls(knot_lmdb_db_t *db, const knot_dname_t *zone,
+ uint32_t *max_ttl, uint32_t *key_ttl);
+
+/*!
+ * \brief Save current zone-max-TTL and DNSKEY-TTL.
+ *
+ * \param db KASP db.
+ * \param max_ttl Current zone max TTL.
+ * \param key_ttl Current DNSKEY TTL.
+ *
+ * \return KNOT_E*
+ */
+int kasp_db_set_saved_ttls(knot_lmdb_db_t *db, const knot_dname_t *zone,
+ uint32_t max_ttl, uint32_t key_ttl);
+
+/*!
+ * \brief Initialize KASP database according to conf, if not already.
+ *
+ * \param db KASP DB to be initialized.
+ * \param conf COnfiguration to take options from.
+ */
+void kasp_db_ensure_init(knot_lmdb_db_t *db, conf_t *conf);
+
+/*!
+ * \brief Backup KASP DB for one zone with keys and all metadata to backup location.
+ *
+ * \param zone Name of the zone to be backed up.
+ * \param db DB to backup from.
+ * \param backup_db DB to backup to.
+ *
+ * \return KNOT_E*
+ */
+int kasp_db_backup(const knot_dname_t *zone, knot_lmdb_db_t *db, knot_lmdb_db_t *backup_db);
diff --git a/src/knot/dnssec/kasp/kasp_zone.c b/src/knot/dnssec/kasp/kasp_zone.c
new file mode 100644
index 0000000..58925fa
--- /dev/null
+++ b/src/knot/dnssec/kasp/kasp_zone.c
@@ -0,0 +1,447 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "knot/dnssec/kasp/kasp_zone.h"
+#include "knot/dnssec/kasp/keystore.h"
+#include "knot/dnssec/zone-keys.h"
+#include "libdnssec/binary.h"
+
+// FIXME DNSSEC errors versus knot errors
+
+/*!
+ * Check if key parameters allow to create a key.
+ */
+static int key_params_check(key_params_t *params)
+{
+ assert(params);
+
+ if (params->algorithm == 0) {
+ return KNOT_INVALID_KEY_ALGORITHM;
+ }
+
+ if (params->public_key.size == 0) {
+ return KNOT_NO_PUBLIC_KEY;
+ }
+
+ return KNOT_EOK;
+}
+
+/*! \brief Determine presence of SEP bit by trial-end-error using known keytag. */
+static int dnskey_guess_flags(dnssec_key_t *key, uint16_t keytag)
+{
+ dnssec_key_set_flags(key, DNSKEY_FLAGS_KSK);
+ if (dnssec_key_get_keytag(key) == keytag) {
+ return KNOT_EOK;
+ }
+
+ dnssec_key_set_flags(key, DNSKEY_FLAGS_ZSK);
+ if (dnssec_key_get_keytag(key) == keytag) {
+ return KNOT_EOK;
+ }
+
+ dnssec_key_set_flags(key, DNSKEY_FLAGS_REVOKED);
+ if (dnssec_key_get_keytag(key) == keytag) {
+ return KNOT_EOK;
+ }
+
+ return KNOT_EMALF;
+}
+
+static int params2dnskey(const knot_dname_t *dname, key_params_t *params,
+ dnssec_key_t **key_ptr)
+{
+ assert(dname);
+ assert(params);
+ assert(key_ptr);
+
+ int ret = key_params_check(params);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ dnssec_key_t *key = NULL;
+ ret = dnssec_key_new(&key);
+ if (ret != KNOT_EOK) {
+ return knot_error_from_libdnssec(ret);
+ }
+
+ ret = dnssec_key_set_dname(key, dname);
+ if (ret != KNOT_EOK) {
+ dnssec_key_free(key);
+ return knot_error_from_libdnssec(ret);
+ }
+
+ dnssec_key_set_algorithm(key, params->algorithm);
+
+ ret = dnssec_key_set_pubkey(key, &params->public_key);
+ if (ret != KNOT_EOK) {
+ dnssec_key_free(key);
+ return knot_error_from_libdnssec(ret);
+ }
+
+ ret = dnskey_guess_flags(key, params->keytag);
+ if (ret != KNOT_EOK) {
+ dnssec_key_free(key);
+ return ret;
+ }
+
+ *key_ptr = key;
+
+ return KNOT_EOK;
+}
+
+static int params2kaspkey(const knot_dname_t *dname, key_params_t *params,
+ knot_kasp_key_t *key)
+{
+ assert(dname != NULL);
+ assert(params != NULL);
+ assert(key != NULL);
+
+ int ret = params2dnskey(dname, params, &key->key);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ key->id = strdup(params->id);
+ if (key->id == NULL) {
+ dnssec_key_free(key->key);
+ return KNOT_ENOMEM;
+ }
+
+ key->timing = params->timing;
+ key->is_pub_only = params->is_pub_only;
+ assert(params->is_ksk || !params->is_csk);
+ key->is_ksk = params->is_ksk;
+ key->is_zsk = (params->is_csk || !params->is_ksk);
+ return KNOT_EOK;
+}
+
+static void kaspkey2params(knot_kasp_key_t *key, key_params_t *params)
+{
+ assert(key);
+ assert(params);
+
+ params->id = key->id;
+ params->keytag = dnssec_key_get_keytag(key->key);
+ dnssec_key_get_pubkey(key->key, &params->public_key);
+ params->algorithm = dnssec_key_get_algorithm(key->key);
+ params->is_ksk = key->is_ksk;
+ params->is_csk = (key->is_ksk && key->is_zsk);
+ params->timing = key->timing;
+ params->is_pub_only = key->is_pub_only;
+}
+
+static void detect_keytag_conflict(knot_kasp_zone_t *zone, bool *kt_cfl)
+{
+ *kt_cfl = false;
+ if (zone->num_keys == 0) {
+ return;
+ }
+ uint16_t keytags[zone->num_keys];
+ for (size_t i = 0; i < zone->num_keys; i++) {
+ keytags[i] = dnssec_key_get_keytag(zone->keys[i].key);
+ for (size_t j = 0; j < i; j++) {
+ if (keytags[j] == keytags[i]) {
+ *kt_cfl = true;
+ return;
+ }
+ }
+ }
+}
+
+int kasp_zone_load(knot_kasp_zone_t *zone,
+ const knot_dname_t *zone_name,
+ knot_lmdb_db_t *kdb,
+ bool *kt_cfl)
+{
+ if (zone == NULL || zone_name == NULL || kdb == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ knot_kasp_key_t *dkeys = NULL;
+ size_t num_dkeys = 0;
+ dnssec_binary_t salt = { 0 };
+ knot_time_t sc = 0;
+
+ list_t key_params;
+ init_list(&key_params);
+ int ret = kasp_db_list_keys(kdb, zone_name, &key_params);
+ if (ret == KNOT_ENOENT) {
+ zone->keys = NULL;
+ zone->num_keys = 0;
+ ret = KNOT_EOK;
+ goto kzl_salt;
+ } else if (ret != KNOT_EOK) {
+ goto kzl_end;
+ }
+
+ num_dkeys = list_size(&key_params);
+ dkeys = calloc(num_dkeys, sizeof(*dkeys));
+ if (dkeys == NULL) {
+ goto kzl_end;
+ }
+
+ ptrnode_t *n;
+ int i = 0;
+ WALK_LIST(n, key_params) {
+ key_params_t *parm = n->d;
+ ret = params2kaspkey(zone_name, parm, &dkeys[i++]);
+ free_key_params(parm);
+ if (ret != KNOT_EOK) {
+ goto kzl_end;
+ }
+ }
+
+kzl_salt:
+ (void)kasp_db_load_nsec3salt(kdb, zone_name, &salt, &sc);
+ // if error, salt was probably not present, no problem to have zero ?
+
+ zone->dname = knot_dname_copy(zone_name, NULL);
+ if (zone->dname == NULL) {
+ ret = KNOT_ENOMEM;
+ goto kzl_end;
+ }
+ zone->keys = dkeys;
+ zone->num_keys = num_dkeys;
+ zone->nsec3_salt = salt;
+ zone->nsec3_salt_created = sc;
+
+ detect_keytag_conflict(zone, kt_cfl);
+
+kzl_end:
+ ptrlist_deep_free(&key_params, NULL);
+ if (ret != KNOT_EOK) {
+ free(dkeys);
+ }
+ return ret;
+}
+
+int kasp_zone_append(knot_kasp_zone_t *zone, const knot_kasp_key_t *appkey)
+{
+ if (zone == NULL || appkey == NULL || (zone->keys == NULL && zone->num_keys > 0)) {
+ return KNOT_EINVAL;
+ }
+
+ size_t new_num_keys = zone->num_keys + 1;
+ knot_kasp_key_t *new_keys = calloc(new_num_keys, sizeof(*new_keys));
+ if (!new_keys) {
+ return KNOT_ENOMEM;
+ }
+ if (zone->num_keys > 0) {
+ memcpy(new_keys, zone->keys, zone->num_keys * sizeof(*new_keys));
+ }
+ memcpy(&new_keys[new_num_keys - 1], appkey, sizeof(*appkey));
+ free(zone->keys);
+ zone->keys = new_keys;
+ zone->num_keys = new_num_keys;
+ return KNOT_EOK;
+}
+
+int kasp_zone_save(const knot_kasp_zone_t *zone,
+ const knot_dname_t *zone_name,
+ knot_lmdb_db_t *kdb)
+{
+ if (zone == NULL || zone_name == NULL || kdb == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ key_params_t parm;
+ for (size_t i = 0; i < zone->num_keys; i++) {
+ kaspkey2params(&zone->keys[i], &parm);
+
+ // Force overwrite already existing key-val pairs.
+ int ret = kasp_db_add_key(kdb, zone_name, &parm);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+
+ return kasp_db_store_nsec3salt(kdb, zone_name, &zone->nsec3_salt,
+ zone->nsec3_salt_created);
+}
+
+static void kasp_zone_clear_keys(knot_kasp_zone_t *zone)
+{
+ for (size_t i = 0; i < zone->num_keys; i++) {
+ dnssec_key_free(zone->keys[i].key);
+ free(zone->keys[i].id);
+ }
+ free(zone->keys);
+ zone->keys = NULL;
+ zone->num_keys = 0;
+}
+
+void kasp_zone_clear(knot_kasp_zone_t *zone)
+{
+ if (zone == NULL) {
+ return;
+ }
+ knot_dname_free(zone->dname, NULL);
+ kasp_zone_clear_keys(zone);
+ free(zone->nsec3_salt.data);
+ memset(zone, 0, sizeof(*zone));
+}
+
+void kasp_zone_free(knot_kasp_zone_t **zone)
+{
+ if (zone != NULL) {
+ kasp_zone_clear(*zone);
+ free(*zone);
+ *zone = NULL;
+ }
+}
+
+void free_key_params(key_params_t *parm)
+{
+ if (parm != NULL) {
+ free(parm->id);
+ dnssec_binary_free(&parm->public_key);
+ memset(parm, 0 , sizeof(*parm));
+ }
+}
+
+int zone_init_keystore(conf_t *conf, conf_val_t *policy_id,
+ dnssec_keystore_t **keystore, unsigned *backend, bool *key_label)
+{
+ char *zone_path = conf_db(conf, C_KASP_DB);
+ if (zone_path == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ conf_id_fix_default(policy_id);
+
+ conf_val_t keystore_id = conf_id_get(conf, C_POLICY, C_KEYSTORE, policy_id);
+ conf_id_fix_default(&keystore_id);
+
+ conf_val_t val = conf_id_get(conf, C_KEYSTORE, C_BACKEND, &keystore_id);
+ unsigned _backend = conf_opt(&val);
+
+ val = conf_id_get(conf, C_KEYSTORE, C_CONFIG, &keystore_id);
+ const char *config = conf_str(&val);
+
+ if (key_label != NULL) {
+ val = conf_id_get(conf, C_KEYSTORE, C_KEY_LABEL, &keystore_id);
+ *key_label = conf_bool(&val);
+ }
+
+ int ret = keystore_load(config, _backend, zone_path, keystore);
+
+ if (backend != NULL) {
+ *backend = _backend;
+ }
+
+ free(zone_path);
+ return ret;
+}
+
+int kasp_zone_keys_from_rr(knot_kasp_zone_t *zone,
+ const knot_rdataset_t *zone_dnskey,
+ bool policy_single_type_signing,
+ bool *keytag_conflict)
+{
+ if (zone == NULL || zone_dnskey == NULL || keytag_conflict == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ kasp_zone_clear_keys(zone);
+
+ zone->num_keys = zone_dnskey->count;
+ zone->keys = calloc(zone->num_keys, sizeof(*zone->keys));
+ if (zone->keys == NULL) {
+ zone->num_keys = 0;
+ return KNOT_ENOMEM;
+ }
+
+ knot_rdata_t *zkey = zone_dnskey->rdata;
+ for (int i = 0; i < zone->num_keys; i++) {
+ int ret = dnssec_key_from_rdata(&zone->keys[i].key, zone->dname,
+ zkey->data, zkey->len);
+ if (ret == KNOT_EOK) {
+ ret = dnssec_key_get_keyid(zone->keys[i].key, &zone->keys[i].id);
+ }
+ if (ret != KNOT_EOK) {
+ free(zone->keys);
+ zone->keys = NULL;
+ zone->num_keys = 0;
+ return ret;
+ }
+ zone->keys[i].is_pub_only = true;
+
+ zone->keys[i].is_ksk = (knot_dnskey_flags(zkey) == DNSKEY_FLAGS_KSK);
+ zone->keys[i].is_zsk = policy_single_type_signing || !zone->keys[i].is_ksk;
+
+ zone->keys[i].timing.publish = 1;
+ zone->keys[i].timing.active = 1;
+
+ zkey = knot_rdataset_next(zkey);
+ }
+
+ detect_keytag_conflict(zone, keytag_conflict);
+ return KNOT_EOK;
+}
+
+int kasp_zone_from_contents(knot_kasp_zone_t *zone,
+ const zone_contents_t *contents,
+ bool policy_single_type_signing,
+ bool policy_nsec3,
+ uint16_t *policy_nsec3_iters,
+ bool *keytag_conflict)
+{
+ if (zone == NULL || contents == NULL || contents->apex == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ memset(zone, 0, sizeof(*zone));
+ zone->dname = knot_dname_copy(contents->apex->owner, NULL);
+ if (zone->dname == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ knot_rdataset_t *zone_dnskey = node_rdataset(contents->apex, KNOT_RRTYPE_DNSKEY);
+ if (zone_dnskey == NULL || zone_dnskey->count < 1) {
+ free(zone->dname);
+ return KNOT_DNSSEC_ENOKEY;
+ }
+
+ int ret = kasp_zone_keys_from_rr(zone, zone_dnskey, policy_single_type_signing, keytag_conflict);
+ if (ret != KNOT_EOK) {
+ free(zone->dname);
+ return ret;
+ }
+
+ zone->nsec3_salt_created = 0;
+ if (policy_nsec3) {
+ knot_rdataset_t *zone_ns3p = node_rdataset(contents->apex, KNOT_RRTYPE_NSEC3PARAM);
+ if (zone_ns3p == NULL || zone_ns3p->count != 1) {
+ kasp_zone_clear(zone);
+ return KNOT_ENSEC3PAR;
+ }
+ zone->nsec3_salt.size = knot_nsec3param_salt_len(zone_ns3p->rdata);
+ zone->nsec3_salt.data = malloc(zone->nsec3_salt.size);
+ if (zone->nsec3_salt.data == NULL) {
+ kasp_zone_clear(zone);
+ return KNOT_ENOMEM;
+ }
+ memcpy(zone->nsec3_salt.data,
+ knot_nsec3param_salt(zone_ns3p->rdata),
+ zone->nsec3_salt.size);
+
+ *policy_nsec3_iters = knot_nsec3param_iters(zone_ns3p->rdata);
+ }
+
+ return KNOT_EOK;
+}
diff --git a/src/knot/dnssec/kasp/kasp_zone.h b/src/knot/dnssec/kasp/kasp_zone.h
new file mode 100644
index 0000000..c4df282
--- /dev/null
+++ b/src/knot/dnssec/kasp/kasp_zone.h
@@ -0,0 +1,63 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/dnssec/kasp/kasp_db.h"
+#include "knot/zone/contents.h"
+#include "libdnssec/keystore.h"
+
+typedef struct {
+ knot_dname_t *dname;
+
+ knot_kasp_key_t *keys;
+ size_t num_keys;
+
+ dnssec_binary_t nsec3_salt;
+ knot_time_t nsec3_salt_created;
+} knot_kasp_zone_t;
+
+int kasp_zone_load(knot_kasp_zone_t *zone,
+ const knot_dname_t *zone_name,
+ knot_lmdb_db_t *kdb,
+ bool *kt_cfl);
+
+int kasp_zone_save(const knot_kasp_zone_t *zone,
+ const knot_dname_t *zone_name,
+ knot_lmdb_db_t *kdb);
+
+int kasp_zone_append(knot_kasp_zone_t *zone,
+ const knot_kasp_key_t *appkey);
+
+void kasp_zone_clear(knot_kasp_zone_t *zone);
+void kasp_zone_free(knot_kasp_zone_t **zone);
+
+void free_key_params(key_params_t *parm);
+
+int zone_init_keystore(conf_t *conf, conf_val_t *policy_id,
+ dnssec_keystore_t **keystore, unsigned *backend, bool *key_label);
+
+int kasp_zone_keys_from_rr(knot_kasp_zone_t *zone,
+ const knot_rdataset_t *zone_dnskey,
+ bool policy_single_type_signing,
+ bool *keytag_conflict);
+
+int kasp_zone_from_contents(knot_kasp_zone_t *zone,
+ const zone_contents_t *contents,
+ bool policy_single_type_signing,
+ bool policy_nsec3,
+ uint16_t *policy_nsec3_iters,
+ bool *keytag_conflict);
diff --git a/src/knot/dnssec/kasp/keystate.c b/src/knot/dnssec/kasp/keystate.c
new file mode 100644
index 0000000..f1eaa54
--- /dev/null
+++ b/src/knot/dnssec/kasp/keystate.c
@@ -0,0 +1,74 @@
+/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "knot/dnssec/kasp/keystate.h"
+
+key_state_t get_key_state(const knot_kasp_key_t *key, knot_time_t moment)
+{
+ if (!key || moment <= 0) {
+ return DNSSEC_KEY_STATE_INVALID;
+ }
+
+ const knot_kasp_key_timing_t *t = &key->timing;
+
+ bool removed = (knot_time_cmp(t->remove, moment) <= 0);
+ bool revoked = (knot_time_cmp(t->revoke, moment) <= 0);
+ bool post_active = (knot_time_cmp(t->post_active, moment) <= 0);
+ bool retired = (knot_time_cmp(t->retire, moment) <= 0);
+ bool retire_active = (knot_time_cmp(t->retire_active, moment) <= 0);
+ bool active = (knot_time_cmp(t->active, moment) <= 0);
+ bool ready = (knot_time_cmp(t->ready, moment) <= 0);
+ bool published = (knot_time_cmp(t->publish, moment) <= 0);
+ bool pre_active = (knot_time_cmp(t->pre_active, moment) <= 0);
+ bool created = (knot_time_cmp(t->created, moment) <= 0);
+
+ if (removed) {
+ return DNSSEC_KEY_STATE_REMOVED;
+ }
+ if (revoked) {
+ return DNSSEC_KEY_STATE_REVOKED;
+ }
+ if (post_active) {
+ if (retired) {
+ return DNSSEC_KEY_STATE_INVALID;
+ } else {
+ return DNSSEC_KEY_STATE_POST_ACTIVE;
+ }
+ }
+ if (retired) {
+ return DNSSEC_KEY_STATE_RETIRED;
+ }
+ if (retire_active) {
+ return DNSSEC_KEY_STATE_RETIRE_ACTIVE;
+ }
+ if (active) {
+ return DNSSEC_KEY_STATE_ACTIVE;
+ }
+ if (ready) {
+ return DNSSEC_KEY_STATE_READY;
+ }
+ if (published) {
+ return DNSSEC_KEY_STATE_PUBLISHED;
+ }
+ if (pre_active) {
+ return DNSSEC_KEY_STATE_PRE_ACTIVE;
+ }
+ if (created) {
+ // don't care
+ }
+
+ return DNSSEC_KEY_STATE_INVALID;
+}
diff --git a/src/knot/dnssec/kasp/keystate.h b/src/knot/dnssec/kasp/keystate.h
new file mode 100644
index 0000000..6b7d398
--- /dev/null
+++ b/src/knot/dnssec/kasp/keystate.h
@@ -0,0 +1,35 @@
+/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "contrib/time.h"
+#include "knot/dnssec/kasp/policy.h"
+
+typedef enum {
+ DNSSEC_KEY_STATE_INVALID = 0,
+ DNSSEC_KEY_STATE_PRE_ACTIVE,
+ DNSSEC_KEY_STATE_PUBLISHED,
+ DNSSEC_KEY_STATE_READY,
+ DNSSEC_KEY_STATE_ACTIVE,
+ DNSSEC_KEY_STATE_RETIRE_ACTIVE,
+ DNSSEC_KEY_STATE_RETIRED,
+ DNSSEC_KEY_STATE_POST_ACTIVE,
+ DNSSEC_KEY_STATE_REVOKED,
+ DNSSEC_KEY_STATE_REMOVED,
+} key_state_t;
+
+key_state_t get_key_state(const knot_kasp_key_t *key, knot_time_t moment);
diff --git a/src/knot/dnssec/kasp/keystore.c b/src/knot/dnssec/kasp/keystore.c
new file mode 100644
index 0000000..2ec5cd1
--- /dev/null
+++ b/src/knot/dnssec/kasp/keystore.c
@@ -0,0 +1,89 @@
+/* Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "libdnssec/error.h"
+#include "knot/dnssec/kasp/keystore.h"
+#include "knot/conf/schema.h"
+#include "libknot/error.h"
+
+static char *fix_path(const char *config, const char *base_path)
+{
+ assert(config);
+ assert(base_path);
+
+ char *path = NULL;
+
+ if (config[0] == '/') {
+ path = strdup(config);
+ } else {
+ if (asprintf(&path, "%s/%s", base_path, config) == -1) {
+ path = NULL;
+ }
+ }
+
+ return path;
+}
+
+int keystore_load(const char *config, unsigned backend,
+ const char *kasp_base_path, dnssec_keystore_t **keystore)
+{
+ int ret = DNSSEC_EINVAL;
+ char *fixed_config = NULL;
+
+ switch (backend) {
+ case KEYSTORE_BACKEND_PEM:
+ ret = dnssec_keystore_init_pkcs8(keystore);
+ fixed_config = fix_path(config, kasp_base_path);
+ break;
+ case KEYSTORE_BACKEND_PKCS11:
+ ret = dnssec_keystore_init_pkcs11(keystore);
+ fixed_config = strdup(config);
+ break;
+ default:
+ assert(0);
+ }
+ if (ret != DNSSEC_EOK) {
+ free(fixed_config);
+ return knot_error_from_libdnssec(ret);
+ }
+ if (fixed_config == NULL) {
+ dnssec_keystore_deinit(*keystore);
+ *keystore = NULL;
+ return KNOT_ENOMEM;
+ }
+
+ ret = dnssec_keystore_init(*keystore, fixed_config);
+ if (ret != DNSSEC_EOK) {
+ free(fixed_config);
+ dnssec_keystore_deinit(*keystore);
+ *keystore = NULL;
+ return knot_error_from_libdnssec(ret);
+ }
+
+ ret = dnssec_keystore_open(*keystore, fixed_config);
+ free(fixed_config);
+ if (ret != DNSSEC_EOK) {
+ dnssec_keystore_deinit(*keystore);
+ *keystore = NULL;
+ return knot_error_from_libdnssec(ret);
+ }
+
+ return KNOT_EOK;
+}
diff --git a/src/knot/dnssec/kasp/keystore.h b/src/knot/dnssec/kasp/keystore.h
new file mode 100644
index 0000000..bd62347
--- /dev/null
+++ b/src/knot/dnssec/kasp/keystore.h
@@ -0,0 +1,22 @@
+/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "libdnssec/keystore.h"
+
+int keystore_load(const char *config, unsigned backend,
+ const char *kasp_base_path, dnssec_keystore_t **keystore);
diff --git a/src/knot/dnssec/kasp/policy.h b/src/knot/dnssec/kasp/policy.h
new file mode 100644
index 0000000..4354b95
--- /dev/null
+++ b/src/knot/dnssec/kasp/policy.h
@@ -0,0 +1,135 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+
+#include "contrib/time.h"
+#include "libdnssec/key.h"
+#include "knot/conf/conf.h"
+
+/*!
+ * KASP key timing information.
+ */
+typedef struct {
+ knot_time_t created; /*!< Time the key was generated/imported. */
+ knot_time_t pre_active; /*!< Signing start with new algorithm. */
+ knot_time_t publish; /*!< Time of DNSKEY record publication. */
+ knot_time_t ready; /*!< Start of RRSIG generation, waiting for parent zone. */
+ knot_time_t active; /*!< RRSIG records generating, other keys can be retired */
+ knot_time_t retire_active; /*!< Still active, but obsoleted. */
+ knot_time_t retire; /*!< End of RRSIG records generating. */
+ knot_time_t post_active; /*!< Still signing with old algorithm, not published. */
+ knot_time_t revoke; /*!< RFC 5011 state of KSK with 'revoked' flag and signed by self. */
+ knot_time_t remove; /*!< Time of DNSKEY record removal. */
+} knot_kasp_key_timing_t;
+
+/*!
+ * Key parameters as writing in zone config file.
+ */
+typedef struct {
+ char *id;
+ bool is_ksk;
+ bool is_csk;
+ bool is_pub_only;
+ uint16_t keytag;
+ uint8_t algorithm;
+ dnssec_binary_t public_key;
+ knot_kasp_key_timing_t timing;
+} key_params_t;
+
+/*!
+ * Zone key.
+ */
+typedef struct {
+ char *id; /*!< Keystore unique key ID. */
+ dnssec_key_t *key; /*!< Instance of the key. */
+ knot_kasp_key_timing_t timing; /*!< Key timing information. */
+ bool is_pub_only;
+ bool is_ksk;
+ bool is_zsk;
+} knot_kasp_key_t;
+
+/*!
+ * Parent for DS checks.
+ */
+typedef struct {
+ conf_remote_t *addr;
+ size_t addrs;
+} knot_kasp_parent_t;
+
+knot_dynarray_declare(parent, knot_kasp_parent_t, DYNARRAY_VISIBILITY_NORMAL, 3)
+
+/*!
+ * Set of DNSSEC key related records.
+ */
+typedef struct {
+ knot_rrset_t dnskey;
+ knot_rrset_t cdnskey;
+ knot_rrset_t cds;
+ knot_rrset_t rrsig;
+} key_records_t;
+
+/*!
+ * Key and signature policy.
+ */
+typedef struct {
+ bool manual;
+ char *string;
+ // DNSKEY
+ dnssec_key_algorithm_t algorithm;
+ uint16_t ksk_size;
+ uint16_t zsk_size;
+ uint32_t dnskey_ttl;
+ uint32_t zsk_lifetime; // like knot_time_t
+ uint32_t ksk_lifetime; // like knot_time_t
+ uint32_t delete_delay; // like knot_timediff_t
+ bool ksk_shared;
+ bool single_type_signing;
+ bool sts_default; // single-type-signing was set to default value
+ // RRSIG
+ bool reproducible_sign; // (EC)DSA creates reproducible signatures
+ uint32_t rrsig_lifetime; // like knot_time_t
+ uint32_t rrsig_refresh_before; // like knot_timediff_t
+ uint32_t rrsig_prerefresh; // like knot_timediff_t
+ // NSEC3
+ bool nsec3_enabled;
+ bool nsec3_opt_out;
+ int64_t nsec3_salt_lifetime; // like knot_time_t
+ uint16_t nsec3_iterations;
+ uint8_t nsec3_salt_length;
+ // zone
+ uint32_t zone_maximal_ttl; // like knot_timediff_t
+ uint32_t saved_max_ttl;
+ uint32_t saved_key_ttl;
+ // data propagation delay
+ uint32_t propagation_delay; // like knot_timediff_t
+ // various
+ uint32_t ksk_sbm_timeout; // like knot_time_t
+ uint32_t ksk_sbm_check_interval; // like knot_time_t
+ uint32_t ksk_sbm_delay;
+ unsigned cds_cdnskey_publish;
+ dnssec_key_digest_t cds_dt; // digest type for CDS
+ parent_dynarray_t parents;
+ uint16_t signing_threads;
+ bool ds_push;
+ bool offline_ksk;
+ bool incremental;
+ bool key_label;
+ unsigned unsafe;
+} knot_kasp_policy_t;
+// TODO make the time parameters knot_timediff_t ??
diff --git a/src/knot/dnssec/key-events.c b/src/knot/dnssec/key-events.c
new file mode 100644
index 0000000..170f5a9
--- /dev/null
+++ b/src/knot/dnssec/key-events.c
@@ -0,0 +1,863 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include "contrib/macros.h"
+#include "knot/common/log.h"
+#include "knot/common/systemd.h"
+#include "knot/dnssec/kasp/keystate.h"
+#include "knot/dnssec/key-events.h"
+#include "knot/dnssec/policy.h"
+#include "knot/dnssec/zone-keys.h"
+
+static bool key_present(const kdnssec_ctx_t *ctx, bool ksk, bool zsk)
+{
+ assert(ctx);
+ assert(ctx->zone);
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ const knot_kasp_key_t *key = &ctx->zone->keys[i];
+ if (key->is_ksk == ksk && key->is_zsk == zsk && !key->is_pub_only &&
+ get_key_state(key, ctx->now) != DNSSEC_KEY_STATE_REMOVED) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool key_id_present(const kdnssec_ctx_t *ctx, const char *keyid, bool want_ksk)
+{
+ assert(ctx);
+ assert(ctx->zone);
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ const knot_kasp_key_t *key = &ctx->zone->keys[i];
+ if (strcmp(keyid, key->id) == 0 &&
+ key->is_ksk == want_ksk &&
+ get_key_state(key, ctx->now) != DNSSEC_KEY_STATE_REMOVED) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static unsigned algorithm_present(const kdnssec_ctx_t *ctx, uint8_t alg)
+{
+ assert(ctx);
+ assert(ctx->zone);
+ unsigned ret = 0;
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ const knot_kasp_key_t *key = &ctx->zone->keys[i];
+ knot_time_t activated = knot_time_min(key->timing.pre_active, key->timing.ready);
+ if (knot_time_cmp(knot_time_min(activated, key->timing.active), ctx->now) <= 0 &&
+ get_key_state(key, ctx->now) != DNSSEC_KEY_STATE_REMOVED &&
+ dnssec_key_get_algorithm(key->key) == alg && !key->is_pub_only) {
+ ret++;
+ }
+ }
+ return ret;
+}
+
+static bool signing_scheme_present(const kdnssec_ctx_t *ctx)
+{
+ if (ctx->policy->single_type_signing) {
+ return (!key_present(ctx, true, false) || !key_present(ctx, false, true) || key_present(ctx, true, true));
+ } else {
+ return (key_present(ctx, true, false) && key_present(ctx, false, true));
+ }
+}
+
+static knot_kasp_key_t *key_get_by_id(kdnssec_ctx_t *ctx, const char *keyid)
+{
+ assert(ctx);
+ assert(ctx->zone);
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *key = &ctx->zone->keys[i];
+ if (strcmp(keyid, key->id) == 0) {
+ return key;
+ }
+ }
+ return NULL;
+}
+
+static int generate_key(kdnssec_ctx_t *ctx, kdnssec_generate_flags_t flags,
+ knot_time_t when_active, bool pre_active)
+{
+ assert(!pre_active || when_active == 0);
+
+ knot_kasp_key_t *key = NULL;
+ int ret = kdnssec_generate_key(ctx, flags, &key);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ key->timing.remove = 0;
+ key->timing.retire = 0;
+ key->timing.active = ((flags & DNSKEY_GENERATE_KSK) ? 0 : when_active);
+ key->timing.ready = ((flags & DNSKEY_GENERATE_KSK) ? when_active : 0);
+ key->timing.publish = (pre_active ? 0 : ctx->now);
+ key->timing.pre_active = (pre_active ? ctx->now : 0);
+
+ return KNOT_EOK;
+}
+
+static int share_or_generate_key(kdnssec_ctx_t *ctx, kdnssec_generate_flags_t flags,
+ knot_time_t when_active, bool pre_active)
+{
+ assert(!pre_active || when_active == 0);
+
+ knot_dname_t *borrow_zone = NULL;
+ char *borrow_key = NULL;
+
+ if (!(flags & DNSKEY_GENERATE_KSK)) {
+ return KNOT_EINVAL;
+ } // for now not designed for rotating shared ZSK
+
+ int ret = kasp_db_get_policy_last(ctx->kasp_db, ctx->policy->string,
+ &borrow_zone, &borrow_key);
+ if (ret != KNOT_EOK && ret != KNOT_ENOENT) {
+ free(borrow_zone);
+ free(borrow_key);
+ return ret;
+ }
+
+ // if we already have the policy-last key, we have to generate new one
+ if (ret == KNOT_ENOENT || key_id_present(ctx, borrow_key, true) ||
+ kasp_db_get_key_algorithm(ctx->kasp_db, borrow_zone, borrow_key) != (int)ctx->policy->algorithm) {
+ knot_kasp_key_t *key = NULL;
+ ret = kdnssec_generate_key(ctx, flags, &key);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ key->timing.remove = 0;
+ key->timing.retire = 0;
+ key->timing.active = ((flags & DNSKEY_GENERATE_KSK) ? 0 : when_active);
+ key->timing.ready = ((flags & DNSKEY_GENERATE_KSK) ? when_active : 0);
+ key->timing.publish = (pre_active ? 0 : ctx->now);
+ key->timing.pre_active = (pre_active ? ctx->now : 0);
+
+ ret = kdnssec_ctx_commit(ctx);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = kasp_db_set_policy_last(ctx->kasp_db, ctx->policy->string,
+ borrow_key, ctx->zone->dname, key->id);
+ free(borrow_zone);
+ free(borrow_key);
+ borrow_zone = NULL;
+ borrow_key = NULL;
+ if (ret != KNOT_ESEMCHECK) {
+ // all ok, we generated new kay and updated policy-last
+ return ret;
+ } else {
+ // another zone updated policy-last key in the meantime
+ ret = kdnssec_delete_key(ctx, key);
+ if (ret == KNOT_EOK) {
+ ret = kdnssec_ctx_commit(ctx);
+ }
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = kasp_db_get_policy_last(ctx->kasp_db, ctx->policy->string,
+ &borrow_zone, &borrow_key);
+ }
+ }
+
+ if (ret == KNOT_EOK) {
+ ret = kdnssec_share_key(ctx, borrow_zone, borrow_key);
+ if (ret == KNOT_EOK) {
+ knot_kasp_key_t *newkey = key_get_by_id(ctx, borrow_key);
+ assert(newkey != NULL);
+ newkey->timing.remove = 0;
+ newkey->timing.retire = 0;
+ newkey->timing.active = ((flags & DNSKEY_GENERATE_KSK) ? 0 : when_active);
+ newkey->timing.ready = ((flags & DNSKEY_GENERATE_KSK) ? when_active : 0);
+ newkey->timing.publish = (pre_active ? 0 : ctx->now);
+ newkey->timing.pre_active = (pre_active ? ctx->now : 0);
+ newkey->is_ksk = (flags & DNSKEY_GENERATE_KSK);
+ newkey->is_zsk = (flags & DNSKEY_GENERATE_ZSK);
+ }
+ }
+ free(borrow_zone);
+ free(borrow_key);
+ return ret;
+}
+
+#define GEN_KSK_FLAGS (DNSKEY_GENERATE_KSK | (ctx->policy->single_type_signing ? DNSKEY_GENERATE_ZSK : 0))
+
+static int generate_ksk(kdnssec_ctx_t *ctx, knot_time_t when_active, bool pre_active)
+{
+ if (ctx->policy->ksk_shared) {
+ return share_or_generate_key(ctx, GEN_KSK_FLAGS, when_active, pre_active);
+ } else {
+ return generate_key(ctx, GEN_KSK_FLAGS, when_active, pre_active);
+ }
+}
+
+static bool running_rollover(const kdnssec_ctx_t *ctx)
+{
+ bool res = false;
+ bool ready_ksk = false, active_ksk = false;
+
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *key = &ctx->zone->keys[i];
+ if (key->is_pub_only) {
+ continue;
+ }
+ switch (get_key_state(key, ctx->now)) {
+ case DNSSEC_KEY_STATE_PRE_ACTIVE:
+ res = true;
+ break;
+ case DNSSEC_KEY_STATE_PUBLISHED:
+ res = true;
+ break;
+ case DNSSEC_KEY_STATE_READY:
+ ready_ksk = (ready_ksk || key->is_ksk);
+ break;
+ case DNSSEC_KEY_STATE_ACTIVE:
+ active_ksk = (active_ksk || key->is_ksk);
+ break;
+ case DNSSEC_KEY_STATE_RETIRE_ACTIVE:
+ case DNSSEC_KEY_STATE_POST_ACTIVE:
+ res = true;
+ break;
+ case DNSSEC_KEY_STATE_RETIRED:
+ case DNSSEC_KEY_STATE_REMOVED:
+ default:
+ break;
+ }
+ }
+ if (ready_ksk && active_ksk) {
+ res = true;
+ }
+ return res;
+}
+
+typedef enum {
+ INVALID = 0,
+ GENERATE = 1,
+ PUBLISH,
+ SUBMIT,
+ REPLACE,
+ RETIRE,
+ REMOVE,
+ REALLY_REMOVE,
+} roll_action_type_t;
+
+typedef struct {
+ roll_action_type_t type;
+ bool ksk;
+ knot_time_t time;
+ knot_kasp_key_t *key;
+ uint16_t ready_keytag;
+ const char *ready_keyid;
+} roll_action_t;
+
+static const char *roll_action_name(roll_action_type_t type)
+{
+ switch (type) {
+ case GENERATE: return "generate";
+ case PUBLISH: return "publish";
+ case SUBMIT: return "submit";
+ case REPLACE: return "replace";
+ case RETIRE: return "retire";
+ case REMOVE: return "remove";
+ case INVALID:
+ // FALLTHROUGH
+ default: return "invalid";
+ }
+}
+
+static knot_time_t zsk_rollover_time(knot_time_t active_time, const kdnssec_ctx_t *ctx)
+{
+ if (active_time <= 0 || ctx->policy->zsk_lifetime == 0) {
+ return 0;
+ }
+ return knot_time_plus(active_time, ctx->policy->zsk_lifetime);
+}
+
+static knot_time_t zsk_active_time(knot_time_t publish_time, const kdnssec_ctx_t *ctx)
+{
+ if (publish_time <= 0) {
+ return 0;
+ }
+ return knot_time_add(publish_time, ctx->policy->propagation_delay + ctx->policy->saved_key_ttl);
+}
+
+static knot_time_t zsk_remove_time(knot_time_t retire_time, const kdnssec_ctx_t *ctx)
+{
+ if (retire_time <= 0) {
+ return 0;
+ }
+ return knot_time_add(retire_time, ctx->policy->propagation_delay + ctx->policy->saved_max_ttl);
+}
+
+static knot_time_t ksk_rollover_time(knot_time_t created_time, const kdnssec_ctx_t *ctx)
+{
+ if (created_time <= 0 || ctx->policy->ksk_lifetime == 0) {
+ return 0;
+ }
+ return knot_time_plus(created_time, ctx->policy->ksk_lifetime);
+}
+
+static knot_time_t ksk_ready_time(knot_time_t publish_time, const kdnssec_ctx_t *ctx)
+{
+ if (publish_time <= 0) {
+ return 0;
+ }
+ return knot_time_add(publish_time, ctx->policy->propagation_delay + ctx->policy->saved_key_ttl);
+}
+
+static knot_time_t ksk_sbm_max_time(knot_time_t ready_time, const kdnssec_ctx_t *ctx)
+{
+ if (ready_time <= 0 || ctx->policy->ksk_sbm_timeout == 0) {
+ return 0;
+ }
+ return knot_time_plus(ready_time, ctx->policy->ksk_sbm_timeout);
+}
+
+static knot_time_t ksk_retire_time(knot_time_t retire_active_time, const kdnssec_ctx_t *ctx)
+{
+ if (retire_active_time <= 0) {
+ return 0;
+ }
+ // this is not correct! It should be parent DS TTL.
+ return knot_time_add(retire_active_time, ctx->policy->propagation_delay + ctx->policy->saved_key_ttl);
+}
+
+static knot_time_t ksk_remove_time(knot_time_t retire_time, bool is_csk, const kdnssec_ctx_t *ctx)
+{
+ if (retire_time <= 0) {
+ return 0;
+ }
+ knot_timediff_t use_ttl = ctx->policy->saved_key_ttl;
+ if (is_csk) {
+ use_ttl = ctx->policy->saved_max_ttl;
+ }
+ return knot_time_add(retire_time, ctx->policy->propagation_delay + use_ttl);
+}
+
+static knot_time_t ksk_really_remove_time(knot_time_t remove_time, const kdnssec_ctx_t *ctx)
+{
+ if (ctx->keep_deleted_keys) {
+ return 0;
+ }
+ return knot_time_add(remove_time, ctx->policy->delete_delay);
+}
+
+static knot_time_t zsk_really_remove_time(knot_time_t remove_time, const kdnssec_ctx_t *ctx)
+{
+ if (ctx->keep_deleted_keys) {
+ return 0;
+ }
+ return knot_time_add(remove_time, ctx->policy->delete_delay);
+}
+
+// algorithm rollover related timers must be the same for KSK and ZSK
+
+static knot_time_t alg_publish_time(knot_time_t pre_active_time, const kdnssec_ctx_t *ctx)
+{
+ if (pre_active_time <= 0) {
+ return 0;
+ }
+ return knot_time_add(pre_active_time, ctx->policy->propagation_delay + ctx->policy->saved_max_ttl);
+}
+
+static knot_time_t alg_remove_time(knot_time_t post_active_time, const kdnssec_ctx_t *ctx)
+{
+ return knot_time_add(post_active_time, ctx->policy->propagation_delay + ctx->policy->saved_key_ttl);
+}
+
+static roll_action_t next_action(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags)
+{
+ roll_action_t res = { 0 };
+
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *key = &ctx->zone->keys[i];
+ knot_time_t keytime = 0;
+ roll_action_type_t restype = INVALID;
+ if (key->is_pub_only ||
+ (key->is_ksk && !(flags & KEY_ROLL_ALLOW_KSK_ROLL)) ||
+ (key->is_zsk && !(flags & KEY_ROLL_ALLOW_ZSK_ROLL))) {
+ continue;
+ }
+ if (key->is_ksk) {
+ switch (get_key_state(key, ctx->now)) {
+ case DNSSEC_KEY_STATE_PRE_ACTIVE:
+ keytime = alg_publish_time(key->timing.pre_active, ctx);
+ restype = PUBLISH;
+ break;
+ case DNSSEC_KEY_STATE_PUBLISHED:
+ keytime = ksk_ready_time(key->timing.publish, ctx);
+ restype = SUBMIT;
+ break;
+ case DNSSEC_KEY_STATE_READY:
+ keytime = ksk_sbm_max_time(key->timing.ready, ctx);
+ restype = REPLACE;
+ res.ready_keyid = key->id;
+ res.ready_keytag = dnssec_key_get_keytag(key->key);
+ break;
+ case DNSSEC_KEY_STATE_ACTIVE:
+ if (!running_rollover(ctx) &&
+ dnssec_key_get_algorithm(key->key) == ctx->policy->algorithm) {
+ knot_time_t ksk_created = key->timing.created == 0 ?
+ key->timing.active :
+ key->timing.created;
+ keytime = ksk_rollover_time(ksk_created, ctx);
+ restype = GENERATE;
+ }
+ break;
+ case DNSSEC_KEY_STATE_RETIRE_ACTIVE:
+ if (key->timing.retire == 0 && key->timing.post_active == 0 && key->timing.remove == 0) { // this shouldn't normally happen
+ // when a KSK is retire_active, it has already some following timer set
+ keytime = ksk_retire_time(key->timing.retire_active, ctx);
+ restype = RETIRE;
+ }
+ break;
+ case DNSSEC_KEY_STATE_POST_ACTIVE:
+ keytime = alg_remove_time(key->timing.post_active, ctx);
+ restype = REMOVE;
+ break;
+ case DNSSEC_KEY_STATE_RETIRED:
+ keytime = knot_time_min(key->timing.retire, key->timing.remove);
+ keytime = ksk_remove_time(keytime, key->is_zsk, ctx);
+ restype = REMOVE;
+ break;
+ case DNSSEC_KEY_STATE_REMOVED:
+ keytime = ksk_really_remove_time(key->timing.remove, ctx);
+ if (knot_time_cmp(keytime, ctx->now) > 0) {
+ keytime = 0;
+ }
+ restype = REALLY_REMOVE;
+ break;
+ default:
+ continue;
+ }
+ } else {
+ switch (get_key_state(key, ctx->now)) {
+ case DNSSEC_KEY_STATE_PRE_ACTIVE:
+ keytime = alg_publish_time(key->timing.pre_active, ctx);
+ restype = PUBLISH;
+ break;
+ case DNSSEC_KEY_STATE_PUBLISHED:
+ keytime = zsk_active_time(key->timing.publish, ctx);
+ restype = REPLACE;
+ break;
+ case DNSSEC_KEY_STATE_ACTIVE:
+ if (!running_rollover(ctx) &&
+ dnssec_key_get_algorithm(key->key) == ctx->policy->algorithm) {
+ keytime = zsk_rollover_time(key->timing.active, ctx);
+ restype = GENERATE;
+ }
+ break;
+ case DNSSEC_KEY_STATE_RETIRE_ACTIVE:
+ // simply waiting for submitted KSK to retire me.
+ break;
+ case DNSSEC_KEY_STATE_POST_ACTIVE:
+ keytime = alg_remove_time(key->timing.post_active, ctx);
+ restype = REMOVE;
+ break;
+ case DNSSEC_KEY_STATE_RETIRED:
+ keytime = knot_time_min(key->timing.retire, key->timing.remove);
+ keytime = zsk_remove_time(keytime, ctx);
+ restype = REMOVE;
+ break;
+ case DNSSEC_KEY_STATE_REMOVED:
+ keytime = zsk_really_remove_time(key->timing.remove, ctx);
+ if (knot_time_cmp(keytime, ctx->now) > 0) {
+ keytime = 0;
+ }
+ restype = REALLY_REMOVE;
+ break;
+ case DNSSEC_KEY_STATE_READY:
+ default:
+ continue;
+ }
+ }
+ if (knot_time_cmp(keytime, res.time) < 0) {
+ res.key = key;
+ res.ksk = key->is_ksk;
+ res.time = keytime;
+ res.type = restype;
+ }
+ }
+
+ return res;
+}
+
+static int submit_key(kdnssec_ctx_t *ctx, knot_kasp_key_t *newkey)
+{
+ assert(get_key_state(newkey, ctx->now) == DNSSEC_KEY_STATE_PUBLISHED);
+ assert(newkey->is_ksk);
+
+ // pushing from READY into ACTIVE decreases the other key's cds_priority
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *key = &ctx->zone->keys[i];
+ if (key->is_ksk && !key->is_pub_only &&
+ get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_READY) {
+ key->timing.active = ctx->now;
+ }
+ }
+
+ newkey->timing.ready = ctx->now;
+ return KNOT_EOK;
+}
+
+knot_kasp_key_t *knot_dnssec_key2retire(kdnssec_ctx_t *ctx, knot_kasp_key_t *newkey)
+{
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *key = &ctx->zone->keys[i];
+ key_state_t keystate = get_key_state(key, ctx->now);
+ if (((newkey->is_ksk && key->is_ksk) || (!newkey->is_ksk && !key->is_ksk))
+ && (keystate == DNSSEC_KEY_STATE_ACTIVE)) {
+ return key;
+ }
+ }
+ return NULL;
+}
+
+static knot_kasp_key_t *zsk2retire(kdnssec_ctx_t *ctx, knot_kasp_key_t *newksk)
+{
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *key = &ctx->zone->keys[i];
+ key_state_t keystate = get_key_state(key, ctx->now);
+ uint8_t keyalg = dnssec_key_get_algorithm(key->key);
+ bool algdiff = (keyalg != dnssec_key_get_algorithm(newksk->key));
+
+ if (key->is_zsk && !key->is_ksk &&
+ (algdiff || newksk->is_zsk) &&
+ (keystate == DNSSEC_KEY_STATE_ACTIVE ||
+ keystate == DNSSEC_KEY_STATE_RETIRE_ACTIVE)) {
+ return key;
+ }
+ }
+ return NULL;
+}
+
+static int exec_new_signatures(kdnssec_ctx_t *ctx, knot_kasp_key_t *newkey, uint32_t active_retire_delay)
+{
+ if (newkey->is_ksk) {
+ log_zone_notice(ctx->zone->dname, "DNSSEC, KSK submission, confirmed");
+ }
+
+ knot_kasp_key_t *oldkey = knot_dnssec_key2retire(ctx, newkey), *oldzsk = NULL;
+ if (oldkey != NULL) {
+ uint8_t keyalg = dnssec_key_get_algorithm(oldkey->key);
+ bool algdiff = (keyalg != dnssec_key_get_algorithm(newkey->key));
+
+ if (algdiff) {
+ oldkey->timing.retire_active = ctx->now;
+ if (oldkey->is_ksk) {
+ oldkey->timing.post_active = ctx->now + active_retire_delay;
+ }
+ } else if (oldkey->is_ksk) {
+ oldkey->timing.retire_active = ctx->now;
+ if (oldkey->is_zsk) { // CSK
+ oldkey->timing.retire = ctx->now + active_retire_delay;
+ } else {
+ oldkey->timing.remove = ctx->now + active_retire_delay;
+ }
+ } else {
+ oldkey->timing.retire = ctx->now;
+ }
+
+ if (newkey->is_ksk && (oldzsk = zsk2retire(ctx, newkey)) != NULL) {
+ if (algdiff) {
+ oldzsk->timing.post_active = ctx->now + active_retire_delay;
+ } else {
+ oldzsk->timing.retire = ctx->now;
+ }
+ }
+ }
+
+ if (newkey->is_ksk) {
+ assert(get_key_state(newkey, ctx->now) == DNSSEC_KEY_STATE_READY);
+ } else {
+ assert(get_key_state(newkey, ctx->now) == DNSSEC_KEY_STATE_PUBLISHED);
+ }
+ newkey->timing.active = knot_time_min(ctx->now, newkey->timing.active);
+
+ return KNOT_EOK;
+}
+
+static int exec_publish(kdnssec_ctx_t *ctx, knot_kasp_key_t *key)
+{
+ assert(get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_PRE_ACTIVE);
+ key->timing.publish = ctx->now;
+
+ return KNOT_EOK;
+}
+
+static int exec_ksk_retire(kdnssec_ctx_t *ctx, knot_kasp_key_t *key)
+{
+ bool alg_rollover = false;
+ knot_kasp_key_t *alg_rollover_friend = NULL;
+
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *k = &ctx->zone->keys[i];
+ int magic = (k->is_ksk && k->is_zsk ? 2 : 3); // :(
+ if (k->is_zsk && get_key_state(k, ctx->now) == DNSSEC_KEY_STATE_RETIRE_ACTIVE &&
+ algorithm_present(ctx, dnssec_key_get_algorithm(k->key)) < magic) {
+ alg_rollover = true;
+ alg_rollover_friend = k;
+ }
+ }
+
+ assert(get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_RETIRE_ACTIVE);
+
+ if (alg_rollover) {
+ key->timing.post_active = ctx->now;
+ alg_rollover_friend->timing.post_active = ctx->now;
+ } else {
+ key->timing.retire = ctx->now;
+ }
+
+ return KNOT_EOK;
+}
+
+static int exec_remove_old_key(kdnssec_ctx_t *ctx, knot_kasp_key_t *key)
+{
+ assert(get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_RETIRED ||
+ get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_POST_ACTIVE ||
+ get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_REMOVED);
+ key->timing.remove = ctx->now;
+ return KNOT_EOK;
+}
+
+static int exec_really_remove(kdnssec_ctx_t *ctx, knot_kasp_key_t *key)
+{
+ assert(get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_REMOVED);
+ assert(!ctx->keep_deleted_keys);
+ return kdnssec_delete_key(ctx, key);
+}
+
+int knot_dnssec_key_rollover(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags,
+ zone_sign_reschedule_t *reschedule)
+{
+ if (ctx == NULL || reschedule == NULL) {
+ return KNOT_EINVAL;
+ }
+ if (ctx->policy->manual) {
+ if ((flags & (KEY_ROLL_FORCE_KSK_ROLL | KEY_ROLL_FORCE_ZSK_ROLL))) {
+ log_zone_notice(ctx->zone->dname, "DNSSEC, ignoring forced key rollover "
+ "due to manual policy");
+ }
+ return KNOT_EOK;
+ }
+ int ret = KNOT_EOK;
+ uint16_t ready_keytag = 0;
+ const char *ready_keyid = NULL;
+ bool allowed_general_roll = ((flags & KEY_ROLL_ALLOW_KSK_ROLL) && (flags & KEY_ROLL_ALLOW_ZSK_ROLL));
+ // generate initial keys if missing
+ if (!key_present(ctx, true, false) && !key_present(ctx, true, true)) {
+ if ((flags & KEY_ROLL_ALLOW_KSK_ROLL)) {
+ if (ctx->policy->ksk_shared) {
+ ret = share_or_generate_key(ctx, GEN_KSK_FLAGS, ctx->now, false);
+ } else {
+ ret = generate_key(ctx, GEN_KSK_FLAGS, ctx->now, false);
+ }
+ if (ret == KNOT_EOK) {
+ reschedule->plan_ds_check = true;
+ ready_keyid = ctx->zone->keys[0].id;
+ ready_keytag = dnssec_key_get_keytag(ctx->zone->keys[0].key);
+ }
+ }
+ if (ret == KNOT_EOK && (flags & KEY_ROLL_ALLOW_ZSK_ROLL)) {
+ reschedule->keys_changed = true;
+ if (!ctx->policy->single_type_signing &&
+ !key_present(ctx, false, true)) {
+ ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, ctx->now, false);
+ }
+ }
+ }
+ // forced KSK rollover
+ if ((flags & KEY_ROLL_FORCE_KSK_ROLL) && ret == KNOT_EOK && (flags & KEY_ROLL_ALLOW_KSK_ROLL)) {
+ flags &= ~KEY_ROLL_FORCE_KSK_ROLL;
+ if (running_rollover(ctx)) {
+ log_zone_warning(ctx->zone->dname, "DNSSEC, ignoring forced KSK rollover "
+ "due to running rollover");
+ } else {
+ ret = generate_ksk(ctx, 0, false);
+ if (ret == KNOT_EOK) {
+ reschedule->keys_changed = true;
+ log_zone_info(ctx->zone->dname, "DNSSEC, KSK rollover started");
+ }
+ }
+ }
+ // forced ZSK rollover
+ if ((flags & KEY_ROLL_FORCE_ZSK_ROLL) && ret == KNOT_EOK && (flags & KEY_ROLL_ALLOW_ZSK_ROLL)) {
+ flags &= ~KEY_ROLL_FORCE_ZSK_ROLL;
+ if (running_rollover(ctx)) {
+ log_zone_warning(ctx->zone->dname, "DNSSEC, ignoring forced ZSK rollover "
+ "due to running rollover");
+ } else {
+ ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, 0, false);
+ if (ret == KNOT_EOK) {
+ reschedule->keys_changed = true;
+ log_zone_info(ctx->zone->dname, "DNSSEC, ZSK rollover started");
+ }
+ }
+ }
+ // algorithm rollover
+ if (algorithm_present(ctx, ctx->policy->algorithm) == 0 &&
+ !running_rollover(ctx) && allowed_general_roll && ret == KNOT_EOK) {
+ ret = generate_ksk(ctx, 0, true);
+ if (!ctx->policy->single_type_signing && ret == KNOT_EOK) {
+ ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, 0, true);
+ }
+ log_zone_info(ctx->zone->dname, "DNSSEC, algorithm rollover started");
+ if (ret == KNOT_EOK) {
+ reschedule->keys_changed = true;
+ }
+ }
+ // scheme rollover
+ if (!signing_scheme_present(ctx) && allowed_general_roll &&
+ !running_rollover(ctx) && ret == KNOT_EOK) {
+ ret = generate_ksk(ctx, 0, false);
+ if (!ctx->policy->single_type_signing && ret == KNOT_EOK) {
+ ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, 0, false);
+ }
+ log_zone_info(ctx->zone->dname, "DNSSEC, signing scheme rollover started");
+ if (ret == KNOT_EOK) {
+ reschedule->keys_changed = true;
+ }
+ }
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ roll_action_t next = next_action(ctx, flags);
+
+ reschedule->next_rollover = next.time;
+
+ if (knot_time_cmp(reschedule->next_rollover, ctx->now) <= 0) {
+ bool log_keytag = true;
+ switch (next.type) {
+ case GENERATE:
+ if (next.ksk) {
+ ret = generate_ksk(ctx, 0, false);
+ } else {
+ ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, 0, false);
+ }
+ if (ret == KNOT_EOK) {
+ log_zone_info(ctx->zone->dname, "DNSSEC, %cSK rollover started",
+ (next.ksk ? 'K' : 'Z'));
+ }
+ log_keytag = false;
+ break;
+ case PUBLISH:
+ ret = exec_publish(ctx, next.key);
+ break;
+ case SUBMIT:
+ ret = submit_key(ctx, next.key);
+ if (ret == KNOT_EOK) {
+ reschedule->plan_ds_check = true;
+ ready_keyid = next.key->id;
+ ready_keytag = dnssec_key_get_keytag(next.key->key);
+ }
+ break;
+ case REPLACE:
+ ret = exec_new_signatures(ctx, next.key, 0);
+ break;
+ case RETIRE:
+ ret = exec_ksk_retire(ctx, next.key);
+ break;
+ case REMOVE:
+ ret = exec_remove_old_key(ctx, next.key);
+ break;
+ case REALLY_REMOVE:
+ ret = exec_really_remove(ctx, next.key);
+ break;
+ default:
+ log_keytag = false;
+ ret = KNOT_EINVAL;
+ }
+
+ if (ret == KNOT_EOK) {
+ reschedule->keys_changed = true;
+ next = next_action(ctx, flags);
+ reschedule->next_rollover = next.time;
+ } else {
+ if (log_keytag) {
+ log_zone_warning(ctx->zone->dname, "DNSSEC, key rollover, tag %5d, action %s (%s)",
+ dnssec_key_get_keytag(next.key->key),
+ roll_action_name(next.type), knot_strerror(ret));
+ } else {
+ log_zone_warning(ctx->zone->dname, "DNSSEC, key rollover, action %s (%s)",
+ roll_action_name(next.type), knot_strerror(ret));
+ }
+ }
+ }
+
+ if (ret == KNOT_EOK && next.ready_keyid != NULL) {
+ // just to make sure DS check is scheduled
+ reschedule->plan_ds_check = true;
+ ready_keyid = next.ready_keyid;
+ ready_keytag = next.ready_keytag;
+ }
+
+ if (ret == KNOT_EOK && knot_time_cmp(reschedule->next_rollover, ctx->now) <= 0) {
+ return knot_dnssec_key_rollover(ctx, flags, reschedule);
+ }
+
+ if (ret == KNOT_EOK && reschedule->keys_changed) {
+ ret = kdnssec_ctx_commit(ctx);
+ }
+
+ if (ret == KNOT_EOK && reschedule->plan_ds_check) {
+ char param[32];
+ (void)snprintf(param, sizeof(param), "KEY_SUBMISSION=%hu", ready_keytag);
+ log_fmt_zone(LOG_NOTICE, LOG_SOURCE_ZONE, ctx->zone->dname, param,
+ "DNSSEC, KSK submission, waiting for confirmation");
+ if (ctx->dbus_event & DBUS_EVENT_ZONE_SUBMISSION) {
+ systemd_emit_zone_submission(ctx->zone->dname, ready_keytag, ready_keyid);
+ }
+ }
+
+ return ret;
+}
+
+int knot_dnssec_ksk_sbm_confirm(kdnssec_ctx_t *ctx, uint32_t retire_delay)
+{
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *key = &ctx->zone->keys[i];
+ if (key->is_ksk && !key->is_pub_only &&
+ get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_READY) {
+ int ret = exec_new_signatures(ctx, key, retire_delay);
+ if (ret == KNOT_EOK) {
+ ret = kdnssec_ctx_commit(ctx);
+ }
+ return ret;
+ }
+ }
+ return KNOT_NO_READY_KEY;
+}
+
+bool zone_has_key_sbm(const kdnssec_ctx_t *ctx)
+{
+ assert(ctx->zone);
+
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *key = &ctx->zone->keys[i];
+ if (key->is_ksk && !key->is_pub_only &&
+ (get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_READY ||
+ get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_ACTIVE)) {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/src/knot/dnssec/key-events.h b/src/knot/dnssec/key-events.h
new file mode 100644
index 0000000..d216f90
--- /dev/null
+++ b/src/knot/dnssec/key-events.h
@@ -0,0 +1,69 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/dnssec/context.h"
+#include "knot/dnssec/zone-events.h"
+
+/*!
+ * \brief Perform correct ZSK and KSK rollover action and plan next one.
+ *
+ * For given zone, check keys in KASP db and decide what shall be done
+ * according to their timers. Perform the action if they shall be done now,
+ * and tell the user the next time it shall be called.
+ *
+ * This function is optimized to be called from KEY_ROLLOVER_EVENT,
+ * but also during zone load so that the zone gets loaded already with
+ * proper DNSSEC chain.
+ *
+ * \param ctx Zone signing context
+ * \param flags Determine if some actions are forced
+ * \param reschedule Out: timestamp of desired next invoke
+ *
+ * \return KNOT_E*
+ */
+int knot_dnssec_key_rollover(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags,
+ zone_sign_reschedule_t *reschedule);
+
+/*!
+ * \brief Get the key that ought to be retired by activating given new key.
+ *
+ * \param ctx DNSSEC context.
+ * \param newkey New key being rolled in.
+ *
+ * \return Old key being rolled out.
+ */
+knot_kasp_key_t *knot_dnssec_key2retire(kdnssec_ctx_t *ctx, knot_kasp_key_t *newkey);
+
+/*!
+ * \brief Set the submitted KSK to active state and the active one to retired
+ *
+ * \param ctx Zone signing context.
+ * \param retire_delay Retire event delay.
+ *
+ * \return KNOT_E*
+ */
+int knot_dnssec_ksk_sbm_confirm(kdnssec_ctx_t *ctx, uint32_t retire_delay);
+
+/*!
+ * \brief Is there a key in submission phase?
+ *
+ * \param ctx zone signing context
+ *
+ * \return False if there is no submitted key or if error; True otherwise
+ */
+bool zone_has_key_sbm(const kdnssec_ctx_t *ctx);
diff --git a/src/knot/dnssec/key_records.c b/src/knot/dnssec/key_records.c
new file mode 100644
index 0000000..9b22f7a
--- /dev/null
+++ b/src/knot/dnssec/key_records.c
@@ -0,0 +1,300 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "knot/dnssec/key_records.h"
+
+#include "libdnssec/error.h"
+#include "libdnssec/sign.h"
+#include "knot/dnssec/rrset-sign.h"
+#include "knot/dnssec/zone-sign.h"
+#include "knot/journal/serialization.h"
+
+void key_records_init(const kdnssec_ctx_t *ctx, key_records_t *r)
+{
+ knot_rrset_init(&r->dnskey, knot_dname_copy(ctx->zone->dname, NULL),
+ KNOT_RRTYPE_DNSKEY, KNOT_CLASS_IN, ctx->policy->dnskey_ttl);
+ knot_rrset_init(&r->cdnskey, knot_dname_copy(ctx->zone->dname, NULL),
+ KNOT_RRTYPE_CDNSKEY, KNOT_CLASS_IN, 0);
+ knot_rrset_init(&r->cds, knot_dname_copy(ctx->zone->dname, NULL),
+ KNOT_RRTYPE_CDS, KNOT_CLASS_IN, 0);
+ knot_rrset_init(&r->rrsig, knot_dname_copy(ctx->zone->dname, NULL),
+ KNOT_RRTYPE_RRSIG, KNOT_CLASS_IN, ctx->policy->dnskey_ttl);
+}
+
+void key_records_from_apex(const zone_node_t *apex, key_records_t *r)
+{
+ r->dnskey = node_rrset(apex, KNOT_RRTYPE_DNSKEY);
+ r->cdnskey = node_rrset(apex, KNOT_RRTYPE_CDNSKEY);
+ r->cds = node_rrset(apex, KNOT_RRTYPE_CDS);
+ knot_rrset_init_empty(&r->rrsig);
+}
+
+int key_records_add_rdata(key_records_t *r, uint16_t rrtype, uint8_t *rdata, uint16_t rdlen, uint32_t ttl)
+{
+ knot_rrset_t *to_add;
+ switch(rrtype) {
+ case KNOT_RRTYPE_DNSKEY:
+ to_add = &r->dnskey;
+ break;
+ case KNOT_RRTYPE_CDNSKEY:
+ to_add = &r->cdnskey;
+ break;
+ case KNOT_RRTYPE_CDS:
+ to_add = &r->cds;
+ break;
+ case KNOT_RRTYPE_RRSIG:
+ to_add = &r->rrsig;
+ break;
+ default:
+ return KNOT_EINVAL;
+ }
+
+ int ret = knot_rrset_add_rdata(to_add, rdata, rdlen, NULL);
+ if (ret == KNOT_EOK) {
+ to_add->ttl = ttl;
+ }
+ return ret;
+}
+
+void key_records_clear(key_records_t *r)
+{
+ knot_rrset_clear(&r->dnskey, NULL);
+ knot_rrset_clear(&r->cdnskey, NULL);
+ knot_rrset_clear(&r->cds, NULL);
+ knot_rrset_clear(&r->rrsig, NULL);
+}
+
+void key_records_clear_rdatasets(key_records_t *r)
+{
+ knot_rdataset_clear(&r->dnskey.rrs, NULL);
+ knot_rdataset_clear(&r->cdnskey.rrs, NULL);
+ knot_rdataset_clear(&r->cds.rrs, NULL);
+ knot_rdataset_clear(&r->rrsig.rrs, NULL);
+}
+
+static int add_one(const knot_rrset_t *rr, changeset_t *ch,
+ bool rem, changeset_flag_t fl, int ret)
+{
+ if (ret == KNOT_EOK && !knot_rrset_empty(rr)) {
+ if (rem) {
+ ret = changeset_add_removal(ch, rr, fl);
+ } else {
+ ret = changeset_add_addition(ch, rr, fl);
+ }
+ }
+ return ret;
+}
+
+int key_records_to_changeset(const key_records_t *r, changeset_t *ch,
+ bool rem, changeset_flag_t chfl)
+{
+ int ret = KNOT_EOK;
+ ret = add_one(&r->dnskey, ch, rem, chfl, ret);
+ ret = add_one(&r->cdnskey, ch, rem, chfl, ret);
+ ret = add_one(&r->cds, ch, rem, chfl, ret);
+ return ret;
+}
+
+static int subtract_one(knot_rrset_t *from, const knot_rrset_t *what,
+ int (*fcn)(knot_rdataset_t *, const knot_rdataset_t *, knot_mm_t *),
+ int ret)
+{
+ if (ret == KNOT_EOK && !knot_rrset_empty(from)) {
+ ret = fcn(&from->rrs, &what->rrs, NULL);
+ }
+ return ret;
+}
+
+int key_records_subtract(key_records_t *r, const key_records_t *against)
+{
+ int ret = KNOT_EOK;
+ ret = subtract_one(&r->dnskey, &against->dnskey, knot_rdataset_subtract, ret);
+ ret = subtract_one(&r->cdnskey, &against->cdnskey, knot_rdataset_subtract, ret);
+ ret = subtract_one(&r->cds, &against->cds, knot_rdataset_subtract, ret);
+ return ret;
+}
+
+int key_records_intersect(key_records_t *r, const key_records_t *against)
+{
+ int ret = KNOT_EOK;
+ ret = subtract_one(&r->dnskey, &against->dnskey, knot_rdataset_intersect2, ret);
+ ret = subtract_one(&r->cdnskey, &against->cdnskey, knot_rdataset_intersect2, ret);
+ ret = subtract_one(&r->cds, &against->cds, knot_rdataset_intersect2, ret);
+ return ret;
+}
+
+int key_records_dump(char **buf, size_t *buf_size, const key_records_t *r, bool verbose)
+{
+ if (*buf == NULL) {
+ if (*buf_size == 0) {
+ *buf_size = 512;
+ }
+ *buf = malloc(*buf_size);
+ if (*buf == NULL) {
+ return KNOT_ENOMEM;
+ }
+ }
+
+ const knot_dump_style_t verb_style = {
+ .wrap = true,
+ .show_ttl = true,
+ .verbose = true,
+ .original_ttl = true,
+ .human_timestamp = true
+ };
+ const knot_dump_style_t *style = verbose ? &verb_style : &KNOT_DUMP_STYLE_DEFAULT;
+
+ int ret = 0;
+ size_t total = 1;
+ const knot_rrset_t *all_rr[4] = { &r->dnskey, &r->cdnskey, &r->cds, &r->rrsig };
+ // first go: just detect the size
+ for (int i = 0; i < 4; i++) {
+ if (ret >= 0 && !knot_rrset_empty(all_rr[i])) {
+ ret = knot_rrset_txt_dump(all_rr[i], buf, buf_size, style);
+ (void)buf;
+ total += ret;
+ }
+ }
+ if (ret >= 0 && total > *buf_size) {
+ free(*buf);
+ *buf_size = total;
+ *buf = malloc(*buf_size);
+ if (*buf == NULL) {
+ return KNOT_ENOMEM;
+ }
+ }
+ char *fake_buf = *buf;
+ size_t fake_size = *buf_size;
+ //second go: do it
+ for (int i = 0; i < 4; i++) {
+ if (ret >= 0 && !knot_rrset_empty(all_rr[i])) {
+ ret = knot_rrset_txt_dump(all_rr[i], &fake_buf, &fake_size, style);
+ fake_buf += ret, fake_size -= ret;
+ }
+ }
+ assert(fake_buf - *buf == total - 1);
+ return ret >= 0 ? KNOT_EOK : ret;
+}
+
+int key_records_sign(const zone_key_t *key, key_records_t *r, const kdnssec_ctx_t *kctx, knot_time_t *expires)
+{
+ dnssec_sign_ctx_t *sign_ctx;
+ int ret = dnssec_sign_new(&sign_ctx, key->key);
+ if (ret != DNSSEC_EOK) {
+ ret = knot_error_from_libdnssec(ret);
+ }
+
+ if (!knot_rrset_empty(&r->dnskey) && knot_zone_sign_use_key(key, &r->dnskey)) {
+ ret = knot_sign_rrset(&r->rrsig, &r->dnskey, key->key, sign_ctx, kctx, NULL, expires);
+ }
+ if (ret == KNOT_EOK && !knot_rrset_empty(&r->cdnskey) && knot_zone_sign_use_key(key, &r->cdnskey)) {
+ ret = knot_sign_rrset(&r->rrsig, &r->cdnskey, key->key, sign_ctx, kctx, NULL, expires);
+ }
+ if (ret == KNOT_EOK && !knot_rrset_empty(&r->cds) && knot_zone_sign_use_key(key, &r->cds)) {
+ ret = knot_sign_rrset(&r->rrsig, &r->cds, key->key, sign_ctx, kctx, NULL, expires);
+ }
+
+ dnssec_sign_free(sign_ctx);
+ return ret;
+}
+
+int key_records_verify(key_records_t *r, kdnssec_ctx_t *kctx, knot_time_t timestamp)
+{
+ kctx->now = timestamp;
+ int ret = kasp_zone_keys_from_rr(kctx->zone, &r->dnskey.rrs, false, &kctx->keytag_conflict);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ zone_sign_ctx_t *sign_ctx = zone_validation_ctx(kctx);
+ if (sign_ctx == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ ret = knot_validate_rrsigs(&r->dnskey, &r->rrsig, sign_ctx, false);
+ if (ret == KNOT_EOK && !knot_rrset_empty(&r->cdnskey)) {
+ ret = knot_validate_rrsigs(&r->cdnskey, &r->rrsig, sign_ctx, false);
+ }
+ if (ret == KNOT_EOK && !knot_rrset_empty(&r->cds)) {
+ ret = knot_validate_rrsigs(&r->cds, &r->rrsig, sign_ctx, false);
+ }
+
+ zone_sign_ctx_free(sign_ctx);
+ return ret;
+}
+
+size_t key_records_serialized_size(const key_records_t *r)
+{
+ return rrset_serialized_size(&r->dnskey) + rrset_serialized_size(&r->cdnskey) +
+ rrset_serialized_size(&r->cds) + rrset_serialized_size(&r->rrsig);
+}
+
+int key_records_serialize(wire_ctx_t *wire, const key_records_t *r)
+{
+ int ret = serialize_rrset(wire, &r->dnskey);
+ if (ret == KNOT_EOK) {
+ ret = serialize_rrset(wire, &r->cdnskey);
+ }
+ if (ret == KNOT_EOK) {
+ ret = serialize_rrset(wire, &r->cds);
+ }
+ if (ret == KNOT_EOK) {
+ ret = serialize_rrset(wire, &r->rrsig);
+ }
+ return ret;
+}
+
+int key_records_deserialize(wire_ctx_t *wire, key_records_t *r)
+{
+ int ret = deserialize_rrset(wire, &r->dnskey);
+ if (ret == KNOT_EOK) {
+ ret = deserialize_rrset(wire, &r->cdnskey);
+ }
+ if (ret == KNOT_EOK) {
+ ret = deserialize_rrset(wire, &r->cds);
+ }
+ if (ret == KNOT_EOK) {
+ ret = deserialize_rrset(wire, &r->rrsig);
+ }
+ return ret;
+}
+
+int key_records_last_timestamp(kdnssec_ctx_t *ctx, knot_time_t *last)
+{
+ knot_time_t from = 0;
+ while (true) {
+ knot_time_t next;
+ key_records_t r = { { 0 } };
+ int ret = kasp_db_load_offline_records(ctx->kasp_db, ctx->zone->dname,
+ &from, &next, &r);
+ key_records_clear(&r);
+ if (ret == KNOT_ENOENT) {
+ break;
+ } else if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (next == 0) {
+ break;
+ }
+ from = next;
+ }
+ if (from == 0) {
+ from = knot_time();
+ }
+ *last = from;
+ return KNOT_EOK;
+}
diff --git a/src/knot/dnssec/key_records.h b/src/knot/dnssec/key_records.h
new file mode 100644
index 0000000..b53ed86
--- /dev/null
+++ b/src/knot/dnssec/key_records.h
@@ -0,0 +1,54 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "contrib/wire_ctx.h"
+#include "knot/dnssec/zone-keys.h"
+#include "knot/updates/changesets.h"
+
+void key_records_init(const kdnssec_ctx_t *ctx, key_records_t *r);
+
+void key_records_from_apex(const zone_node_t *apex, key_records_t *r);
+
+int key_records_add_rdata(key_records_t *r, uint16_t rrtype, uint8_t *rdata, uint16_t rdlen, uint32_t ttl);
+
+void key_records_clear(key_records_t *r);
+
+void key_records_clear_rdatasets(key_records_t *r);
+
+int key_records_to_changeset(const key_records_t *r, changeset_t *ch,
+ bool rem, changeset_flag_t chfl);
+
+int key_records_subtract(key_records_t *r, const key_records_t *against);
+
+int key_records_intersect(key_records_t *r, const key_records_t *against);
+
+int key_records_dump(char **buf, size_t *buf_size, const key_records_t *r, bool verbose);
+
+int key_records_sign(const zone_key_t *key, key_records_t *r, const kdnssec_ctx_t *kctx, knot_time_t *expires);
+
+// WARNING this modifies 'kctx' with updated timestamp and with zone_keys from r->dnskey
+int key_records_verify(key_records_t *r, kdnssec_ctx_t *kctx, knot_time_t timestamp);
+
+size_t key_records_serialized_size(const key_records_t *r);
+
+int key_records_serialize(wire_ctx_t *wire, const key_records_t *r);
+
+int key_records_deserialize(wire_ctx_t *wire, key_records_t *r);
+
+// Returns now if no records available.
+int key_records_last_timestamp(kdnssec_ctx_t *ctx, knot_time_t *last);
diff --git a/src/knot/dnssec/nsec-chain.c b/src/knot/dnssec/nsec-chain.c
new file mode 100644
index 0000000..dc35097
--- /dev/null
+++ b/src/knot/dnssec/nsec-chain.c
@@ -0,0 +1,797 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include "contrib/base32hex.h"
+#include "knot/dnssec/nsec-chain.h"
+#include "knot/dnssec/rrset-sign.h"
+#include "knot/dnssec/zone-nsec.h"
+#include "knot/dnssec/zone-sign.h"
+#include "knot/zone/adjust.h"
+
+void bitmap_add_node_rrsets(dnssec_nsec_bitmap_t *bitmap, const zone_node_t *node,
+ bool exact)
+{
+ bool deleg = node->flags & NODE_FLAGS_DELEG;
+ for (int i = 0; i < node->rrset_count; i++) {
+ knot_rrset_t rr = node_rrset_at(node, i);
+ if (deleg && (rr.type != KNOT_RRTYPE_NS && rr.type != KNOT_RRTYPE_DS &&
+ rr.type != KNOT_RRTYPE_NSEC)) {
+ if (rr.type != KNOT_RRTYPE_RRSIG) {
+ continue;
+ }
+ if (!rrsig_covers_type(&rr, KNOT_RRTYPE_DS) &&
+ !rrsig_covers_type(&rr, KNOT_RRTYPE_NSEC)) {
+ continue;
+ }
+ }
+ if (!exact && (rr.type == KNOT_RRTYPE_NSEC || rr.type == KNOT_RRTYPE_RRSIG)) {
+ continue;
+ }
+
+ dnssec_nsec_bitmap_add(bitmap, rr.type);
+ }
+}
+
+/* - NSEC chain construction ------------------------------------------------ */
+
+static int create_nsec_base(knot_rrset_t *rrset, knot_dname_t *from_owner,
+ const knot_dname_t *to_owner, uint32_t ttl,
+ size_t bitmap_size, uint8_t **bitmap_writeto)
+{
+ knot_rrset_init(rrset, from_owner, KNOT_RRTYPE_NSEC, KNOT_CLASS_IN, ttl);
+
+ size_t next_owner_size = knot_dname_size(to_owner);
+ size_t rdsize = next_owner_size + bitmap_size;
+ uint8_t rdata[rdsize];
+ memcpy(rdata, to_owner, next_owner_size);
+
+ int ret = knot_rrset_add_rdata(rrset, rdata, rdsize, NULL);
+
+ assert(ret != KNOT_EOK || rrset->rrs.rdata->len == rdsize);
+ *bitmap_writeto = rrset->rrs.rdata->data + next_owner_size;
+
+ return ret;
+}
+
+/*!
+ * \brief Create NSEC RR set.
+ *
+ * \param rrset RRSet to be initialized.
+ * \param from Node that should contain the new RRSet.
+ * \param to Node that should be pointed to from 'from'.
+ * \param ttl Record TTL (SOA's minimum TTL).
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int create_nsec_rrset(knot_rrset_t *rrset, const zone_node_t *from,
+ const knot_dname_t *to, uint32_t ttl)
+{
+ assert(from);
+ assert(to);
+
+ dnssec_nsec_bitmap_t *rr_types = dnssec_nsec_bitmap_new();
+ if (!rr_types) {
+ return KNOT_ENOMEM;
+ }
+
+ bitmap_add_node_rrsets(rr_types, from, false);
+ dnssec_nsec_bitmap_add(rr_types, KNOT_RRTYPE_NSEC);
+ dnssec_nsec_bitmap_add(rr_types, KNOT_RRTYPE_RRSIG);
+
+ uint8_t *bitmap_write;
+ int ret = create_nsec_base(rrset, from->owner, to, ttl,
+ dnssec_nsec_bitmap_size(rr_types), &bitmap_write);
+ if (ret == KNOT_EOK) {
+ dnssec_nsec_bitmap_write(rr_types, bitmap_write);
+ }
+ dnssec_nsec_bitmap_free(rr_types);
+
+ return ret;
+}
+
+/*!
+ * \brief Connect two nodes by adding a NSEC RR into the first node.
+ *
+ * Callback function, signature chain_iterate_cb.
+ *
+ * \param a First node.
+ * \param b Second node (immediate follower of a).
+ * \param data Pointer to nsec_chain_iterate_data_t holding parameters
+ * including changeset.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int connect_nsec_nodes(zone_node_t *a, zone_node_t *b,
+ nsec_chain_iterate_data_t *data)
+{
+ assert(a);
+ assert(b);
+ assert(data);
+
+ if (b->rrset_count == 0 || b->flags & NODE_FLAGS_NONAUTH) {
+ return NSEC_NODE_SKIP;
+ }
+
+ int ret = KNOT_EOK;
+
+ /*!
+ * If the node has no other RRSets than NSEC (and possibly RRSIGs),
+ * just remove the NSEC and its RRSIG, they are redundant
+ */
+ if (node_rrtype_exists(b, KNOT_RRTYPE_NSEC)
+ && knot_nsec_empty_nsec_and_rrsigs_in_node(b)) {
+ ret = knot_nsec_changeset_remove(b, data->update);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ // Skip the 'b' node
+ return NSEC_NODE_SKIP;
+ }
+
+ // create new NSEC
+ knot_rrset_t new_nsec;
+ ret = create_nsec_rrset(&new_nsec, a, b->owner, data->ttl);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ knot_rrset_t old_nsec = node_rrset(a, KNOT_RRTYPE_NSEC);
+
+ if (!knot_rrset_empty(&old_nsec)) {
+ /* Convert old NSEC to lowercase, just in case it's not. */
+ knot_rrset_t *old_nsec_lc = knot_rrset_copy(&old_nsec, NULL);
+ ret = knot_rrset_rr_to_canonical(old_nsec_lc);
+ if (ret != KNOT_EOK) {
+ knot_rrset_free(old_nsec_lc, NULL);
+ return ret;
+ }
+
+ bool equal = knot_rrset_equal(&new_nsec, old_nsec_lc, true);
+ knot_rrset_free(old_nsec_lc, NULL);
+
+ if (equal) {
+ // current NSEC is valid, do nothing
+ knot_rdataset_clear(&new_nsec.rrs, NULL);
+ return KNOT_EOK;
+ }
+
+ ret = knot_nsec_changeset_remove(a, data->update);
+ if (ret != KNOT_EOK) {
+ knot_rdataset_clear(&new_nsec.rrs, NULL);
+ return ret;
+ }
+ }
+
+ // Add new NSEC to the changeset (no matter if old was removed)
+ ret = zone_update_add(data->update, &new_nsec);
+ knot_rdataset_clear(&new_nsec.rrs, NULL);
+ return ret;
+}
+
+/*!
+ * \brief Replace b's NSEC "next" field with a's, keeping the NSEC bitmap.
+ *
+ * \param a Node to take the NSEC "next" field from.
+ * \param b Node to update the NSEC "next" field in.
+ * \param data Contains changeset to be updated.
+ *
+ * \return KNOT_E*
+ */
+static int reconnect_nsec_nodes(zone_node_t *a, zone_node_t *b,
+ nsec_chain_iterate_data_t *data)
+{
+ assert(a);
+ assert(b);
+ assert(data);
+
+ knot_rrset_t an = node_rrset(a, KNOT_RRTYPE_NSEC);
+ assert(!knot_rrset_empty(&an));
+
+ knot_rrset_t bnorig = node_rrset(b, KNOT_RRTYPE_NSEC);
+ assert(!knot_rrset_empty(&bnorig));
+
+ size_t b_bitmap_len = knot_nsec_bitmap_len(bnorig.rrs.rdata);
+
+ knot_rrset_t bnnew;
+ uint8_t *bitmap_write;
+ int ret = create_nsec_base(&bnnew, bnorig.owner, knot_nsec_next(an.rrs.rdata),
+ bnorig.ttl, b_bitmap_len, &bitmap_write);
+ if (ret == KNOT_EOK) {
+ memcpy(bitmap_write, knot_nsec_bitmap(bnorig.rrs.rdata), b_bitmap_len);
+ }
+
+ ret = zone_update_remove(data->update, &bnorig);
+ if (ret == KNOT_EOK) {
+ ret = zone_update_add(data->update, &bnnew);
+ }
+
+ knot_rdataset_clear(&bnnew.rrs, NULL);
+ return ret;
+}
+
+static bool node_no_nsec(zone_node_t *node)
+{
+ return ((node->flags & NODE_FLAGS_DELETED) ||
+ (node->flags & NODE_FLAGS_NONAUTH) ||
+ node->rrset_count == 0);
+}
+
+/*!
+ * \brief Create or fix the node's NSEC record with correct bitmap.
+ *
+ * \param node Node to fix the NSEC bitmap in.
+ * \param data_voidp NSEC creation data.
+ *
+ * \return KNOT_E*
+ */
+static int nsec_update_bitmap(zone_node_t *node,
+ nsec_chain_iterate_data_t *data)
+{
+ if (node_no_nsec(node) || knot_nsec_empty_nsec_and_rrsigs_in_node(node)) {
+ return knot_nsec_changeset_remove(node, data->update);
+ }
+
+ knot_rrset_t old_nsec = node_rrset(node, KNOT_RRTYPE_NSEC);
+ const knot_dname_t *next = knot_rrset_empty(&old_nsec) ?
+ (const knot_dname_t *)"" :
+ knot_nsec_next(old_nsec.rrs.rdata);
+ knot_rrset_t new_nsec;
+ int ret = create_nsec_rrset(&new_nsec, node, next, data->ttl);
+
+ if (ret == KNOT_EOK && !knot_rrset_empty(&old_nsec)) {
+ ret = zone_update_remove(data->update, &old_nsec);
+ }
+ if (ret == KNOT_EOK) {
+ ret = zone_update_add(data->update, &new_nsec);
+ }
+ knot_rdataset_clear(&new_nsec.rrs, NULL);
+ return ret;
+}
+
+static int nsec_update_bitmaps(zone_tree_t *node_ptrs,
+ nsec_chain_iterate_data_t *data)
+{
+ zone_tree_delsafe_it_t it = { 0 };
+ int ret = zone_tree_delsafe_it_begin(node_ptrs, &it, false);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ while (!zone_tree_delsafe_it_finished(&it) && ret == KNOT_EOK) {
+ ret = nsec_update_bitmap(zone_tree_delsafe_it_val(&it), data);
+ zone_tree_delsafe_it_next(&it);
+ }
+ zone_tree_delsafe_it_free(&it);
+ return ret;
+}
+
+static bool node_nsec3_unmatching(const zone_node_t *node, const dnssec_nsec3_params_t *params)
+{
+ knot_rdataset_t *nsec3 = node_rdataset(node, KNOT_RRTYPE_NSEC3);
+ if (nsec3 == NULL || nsec3->count < 1 || params == NULL) {
+ return false;
+ }
+ knot_rdata_t *rdata = nsec3->rdata;
+ for (int i = 0; i < nsec3->count; i++) {
+ if (knot_nsec3_alg(rdata) == params->algorithm &&
+ knot_nsec3_iters(rdata) == params->iterations &&
+ knot_nsec3_salt_len(rdata) == params->salt.size &&
+ memcmp(knot_nsec3_salt(rdata), params->salt.data, params->salt.size) == 0) {
+ return false;
+ }
+ rdata = knot_rdataset_next(rdata);
+ }
+ return true;
+}
+
+int nsec_check_connect_nodes(zone_node_t *a, zone_node_t *b,
+ nsec_chain_iterate_data_t *data)
+{
+ if (node_no_nsec(b) || node_nsec3_unmatching(b, data->nsec3_params)) {
+ return NSEC_NODE_SKIP;
+ }
+ knot_rdataset_t *nsec = node_rdataset(a, data->nsec_type);
+ if (nsec == NULL || nsec->count != 1) {
+ data->update->validation_hint.node = a->owner;
+ data->update->validation_hint.rrtype = KNOT_RRTYPE_ANY;
+ return KNOT_DNSSEC_ENSEC_CHAIN;
+ }
+ if (data->nsec_type == KNOT_RRTYPE_NSEC) {
+ const knot_dname_t *a_next = knot_nsec_next(nsec->rdata);
+ if (!knot_dname_is_case_equal(a_next, b->owner)) {
+ data->update->validation_hint.node = a->owner;
+ data->update->validation_hint.rrtype = data->nsec_type;
+ return KNOT_DNSSEC_ENSEC_CHAIN;
+ }
+ } else {
+ uint8_t next_len = knot_nsec3_next_len(nsec->rdata);
+ uint8_t bdecoded[next_len];
+ int len = knot_base32hex_decode(b->owner + 1, b->owner[0], bdecoded, next_len);
+ if (len != next_len ||
+ memcmp(knot_nsec3_next(nsec->rdata), bdecoded, len) != 0) {
+ data->update->validation_hint.node = a->owner;
+ data->update->validation_hint.rrtype = data->nsec_type;
+ return KNOT_DNSSEC_ENSEC_CHAIN;
+ }
+ }
+ return KNOT_EOK;
+}
+
+static zone_node_t *nsec_prev(zone_node_t *node, const dnssec_nsec3_params_t *matching_params)
+{
+ zone_node_t *res = node;
+ do {
+ res = node_prev(res);
+ } while (res != NULL && ((res->flags & NODE_FLAGS_NONAUTH) ||
+ res->rrset_count == 0 ||
+ node_nsec3_unmatching(res, matching_params)));
+ assert(res == NULL || !knot_nsec_empty_nsec_and_rrsigs_in_node(res));
+ return res;
+}
+
+static int nsec_check_prev_next(zone_node_t *node, void *ctx)
+{
+ if (node_no_nsec(node)) {
+ return KNOT_EOK;
+ }
+
+ nsec_chain_iterate_data_t *data = ctx;
+ int ret = nsec_check_connect_nodes(nsec_prev(node, data->nsec3_params), node, data);
+ if (ret == NSEC_NODE_SKIP) {
+ return KNOT_EOK;
+ }
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ dnssec_validation_hint_t *hint = &data->update->validation_hint;
+ knot_rdataset_t *nsec = node_rdataset(node, data->nsec_type);
+ if (nsec == NULL || nsec->count != 1) {
+ hint->node = node->owner;
+ hint->rrtype = KNOT_RRTYPE_ANY;
+ return KNOT_DNSSEC_ENSEC_CHAIN;
+ }
+
+ const zone_node_t *nn;
+ if (data->nsec_type == KNOT_RRTYPE_NSEC) {
+ if (knot_dname_store(hint->next, knot_nsec_next(nsec->rdata)) == 0) {
+ return KNOT_EINVAL;
+ }
+ knot_dname_to_lower(hint->next);
+ nn = zone_contents_find_node(data->update->new_cont, hint->next);
+ } else {
+ ret = knot_nsec3_hash_to_dname(hint->next, sizeof(hint->next),
+ knot_nsec3_next(nsec->rdata),
+ knot_nsec3_next_len(nsec->rdata),
+ data->update->new_cont->apex->owner);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ nn = zone_contents_find_nsec3_node(data->update->new_cont, hint->next);
+ }
+ if (nn == NULL) {
+ hint->node = hint->next;
+ hint->rrtype = KNOT_RRTYPE_ANY;
+ return KNOT_DNSSEC_ENSEC_CHAIN;
+ }
+ if (nsec_prev((zone_node_t *)nn, data->nsec3_params) != node) {
+ hint->node = node->owner;
+ hint->rrtype = data->nsec_type;
+ return KNOT_DNSSEC_ENSEC_CHAIN;
+ }
+ return KNOT_EOK;
+}
+
+int nsec_check_new_connects(zone_tree_t *tree, nsec_chain_iterate_data_t *data)
+{
+ return zone_tree_apply(tree, nsec_check_prev_next, data);
+}
+
+static int check_subtree_optout(zone_node_t *node, void *ctx)
+{
+ bool *res = ctx;
+ if ((node->flags & NODE_FLAGS_NONAUTH) || !*res) {
+ return KNOT_EOK;
+ }
+ if (node_nsec3_get(node) != NULL &&
+ node_rdataset(node_nsec3_get(node), KNOT_RRTYPE_NSEC3) != NULL) {
+ *res = false;
+ }
+ return KNOT_EOK;
+}
+
+static int check_nsec_bitmap(zone_node_t *node, void *ctx)
+{
+ nsec_chain_iterate_data_t *data = ctx;
+ assert((bool)(data->nsec_type == KNOT_RRTYPE_NSEC3) == (bool)(data->nsec3_params != NULL));
+ const zone_node_t *nsec_node = node;
+ bool shall_no_nsec = node_no_nsec(node);
+ if (data->nsec3_params != NULL) {
+ if ((node->flags & NODE_FLAGS_DELETED) ||
+ node_rrtype_exists(node, KNOT_RRTYPE_NSEC3)) {
+ // this can happen when checking nodes from adjust_ptrs
+ return KNOT_EOK;
+ }
+ nsec_node = node_nsec3_get(node);
+ shall_no_nsec = (node->flags & NODE_FLAGS_DELETED) ||
+ (node->flags & NODE_FLAGS_NONAUTH);
+ }
+ bool may_no_nsec = (data->nsec3_params != NULL && !(node->flags & NODE_FLAGS_SUBTREE_AUTH));
+ knot_rdataset_t *nsec = node_rdataset(nsec_node, data->nsec_type);
+ if (may_no_nsec && nsec == NULL) {
+ int ret = zone_tree_sub_apply(data->update->new_cont->nodes, node->owner,
+ true, check_subtree_optout, &may_no_nsec);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+ if ((nsec == NULL || nsec->count != 1) && !shall_no_nsec && !may_no_nsec) {
+ data->update->validation_hint.node = (nsec_node == NULL ? node->owner : nsec_node->owner);
+ data->update->validation_hint.rrtype = KNOT_RRTYPE_ANY;
+ return KNOT_DNSSEC_ENONSEC;
+ }
+ if (shall_no_nsec && nsec != NULL && nsec->count > 0) {
+ data->update->validation_hint.node = nsec_node->owner;
+ data->update->validation_hint.rrtype = data->nsec_type;
+ return KNOT_DNSSEC_ENSEC_BITMAP;
+ }
+ if (shall_no_nsec) {
+ return KNOT_EOK;
+ }
+ if (may_no_nsec && nsec == NULL) {
+ assert(data->nsec_type == KNOT_RRTYPE_NSEC3);
+ const zone_node_t *found_nsec3 = NULL, *prev_nsec3 = NULL;
+ if (node->nsec3_hash == NULL ||
+ zone_contents_find_nsec3(data->update->new_cont, node->nsec3_hash, &found_nsec3, &prev_nsec3) != ZONE_NAME_NOT_FOUND ||
+ found_nsec3 != NULL) {
+ return KNOT_ERROR;
+ }
+ if (prev_nsec3 == NULL) {
+ data->update->validation_hint.node = (nsec_node == NULL ? node->owner : nsec_node->owner);
+ data->update->validation_hint.rrtype = KNOT_RRTYPE_ANY;
+ return KNOT_DNSSEC_ENONSEC;
+ }
+ knot_rdataset_t *nsec3 = node_rdataset(prev_nsec3, KNOT_RRTYPE_NSEC3);
+ if (nsec3 == NULL) {
+ return KNOT_ERROR;
+ }
+ if (nsec3->count != 1 || !(knot_nsec3param_flags(nsec3->rdata) & KNOT_NSEC3_FLAG_OPT_OUT)) {
+ data->update->validation_hint.node = prev_nsec3->owner;
+ data->update->validation_hint.rrtype = data->nsec_type;
+ return KNOT_DNSSEC_ENSEC3_OPTOUT;
+ }
+ return KNOT_EOK;
+ }
+
+ dnssec_nsec_bitmap_t *rr_types = dnssec_nsec_bitmap_new();
+ if (rr_types == NULL) {
+ return KNOT_ENOMEM;
+ }
+ bitmap_add_node_rrsets(rr_types, node, true);
+
+ uint16_t node_wire_size = dnssec_nsec_bitmap_size(rr_types);
+ uint8_t *node_wire = malloc(node_wire_size);
+ if (node_wire == NULL) {
+ dnssec_nsec_bitmap_free(rr_types);
+ return KNOT_ENOMEM;
+ }
+ dnssec_nsec_bitmap_write(rr_types, node_wire);
+ dnssec_nsec_bitmap_free(rr_types);
+
+ const uint8_t *nsec_wire = NULL;
+ uint16_t nsec_wire_size = 0;
+ if (data->nsec3_params == NULL) {
+ nsec_wire = knot_nsec_bitmap(nsec->rdata);
+ nsec_wire_size = knot_nsec_bitmap_len(nsec->rdata);
+ } else {
+ nsec_wire = knot_nsec3_bitmap(nsec->rdata);
+ nsec_wire_size = knot_nsec3_bitmap_len(nsec->rdata);
+ }
+
+ if (node_wire_size != nsec_wire_size ||
+ memcmp(node_wire, nsec_wire, node_wire_size) != 0) {
+ free(node_wire);
+ data->update->validation_hint.node = node->owner;
+ data->update->validation_hint.rrtype = data->nsec_type;
+ return KNOT_DNSSEC_ENSEC_BITMAP;
+ }
+ free(node_wire);
+ return KNOT_EOK;
+}
+
+int nsec_check_bitmaps(zone_tree_t *nsec_ptrs, nsec_chain_iterate_data_t *data)
+{
+ return zone_tree_apply(nsec_ptrs, check_nsec_bitmap, data);
+}
+
+/*! \brief Return the one from those nodes which has
+ * closest lower (lexicographically) owner name to ref. */
+static zone_node_t *node_nearer(zone_node_t *a, zone_node_t *b, zone_node_t *ref)
+{
+ if (a == NULL || a == b) {
+ return b;
+ } else if (b == NULL) {
+ return a;
+ } else {
+ int abigger = knot_dname_cmp(a->owner, ref->owner) >= 0 ? 1 : 0;
+ int bbigger = knot_dname_cmp(b->owner, ref->owner) >= 0 ? 1 : 0;
+ int cmp = knot_dname_cmp(a->owner, b->owner);
+ if (abigger != bbigger) {
+ cmp = -cmp;
+ }
+ return cmp < 0 ? b : a;
+ }
+}
+
+/* - API - iterations ------------------------------------------------------- */
+
+/*!
+ * \brief Call a function for each piece of the chain formed by sorted nodes.
+ */
+int knot_nsec_chain_iterate_create(zone_tree_t *nodes,
+ chain_iterate_create_cb callback,
+ nsec_chain_iterate_data_t *data)
+{
+ assert(nodes);
+ assert(callback);
+
+ zone_tree_delsafe_it_t it = { 0 };
+ int result = zone_tree_delsafe_it_begin(nodes, &it, false);
+ if (result != KNOT_EOK) {
+ return result;
+ }
+
+ if (zone_tree_delsafe_it_finished(&it)) {
+ zone_tree_delsafe_it_free(&it);
+ return KNOT_EINVAL;
+ }
+
+ zone_node_t *first = zone_tree_delsafe_it_val(&it);
+ zone_node_t *previous = first;
+ zone_node_t *current = first;
+
+ zone_tree_delsafe_it_next(&it);
+
+ while (!zone_tree_delsafe_it_finished(&it)) {
+ current = zone_tree_delsafe_it_val(&it);
+
+ result = callback(previous, current, data);
+ if (result == NSEC_NODE_SKIP) {
+ // No NSEC should be created for 'current' node, skip
+ ;
+ } else if (result == KNOT_EOK) {
+ previous = current;
+ } else {
+ zone_tree_delsafe_it_free(&it);
+ return result;
+ }
+ zone_tree_delsafe_it_next(&it);
+ }
+
+ zone_tree_delsafe_it_free(&it);
+
+ return result == NSEC_NODE_SKIP ? callback(previous, first, data) :
+ callback(current, first, data);
+}
+
+int knot_nsec_chain_iterate_fix(zone_tree_t *node_ptrs,
+ chain_iterate_create_cb callback,
+ chain_iterate_create_cb cb_reconn,
+ nsec_chain_iterate_data_t *data)
+{
+ zone_tree_delsafe_it_t it = { 0 };
+ int ret = zone_tree_delsafe_it_begin(node_ptrs, &it, true);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ zone_node_t *prev_it = NULL;
+ zone_node_t *started_with = NULL;
+ while (ret == KNOT_EOK) {
+ if (zone_tree_delsafe_it_finished(&it)) {
+ assert(started_with != NULL);
+ zone_tree_delsafe_it_restart(&it);
+ }
+
+ zone_node_t *curr_new = zone_tree_delsafe_it_val(&it);
+ zone_node_t *curr_old = binode_counterpart(curr_new);
+ bool del_new = node_no_nsec(curr_new);
+ bool del_old = node_no_nsec(curr_old);
+
+ if (started_with == curr_new) {
+ assert(started_with != NULL);
+ break;
+ }
+ if (!del_old && !del_new && started_with == NULL) {
+ // Once this must happen since the NSEC(3) node belonging
+ // to zone apex is always present.
+ started_with = curr_new;
+ }
+
+ if (!del_old && del_new && started_with != NULL) {
+ zone_node_t *prev_old = curr_old, *prev_new;
+ do {
+ prev_old = nsec_prev(prev_old, NULL);
+ prev_new = binode_counterpart(prev_old);
+ } while (node_no_nsec(prev_new));
+
+ zone_node_t *prev_near = node_nearer(prev_new, prev_it, curr_old);
+ ret = cb_reconn(curr_old, prev_near, data);
+ }
+ if (del_old && !del_new && started_with != NULL) {
+ zone_node_t *prev_new = nsec_prev(curr_new, NULL);
+ ret = cb_reconn(prev_new, curr_new, data);
+ if (ret == KNOT_EOK) {
+ ret = callback(prev_new, curr_new, data);
+ }
+ prev_it = curr_new;
+ }
+
+ zone_tree_delsafe_it_next(&it);
+ }
+ zone_tree_delsafe_it_free(&it);
+ return ret;
+}
+
+/* - API - utility functions ------------------------------------------------ */
+
+/*!
+ * \brief Add entry for removed NSEC to the changeset.
+ */
+int knot_nsec_changeset_remove(const zone_node_t *n, zone_update_t *update)
+{
+ if (update == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ int result = KNOT_EOK;
+ knot_rrset_t nsec_rem = node_rrset(n, KNOT_RRTYPE_NSEC);
+ knot_rrset_t nsec3_rem = node_rrset(n, KNOT_RRTYPE_NSEC3);
+ knot_rrset_t rrsigs = node_rrset(n, KNOT_RRTYPE_RRSIG);
+
+ if (!knot_rrset_empty(&nsec_rem)) {
+ result = zone_update_remove(update, &nsec_rem);
+ }
+ if (result == KNOT_EOK && !knot_rrset_empty(&nsec3_rem)) {
+ result = zone_update_remove(update, &nsec3_rem);
+ }
+ if (!knot_rrset_empty(&rrsigs) && result == KNOT_EOK) {
+ knot_rrset_t synth_rrsigs;
+ knot_rrset_init(&synth_rrsigs, n->owner, KNOT_RRTYPE_RRSIG,
+ KNOT_CLASS_IN, rrsigs.ttl);
+ result = knot_synth_rrsig(KNOT_RRTYPE_NSEC, &rrsigs.rrs,
+ &synth_rrsigs.rrs, NULL);
+ if (result == KNOT_ENOENT) {
+ // Try removing NSEC3 RRSIGs
+ result = knot_synth_rrsig(KNOT_RRTYPE_NSEC3, &rrsigs.rrs,
+ &synth_rrsigs.rrs, NULL);
+ }
+
+ if (result != KNOT_EOK) {
+ knot_rdataset_clear(&synth_rrsigs.rrs, NULL);
+ if (result != KNOT_ENOENT) {
+ return result;
+ }
+ return KNOT_EOK;
+ }
+
+ // store RRSIG
+ result = zone_update_remove(update, &synth_rrsigs);
+ knot_rdataset_clear(&synth_rrsigs.rrs, NULL);
+ }
+
+ return result;
+}
+
+/*!
+ * \brief Checks whether the node is empty or eventually contains only NSEC and
+ * RRSIGs.
+ */
+bool knot_nsec_empty_nsec_and_rrsigs_in_node(const zone_node_t *n)
+{
+ assert(n);
+ for (int i = 0; i < n->rrset_count; ++i) {
+ knot_rrset_t rrset = node_rrset_at(n, i);
+ if (rrset.type != KNOT_RRTYPE_NSEC &&
+ rrset.type != KNOT_RRTYPE_RRSIG) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/* - API - Chain creation --------------------------------------------------- */
+
+/*!
+ * \brief Create new NSEC chain, add differences from current into a changeset.
+ */
+int knot_nsec_create_chain(zone_update_t *update, uint32_t ttl)
+{
+ assert(update);
+ assert(update->new_cont->nodes);
+
+ nsec_chain_iterate_data_t data = { ttl, update, KNOT_RRTYPE_NSEC };
+
+ return knot_nsec_chain_iterate_create(update->new_cont->nodes,
+ connect_nsec_nodes, &data);
+}
+
+int knot_nsec_fix_chain(zone_update_t *update, uint32_t ttl)
+{
+ assert(update);
+ assert(update->zone->contents->nodes);
+ assert(update->new_cont->nodes);
+
+ nsec_chain_iterate_data_t data = { ttl, update, KNOT_RRTYPE_NSEC };
+
+ int ret = nsec_update_bitmaps(update->a_ctx->node_ptrs, &data);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = zone_adjust_contents(update->new_cont, adjust_cb_void, NULL, false, true, 1, update->a_ctx->node_ptrs);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // ensure that zone root is in list of changed nodes
+ ret = zone_tree_insert(update->a_ctx->node_ptrs, &update->new_cont->apex);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ return knot_nsec_chain_iterate_fix(update->a_ctx->node_ptrs,
+ connect_nsec_nodes, reconnect_nsec_nodes, &data);
+}
+
+int knot_nsec_check_chain(zone_update_t *update)
+{
+ if (!zone_tree_is_empty(update->new_cont->nsec3_nodes)) {
+ update->validation_hint.node = update->zone->name;
+ update->validation_hint.rrtype = KNOT_RRTYPE_NSEC3;
+ return KNOT_DNSSEC_ENSEC_BITMAP;
+ }
+
+ nsec_chain_iterate_data_t data = { 0, update, KNOT_RRTYPE_NSEC };
+
+ int ret = nsec_check_bitmaps(update->new_cont->nodes, &data);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ return knot_nsec_chain_iterate_create(update->new_cont->nodes,
+ nsec_check_connect_nodes, &data);
+}
+
+int knot_nsec_check_chain_fix(zone_update_t *update)
+{
+ if (!zone_tree_is_empty(update->new_cont->nsec3_nodes)) {
+ update->validation_hint.node = update->zone->name;
+ update->validation_hint.rrtype = KNOT_RRTYPE_NSEC3;
+ return KNOT_DNSSEC_ENSEC_BITMAP;
+ }
+
+ nsec_chain_iterate_data_t data = { 0, update, KNOT_RRTYPE_NSEC };
+
+ int ret = nsec_check_bitmaps(update->a_ctx->node_ptrs, &data);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ return nsec_check_new_connects(update->a_ctx->node_ptrs, &data);
+}
diff --git a/src/knot/dnssec/nsec-chain.h b/src/knot/dnssec/nsec-chain.h
new file mode 100644
index 0000000..362780e
--- /dev/null
+++ b/src/knot/dnssec/nsec-chain.h
@@ -0,0 +1,174 @@
+/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "knot/zone/contents.h"
+#include "knot/updates/zone-update.h"
+#include "libdnssec/nsec.h"
+
+/*!
+ * \brief Parameters to be used in connect_nsec_nodes callback.
+ */
+typedef struct {
+ uint32_t ttl; // TTL for NSEC(3) records
+ zone_update_t *update; // The zone update for NSECs
+ uint16_t nsec_type; // NSEC or NSEC3
+ const dnssec_nsec3_params_t *nsec3_params;
+} nsec_chain_iterate_data_t;
+
+/*!
+ * \brief Used to control changeset iteration functions.
+ */
+enum {
+ NSEC_NODE_SKIP = 1,
+};
+
+/*!
+ * \brief Callback used when creating NSEC chains.
+ */
+typedef int (*chain_iterate_create_cb)(zone_node_t *, zone_node_t *,
+ nsec_chain_iterate_data_t *);
+
+/*!
+ * \brief Add all RR types from a node into the bitmap.
+ */
+void bitmap_add_node_rrsets(dnssec_nsec_bitmap_t *bitmap, const zone_node_t *node,
+ bool exact);
+
+/*!
+ * \brief Check that the NSEC(3) record in node A points to B.
+ *
+ * \param a Node A.
+ * \param b Node B.
+ * \param data Validation context.
+ *
+ * \retval NSEC_NODE_SKIP Node B is not part of NSEC chain, call again with A and B->next.
+ * \retval KNOT_DNSSEC_ENSEC_CHAIN The NSEC(3) chain is broken.
+ * \return KNOT_E*
+ */
+int nsec_check_connect_nodes(zone_node_t *a, zone_node_t *b,
+ nsec_chain_iterate_data_t *data);
+
+/*!
+ * \brief Check NSEC connections of updated nodes.
+ *
+ * \param tree Trie with updated nodes.
+ * \param data Validation context.
+ *
+ * \return KNOT_DNSSEC_ENSEC_CHAIN, KNOT_E*
+ */
+int nsec_check_new_connects(zone_tree_t *tree, nsec_chain_iterate_data_t *data);
+
+/*!
+ * \brief Check NSEC(3) bitmaps for updated nodes.
+ *
+ * \param nsec_ptrs Trie with nodes to be checked.
+ * \param data Validation context.
+ *
+ * \return KNOT_DNSSEC_ENSEC_BITMAP, KNOT_E*
+ */
+int nsec_check_bitmaps(zone_tree_t *nsec_ptrs, nsec_chain_iterate_data_t *data);
+
+/*!
+ * \brief Call a function for each piece of the chain formed by sorted nodes.
+ *
+ * \note If the callback function returns anything other than KNOT_EOK, the
+ * iteration is terminated and the error code is propagated.
+ *
+ * \param nodes Zone nodes.
+ * \param callback Callback function.
+ * \param data Custom data supplied to the callback function.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_nsec_chain_iterate_create(zone_tree_t *nodes,
+ chain_iterate_create_cb callback,
+ nsec_chain_iterate_data_t *data);
+
+/*!
+ * \brief Call the chain-connecting function for modified records and their neighbours.
+ *
+ * \param node_ptrs Tree of those nodes that have ben changed by the update.
+ * \param callback Callback function.
+ * \param cb_reconn Callback for re-connecting "next" link to another node.
+ * \param data Custom data supplied, incl. changeset to be updated.
+ *
+ * \retval KNOT_ENORECORD if the chain must be recreated from scratch.
+ * \return KNOT_E*
+ */
+int knot_nsec_chain_iterate_fix(zone_tree_t *node_ptrs,
+ chain_iterate_create_cb callback,
+ chain_iterate_create_cb cb_reconn,
+ nsec_chain_iterate_data_t *data);
+
+/*!
+ * \brief Add entry for removed NSEC(3) and its RRSIG to the changeset.
+ *
+ * \param n Node to extract NSEC(3) from.
+ * \param update Update to add the old RR removal into.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_nsec_changeset_remove(const zone_node_t *n, zone_update_t *update);
+
+/*!
+ * \brief Checks whether the node is empty or eventually contains only NSEC and
+ * RRSIGs.
+ *
+ * \param n Node to check.
+ *
+ * \retval true if the node is empty or contains only NSEC and RRSIGs.
+ * \retval false otherwise.
+ */
+bool knot_nsec_empty_nsec_and_rrsigs_in_node(const zone_node_t *n);
+
+/*!
+ * \brief Create new NSEC chain.
+ *
+ * \param update Zone update to create NSEC chain for.
+ * \param ttl TTL for created NSEC records.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_nsec_create_chain(zone_update_t *update, uint32_t ttl);
+
+/*!
+ * \brief Fix existing NSEC chain to cover the changes in zone contents.
+ *
+ * \param update Zone update to update NSEC chain for.
+ * \param ttl TTL for created NSEC records.
+ *
+ * \retval KNOT_ENORECORD if the chain must be recreated from scratch.
+ * \return KNOT_E*
+ */
+int knot_nsec_fix_chain(zone_update_t *update, uint32_t ttl);
+
+/*!
+ * \brief Validate NSEC chain in new_cont as whole.
+ *
+ * \note new_cont must have been adjusted already!
+ */
+int knot_nsec_check_chain(zone_update_t *update);
+
+/*!
+ * \brief Validate NSEC chain in new_cont incrementally.
+ */
+int knot_nsec_check_chain_fix(zone_update_t *update);
diff --git a/src/knot/dnssec/nsec3-chain.c b/src/knot/dnssec/nsec3-chain.c
new file mode 100644
index 0000000..97010be
--- /dev/null
+++ b/src/knot/dnssec/nsec3-chain.c
@@ -0,0 +1,733 @@
+/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include "libknot/dname.h"
+#include "knot/dnssec/nsec-chain.h"
+#include "knot/dnssec/nsec3-chain.h"
+#include "knot/dnssec/zone-sign.h"
+#include "knot/dnssec/zone-nsec.h"
+#include "knot/zone/adjust.h"
+#include "knot/zone/zone-diff.h"
+#include "contrib/base32hex.h"
+#include "contrib/wire_ctx.h"
+
+static bool nsec3_empty(const zone_node_t *node, const dnssec_nsec3_params_t *params)
+{
+ bool opt_out = (params->flags & KNOT_NSEC3_FLAG_OPT_OUT);
+ return opt_out ? !(node->flags & NODE_FLAGS_SUBTREE_AUTH) : !(node->flags & NODE_FLAGS_SUBTREE_DATA);
+}
+
+/*!
+ * \brief Check whether at least one RR type in node should be signed,
+ * used when signing with NSEC3.
+ *
+ * \param node Node for which the check is done.
+ *
+ * \return true/false.
+ */
+static bool node_should_be_signed_nsec3(const zone_node_t *n)
+{
+ for (int i = 0; i < n->rrset_count; i++) {
+ knot_rrset_t rrset = node_rrset_at(n, i);
+ if (rrset.type == KNOT_RRTYPE_NSEC ||
+ rrset.type == KNOT_RRTYPE_RRSIG) {
+ continue;
+ }
+
+ if (knot_zone_sign_rr_should_be_signed(n, &rrset)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*!
+ * \brief Custom NSEC3 tree free function.
+ *
+ */
+static void free_nsec3_tree(zone_tree_t *nodes)
+{
+ assert(nodes);
+
+ zone_tree_it_t it = { 0 };
+ for ((void)zone_tree_it_begin(nodes, &it); !zone_tree_it_finished(&it); zone_tree_it_next(&it)) {
+ zone_node_t *node = zone_tree_it_val(&it);
+ // newly allocated NSEC3 nodes
+ knot_rdataset_t *nsec3 = node_rdataset(node, KNOT_RRTYPE_NSEC3);
+ knot_rdataset_t *rrsig = node_rdataset(node, KNOT_RRTYPE_RRSIG);
+ knot_rdataset_clear(nsec3, NULL);
+ knot_rdataset_clear(rrsig, NULL);
+ node_free(node, NULL);
+ }
+
+ zone_tree_it_free(&it);
+ zone_tree_free(&nodes);
+}
+
+/* - NSEC3 nodes construction ----------------------------------------------- */
+
+/*!
+ * \brief Get NSEC3 RDATA size.
+ */
+static size_t nsec3_rdata_size(const dnssec_nsec3_params_t *params,
+ const dnssec_nsec_bitmap_t *rr_types)
+{
+ assert(params);
+ assert(rr_types);
+
+ return 6 + params->salt.size
+ + dnssec_nsec3_hash_length(params->algorithm)
+ + dnssec_nsec_bitmap_size(rr_types);
+}
+
+/*!
+ * \brief Fill NSEC3 RDATA.
+ *
+ * \note Content of next hash field is not changed.
+ */
+static int nsec3_fill_rdata(uint8_t *rdata, size_t rdata_len,
+ const dnssec_nsec3_params_t *params,
+ const dnssec_nsec_bitmap_t *rr_types,
+ const uint8_t *next_hashed)
+{
+ assert(rdata);
+ assert(params);
+ assert(rr_types);
+
+ uint8_t hash_length = dnssec_nsec3_hash_length(params->algorithm);
+
+ wire_ctx_t wire = wire_ctx_init(rdata, rdata_len);
+
+ wire_ctx_write_u8(&wire, params->algorithm);
+ wire_ctx_write_u8(&wire, params->flags);
+ wire_ctx_write_u16(&wire, params->iterations);
+ wire_ctx_write_u8(&wire, params->salt.size);
+ wire_ctx_write(&wire, params->salt.data, params->salt.size);
+ wire_ctx_write_u8(&wire, hash_length);
+
+ if (next_hashed != NULL) {
+ wire_ctx_write(&wire, next_hashed, hash_length);
+ } else {
+ wire_ctx_skip(&wire, hash_length);
+ }
+
+ if (wire.error != KNOT_EOK) {
+ return wire.error;
+ }
+
+ dnssec_nsec_bitmap_write(rr_types, wire.position);
+
+ return KNOT_EOK;
+}
+
+/*!
+ * \brief Creates NSEC3 RRSet.
+ *
+ * \param owner Owner for the RRSet.
+ * \param params Parsed NSEC3PARAM.
+ * \param rr_types Bitmap.
+ * \param next_hashed Next hashed.
+ * \param ttl TTL for the RRSet.
+ *
+ * \return Pointer to created RRSet on success, NULL on errors.
+ */
+static int create_nsec3_rrset(knot_rrset_t *rrset,
+ const knot_dname_t *owner,
+ const dnssec_nsec3_params_t *params,
+ const dnssec_nsec_bitmap_t *rr_types,
+ const uint8_t *next_hashed,
+ uint32_t ttl)
+{
+ assert(rrset);
+ assert(owner);
+ assert(params);
+ assert(rr_types);
+
+ knot_dname_t *owner_copy = knot_dname_copy(owner, NULL);
+ if (owner_copy == NULL) {
+ return KNOT_ENOMEM;
+ }
+ knot_rrset_init(rrset, owner_copy, KNOT_RRTYPE_NSEC3, KNOT_CLASS_IN, ttl);
+
+ size_t rdata_size = nsec3_rdata_size(params, rr_types);
+ uint8_t rdata[rdata_size];
+ memset(rdata, 0, rdata_size);
+ int ret = nsec3_fill_rdata(rdata, rdata_size, params, rr_types,
+ next_hashed);
+ if (ret != KNOT_EOK) {
+ knot_dname_free(owner_copy, NULL);
+ return ret;
+ }
+
+ ret = knot_rrset_add_rdata(rrset, rdata, rdata_size, NULL);
+ if (ret != KNOT_EOK) {
+ knot_dname_free(owner_copy, NULL);
+ return ret;
+ }
+
+ return KNOT_EOK;
+}
+
+/*!
+ * \brief Create NSEC3 node.
+ */
+static zone_node_t *create_nsec3_node(const knot_dname_t *owner,
+ const dnssec_nsec3_params_t *nsec3_params,
+ zone_node_t *apex_node,
+ const dnssec_nsec_bitmap_t *rr_types,
+ uint32_t ttl)
+{
+ assert(owner);
+ assert(nsec3_params);
+ assert(apex_node);
+ assert(rr_types);
+
+ zone_node_t *new_node = node_new(owner, false, false, NULL);
+ if (!new_node) {
+ return NULL;
+ }
+
+ knot_rrset_t nsec3_rrset;
+ int ret = create_nsec3_rrset(&nsec3_rrset, owner, nsec3_params,
+ rr_types, NULL, ttl);
+ if (ret != KNOT_EOK) {
+ node_free(new_node, NULL);
+ return NULL;
+ }
+
+ ret = node_add_rrset(new_node, &nsec3_rrset, NULL);
+ knot_rrset_clear(&nsec3_rrset, NULL);
+ if (ret != KNOT_EOK) {
+ node_free(new_node, NULL);
+ return NULL;
+ }
+
+ return new_node;
+}
+
+/*!
+ * \brief Create new NSEC3 node for given regular node.
+ *
+ * \param node Node for which the NSEC3 node is created.
+ * \param apex Zone apex node.
+ * \param params NSEC3 hash function parameters.
+ * \param ttl TTL of the new NSEC3 node.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static zone_node_t *create_nsec3_node_for_node(const zone_node_t *node,
+ zone_node_t *apex,
+ const dnssec_nsec3_params_t *params,
+ uint32_t ttl)
+{
+ assert(node);
+ assert(apex);
+ assert(params);
+
+ knot_dname_storage_t nsec3_owner;
+ int ret = knot_create_nsec3_owner(nsec3_owner, sizeof(nsec3_owner),
+ node->owner, apex->owner, params);
+ if (ret != KNOT_EOK) {
+ return NULL;
+ }
+
+ dnssec_nsec_bitmap_t *rr_types = dnssec_nsec_bitmap_new();
+ if (!rr_types) {
+ return NULL;
+ }
+
+ bitmap_add_node_rrsets(rr_types, node, false);
+ if (node->rrset_count > 0 && node_should_be_signed_nsec3(node)) {
+ dnssec_nsec_bitmap_add(rr_types, KNOT_RRTYPE_RRSIG);
+ }
+ if (node == apex) {
+ dnssec_nsec_bitmap_add(rr_types, KNOT_RRTYPE_NSEC3PARAM);
+ }
+
+ zone_node_t *nsec3_node = create_nsec3_node(nsec3_owner, params, apex,
+ rr_types, ttl);
+ dnssec_nsec_bitmap_free(rr_types);
+
+ return nsec3_node;
+}
+
+/* - NSEC3 chain creation --------------------------------------------------- */
+
+// see connect_nsec3_nodes() for what this function does
+static int connect_nsec3_base(knot_rdataset_t *a_rrs, const knot_dname_t *b_name)
+{
+ assert(a_rrs);
+ uint8_t algorithm = knot_nsec3_alg(a_rrs->rdata);
+ if (algorithm == 0) {
+ return KNOT_EINVAL;
+ }
+
+ uint8_t raw_length = knot_nsec3_next_len(a_rrs->rdata);
+ assert(raw_length == dnssec_nsec3_hash_length(algorithm));
+ uint8_t *raw_hash = (uint8_t *)knot_nsec3_next(a_rrs->rdata);
+ if (raw_hash == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ assert(b_name);
+ uint8_t b32_length = b_name[0];
+ const uint8_t *b32_hash = &(b_name[1]);
+ int32_t written = knot_base32hex_decode(b32_hash, b32_length, raw_hash, raw_length);
+ if (written != raw_length) {
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+/*!
+ * \brief Connect two nodes by filling 'hash' field of NSEC3 RDATA of the first node.
+ *
+ * \param a First node. Gets modified in-place!
+ * \param b Second node (immediate follower of a).
+ * \param data Unused parameter.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int connect_nsec3_nodes(zone_node_t *a, zone_node_t *b,
+ _unused_ nsec_chain_iterate_data_t *data)
+{
+ assert(a);
+ assert(b);
+ assert(a->rrset_count == 1);
+
+ return connect_nsec3_base(node_rdataset(a, KNOT_RRTYPE_NSEC3), b->owner);
+}
+
+/*!
+ * \brief Connect two nodes by updating the changeset.
+ *
+ * \param a First node.
+ * \param b Second node.
+ * \param data Contains the changeset to be updated.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int connect_nsec3_nodes2(zone_node_t *a, zone_node_t *b,
+ nsec_chain_iterate_data_t *data)
+{
+ assert(data);
+
+ knot_rrset_t aorig = node_rrset(a, KNOT_RRTYPE_NSEC3);
+ assert(!knot_rrset_empty(&aorig));
+
+ // prepare a copy of NSEC3 rrsets in question
+ knot_rrset_t *acopy = knot_rrset_copy(&aorig, NULL);
+ if (acopy == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ // connect the copied rrset
+ int ret = connect_nsec3_base(&acopy->rrs, b->owner);
+ if (ret != KNOT_EOK || knot_rrset_equal(&aorig, acopy, true)) {
+ knot_rrset_free(acopy, NULL);
+ return ret;
+ }
+
+ // add the removed original and the updated copy to changeset
+ ret = zone_update_remove(data->update, &aorig);
+ if (ret == KNOT_EOK) {
+ ret = zone_update_add(data->update, acopy);
+ }
+ knot_rrset_free(acopy, NULL);
+ return ret;
+}
+
+/*!
+ * \brief Replace the "next hash" field in b's NSEC3 by that in a's NSEC3, by updating the changeset.
+ *
+ * \param a A node to take the "next hash" from.
+ * \param b A node to put the "next hash" into.
+ * \param data Contains the changeset to be updated.
+ *
+ * \return KNOT_E*
+ */
+static int reconnect_nsec3_nodes2(zone_node_t *a, zone_node_t *b,
+ nsec_chain_iterate_data_t *data)
+{
+ assert(data);
+
+ knot_rrset_t an = node_rrset(a, KNOT_RRTYPE_NSEC3);
+ assert(!knot_rrset_empty(&an));
+
+ knot_rrset_t bnorig = node_rrset(b, KNOT_RRTYPE_NSEC3);
+ assert(!knot_rrset_empty(&bnorig));
+
+ // prepare a copy of NSEC3 rrsets in question
+ knot_rrset_t *bnnew = knot_rrset_copy(&bnorig, NULL);
+ if (bnnew == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ uint8_t raw_length = knot_nsec3_next_len(an.rrs.rdata);
+ uint8_t *a_hash = (uint8_t *)knot_nsec3_next(an.rrs.rdata);
+ uint8_t *bnew_hash = (uint8_t *)knot_nsec3_next(bnnew->rrs.rdata);
+ if (a_hash == NULL || bnew_hash == NULL ||
+ raw_length != knot_nsec3_next_len(bnnew->rrs.rdata)) {
+ knot_rrset_free(bnnew, NULL);
+ return KNOT_ERROR;
+ }
+ memcpy(bnew_hash, a_hash, raw_length);
+
+ int ret = zone_update_remove(data->update, &bnorig);
+ if (ret == KNOT_EOK) {
+ ret = zone_update_add(data->update, bnnew);
+ }
+ knot_rrset_free(bnnew, NULL);
+ return ret;
+}
+
+/*!
+ * \brief Create NSEC3 node for each regular node in the zone.
+ *
+ * \param zone Zone.
+ * \param params NSEC3 params.
+ * \param ttl TTL for the created NSEC records.
+ * \param cds_in_apex Hint to guess apex node type bitmap: false=just DNSKEY, true=DNSKEY,CDS,CDNSKEY.
+ * \param nsec3_nodes Tree whereto new NSEC3 nodes will be added.
+ * \param update Zone update for possible NSEC removals
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int create_nsec3_nodes(const zone_contents_t *zone,
+ const dnssec_nsec3_params_t *params,
+ uint32_t ttl,
+ zone_tree_t *nsec3_nodes,
+ zone_update_t *update)
+{
+ assert(zone);
+ assert(nsec3_nodes);
+ assert(update);
+
+ zone_tree_delsafe_it_t it = { 0 };
+ int result = zone_tree_delsafe_it_begin(zone->nodes, &it, false); // delsafe - removing nodes that contain only NSEC+RRSIG
+
+ while (!zone_tree_delsafe_it_finished(&it)) {
+ zone_node_t *node = zone_tree_delsafe_it_val(&it);
+
+ /*!
+ * Remove possible NSEC from the node. (Do not allow both NSEC
+ * and NSEC3 in the zone at once.)
+ */
+ result = knot_nsec_changeset_remove(node, update);
+ if (result != KNOT_EOK) {
+ break;
+ }
+ if (node->flags & NODE_FLAGS_NONAUTH || nsec3_empty(node, params) || node->flags & NODE_FLAGS_DELETED) {
+ zone_tree_delsafe_it_next(&it);
+ continue;
+ }
+
+ zone_node_t *nsec3_node;
+ nsec3_node = create_nsec3_node_for_node(node, zone->apex,
+ params, ttl);
+ if (!nsec3_node) {
+ result = KNOT_ENOMEM;
+ break;
+ }
+
+ result = zone_tree_insert(nsec3_nodes, &nsec3_node);
+ if (result != KNOT_EOK) {
+ break;
+ }
+
+ zone_tree_delsafe_it_next(&it);
+ }
+
+ zone_tree_delsafe_it_free(&it);
+
+ return result;
+}
+
+/*!
+ * \brief For given dname, check if anything changed in zone_update, and recreate (possibly unconnected) NSEC3 nodes appropriately.
+ *
+ * \param update Zone update structure holding zone contents changes.
+ * \param params NSEC3 params.
+ * \param ttl TTL for newly created NSEC3 records.
+ * \param for_node Domain name of the node in question.
+ *
+ * \retval KNOT_ENORECORD if the NSEC3 chain shall be rather recreated completely.
+ * \return KNOT_EOK, KNOT_E* if any error.
+ */
+static int fix_nsec3_for_node(zone_update_t *update, const dnssec_nsec3_params_t *params,
+ uint32_t ttl, const knot_dname_t *for_node)
+{
+ // check if we need to do something
+ const zone_node_t *old_n = zone_contents_find_node(update->zone->contents, for_node);
+ const zone_node_t *new_n = zone_contents_find_node(update->new_cont, for_node);
+
+ bool had_no_nsec = (old_n == NULL || old_n->nsec3_node == NULL || !(old_n->flags & NODE_FLAGS_NSEC3_NODE));
+ bool shall_no_nsec = (new_n == NULL || new_n->flags & NODE_FLAGS_NONAUTH || nsec3_empty(new_n, params) || new_n->flags & NODE_FLAGS_DELETED);
+
+ if (had_no_nsec == shall_no_nsec && node_bitmap_equal(old_n, new_n)) {
+ return KNOT_EOK;
+ }
+
+ knot_dname_storage_t for_node_hashed;
+ int ret = knot_create_nsec3_owner(for_node_hashed, sizeof(for_node_hashed),
+ for_node, update->new_cont->apex->owner, params);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // saved hash of next node
+ uint8_t *next_hash = NULL;
+ uint8_t next_length = 0;
+
+ // remove (all) existing NSEC3
+ const zone_node_t *old_nsec3_n = zone_contents_find_nsec3_node(update->new_cont, for_node_hashed);
+ assert((bool)(old_nsec3_n == NULL) == had_no_nsec);
+ if (old_nsec3_n != NULL) {
+ knot_rrset_t rem_nsec3 = node_rrset(old_nsec3_n, KNOT_RRTYPE_NSEC3);
+ if (!knot_rrset_empty(&rem_nsec3)) {
+ knot_rrset_t rem_rrsig = node_rrset(old_nsec3_n, KNOT_RRTYPE_RRSIG);
+ ret = zone_update_remove(update, &rem_nsec3);
+ if (ret == KNOT_EOK && !knot_rrset_empty(&rem_rrsig)) {
+ ret = zone_update_remove(update, &rem_rrsig);
+ }
+ assert(update->flags & UPDATE_INCREMENTAL); // to make sure the following pointer remains valid
+ next_hash = (uint8_t *)knot_nsec3_next(rem_nsec3.rrs.rdata);
+ next_length = knot_nsec3_next_len(rem_nsec3.rrs.rdata);
+ }
+ }
+
+ // add NSEC3 with correct bitmap
+ if (!shall_no_nsec && ret == KNOT_EOK) {
+ zone_node_t *new_nsec3_n = create_nsec3_node_for_node(new_n, update->new_cont->apex, params, ttl);
+ if (new_nsec3_n == NULL) {
+ return KNOT_ENOMEM;
+ }
+ knot_rrset_t nsec3 = node_rrset(new_nsec3_n, KNOT_RRTYPE_NSEC3);
+ assert(!knot_rrset_empty(&nsec3));
+
+ // copy hash of next element from removed record
+ if (next_hash != NULL) {
+ uint8_t *raw_hash = (uint8_t *)knot_nsec3_next(nsec3.rrs.rdata);
+ uint8_t raw_length = knot_nsec3_next_len(nsec3.rrs.rdata);
+ assert(raw_hash != NULL);
+ if (raw_length != next_length) {
+ ret = KNOT_EMALF;
+ } else {
+ memcpy(raw_hash, next_hash, raw_length);
+ }
+ }
+ if (ret == KNOT_EOK) {
+ ret = zone_update_add(update, &nsec3);
+ }
+ binode_unify(new_nsec3_n, false, NULL);
+ node_free_rrsets(new_nsec3_n, NULL);
+ node_free(new_nsec3_n, NULL);
+ }
+
+ return ret;
+}
+
+static int fix_nsec3_nodes(zone_update_t *update, const dnssec_nsec3_params_t *params,
+ uint32_t ttl)
+{
+ assert(update);
+
+ zone_tree_it_t it = { 0 };
+ int ret = zone_tree_it_begin(update->a_ctx->node_ptrs, &it);
+
+ while (!zone_tree_it_finished(&it) && ret == KNOT_EOK) {
+ zone_node_t *n = zone_tree_it_val(&it);
+ ret = fix_nsec3_for_node(update, params, ttl, n->owner);
+ zone_tree_it_next(&it);
+ }
+ zone_tree_it_free(&it);
+
+ return ret;
+}
+
+static int zone_update_nsec3_nodes(zone_update_t *up, zone_tree_t *nsec3n)
+{
+ int ret = KNOT_EOK;
+ zone_tree_delsafe_it_t dit = { 0 };
+ zone_tree_it_t it = { 0 };
+ if (up->new_cont->nsec3_nodes == NULL) {
+ goto add_nsec3n;
+ }
+ ret = zone_tree_delsafe_it_begin(up->new_cont->nsec3_nodes, &dit, false);
+ while (ret == KNOT_EOK && !zone_tree_delsafe_it_finished(&dit)) {
+ zone_node_t *nold = zone_tree_delsafe_it_val(&dit);
+ knot_rrset_t ns3old = node_rrset(nold, KNOT_RRTYPE_NSEC3);
+ zone_node_t *nnew = zone_tree_get(nsec3n, nold->owner);
+ if (!knot_rrset_empty(&ns3old)) {
+ knot_rrset_t ns3new = node_rrset(nnew, KNOT_RRTYPE_NSEC3);
+ if (knot_rrset_equal(&ns3old, &ns3new, true)) {
+ node_remove_rdataset(nnew, KNOT_RRTYPE_NSEC3);
+ } else {
+ ret = knot_nsec_changeset_remove(nold, up);
+ }
+ } else if (node_rrtype_exists(nold, KNOT_RRTYPE_RRSIG)) {
+ ret = knot_nsec_changeset_remove(nold, up);
+ }
+ zone_tree_delsafe_it_next(&dit);
+ }
+ zone_tree_delsafe_it_free(&dit);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+add_nsec3n:
+ ret = zone_tree_it_begin(nsec3n, &it);
+ while (ret == KNOT_EOK && !zone_tree_it_finished(&it)) {
+ zone_node_t *nnew = zone_tree_it_val(&it);
+ knot_rrset_t ns3new = node_rrset(nnew, KNOT_RRTYPE_NSEC3);
+ if (!knot_rrset_empty(&ns3new)) {
+ ret = zone_update_add(up, &ns3new);
+ }
+ zone_tree_it_next(&it);
+ }
+ zone_tree_it_free(&it);
+ return ret;
+}
+
+/* - Public API ------------------------------------------------------------- */
+
+int delete_nsec3_chain(zone_update_t *up)
+{
+ zone_tree_t *empty = zone_tree_create(false);
+ if (empty == NULL) {
+ return KNOT_ENOMEM;
+ }
+ int ret = zone_update_nsec3_nodes(up, empty);
+ zone_tree_free(&empty);
+ return ret;
+}
+
+/*!
+ * \brief Create new NSEC3 chain, add differences from current into a changeset.
+ */
+int knot_nsec3_create_chain(const zone_contents_t *zone,
+ const dnssec_nsec3_params_t *params,
+ uint32_t ttl,
+ zone_update_t *update)
+{
+ assert(zone);
+ assert(params);
+
+ zone_tree_t *nsec3_nodes = zone_tree_create(false);
+ if (!nsec3_nodes) {
+ return KNOT_ENOMEM;
+ }
+
+ int result = create_nsec3_nodes(zone, params, ttl, nsec3_nodes, update);
+ if (result != KNOT_EOK) {
+ free_nsec3_tree(nsec3_nodes);
+ return result;
+ }
+
+ result = knot_nsec_chain_iterate_create(nsec3_nodes,
+ connect_nsec3_nodes, NULL);
+ if (result != KNOT_EOK) {
+ free_nsec3_tree(nsec3_nodes);
+ return result;
+ }
+
+ result = zone_update_nsec3_nodes(update, nsec3_nodes);
+
+ free_nsec3_tree(nsec3_nodes);
+
+ return result;
+}
+
+int knot_nsec3_fix_chain(zone_update_t *update,
+ const dnssec_nsec3_params_t *params,
+ uint32_t ttl)
+{
+ assert(update);
+ assert(params);
+
+ // ensure that the salt has not changed
+ if (!knot_nsec3param_uptodate(update->new_cont, params)) {
+ int ret = knot_nsec3param_update(update, params, ttl);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ return knot_nsec3_create_chain(update->new_cont, params, ttl, update);
+ }
+
+ int ret = fix_nsec3_nodes(update, params, ttl);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = zone_adjust_contents(update->new_cont, NULL, adjust_cb_void, false, true, 1, update->a_ctx->nsec3_ptrs);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // ensure that nsec3 node for zone root is in list of changed nodes
+ const zone_node_t *nsec3_for_root = NULL, *unused;
+ ret = zone_contents_find_nsec3_for_name(update->new_cont, update->zone->name, &nsec3_for_root, &unused);
+ if (ret >= 0) {
+ assert(ret == ZONE_NAME_FOUND);
+ assert(!(nsec3_for_root->flags & NODE_FLAGS_DELETED));
+ assert(!(binode_counterpart((zone_node_t *)nsec3_for_root)->flags & NODE_FLAGS_DELETED));
+ ret = zone_tree_insert(update->a_ctx->nsec3_ptrs, (zone_node_t **)&nsec3_for_root);
+ }
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ nsec_chain_iterate_data_t data = { ttl, update, KNOT_RRTYPE_NSEC3 };
+
+ ret = knot_nsec_chain_iterate_fix(update->a_ctx->nsec3_ptrs,
+ connect_nsec3_nodes2, reconnect_nsec3_nodes2, &data);
+
+ return ret;
+}
+
+int knot_nsec3_check_chain(zone_update_t *update, const dnssec_nsec3_params_t *params)
+{
+ nsec_chain_iterate_data_t data = { 0, update, KNOT_RRTYPE_NSEC3, params };
+
+ int ret = nsec_check_bitmaps(update->new_cont->nodes, &data);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ return knot_nsec_chain_iterate_create(update->new_cont->nsec3_nodes,
+ nsec_check_connect_nodes, &data);
+}
+
+int knot_nsec3_check_chain_fix(zone_update_t *update, const dnssec_nsec3_params_t *params)
+{
+ nsec_chain_iterate_data_t data = { 0, update, KNOT_RRTYPE_NSEC3, params };
+
+ int ret = nsec_check_bitmaps(update->a_ctx->node_ptrs, &data);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = nsec_check_bitmaps(update->a_ctx->adjust_ptrs, &data); // adjust_ptrs contain also NSEC3-nodes. See check_nsec_bitmap() how this is handled.
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ return nsec_check_new_connects(update->a_ctx->nsec3_ptrs, &data);
+}
diff --git a/src/knot/dnssec/nsec3-chain.h b/src/knot/dnssec/nsec3-chain.h
new file mode 100644
index 0000000..5b3708f
--- /dev/null
+++ b/src/knot/dnssec/nsec3-chain.h
@@ -0,0 +1,69 @@
+/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdint.h>
+#include "libdnssec/nsec.h"
+#include "knot/updates/changesets.h"
+#include "knot/updates/zone-update.h"
+#include "knot/zone/contents.h"
+
+/*!
+ * \brief delete_nsec3_chain Delete all NSEC3 records and their RRSIGs.
+ */
+int delete_nsec3_chain(zone_update_t *up);
+
+/*!
+ * \brief Creates new NSEC3 chain, add differences from current into a changeset.
+ *
+ * \param zone Zone to be checked.
+ * \param params NSEC3 parameters.
+ * \param ttl TTL for new records.
+ * \param update Zone update to stare immediate changes into.
+ *
+ * \return KNOT_E*
+ */
+int knot_nsec3_create_chain(const zone_contents_t *zone,
+ const dnssec_nsec3_params_t *params,
+ uint32_t ttl,
+ zone_update_t *update);
+
+/*!
+ * \brief Updates zone's NSEC3 chain to follow the differences in zone update.
+ *
+ * \param update Zone Update structure holding the zone and its update. Also modified!
+ * \param params NSEC3 parameters.
+ * \param ttl TTL for new records.
+ *
+ * \retval KNOT_ENORECORD if the chain must be recreated from scratch.
+ * \return KNOT_E*
+ */
+int knot_nsec3_fix_chain(zone_update_t *update,
+ const dnssec_nsec3_params_t *params,
+ uint32_t ttl);
+
+/*!
+ * \brief Validate NSEC3 chain in new_cont as whole.
+ *
+ * \note new_cont must have been adjusted already!
+ */
+int knot_nsec3_check_chain(zone_update_t *update, const dnssec_nsec3_params_t *params);
+
+/*!
+ * \brief Validate NSEC3 chain in new_cont incrementally.
+ */
+int knot_nsec3_check_chain_fix(zone_update_t *update, const dnssec_nsec3_params_t *params);
diff --git a/src/knot/dnssec/policy.c b/src/knot/dnssec/policy.c
new file mode 100644
index 0000000..2589ae6
--- /dev/null
+++ b/src/knot/dnssec/policy.c
@@ -0,0 +1,51 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include "knot/dnssec/policy.h"
+#include "libknot/rrtype/soa.h"
+
+static uint32_t zone_soa_ttl(const zone_contents_t *zone)
+{
+ knot_rrset_t soa = node_rrset(zone->apex, KNOT_RRTYPE_SOA);
+ return soa.ttl;
+}
+
+void update_policy_from_zone(knot_kasp_policy_t *policy,
+ const zone_contents_t *zone)
+{
+ assert(policy);
+ assert(zone);
+
+ if (policy->dnskey_ttl == UINT32_MAX) {
+ policy->dnskey_ttl = zone_soa_ttl(zone);
+ }
+ if (policy->saved_key_ttl == 0) { // possibly not set yet
+ policy->saved_key_ttl = policy->dnskey_ttl;
+ }
+
+ if (policy->zone_maximal_ttl == UINT32_MAX) {
+ policy->zone_maximal_ttl = zone->max_ttl;
+ if (policy->rrsig_refresh_before == UINT32_MAX) {
+ policy->rrsig_refresh_before = policy->propagation_delay +
+ policy->zone_maximal_ttl;
+ }
+ }
+ if (policy->saved_max_ttl == 0) { // possibly not set yet
+ policy->saved_max_ttl = policy->zone_maximal_ttl;
+ }
+}
diff --git a/src/knot/dnssec/policy.h b/src/knot/dnssec/policy.h
new file mode 100644
index 0000000..8c0149b
--- /dev/null
+++ b/src/knot/dnssec/policy.h
@@ -0,0 +1,26 @@
+/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/dnssec/context.h"
+#include "knot/zone/contents.h"
+
+/*!
+ * \brief Update policy parameters depending on zone content.
+ */
+void update_policy_from_zone(knot_kasp_policy_t *policy,
+ const zone_contents_t *zone);
diff --git a/src/knot/dnssec/rrset-sign.c b/src/knot/dnssec/rrset-sign.c
new file mode 100644
index 0000000..3522a24
--- /dev/null
+++ b/src/knot/dnssec/rrset-sign.c
@@ -0,0 +1,425 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include "contrib/wire_ctx.h"
+#include "libdnssec/error.h"
+#include "knot/dnssec/rrset-sign.h"
+#include "knot/dnssec/zone-sign.h"
+#include "knot/zone/serial.h" // DNS uint32 arithmetics
+#include "libknot/libknot.h"
+
+#define RRSIG_RDATA_SIGNER_OFFSET 18
+
+#define RRSIG_INCEPT_IN_PAST (90 * 60)
+
+/*- Creating of RRSIGs -------------------------------------------------------*/
+
+/*!
+ * \brief Get size of RRSIG RDATA for a given key without signature.
+ */
+static size_t rrsig_rdata_header_size(const dnssec_key_t *key)
+{
+ if (!key) {
+ return 0;
+ }
+
+ size_t size;
+
+ // static part
+
+ size = sizeof(uint16_t) // type covered
+ + sizeof(uint8_t) // algorithm
+ + sizeof(uint8_t) // labels
+ + sizeof(uint32_t) // original TTL
+ + sizeof(uint32_t) // signature expiration
+ + sizeof(uint32_t) // signature inception
+ + sizeof(uint16_t); // key tag (footprint)
+
+ assert(size == RRSIG_RDATA_SIGNER_OFFSET);
+
+ // variable part
+
+ size += knot_dname_size(dnssec_key_get_dname(key));
+
+ return size;
+}
+
+/*!
+ * \brief Write RRSIG RDATA except signature.
+ *
+ * \note This can be also used for SIG(0) if proper parameters are supplied.
+ *
+ * \param rdata_len Length of RDATA.
+ * \param rdata Pointer to RDATA.
+ * \param key Key used for signing.
+ * \param covered_type Type of the covered RR.
+ * \param owner_labels Number of labels covered by the signature.
+ * \param sig_incepted Timestamp of signature inception.
+ * \param sig_expires Timestamp of signature expiration.
+ */
+static int rrsig_write_rdata(uint8_t *rdata, size_t rdata_len,
+ const dnssec_key_t *key,
+ uint16_t covered_type, uint8_t owner_labels,
+ uint32_t owner_ttl, uint32_t sig_incepted,
+ uint32_t sig_expires)
+{
+ if (!rdata || !key || serial_compare(sig_incepted, sig_expires) != SERIAL_LOWER) {
+ return KNOT_EINVAL;
+ }
+
+ uint8_t algorithm = dnssec_key_get_algorithm(key);
+ uint16_t keytag = dnssec_key_get_keytag(key);
+ const uint8_t *signer = dnssec_key_get_dname(key);
+ assert(signer);
+
+ wire_ctx_t wire = wire_ctx_init(rdata, rdata_len);
+
+ wire_ctx_write_u16(&wire, covered_type); // type covered
+ wire_ctx_write_u8(&wire, algorithm); // algorithm
+ wire_ctx_write_u8(&wire, owner_labels); // labels
+ wire_ctx_write_u32(&wire, owner_ttl); // original TTL
+ wire_ctx_write_u32(&wire, sig_expires); // signature expiration
+ wire_ctx_write_u32(&wire, sig_incepted); // signature inception
+ wire_ctx_write_u16(&wire, keytag); // key fingerprint
+ assert(wire_ctx_offset(&wire) == RRSIG_RDATA_SIGNER_OFFSET);
+ wire_ctx_write(&wire, signer, knot_dname_size(signer)); // signer
+
+ return wire.error;
+}
+
+/*- Computation of signatures ------------------------------------------------*/
+
+/*!
+ * \brief Add RRSIG RDATA without signature to signing context.
+ *
+ * Requires signer name in RDATA in canonical form.
+ *
+ * \param ctx Signing context.
+ * \param rdata Pointer to RRSIG RDATA.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int sign_ctx_add_self(dnssec_sign_ctx_t *ctx, const uint8_t *rdata)
+{
+ assert(ctx);
+ assert(rdata);
+
+ int result;
+
+ // static header
+
+ dnssec_binary_t header = { 0 };
+ header.data = (uint8_t *)rdata;
+ header.size = RRSIG_RDATA_SIGNER_OFFSET;
+
+ result = dnssec_sign_add(ctx, &header);
+ if (result != DNSSEC_EOK) {
+ return result;
+ }
+
+ // signer name
+
+ const uint8_t *rdata_signer = rdata + RRSIG_RDATA_SIGNER_OFFSET;
+ dnssec_binary_t signer = { 0 };
+ signer.data = knot_dname_copy(rdata_signer, NULL);
+ signer.size = knot_dname_size(signer.data);
+
+ result = dnssec_sign_add(ctx, &signer);
+ free(signer.data);
+
+ return result;
+}
+
+/*!
+ * \brief Add covered RRs to signing context.
+ *
+ * Requires all DNAMEs in canonical form and all RRs ordered canonically.
+ *
+ * \param ctx Signing context.
+ * \param covered Covered RRs.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int sign_ctx_add_records(dnssec_sign_ctx_t *ctx, const knot_rrset_t *covered)
+{
+ // huge block of rrsets can be optionally created
+ uint8_t *rrwf = malloc(KNOT_WIRE_MAX_PKTSIZE);
+ if (!rrwf) {
+ return KNOT_ENOMEM;
+ }
+
+ int written = knot_rrset_to_wire(covered, rrwf, KNOT_WIRE_MAX_PKTSIZE, NULL);
+ if (written < 0) {
+ free(rrwf);
+ return written;
+ }
+
+ dnssec_binary_t rrset_wire = { 0 };
+ rrset_wire.size = written;
+ rrset_wire.data = rrwf;
+ int result = dnssec_sign_add(ctx, &rrset_wire);
+ free(rrwf);
+
+ return result;
+}
+
+int knot_sign_ctx_add_data(dnssec_sign_ctx_t *ctx,
+ const uint8_t *rrsig_rdata,
+ const knot_rrset_t *covered)
+{
+ if (!ctx || !rrsig_rdata || knot_rrset_empty(covered)) {
+ return KNOT_EINVAL;
+ }
+
+ int result = sign_ctx_add_self(ctx, rrsig_rdata);
+ if (result != KNOT_EOK) {
+ return result;
+ }
+
+ return sign_ctx_add_records(ctx, covered);
+}
+
+/*!
+ * \brief Create RRSIG RDATA.
+ *
+ * \param[in] rrsigs RR set with RRSIGS.
+ * \param[in] ctx DNSSEC signing context.
+ * \param[in] covered RR covered by the signature.
+ * \param[in] key Key used for signing.
+ * \param[in] sig_incepted Timestamp of signature inception.
+ * \param[in] sig_expires Timestamp of signature expiration.
+ * \param[in] sign_flags Signing flags.
+ * \param[in] mm Memory context.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int rrsigs_create_rdata(knot_rrset_t *rrsigs, dnssec_sign_ctx_t *ctx,
+ const knot_rrset_t *covered,
+ const dnssec_key_t *key,
+ uint32_t sig_incepted, uint32_t sig_expires,
+ dnssec_sign_flags_t sign_flags,
+ knot_mm_t *mm)
+{
+ assert(rrsigs);
+ assert(rrsigs->type == KNOT_RRTYPE_RRSIG);
+ assert(!knot_rrset_empty(covered));
+ assert(key);
+
+ size_t header_size = rrsig_rdata_header_size(key);
+ assert(header_size != 0);
+
+ uint8_t owner_labels = knot_dname_labels(covered->owner, NULL);
+ if (knot_dname_is_wildcard(covered->owner)) {
+ owner_labels -= 1;
+ }
+
+ uint8_t header[header_size];
+ int res = rrsig_write_rdata(header, header_size,
+ key, covered->type, owner_labels,
+ covered->ttl, sig_incepted, sig_expires);
+ assert(res == KNOT_EOK);
+
+ res = dnssec_sign_init(ctx);
+ if (res != KNOT_EOK) {
+ return res;
+ }
+
+ res = knot_sign_ctx_add_data(ctx, header, covered);
+ if (res != KNOT_EOK) {
+ return res;
+ }
+
+ dnssec_binary_t signature = { 0 };
+ res = dnssec_sign_write(ctx, sign_flags, &signature);
+ if (res != DNSSEC_EOK) {
+ return res;
+ }
+ assert(signature.size > 0);
+
+ size_t rrsig_size = header_size + signature.size;
+ uint8_t rrsig[rrsig_size];
+ memcpy(rrsig, header, header_size);
+ memcpy(rrsig + header_size, signature.data, signature.size);
+
+ dnssec_binary_free(&signature);
+
+ return knot_rrset_add_rdata(rrsigs, rrsig, rrsig_size, mm);
+}
+
+int knot_sign_rrset(knot_rrset_t *rrsigs, const knot_rrset_t *covered,
+ const dnssec_key_t *key, dnssec_sign_ctx_t *sign_ctx,
+ const kdnssec_ctx_t *dnssec_ctx, knot_mm_t *mm, knot_time_t *expires)
+{
+ if (knot_rrset_empty(covered) || !key || !sign_ctx || !dnssec_ctx ||
+ rrsigs->type != KNOT_RRTYPE_RRSIG ||
+ !knot_dname_is_equal(rrsigs->owner, covered->owner)
+ ) {
+ return KNOT_EINVAL;
+ }
+
+ uint64_t sig_incept = dnssec_ctx->now - RRSIG_INCEPT_IN_PAST;
+ uint64_t sig_expire = dnssec_ctx->now + dnssec_ctx->policy->rrsig_lifetime;
+ dnssec_sign_flags_t sign_flags = dnssec_ctx->policy->reproducible_sign ?
+ DNSSEC_SIGN_REPRODUCIBLE : DNSSEC_SIGN_NORMAL;
+
+ int ret = rrsigs_create_rdata(rrsigs, sign_ctx, covered, key, (uint32_t)sig_incept,
+ (uint32_t)sig_expire, sign_flags, mm);
+ if (ret == KNOT_EOK && expires != NULL) {
+ *expires = knot_time_min(*expires, sig_expire);
+ }
+ return ret;
+}
+
+int knot_sign_rrset2(knot_rrset_t *rrsigs, const knot_rrset_t *rrset,
+ zone_sign_ctx_t *sign_ctx, knot_mm_t *mm)
+{
+ if (rrsigs == NULL || rrset == NULL || sign_ctx == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ for (size_t i = 0; i < sign_ctx->count; i++) {
+ zone_key_t *key = &sign_ctx->keys[i];
+
+ if (!knot_zone_sign_use_key(key, rrset)) {
+ continue;
+ }
+
+ int ret = knot_sign_rrset(rrsigs, rrset, key->key, sign_ctx->sign_ctxs[i],
+ sign_ctx->dnssec_ctx, mm, NULL);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+int knot_synth_rrsig(uint16_t type, const knot_rdataset_t *rrsig_rrs,
+ knot_rdataset_t *out_sig, knot_mm_t *mm)
+{
+ if (rrsig_rrs == NULL) {
+ return KNOT_ENOENT;
+ }
+
+ if (out_sig == NULL || out_sig->count > 0) {
+ return KNOT_EINVAL;
+ }
+
+ knot_rdata_t *rr_to_copy = rrsig_rrs->rdata;
+ for (int i = 0; i < rrsig_rrs->count; ++i) {
+ if (type == KNOT_RRTYPE_ANY) {
+ type = knot_rrsig_type_covered(rr_to_copy);
+ }
+ if (type == knot_rrsig_type_covered(rr_to_copy)) {
+ int ret = knot_rdataset_add(out_sig, rr_to_copy, mm);
+ if (ret != KNOT_EOK) {
+ knot_rdataset_clear(out_sig, mm);
+ return ret;
+ }
+ }
+ rr_to_copy = knot_rdataset_next(rr_to_copy);
+ }
+
+ return out_sig->count > 0 ? KNOT_EOK : KNOT_ENOENT;
+}
+
+bool knot_synth_rrsig_exists(uint16_t type, const knot_rdataset_t *rrsig_rrs)
+{
+ if (rrsig_rrs == NULL) {
+ return false;
+ }
+
+ knot_rdata_t *rr = rrsig_rrs->rdata;
+ for (int i = 0; i < rrsig_rrs->count; ++i) {
+ if (type == knot_rrsig_type_covered(rr)) {
+ return true;
+ }
+ rr = knot_rdataset_next(rr);
+ }
+
+ return false;
+}
+
+/*- Verification of signatures -----------------------------------------------*/
+
+static bool is_expired_signature(const knot_rdata_t *rrsig, knot_time_t now,
+ uint32_t refresh_before)
+{
+ assert(rrsig);
+
+ uint32_t expire32 = knot_rrsig_sig_expiration(rrsig);
+ uint32_t incept32 = knot_rrsig_sig_inception(rrsig);
+ knot_time_t expire64 = knot_time_from_u32(expire32, now);
+ knot_time_t incept64 = knot_time_from_u32(incept32, now);
+
+ return now >= expire64 - refresh_before || now < incept64;
+}
+
+int knot_check_signature(const knot_rrset_t *covered,
+ const knot_rrset_t *rrsigs, size_t pos,
+ const dnssec_key_t *key,
+ dnssec_sign_ctx_t *sign_ctx,
+ const kdnssec_ctx_t *dnssec_ctx,
+ knot_timediff_t refresh,
+ bool skip_crypto)
+{
+ if (knot_rrset_empty(covered) || knot_rrset_empty(rrsigs) || !key ||
+ !sign_ctx || !dnssec_ctx) {
+ return KNOT_EINVAL;
+ }
+
+ knot_rdata_t *rrsig = knot_rdataset_at(&rrsigs->rrs, pos);
+ assert(rrsig);
+
+ if (!(dnssec_ctx->policy->unsafe & UNSAFE_EXPIRED) &&
+ is_expired_signature(rrsig, dnssec_ctx->now, refresh)) {
+ return DNSSEC_INVALID_SIGNATURE;
+ }
+
+ if (skip_crypto) {
+ return KNOT_EOK;
+ }
+
+ // identify fields in the signature being validated
+
+ dnssec_binary_t signature = {
+ .size = knot_rrsig_signature_len(rrsig),
+ .data = (uint8_t *)knot_rrsig_signature(rrsig)
+ };
+ if (signature.data == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ // perform the validation
+
+ int result = dnssec_sign_init(sign_ctx);
+ if (result != KNOT_EOK) {
+ return result;
+ }
+
+ result = knot_sign_ctx_add_data(sign_ctx, rrsig->data, covered);
+ if (result != KNOT_EOK) {
+ return result;
+ }
+
+ bool sign_cmp = dnssec_algorithm_reproducible(
+ dnssec_ctx->policy->algorithm,
+ dnssec_ctx->policy->reproducible_sign);
+
+ return dnssec_sign_verify(sign_ctx, sign_cmp, &signature);
+}
diff --git a/src/knot/dnssec/rrset-sign.h b/src/knot/dnssec/rrset-sign.h
new file mode 100644
index 0000000..8e00402
--- /dev/null
+++ b/src/knot/dnssec/rrset-sign.h
@@ -0,0 +1,123 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "libdnssec/key.h"
+#include "libdnssec/sign.h"
+#include "knot/dnssec/context.h"
+#include "knot/dnssec/zone-keys.h"
+#include "libknot/rrset.h"
+
+/*!
+ * \brief Create RRSIG RR for given RR set.
+ *
+ * \param rrsigs RR set with RRSIGs into which the result will be added.
+ * \param covered RR set to create a new signature for.
+ * \param key Signing key.
+ * \param sign_ctx Signing context.
+ * \param dnssec_ctx DNSSEC context.
+ * \param mm Memory context.
+ * \param expires Out: When will the new RRSIG expire.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_sign_rrset(knot_rrset_t *rrsigs,
+ const knot_rrset_t *covered,
+ const dnssec_key_t *key,
+ dnssec_sign_ctx_t *sign_ctx,
+ const kdnssec_ctx_t *dnssec_ctx,
+ knot_mm_t *mm,
+ knot_time_t *expires);
+
+/*!
+ * \brief Create RRSIG RR for given RR set, choose which key to use.
+ *
+ * \param rrsigs RR set with RRSIGs into which the result will be added.
+ * \param rrset RR set to create a new signature for.
+ * \param sign_ctx Zone signing context.
+ * \param mm Memory context.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_sign_rrset2(knot_rrset_t *rrsigs,
+ const knot_rrset_t *rrset,
+ zone_sign_ctx_t *sign_ctx,
+ knot_mm_t *mm);
+
+/*!
+ * \brief Add all data covered by signature into signing context.
+ *
+ * RFC 4034: The signature covers RRSIG RDATA field (excluding the signature)
+ * and all matching RR records, which are ordered canonically.
+ *
+ * Requires all DNAMEs in canonical form and all RRs ordered canonically.
+ *
+ * \param ctx Signing context.
+ * \param rrsig_rdata RRSIG RDATA with populated fields except signature.
+ * \param covered Covered RRs.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_sign_ctx_add_data(dnssec_sign_ctx_t *ctx,
+ const uint8_t *rrsig_rdata,
+ const knot_rrset_t *covered);
+
+/*!
+ * \brief Creates new RRS using \a rrsig_rrs as a source. Only those RRs that
+ * cover given \a type are copied into \a out_sig
+ *
+ * \note If given \a type is ANY, put a random subset, not all.
+ *
+ * \param type Covered type.
+ * \param rrsig_rrs Source RRS.
+ * \param out_sig Output RRS.
+ * \param mm Memory context.
+ *
+ * \retval KNOT_EOK if some RRSIG was found.
+ * \retval KNOT_EINVAL if no RRSIGs were found.
+ * \retval Error code other than EINVAL on error.
+ */
+int knot_synth_rrsig(uint16_t type, const knot_rdataset_t *rrsig_rrs,
+ knot_rdataset_t *out_sig, knot_mm_t *mm);
+
+/*!
+ * \brief Determines if a RRSIG exists, covering the specified type.
+ */
+bool knot_synth_rrsig_exists(uint16_t type, const knot_rdataset_t *rrsig_rrs);
+
+/*!
+ * \brief Check if RRSIG signature is valid.
+ *
+ * \param covered RRs covered by the signature.
+ * \param rrsigs RR set with RRSIGs.
+ * \param pos Number of RRSIG RR in 'rrsigs' to be validated.
+ * \param key Signing key.
+ * \param sign_ctx Signing context.
+ * \param dnssec_ctx DNSSEC context.
+ * \param refresh Consider RRSIG expired when gonna expire this soon.
+ * \param skip_crypto All RRSIGs in this node have been verified, just check validity.
+ *
+ * \return Error code, KNOT_EOK if successful and the signature is valid.
+ * \retval KNOT_DNSSEC_EINVALID_SIGNATURE The signature is invalid.
+ */
+int knot_check_signature(const knot_rrset_t *covered,
+ const knot_rrset_t *rrsigs, size_t pos,
+ const dnssec_key_t *key,
+ dnssec_sign_ctx_t *sign_ctx,
+ const kdnssec_ctx_t *dnssec_ctx,
+ knot_timediff_t refresh,
+ bool skip_crypto);
diff --git a/src/knot/dnssec/zone-events.c b/src/knot/dnssec/zone-events.c
new file mode 100644
index 0000000..22a2a76
--- /dev/null
+++ b/src/knot/dnssec/zone-events.c
@@ -0,0 +1,470 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include "libdnssec/error.h"
+#include "libdnssec/random.h"
+#include "libknot/libknot.h"
+#include "knot/conf/conf.h"
+#include "knot/common/log.h"
+#include "knot/dnssec/key-events.h"
+#include "knot/dnssec/key_records.h"
+#include "knot/dnssec/policy.h"
+#include "knot/dnssec/zone-events.h"
+#include "knot/dnssec/zone-keys.h"
+#include "knot/dnssec/zone-nsec.h"
+#include "knot/dnssec/zone-sign.h"
+#include "knot/zone/adjust.h"
+#include "knot/zone/digest.h"
+
+static knot_time_t schedule_next(kdnssec_ctx_t *kctx, const zone_keyset_t *keyset,
+ knot_time_t keys_expire, knot_time_t rrsigs_expire)
+{
+ knot_time_t rrsigs_refresh = knot_time_add(rrsigs_expire, -(knot_timediff_t)kctx->policy->rrsig_refresh_before);
+ knot_time_t zone_refresh = knot_time_min(keys_expire, rrsigs_refresh);
+
+ knot_time_t dnskey_update = knot_get_next_zone_key_event(keyset);
+ knot_time_t next = knot_time_min(zone_refresh, dnskey_update);
+
+ return next;
+}
+
+static int generate_salt(dnssec_binary_t *salt, uint16_t length)
+{
+ assert(salt);
+ dnssec_binary_t new_salt = { 0 };
+
+ if (length > 0) {
+ int r = dnssec_binary_alloc(&new_salt, length);
+ if (r != KNOT_EOK) {
+ return knot_error_from_libdnssec(r);
+ }
+
+ r = dnssec_random_binary(&new_salt);
+ if (r != KNOT_EOK) {
+ dnssec_binary_free(&new_salt);
+ return knot_error_from_libdnssec(r);
+ }
+ }
+
+ dnssec_binary_free(salt);
+ *salt = new_salt;
+
+ return KNOT_EOK;
+}
+
+int knot_dnssec_nsec3resalt(kdnssec_ctx_t *ctx, bool soa_rrsigs_ok,
+ knot_time_t *salt_changed, knot_time_t *when_resalt)
+{
+ int ret = KNOT_EOK;
+
+ if (!ctx->policy->nsec3_enabled) {
+ return KNOT_EOK;
+ }
+
+ if (ctx->policy->nsec3_salt_lifetime < 0 && !soa_rrsigs_ok) {
+ *when_resalt = ctx->now;
+ } else if (ctx->zone->nsec3_salt.size != ctx->policy->nsec3_salt_length || ctx->zone->nsec3_salt_created == 0) {
+ *when_resalt = ctx->now;
+ } else if (knot_time_cmp(ctx->now, ctx->zone->nsec3_salt_created) < 0) {
+ return KNOT_EINVAL;
+ } else if (ctx->policy->nsec3_salt_lifetime > 0) {
+ *when_resalt = knot_time_plus(ctx->zone->nsec3_salt_created, ctx->policy->nsec3_salt_lifetime);
+ }
+
+ if (knot_time_cmp(*when_resalt, ctx->now) <= 0) {
+ if (ctx->policy->nsec3_salt_length == 0) {
+ ctx->zone->nsec3_salt.size = 0;
+ ctx->zone->nsec3_salt_created = ctx->now;
+ *salt_changed = ctx->now;
+ *when_resalt = 0;
+ return kdnssec_ctx_commit(ctx);
+ }
+
+ ret = generate_salt(&ctx->zone->nsec3_salt, ctx->policy->nsec3_salt_length);
+ if (ret == KNOT_EOK) {
+ ctx->zone->nsec3_salt_created = ctx->now;
+ ret = kdnssec_ctx_commit(ctx);
+ *salt_changed = ctx->now;
+ }
+ // continue to planning next resalt even if NOK
+ if (ctx->policy->nsec3_salt_lifetime > 0) {
+ *when_resalt = knot_time_plus(ctx->now, ctx->policy->nsec3_salt_lifetime);
+ }
+ }
+
+ return ret;
+}
+
+static int check_offline_records(kdnssec_ctx_t *ctx)
+{
+ if (!ctx->policy->offline_ksk) {
+ return KNOT_EOK;
+ }
+
+ if (ctx->offline_records.dnskey.rrs.count == 0 ||
+ ctx->offline_records.rrsig.rrs.count == 0) {
+ log_zone_error(ctx->zone->dname,
+ "DNSSEC, no offline KSK records available");
+ return KNOT_ENOENT;
+ }
+
+ int ret;
+ knot_time_t last;
+ if (ctx->offline_next_time == 0) {
+ log_zone_warning(ctx->zone->dname,
+ "DNSSEC, using last offline KSK records available, "
+ "import new SKR before RRSIGs expire");
+ } else if ((ret = key_records_last_timestamp(ctx, &last)) != KNOT_EOK) {
+ log_zone_error(ctx->zone->dname,
+ "DNSSEC, failed to load offline KSK records (%s)",
+ knot_strerror(ret));
+ } else if (knot_time_diff(last, ctx->now) < 7 * 24 * 3600) {
+ log_zone_notice(ctx->zone->dname,
+ "DNSSEC, having offline KSK records for less than "
+ "a week, import new SKR");
+ }
+
+ return KNOT_EOK;
+}
+
+int knot_dnssec_zone_sign(zone_update_t *update,
+ conf_t *conf,
+ zone_sign_flags_t flags,
+ zone_sign_roll_flags_t roll_flags,
+ knot_time_t adjust_now,
+ zone_sign_reschedule_t *reschedule)
+{
+ if (!update || !reschedule) {
+ return KNOT_EINVAL;
+ }
+
+ const knot_dname_t *zone_name = update->new_cont->apex->owner;
+ kdnssec_ctx_t ctx = { 0 };
+ zone_keyset_t keyset = { 0 };
+ knot_time_t zone_expire = 0;
+
+ int result = kdnssec_ctx_init(conf, &ctx, zone_name, zone_kaspdb(update->zone), NULL);
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to initialize signing context (%s)",
+ knot_strerror(result));
+ return result;
+ }
+ if (adjust_now) {
+ ctx.now = adjust_now;
+ }
+
+ // update policy based on the zone content
+ update_policy_from_zone(ctx.policy, update->new_cont);
+
+ if (ctx.policy->rrsig_refresh_before < ctx.policy->zone_maximal_ttl + ctx.policy->propagation_delay) {
+ log_zone_error(zone_name, "DNSSEC, rrsig-refresh too low to prevent expired RRSIGs in resolver caches");
+ result = KNOT_EINVAL;
+ goto done;
+ }
+ if (ctx.policy->rrsig_lifetime <= ctx.policy->rrsig_refresh_before) {
+ log_zone_error(zone_name, "DNSSEC, rrsig-lifetime lower than rrsig-refresh");
+ result = KNOT_EINVAL;
+ goto done;
+ }
+
+ // perform key rollover if needed
+ result = knot_dnssec_key_rollover(&ctx, roll_flags, reschedule);
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to update key set (%s)",
+ knot_strerror(result));
+ goto done;
+ }
+
+ ctx.rrsig_drop_existing = flags & ZONE_SIGN_DROP_SIGNATURES;
+
+ conf_val_t val = conf_zone_get(conf, C_ZONEMD_GENERATE, zone_name);
+ unsigned zonemd_alg = conf_opt(&val);
+ if (zonemd_alg != ZONE_DIGEST_NONE) {
+ result = zone_update_add_digest(update, zonemd_alg, true);
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to reserve dummy ZONEMD (%s)",
+ knot_strerror(result));
+ goto done;
+ }
+ }
+
+ uint32_t ms;
+ if (zone_is_slave(conf, update->zone) && zone_get_master_serial(update->zone, &ms) == KNOT_ENOENT) {
+ // zone had been XFRed before on-slave-signing turned on
+ zone_set_master_serial(update->zone, zone_contents_serial(update->new_cont));
+ }
+
+ result = load_zone_keys(&ctx, &keyset, true);
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to load keys (%s)",
+ knot_strerror(result));
+ goto done;
+ }
+
+ // perform nsec3resalt if pending
+ if (roll_flags & KEY_ROLL_ALLOW_NSEC3RESALT) {
+ knot_rdataset_t *rrsig = node_rdataset(update->new_cont->apex, KNOT_RRTYPE_RRSIG);
+ bool issbaz = is_soa_signed_by_all_zsks(&keyset, rrsig);
+ result = knot_dnssec_nsec3resalt(&ctx, issbaz, &reschedule->last_nsec3resalt, &reschedule->next_nsec3resalt);
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to update NSEC3 salt (%s)",
+ knot_strerror(result));
+ goto done;
+ }
+ }
+
+ log_zone_info(zone_name, "DNSSEC, signing started");
+
+ result = knot_zone_sign_update_dnskeys(update, &keyset, &ctx);
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to update DNSKEY records (%s)",
+ knot_strerror(result));
+ goto done;
+ }
+
+ result = zone_adjust_contents(update->new_cont, adjust_cb_flags, NULL,
+ false, false, 1, update->a_ctx->node_ptrs);
+ if (result != KNOT_EOK) {
+ return result;
+ }
+
+ result = knot_zone_create_nsec_chain(update, &ctx);
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to create NSEC%s chain (%s)",
+ ctx.policy->nsec3_enabled ? "3" : "",
+ knot_strerror(result));
+ goto done;
+ }
+
+ result = check_offline_records(&ctx);
+ if (result != KNOT_EOK) {
+ goto done;
+ }
+
+ result = knot_zone_sign(update, &keyset, &ctx, &zone_expire);
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to sign zone content (%s)",
+ knot_strerror(result));
+ goto done;
+ }
+
+ // SOA finishing
+
+ if (zone_update_no_change(update)) {
+ log_zone_info(zone_name, "DNSSEC, zone is up-to-date");
+ update->zone->zonefile.resigned = false;
+ goto done;
+ } else {
+ update->zone->zonefile.resigned = true;
+ }
+
+ if (!(flags & ZONE_SIGN_KEEP_SERIAL) && zone_update_to(update) == NULL) {
+ result = zone_update_increment_soa(update, conf);
+ if (result == KNOT_EOK) {
+ result = knot_zone_sign_apex_rr(update, KNOT_RRTYPE_SOA, &keyset, &ctx);
+ }
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to update SOA record (%s)",
+ knot_strerror(result));
+ goto done;
+ }
+ }
+
+ if (zonemd_alg != ZONE_DIGEST_NONE) {
+ result = zone_update_add_digest(update, zonemd_alg, false);
+ if (result == KNOT_EOK) {
+ result = knot_zone_sign_apex_rr(update, KNOT_RRTYPE_ZONEMD, &keyset, &ctx);
+ }
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to update ZONEMD record (%s)",
+ knot_strerror(result));
+ goto done;
+ }
+ }
+
+ log_zone_info(zone_name, "DNSSEC, successfully signed");
+
+done:
+ if (result == KNOT_EOK) {
+ reschedule->next_sign = schedule_next(&ctx, &keyset, ctx.offline_next_time, zone_expire);
+ } else {
+ reschedule->next_sign = knot_dnssec_failover_delay(&ctx);
+ reschedule->next_rollover = 0;
+ }
+
+ free_zone_keys(&keyset);
+ kdnssec_ctx_deinit(&ctx);
+
+ return result;
+}
+
+int knot_dnssec_sign_update(zone_update_t *update, conf_t *conf)
+{
+ if (update == NULL || conf == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ const knot_dname_t *zone_name = update->new_cont->apex->owner;
+ kdnssec_ctx_t ctx = { 0 };
+ zone_keyset_t keyset = { 0 };
+ knot_time_t zone_expire = 0;
+
+ int result = kdnssec_ctx_init(conf, &ctx, zone_name, zone_kaspdb(update->zone), NULL);
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to initialize signing context (%s)",
+ knot_strerror(result));
+ return result;
+ }
+
+ update_policy_from_zone(ctx.policy, update->new_cont);
+
+ conf_val_t val = conf_zone_get(conf, C_ZONEMD_GENERATE, zone_name);
+ unsigned zonemd_alg = conf_opt(&val);
+ if (zonemd_alg != ZONE_DIGEST_NONE) {
+ result = zone_update_add_digest(update, zonemd_alg, true);
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to reserve dummy ZONEMD (%s)",
+ knot_strerror(result));
+ goto done;
+ }
+ }
+
+ result = load_zone_keys(&ctx, &keyset, false);
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to load keys (%s)",
+ knot_strerror(result));
+ goto done;
+ }
+
+ if (zone_update_changes_dnskey(update)) {
+ result = knot_zone_sign_update_dnskeys(update, &keyset, &ctx);
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to update DNSKEY records (%s)",
+ knot_strerror(result));
+ goto done;
+ }
+ }
+
+ result = zone_adjust_contents(update->new_cont, adjust_cb_flags, NULL,
+ false, false, 1, update->a_ctx->node_ptrs);
+ if (result != KNOT_EOK) {
+ goto done;
+ }
+
+ result = check_offline_records(&ctx);
+ if (result != KNOT_EOK) {
+ goto done;
+ }
+
+ result = knot_zone_sign_update(update, &keyset, &ctx, &zone_expire);
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to sign changeset (%s)",
+ knot_strerror(result));
+ goto done;
+ }
+
+ result = knot_zone_fix_nsec_chain(update, &keyset, &ctx);
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to fix NSEC%s chain (%s)",
+ ctx.policy->nsec3_enabled ? "3" : "",
+ knot_strerror(result));
+ goto done;
+ }
+
+ bool soa_changed = (knot_soa_serial(node_rdataset(update->zone->contents->apex, KNOT_RRTYPE_SOA)->rdata) !=
+ knot_soa_serial(node_rdataset(update->new_cont->apex, KNOT_RRTYPE_SOA)->rdata));
+
+ if (zone_update_no_change(update) && !soa_changed) {
+ log_zone_info(zone_name, "DNSSEC, zone is up-to-date");
+ update->zone->zonefile.resigned = false;
+ goto done;
+ } else {
+ update->zone->zonefile.resigned = true;
+ }
+
+ if (!soa_changed) {
+ // incrementing SOA just of it has not been modified by the update
+ result = zone_update_increment_soa(update, conf);
+ }
+ if (result == KNOT_EOK) {
+ result = knot_zone_sign_apex_rr(update, KNOT_RRTYPE_SOA, &keyset, &ctx);
+ }
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to update SOA record (%s)",
+ knot_strerror(result));
+ goto done;
+ }
+
+ if (zonemd_alg != ZONE_DIGEST_NONE) {
+ result = zone_update_add_digest(update, zonemd_alg, false);
+ if (result == KNOT_EOK) {
+ result = knot_zone_sign_apex_rr(update, KNOT_RRTYPE_ZONEMD, &keyset, &ctx);
+ }
+ if (result != KNOT_EOK) {
+ log_zone_error(zone_name, "DNSSEC, failed to update ZONEMD record (%s)",
+ knot_strerror(result));
+ goto done;
+ }
+ }
+
+ log_zone_info(zone_name, "DNSSEC, incrementally signed");
+
+done:
+ if (result == KNOT_EOK) {
+ knot_time_t next = knot_time_min(ctx.offline_next_time, zone_expire);
+ // NOTE: this is usually NOOP since signing planned earlier
+ zone_events_schedule_at(update->zone, ZONE_EVENT_DNSSEC, next ? next : -1);
+ }
+
+ free_zone_keys(&keyset);
+ kdnssec_ctx_deinit(&ctx);
+
+ return result;
+}
+
+knot_time_t knot_dnssec_failover_delay(const kdnssec_ctx_t *ctx)
+{
+ if (ctx->policy == NULL) {
+ return ctx->now + 3600; // failed before allocating ctx->policy, use default
+ } else {
+ return ctx->now + ctx->policy->rrsig_prerefresh;
+ }
+}
+
+int knot_dnssec_validate_zone(zone_update_t *update, conf_t *conf, knot_time_t now, bool incremental)
+{
+ kdnssec_ctx_t ctx = { 0 };
+ int ret = kdnssec_validation_ctx(conf, &ctx, update->new_cont);
+ if (now != 0) {
+ ctx.now = now;
+ }
+ if (ret == KNOT_EOK) {
+ ret = knot_zone_check_nsec_chain(update, &ctx, incremental);
+ }
+ if (ret == KNOT_EOK) {
+ knot_time_t unused = 0;
+ assert(ctx.validation_mode);
+ if (incremental) {
+ ret = knot_zone_sign_update(update, NULL, &ctx, &unused);
+ } else {
+ ret = knot_zone_sign(update, NULL, &ctx, &unused);
+ }
+ }
+ kdnssec_ctx_deinit(&ctx);
+ return ret;
+}
diff --git a/src/knot/dnssec/zone-events.h b/src/knot/dnssec/zone-events.h
new file mode 100644
index 0000000..d3667f3
--- /dev/null
+++ b/src/knot/dnssec/zone-events.h
@@ -0,0 +1,134 @@
+/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <time.h>
+
+#include "knot/zone/zone.h"
+#include "knot/updates/changesets.h"
+#include "knot/updates/zone-update.h"
+#include "knot/dnssec/context.h"
+
+enum zone_sign_flags {
+ ZONE_SIGN_NONE = 0,
+ ZONE_SIGN_DROP_SIGNATURES = (1 << 0),
+ ZONE_SIGN_KEEP_SERIAL = (1 << 1),
+};
+
+typedef enum zone_sign_flags zone_sign_flags_t;
+
+typedef enum {
+ KEY_ROLL_ALLOW_KSK_ROLL = (1 << 0),
+ KEY_ROLL_FORCE_KSK_ROLL = (1 << 1),
+ KEY_ROLL_ALLOW_ZSK_ROLL = (1 << 2),
+ KEY_ROLL_FORCE_ZSK_ROLL = (1 << 3),
+ KEY_ROLL_ALLOW_NSEC3RESALT = (1 << 4),
+ KEY_ROLL_ALLOW_ALL = KEY_ROLL_ALLOW_KSK_ROLL |
+ KEY_ROLL_ALLOW_ZSK_ROLL |
+ KEY_ROLL_ALLOW_NSEC3RESALT
+} zone_sign_roll_flags_t;
+
+typedef struct {
+ knot_time_t next_sign;
+ knot_time_t next_rollover;
+ knot_time_t next_nsec3resalt;
+ knot_time_t last_nsec3resalt;
+ bool keys_changed;
+ bool plan_ds_check;
+} zone_sign_reschedule_t;
+
+/*!
+ * \brief Generate/rollover keys in keystore as needed.
+ *
+ * \param kctx Pointers to the keytore, policy, etc.
+ * \param zone_name Zone name.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_dnssec_sign_process_events(const kdnssec_ctx_t *kctx,
+ const knot_dname_t *zone_name);
+
+/*!
+ * \brief DNSSEC re-sign zone, store new records into changeset. Valid signatures
+ * and NSEC(3) records will not be changed.
+ *
+ * \param update Zone Update structure with current zone contents to be updated by signing.
+ * \param conf Knot configuration.
+ * \param flags Zone signing flags.
+ * \param roll_flags Key rollover flags.
+ * \param adjust_now If not zero: adjust "now" to this timestamp.
+ * \param reschedule Signature refresh time of the oldest signature in zone.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_dnssec_zone_sign(zone_update_t *update,
+ conf_t *conf,
+ zone_sign_flags_t flags,
+ zone_sign_roll_flags_t roll_flags,
+ knot_time_t adjust_now,
+ zone_sign_reschedule_t *reschedule);
+
+/*!
+ * \brief Sign changeset (inside incremental Zone Update) created by DDNS or so...
+ *
+ * \param update Zone Update structure with current zone contents, changes to be signed and to be updated with signatures.
+ * \param conf Knot configuration.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_dnssec_sign_update(zone_update_t *update, conf_t *conf);
+
+/*!
+ * \brief Create new NCES3 salt if the old one is too old, and plan next resalt.
+ *
+ * For given zone, check NSEC3 salt in KASP db and decide if it shall be recreated
+ * and tell the user the next time it shall be called.
+ *
+ * This function is optimized to be called from NSEC3RESALT_EVENT,
+ * but also during zone load so that the zone gets loaded already with
+ * proper DNSSEC chain.
+ *
+ * \param ctx zone signing context
+ * \param soa_rrsigs_ok Zone is signed by current active ZSKs.
+ * \param salt_changed output if KNOT_EOK: when was the salt last changed? (either ctx->now or 0)
+ * \param when_resalt output: timestamp when next resalt takes place
+ *
+ * \return KNOT_E*
+ */
+int knot_dnssec_nsec3resalt(kdnssec_ctx_t *ctx, bool soa_rrsigs_ok,
+ knot_time_t *salt_changed, knot_time_t *when_resalt);
+
+/*!
+ * \brief When DNSSEC signing failed, re-plan on this time.
+ *
+ * \param ctx zone signing context
+ *
+ * \return Timestamp of next signing attempt.
+ */
+knot_time_t knot_dnssec_failover_delay(const kdnssec_ctx_t *ctx);
+
+/*!
+ * \brief Validate zone DNSSEC based on its contents.
+ *
+ * \param update Zone update with contents.
+ * \param conf Knot configuration.
+ * \param now If not zero: adjust "now" to this timestamp.
+ * \param incremental Try to validate incrementally.
+ *
+ * \return KNOT_E*
+ */
+int knot_dnssec_validate_zone(zone_update_t *update, conf_t *conf, knot_time_t now, bool incremental);
diff --git a/src/knot/dnssec/zone-keys.c b/src/knot/dnssec/zone-keys.c
new file mode 100644
index 0000000..a76c4be
--- /dev/null
+++ b/src/knot/dnssec/zone-keys.c
@@ -0,0 +1,767 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <limits.h>
+#include <stdio.h>
+
+#include "libdnssec/error.h"
+#include "knot/common/log.h"
+#include "knot/dnssec/zone-keys.h"
+#include "libknot/libknot.h"
+#include "contrib/openbsd/strlcat.h"
+
+#define MAX_KEY_INFO 128
+
+typedef struct {
+ char msg[MAX_KEY_INFO];
+ knot_time_t key_time;
+} key_info_t;
+
+knot_dynarray_define(keyptr, zone_key_t *, DYNARRAY_VISIBILITY_NORMAL)
+
+void normalize_generate_flags(kdnssec_generate_flags_t *flags)
+{
+ if (!(*flags & DNSKEY_GENERATE_KSK) && !(*flags & DNSKEY_GENERATE_ZSK)) {
+ *flags |= DNSKEY_GENERATE_ZSK;
+ }
+ if (!(*flags & DNSKEY_GENERATE_SEP_SPEC)) {
+ if ((*flags & DNSKEY_GENERATE_KSK)) {
+ *flags |= DNSKEY_GENERATE_SEP_ON;
+ } else {
+ *flags &= ~DNSKEY_GENERATE_SEP_ON;
+ }
+ }
+}
+
+static int generate_dnssec_key(dnssec_keystore_t *keystore,
+ const knot_dname_t *zone_name,
+ const char *key_label,
+ dnssec_key_algorithm_t alg,
+ unsigned size,
+ kdnssec_generate_flags_t flags,
+ char **id,
+ dnssec_key_t **key)
+{
+ *key = NULL;
+ *id = NULL;
+
+ int ret = dnssec_keystore_generate(keystore, alg, size, key_label, id);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = dnssec_key_new(key);
+ if (ret != KNOT_EOK) {
+ goto fail;
+ }
+
+ ret = dnssec_key_set_dname(*key, zone_name);
+ if (ret != KNOT_EOK) {
+ goto fail;
+ }
+
+ dnssec_key_set_flags(*key, dnskey_flags(flags & DNSKEY_GENERATE_SEP_ON));
+ dnssec_key_set_algorithm(*key, alg);
+
+ ret = dnssec_keystore_get_private(keystore, *id, *key);
+ if (ret != KNOT_EOK) {
+ goto fail;
+ }
+
+ return KNOT_EOK;
+
+fail:
+ dnssec_key_free(*key);
+ *key = NULL;
+ free(*id);
+ *id = NULL;
+ return ret;
+}
+
+static bool keytag_in_use(kdnssec_ctx_t *ctx, uint16_t keytag)
+{
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ uint16_t used = dnssec_key_get_keytag(ctx->zone->keys[i].key);
+ if (used == keytag) {
+ return true;
+ }
+ }
+ return false;
+}
+
+#define GENERATE_KEYTAG_ATTEMPTS (20)
+
+static int generate_keytag_unconflict(kdnssec_ctx_t *ctx,
+ kdnssec_generate_flags_t flags,
+ char **id,
+ dnssec_key_t **key)
+{
+ unsigned size = (flags & DNSKEY_GENERATE_KSK) ? ctx->policy->ksk_size :
+ ctx->policy->zsk_size;
+
+ const char *label = NULL;
+
+ char label_buf[sizeof(knot_dname_txt_storage_t) + 16];
+ if (ctx->policy->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;
+ }
+
+ for (size_t i = 0; i < GENERATE_KEYTAG_ATTEMPTS; i++) {
+ dnssec_key_free(*key);
+ free(*id);
+
+ int ret = generate_dnssec_key(ctx->keystore, ctx->zone->dname, label,
+ ctx->policy->algorithm, size, flags,
+ id, key);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ if (!keytag_in_use(ctx, dnssec_key_get_keytag(*key))) {
+ return KNOT_EOK;
+ }
+ }
+
+ log_zone_notice(ctx->zone->dname, "generated key with conflicting keytag %hu",
+ dnssec_key_get_keytag(*key));
+ return KNOT_EOK;
+}
+
+int kdnssec_generate_key(kdnssec_ctx_t *ctx, kdnssec_generate_flags_t flags,
+ knot_kasp_key_t **key_ptr)
+{
+ assert(ctx);
+ assert(ctx->zone);
+ assert(ctx->keystore);
+ assert(ctx->policy);
+
+ normalize_generate_flags(&flags);
+
+ // generate key in the keystore
+
+ char *id = NULL;
+ dnssec_key_t *dnskey = NULL;
+
+ int r = generate_keytag_unconflict(ctx, flags, &id, &dnskey);
+ if (r != KNOT_EOK) {
+ return r;
+ }
+
+ knot_kasp_key_t *key = calloc(1, sizeof(*key));
+ if (!key) {
+ dnssec_key_free(dnskey);
+ free(id);
+ return KNOT_ENOMEM;
+ }
+
+ key->id = id;
+ key->key = dnskey;
+ key->is_ksk = (flags & DNSKEY_GENERATE_KSK);
+ key->is_zsk = (flags & DNSKEY_GENERATE_ZSK);
+ key->timing.created = ctx->now;
+
+ r = kasp_zone_append(ctx->zone, key);
+ free(key);
+ if (r != KNOT_EOK) {
+ dnssec_key_free(dnskey);
+ free(id);
+ return r;
+ }
+
+ if (key_ptr) {
+ *key_ptr = &ctx->zone->keys[ctx->zone->num_keys - 1];
+ }
+
+ return KNOT_EOK;
+}
+
+int kdnssec_share_key(kdnssec_ctx_t *ctx, const knot_dname_t *from_zone, const char *key_id)
+{
+ knot_dname_t *to_zone = knot_dname_copy(ctx->zone->dname, NULL);
+ if (to_zone == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ int ret = kdnssec_ctx_commit(ctx);
+ if (ret != KNOT_EOK) {
+ free(to_zone);
+ return ret;
+ }
+
+ ret = kasp_db_share_key(ctx->kasp_db, from_zone, ctx->zone->dname, key_id);
+ if (ret != KNOT_EOK) {
+ free(to_zone);
+ return ret;
+ }
+
+ kasp_zone_clear(ctx->zone);
+ ret = kasp_zone_load(ctx->zone, to_zone, ctx->kasp_db,
+ &ctx->keytag_conflict);
+ free(to_zone);
+ return ret;
+}
+
+int kdnssec_delete_key(kdnssec_ctx_t *ctx, knot_kasp_key_t *key_ptr)
+{
+ assert(ctx);
+ assert(ctx->zone);
+ assert(ctx->keystore);
+ assert(ctx->policy);
+
+ ssize_t key_index = key_ptr - ctx->zone->keys;
+
+ if (key_index < 0 || key_index >= ctx->zone->num_keys) {
+ return KNOT_EINVAL;
+ }
+
+ bool key_still_used_in_keystore = false;
+ int ret = kasp_db_delete_key(ctx->kasp_db, ctx->zone->dname, key_ptr->id, &key_still_used_in_keystore);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (!key_still_used_in_keystore && !key_ptr->is_pub_only) {
+ ret = dnssec_keystore_remove(ctx->keystore, key_ptr->id);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ dnssec_key_free(key_ptr->key);
+ free(key_ptr->id);
+ memmove(key_ptr, key_ptr + 1, (ctx->zone->num_keys - key_index - 1) * sizeof(*key_ptr));
+ ctx->zone->num_keys--;
+ return KNOT_EOK;
+}
+
+static bool is_published(knot_kasp_key_timing_t *timing, knot_time_t now)
+{
+ return (knot_time_cmp(timing->publish, now) <= 0 &&
+ knot_time_cmp(timing->post_active, now) > 0 &&
+ knot_time_cmp(timing->remove, now) > 0);
+}
+
+static bool is_ready(knot_kasp_key_timing_t *timing, knot_time_t now)
+{
+ return (knot_time_cmp(timing->ready, now) <= 0 &&
+ knot_time_cmp(timing->active, now) > 0);
+}
+
+static bool is_active(knot_kasp_key_timing_t *timing, knot_time_t now)
+{
+ return (knot_time_cmp(timing->active, now) <= 0 &&
+ knot_time_cmp(timing->retire, now) > 0 &&
+ knot_time_cmp(timing->retire_active, now) > 0 &&
+ knot_time_cmp(timing->remove, now) > 0);
+}
+
+static bool alg_has_active_zsk(kdnssec_ctx_t *ctx, uint8_t alg)
+{
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *k = &ctx->zone->keys[i];
+ if (dnssec_key_get_algorithm(k->key) == alg &&
+ k->is_zsk && is_active(&k->timing, ctx->now)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static void fix_revoked_flag(knot_kasp_key_t *key)
+{
+ uint16_t flags = dnssec_key_get_flags(key->key);
+ if ((flags & DNSKEY_FLAGS_REVOKED) != DNSKEY_FLAGS_REVOKED) {
+ dnssec_key_set_flags(key->key, flags | DNSKEY_FLAGS_REVOKED); // FYI leading to change of keytag
+ }
+}
+
+/*!
+ * \brief Get key feature flags from key parameters.
+ */
+static void set_key(knot_kasp_key_t *kasp_key, knot_time_t now,
+ zone_key_t *zone_key, bool same_alg_act_zsk)
+{
+ assert(kasp_key);
+ assert(zone_key);
+
+ knot_kasp_key_timing_t *timing = &kasp_key->timing;
+
+ zone_key->id = kasp_key->id;
+ zone_key->key = kasp_key->key;
+
+ // next event computation
+
+ knot_time_t next = 0;
+ knot_time_t timestamps[] = {
+ timing->pre_active,
+ timing->publish,
+ timing->ready,
+ timing->active,
+ timing->retire_active,
+ timing->retire,
+ timing->post_active,
+ timing->revoke,
+ timing->remove,
+ };
+
+ for (int i = 0; i < sizeof(timestamps) / sizeof(knot_time_t); i++) {
+ knot_time_t ts = timestamps[i];
+ if (knot_time_cmp(now, ts) < 0 && knot_time_cmp(ts, next) < 0) {
+ next = ts;
+ }
+ }
+
+ zone_key->next_event = next;
+
+ zone_key->is_ksk = kasp_key->is_ksk;
+ zone_key->is_zsk = kasp_key->is_zsk;
+
+ zone_key->is_public = is_published(timing, now);
+ zone_key->is_ready = (zone_key->is_ksk && is_ready(timing, now));
+ zone_key->is_active = is_active(timing, now);
+
+ zone_key->is_ksk_active_plus = zone_key->is_public && zone_key->is_ksk && !zone_key->is_active; // KSK is active+ whenever published
+ zone_key->is_zsk_active_plus = zone_key->is_ready && !same_alg_act_zsk;
+ if (knot_time_cmp(timing->pre_active, now) <= 0 &&
+ knot_time_cmp(timing->ready, now) > 0 &&
+ knot_time_cmp(timing->active, now) > 0 &&
+ knot_time_cmp(timing->remove, now) > 0) {
+ zone_key->is_zsk_active_plus = zone_key->is_zsk;
+ // zone_key->is_ksk_active_plus = (knot_time_cmp(timing->publish, now) <= 0 && zone_key->is_ksk); // redundant, but helps understand
+ }
+ if (knot_time_cmp(timing->retire, now) <= 0 &&
+ knot_time_cmp(timing->remove, now) > 0) {
+ zone_key->is_ksk_active_plus = false;
+ zone_key->is_public = zone_key->is_zsk;
+ }
+ if (knot_time_cmp(timing->retire_active, now) <= 0 &&
+ knot_time_cmp(timing->retire, now) > 0 &&
+ knot_time_cmp(timing->remove, now) > 0) {
+ zone_key->is_ksk_active_plus = zone_key->is_ksk;
+ zone_key->is_zsk_active_plus = !same_alg_act_zsk;
+ } // not "else" !
+ if (knot_time_cmp(timing->post_active, now) <= 0 &&
+ knot_time_cmp(timing->remove, now) > 0) {
+ zone_key->is_ksk_active_plus = false;
+ zone_key->is_zsk_active_plus = zone_key->is_zsk;
+ }
+ if (zone_key->is_ksk &&
+ knot_time_cmp(timing->revoke, now) <= 0 &&
+ knot_time_cmp(timing->remove, now) > 0) {
+ zone_key->is_ready = false;
+ zone_key->is_active = false;
+ zone_key->is_ksk_active_plus = true;
+ zone_key->is_public = true;
+ zone_key->is_revoked = true;
+ fix_revoked_flag(kasp_key);
+ }
+ if (kasp_key->is_pub_only) {
+ zone_key->is_active = false;
+ zone_key->is_ksk_active_plus = false;
+ zone_key->is_zsk_active_plus = false;
+ zone_key->is_pub_only = true;
+ }
+}
+
+/*!
+ * \brief Check if algorithm is allowed with NSEC3.
+ */
+static bool is_nsec3_allowed(uint8_t algorithm)
+{
+ switch (algorithm) {
+ case DNSSEC_KEY_ALGORITHM_RSA_SHA1:
+ return false;
+ default:
+ return true;
+ }
+}
+
+static int walk_algorithms(kdnssec_ctx_t *ctx, zone_keyset_t *keyset)
+{
+ if (ctx->policy->unsafe & UNSAFE_KEYSET) {
+ return KNOT_EOK;
+ }
+
+ uint8_t alg_usage[256] = { 0 };
+ bool have_active_alg = false;
+
+ for (size_t i = 0; i < keyset->count; i++) {
+ zone_key_t *key = &keyset->keys[i];
+ if (key->is_pub_only) {
+ continue;
+ }
+ uint8_t alg = dnssec_key_get_algorithm(key->key);
+
+ if (ctx->policy->nsec3_enabled && !is_nsec3_allowed(alg)) {
+ log_zone_warning(ctx->zone->dname, "DNSSEC, key %d "
+ "cannot be used with NSEC3",
+ dnssec_key_get_keytag(key->key));
+ key->is_public = false;
+ key->is_active = false;
+ key->is_ready = false;
+ key->is_ksk_active_plus = false;
+ key->is_zsk_active_plus = false;
+ continue;
+ }
+
+ if (key->is_ksk && key->is_public) { alg_usage[alg] |= 1; }
+ if (key->is_zsk && key->is_public) { alg_usage[alg] |= 2; }
+ if (key->is_ksk && (key->is_active || key->is_ksk_active_plus)) { alg_usage[alg] |= 4; }
+ if (key->is_zsk && (key->is_active || key->is_zsk_active_plus)) { alg_usage[alg] |= 8; }
+ }
+
+ for (size_t i = 0; i < sizeof(alg_usage); i++) {
+ if (!(alg_usage[i] & 3)) {
+ continue; // no public keys, ignore
+ }
+ switch (alg_usage[i]) {
+ case 15: // all keys ready for signing
+ have_active_alg = true;
+ break;
+ case 5:
+ case 10:
+ if (ctx->policy->offline_ksk) {
+ have_active_alg = true;
+ break;
+ }
+ // else FALLTHROUGH
+ default:
+ return KNOT_DNSSEC_EMISSINGKEYTYPE;
+ }
+ }
+
+ if (!have_active_alg) {
+ return KNOT_DNSSEC_ENOKEY;
+ }
+
+ return KNOT_EOK;
+}
+
+/*!
+ * \brief Load private keys for active keys.
+ */
+static int load_private_keys(dnssec_keystore_t *keystore, zone_keyset_t *keyset)
+{
+ assert(keystore);
+ assert(keyset);
+
+ for (size_t i = 0; i < keyset->count; i++) {
+ zone_key_t *key = &keyset->keys[i];
+ if (!key->is_active && !key->is_ksk_active_plus && !key->is_zsk_active_plus) {
+ continue;
+ }
+ int r = dnssec_keystore_get_private(keystore, key->id, key->key);
+ switch (r) {
+ case DNSSEC_EOK:
+ case DNSSEC_KEY_ALREADY_PRESENT:
+ break;
+ default:
+ return r;
+ }
+ }
+
+ return DNSSEC_EOK;
+}
+
+/*!
+ * \brief Log information about zone keys.
+ */
+static void log_key_info(const zone_key_t *key, char *out, size_t out_len)
+{
+ assert(key);
+ assert(out);
+
+ uint8_t alg_code = dnssec_key_get_algorithm(key->key);
+ const knot_lookup_t *alg = knot_lookup_by_id(knot_dnssec_alg_names, alg_code);
+
+ char alg_code_str[8] = "";
+ if (alg == NULL) {
+ (void)snprintf(alg_code_str, sizeof(alg_code_str), "%d", alg_code);
+ }
+
+ (void)snprintf(out, out_len, "DNSSEC, key, tag %5d, algorithm %s%s%s%s%s%s",
+ dnssec_key_get_keytag(key->key),
+ (alg != NULL ? alg->name : alg_code_str),
+ (key->is_ksk ? (key->is_zsk ? ", CSK" : ", KSK") : ""),
+ (key->is_public ? ", public" : ""),
+ (key->is_ready ? ", ready" : ""),
+ (key->is_active ? ", active" : ""),
+ (key->is_ksk_active_plus || key->is_zsk_active_plus ? ", active+" : ""));
+}
+
+static int log_key_sort(const void *a, const void *b)
+{
+ const key_info_t *x = a, *y = b;
+ return knot_time_cmp(x->key_time, y->key_time);
+}
+
+/*!
+ * \brief Load zone keys and init cryptographic context.
+ */
+int load_zone_keys(kdnssec_ctx_t *ctx, zone_keyset_t *keyset_ptr, bool verbose)
+{
+ if (!ctx || !keyset_ptr) {
+ return KNOT_EINVAL;
+ }
+
+ zone_keyset_t keyset = { 0 };
+
+ if (ctx->zone->num_keys < 1) {
+ log_zone_error(ctx->zone->dname, "DNSSEC, no keys are available");
+ return KNOT_DNSSEC_ENOKEY;
+ }
+
+ keyset.count = ctx->zone->num_keys;
+ keyset.keys = calloc(keyset.count, sizeof(zone_key_t));
+ if (!keyset.keys) {
+ free_zone_keys(&keyset);
+ return KNOT_ENOMEM;
+ }
+
+ key_info_t key_info[ctx->zone->num_keys];
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ knot_kasp_key_t *kasp_key = &ctx->zone->keys[i];
+ uint8_t kk_alg = dnssec_key_get_algorithm(kasp_key->key);
+ bool same_alg_zsk = alg_has_active_zsk(ctx, kk_alg);
+ set_key(kasp_key, ctx->now, &keyset.keys[i], same_alg_zsk);
+ if (verbose) {
+ log_key_info(&keyset.keys[i], key_info[i].msg, MAX_KEY_INFO);
+ if (knot_time_cmp(kasp_key->timing.pre_active, kasp_key->timing.publish) < 0) {
+ key_info[i].key_time = kasp_key->timing.pre_active;
+ } else {
+ key_info[i].key_time = kasp_key->timing.publish;
+ }
+ }
+ }
+
+ // Sort the keys by publish/pre_active timestamps.
+ if (verbose) {
+ qsort(key_info, ctx->zone->num_keys, sizeof(key_info[0]), log_key_sort);
+ for (size_t i = 0; i < ctx->zone->num_keys; i++) {
+ log_zone_info(ctx->zone->dname, "%s", key_info[i].msg);
+ }
+ }
+
+ int ret = walk_algorithms(ctx, &keyset);
+ if (ret != KNOT_EOK) {
+ log_zone_error(ctx->zone->dname, "DNSSEC, keys validation failed (%s)",
+ knot_strerror(ret));
+ free_zone_keys(&keyset);
+ return ret;
+ }
+
+ ret = load_private_keys(ctx->keystore, &keyset);
+ ret = knot_error_from_libdnssec(ret);
+ if (ret != KNOT_EOK) {
+ log_zone_error(ctx->zone->dname, "DNSSEC, failed to load private "
+ "keys (%s)", knot_strerror(ret));
+ free_zone_keys(&keyset);
+ return ret;
+ }
+
+ *keyset_ptr = keyset;
+
+ return KNOT_EOK;
+}
+
+/*!
+ * \brief Free structure with zone keys and associated DNSSEC contexts.
+ */
+void free_zone_keys(zone_keyset_t *keyset)
+{
+ if (!keyset) {
+ return;
+ }
+
+ for (size_t i = 0; i < keyset->count; i++) {
+ dnssec_binary_free(&keyset->keys[i].precomputed_ds);
+ }
+
+ free(keyset->keys);
+
+ memset(keyset, '\0', sizeof(*keyset));
+}
+
+/*!
+ * \brief Get timestamp of next key event.
+ */
+knot_time_t knot_get_next_zone_key_event(const zone_keyset_t *keyset)
+{
+ assert(keyset);
+
+ knot_time_t result = 0;
+
+ for (size_t i = 0; i < keyset->count; i++) {
+ zone_key_t *key = &keyset->keys[i];
+ if (knot_time_cmp(key->next_event, result) < 0) {
+ result = key->next_event;
+ }
+ }
+
+ return result;
+}
+
+/*!
+ * \brief Compute DS record rdata from key + cache it.
+ */
+int zone_key_calculate_ds(zone_key_t *for_key, dnssec_key_digest_t digesttype,
+ dnssec_binary_t *out_donotfree)
+{
+ assert(for_key);
+ assert(out_donotfree);
+
+ int ret = KNOT_EOK;
+
+ if (for_key->precomputed_ds.data == NULL || for_key->precomputed_digesttype != digesttype) {
+ dnssec_binary_free(&for_key->precomputed_ds);
+ ret = dnssec_key_create_ds(for_key->key, digesttype, &for_key->precomputed_ds);
+ ret = knot_error_from_libdnssec(ret);
+ for_key->precomputed_digesttype = digesttype;
+ }
+
+ *out_donotfree = for_key->precomputed_ds;
+ return ret;
+}
+
+zone_sign_ctx_t *zone_sign_ctx(const zone_keyset_t *keyset, const kdnssec_ctx_t *dnssec_ctx)
+{
+ zone_sign_ctx_t *ctx = calloc(1, sizeof(*ctx) + keyset->count * sizeof(*ctx->sign_ctxs));
+ if (ctx == NULL) {
+ return NULL;
+ }
+
+ ctx->sign_ctxs = (dnssec_sign_ctx_t **)(ctx + 1);
+ ctx->count = keyset->count;
+ ctx->keys = keyset->keys;
+ ctx->dnssec_ctx = dnssec_ctx;
+ for (size_t i = 0; i < ctx->count; i++) {
+ int ret = dnssec_sign_new(&ctx->sign_ctxs[i], ctx->keys[i].key);
+ if (ret != DNSSEC_EOK) {
+ zone_sign_ctx_free(ctx);
+ return NULL;
+ }
+ }
+
+ return ctx;
+}
+
+zone_sign_ctx_t *zone_validation_ctx(const kdnssec_ctx_t *dnssec_ctx)
+{
+ size_t count = dnssec_ctx->zone->num_keys;
+ zone_sign_ctx_t *ctx = calloc(1, sizeof(*ctx) + count * sizeof(*ctx->sign_ctxs));
+ if (ctx == NULL) {
+ return NULL;
+ }
+
+ ctx->sign_ctxs = (dnssec_sign_ctx_t **)(ctx + 1);
+ ctx->count = count;
+ ctx->keys = NULL;
+ ctx->dnssec_ctx = dnssec_ctx;
+ for (size_t i = 0; i < ctx->count; i++) {
+ int ret = dnssec_sign_new(&ctx->sign_ctxs[i], dnssec_ctx->zone->keys[i].key);
+ if (ret != DNSSEC_EOK) {
+ zone_sign_ctx_free(ctx);
+ return NULL;
+ }
+ }
+
+ return ctx;
+}
+
+void zone_sign_ctx_free(zone_sign_ctx_t *ctx)
+{
+ if (ctx != NULL) {
+ for (size_t i = 0; i < ctx->count; i++) {
+ dnssec_sign_free(ctx->sign_ctxs[i]);
+ }
+ free(ctx);
+ }
+}
+
+int dnssec_key_from_rdata(dnssec_key_t **key, const knot_dname_t *owner,
+ const uint8_t *rdata, size_t rdlen)
+{
+ if (key == NULL || rdata == NULL || rdlen == 0) {
+ return KNOT_EINVAL;
+ }
+
+ const dnssec_binary_t binary_key = {
+ .size = rdlen,
+ .data = (uint8_t *)rdata
+ };
+
+ dnssec_key_t *new_key = NULL;
+ int ret = dnssec_key_new(&new_key);
+ if (ret != DNSSEC_EOK) {
+ return knot_error_from_libdnssec(ret);
+ }
+ ret = dnssec_key_set_rdata(new_key, &binary_key);
+ if (ret != DNSSEC_EOK) {
+ dnssec_key_free(new_key);
+ return knot_error_from_libdnssec(ret);
+ }
+ if (owner != NULL) {
+ ret = dnssec_key_set_dname(new_key, owner);
+ if (ret != DNSSEC_EOK) {
+ dnssec_key_free(new_key);
+ return knot_error_from_libdnssec(ret);
+ }
+ }
+
+ *key = new_key;
+ return KNOT_EOK;
+}
+
+static bool soa_signed_by_key(const zone_key_t *key, const knot_rdataset_t *apex_rrsig)
+{
+ assert(key != NULL);
+ if (apex_rrsig == NULL) {
+ return false;
+ }
+ uint16_t keytag = dnssec_key_get_keytag(key->key);
+
+ knot_rdata_t *rr = apex_rrsig->rdata;
+ for (int i = 0; i < apex_rrsig->count; i++) {
+ if (knot_rrsig_type_covered(rr) == KNOT_RRTYPE_SOA &&
+ knot_rrsig_key_tag(rr) == keytag) {
+ return true;
+ }
+ rr = knot_rdataset_next(rr);
+ }
+
+ return false;
+}
+
+int is_soa_signed_by_all_zsks(const zone_keyset_t *keyset,
+ const knot_rdataset_t *apex_rrsig)
+{
+ if (keyset == NULL || keyset->count == 0) {
+ return false;
+ }
+
+ for (size_t i = 0; i < keyset->count; i++) {
+ const zone_key_t *key = &keyset->keys[i];
+ if (key->is_zsk && key->is_active &&
+ !soa_signed_by_key(key, apex_rrsig)) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/src/knot/dnssec/zone-keys.h b/src/knot/dnssec/zone-keys.h
new file mode 100644
index 0000000..6d72572
--- /dev/null
+++ b/src/knot/dnssec/zone-keys.h
@@ -0,0 +1,213 @@
+/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "libknot/dynarray.h"
+#include "libdnssec/keystore.h"
+#include "libdnssec/sign.h"
+#include "knot/dnssec/kasp/kasp_zone.h"
+#include "knot/dnssec/kasp/policy.h"
+#include "knot/dnssec/context.h"
+
+/*!
+ * \brief Zone key context used during signing.
+ */
+typedef struct {
+ const char *id;
+ dnssec_key_t *key;
+
+ dnssec_binary_t precomputed_ds;
+ dnssec_key_digest_t precomputed_digesttype;
+
+ knot_time_t next_event;
+
+ bool is_ksk;
+ bool is_zsk;
+ bool is_active;
+ bool is_public;
+ bool is_ready;
+ bool is_zsk_active_plus;
+ bool is_ksk_active_plus;
+ bool is_pub_only;
+ bool is_revoked;
+} zone_key_t;
+
+knot_dynarray_declare(keyptr, zone_key_t *, DYNARRAY_VISIBILITY_NORMAL, 1)
+
+typedef struct {
+ size_t count;
+ zone_key_t *keys;
+} zone_keyset_t;
+
+/*!
+ * \brief Signing context used for single signing thread.
+ */
+typedef struct {
+ size_t count; // number of keys in keyset
+ zone_key_t *keys; // keys in keyset
+ dnssec_sign_ctx_t **sign_ctxs; // signing buffers for keys in keyset
+ const kdnssec_ctx_t *dnssec_ctx; // dnssec context
+} zone_sign_ctx_t;
+
+/*!
+ * \brief Flags determining key type
+ */
+enum {
+ DNSKEY_FLAGS_ZSK = KNOT_DNSKEY_FLAG_ZONE,
+ DNSKEY_FLAGS_KSK = KNOT_DNSKEY_FLAG_ZONE | KNOT_DNSKEY_FLAG_SEP,
+ DNSKEY_FLAGS_REVOKED = KNOT_DNSKEY_FLAG_ZONE | KNOT_DNSKEY_FLAG_SEP | KNOT_DNSKEY_FLAG_REVOKE,
+};
+
+inline static uint16_t dnskey_flags(bool is_ksk)
+{
+ return is_ksk ? DNSKEY_FLAGS_KSK : DNSKEY_FLAGS_ZSK;
+}
+
+typedef enum {
+ DNSKEY_GENERATE_KSK = (1 << 0), // KSK flag in metadata
+ DNSKEY_GENERATE_ZSK = (1 << 1), // ZSK flag in metadata
+ DNSKEY_GENERATE_SEP_SPEC = (1 << 2), // not (SEP bit set iff KSK)
+ DNSKEY_GENERATE_SEP_ON = (1 << 3), // SEP bit set on
+} kdnssec_generate_flags_t;
+
+void normalize_generate_flags(kdnssec_generate_flags_t *flags);
+
+/*!
+ * \brief Generate new key, store all details in new kasp key structure.
+ *
+ * \param ctx kasp context
+ * \param flags determine if to use the key as KSK and/or ZSK and SEP flag
+ * \param key_ptr output if KNOT_EOK: new pointer to generated key
+ *
+ * \return KNOT_E*
+ */
+int kdnssec_generate_key(kdnssec_ctx_t *ctx, kdnssec_generate_flags_t flags,
+ knot_kasp_key_t **key_ptr);
+
+/*!
+ * \brief Take a key from another zone (copying info, sharing privkey).
+ *
+ * \param ctx kasp context
+ * \param from_zone name of the zone to take from
+ * \param key_id ID of the key to take
+ *
+ * \return KNOT_E*
+ */
+int kdnssec_share_key(kdnssec_ctx_t *ctx, const knot_dname_t *from_zone, const char *key_id);
+
+/*!
+ * \brief Remove key from zone.
+ *
+ * Deletes the key in keystore, unlinks the key from the zone in KASP db,
+ * moreover if no more zones use this key in KASP db, deletes it completely there
+ * and deletes it also from key storage (PKCS8dir/PKCS11).
+ *
+ * \param ctx kasp context (zone, keystore, kaspdb) to be modified
+ * \param key_ptr pointer to key to be removed, must be inside keystore structure, NOT a copy of it!
+ *
+ * \return KNOT_E*
+ */
+int kdnssec_delete_key(kdnssec_ctx_t *ctx, knot_kasp_key_t *key_ptr);
+
+/*!
+ * \brief Load zone keys and init cryptographic context.
+ *
+ * \param ctx Zone signing context.
+ * \param keyset_ptr Resulting zone keyset.
+ * \param verbose Print key summary into log.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int load_zone_keys(kdnssec_ctx_t *ctx, zone_keyset_t *keyset_ptr, bool verbose);
+
+/*!
+ * \brief Free structure with zone keys and associated DNSSEC contexts.
+ *
+ * \param keyset Zone keys.
+ */
+void free_zone_keys(zone_keyset_t *keyset);
+
+/*!
+ * \brief Get timestamp of next key event.
+ *
+ * \param keyset Zone keys.
+ *
+ * \return Timestamp of next key event.
+ */
+knot_time_t knot_get_next_zone_key_event(const zone_keyset_t *keyset);
+
+/*!
+ * \brief Returns DS record rdata for given key.
+ *
+ * This function caches the results, so calling again with the same key returns immediately.
+ *
+ * \param for_key The key to compute DS for.
+ * \param digesttype DS digest algorithm.
+ * \param out_donotfree Output: the DS record rdata. Do not call dnssec_binary_free() on this ever.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int zone_key_calculate_ds(zone_key_t *for_key, dnssec_key_digest_t digesttype,
+ dnssec_binary_t *out_donotfree);
+
+/*!
+ * \brief Initialize local signing context.
+ *
+ * \param keyset Key set.
+ * \param dnssec_ctx DNSSEC context.
+ *
+ * \return New local signing context or NULL.
+ */
+zone_sign_ctx_t *zone_sign_ctx(const zone_keyset_t *keyset, const kdnssec_ctx_t *dnssec_ctx);
+
+/*!
+ * \brief Initialize local validating context.
+ * \param dnssec_ctx DNSSEC context.
+ * \return New local validating context or NULL.
+ */
+zone_sign_ctx_t *zone_validation_ctx(const kdnssec_ctx_t *dnssec_ctx);
+
+/*!
+ * \brief Free local signing context.
+ *
+ * \note This doesn't free the underlying keyset.
+ *
+ * \param ctx Local context to be freed.
+ */
+void zone_sign_ctx_free(zone_sign_ctx_t *ctx);
+
+/*!
+ * \brief Create key signing structure from DNSKEY zone record.
+ *
+ * \param key Dnssec key to be allocated.
+ * \param owner Zone name.
+ * \param rdata DNSKEY rdata.
+ * \param rdlen DNSKEY rdata length.
+ *
+ * \return KNOT_E*
+ */
+int dnssec_key_from_rdata(dnssec_key_t **key, const knot_dname_t *owner,
+ const uint8_t *rdata, size_t rdlen);
+
+/*!
+ * \brief Tell if apex SOA is signed by all active ZSKs.
+ *
+ * \param keyset Zone key set.
+ * \param apex_rrsig Apex RRSIG RRSet.
+ */
+int is_soa_signed_by_all_zsks(const zone_keyset_t *keyset,
+ const knot_rdataset_t *apex_rrsig);
diff --git a/src/knot/dnssec/zone-nsec.c b/src/knot/dnssec/zone-nsec.c
new file mode 100644
index 0000000..e952061
--- /dev/null
+++ b/src/knot/dnssec/zone-nsec.c
@@ -0,0 +1,429 @@
+/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include "libdnssec/error.h"
+#include "libknot/descriptor.h"
+#include "libknot/rrtype/nsec3.h"
+#include "libknot/rrtype/soa.h"
+#include "knot/common/log.h"
+#include "knot/dnssec/nsec-chain.h"
+#include "knot/dnssec/nsec3-chain.h"
+#include "knot/dnssec/key-events.h"
+#include "knot/dnssec/rrset-sign.h"
+#include "knot/dnssec/zone-nsec.h"
+#include "knot/dnssec/zone-sign.h"
+#include "knot/zone/zone-diff.h"
+#include "contrib/base32hex.h"
+#include "contrib/wire_ctx.h"
+
+int knot_nsec3_hash_to_dname(uint8_t *out, size_t out_size, const uint8_t *hash,
+ size_t hash_size, const knot_dname_t *zone_apex)
+
+{
+ if (out == NULL || hash == NULL || zone_apex == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ // Encode raw hash to the first label.
+ uint8_t label[KNOT_DNAME_MAXLABELLEN];
+ int32_t label_size = knot_base32hex_encode(hash, hash_size, label, sizeof(label));
+ if (label_size <= 0) {
+ return label_size;
+ }
+
+ // Write the result, which already is in lower-case.
+ wire_ctx_t wire = wire_ctx_init(out, out_size);
+
+ wire_ctx_write_u8(&wire, label_size);
+ wire_ctx_write(&wire, label, label_size);
+ wire_ctx_write(&wire, zone_apex, knot_dname_size(zone_apex));
+
+ return wire.error;
+}
+
+int knot_create_nsec3_owner(uint8_t *out, size_t out_size,
+ const knot_dname_t *owner, const knot_dname_t *zone_apex,
+ const dnssec_nsec3_params_t *params)
+{
+ if (out == NULL || owner == NULL || zone_apex == NULL || params == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ dnssec_binary_t data = {
+ .data = (uint8_t *)owner,
+ .size = knot_dname_size(owner)
+ };
+
+ dnssec_binary_t hash = { 0 };
+
+ int ret = dnssec_nsec3_hash(&data, params, &hash);
+ if (ret != DNSSEC_EOK) {
+ return knot_error_from_libdnssec(ret);
+ }
+
+ ret = knot_nsec3_hash_to_dname(out, out_size, hash.data, hash.size, zone_apex);
+
+ dnssec_binary_free(&hash);
+
+ return ret;
+}
+
+knot_dname_t *node_nsec3_hash(zone_node_t *node, const zone_contents_t *zone)
+{
+ if (node->nsec3_hash == NULL && knot_is_nsec3_enabled(zone)) {
+ assert(!(node->flags & NODE_FLAGS_NSEC3_NODE));
+ size_t hash_size = zone_nsec3_name_len(zone);
+ knot_dname_t *hash = malloc(hash_size);
+ if (hash == NULL) {
+ return NULL;
+ }
+ if (knot_create_nsec3_owner(hash, hash_size, node->owner, zone->apex->owner,
+ &zone->nsec3_params) != KNOT_EOK) {
+ free(hash);
+ return NULL;
+ }
+ node->nsec3_hash = hash;
+ }
+
+ if (node->flags & NODE_FLAGS_NSEC3_NODE) {
+ return node->nsec3_node->owner;
+ } else {
+ return node->nsec3_hash;
+ }
+}
+
+zone_node_t *node_nsec3_node(zone_node_t *node, const zone_contents_t *zone)
+{
+ if (!(node->flags & NODE_FLAGS_NSEC3_NODE) && knot_is_nsec3_enabled(zone)) {
+ knot_dname_t *hash = node_nsec3_hash(node, zone);
+ zone_node_t *nsec3 = zone_tree_get(zone->nsec3_nodes, hash);
+ if (nsec3 != NULL) {
+ if (node->nsec3_hash != binode_counterpart(node)->nsec3_hash) {
+ free(node->nsec3_hash);
+ }
+ node->nsec3_node = binode_first(nsec3);
+ node->flags |= NODE_FLAGS_NSEC3_NODE;
+ }
+ }
+
+ return node_nsec3_get(node);
+}
+
+int binode_fix_nsec3_pointer(zone_node_t *node, const zone_contents_t *zone)
+{
+ zone_node_t *counter = binode_counterpart(node);
+ if (counter->nsec3_hash == NULL) {
+ (void)node_nsec3_node(node, zone);
+ return KNOT_EOK;
+ }
+ assert(counter->nsec3_node != NULL); // shut up cppcheck
+
+ zone_node_t *nsec3_counter = (counter->flags & NODE_FLAGS_NSEC3_NODE) ?
+ counter->nsec3_node : NULL;
+ if (nsec3_counter != NULL && !(binode_node_as(nsec3_counter, node)->flags & NODE_FLAGS_DELETED)) {
+ assert(node->flags & NODE_FLAGS_NSEC3_NODE);
+ node->flags |= NODE_FLAGS_NSEC3_NODE;
+ assert(!(nsec3_counter->flags & NODE_FLAGS_SECOND));
+ node->nsec3_node = nsec3_counter;
+ } else {
+ node->flags &= ~NODE_FLAGS_NSEC3_NODE;
+ if (counter->flags & NODE_FLAGS_NSEC3_NODE) {
+ // downgrade the NSEC3 node pointer to NSEC3 name
+ node->nsec3_hash = knot_dname_copy(counter->nsec3_node->owner, NULL);
+ } else {
+ node->nsec3_hash = counter->nsec3_hash;
+ }
+ (void)node_nsec3_node(node, zone);
+ }
+ return KNOT_EOK;
+}
+
+static bool nsec3param_valid(const knot_rdataset_t *rrs,
+ const dnssec_nsec3_params_t *params)
+{
+ assert(rrs);
+ assert(params);
+
+ // NSEC3 disabled
+ if (params->algorithm == 0) {
+ return false;
+ }
+
+ // multiple NSEC3 records
+ if (rrs->count != 1) {
+ return false;
+ }
+
+ dnssec_binary_t rdata = {
+ .size = rrs->rdata->len,
+ .data = rrs->rdata->data,
+ };
+
+ dnssec_nsec3_params_t parsed = { 0 };
+ int r = dnssec_nsec3_params_from_rdata(&parsed, &rdata);
+ if (r != DNSSEC_EOK) {
+ return false;
+ }
+
+ bool equal = parsed.algorithm == params->algorithm &&
+ parsed.flags == 0 && // opt-out flag is always 0 in NSEC3PARAM
+ parsed.iterations == params->iterations &&
+ dnssec_binary_cmp(&parsed.salt, &params->salt) == 0;
+
+ dnssec_nsec3_params_free(&parsed);
+
+ return equal;
+}
+
+static int remove_nsec3param(zone_update_t *update, bool also_rrsig)
+{
+ knot_rrset_t rrset = node_rrset(update->new_cont->apex, KNOT_RRTYPE_NSEC3PARAM);
+ int ret = zone_update_remove(update, &rrset);
+
+ rrset = node_rrset(update->new_cont->apex, KNOT_RRTYPE_RRSIG);
+ if (!knot_rrset_empty(&rrset) && ret == KNOT_EOK && also_rrsig) {
+ knot_rrset_t rrsig;
+ knot_rrset_init(&rrsig, update->new_cont->apex->owner,
+ KNOT_RRTYPE_RRSIG, KNOT_CLASS_IN, 0);
+ ret = knot_synth_rrsig(KNOT_RRTYPE_NSEC3PARAM, &rrset.rrs, &rrsig.rrs, NULL);
+ if (ret == KNOT_EOK) {
+ ret = zone_update_remove(update, &rrsig);
+ }
+ knot_rdataset_clear(&rrsig.rrs, NULL);
+ }
+
+ return ret;
+}
+
+static int set_nsec3param(knot_rrset_t *rrset, const dnssec_nsec3_params_t *params)
+{
+ assert(rrset);
+ assert(params);
+
+ // Prepare wire rdata.
+ size_t rdata_len = 3 * sizeof(uint8_t) + sizeof(uint16_t) + params->salt.size;
+ uint8_t rdata[rdata_len];
+ wire_ctx_t wire = wire_ctx_init(rdata, rdata_len);
+
+ wire_ctx_write_u8(&wire, params->algorithm);
+ wire_ctx_write_u8(&wire, 0); // (RFC 5155 Section 4.1.2)
+ wire_ctx_write_u16(&wire, params->iterations);
+ wire_ctx_write_u8(&wire, params->salt.size);
+ wire_ctx_write(&wire, params->salt.data, params->salt.size);
+
+ if (wire.error != KNOT_EOK) {
+ return wire.error;
+ }
+
+ assert(wire_ctx_available(&wire) == 0);
+
+ return knot_rrset_add_rdata(rrset, rdata, rdata_len, NULL);
+}
+
+static int add_nsec3param(zone_update_t *update,
+ const dnssec_nsec3_params_t *params,
+ uint32_t ttl)
+{
+ assert(update);
+ assert(params);
+
+ knot_rrset_t *rrset = NULL;
+ rrset = knot_rrset_new(update->new_cont->apex->owner, KNOT_RRTYPE_NSEC3PARAM,
+ KNOT_CLASS_IN, ttl, NULL);
+ if (rrset == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ int r = set_nsec3param(rrset, params);
+ if (r == KNOT_EOK) {
+ r = zone_update_add(update, rrset);
+ }
+ knot_rrset_free(rrset, NULL);
+ return r;
+}
+
+bool knot_nsec3param_uptodate(const zone_contents_t *zone,
+ const dnssec_nsec3_params_t *params)
+{
+ assert(zone);
+ assert(params);
+
+ knot_rdataset_t *nsec3param = node_rdataset(zone->apex, KNOT_RRTYPE_NSEC3PARAM);
+
+ return (nsec3param != NULL && nsec3param_valid(nsec3param, params));
+}
+
+int knot_nsec3param_update(zone_update_t *update,
+ const dnssec_nsec3_params_t *params,
+ uint32_t ttl)
+{
+ assert(update);
+ assert(params);
+
+ knot_rdataset_t *nsec3param = node_rdataset(update->new_cont->apex, KNOT_RRTYPE_NSEC3PARAM);
+ bool valid = nsec3param && nsec3param_valid(nsec3param, params);
+
+ if (nsec3param && !valid) {
+ int r = remove_nsec3param(update, params->algorithm == 0);
+ if (r != KNOT_EOK) {
+ return r;
+ }
+ }
+
+ if (params->algorithm != 0 && !valid) {
+ return add_nsec3param(update, params, ttl);
+ }
+
+ return KNOT_EOK;
+}
+
+/*!
+ * \brief Initialize NSEC3PARAM based on the signing policy.
+ *
+ * \note For NSEC, the algorithm number is set to 0.
+ */
+static dnssec_nsec3_params_t nsec3param_init(const knot_kasp_policy_t *policy,
+ const knot_kasp_zone_t *zone)
+{
+ assert(policy);
+ assert(zone);
+
+ dnssec_nsec3_params_t params = { 0 };
+ if (policy->nsec3_enabled) {
+ params.algorithm = DNSSEC_NSEC3_ALGORITHM_SHA1;
+ params.iterations = policy->nsec3_iterations;
+ params.salt = zone->nsec3_salt;
+ params.flags = (policy->nsec3_opt_out ? KNOT_NSEC3_FLAG_OPT_OUT : 0);
+ }
+
+ return params;
+}
+
+// int: returns KNOT_E* if error
+static int zone_nsec_ttl(zone_contents_t *zone)
+{
+ knot_rrset_t soa = node_rrset(zone->apex, KNOT_RRTYPE_SOA);
+ if (knot_rrset_empty(&soa)) {
+ return KNOT_EINVAL;
+ }
+
+ return MIN(knot_soa_minimum(soa.rrs.rdata), soa.ttl);
+}
+
+int knot_zone_create_nsec_chain(zone_update_t *update, const kdnssec_ctx_t *ctx)
+{
+ if (update == NULL || ctx == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ if (ctx->policy->unsafe & UNSAFE_NSEC) {
+ return KNOT_EOK;
+ }
+
+ int nsec_ttl = zone_nsec_ttl(update->new_cont);
+ if (nsec_ttl < 0) {
+ return nsec_ttl;
+ }
+
+ dnssec_nsec3_params_t params = nsec3param_init(ctx->policy, ctx->zone);
+
+ int ret = knot_nsec3param_update(update, &params, nsec_ttl);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (ctx->policy->nsec3_enabled) {
+ ret = knot_nsec3_create_chain(update->new_cont, &params, nsec_ttl,
+ update);
+ } else {
+ ret = knot_nsec_create_chain(update, nsec_ttl);
+ if (ret == KNOT_EOK) {
+ ret = delete_nsec3_chain(update);
+ }
+ }
+ return ret;
+}
+
+int knot_zone_fix_nsec_chain(zone_update_t *update,
+ const zone_keyset_t *zone_keys,
+ const kdnssec_ctx_t *ctx)
+{
+ if (update == NULL || ctx == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ if (ctx->policy->unsafe & UNSAFE_NSEC) {
+ return KNOT_EOK;
+ }
+
+ int nsec_ttl_old = zone_nsec_ttl(update->zone->contents);
+ int nsec_ttl_new = zone_nsec_ttl(update->new_cont);
+ if (nsec_ttl_old < 0 || nsec_ttl_new < 0) {
+ return MIN(nsec_ttl_old, nsec_ttl_new);
+ }
+
+ dnssec_nsec3_params_t params = nsec3param_init(ctx->policy, ctx->zone);
+
+ int ret;
+ if (nsec_ttl_old != nsec_ttl_new || (update->flags & UPDATE_CHANGED_NSEC)) {
+ ret = KNOT_ENORECORD;
+ } else if (ctx->policy->nsec3_enabled) {
+ ret = knot_nsec3_fix_chain(update, &params, nsec_ttl_new);
+ } else {
+ ret = knot_nsec_fix_chain(update, nsec_ttl_new);
+ }
+ if (ret == KNOT_ENORECORD) {
+ log_zone_info(update->zone->name, "DNSSEC, re-creating whole NSEC%s chain",
+ (ctx->policy->nsec3_enabled ? "3" : ""));
+ if (ctx->policy->nsec3_enabled) {
+ ret = knot_nsec3_create_chain(update->new_cont, &params,
+ nsec_ttl_new, update);
+ } else {
+ ret = knot_nsec_create_chain(update, nsec_ttl_new);
+ }
+ }
+ if (ret == KNOT_EOK) {
+ ret = knot_zone_sign_nsecs_in_changeset(zone_keys, ctx, update);
+ }
+ return ret;
+}
+
+int knot_zone_check_nsec_chain(zone_update_t *update, const kdnssec_ctx_t *ctx,
+ bool incremental)
+{
+ int ret = KNOT_EOK;
+ dnssec_nsec3_params_t params = nsec3param_init(ctx->policy, ctx->zone);
+
+ if (incremental) {
+ ret = ctx->policy->nsec3_enabled
+ ? knot_nsec3_check_chain_fix(update, &params)
+ : knot_nsec_check_chain_fix(update);
+ }
+ if (ret == KNOT_ENORECORD) {
+ log_zone_info(update->zone->name, "DNSSEC, re-validating whole NSEC%s chain",
+ (ctx->policy->nsec3_enabled ? "3" : ""));
+ incremental = false;
+ }
+
+ if (incremental) {
+ return ret;
+ }
+
+ return ctx->policy->nsec3_enabled ? knot_nsec3_check_chain(update, &params) :
+ knot_nsec_check_chain(update);
+}
diff --git a/src/knot/dnssec/zone-nsec.h b/src/knot/dnssec/zone-nsec.h
new file mode 100644
index 0000000..c43b658
--- /dev/null
+++ b/src/knot/dnssec/zone-nsec.h
@@ -0,0 +1,163 @@
+/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+
+#include "knot/dnssec/context.h"
+#include "knot/dnssec/zone-keys.h"
+#include "knot/updates/zone-update.h"
+#include "knot/zone/contents.h"
+
+/*!
+ * Check if NSEC3 is enabled for the given zone.
+ *
+ * \param zone Zone to be checked.
+ *
+ * \return NSEC3 is enabled.
+ */
+inline static bool knot_is_nsec3_enabled(const zone_contents_t *zone)
+{
+ return zone != NULL && zone->nsec3_params.algorithm != 0;
+}
+
+inline static size_t zone_nsec3_hash_len(const zone_contents_t *zone)
+{
+ return knot_is_nsec3_enabled(zone) ? dnssec_nsec3_hash_length(zone->nsec3_params.algorithm) : 0;
+}
+
+inline static size_t zone_nsec3_name_len(const zone_contents_t *zone)
+{
+ return 1 + ((zone_nsec3_hash_len(zone) + 4) / 5) * 8 + knot_dname_size(zone->apex->owner);
+}
+
+/*!
+ * \brief Create NSEC3 owner name from hash and zone apex.
+ *
+ * \param out Output buffer.
+ * \param out_size Size of the output buffer.
+ * \param hash Raw hash.
+ * \param hash_size Size of the hash.
+ * \param zone_apex Zone apex.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_nsec3_hash_to_dname(uint8_t *out, size_t out_size, const uint8_t *hash,
+ size_t hash_size, const knot_dname_t *zone_apex);
+
+/*!
+ * \brief Create NSEC3 owner name from regular owner name.
+ *
+ * \param out Output buffer.
+ * \param out_size Size of the output buffer.
+ * \param owner Node owner name.
+ * \param zone_apex Zone apex name.
+ * \param params Params for NSEC3 hashing function.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_create_nsec3_owner(uint8_t *out, size_t out_size,
+ const knot_dname_t *owner, const knot_dname_t *zone_apex,
+ const dnssec_nsec3_params_t *params);
+
+/*!
+ * \brief Return (and compute of needed) the corresponding NSEC3 node's name.
+ *
+ * \param node Normal node.
+ * \param zone Optional: zone contents with NSEC3 params.
+ *
+ * \return NSEC3 node owner.
+ *
+ * \note The result is also stored in (node), unless zone == NULL;
+ */
+knot_dname_t *node_nsec3_hash(zone_node_t *node, const zone_contents_t *zone);
+
+/*!
+ * \brief Return (and compute if needed) the corresponding NSEC3 node.
+ *
+ * \param node Normal node.
+ * \param zone Optional: zone contents with NSEC3 params and NSEC3 tree.
+ *
+ * \return NSEC3 node.
+ *
+ * \note The result is also stored in (node), unless zone == NULL;
+ */
+zone_node_t *node_nsec3_node(zone_node_t *node, const zone_contents_t *zone);
+
+/*!
+ * \brief Update node's NSEC3 pointer (or hash), taking it from bi-node counterpart if possible.
+ *
+ * \param node Bi-node with this node to be updated.
+ * \param zone Zone contents the node is in.
+ *
+ * \return KNOT_EOK :)
+ */
+int binode_fix_nsec3_pointer(zone_node_t *node, const zone_contents_t *zone);
+
+/*!
+ * \brief Check if NSEC3 record in zone is consistent with configured params.
+ */
+bool knot_nsec3param_uptodate(const zone_contents_t *zone,
+ const dnssec_nsec3_params_t *params);
+
+/*!
+ * \brief Update NSEC3PARAM in zone to be consistent with configured params.
+ *
+ * \param update Zone to be updated.
+ * \param params NSEC3 params.
+ * \param ttl Desired TTL for NSEC3PARAM.
+ *
+ * \return KNOT_E*
+ */
+int knot_nsec3param_update(zone_update_t *update,
+ const dnssec_nsec3_params_t *params,
+ uint32_t ttl);
+
+/*!
+ * \brief Create NSEC or NSEC3 chain in the zone.
+ *
+ * \param update Zone Update with current zone contents and to be updated with NSEC chain.
+ * \param ctx Signing context.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_zone_create_nsec_chain(zone_update_t *update, const kdnssec_ctx_t *ctx);
+
+/*!
+ * \brief Fix NSEC or NSEC3 chain after zone was updated, and sign the changed NSECs.
+ *
+ * \param update Zone Update with the update and to be update with NSEC chain.
+ * \param zone_keys Zone keys used for NSEC(3) creation.
+ * \param ctx Signing context.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_zone_fix_nsec_chain(zone_update_t *update,
+ const zone_keyset_t *zone_keys,
+ const kdnssec_ctx_t *ctx);
+
+/*!
+ * \brief Validate NSEC or NSEC3 chain in the zone.
+ *
+ * \param update Zone update with current/previous contents.
+ * \param ctx Signing context.
+ * \param incremental Validate incremental update.
+ *
+ * \return KNOT_E*
+ */
+int knot_zone_check_nsec_chain(zone_update_t *update, const kdnssec_ctx_t *ctx,
+ bool incremental);
diff --git a/src/knot/dnssec/zone-sign.c b/src/knot/dnssec/zone-sign.c
new file mode 100644
index 0000000..ffa10c4
--- /dev/null
+++ b/src/knot/dnssec/zone-sign.c
@@ -0,0 +1,1081 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <pthread.h>
+#include <sys/types.h>
+
+#include "libdnssec/error.h"
+#include "libdnssec/key.h"
+#include "libdnssec/keytag.h"
+#include "libdnssec/sign.h"
+#include "knot/common/log.h"
+#include "knot/dnssec/key-events.h"
+#include "knot/dnssec/key_records.h"
+#include "knot/dnssec/rrset-sign.h"
+#include "knot/dnssec/zone-sign.h"
+#include "libknot/libknot.h"
+#include "libknot/dynarray.h"
+#include "contrib/wire_ctx.h"
+
+typedef struct {
+ node_t n;
+ uint16_t type;
+} type_node_t;
+
+typedef struct {
+ knot_dname_t *dname;
+ knot_dname_t *hashed_dname;
+ list_t *type_list;
+} signed_info_t;
+
+/*- private API - common functions -------------------------------------------*/
+
+/*!
+ * \brief Initializes RR set and set owner and rclass from template RR set.
+ */
+static knot_rrset_t rrset_init_from(const knot_rrset_t *src, uint16_t type)
+{
+ assert(src);
+ knot_rrset_t rrset;
+ knot_rrset_init(&rrset, src->owner, type, src->rclass, src->ttl);
+ return rrset;
+}
+
+/*!
+ * \brief Create empty RRSIG RR set for a given RR set to be covered.
+ */
+static knot_rrset_t create_empty_rrsigs_for(const knot_rrset_t *covered)
+{
+ assert(!knot_rrset_empty(covered));
+ return rrset_init_from(covered, KNOT_RRTYPE_RRSIG);
+}
+
+static bool apex_rr_changed(const zone_node_t *old_apex,
+ const zone_node_t *new_apex,
+ uint16_t type)
+{
+ assert(old_apex);
+ assert(new_apex);
+ knot_rrset_t old_rr = node_rrset(old_apex, type);
+ knot_rrset_t new_rr = node_rrset(new_apex, type);
+
+ return !knot_rrset_equal(&old_rr, &new_rr, false);
+}
+
+static bool apex_dnssec_changed(zone_update_t *update)
+{
+ if (update->zone->contents == NULL || update->new_cont == NULL) {
+ return false;
+ }
+ return apex_rr_changed(update->zone->contents->apex,
+ update->new_cont->apex, KNOT_RRTYPE_DNSKEY) ||
+ apex_rr_changed(update->zone->contents->apex,
+ update->new_cont->apex, KNOT_RRTYPE_NSEC3PARAM);
+}
+
+/*- private API - signing of in-zone nodes -----------------------------------*/
+
+/*!
+ * \brief Check if there is a valid signature for a given RR set and key.
+ *
+ * \param covered RR set with covered records.
+ * \param rrsigs RR set with RRSIGs.
+ * \param key Signing key.
+ * \param ctx Signing context.
+ * \param policy DNSSEC policy.
+ * \param skip_crypto All RRSIGs in this node have been verified, just check validity.
+ * \param refresh Consider RRSIG expired when gonna expire this soon.
+ * \param found_invalid Out: some matching but expired%invalid RRSIG found.
+ * \param at Out: RRSIG position.
+ *
+ * \return The signature exists and is valid.
+ */
+static bool valid_signature_exists(const knot_rrset_t *covered,
+ const knot_rrset_t *rrsigs,
+ const dnssec_key_t *key,
+ dnssec_sign_ctx_t *ctx,
+ const kdnssec_ctx_t *dnssec_ctx,
+ knot_timediff_t refresh,
+ bool skip_crypto,
+ int *found_invalid,
+ uint16_t *at)
+{
+ assert(key);
+
+ if (knot_rrset_empty(rrsigs)) {
+ return false;
+ }
+
+ uint16_t rrsigs_rdata_count = rrsigs->rrs.count;
+ knot_rdata_t *rdata = rrsigs->rrs.rdata;
+ bool found_valid = false;
+ for (uint16_t i = 0; i < rrsigs_rdata_count; i++) {
+ uint16_t rr_keytag = knot_rrsig_key_tag(rdata);
+ uint16_t rr_covered = knot_rrsig_type_covered(rdata);
+ uint8_t rr_algo = knot_rrsig_alg(rdata);
+ rdata = knot_rdataset_next(rdata);
+
+ uint16_t keytag = dnssec_key_get_keytag(key);
+ uint8_t algo = dnssec_key_get_algorithm(key);
+ if (rr_keytag != keytag || rr_algo != algo || rr_covered != covered->type) {
+ continue;
+ }
+
+ int ret = knot_check_signature(covered, rrsigs, i, key, ctx,
+ dnssec_ctx, refresh, skip_crypto);
+ if (ret == KNOT_EOK) {
+ if (at != NULL) {
+ *at = i;
+ }
+ if (found_invalid == NULL) {
+ return true;
+ } else {
+ found_valid = true; // continue searching for invalid RRSIG
+ }
+ } else if (found_invalid != NULL) {
+ *found_invalid = ret;
+ }
+ }
+
+ return found_valid;
+}
+
+/*!
+ * \brief Note earliest expiration of a signature.
+ *
+ * \param rrsig RRSIG rdata.
+ * \param now Current 64-bit timestamp.
+ * \param expires_at Current earliest expiration, will be updated.
+ */
+static void note_earliest_expiration(const knot_rdata_t *rrsig, knot_time_t now,
+ knot_time_t *expires_at)
+{
+ assert(rrsig);
+ if (expires_at == NULL) {
+ return;
+ }
+
+ uint32_t curr_rdata = knot_rrsig_sig_expiration(rrsig);
+ knot_time_t current = knot_time_from_u32(curr_rdata, now);
+
+ *expires_at = knot_time_min(current, *expires_at);
+}
+
+bool rrsig_covers_type(const knot_rrset_t *rrsig, uint16_t type)
+{
+ if (knot_rrset_empty(rrsig)) {
+ return false;
+ }
+ assert(rrsig->type == KNOT_RRTYPE_RRSIG);
+ knot_rdata_t *one_rr = rrsig->rrs.rdata;
+ for (int i = 0; i < rrsig->rrs.count; i++) {
+ if (type == knot_rrsig_type_covered(one_rr)) {
+ return true;
+ }
+ one_rr = knot_rdataset_next(one_rr);
+ }
+ return false;
+}
+
+/*!
+ * \brief Add missing RRSIGs into the changeset for adding.
+ *
+ * \note Also removes invalid RRSIGs.
+ *
+ * \param covered RR set with covered records.
+ * \param rrsigs RR set with RRSIGs.
+ * \param sign_ctx Local zone signing context.
+ * \param skip_crypto All RRSIGs in this node have been verified, just check validity.
+ * \param changeset Changeset to be updated.
+ * \param update Zone update to be updated. Exactly one of "changeset" and "update" must be NULL!
+ * \param expires_at Earliest RRSIG expiration.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int add_missing_rrsigs(const knot_rrset_t *covered,
+ const knot_rrset_t *rrsigs,
+ zone_sign_ctx_t *sign_ctx,
+ bool skip_crypto,
+ changeset_t *changeset,
+ zone_update_t *update,
+ knot_time_t *expires_at)
+{
+ assert(!knot_rrset_empty(covered));
+ assert(sign_ctx);
+ assert((bool)changeset != (bool)update);
+
+ knot_rrset_t to_add = create_empty_rrsigs_for(covered);
+ knot_rrset_t to_remove = create_empty_rrsigs_for(covered);
+ int result = (!rrsig_covers_type(rrsigs, covered->type) ? KNOT_EOK :
+ knot_synth_rrsig(covered->type, &rrsigs->rrs, &to_remove.rrs, NULL));
+
+ if (result == KNOT_EOK && sign_ctx->dnssec_ctx->offline_records.rrsig.rrs.count > 0 &&
+ knot_dname_cmp(sign_ctx->dnssec_ctx->offline_records.rrsig.owner, covered->owner) == 0 &&
+ rrsig_covers_type(&sign_ctx->dnssec_ctx->offline_records.rrsig, covered->type)) {
+ result = knot_synth_rrsig(covered->type,
+ &sign_ctx->dnssec_ctx->offline_records.rrsig.rrs, &to_add.rrs, NULL);
+ if (result == KNOT_EOK) {
+ // don't remove what shall be added
+ result = knot_rdataset_subtract(&to_remove.rrs, &to_add.rrs, NULL);
+ }
+ if (result == KNOT_EOK && !knot_rrset_empty(rrsigs)) {
+ // don't add what's already present
+ result = knot_rdataset_subtract(&to_add.rrs, &rrsigs->rrs, NULL);
+ }
+ }
+
+ for (size_t i = 0; i < sign_ctx->count && result == KNOT_EOK; i++) {
+ const zone_key_t *key = &sign_ctx->keys[i];
+ if (!knot_zone_sign_use_key(key, covered)) {
+ continue;
+ }
+
+ uint16_t valid_at;
+ knot_timediff_t refresh = sign_ctx->dnssec_ctx->policy->rrsig_refresh_before +
+ sign_ctx->dnssec_ctx->policy->rrsig_prerefresh;
+ if (valid_signature_exists(covered, rrsigs, key->key, sign_ctx->sign_ctxs[i],
+ sign_ctx->dnssec_ctx, refresh, skip_crypto, NULL, &valid_at)) {
+ knot_rdata_t *valid_rr = knot_rdataset_at(&rrsigs->rrs, valid_at);
+ result = knot_rdataset_remove(&to_remove.rrs, valid_rr, NULL);
+ note_earliest_expiration(valid_rr, sign_ctx->dnssec_ctx->now, expires_at);
+ continue;
+ }
+ result = knot_sign_rrset(&to_add, covered, key->key, sign_ctx->sign_ctxs[i],
+ sign_ctx->dnssec_ctx, NULL, expires_at);
+ }
+
+ if (!knot_rrset_empty(&to_remove) && result == KNOT_EOK) {
+ if (changeset != NULL) {
+ result = changeset_add_removal(changeset, &to_remove, 0);
+ } else {
+ result = zone_update_remove(update, &to_remove);
+ }
+ }
+
+ if (!knot_rrset_empty(&to_add) && result == KNOT_EOK) {
+ if (changeset != NULL) {
+ result = changeset_add_addition(changeset, &to_add, 0);
+ } else {
+ result = zone_update_add(update, &to_add);
+ }
+ }
+
+ knot_rdataset_clear(&to_add.rrs, NULL);
+ knot_rdataset_clear(&to_remove.rrs, NULL);
+
+ return result;
+}
+
+static bool key_used(bool ksk, bool zsk, uint16_t type,
+ const knot_dname_t *owner, const knot_dname_t *zone_apex)
+{
+ if (knot_dname_cmp(owner, zone_apex) != 0) {
+ return zsk;
+ }
+ switch (type) {
+ case KNOT_RRTYPE_DNSKEY:
+ case KNOT_RRTYPE_CDNSKEY:
+ case KNOT_RRTYPE_CDS:
+ return ksk;
+ default:
+ return zsk;
+ }
+}
+
+int knot_validate_rrsigs(const knot_rrset_t *covered,
+ const knot_rrset_t *rrsigs,
+ zone_sign_ctx_t *sign_ctx,
+ bool skip_crypto)
+{
+ if (covered == NULL || rrsigs == NULL || sign_ctx == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ bool valid_exists = false;
+ int ret = KNOT_EOK;
+ for (size_t i = 0; i < sign_ctx->count; i++) {
+ const knot_kasp_key_t *key = &sign_ctx->dnssec_ctx->zone->keys[i];
+ if (!key_used(key->is_ksk, key->is_zsk, covered->type,
+ covered->owner, sign_ctx->dnssec_ctx->zone->dname)) {
+ continue;
+ }
+
+ uint16_t valid_at;
+ if (valid_signature_exists(covered, rrsigs, key->key, sign_ctx->sign_ctxs[i],
+ sign_ctx->dnssec_ctx, 0, skip_crypto, &ret, &valid_at)) {
+ valid_exists = true;
+ }
+ }
+
+ return valid_exists ? ret : KNOT_DNSSEC_ENOSIG;
+}
+
+/*!
+ * \brief Add all RRSIGs into the changeset for removal.
+ *
+ * \param covered RR set with covered records.
+ * \param changeset Changeset to be updated.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int remove_rrset_rrsigs(const knot_dname_t *owner, uint16_t type,
+ const knot_rrset_t *rrsigs,
+ changeset_t *changeset)
+{
+ assert(owner);
+ assert(changeset);
+ knot_rrset_t synth_rrsig;
+ knot_rrset_init(&synth_rrsig, (knot_dname_t *)owner,
+ KNOT_RRTYPE_RRSIG, rrsigs->rclass, rrsigs->ttl);
+ int ret = knot_synth_rrsig(type, &rrsigs->rrs, &synth_rrsig.rrs, NULL);
+ if (ret != KNOT_EOK) {
+ if (ret != KNOT_ENOENT) {
+ return ret;
+ }
+ return KNOT_EOK;
+ }
+
+ ret = changeset_add_removal(changeset, &synth_rrsig, 0);
+ knot_rdataset_clear(&synth_rrsig.rrs, NULL);
+
+ return ret;
+}
+
+/*!
+ * \brief Drop all existing and create new RRSIGs for covered records.
+ *
+ * \param covered RR set with covered records.
+ * \param rrsigs Existing RRSIGs for covered RR set.
+ * \param sign_ctx Local zone signing context.
+ * \param changeset Changeset to be updated.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int force_resign_rrset(const knot_rrset_t *covered,
+ const knot_rrset_t *rrsigs,
+ zone_sign_ctx_t *sign_ctx,
+ changeset_t *changeset)
+{
+ assert(!knot_rrset_empty(covered));
+
+ if (!knot_rrset_empty(rrsigs)) {
+ int result = remove_rrset_rrsigs(covered->owner, covered->type,
+ rrsigs, changeset);
+ if (result != KNOT_EOK) {
+ return result;
+ }
+ }
+
+ return add_missing_rrsigs(covered, NULL, sign_ctx, false, changeset, NULL, NULL);
+}
+
+/*!
+ * \brief Drop all expired and create new RRSIGs for covered records.
+ *
+ * \param covered RR set with covered records.
+ * \param rrsigs Existing RRSIGs for covered RR set.
+ * \param sign_ctx Local zone signing context.
+ * \param skip_crypto All RRSIGs in this node have been verified, just check validity.
+ * \param changeset Changeset to be updated.
+ * \param expires_at Current earliest expiration, will be updated.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int resign_rrset(const knot_rrset_t *covered,
+ const knot_rrset_t *rrsigs,
+ zone_sign_ctx_t *sign_ctx,
+ bool skip_crypto,
+ changeset_t *changeset,
+ knot_time_t *expires_at)
+{
+ assert(!knot_rrset_empty(covered));
+
+ return add_missing_rrsigs(covered, rrsigs, sign_ctx, skip_crypto, changeset, NULL, expires_at);
+}
+
+static int remove_standalone_rrsigs(const zone_node_t *node,
+ const knot_rrset_t *rrsigs,
+ changeset_t *changeset)
+{
+ if (rrsigs == NULL) {
+ return KNOT_EOK;
+ }
+
+ uint16_t rrsigs_rdata_count = rrsigs->rrs.count;
+ knot_rdata_t *rdata = rrsigs->rrs.rdata;
+ for (uint16_t i = 0; i < rrsigs_rdata_count; ++i) {
+ uint16_t type_covered = knot_rrsig_type_covered(rdata);
+ if (!node_rrtype_exists(node, type_covered)) {
+ knot_rrset_t to_remove;
+ knot_rrset_init(&to_remove, rrsigs->owner, rrsigs->type,
+ rrsigs->rclass, rrsigs->ttl);
+ int ret = knot_rdataset_add(&to_remove.rrs, rdata, NULL);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ ret = changeset_add_removal(changeset, &to_remove, 0);
+ knot_rdataset_clear(&to_remove.rrs, NULL);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+ rdata = knot_rdataset_next(rdata);
+ }
+
+ return KNOT_EOK;
+}
+
+/*!
+ * \brief Update RRSIGs in a given node by updating changeset.
+ *
+ * \param node Node to be signed.
+ * \param sign_ctx Local zone signing context.
+ * \param changeset Changeset to be updated.
+ * \param expires_at Current earliest expiration, will be updated.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int sign_node_rrsets(const zone_node_t *node,
+ zone_sign_ctx_t *sign_ctx,
+ changeset_t *changeset,
+ knot_time_t *expires_at,
+ dnssec_validation_hint_t *hint)
+{
+ assert(node);
+ assert(sign_ctx);
+
+ int result = KNOT_EOK;
+ knot_rrset_t rrsigs = node_rrset(node, KNOT_RRTYPE_RRSIG);
+ bool skip_crypto = (node->flags & NODE_FLAGS_RRSIGS_VALID) &&
+ !sign_ctx->dnssec_ctx->keytag_conflict;
+
+ for (int i = 0; result == KNOT_EOK && i < node->rrset_count; i++) {
+ knot_rrset_t rrset = node_rrset_at(node, i);
+ assert(rrset.type != KNOT_RRTYPE_ANY);
+
+ if (!knot_zone_sign_rr_should_be_signed(node, &rrset)) {
+ if (!sign_ctx->dnssec_ctx->validation_mode) {
+ result = remove_rrset_rrsigs(rrset.owner, rrset.type, &rrsigs, changeset);
+ } else {
+ if (knot_synth_rrsig_exists(rrset.type, &rrsigs.rrs)) {
+ hint->node = node->owner;
+ hint->rrtype = rrset.type;
+ result = KNOT_DNSSEC_ENOSIG;
+ }
+ }
+ continue;
+ }
+
+ if (sign_ctx->dnssec_ctx->validation_mode) {
+ result = knot_validate_rrsigs(&rrset, &rrsigs, sign_ctx, skip_crypto);
+ if (result != KNOT_EOK) {
+ hint->node = node->owner;
+ hint->rrtype = rrset.type;
+ }
+ } else if (sign_ctx->dnssec_ctx->rrsig_drop_existing) {
+ result = force_resign_rrset(&rrset, &rrsigs,
+ sign_ctx, changeset);
+ } else {
+ result = resign_rrset(&rrset, &rrsigs, sign_ctx, skip_crypto,
+ changeset, expires_at);
+ }
+ }
+
+ if (result == KNOT_EOK) {
+ result = remove_standalone_rrsigs(node, &rrsigs, changeset);
+ }
+ return result;
+}
+
+/*!
+ * \brief Struct to carry data for 'sign_data' callback function.
+ */
+typedef struct {
+ zone_tree_t *tree;
+ zone_sign_ctx_t *sign_ctx;
+ changeset_t changeset;
+ knot_time_t expires_at;
+ dnssec_validation_hint_t *hint;
+ size_t num_threads;
+ size_t thread_index;
+ size_t rrset_index;
+ int errcode;
+ int thread_init_errcode;
+ pthread_t thread;
+} node_sign_args_t;
+
+/*!
+ * \brief Sign node (callback function).
+ *
+ * \param node Node to be signed.
+ * \param data Callback data, node_sign_args_t.
+ */
+static int sign_node(zone_node_t *node, void *data)
+{
+ assert(node);
+ assert(data);
+
+ node_sign_args_t *args = (node_sign_args_t *)data;
+
+ if (node->rrset_count == 0) {
+ return KNOT_EOK;
+ }
+
+ if (args->rrset_index++ % args->num_threads != args->thread_index) {
+ return KNOT_EOK;
+ }
+
+ int result = sign_node_rrsets(node, args->sign_ctx,
+ &args->changeset, &args->expires_at,
+ args->hint);
+
+ return result;
+}
+
+static void *tree_sign_thread(void *_arg)
+{
+ node_sign_args_t *arg = _arg;
+ arg->errcode = zone_tree_apply(arg->tree, sign_node, _arg);
+ return NULL;
+}
+
+static int set_signed(zone_node_t *node, _unused_ void *data)
+{
+ node->flags |= NODE_FLAGS_RRSIGS_VALID;
+ return KNOT_EOK;
+}
+
+/*!
+ * \brief Update RRSIGs in a given zone tree by updating changeset.
+ *
+ * \param tree Zone tree to be signed.
+ * \param num_threads Number of threads to use for parallel signing.
+ * \param zone_keys Zone keys.
+ * \param policy DNSSEC policy.
+ * \param update Zone update structure to be updated.
+ * \param expires_at Expiration time of the oldest signature in zone.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int zone_tree_sign(zone_tree_t *tree,
+ size_t num_threads,
+ zone_keyset_t *zone_keys,
+ const kdnssec_ctx_t *dnssec_ctx,
+ zone_update_t *update,
+ knot_time_t *expires_at)
+{
+ assert(zone_keys || dnssec_ctx->validation_mode);
+ assert(dnssec_ctx);
+ assert(update || dnssec_ctx->validation_mode);
+
+ int ret = KNOT_EOK;
+ node_sign_args_t args[num_threads];
+ memset(args, 0, sizeof(args));
+ *expires_at = knot_time_plus(dnssec_ctx->now, dnssec_ctx->policy->rrsig_lifetime);
+
+ // init context structures
+ for (size_t i = 0; i < num_threads; i++) {
+ args[i].tree = tree;
+ args[i].sign_ctx = dnssec_ctx->validation_mode
+ ? zone_validation_ctx(dnssec_ctx)
+ : zone_sign_ctx(zone_keys, dnssec_ctx);
+ if (args[i].sign_ctx == NULL) {
+ ret = KNOT_ENOMEM;
+ break;
+ }
+ ret = changeset_init(&args[i].changeset, dnssec_ctx->zone->dname);
+ if (ret != KNOT_EOK) {
+ break;
+ }
+ args[i].expires_at = 0;
+ args[i].hint = &update->validation_hint;
+ args[i].num_threads = num_threads;
+ args[i].thread_index = i;
+ args[i].rrset_index = 0;
+ args[i].errcode = KNOT_EOK;
+ args[i].thread_init_errcode = -1;
+ }
+ if (ret != KNOT_EOK) {
+ for (size_t i = 0; i < num_threads; i++) {
+ changeset_clear(&args[i].changeset);
+ zone_sign_ctx_free(args[i].sign_ctx);
+ }
+ return ret;
+ }
+
+ if (num_threads == 1) {
+ args[0].thread_init_errcode = 0;
+ tree_sign_thread(&args[0]);
+ } else {
+ // start working threads
+ for (size_t i = 0; i < num_threads; i++) {
+ args[i].thread_init_errcode =
+ pthread_create(&args[i].thread, NULL, tree_sign_thread, &args[i]);
+ }
+
+ // join those threads that have been really started
+ for (size_t i = 0; i < num_threads; i++) {
+ if (args[i].thread_init_errcode == 0) {
+ args[i].thread_init_errcode = pthread_join(args[i].thread, NULL);
+ }
+ }
+ }
+
+ // collect return code and results
+ for (size_t i = 0; i < num_threads; i++) {
+ if (ret == KNOT_EOK) {
+ if (args[i].thread_init_errcode != 0) {
+ ret = knot_map_errno_code(args[i].thread_init_errcode);
+ } else {
+ ret = args[i].errcode;
+ if (ret == KNOT_EOK && !dnssec_ctx->validation_mode) {
+ ret = zone_update_apply_changeset(update, &args[i].changeset); // _fix not needed
+ *expires_at = knot_time_min(*expires_at, args[i].expires_at);
+ }
+ }
+ }
+ assert(!dnssec_ctx->validation_mode || changeset_empty(&args[i].changeset));
+ changeset_clear(&args[i].changeset);
+ zone_sign_ctx_free(args[i].sign_ctx);
+ }
+
+ return ret;
+}
+
+/*- private API - signing of NSEC(3) in changeset ----------------------------*/
+
+/*!
+ * \brief Struct to carry data for changeset signing callback functions.
+ */
+typedef struct {
+ const zone_contents_t *zone;
+ changeset_iter_t itt;
+ zone_sign_ctx_t *sign_ctx;
+ changeset_t changeset;
+ knot_time_t expires_at;
+ size_t num_threads;
+ size_t thread_index;
+ size_t rrset_index;
+ int errcode;
+ int thread_init_errcode;
+ pthread_t thread;
+} changeset_signing_data_t;
+
+int rrset_add_zone_key(knot_rrset_t *rrset, zone_key_t *zone_key)
+{
+ if (rrset == NULL || zone_key == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ dnssec_binary_t dnskey_rdata = { 0 };
+ dnssec_key_get_rdata(zone_key->key, &dnskey_rdata);
+
+ return knot_rrset_add_rdata(rrset, dnskey_rdata.data, dnskey_rdata.size, NULL);
+}
+
+static int rrset_add_zone_ds(knot_rrset_t *rrset, zone_key_t *zone_key, dnssec_key_digest_t dt)
+{
+ assert(rrset);
+ assert(zone_key);
+
+ dnssec_binary_t cds_rdata = { 0 };
+ zone_key_calculate_ds(zone_key, dt, &cds_rdata);
+
+ return knot_rrset_add_rdata(rrset, cds_rdata.data, cds_rdata.size, NULL);
+}
+
+int knot_zone_sign(zone_update_t *update,
+ zone_keyset_t *zone_keys,
+ const kdnssec_ctx_t *dnssec_ctx,
+ knot_time_t *expire_at)
+{
+ if (!update || !dnssec_ctx || !expire_at ||
+ dnssec_ctx->policy->signing_threads < 1 ||
+ (zone_keys == NULL && !dnssec_ctx->validation_mode)) {
+ return KNOT_EINVAL;
+ }
+
+ int result;
+
+ knot_time_t normal_expire = 0;
+ result = zone_tree_sign(update->new_cont->nodes, dnssec_ctx->policy->signing_threads,
+ zone_keys, dnssec_ctx, update, &normal_expire);
+ if (result != KNOT_EOK) {
+ return result;
+ }
+
+ knot_time_t nsec3_expire = 0;
+ result = zone_tree_sign(update->new_cont->nsec3_nodes, dnssec_ctx->policy->signing_threads,
+ zone_keys, dnssec_ctx, update, &nsec3_expire);
+ if (result != KNOT_EOK) {
+ return result;
+ }
+
+ bool whole = !(update->flags & UPDATE_INCREMENTAL);
+ result = zone_tree_apply(whole ? update->new_cont->nodes : update->a_ctx->node_ptrs, set_signed, NULL);
+ if (result == KNOT_EOK) {
+ result = zone_tree_apply(whole ? update->new_cont->nsec3_nodes : update->a_ctx->nsec3_ptrs, set_signed, NULL);
+ }
+
+ *expire_at = knot_time_min(normal_expire, nsec3_expire);
+
+ return result;
+}
+
+keyptr_dynarray_t knot_zone_sign_get_cdnskeys(const kdnssec_ctx_t *ctx,
+ zone_keyset_t *zone_keys)
+{
+ keyptr_dynarray_t r = { 0 };
+ unsigned crp = ctx->policy->cds_cdnskey_publish;
+ unsigned cds_published = 0;
+ uint8_t ready_alg = 0;
+
+ if (crp == CDS_CDNSKEY_ROLLOVER || crp == CDS_CDNSKEY_ALWAYS ||
+ crp == CDS_CDNSKEY_DOUBLE_DS) {
+ // first, add strictly-ready keys
+ for (int i = 0; i < zone_keys->count; i++) {
+ zone_key_t *key = &zone_keys->keys[i];
+ if (key->is_ready) {
+ assert(key->is_ksk);
+ ready_alg = dnssec_key_get_algorithm(key->key);
+ keyptr_dynarray_add(&r, &key);
+ if (!key->is_pub_only) {
+ cds_published++;
+ }
+ }
+ }
+
+ // second, add active keys
+ if ((crp == CDS_CDNSKEY_ALWAYS && cds_published == 0) ||
+ (crp == CDS_CDNSKEY_DOUBLE_DS)) {
+ for (int i = 0; i < zone_keys->count; i++) {
+ zone_key_t *key = &zone_keys->keys[i];
+ if (key->is_ksk && key->is_active && !key->is_ready &&
+ (cds_published == 0 || ready_alg == dnssec_key_get_algorithm(key->key))) {
+ keyptr_dynarray_add(&r, &key);
+ }
+ }
+ }
+
+ if ((crp != CDS_CDNSKEY_DOUBLE_DS && cds_published > 1) ||
+ (cds_published > 2)) {
+ log_zone_warning(ctx->zone->dname, "DNSSEC, published CDS/CDNSKEY records for too many (%u) keys", cds_published);
+ }
+ }
+
+ return r;
+}
+
+int knot_zone_sign_add_dnskeys(zone_keyset_t *zone_keys, const kdnssec_ctx_t *dnssec_ctx,
+ key_records_t *add_r, key_records_t *rem_r, key_records_t *orig_r)
+{
+ if (add_r == NULL || (rem_r != NULL && orig_r == NULL)) {
+ return KNOT_EINVAL;
+ }
+
+ bool incremental = (dnssec_ctx->policy->incremental && rem_r != NULL);
+ dnssec_key_digest_t cds_dt = dnssec_ctx->policy->cds_dt;
+ int ret = KNOT_EOK;
+
+ for (int i = 0; i < zone_keys->count; i++) {
+ zone_key_t *key = &zone_keys->keys[i];
+ if (key->is_public) {
+ ret = rrset_add_zone_key(&add_r->dnskey, key);
+ } else if (incremental) {
+ ret = rrset_add_zone_key(&rem_r->dnskey, key);
+ }
+
+ // add all possible known CDNSKEYs and CDSs to removals. Sort it out later
+ if (incremental && ret == KNOT_EOK) {
+ ret = rrset_add_zone_key(&rem_r->cdnskey, key);
+ }
+ if (incremental && ret == KNOT_EOK) {
+ ret = rrset_add_zone_ds(&rem_r->cds, key, cds_dt);
+ }
+
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ keyptr_dynarray_t kcdnskeys = knot_zone_sign_get_cdnskeys(dnssec_ctx, zone_keys);
+ knot_dynarray_foreach(keyptr, zone_key_t *, ksk_for_cds, kcdnskeys) {
+ ret = rrset_add_zone_key(&add_r->cdnskey, *ksk_for_cds);
+ if (ret == KNOT_EOK) {
+ ret = rrset_add_zone_ds(&add_r->cds, *ksk_for_cds, cds_dt);
+ }
+ }
+
+ if (incremental && ret == KNOT_EOK) { // else rem_r is empty
+ ret = key_records_subtract(rem_r, add_r);
+ if (ret == KNOT_EOK) {
+ ret = key_records_intersect(rem_r, orig_r);
+ }
+ if (ret == KNOT_EOK) {
+ ret = key_records_subtract(add_r, orig_r);
+ }
+ }
+
+ if (dnssec_ctx->policy->cds_cdnskey_publish == CDS_CDNSKEY_EMPTY && ret == KNOT_EOK) {
+ const uint8_t cdnskey_empty[5] = { 0, 0, 3, 0, 0 };
+ const uint8_t cds_empty[5] = { 0, 0, 0, 0, 0 };
+ ret = knot_rrset_add_rdata(&add_r->cdnskey, cdnskey_empty, sizeof(cdnskey_empty), NULL);
+ if (ret == KNOT_EOK) {
+ ret = knot_rrset_add_rdata(&add_r->cds, cds_empty, sizeof(cds_empty), NULL);
+ }
+ }
+
+ keyptr_dynarray_free(&kcdnskeys);
+ return ret;
+}
+
+int knot_zone_sign_update_dnskeys(zone_update_t *update,
+ zone_keyset_t *zone_keys,
+ kdnssec_ctx_t *dnssec_ctx)
+{
+ if (update == NULL || zone_keys == NULL || dnssec_ctx == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ if (dnssec_ctx->policy->unsafe & UNSAFE_DNSKEY) {
+ return KNOT_EOK;
+ }
+
+ const zone_node_t *apex = update->new_cont->apex;
+ knot_rrset_t soa = node_rrset(apex, KNOT_RRTYPE_SOA);
+ if (knot_rrset_empty(&soa)) {
+ return KNOT_EINVAL;
+ }
+
+ key_records_t orig_r;
+ key_records_from_apex(apex, &orig_r);
+
+ changeset_t ch;
+ int ret = changeset_init(&ch, apex->owner);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (!dnssec_ctx->policy->incremental) {
+ // remove all. This will cancel out with additions later
+ ret = key_records_to_changeset(&orig_r, &ch, true, 0);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ key_records_t add_r, rem_r;
+ key_records_init(dnssec_ctx, &add_r);
+ key_records_init(dnssec_ctx, &rem_r);
+
+#define CHECK_RET if (ret != KNOT_EOK) goto cleanup
+
+ if (dnssec_ctx->policy->offline_ksk) {
+ key_records_t *r = &dnssec_ctx->offline_records;
+ log_zone_info(dnssec_ctx->zone->dname,
+ "DNSSEC, using offline records, DNSKEYs %hu, CDNSKEYs %hu, CDs %hu, RRSIGs %hu",
+ r->dnskey.rrs.count, r->cdnskey.rrs.count, r->cds.rrs.count, r->rrsig.rrs.count);
+ ret = key_records_to_changeset(r, &ch, false, CHANGESET_CHECK);
+ CHECK_RET;
+ } else {
+ ret = knot_zone_sign_add_dnskeys(zone_keys, dnssec_ctx, &add_r, &rem_r, &orig_r);
+ CHECK_RET;
+ ret = key_records_to_changeset(&rem_r, &ch, true, CHANGESET_CHECK);
+ CHECK_RET;
+ ret = key_records_to_changeset(&add_r, &ch, false, CHANGESET_CHECK);
+ CHECK_RET;
+ }
+
+ if (dnssec_ctx->policy->ds_push && node_rrtype_exists(ch.add->apex, KNOT_RRTYPE_CDS)) {
+ // there is indeed a change to CDS
+ update->zone->timers.next_ds_push = time(NULL) + dnssec_ctx->policy->propagation_delay;
+ zone_events_schedule_at(update->zone, ZONE_EVENT_DS_PUSH, update->zone->timers.next_ds_push);
+ }
+
+ ret = zone_update_apply_changeset(update, &ch);
+
+#undef CHECK_RET
+
+cleanup:
+ key_records_clear(&add_r);
+ key_records_clear(&rem_r);
+ changeset_clear(&ch);
+ return ret;
+}
+
+bool knot_zone_sign_use_key(const zone_key_t *key, const knot_rrset_t *covered)
+{
+ if (key == NULL || covered == NULL) {
+ return false;
+ }
+
+ bool active_ksk = ((key->is_active || key->is_ksk_active_plus) && key->is_ksk);
+ bool active_zsk = ((key->is_active || key->is_zsk_active_plus) && key->is_zsk);;
+
+ // this may be a problem with offline KSK
+ bool cds_sign_by_ksk = true;
+
+ assert(key->is_zsk || key->is_ksk);
+ bool is_apex = knot_dname_is_equal(covered->owner,
+ dnssec_key_get_dname(key->key));
+ if (!is_apex) {
+ return active_zsk;
+ }
+
+ switch (covered->type) {
+ case KNOT_RRTYPE_DNSKEY:
+ return active_ksk;
+ case KNOT_RRTYPE_CDS:
+ case KNOT_RRTYPE_CDNSKEY:
+ return (cds_sign_by_ksk ? active_ksk : active_zsk);
+ default:
+ return active_zsk;
+ }
+}
+
+static int sign_in_changeset(zone_node_t *node, uint16_t rrtype, knot_rrset_t *rrsigs,
+ zone_sign_ctx_t *sign_ctx, int ret_prev,
+ bool skip_crypto, zone_update_t *up)
+{
+ if (ret_prev != KNOT_EOK) {
+ return ret_prev;
+ }
+ knot_rrset_t rr = node_rrset(node, rrtype);
+ if (knot_rrset_empty(&rr)) {
+ return KNOT_EOK;
+ }
+ return add_missing_rrsigs(&rr, rrsigs, sign_ctx, skip_crypto, NULL, up, NULL);
+}
+
+int knot_zone_sign_nsecs_in_changeset(const zone_keyset_t *zone_keys,
+ const kdnssec_ctx_t *dnssec_ctx,
+ zone_update_t *update)
+{
+ if (zone_keys == NULL || dnssec_ctx == NULL || update == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ zone_sign_ctx_t *sign_ctx = zone_sign_ctx(zone_keys, dnssec_ctx);
+ if (sign_ctx == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ zone_tree_it_t it = { 0 };
+ int ret = zone_tree_it_double_begin(update->a_ctx->node_ptrs, update->a_ctx->nsec3_ptrs, &it);
+
+ while (!zone_tree_it_finished(&it) && ret == KNOT_EOK) {
+ zone_node_t *n = zone_tree_it_val(&it);
+ bool skip_crypto = (n->flags & NODE_FLAGS_RRSIGS_VALID) && !dnssec_ctx->keytag_conflict;
+
+ knot_rrset_t rrsigs = node_rrset(n, KNOT_RRTYPE_RRSIG);
+ ret = sign_in_changeset(n, KNOT_RRTYPE_NSEC, &rrsigs, sign_ctx, ret, skip_crypto, update);
+ ret = sign_in_changeset(n, KNOT_RRTYPE_NSEC3, &rrsigs, sign_ctx, ret, skip_crypto, update);
+ ret = sign_in_changeset(n, KNOT_RRTYPE_NSEC3PARAM, &rrsigs, sign_ctx, ret, skip_crypto, update);
+
+ if (ret == KNOT_EOK) {
+ n->flags |= NODE_FLAGS_RRSIGS_VALID; // non-NSEC RRSIGs had been validated in knot_dnssec_sign_update()
+ }
+
+ zone_tree_it_next(&it);
+ }
+ zone_tree_it_free(&it);
+ zone_sign_ctx_free(sign_ctx);
+
+ return ret;
+}
+
+bool knot_zone_sign_rr_should_be_signed(const zone_node_t *node,
+ const knot_rrset_t *rrset)
+{
+ if (node == NULL || knot_rrset_empty(rrset)) {
+ return false;
+ }
+
+ if (rrset->type == KNOT_RRTYPE_RRSIG || (node->flags & NODE_FLAGS_NONAUTH)) {
+ return false;
+ }
+
+ // At delegation points we only want to sign NSECs and DSs
+ if (node->flags & NODE_FLAGS_DELEG) {
+ if (!(rrset->type == KNOT_RRTYPE_NSEC ||
+ rrset->type == KNOT_RRTYPE_DS)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+int knot_zone_sign_update(zone_update_t *update,
+ zone_keyset_t *zone_keys,
+ const kdnssec_ctx_t *dnssec_ctx,
+ knot_time_t *expire_at)
+{
+ if (update == NULL || dnssec_ctx == NULL || expire_at == NULL ||
+ dnssec_ctx->policy->signing_threads < 1 ||
+ (zone_keys == NULL && !dnssec_ctx->validation_mode)) {
+ return KNOT_EINVAL;
+ }
+
+ int ret = KNOT_EOK;
+
+ /* Check if the UPDATE changed DNSKEYs or NSEC3PARAM.
+ * If so, we have to sign the whole zone. */
+ const bool full_sign = apex_dnssec_changed(update);
+ if (full_sign) {
+ ret = knot_zone_sign(update, zone_keys, dnssec_ctx, expire_at);
+ } else {
+ ret = zone_tree_sign(update->a_ctx->node_ptrs, dnssec_ctx->policy->signing_threads,
+ zone_keys, dnssec_ctx, update, expire_at);
+ if (ret == KNOT_EOK) {
+ ret = zone_tree_apply(update->a_ctx->node_ptrs, set_signed, NULL);
+ }
+ if (ret == KNOT_EOK && dnssec_ctx->validation_mode) {
+ ret = zone_tree_sign(update->a_ctx->nsec3_ptrs, dnssec_ctx->policy->signing_threads,
+ zone_keys, dnssec_ctx, update, expire_at);
+ }
+ if (ret == KNOT_EOK && dnssec_ctx->validation_mode) {
+ ret = zone_tree_apply(update->a_ctx->nsec3_ptrs, set_signed, NULL);
+ }
+ }
+
+ return ret;
+}
+
+int knot_zone_sign_apex_rr(zone_update_t *update, uint16_t rrtype,
+ const zone_keyset_t *zone_keys,
+ const kdnssec_ctx_t *dnssec_ctx)
+{
+ knot_rrset_t rr = node_rrset(update->new_cont->apex, rrtype);
+ knot_rrset_t rrsig = node_rrset(update->new_cont->apex, KNOT_RRTYPE_RRSIG);
+ changeset_t ch;
+ int ret = changeset_init(&ch, update->zone->name);
+ if (ret == KNOT_EOK) {
+ zone_sign_ctx_t *sign_ctx = zone_sign_ctx(zone_keys, dnssec_ctx);
+ if (sign_ctx == NULL) {
+ changeset_clear(&ch);
+ return KNOT_ENOMEM;
+ }
+ ret = force_resign_rrset(&rr, &rrsig, sign_ctx, &ch);
+ if (ret == KNOT_EOK) {
+ ret = zone_update_apply_changeset(update, &ch);
+ }
+ zone_sign_ctx_free(sign_ctx);
+ }
+ changeset_clear(&ch);
+ return ret;
+}
diff --git a/src/knot/dnssec/zone-sign.h b/src/knot/dnssec/zone-sign.h
new file mode 100644
index 0000000..ba6e2b2
--- /dev/null
+++ b/src/knot/dnssec/zone-sign.h
@@ -0,0 +1,162 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/updates/changesets.h"
+#include "knot/updates/zone-update.h"
+#include "knot/zone/contents.h"
+#include "knot/dnssec/context.h"
+#include "knot/dnssec/zone-keys.h"
+
+int rrset_add_zone_key(knot_rrset_t *rrset, zone_key_t *zone_key);
+
+bool rrsig_covers_type(const knot_rrset_t *rrsig, uint16_t type);
+
+/*!
+ * \brief Prepare DNSKEYs, CDNSKEYs and CDSs to be added to the zone into rrsets.
+ *
+ * \param zone_keys Zone keyset.
+ * \param dnssec_ctx KASP context.
+ * \param add_r RRSets to be added.
+ * \param rem_r RRSets to be removed (only for incremental policy).
+ * \param orig_r RRSets that was originally in zone (only for incremental policy).
+ *
+ * \return KNOT_E*
+ */
+int knot_zone_sign_add_dnskeys(zone_keyset_t *zone_keys, const kdnssec_ctx_t *dnssec_ctx,
+ key_records_t *add_r, key_records_t *rem_r, key_records_t *orig_r);
+
+/*!
+ * \brief Adds/removes DNSKEY (and CDNSKEY, CDS) records to zone according to zone keyset.
+ *
+ * \param update Structure holding zone contents and to be updated with changes.
+ * \param zone_keys Keyset with private keys.
+ * \param dnssec_ctx KASP context.
+ *
+ * \return KNOT_E*
+ */
+int knot_zone_sign_update_dnskeys(zone_update_t *update,
+ zone_keyset_t *zone_keys,
+ kdnssec_ctx_t *dnssec_ctx);
+
+/*!
+ * \brief Check if key can be used to sign given RR.
+ *
+ * \param key Zone key.
+ * \param covered RR to be checked.
+ *
+ * \return The RR should be signed.
+ */
+bool knot_zone_sign_use_key(const zone_key_t *key, const knot_rrset_t *covered);
+
+/*!
+ * \brief Return those keys for whose the CDNSKEY/CDS records shall be created.
+ *
+ * \param ctx DNSSEC context.
+ * \param zone_keys Zone keyset, including ZSKs.
+ *
+ * \return Dynarray containing pointers on some KSKs in keyset.
+ */
+keyptr_dynarray_t knot_zone_sign_get_cdnskeys(const kdnssec_ctx_t *ctx,
+ zone_keyset_t *zone_keys);
+
+/*!
+ * \brief Check that at least one correct signature exists to at least one DNSKEY and that none incorrect exists.
+ *
+ * \param covered RRSet bein validated.
+ * \param rrsigs RRSIG with signatures.
+ * \param sign_ctx Signing context (with keys == NULL)
+ * \param skip_crypto Crypto operations might be skipped as they had been successful earlier.
+ *
+ * \return KNOT_E*
+ */
+int knot_validate_rrsigs(const knot_rrset_t *covered,
+ const knot_rrset_t *rrsigs,
+ zone_sign_ctx_t *sign_ctx,
+ bool skip_crypto);
+
+/*!
+ * \brief Update zone signatures and store performed changes in update.
+ *
+ * Updates RRSIGs, NSEC(3)s, and DNSKEYs.
+ *
+ * \param update Zone Update containing the zone and to be updated with new DNSKEYs and RRSIGs.
+ * \param zone_keys Zone keys.
+ * \param dnssec_ctx DNSSEC context.
+ * \param expire_at Time, when the oldest signature in the zone expires.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_zone_sign(zone_update_t *update,
+ zone_keyset_t *zone_keys,
+ const kdnssec_ctx_t *dnssec_ctx,
+ knot_time_t *expire_at);
+
+/*!
+ * \brief Sign NSEC/NSEC3 nodes in changeset and update the changeset.
+ *
+ * \param zone_keys Zone keys.
+ * \param dnssec_ctx DNSSEC context.
+ * \param changeset Changeset to be updated.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_zone_sign_nsecs_in_changeset(const zone_keyset_t *zone_keys,
+ const kdnssec_ctx_t *dnssec_ctx,
+ zone_update_t *update);
+
+/*!
+ * \brief Checks whether RRSet in a node has to be signed. Will not return
+ * true for all types that should be signed, do not use this as an
+ * universal function, it is implementation specific.
+ *
+ * \param node Node containing the RRSet.
+ * \param rrset RRSet we are checking for.
+ *
+ * \retval true if should be signed.
+ */
+bool knot_zone_sign_rr_should_be_signed(const zone_node_t *node,
+ const knot_rrset_t *rrset);
+
+/*!
+ * \brief Sign updates of the zone, storing new RRSIGs in this update again.
+ *
+ * \param update Zone Update structure.
+ * \param zone_keys Zone keys.
+ * \param dnssec_ctx DNSSEC context.
+ * \param expire_at Time, when the oldest signature in the update expires.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+int knot_zone_sign_update(zone_update_t *update,
+ zone_keyset_t *zone_keys,
+ const kdnssec_ctx_t *dnssec_ctx,
+ knot_time_t *expire_at);
+
+/*!
+ * \brief Force re-sign of a RRSet in zone apex.
+ *
+ * \param update Zone update to be updated.
+ * \param rrtype Type of the apex RR.
+ * \param zone_keys Zone keyset.
+ * \param dnssec_ctx DNSSEC context.
+ *
+ * \return KNOT_E*
+ */
+int knot_zone_sign_apex_rr(zone_update_t *update, uint16_t rrtype,
+ const zone_keyset_t *zone_keys,
+ const kdnssec_ctx_t *dnssec_ctx);