diff options
Diffstat (limited to 'src/knot/dnssec')
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, ¶ms->keytag, ¶ms->algorithm, &flags, + ¶ms->timing.created, ¶ms->timing.pre_active, ¶ms->timing.publish, + ¶ms->timing.ready, ¶ms->timing.active, ¶ms->timing.retire_active, + ¶ms->timing.retire, ¶ms->timing.post_active, ¶ms->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, ¶ms->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, ¶ms->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, ¶ms->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, ¶ms, nsec_ttl); + if (ret != KNOT_EOK) { + return ret; + } + + if (ctx->policy->nsec3_enabled) { + ret = knot_nsec3_create_chain(update->new_cont, ¶ms, 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, ¶ms, 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, ¶ms, + 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, ¶ms) + : 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, ¶ms) : + 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); |