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