diff options
Diffstat (limited to 'src/knot/dnssec/zone-sign.c')
-rw-r--r-- | src/knot/dnssec/zone-sign.c | 1081 |
1 files changed, 1081 insertions, 0 deletions
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; +} |