From 64186ee3a763399ebb7434b3030d02e571ca2dd9 Mon Sep 17 00:00:00 2001 From: Libor Peltan <libor.peltan@nic.cz> Date: Sun, 23 Feb 2025 14:05:53 +0100 Subject: [PATCH 1/9] adjust: bugfix: prev pointers for non-binode contents --- src/knot/zone/adjust.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/knot/zone/adjust.c b/src/knot/zone/adjust.c index 0c2b82ff16..0d126827c6 100644 --- a/src/knot/zone/adjust.c +++ b/src/knot/zone/adjust.c @@ -372,7 +372,7 @@ static int adjust_single(zone_node_t *node, void *data) // set pointer to previous node if (args->adjust_prevs && args->previous_node != NULL && node->prev != args->previous_node && - node->prev != binode_counterpart(args->previous_node)) { + (!(args->previous_node->flags & NODE_FLAGS_BINODE) || node->prev != binode_counterpart(args->previous_node))) { zone_tree_insert(args->ctx.changed_nodes, &node); node->prev = args->previous_node; } -- GitLab From 6a3441527d437464f1060432b91f66d28f971cb1 Mon Sep 17 00:00:00 2001 From: Libor Peltan <libor.peltan@nic.cz> Date: Sun, 23 Feb 2025 19:23:40 +0100 Subject: [PATCH 2/9] libdnssec: allow loading nsec3 params from NSEC3 --- src/libdnssec/nsec/nsec.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libdnssec/nsec/nsec.c b/src/libdnssec/nsec/nsec.c index 2e71598fcf..5ee6deb289 100644 --- a/src/libdnssec/nsec/nsec.c +++ b/src/libdnssec/nsec/nsec.c @@ -61,7 +61,7 @@ int dnssec_nsec3_params_from_rdata(dnssec_nsec3_params_t *params, new_params.iterations = wire_ctx_read_u16(&wire); new_params.salt.size = wire_ctx_read_u8(&wire); - if (wire_ctx_available(&wire) != new_params.salt.size) { + if (wire_ctx_available(&wire) < new_params.salt.size) { return DNSSEC_MALFORMED_DATA; } @@ -71,7 +71,7 @@ int dnssec_nsec3_params_from_rdata(dnssec_nsec3_params_t *params, } binary_read(&wire, &new_params.salt); - assert(wire_ctx_offset(&wire) == rdata->size); + assert(wire_ctx_offset(&wire) <= rdata->size); *params = new_params; -- GitLab From de877e78c65e0df159f0b381b74f099a11cace7e Mon Sep 17 00:00:00 2001 From: Libor Peltan <libor.peltan@nic.cz> Date: Sun, 9 Mar 2025 21:00:50 +0100 Subject: [PATCH 3/9] dnssec validation: bugfix: dont remove any even redundant RRSIGs --- src/knot/dnssec/zone-sign.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/knot/dnssec/zone-sign.c b/src/knot/dnssec/zone-sign.c index 3cd2420373..1055282784 100644 --- a/src/knot/dnssec/zone-sign.c +++ b/src/knot/dnssec/zone-sign.c @@ -527,7 +527,7 @@ static int sign_node_rrsets(const zone_node_t *node, } } - if (result == KNOT_EOK) { + if (result == KNOT_EOK && !sign_ctx->dnssec_ctx->validation_mode) { result = remove_standalone_rrsigs(node, &rrsigs, changeset); } return result; -- GitLab From 5de7ec6d0f8dc95d65581549ceccc52ccabc9bfe Mon Sep 17 00:00:00 2001 From: Libor Peltan <libor.peltan@nic.cz> Date: Fri, 21 Feb 2025 20:51:31 +0100 Subject: [PATCH 4/9] kdig: implemented DNSSEC validation (+validate) --- Knot.files | 2 + configure.ac | 8 + doc/man_kdig.rst | 4 + src/knot/dnssec/nsec-chain.c | 2 +- src/knot/dnssec/nsec3-chain.c | 2 +- src/knot/dnssec/zone-events.c | 4 +- src/knot/updates/zone-update.c | 2 +- src/knot/zone/adjust.c | 12 +- src/knot/zone/adjust.h | 3 +- src/knot/zone/node.c | 13 + src/knot/zone/node.h | 11 + src/knot/zone/zonefile.c | 4 +- src/libknot/dname.c | 18 + src/libknot/dname.h | 19 + src/utils/Makefile.inc | 13 +- src/utils/kdig/dnssec_validation.c | 713 +++++++++++++++++++++++++++++ src/utils/kdig/dnssec_validation.h | 45 ++ src/utils/kdig/kdig_exec.c | 39 ++ src/utils/kdig/kdig_params.c | 32 ++ src/utils/kdig/kdig_params.h | 3 + tests/.gitignore | 1 + tests/Makefile.am | 14 +- tests/utils/test_kdig_validate.in | 107 +++++ 23 files changed, 1053 insertions(+), 18 deletions(-) create mode 100644 src/utils/kdig/dnssec_validation.c create mode 100644 src/utils/kdig/dnssec_validation.h create mode 100755 tests/utils/test_kdig_validate.in diff --git a/Knot.files b/Knot.files index 4bdfa08a9c..6f28838104 100644 --- a/Knot.files +++ b/Knot.files @@ -593,6 +593,8 @@ src/utils/common/token.h src/utils/common/util_conf.c src/utils/common/util_conf.h src/utils/kcatalogprint/main.c +src/utils/kdig/dnssec_validation.c +src/utils/kdig/dnssec_validation.h src/utils/kdig/kdig_exec.c src/utils/kdig/kdig_exec.h src/utils/kdig/kdig_main.c diff --git a/configure.ac b/configure.ac index 4f485f9a94..91b3e3357b 100644 --- a/configure.ac +++ b/configure.ac @@ -580,6 +580,14 @@ AC_ARG_WITH(libnghttp2, with_libnghttp2=yes ) +# DNSSEC validation support for kdig +AC_ARG_ENABLE([kdig_validation], + AS_HELP_STRING([--enable-kdig-validation=yes|no], [enable DNSSEC validation in kdig [default=no]]), + [enable_kdig_validation="$enableval"], [enable_kdig_validation=no]) +AS_IF([test "$enable_daemon" = "no"],[enable_kdig_validation=no]) +AS_IF([test "$enable_kdig_validation" = yes], [AC_DEFINE([HAVE_KDIG_VALIDATION], [1], [Define to 1 to enable DNSSEC validation in kdig.])]) +AM_CONDITIONAL([HAVE_KDIG_VALIDATION], [test "$enable_kdig_validation" = yes]) + AS_IF([test "$enable_utilities" = "yes"], [ AS_IF([test "$with_libidn" != "no"], [ PKG_CHECK_MODULES([libidn2], [libidn2 >= 2.0.0], [ diff --git a/doc/man_kdig.rst b/doc/man_kdig.rst index 5ca751d81c..c7d63ad8db 100644 --- a/doc/man_kdig.rst +++ b/doc/man_kdig.rst @@ -170,6 +170,10 @@ Options **+**\ [\ **no**\ ]\ **dnssec** Set the DO flag. +**+**\ [\ **no**\ ]\ **validate** + Also query for SOA and DNSKEY, validate DNSSEC in the answer. Implies DO flag. + Optional argument specifies verbosity (1-3, default 3). + **+**\ [\ **no**\ ]\ **all** Show all packet sections. diff --git a/src/knot/dnssec/nsec-chain.c b/src/knot/dnssec/nsec-chain.c index 123020a895..86ba4b4322 100644 --- a/src/knot/dnssec/nsec-chain.c +++ b/src/knot/dnssec/nsec-chain.c @@ -745,7 +745,7 @@ int knot_nsec_fix_chain(zone_update_t *update, uint32_t ttl) return ret; } - ret = zone_adjust_contents(update->new_cont, adjust_cb_void, NULL, false, true, 1, update->a_ctx->node_ptrs); + ret = zone_adjust_contents(update->new_cont, adjust_cb_void, NULL, false, true, true, 1, update->a_ctx->node_ptrs); if (ret != KNOT_EOK) { return ret; } diff --git a/src/knot/dnssec/nsec3-chain.c b/src/knot/dnssec/nsec3-chain.c index 97010be500..ed8174644c 100644 --- a/src/knot/dnssec/nsec3-chain.c +++ b/src/knot/dnssec/nsec3-chain.c @@ -676,7 +676,7 @@ int knot_nsec3_fix_chain(zone_update_t *update, return ret; } - ret = zone_adjust_contents(update->new_cont, NULL, adjust_cb_void, false, true, 1, update->a_ctx->nsec3_ptrs); + ret = zone_adjust_contents(update->new_cont, NULL, adjust_cb_void, false, true, true, 1, update->a_ctx->nsec3_ptrs); if (ret != KNOT_EOK) { return ret; } diff --git a/src/knot/dnssec/zone-events.c b/src/knot/dnssec/zone-events.c index 87acb32fdb..1b6a8f8a96 100644 --- a/src/knot/dnssec/zone-events.c +++ b/src/knot/dnssec/zone-events.c @@ -243,7 +243,7 @@ int knot_dnssec_zone_sign(zone_update_t *update, } result = zone_adjust_contents(update->new_cont, adjust_cb_flags, NULL, - false, false, 1, update->a_ctx->node_ptrs); + false, false, true, 1, update->a_ctx->node_ptrs); if (result != KNOT_EOK) { return result; } @@ -380,7 +380,7 @@ int knot_dnssec_sign_update(zone_update_t *update, conf_t *conf) } result = zone_adjust_contents(update->new_cont, adjust_cb_flags, NULL, - false, false, 1, update->a_ctx->node_ptrs); + false, false, true, 1, update->a_ctx->node_ptrs); if (result != KNOT_EOK) { goto done; } diff --git a/src/knot/updates/zone-update.c b/src/knot/updates/zone-update.c index caae3ccc73..fa5461d796 100644 --- a/src/knot/updates/zone-update.c +++ b/src/knot/updates/zone-update.c @@ -854,7 +854,7 @@ int zone_update_semcheck(conf_t *conf, zone_update_t *update) // adjust_cb_nsec3_pointer not needed as we don't check DNSSEC here int ret = zone_adjust_contents(update->new_cont, adjust_cb_flags, NULL, - false, false, 1, node_ptrs); + false, false, true, 1, node_ptrs); if (ret != KNOT_EOK) { return ret; } diff --git a/src/knot/zone/adjust.c b/src/knot/zone/adjust.c index 0d126827c6..4bf8c0561c 100644 --- a/src/knot/zone/adjust.c +++ b/src/knot/zone/adjust.c @@ -479,10 +479,10 @@ static int zone_adjust_tree_parallel(zone_tree_t *tree, adjust_ctx_t *ctx, } int zone_adjust_contents(zone_contents_t *zone, adjust_cb_t nodes_cb, adjust_cb_t nsec3_cb, - bool measure_zone, bool adjust_prevs, unsigned threads, + bool measure_zone, bool adjust_prevs, bool load_nsec3p, unsigned threads, zone_tree_t *add_changed) { - int ret = zone_contents_load_nsec3param(zone); + int ret = load_nsec3p ? zone_contents_load_nsec3param(zone) : KNOT_EOK; if (ret != KNOT_EOK) { log_zone_error(zone->apex->owner, "failed to load NSEC3 parameters (%s)", @@ -550,10 +550,10 @@ int zone_adjust_update(zone_update_t *update, adjust_cb_t nodes_cb, adjust_cb_t int zone_adjust_full(zone_contents_t *zone, unsigned threads) { int ret = zone_adjust_contents(zone, adjust_cb_flags, adjust_cb_nsec3_flags, - true, true, 1, NULL); + true, true, true, 1, NULL); if (ret == KNOT_EOK) { ret = zone_adjust_contents(zone, adjust_cb_nsec3_and_additionals, NULL, - false, false, threads, NULL); + false, false, true, threads, NULL); } if (ret == KNOT_EOK) { additionals_tree_free(zone->adds_tree); @@ -590,11 +590,11 @@ int zone_adjust_incremental_update(zone_update_t *update, unsigned threads) }; ret = zone_adjust_contents(update->new_cont, adjust_cb_flags, adjust_cb_nsec3_flags, - false, true, 1, update->a_ctx->adjust_ptrs); + false, true, true, 1, update->a_ctx->adjust_ptrs); if (ret == KNOT_EOK) { if (nsec3change) { ret = zone_adjust_contents(update->new_cont, adjust_cb_nsec3_and_wildcard, NULL, - false, false, threads, update->a_ctx->adjust_ptrs); + false, false, true, threads, update->a_ctx->adjust_ptrs); if (ret == KNOT_EOK) { // just measure zone size ret = zone_adjust_update(update, adjust_cb_void, adjust_cb_void, true); diff --git a/src/knot/zone/adjust.h b/src/knot/zone/adjust.h index 5828e5a48e..440771bf3b 100644 --- a/src/knot/zone/adjust.h +++ b/src/knot/zone/adjust.h @@ -74,13 +74,14 @@ int adjust_cb_void(zone_node_t *node, adjust_ctx_t *ctx); * \param nsec3_cb Callback for NSEC3 nodes. * \param measure_zone While adjusting, count the size and max TTL of the zone. * \param adjust_prevs Also (re-)generate node->prev pointers. + * \param load_nsec3p Load NSEC3PARAM from zone. * \param threads Operate in parallel using specified threads. * \param add_changed Special tree to add any changed node (by adjusting) into. * * \return KNOT_E* */ int zone_adjust_contents(zone_contents_t *zone, adjust_cb_t nodes_cb, adjust_cb_t nsec3_cb, - bool measure_zone, bool adjust_prevs, unsigned threads, + bool measure_zone, bool adjust_prevs, bool load_nsec3p, unsigned threads, zone_tree_t *add_changed); /*! diff --git a/src/knot/zone/node.c b/src/knot/zone/node.c index 0a29cc148d..4e512ae773 100644 --- a/src/knot/zone/node.c +++ b/src/knot/zone/node.c @@ -445,6 +445,19 @@ bool node_rrtype_is_signed(const zone_node_t *node, uint16_t type) return false; } +int node_set_ttl(zone_node_t *node, uint16_t type, uint32_t ttl) +{ + int res = 0, remain = node->rrset_count; + while (--remain >= 0) { + if (node->rrs[remain].type == type) { + node->rrs[remain].ttl = ttl; + res++; + } + } + assert(res < 2); + return res; +} + bool node_bitmap_equal(const zone_node_t *a, const zone_node_t *b) { if (a == NULL || b == NULL || a->rrset_count != b->rrset_count) { diff --git a/src/knot/zone/node.h b/src/knot/zone/node.h index cab0604f96..5a4c68127a 100644 --- a/src/knot/zone/node.h +++ b/src/knot/zone/node.h @@ -383,6 +383,17 @@ static inline knot_rrset_t node_rrset(const zone_node_t *node, uint16_t type) return rrset; } +/*! + * \brief Set TTL of specific RRset. + * + * \param node Zone node. + * \param type RRtype to search the RRset. + * \param ttl TTL to be set. + * + * \return 0 if nothing set, 1 if set, >1 should not happen (two rrsets of same type in node) + */ +int node_set_ttl(zone_node_t *node, uint16_t type, uint32_t ttl); + /*! * \brief Returns RRSet structure initialized with data from node at position * equal to \a pos. diff --git a/src/knot/zone/zonefile.c b/src/knot/zone/zonefile.c index cfd769a29f..0565c617e2 100644 --- a/src/knot/zone/zonefile.c +++ b/src/knot/zone/zonefile.c @@ -223,7 +223,7 @@ zone_contents_t *zonefile_load(zloader_t *loader) } ret = zone_adjust_contents(zc->z, adjust_cb_flags_and_nsec3, adjust_cb_nsec3_flags, - true, true, 1, NULL); + true, true, true, 1, NULL); if (ret != KNOT_EOK) { ERROR(zname, "failed to finalize zone contents (%s)", knot_strerror(ret)); @@ -242,7 +242,7 @@ zone_contents_t *zonefile_load(zloader_t *loader) /* The contents will now change possibly messing up NSEC3 tree, it will be adjusted again at zone_update_commit. */ ret = zone_adjust_contents(zc->z, unadjust_cb_point_to_nsec3, NULL, - false, false, 1, NULL); + false, false, true, 1, NULL); if (ret != KNOT_EOK) { ERROR(zname, "failed to finalize zone contents (%s)", knot_strerror(ret)); diff --git a/src/libknot/dname.c b/src/libknot/dname.c index 3cfc5a5570..da7456d86b 100644 --- a/src/libknot/dname.c +++ b/src/libknot/dname.c @@ -471,6 +471,24 @@ dname_from_str_failed: return NULL; } +_public_ +knot_dname_t *knot_dname_wildcard(const knot_dname_t *from, knot_dname_t *dest, size_t dest_size) +{ + size_t from_size = knot_dname_size(from); + if ((from_size + 2 > dest_size && dest != NULL) || from_size + 2 > KNOT_DNAME_MAXLEN) { + return NULL; + } + if (dest == NULL) { + dest = malloc(from_size + 2); + if (dest == NULL) { + return NULL; + } + } + memcpy(dest, "\x01*", 2); + memcpy(dest + 2, from, from_size); + return dest; +} + _public_ void knot_dname_to_lower(knot_dname_t *name) { diff --git a/src/libknot/dname.h b/src/libknot/dname.h index 47e514c40a..44dcada3b3 100644 --- a/src/libknot/dname.h +++ b/src/libknot/dname.h @@ -160,6 +160,17 @@ static inline knot_dname_t *knot_dname_from_str_alloc(const char *name) return knot_dname_from_str(NULL, name, 0); } +/*! + * \brief Append an asterix label on the beginning. + * + * \param from Original name to append to. + * \param dest Optional: destination buffer. + * \param dest_size Destination buffer length. + * + * \return Wildcard name, or NULL if error. + */ +knot_dname_t *knot_dname_wildcard(const knot_dname_t *from, knot_dname_t *dest, size_t dest_size); + /*! * \brief Convert domain name to lowercase. * @@ -312,6 +323,14 @@ bool knot_dname_with_null(const knot_dname_t *name); _pure_ size_t knot_dname_prefixlen(const uint8_t *name, unsigned nlabels); +/*! + * \brief Shift by given number of labels, but no more than to the final one. + */ +inline static const knot_dname_t *knot_dname_next_labels(const knot_dname_t *name, unsigned nlabels) +{ + return name + knot_dname_prefixlen(name, nlabels); +} + /*! * \brief Return number of labels in the domain name. * diff --git a/src/utils/Makefile.inc b/src/utils/Makefile.inc index 1f11282bef..c40a1bd8be 100644 --- a/src/utils/Makefile.inc +++ b/src/utils/Makefile.inc @@ -59,6 +59,12 @@ kdig_SOURCES = \ utils/kdig/kdig_params.c \ utils/kdig/kdig_params.h +if HAVE_KDIG_VALIDATION +kdig_SOURCES += \ + utils/kdig/dnssec_validation.c \ + utils/kdig/dnssec_validation.h +endif HAVE_KDIG_VALIDATION + khost_SOURCES = \ utils/kdig/kdig_exec.c \ utils/kdig/kdig_exec.h \ @@ -80,9 +86,10 @@ knsupdate_SOURCES = \ utils/knsupdate/knsupdate_params.c \ utils/knsupdate/knsupdate_params.h -kdig_CPPFLAGS = $(libknotus_la_CPPFLAGS) -kdig_LDADD = $(libknotus_LIBS) -khost_CPPFLAGS = $(libknotus_la_CPPFLAGS) +kdig_CPPFLAGS = $(libknotus_la_CPPFLAGS) +kdig_LDADD = $(libknotd_LIBS) $(libknotus_LIBS) +kdig_LDFLAGS = $(AM_LDFLAGS) -rdynamic +khost_CPPFLAGS = $(libknotus_la_CPPFLAGS) -DNO_DNSSEC_VALIDATION khost_LDADD = $(libknotus_LIBS) knsec3hash_CPPFLAGS = $(libknotus_la_CPPFLAGS) knsec3hash_LDADD = libknot.la libdnssec.la $(libcontrib_LIBS) diff --git a/src/utils/kdig/dnssec_validation.c b/src/utils/kdig/dnssec_validation.c new file mode 100644 index 0000000000..0ec532f550 --- /dev/null +++ b/src/utils/kdig/dnssec_validation.c @@ -0,0 +1,713 @@ +/* Copyright (C) 2025 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 "utils/kdig/dnssec_validation.h" + +#include "knot/dnssec/zone-nsec.h" +#include "knot/dnssec/zone-sign.h" +#include "knot/zone/adjust.h" + +#include <string.h> + +#define CNAME_LIMIT 3 + +typedef struct kdig_dnssec_ctx { + zone_contents_t *conts; + knot_dname_t *orig_qname; + uint16_t orig_qtype; + knot_rcode_t orig_rcode; + unsigned cname_visit; +} kdig_dnssec_ctx_t; + +typedef struct { + zone_contents_t *conts; + kdig_validation_log_level_t level; +} tmp_ctx_t; + +static void kdv_log(kdig_validation_log_level_t log_level, kdig_validation_log_level_t set_level, + const knot_dname_t *at, const char *msg, ...) +{ + if (set_level >= log_level) { + fprintf(stdout, ";; DNSSEC VALIDATION: "); + va_list args; + va_start(args, msg); + vfprintf(stdout, msg, args); + va_end(args); + if (at != NULL) { + char at_txt[KNOT_DNAME_TXT_MAXLEN] = { 0 }; + knot_dname_to_str(at_txt, at, sizeof(at_txt)); + fprintf(stdout, " at %s\n", at_txt); + } else { + fprintf(stdout, "\n"); + } + } +} + +#define LOG_OUTCOME(level, at, msg, ...) kdv_log(KDIG_VALIDATION_LOG_OUTCOME, level, at, msg, ##__VA_ARGS__) +#define LOG_ERROR(level, at, msg, ...) kdv_log(KDIG_VALIDATION_LOG_ERRORS, level, at, msg, ##__VA_ARGS__) +#define LOG_INF(level, at, msg, ...) kdv_log(KDIG_VALIDATION_LOG_INFOS, level, at, msg, ##__VA_ARGS__) + +static bool dname_between(const knot_dname_t *first, const knot_dname_t *between, const knot_dname_t *second) +{ + if (knot_dname_cmp(first, second) < 0) { + return knot_dname_cmp(first, between) < 0 && knot_dname_cmp(between, second) < 0; + } else { + return knot_dname_cmp(first, between) < 0 || knot_dname_cmp(between, second) < 0; + } +} + +static bool nsec_covers_name(const knot_dname_t *nsec_owner, const knot_rdata_t *nsec_rdata, + const knot_dname_t *name) +{ + const knot_dname_t *nsec_next = knot_nsec_next(nsec_rdata); + return dname_between(nsec_owner, name, nsec_next); +} + +static bool nsec3_covers_name(const knot_dname_t *nsec3_owner, const knot_rdata_t *nsec3_rdata, + const knot_dname_t *name, const knot_dname_t *apex) +{ + const uint8_t *nsec3_hash = knot_nsec3_next(nsec3_rdata); + uint16_t n3h_len = knot_nsec3_next_len(nsec3_rdata); + uint8_t nsec3_next[KNOT_DNAME_MAXLEN] = { 0 }; + int ret = knot_nsec3_hash_to_dname(nsec3_next, sizeof(nsec3_next), nsec3_hash, n3h_len, apex); + return ret == KNOT_EOK /* best effort */ && dname_between(nsec3_owner,name, nsec3_next); +} + +static int check_nsec3(zone_node_t *node, void *data) +{ + tmp_ctx_t *ctx = data; + dnssec_nsec3_params_t *params = &ctx->conts->nsec3_params, found = { 0 }; + knot_rdataset_t *nsec3 = node_rdataset(node, KNOT_RRTYPE_NSEC3); + dnssec_binary_t rd = { .data = nsec3->rdata->data, .size = nsec3->rdata->len }; + int ret; + if ((ret = dnssec_nsec3_params_from_rdata(&found, &rd)) != KNOT_EOK || + !dnssec_nsec3_params_match(&found, params)) { + LOG_ERROR(ctx->level, node->owner, "invalid or unmatching NSEC3"); + return 1; + } + free(found.salt.data); + + zone_node_t *prev = node_prev(node); + nsec3 = node_rdataset(prev, KNOT_RRTYPE_NSEC3); + if (prev != node && nsec3_covers_name(prev->owner, nsec3->rdata, node->owner, ctx->conts->apex->owner)) { + LOG_ERROR(ctx->level, node->owner, "overlapping NSEC3 ranges"); + return 1; + } + return KNOT_EOK; +} + +static bool parents_have_rrtype(zone_node_t *n, uint16_t type) +{ + while ((n = node_parent(n)) != NULL) { + if (node_rrtype_exists(n, type)) { + return true; + } + } + return false; +} + +static int move_rrset(zone_contents_t *c, zone_node_t *n, uint16_t type, const knot_dname_t *target) +{ + zone_node_t *unused = NULL; + knot_rrset_t rr = node_rrset(n, type); + if (knot_rrset_empty(&rr)) { + return KNOT_EOK; + } + const knot_rrset_t rr2 = { .owner = (knot_dname_t *)target, .type = rr.type, .rclass = rr.rclass, .ttl = rr.ttl, .rrs = rr.rrs }; + int ret = zone_contents_add_rr(c, &rr2, &unused); + if (ret == KNOT_EOK) { + ret = zone_contents_remove_rr(c, &rr, &n); + } + return ret; +} + +static int rrsig_types_lbcnt(const knot_rdataset_t *rrsig, uint16_t *types /* must be pre-allocated to rrsig->count+1 */, uint16_t *lbcnt) +{ + knot_rdata_t *rd = rrsig->rdata; + for (int i = 0; i < rrsig->count; i++) { + if (*lbcnt == 0) { + *lbcnt = knot_rrsig_labels(rd); + } else if (*lbcnt != knot_rrsig_labels(rd)) { + return KNOT_ESEMCHECK; + } + types[i] = knot_rrsig_type_covered(rd); + rd = knot_rdataset_next(rd); + } + return KNOT_EOK; +} + +static int restore_orig_ttls(zone_node_t *node, void *unused) +{ + knot_rdataset_t *rrsig = node_rdataset(node, KNOT_RRTYPE_RRSIG); + if (rrsig != NULL) { + knot_rdata_t *rd = rrsig->rdata; + for (int i = 0; i < rrsig->count; i++) { + (void)node_set_ttl(node, knot_rrsig_type_covered(rd), knot_rrsig_original_ttl(rd)); + rd = knot_rdataset_next(rd); + } + } + return KNOT_EOK; +} + +static bool has_nsec3(const zone_contents_t *conts) +{ + return conts->nsec3_params.algorithm > 0; +} + +static bool bitmap_covers(const uint8_t *bitmap, uint16_t bm_len, + uint16_t rrtype, const zone_node_t *node) +{ + if (node != NULL) { + for (int i = 0; i < node->rrset_count; i++) { + uint16_t rrt = node->rrs[i].type; + if (!dnssec_nsec_bitmap_contains(bitmap, bm_len, rrt)) { + return true; + } + } + return false; + } else if (rrtype == 0) { + return true; + } else { + return !dnssec_nsec_bitmap_contains(bitmap, bm_len, rrtype); + } +} + +static bool has_nodata(zone_contents_t *conts, const knot_dname_t *name, uint16_t type, + const zone_node_t *from_node, const knot_dname_t **where) +{ + if (!has_nsec3(conts)) { + const zone_node_t *node = zone_contents_find_node(conts, name); + knot_rrset_t nsec = node_rrset(node, KNOT_RRTYPE_NSEC); + if (where != NULL) { + *where = nsec.owner; + } + return !knot_rrset_empty(&nsec) && bitmap_covers(knot_nsec_bitmap(nsec.rrs.rdata), knot_nsec_bitmap_len(nsec.rrs.rdata), type, from_node); + } + + const zone_node_t *nsec3_node = NULL, *nsec3_prev = NULL; + int ret = zone_contents_find_nsec3_for_name(conts, name, &nsec3_node, &nsec3_prev); + if (ret != ZONE_NAME_FOUND) { + return false; // best effort + } + knot_rrset_t nsec3 = node_rrset(nsec3_node, KNOT_RRTYPE_NSEC3); + if (where != NULL) { + *where = nsec3.owner; + } + return !knot_rrset_empty(&nsec3) && bitmap_covers(knot_nsec3_bitmap(nsec3.rrs.rdata), knot_nsec3_bitmap_len(nsec3.rrs.rdata), type, from_node); +} + +static bool has_nxdomain(zone_contents_t *conts, const knot_dname_t *name, bool opt_out, + kdig_validation_log_level_t level, bool *has_opt_out, + const knot_dname_t **where, const knot_dname_t **encloser) +{ + if (!has_nsec3(conts)) { + const zone_node_t *match = NULL, *closest = NULL, *prev = NULL; + int ret = zone_contents_find_dname(conts, name, &match, &closest, &prev, knot_dname_with_null(name)); + if (ret < 0 || match == prev) { + return false; + } + while (prev->rrset_count == 0) { + prev = node_prev(prev); + } + knot_rrset_t nsec = node_rrset(prev, KNOT_RRTYPE_NSEC); + *where = nsec.owner; + *encloser = closest->owner; + if (!knot_rrset_empty(&nsec) && knot_dname_in_bailiwick(knot_nsec_next(nsec.rrs.rdata), name) >= 0) { + *encloser = name; // empty-non-terminal detected + } + return !opt_out && !knot_rrset_empty(&nsec) && nsec_covers_name(prev->owner, nsec.rrs.rdata, name); + } + + // scan for closest encloser represented by some NSEC3, because the closest encloser node might not be here + size_t apex_lbs = knot_dname_labels(conts->apex->owner, NULL), name_lbs = knot_dname_labels(name, NULL); + const knot_dname_t *enc_where = NULL; + *encloser = knot_dname_next_label(name); + for ( ; name_lbs > apex_lbs; name_lbs--) { + if (has_nodata(conts, *encloser, 0, NULL, &enc_where) || + zone_contents_find_node(conts, *encloser) != NULL) { // tricky exception: in some cases the closest encloser is proven by existence of stuff, e.g. RFC 5155 § 7.2.6 + break; + } + name = *encloser; + *encloser = knot_dname_next_label(name); + } + if (name_lbs <= apex_lbs) { + LOG_ERROR(level, name, "NSEC3 encloser proof missing"); + return false; + } else { + char enc_name[KNOT_DNAME_TXT_MAXLEN] = { 0 }; + (void)knot_dname_to_str(enc_name, *encloser, sizeof(enc_name)); + LOG_INF(level, enc_where != NULL ? enc_where : *encloser, "NSEC3 encloser %s found", enc_name); + } + + const zone_node_t *nsec3_node = NULL, *nsec3_prev = NULL; + knot_dname_storage_t nsec3_name; + int ret = knot_create_nsec3_owner(nsec3_name, sizeof(nsec3_name), name, + conts->apex->owner, &conts->nsec3_params); + if (ret == KNOT_EOK) { + ret = zone_contents_find_nsec3(conts, nsec3_name, &nsec3_node, &nsec3_prev); + } + if (ret != ZONE_NAME_NOT_FOUND) { + return false; // best effort + } + knot_rrset_t nsec3 = node_rrset(nsec3_prev, KNOT_RRTYPE_NSEC3); + *where = nsec3.owner; + if (has_opt_out != NULL) { + *has_opt_out = (knot_nsec3_flags(nsec3.rrs.rdata) & KNOT_NSEC3_FLAG_OPT_OUT); + } + return !knot_rrset_empty(&nsec3) && nsec3_covers_name(nsec3_prev->owner, nsec3.rrs.rdata, nsec3_name, conts->apex->owner) && + (!opt_out || (knot_nsec3_flags(nsec3.rrs.rdata) & KNOT_NSEC3_FLAG_OPT_OUT)); +} + +static int check_existing_with_nsecs(zone_node_t *node, void *data) +{ + tmp_ctx_t *ctx = data; + const knot_dname_t *where = NULL, *encloser = NULL; + bool has_opt_out = false; + if (node->flags & NODE_FLAGS_DELEG) { + bool has_nxd = has_nxdomain(ctx->conts, node->owner, false, KDIG_VALIDATION_LOG_NONE, &has_opt_out, &where, &encloser); + if (node_rrtype_exists(node, KNOT_RRTYPE_DS)) { + if (has_nodata(ctx->conts, node->owner, KNOT_RRTYPE_DS, NULL, NULL)) { + LOG_ERROR(ctx->level, node->owner, "NSEC(3) wrongly proves insecure delegation"); + return 1; + } else if (has_nxd) { + if (has_opt_out) { + LOG_ERROR(ctx->level, node->owner, "NSEC3 opt-out wrongly applied to secure delegation"); + } else { + LOG_ERROR(ctx->level, node->owner, "NSEC(3) wrongly proves NXDOMAIN for secure delegation"); + } + return 1; + } + } else if (has_nxd && !has_opt_out) { + if (has_nsec3(ctx->conts)) { + LOG_ERROR(ctx->level, node->owner, "NSEC3 opt-out flag missing, proving NXDOMAIN fro insecure delegation"); + } else { + LOG_ERROR(ctx->level, node->owner, "NSEC wrongly proves NXDOMAIN for insecure delegation"); + } + return 1; + } + } else if (!(node->flags & NODE_FLAGS_NONAUTH)) { + if (has_nodata(ctx->conts, node->owner, 0, node, NULL)) { + LOG_ERROR(ctx->level, node->owner, "NSEC(3) wrongly proves NODATA"); + return 1; + } else if (has_nxdomain(ctx->conts, node->owner, false, KDIG_VALIDATION_LOG_NONE, &has_opt_out, &where, &encloser) && + (!has_opt_out || (node->flags & NODE_FLAGS_SUBTREE_AUTH))) { + LOG_ERROR(ctx->level, node->owner, "NSEC(3) wrongly proves NXDOMAIN"); + return 1; + } + } + return KNOT_EOK; +} + +static const knot_rrset_t *find_first(knot_pkt_t *pkt, uint16_t rrtype, knot_section_t limit) +{ + for (int i = 0; i <= limit; i++) { + for (int j = 0; j < pkt->sections[i].count; j++) { + const knot_rrset_t *rr = knot_pkt_rr(&pkt->sections[i], j); + if (rr->type == rrtype) { + return rr; + } + } + } + return NULL; +} + +int remove_cnames(zone_node_t *node, void *data) +{ + knot_rrset_t cname = node_rrset(node, KNOT_RRTYPE_CNAME); + if (!knot_rrset_empty(&cname)) { + zone_node_t *unused = NULL; + return zone_contents_remove_rr(data, &cname, &unused); + } + return KNOT_EOK; +} + +static int rrsets_pkt2conts(knot_pkt_t *pkt, zone_contents_t *conts, + knot_section_t limit, uint16_t type_only, + kdig_validation_log_level_t level) +{ + int ret = KNOT_EOK; + for (int i = 0; i <= limit && ret == KNOT_EOK; i++) { + for (int j = 0; j < pkt->sections[i].count && ret == KNOT_EOK; j++) { + const knot_rrset_t *rr = knot_pkt_rr(&pkt->sections[i], j); + if (rr->type == KNOT_RRTYPE_RRSIG) { + assert(rr->rrs.count == 1); + if (type_only && knot_rrsig_type_covered(rr->rrs.rdata) != type_only) { + continue; + } + } else if ((type_only && rr->type != type_only) || knot_rrtype_is_metatype(rr->type)) { + continue; + } + + uint16_t rr_pos = knot_pkt_rr_offset(&pkt->sections[i], j); + knot_dname_storage_t owner; + knot_dname_unpack(owner, pkt->wire + rr_pos, sizeof(owner), pkt->wire); + + knot_rrset_t rrcpy = *rr; + rrcpy.owner = (knot_dname_t *)&owner; + ret = knot_rrset_rr_to_canonical(&rrcpy); + if (ret != KNOT_EOK) { + break; + } + + zone_node_t *inserted = NULL; + ret = zone_contents_add_rr(conts, &rrcpy, &inserted); + if (ret == KNOT_ETTL) { + char rrtype[16] = { 0 }; + knot_rrtype_to_string(rr->type, rrtype, sizeof(rrtype)); + LOG_INF(level, rr->owner, "WARNING: mismatched TTLs for type %s", rrtype); + ret = KNOT_EOK; + } + } + } + return ret; +} + +static int solve_missing_apex(knot_pkt_t *pkt, uint16_t rrtype, zone_contents_t *conts, kdig_validation_log_level_t level) +{ + if (node_rrtype_exists(conts->apex, rrtype)) { + return KNOT_EOK; + } + if (knot_pkt_qtype(pkt) != rrtype || !knot_dname_is_equal(knot_pkt_qname(pkt), conts->apex->owner)) { + return KNOT_EAGAIN; + } + int ret = rrsets_pkt2conts(pkt, conts, KNOT_ANSWER, rrtype, level); + if (ret == KNOT_EOK && !node_rrtype_exists(conts->apex, rrtype)) { + ret = KNOT_ENOENT; + } + return ret; +} + +static int check_cname(kdig_dnssec_ctx_t *ctx, const knot_dname_t *cname, + uint16_t type, kdig_validation_log_level_t level, + knot_rcode_t *expected_rcode); + +static int check_name(kdig_dnssec_ctx_t *ctx, const knot_dname_t *name, + uint16_t type, kdig_validation_log_level_t level, + knot_rcode_t *expected_rcode) +{ + const knot_dname_t *where = NULL, *encloser = NULL; + const zone_node_t *match = NULL, *closest = NULL, *prev = NULL; + bool has_opt_out = false, wc_match = knot_dname_is_wildcard(name) && !knot_dname_is_wildcard(ctx->orig_qname); + int ret = zone_contents_find_dname(ctx->conts, name, &match, &closest, &prev, knot_dname_with_null(name)); + if (ret < 0) { + return ret; + } + if (expected_rcode != NULL) { + *expected_rcode = KNOT_RCODE_NOERROR; + } + while ((closest->flags & NODE_FLAGS_NONAUTH)) { + closest = node_parent(closest); + } + if ((closest->flags & NODE_FLAGS_DELEG)) { + if (node_rrtype_exists(closest, KNOT_RRTYPE_DS)) { + LOG_INF(level, closest->owner, "secure delegation, DS found"); + return KNOT_EOK; + } else if (has_nodata(ctx->conts, closest->owner, KNOT_RRTYPE_DS, NULL, &where)) { + LOG_INF(level, where, "insecure delegation, DS NODATA proof found"); + } else if (has_nxdomain(ctx->conts, closest->owner, true, level, &has_opt_out, &where, &encloser)) { + assert(has_opt_out); + LOG_INF(level, where, "insecure delegation, opt-out proof found"); + } else { + LOG_ERROR(level, closest->owner, "delegation, DS non-existence proof missing"); + return 1; + } + } else if (ret == ZONE_NAME_NOT_FOUND) { + if (!wc_match && has_nsec3(ctx->conts) && has_nodata(ctx->conts, name, 0, NULL, &where)) { + if (has_nodata(ctx->conts, name, type, NULL, &where)) { + LOG_INF(level, where, "NSEC3 NODATA proof found"); + return KNOT_EOK; + } else { + LOG_ERROR(level, where, "NSEC3 NODATA proof missing"); + return 1; + } + } + if (node_rrtype_exists(closest, KNOT_RRTYPE_DNAME)) { + const knot_dname_t *dname_tgt = knot_dname_target(node_rdataset(closest, KNOT_RRTYPE_DNAME)->rdata); + size_t labels = knot_dname_labels(closest->owner, NULL); + knot_dname_t *cname = knot_dname_replace_suffix(name, labels, dname_tgt, NULL); + if (cname == NULL) { + return KNOT_ENOMEM; + } + LOG_INF(level, cname, "DNAME found, continuing validation"); + ret = check_cname(ctx, cname, type, level, expected_rcode); + knot_dname_free(cname, NULL); + return ret; + } + if (has_nxdomain(ctx->conts, name, false, level, &has_opt_out, &where, &encloser)) { + if (wc_match) { + LOG_INF(level, where, "wildcard non-existence proven"); + } else { + LOG_INF(level, where, "NXDOMAIN proven"); + } + } else { + if (ctx->cname_visit > 0) { + LOG_INF(level, name, "CNAME/DNAME chain not returned whole, please re-query for the target"); + return KNOT_EOK; // auth is not obligated to follow the chain whole + } + if (wc_match) { + LOG_INF(level, where, "wildcard non-existence proof missing"); + } else { + LOG_INF(level, where, "NXDOMAIN proof missing"); + } + return 1; + } + if (encloser == name) { + LOG_INF(level, name, "empty non-terminal detected, wildcard not applicable"); + return KNOT_EOK; + } + if (knot_dname_is_wildcard(name)) { + if (expected_rcode != NULL) { + *expected_rcode = KNOT_RCODE_NXDOMAIN; + } + } else { + knot_dname_t wc[2 + knot_dname_size(encloser)]; + knot_dname_wildcard(encloser, wc, sizeof(wc)); + if (has_opt_out && ctx->orig_rcode == KNOT_RCODE_NOERROR && + zone_contents_find_node(ctx->conts, wc) == NULL) { + LOG_INF(level, wc, "this is empty non-terminal NODATA unprovable due to NSEC3 opt-out, skipping wildcard non-existence proof"); + return KNOT_EOK; + } + LOG_INF(level, wc, "checking wildcard non/existence"); + return check_name(ctx, wc, type, level, expected_rcode); + } + } else if (node_rrtype_exists(match, KNOT_RRTYPE_CNAME)) { + const knot_rdataset_t *cn = node_rdataset(match, KNOT_RRTYPE_CNAME); + LOG_INF(level, knot_cname_name(cn->rdata), "CNAME found, continuing validation"); + return check_cname(ctx, knot_cname_name(cn->rdata), type, level, expected_rcode); + } else if (!node_rrtype_exists(match, type)) { + if (has_nodata(ctx->conts, match->owner, type, NULL, &where)) { + LOG_INF(level, match->owner, "NSEC NODATA proof found"); + } else { + LOG_ERROR(level, match->owner, "NODATA proof missing"); + return 1; + } + } else { + LOG_INF(level, match->owner, "positive answer found"); + } + return KNOT_EOK; +} + +static int check_cname(kdig_dnssec_ctx_t *ctx, const knot_dname_t *cname, + uint16_t type, kdig_validation_log_level_t level, + knot_rcode_t *expected_rcode) +{ + if (knot_dname_in_bailiwick(cname, ctx->conts->apex->owner) < 0) { + return KNOT_EOK; + } + if (++ctx->cname_visit >= CNAME_LIMIT) { + LOG_INF(level, cname, "limit of CNAME/DNAME chain reached, giving up"); + return KNOT_EOK; + } + return check_name(ctx, cname, type, level, expected_rcode); +} + +static int init_conts_from_pkt(knot_pkt_t *pkt, kdig_dnssec_ctx_t *ctx, + kdig_validation_log_level_t level) +{ + const knot_rrset_t *some_rrsig = find_first(pkt, KNOT_RRTYPE_RRSIG, KNOT_AUTHORITY); + if (some_rrsig == NULL) { + return KNOT_DNSSEC_ENOSIG; + } + const knot_dname_t *rrsig_zone = knot_rrsig_signer_name(some_rrsig->rrs.rdata); + + ctx->orig_qname = knot_dname_copy(knot_pkt_qname(pkt), NULL); + ctx->conts = zone_contents_new(rrsig_zone, false); + if (ctx->orig_qname == NULL || ctx->conts == NULL) { + return KNOT_ENOMEM; + } + ctx->orig_qtype = knot_pkt_qtype(pkt); + ctx->orig_rcode = knot_pkt_ext_rcode(pkt); + + int ret = rrsets_pkt2conts(pkt, ctx->conts, KNOT_AUTHORITY, 0, level); + if (ret != KNOT_EOK) { + return ret; + } + + const knot_rrset_t *some_nsec3 = find_first(pkt, KNOT_RRTYPE_NSEC3, KNOT_AUTHORITY); + if (some_nsec3 != NULL) { + dnssec_binary_t nsec3rd = { .data = some_nsec3->rrs.rdata->data, .size = some_nsec3->rrs.rdata->len }; + ret = dnssec_nsec3_params_from_rdata(&ctx->conts->nsec3_params, &nsec3rd); + if (ret != KNOT_EOK) { + return knot_error_from_libdnssec(ret); + } + } + + return KNOT_EOK; +} + +static int dv(knot_pkt_t *pkt, kdig_dnssec_ctx_t **dv_ctx, + kdig_validation_log_level_t level, + knot_dname_t zone_name[KNOT_DNAME_MAXLEN], uint16_t *type_needed) +{ + if (pkt == NULL || dv_ctx == NULL || zone_name == NULL || + zone_name == NULL || type_needed == NULL) { + return KNOT_EINVAL; + } + + if (*dv_ctx == NULL) { + *dv_ctx = calloc(1, sizeof(**dv_ctx)); + if (*dv_ctx == NULL) { + return KNOT_ENOMEM; + } + + int ret = init_conts_from_pkt(pkt, *dv_ctx, level); + if (ret != KNOT_EOK) { + return ret; + } else if (level >= KDIG_VALIDATION_LOG_INFOS) { + char zn[KNOT_DNAME_TXT_MAXLEN] = { 0 }; + knot_dname_to_str(zn, (*dv_ctx)->conts->apex->owner, sizeof(zn)); + LOG_INF(level, NULL, "for zone: %s", zn); + } + } + + zone_contents_t *conts = (*dv_ctx)->conts; + memcpy(zone_name, conts->apex->owner, knot_dname_size(conts->apex->owner)); + + int ret = solve_missing_apex(pkt, KNOT_RRTYPE_SOA, conts, level); + if (ret != KNOT_EOK) { // EAGAIN or failure + *type_needed = KNOT_RRTYPE_SOA; + return ret; + } + + ret = solve_missing_apex(pkt, KNOT_RRTYPE_DNSKEY, conts, level); + if (ret != KNOT_EOK) { // EAGAIN or failure + *type_needed = KNOT_RRTYPE_DNSKEY; + return ret; + } + + // revert answering quirks: wildcard expansion and CNAME synthesis + zone_tree_delsafe_it_t it = { 0 }; + ret = zone_tree_delsafe_it_begin(conts->nodes, &it, false); + while (ret == KNOT_EOK && !zone_tree_delsafe_it_finished(&it)) { + zone_node_t *n = zone_tree_delsafe_it_val(&it); + knot_rrset_t cname = node_rrset(n, KNOT_RRTYPE_CNAME), rrsig = node_rrset(n, KNOT_RRTYPE_RRSIG); + if (!knot_rrset_empty(&cname) && parents_have_rrtype(n, KNOT_RRTYPE_DNAME)) { + ret = zone_contents_remove_rr(conts, &cname, &n); + zone_tree_delsafe_it_next(&it); + continue; + } + + uint16_t rrsigcnt = 0, lbcnt = knot_dname_labels(n->owner, NULL), types[rrsig.rrs.count+1]; + ret = rrsig_types_lbcnt(&rrsig.rrs, (uint16_t *)&types, &rrsigcnt); + types[rrsig.rrs.count] = KNOT_RRTYPE_RRSIG; + if (lbcnt > rrsigcnt && rrsigcnt > 0 && !knot_dname_is_wildcard(n->owner)) { + knot_dname_t wcbuf[knot_dname_size(n->owner)]; + knot_dname_t *wc = knot_dname_wildcard(knot_dname_next_labels(n->owner, lbcnt - rrsigcnt), wcbuf, sizeof(wcbuf)); + assert(wc != NULL); + for (int i = 0; i < rrsig.rrs.count + 1 && ret == KNOT_EOK; i++) { + ret = move_rrset(conts, n, types[i], wc); + } + } + + zone_tree_delsafe_it_next(&it); + } + zone_tree_delsafe_it_free(&it); + + if (ret == KNOT_EOK) { + ret = zone_adjust_contents(conts, adjust_cb_flags, adjust_cb_nsec3_flags, false, true, false, 1, NULL); + } + if (ret == KNOT_EOK) { + ret = zone_tree_apply(conts->nodes, restore_orig_ttls, NULL); + } + if (ret == KNOT_EOK) { + ret = zone_tree_apply(conts->nsec3_nodes, restore_orig_ttls, NULL); + } + if (ret != KNOT_EOK) { + return ret; + } + + // NOTE at this point we have complete "contents" filled with the answer, relevant SOA and DNSKEY and their RRSIGs + + knot_rcode_t expected_rcode = KNOT_RCODE_NOERROR; + tmp_ctx_t tmp = { .conts = conts, .level = level }; + + // check NSEC3 tree consistence + ret = zone_tree_apply(conts->nsec3_nodes, check_nsec3, &tmp); + if (ret != KNOT_EOK) { // also '1' + return ret; + } + + // check the NSEC(3) proofs relevant for the queried name + ret = check_name(*dv_ctx, (*dv_ctx)->orig_qname, (*dv_ctx)->orig_qtype, level, &expected_rcode); + if (ret != KNOT_EOK) { // also '1' + return ret; + } + + // check that any NSEC does not prove non-existence of anything existing + ret = zone_tree_apply(conts->nodes, check_existing_with_nsecs, &tmp); + if (ret != KNOT_EOK) { // also '1' + return ret; + } + + // check validity of all RRSIGs + kdnssec_ctx_t kd_ctx = { 0 }; + ret = kdnssec_validation_ctx(NULL, &kd_ctx, conts); + if (ret != KNOT_EOK) { + return ret; + } + kd_ctx.policy->signing_threads = 1; + zone_update_t fake_up = { .new_cont = conts }; + ret = knot_zone_sign(&fake_up, NULL, &kd_ctx); + kdnssec_ctx_deinit(&kd_ctx); + if (ret == KNOT_DNSSEC_ENOSIG) { + char type_txt[16] = { 0 }; + (void)knot_rrtype_to_string(fake_up.validation_hint.rrtype, type_txt, sizeof(type_txt)); + LOG_ERROR(level, fake_up.validation_hint.node, "invalid or missing RRSIG for %s", type_txt); + return 1; + } + + // check RCODE + if (expected_rcode != (*dv_ctx)->orig_rcode) { + const knot_lookup_t *item = knot_lookup_by_id(knot_rcode_names, expected_rcode); + LOG_ERROR(level, NULL, "expected RCODE was: %s", item->name); + return 1; + } else { + LOG_INF(level, NULL, "correct RCODE found"); + } + + return ret; +} + +int kdig_dnssec_validate(knot_pkt_t *pkt, struct kdig_dnssec_ctx **dv_ctx, + kdig_validation_log_level_t level, + knot_dname_t zone_name[KNOT_DNAME_MAXLEN], uint16_t *type_needed) +{ + char type_txt[16] = { 0 }; + int ret = dv(pkt, dv_ctx, level, zone_name, type_needed); + if (ret == 1) { + LOG_OUTCOME(level, NULL, "NOK!"); + ret = KNOT_EOK; + } else if (ret == KNOT_DNSSEC_ENOSIG) { // ONLY the case when no RRSIG at all + LOG_ERROR(level, NULL, "Missing any RRSIGs."); + LOG_OUTCOME(level, NULL, "NOK!"); + ret = KNOT_EOK; + } else if (ret == KNOT_EOK) { + LOG_OUTCOME(level, NULL, "OK!"); + } + + if (ret == KNOT_EAGAIN) { + knot_rrtype_to_string(*type_needed, type_txt, sizeof(type_txt)); + LOG_INF(level, zone_name, "need to re-query for %s", type_txt); + } else { + if (*dv_ctx != NULL) { + zone_contents_deep_free((*dv_ctx)->conts); + free((*dv_ctx)->orig_qname); + free(*dv_ctx); + *dv_ctx = NULL; + } + } + return ret; +} diff --git a/src/utils/kdig/dnssec_validation.h b/src/utils/kdig/dnssec_validation.h new file mode 100644 index 0000000000..eb5b7e81a6 --- /dev/null +++ b/src/utils/kdig/dnssec_validation.h @@ -0,0 +1,45 @@ +/* Copyright (C) 2025 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/packet/pkt.h" + +struct kdig_dnssec_ctx; + +typedef enum { + KDIG_VALIDATION_LOG_NONE, + KDIG_VALIDATION_LOG_OUTCOME, + KDIG_VALIDATION_LOG_ERRORS, + KDIG_VALIDATION_LOG_INFOS, +} kdig_validation_log_level_t; + +/*! + * \brief Detailed DNSSEC validation of response pkt, logging to stdout. + * + * \param pkt The packet with a DNS response. + * \param dv_ctx In/out: context structure persistent across calling this function. + * \param level Verbosity of the logging. + * \param zone_name Detected zone name. + * \param type_needed Out: RRtype to re-query for. + * + * \retval KNOT_EAGAIN The caller shall re-query the detected zone's apex (zone_name) for requested RRtye (type_needed) and call this function again with the same context (dv_ctx) and the new DNS response packet. + * \retval KNOT_EOK The validation successfully took place, either finding errors and logging them, or finding all OK. + * \return KNOT_E* An error occured so that the validation couldn't take place. + */ +int kdig_dnssec_validate(knot_pkt_t *pkt, struct kdig_dnssec_ctx **dv_ctx, + kdig_validation_log_level_t level, + knot_dname_t zone_name[KNOT_DNAME_MAXLEN], uint16_t *type_needed); diff --git a/src/utils/kdig/kdig_exec.c b/src/utils/kdig/kdig_exec.c index 0758366e64..01e75f0b8f 100644 --- a/src/utils/kdig/kdig_exec.c +++ b/src/utils/kdig/kdig_exec.c @@ -19,6 +19,7 @@ #include <sys/socket.h> #include <sys/time.h> +#include "utils/kdig/dnssec_validation.h" #include "utils/kdig/kdig_exec.h" #include "utils/common/exec.h" #include "utils/common/msg.h" @@ -809,6 +810,44 @@ static int process_query_packet(const knot_pkt_t *query, return ret; } + if (query_ctx->dnssec_validation > 0) { +#if HAVE_KDIG_VALIDATION + 0 == 1 && !defined(NO_DNSSEC_VALIDATION) + knot_dname_t zone_name[KNOT_DNAME_MAXLEN] = { 0 }; + uint16_t type_needed = 0; + struct kdig_dnssec_ctx *dv_ctx = query_ctx->dv_ctx; + ret = kdig_dnssec_validate(reply, &dv_ctx, query_ctx->dnssec_validation, zone_name, &type_needed); + if (ret == KNOT_EAGAIN) { // need to re-query to get DNSKEY and/or SOA + knot_pkt_free(reply); + + query_t new_ctx = *query_ctx; + new_ctx.owner = knot_dname_to_str_alloc(zone_name); + if (new_ctx.owner == NULL) { + return KNOT_ENOMEM; + } + new_ctx.type_num = type_needed; + new_ctx.dv_ctx = dv_ctx; + new_ctx.style.show_header = false; + new_ctx.style.show_edns = false; + new_ctx.style.show_footer = false; + new_ctx.style.show_section = false; + new_ctx.style.show_question = false; + new_ctx.style.show_authority = false; + new_ctx.style.show_additional = false; + knot_pkt_t *new_query = create_query_packet(&new_ctx); + ret = process_query_packet(new_query, net, &new_ctx, ignore_tc, + sign_ctx, &new_ctx.style); + knot_pkt_free(new_query); + free(new_ctx.owner); + return ret; + } + if (ret != KNOT_EOK) { + ERR("DNSSEC VALIDATION FAILED to proceed (%s)", knot_strerror(ret)); + } +#else + assert("DNSSEC validation support not compiled - faulty code path" && false); +#endif // HAVE_KDIG_VALIDATION && !NO_DNSSEC_VALIDATION + } + knot_pkt_free(reply); net_close_keepopen(net, query_ctx); diff --git a/src/utils/kdig/kdig_params.c b/src/utils/kdig/kdig_params.c index f33da7fd95..e9a9e34bfa 100644 --- a/src/utils/kdig/kdig_params.c +++ b/src/utils/kdig/kdig_params.c @@ -289,6 +289,34 @@ static int opt_nodoflag(const char *arg, void *query) return KNOT_EOK; } +static int opt_validate(const char *arg, void *query) +{ +#if HAVE_KDIG_VALIDATION + 0 == 1 && !defined(NO_DNSSEC_VALIDATION) + query_t *q = query; + + q->dnssec_validation = 3; + if (arg != NULL && isdigit(arg[0])) { + q->dnssec_validation = arg[0] - '0'; + } + q->flags.do_flag = true; + + return KNOT_EOK; +#else + ERR2("DNSSEC validation support not compiled"); + return KNOT_ENOTSUP; +#endif +} + +static int opt_novalidate(const char *arg, void *query) +{ + query_t *q = query; + + q->dnssec_validation = 0; + + return KNOT_EOK; +} + + static int opt_all(const char *arg, void *query) { query_t *q = query; @@ -1532,6 +1560,9 @@ static const param_t kdig_opts2[] = { { "dnssec", ARG_NONE, opt_doflag }, { "nodnssec", ARG_NONE, opt_nodoflag }, + { "validate", ARG_OPTIONAL, opt_validate }, + { "novalidate", ARG_NONE, opt_novalidate }, + { "all", ARG_NONE, opt_all }, { "noall", ARG_NONE, opt_noall }, @@ -2362,6 +2393,7 @@ static void print_help(void) " +[no]adflag Set AD flag.\n" " +[no]cdflag Set CD flag.\n" " +[no]dnssec Set DO flag.\n" + " +[no]validate Re-query for SOA and DNSKEY, validate DNSSEC.\n" " +[no]all Show all packet sections.\n" " +[no]qr Show query packet.\n" " +[no]header Show packet header.\n" diff --git a/src/utils/kdig/kdig_params.h b/src/utils/kdig/kdig_params.h index a24f43b5a5..e289b78788 100644 --- a/src/utils/kdig/kdig_params.h +++ b/src/utils/kdig/kdig_params.h @@ -141,6 +141,9 @@ struct query { struct sockaddr_storage src; struct sockaddr_storage dst; } proxy; + /*!< Trigger of DNSSEC validation and related contents_t. */ + int dnssec_validation; + struct kdig_dnssec_ctx *dv_ctx; #if USE_DNSTAP /*!< Context for dnstap reader input. */ dt_reader_t *dt_reader; diff --git a/tests/.gitignore b/tests/.gitignore index a9763744bb..6641748656 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -99,3 +99,4 @@ /modules/test_rrl /utils/test_lookup +/utils/test_kdig_validate diff --git a/tests/Makefile.am b/tests/Makefile.am index ac88c46721..39ea529711 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -47,6 +47,8 @@ libtap_la_SOURCES = \ EXTRA_PROGRAMS = tap/runtests +check_SCRIPTS = + check_PROGRAMS = \ contrib/test_base32hex \ contrib/test_base64 \ @@ -168,6 +170,16 @@ endif ENABLE_XDP if HAVE_LIBUTILS check_PROGRAMS += \ utils/test_lookup + +if HAVE_KDIG_VALIDATION +check_SCRIPTS += \ + utils/test_kdig_validate + +utils/test_kdig_validate: + @$(edit) < $(top_srcdir)/tests/$@.in > $(top_builddir)/tests/$@ + @chmod +x $(top_builddir)/tests/$@ +endif HAVE_KDIG_VALIDATION + endif HAVE_LIBUTILS if HAVE_DAEMON @@ -214,7 +226,7 @@ libzscanner_zscanner_tool_SOURCES = \ libzscanner/processing.h \ libzscanner/processing.c -check_SCRIPTS = \ +check_SCRIPTS += \ libzscanner/test_zscanner edit = $(SED) \ diff --git a/tests/utils/test_kdig_validate.in b/tests/utils/test_kdig_validate.in new file mode 100755 index 0000000000..149b621e4c --- /dev/null +++ b/tests/utils/test_kdig_validate.in @@ -0,0 +1,107 @@ +#!/bin/bash + +BUILDROOT="@top_builddir@" +SRCROOT="@top_srcdir@" +TMPDIR=/tmp +PREFIX=ktv_$$_$(date +%s) +SCN=$TMPDIR/${PREFIX}_scenario.txt +CONF=$TMPDIR/${PREFIX}_knot.conf +PORT=$(comm -23 <(seq 50000 65000 | sort) <(ss -Htan | awk '{print $4}' | cut -d':' -f2 | sort -u) | shuf | head -n 1) +RUNDIR=$TMPDIR/${PREFIX}_knot +VALGRIND= + +. "@top_srcdir@/tests/tap/libtap.sh" + +if [ "$2" == "v" ]; then + VALGRIND="valgrind --leak-check=full --show-leak-kinds=all" +fi + +cat << EOF > $SCN +delegation.signed deleg A NOK! DS.NODATA.*found x.deleg A NOK! DS.NODATA.*found +different_signer_name.signed dns1 A OK! answer.found dns1 TXT NOK! NODATA.*found +dname_apex_nsec3.signed foo A OK! limit.of.*DNAME x TXT OK! limit.of.*DNAME +dnskey_keytags.many dns1 A FAILED many.*keytag dns2 A FAILED many.*keytag +no_rrsig.signed dns1 AAAA NOK! missing.RRSIG.*NSEC dns2 A NOK! missing.RRSIG.*NSEC +no_rrsig_with_delegation.signed deleg A NOK! any.RRSIG deleg DS NOK! missing.RRSIG.*NSEC +nsec_broken_chain_01.signed eee A NOK! invalid.*RRSIG.*NSEC zzz A OK! wildcard.non.*proven +nsec_broken_chain_02.signed eee A OK! wildcard.non.*proven zzz A NOK! wrongly.proves.NXDOMAIN +nsec_missing.signed www AAAA NOK! NXDOMAIN.*missing dns2 A NOK! invalid.*RRSIG.*NSEC +nsec_multiple.signed www AAAA NOK! wrongly.proves.NXDOMAIN zzz A NOK! wrongly.proves.NXDOMAIN +nsec_nonauth.invalid nonauth.deleg NS NOK! invalid.*RRSIG.*SOA nonauth.deleg DS NOK! invalid.*RRSIG.*SOA +nsec_wrong_bitmap_01.signed www A OK! answer.found www AAAA NOK! NODATA.*missing +nsec_wrong_bitmap_02.signed www A OK! answer.found www AAAA NOK! invalid.*RRSIG.*NSEC +nsec3_chain_01.signed deleg A NOK! invalid.*RRSIG.*NSEC3 dns2 A NOK! overlapping.*NSEC3 +nsec3_chain_02.signed deleg A OK! DS.NODATA.*found dns2 A NOK! overlapping.*NSEC3 +nsec3_chain_03.signed deleg A NOK! invalid.*RRSIG.*NSEC3 dns2 A NOK! overlapping.*NSEC3 +nsec3_missing.signed extra AAAA NOK! NXDOMAIN.*missing extrb A NOK! invalid.*RRSIG.*NSEC3 +nsec3_optout_ent.all x.deleg2.ent A OK! opt-out.*found ent A OK! NODATA.*unprovable +nsec3_optout_ent.invalid x.deleg1.ent A OK! DS.NODATA.*found ent A OK! NODATA.*unprovable +nsec3_optout_ent.valid x.deleg1.ent A OK! DS.NODATA.*found ent A OK! NODATA.*found +nsec3_optout.signed zzz A NOK! DS.non.*missing xx.zzz A NOK! DS.non.*missing +nsec3_param_invalid.signed dns1 A OK! answer.found dns2 A NOK! any.RRSIG +nsec3_wrong_bitmap_01.signed example.com. DNSKEY OK! answer.found example.com. SSHFP NOK! wrongly.proves.NODATA +nsec3_wrong_bitmap_02.signed dns1 TXT NOK! invalid.*RRSIG.*NSEC3 dns1 NSEC NOK! NODATA.*missing +rrsig_rdata_ttl.signed dns1 A NOK! invalid.*RRSIG.*A dns1 TXT OK! NODATA.*found +rrsig_signed.signed dns1 A OK! answer.found dns1 RRSIG OK! answer.found +rrsig_ttl.signed dns1 A OK! answer.found dns1 AAAA OK! NODATA.*found +EOF + +cat << EOF > $CONF +server: + listen: 0.0.0.0@$PORT + rundir: $RUNDIR +database: + storage: $RUNDIR +zone: + - domain: example.com. + storage: $RUNDIR + file: example.com.zone +log: + - target: stdout + any: debug +EOF + +plan $(( $(cat "$SCN" | wc -l) * 4 )) + +function q() { + echo "debug count $count" + QN="$2" + if [ "${QN: -1}" != "." ]; then + QN="$QN.example.com." + fi + echo "$1" $VALGRIND $BUILDROOT/src/kdig @127.0.0.1 -p $PORT +validate "$QN" -t "$3" >&2 + RESP=$($VALGRIND $BUILDROOT/src/kdig @127.0.0.1 -p $PORT +validate "$QN" -t "$3" 2>&1) + echo "$RESP" >&2 + echo "$RESP" | grep -q "[^N]$4" + ok "$1 outcome" test $? -eq 0 + echo "$RESP" | grep -q "$5" + ok "$1 point" test $? -eq 0 +} + +rm -rf $RUNDIR; mkdir $RUNDIR +$BUILDROOT/src/knotd -c $CONF > $RUNDIR/knot.log & +PID=$! +while ! grep -q 'server started' $RUNDIR/knot.log; do + continue +done + +i=0 +while read ZFILE QNAME QTYPE OUT POINT QNAME2 QTYPE2 OUT2 POINT2; do + i=$((i+1)) + if [ -n "$1" -a "$1" != "$i" ]; then + continue + fi + NLOADED_WAS=$(grep -c 'loaded, serial' $RUNDIR/knot.log) + cat $SRCROOT/tests/knot/semantic_check_data/$ZFILE > $RUNDIR/example.com.zone + $BUILDROOT/src/knotc -s $RUNDIR/knot.sock -f zone-reload >&2 + while [ $(grep -c 'loaded, serial' $RUNDIR/knot.log) == "$NLOADED_WAS" ]; do + sleep 0.02 + done + q "(${i}a)" "$QNAME" "$QTYPE" "$OUT" "$POINT" + q "(${i}b)" "$QNAME2" "$QTYPE2" "$OUT2" "$POINT2" +done < "$SCN" + + +kill -TERM $PID +sleep 0.1 +rm -rf $RUNDIR $SCN $CONF -- GitLab From f95390a3710fca9e92ab97985ef3d787bbc08e39 Mon Sep 17 00:00:00 2001 From: Libor Peltan <libor.peltan@nic.cz> Date: Tue, 11 Mar 2025 07:42:05 +0100 Subject: [PATCH 5/9] tests: support for calling kdig and its validation --- tests-extra/tests/modules/onlinesign/test.py | 3 +++ tests-extra/tools/dnstest/params.py | 2 ++ tests-extra/tools/dnstest/server.py | 28 ++++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/tests-extra/tests/modules/onlinesign/test.py b/tests-extra/tests/modules/onlinesign/test.py index b3878cceee..7a66e3e9dc 100644 --- a/tests-extra/tests/modules/onlinesign/test.py +++ b/tests-extra/tests/modules/onlinesign/test.py @@ -25,6 +25,7 @@ def check_zone(zone, dnskey_rdata_start): soa1 = knot.dig(zone.name, "SOA", dnssec=True) soa1.check(rcode="NOERROR", flags="QR AA") soa1.check_count(1, "RRSIG") + knot.kdig(zone.name, "SOA", validate=True) t.sleep(1) # Ensure different RRSIGs. @@ -51,6 +52,7 @@ def check_zone(zone, dnskey_rdata_start): resp.check(rcode="NOERROR", flags="QR AA") resp.check_count(1, "DNSKEY") resp.check_count(1, "RRSIG") + knot.kdig(zone.name, "DNSKEY", validate=True) for rrset in resp.resp.answer: if rrset.rdtype != dns.rdatatype.DNSKEY: @@ -65,6 +67,7 @@ def check_zone(zone, dnskey_rdata_start): resp.check_count(1, "SOA", section="authority") resp.check_count(1, "NSEC", section="authority") resp.check_count(2, "RRSIG", section="authority") + knot.kdig("nx." + zone.name, "A", validate=True) t.start() serial = knot.zones_wait(zones) diff --git a/tests-extra/tools/dnstest/params.py b/tests-extra/tools/dnstest/params.py index fcf3bcc60b..7278d3d0d9 100644 --- a/tests-extra/tools/dnstest/params.py +++ b/tests-extra/tools/dnstest/params.py @@ -62,6 +62,8 @@ libknot_lib = get_binary("KNOT_TEST_LIBKNOT", repo_binary("src/.libs/libknot.so" knot_bin = get_binary("KNOT_TEST_KNOT", repo_binary("src/knotd")) # KNOT_TEST_KNOTC - Knot control binary. knot_ctl = get_binary("KNOT_TEST_KNOTC", repo_binary("src/knotc")) +# KNOT_TEST_KDIG - Digging binary. +kdig_bin = get_binary("KNOT_TEST_KDIG", repo_binary("src/kdig")) # KNOT_TEST_KEYMGR - Knot key management binary. keymgr_bin = get_binary("KNOT_TEST_KEYMGR", repo_binary("src/keymgr")) # KNOT_TEST_KJOURNALPRINT - Knot journal print binary. diff --git a/tests-extra/tools/dnstest/server.py b/tests-extra/tools/dnstest/server.py index f480849b86..2a820e3446 100644 --- a/tests-extra/tools/dnstest/server.py +++ b/tests-extra/tools/dnstest/server.py @@ -612,6 +612,34 @@ class Server(object): hostname3 = socket.gethostname() return ("", certfile, hostname1 or hostname2 or hostname3, ssearch(gcli_s, r'pin-sha256:([^\n]*)')) + def kdig(self, rname, rtype, rclass="IN", dnssec=None, validate=None): + cmd = [ params.kdig_bin, "@" + self.addr, "-p", str(self.port), rname, "-t", rtype, "-c", rclass ] + if dnssec: + cmd += [ "+dnssec" ] + if validate: + cmd += [ "+validate" ] + outcome = run(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True) + out_s = outcome.stdout.rstrip() + err_s = outcome.stderr.rstrip() + with open(self.dir + "/kdig.out", mode="a") as sout: + sout.write(out_s) + sout.write("\n") + with open(self.dir + "/kdig.err", mode="a") as serr: + serr.write(err_s) + serr.write("\n") + + if validate and "validation support not compiled" in err_s: + return out_s + if outcome.returncode != 0: + set_err("KDIG FAILED") + + expect = "OK" + if isinstance(validate, str): + expect = validate + if validate and not (("DNSSEC VALIDATION: %s!" % expect) in out_s): + set_err("KDIG VALIDATION") + return out_s + def dig(self, rname, rtype, rclass="IN", udp=None, serial=None, timeout=None, tries=3, flags="", bufsize=None, edns=None, nsid=False, dnssec=False, log_no_sep=False, tsig=None, addr=None, source=None, xdp=None): -- GitLab From 2e31e5463fad05ceff58616715df35fe021772dc Mon Sep 17 00:00:00 2001 From: Libor Peltan <libor.peltan@nic.cz> Date: Fri, 21 Mar 2025 10:27:18 +0100 Subject: [PATCH 6/9] kdig/validate: dont query for SOA it is useless --- doc/man_kdig.rst | 2 +- src/utils/kdig/dnssec_validation.c | 10 ++-------- src/utils/kdig/kdig_exec.c | 2 +- tests/utils/test_kdig_validate.in | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/doc/man_kdig.rst b/doc/man_kdig.rst index c7d63ad8db..e4d359c7e4 100644 --- a/doc/man_kdig.rst +++ b/doc/man_kdig.rst @@ -171,7 +171,7 @@ Options Set the DO flag. **+**\ [\ **no**\ ]\ **validate** - Also query for SOA and DNSKEY, validate DNSSEC in the answer. Implies DO flag. + Also query for DNSKEY, validate DNSSEC in the answer. Implies DO flag. Optional argument specifies verbosity (1-3, default 3). **+**\ [\ **no**\ ]\ **all** diff --git a/src/utils/kdig/dnssec_validation.c b/src/utils/kdig/dnssec_validation.c index 0ec532f550..149f050d5f 100644 --- a/src/utils/kdig/dnssec_validation.c +++ b/src/utils/kdig/dnssec_validation.c @@ -576,13 +576,7 @@ static int dv(knot_pkt_t *pkt, kdig_dnssec_ctx_t **dv_ctx, zone_contents_t *conts = (*dv_ctx)->conts; memcpy(zone_name, conts->apex->owner, knot_dname_size(conts->apex->owner)); - int ret = solve_missing_apex(pkt, KNOT_RRTYPE_SOA, conts, level); - if (ret != KNOT_EOK) { // EAGAIN or failure - *type_needed = KNOT_RRTYPE_SOA; - return ret; - } - - ret = solve_missing_apex(pkt, KNOT_RRTYPE_DNSKEY, conts, level); + int ret = solve_missing_apex(pkt, KNOT_RRTYPE_DNSKEY, conts, level); if (ret != KNOT_EOK) { // EAGAIN or failure *type_needed = KNOT_RRTYPE_DNSKEY; return ret; @@ -629,7 +623,7 @@ static int dv(knot_pkt_t *pkt, kdig_dnssec_ctx_t **dv_ctx, return ret; } - // NOTE at this point we have complete "contents" filled with the answer, relevant SOA and DNSKEY and their RRSIGs + // NOTE at this point we have complete "contents" filled with the answer, DNSKEY and their RRSIGs knot_rcode_t expected_rcode = KNOT_RCODE_NOERROR; tmp_ctx_t tmp = { .conts = conts, .level = level }; diff --git a/src/utils/kdig/kdig_exec.c b/src/utils/kdig/kdig_exec.c index 01e75f0b8f..88473c2179 100644 --- a/src/utils/kdig/kdig_exec.c +++ b/src/utils/kdig/kdig_exec.c @@ -816,7 +816,7 @@ static int process_query_packet(const knot_pkt_t *query, uint16_t type_needed = 0; struct kdig_dnssec_ctx *dv_ctx = query_ctx->dv_ctx; ret = kdig_dnssec_validate(reply, &dv_ctx, query_ctx->dnssec_validation, zone_name, &type_needed); - if (ret == KNOT_EAGAIN) { // need to re-query to get DNSKEY and/or SOA + if (ret == KNOT_EAGAIN) { // need to re-query to get DNSKEY knot_pkt_free(reply); query_t new_ctx = *query_ctx; diff --git a/tests/utils/test_kdig_validate.in b/tests/utils/test_kdig_validate.in index 149b621e4c..59df2a2966 100755 --- a/tests/utils/test_kdig_validate.in +++ b/tests/utils/test_kdig_validate.in @@ -27,7 +27,7 @@ nsec_broken_chain_01.signed eee A NOK! invalid.*RRSIG.*NS nsec_broken_chain_02.signed eee A OK! wildcard.non.*proven zzz A NOK! wrongly.proves.NXDOMAIN nsec_missing.signed www AAAA NOK! NXDOMAIN.*missing dns2 A NOK! invalid.*RRSIG.*NSEC nsec_multiple.signed www AAAA NOK! wrongly.proves.NXDOMAIN zzz A NOK! wrongly.proves.NXDOMAIN -nsec_nonauth.invalid nonauth.deleg NS NOK! invalid.*RRSIG.*SOA nonauth.deleg DS NOK! invalid.*RRSIG.*SOA +nsec_nonauth.invalid nonauth.deleg NS NOK! invalid.*RRSIG.*DNSKEY nonauth.deleg DS NOK! invalid.*RRSIG.*DNSKEY nsec_wrong_bitmap_01.signed www A OK! answer.found www AAAA NOK! NODATA.*missing nsec_wrong_bitmap_02.signed www A OK! answer.found www AAAA NOK! invalid.*RRSIG.*NSEC nsec3_chain_01.signed deleg A NOK! invalid.*RRSIG.*NSEC3 dns2 A NOK! overlapping.*NSEC3 -- GitLab From c5348c6990147e287782bc9ed292383297bacb32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Dosko=C4=8Dil?= <jan.doskocil@nic.cz> Date: Wed, 26 Mar 2025 15:30:03 +0100 Subject: [PATCH 7/9] kdig/validate: cosmetic changes Shorter lines, more descriptive names. --- src/utils/kdig/dnssec_validation.c | 196 ++++++++++++++++++----------- 1 file changed, 125 insertions(+), 71 deletions(-) diff --git a/src/utils/kdig/dnssec_validation.c b/src/utils/kdig/dnssec_validation.c index 149f050d5f..9100e1a56d 100644 --- a/src/utils/kdig/dnssec_validation.c +++ b/src/utils/kdig/dnssec_validation.c @@ -26,16 +26,16 @@ typedef struct kdig_dnssec_ctx { zone_contents_t *conts; - knot_dname_t *orig_qname; - uint16_t orig_qtype; - knot_rcode_t orig_rcode; - unsigned cname_visit; + knot_dname_t *orig_qname; + uint16_t orig_qtype; + knot_rcode_t orig_rcode; + unsigned cname_visit; } kdig_dnssec_ctx_t; typedef struct { zone_contents_t *conts; kdig_validation_log_level_t level; -} tmp_ctx_t; +} tree_cb_ctx_t; static void kdv_log(kdig_validation_log_level_t log_level, kdig_validation_log_level_t set_level, const knot_dname_t *at, const char *msg, ...) @@ -76,25 +76,28 @@ static bool nsec_covers_name(const knot_dname_t *nsec_owner, const knot_rdata_t return dname_between(nsec_owner, name, nsec_next); } -static bool nsec3_covers_name(const knot_dname_t *nsec3_owner, const knot_rdata_t *nsec3_rdata, - const knot_dname_t *name, const knot_dname_t *apex) +static bool nsec3_covers_name(const knot_dname_t *nsec3_owner, + const knot_rdata_t *nsec3_rdata, + const knot_dname_t *name, + const knot_dname_t *apex) { const uint8_t *nsec3_hash = knot_nsec3_next(nsec3_rdata); uint16_t n3h_len = knot_nsec3_next_len(nsec3_rdata); uint8_t nsec3_next[KNOT_DNAME_MAXLEN] = { 0 }; int ret = knot_nsec3_hash_to_dname(nsec3_next, sizeof(nsec3_next), nsec3_hash, n3h_len, apex); - return ret == KNOT_EOK /* best effort */ && dname_between(nsec3_owner,name, nsec3_next); + return ret == KNOT_EOK /* best effort */ && dname_between(nsec3_owner, name, nsec3_next); } static int check_nsec3(zone_node_t *node, void *data) { - tmp_ctx_t *ctx = data; - dnssec_nsec3_params_t *params = &ctx->conts->nsec3_params, found = { 0 }; + tree_cb_ctx_t *ctx = data; + dnssec_nsec3_params_t *params = &ctx->conts->nsec3_params; + dnssec_nsec3_params_t found = { 0 }; knot_rdataset_t *nsec3 = node_rdataset(node, KNOT_RRTYPE_NSEC3); dnssec_binary_t rd = { .data = nsec3->rdata->data, .size = nsec3->rdata->len }; int ret; - if ((ret = dnssec_nsec3_params_from_rdata(&found, &rd)) != KNOT_EOK || - !dnssec_nsec3_params_match(&found, params)) { + if ((ret = dnssec_nsec3_params_from_rdata(&found, &rd)) != KNOT_EOK + || !dnssec_nsec3_params_match(&found, params)) { LOG_ERROR(ctx->level, node->owner, "invalid or unmatching NSEC3"); return 1; } @@ -126,7 +129,15 @@ static int move_rrset(zone_contents_t *c, zone_node_t *n, uint16_t type, const k if (knot_rrset_empty(&rr)) { return KNOT_EOK; } - const knot_rrset_t rr2 = { .owner = (knot_dname_t *)target, .type = rr.type, .rclass = rr.rclass, .ttl = rr.ttl, .rrs = rr.rrs }; + + const knot_rrset_t rr2 = { + .owner = (knot_dname_t *)target, + .type = rr.type, + .rclass = rr.rclass, + .ttl = rr.ttl, + .rrs = rr.rrs, + }; + int ret = zone_contents_add_rr(c, &rr2, &unused); if (ret == KNOT_EOK) { ret = zone_contents_remove_rr(c, &rr, &n); @@ -134,7 +145,9 @@ static int move_rrset(zone_contents_t *c, zone_node_t *n, uint16_t type, const k return ret; } -static int rrsig_types_lbcnt(const knot_rdataset_t *rrsig, uint16_t *types /* must be pre-allocated to rrsig->count+1 */, uint16_t *lbcnt) +static int rrsig_types_labelcnt(const knot_rdataset_t *rrsig, + uint16_t *types, /* must be pre-allocated to rrsig->count+1 */ + uint16_t *lbcnt) { knot_rdata_t *rd = rrsig->rdata; for (int i = 0; i < rrsig->count; i++) { @@ -149,7 +162,7 @@ static int rrsig_types_lbcnt(const knot_rdataset_t *rrsig, uint16_t *types /* mu return KNOT_EOK; } -static int restore_orig_ttls(zone_node_t *node, void *unused) +static int restore_orig_ttls(zone_node_t *node, [[__maybe_unused__]] void *unused) { knot_rdataset_t *rrsig = node_rdataset(node, KNOT_RRTYPE_RRSIG); if (rrsig != NULL) { @@ -194,7 +207,9 @@ static bool has_nodata(zone_contents_t *conts, const knot_dname_t *name, uint16_ if (where != NULL) { *where = nsec.owner; } - return !knot_rrset_empty(&nsec) && bitmap_covers(knot_nsec_bitmap(nsec.rrs.rdata), knot_nsec_bitmap_len(nsec.rrs.rdata), type, from_node); + return !knot_rrset_empty(&nsec) + && bitmap_covers(knot_nsec_bitmap(nsec.rrs.rdata), + knot_nsec_bitmap_len(nsec.rrs.rdata), type, from_node); } const zone_node_t *nsec3_node = NULL, *nsec3_prev = NULL; @@ -206,7 +221,9 @@ static bool has_nodata(zone_contents_t *conts, const knot_dname_t *name, uint16_ if (where != NULL) { *where = nsec3.owner; } - return !knot_rrset_empty(&nsec3) && bitmap_covers(knot_nsec3_bitmap(nsec3.rrs.rdata), knot_nsec3_bitmap_len(nsec3.rrs.rdata), type, from_node); + return !knot_rrset_empty(&nsec3) + && bitmap_covers(knot_nsec3_bitmap(nsec3.rrs.rdata), + knot_nsec3_bitmap_len(nsec3.rrs.rdata), type, from_node); } static bool has_nxdomain(zone_contents_t *conts, const knot_dname_t *name, bool opt_out, @@ -215,7 +232,8 @@ static bool has_nxdomain(zone_contents_t *conts, const knot_dname_t *name, bool { if (!has_nsec3(conts)) { const zone_node_t *match = NULL, *closest = NULL, *prev = NULL; - int ret = zone_contents_find_dname(conts, name, &match, &closest, &prev, knot_dname_with_null(name)); + int ret = zone_contents_find_dname(conts, name, &match, &closest, &prev, + knot_dname_with_null(name)); if (ret < 0 || match == prev) { return false; } @@ -225,25 +243,31 @@ static bool has_nxdomain(zone_contents_t *conts, const knot_dname_t *name, bool knot_rrset_t nsec = node_rrset(prev, KNOT_RRTYPE_NSEC); *where = nsec.owner; *encloser = closest->owner; - if (!knot_rrset_empty(&nsec) && knot_dname_in_bailiwick(knot_nsec_next(nsec.rrs.rdata), name) >= 0) { + if (!knot_rrset_empty(&nsec) + && knot_dname_in_bailiwick(knot_nsec_next(nsec.rrs.rdata), name) >= 0) { *encloser = name; // empty-non-terminal detected } - return !opt_out && !knot_rrset_empty(&nsec) && nsec_covers_name(prev->owner, nsec.rrs.rdata, name); + return !opt_out && !knot_rrset_empty(&nsec) + && nsec_covers_name(prev->owner, nsec.rrs.rdata, name); } - // scan for closest encloser represented by some NSEC3, because the closest encloser node might not be here - size_t apex_lbs = knot_dname_labels(conts->apex->owner, NULL), name_lbs = knot_dname_labels(name, NULL); + // scan for closest encloser represented by some NSEC3, because the closest encloser node + // might not be here + size_t apex_nlabels = knot_dname_labels(conts->apex->owner, NULL); + size_t name_nlabels = knot_dname_labels(name, NULL); const knot_dname_t *enc_where = NULL; *encloser = knot_dname_next_label(name); - for ( ; name_lbs > apex_lbs; name_lbs--) { + for (; name_nlabels > apex_nlabels; name_nlabels--) { if (has_nodata(conts, *encloser, 0, NULL, &enc_where) || - zone_contents_find_node(conts, *encloser) != NULL) { // tricky exception: in some cases the closest encloser is proven by existence of stuff, e.g. RFC 5155 § 7.2.6 + // tricky exception: in some cases the closest encloser is + // proven by existence of stuff, e.g. RFC 5155 § 7.2.6 + zone_contents_find_node(conts, *encloser) != NULL) { break; } name = *encloser; *encloser = knot_dname_next_label(name); } - if (name_lbs <= apex_lbs) { + if (name_nlabels <= apex_nlabels) { LOG_ERROR(level, name, "NSEC3 encloser proof missing"); return false; } else { @@ -267,34 +291,41 @@ static bool has_nxdomain(zone_contents_t *conts, const knot_dname_t *name, bool if (has_opt_out != NULL) { *has_opt_out = (knot_nsec3_flags(nsec3.rrs.rdata) & KNOT_NSEC3_FLAG_OPT_OUT); } - return !knot_rrset_empty(&nsec3) && nsec3_covers_name(nsec3_prev->owner, nsec3.rrs.rdata, nsec3_name, conts->apex->owner) && - (!opt_out || (knot_nsec3_flags(nsec3.rrs.rdata) & KNOT_NSEC3_FLAG_OPT_OUT)); + return !knot_rrset_empty(&nsec3) + && nsec3_covers_name(nsec3_prev->owner, nsec3.rrs.rdata, nsec3_name, conts->apex->owner) + && (!opt_out || (knot_nsec3_flags(nsec3.rrs.rdata) & KNOT_NSEC3_FLAG_OPT_OUT)); } static int check_existing_with_nsecs(zone_node_t *node, void *data) { - tmp_ctx_t *ctx = data; + tree_cb_ctx_t *ctx = data; const knot_dname_t *where = NULL, *encloser = NULL; bool has_opt_out = false; if (node->flags & NODE_FLAGS_DELEG) { - bool has_nxd = has_nxdomain(ctx->conts, node->owner, false, KDIG_VALIDATION_LOG_NONE, &has_opt_out, &where, &encloser); + bool has_nxd = has_nxdomain(ctx->conts, node->owner, false, KDIG_VALIDATION_LOG_NONE, + &has_opt_out, &where, &encloser); if (node_rrtype_exists(node, KNOT_RRTYPE_DS)) { if (has_nodata(ctx->conts, node->owner, KNOT_RRTYPE_DS, NULL, NULL)) { - LOG_ERROR(ctx->level, node->owner, "NSEC(3) wrongly proves insecure delegation"); + LOG_ERROR(ctx->level, node->owner, + "NSEC(3) wrongly proves insecure delegation"); return 1; } else if (has_nxd) { if (has_opt_out) { - LOG_ERROR(ctx->level, node->owner, "NSEC3 opt-out wrongly applied to secure delegation"); + LOG_ERROR(ctx->level, node->owner, + "NSEC3 opt-out wrongly applied to secure delegation"); } else { - LOG_ERROR(ctx->level, node->owner, "NSEC(3) wrongly proves NXDOMAIN for secure delegation"); + LOG_ERROR(ctx->level, node->owner, + "NSEC(3) wrongly proves NXDOMAIN for secure delegation"); } return 1; } } else if (has_nxd && !has_opt_out) { if (has_nsec3(ctx->conts)) { - LOG_ERROR(ctx->level, node->owner, "NSEC3 opt-out flag missing, proving NXDOMAIN fro insecure delegation"); + LOG_ERROR(ctx->level, node->owner, + "NSEC3 opt-out flag missing, proving NXDOMAIN fro insecure delegation"); } else { - LOG_ERROR(ctx->level, node->owner, "NSEC wrongly proves NXDOMAIN for insecure delegation"); + LOG_ERROR(ctx->level, node->owner, + "NSEC wrongly proves NXDOMAIN for insecure delegation"); } return 1; } @@ -302,8 +333,9 @@ static int check_existing_with_nsecs(zone_node_t *node, void *data) if (has_nodata(ctx->conts, node->owner, 0, node, NULL)) { LOG_ERROR(ctx->level, node->owner, "NSEC(3) wrongly proves NODATA"); return 1; - } else if (has_nxdomain(ctx->conts, node->owner, false, KDIG_VALIDATION_LOG_NONE, &has_opt_out, &where, &encloser) && - (!has_opt_out || (node->flags & NODE_FLAGS_SUBTREE_AUTH))) { + } else if (has_nxdomain(ctx->conts, node->owner, false, KDIG_VALIDATION_LOG_NONE, + &has_opt_out, &where, &encloser) + && (!has_opt_out || (node->flags & NODE_FLAGS_SUBTREE_AUTH))) { LOG_ERROR(ctx->level, node->owner, "NSEC(3) wrongly proves NXDOMAIN"); return 1; } @@ -347,7 +379,8 @@ static int rrsets_pkt2conts(knot_pkt_t *pkt, zone_contents_t *conts, if (type_only && knot_rrsig_type_covered(rr->rrs.rdata) != type_only) { continue; } - } else if ((type_only && rr->type != type_only) || knot_rrtype_is_metatype(rr->type)) { + } else if ((type_only && rr->type != type_only) + || knot_rrtype_is_metatype(rr->type)) { continue; } @@ -375,12 +408,16 @@ static int rrsets_pkt2conts(knot_pkt_t *pkt, zone_contents_t *conts, return ret; } -static int solve_missing_apex(knot_pkt_t *pkt, uint16_t rrtype, zone_contents_t *conts, kdig_validation_log_level_t level) +static int solve_missing_apex(knot_pkt_t *pkt, + uint16_t rrtype, + zone_contents_t *conts, + kdig_validation_log_level_t level) { if (node_rrtype_exists(conts->apex, rrtype)) { return KNOT_EOK; } - if (knot_pkt_qtype(pkt) != rrtype || !knot_dname_is_equal(knot_pkt_qname(pkt), conts->apex->owner)) { + if (knot_pkt_qtype(pkt) != rrtype + || !knot_dname_is_equal(knot_pkt_qname(pkt), conts->apex->owner)) { return KNOT_EAGAIN; } int ret = rrsets_pkt2conts(pkt, conts, KNOT_ANSWER, rrtype, level); @@ -400,8 +437,10 @@ static int check_name(kdig_dnssec_ctx_t *ctx, const knot_dname_t *name, { const knot_dname_t *where = NULL, *encloser = NULL; const zone_node_t *match = NULL, *closest = NULL, *prev = NULL; - bool has_opt_out = false, wc_match = knot_dname_is_wildcard(name) && !knot_dname_is_wildcard(ctx->orig_qname); - int ret = zone_contents_find_dname(ctx->conts, name, &match, &closest, &prev, knot_dname_with_null(name)); + bool has_opt_out = false; + bool wc_match = knot_dname_is_wildcard(name) && !knot_dname_is_wildcard(ctx->orig_qname); + int ret = zone_contents_find_dname(ctx->conts, name, &match, &closest, &prev, + knot_dname_with_null(name)); if (ret < 0) { return ret; } @@ -417,15 +456,18 @@ static int check_name(kdig_dnssec_ctx_t *ctx, const knot_dname_t *name, return KNOT_EOK; } else if (has_nodata(ctx->conts, closest->owner, KNOT_RRTYPE_DS, NULL, &where)) { LOG_INF(level, where, "insecure delegation, DS NODATA proof found"); - } else if (has_nxdomain(ctx->conts, closest->owner, true, level, &has_opt_out, &where, &encloser)) { + } else if (has_nxdomain(ctx->conts, closest->owner, true, level, &has_opt_out, + &where, &encloser)) { assert(has_opt_out); LOG_INF(level, where, "insecure delegation, opt-out proof found"); } else { - LOG_ERROR(level, closest->owner, "delegation, DS non-existence proof missing"); + LOG_ERROR(level, closest->owner, + "delegation, DS non-existence proof missing"); return 1; } } else if (ret == ZONE_NAME_NOT_FOUND) { - if (!wc_match && has_nsec3(ctx->conts) && has_nodata(ctx->conts, name, 0, NULL, &where)) { + if (!wc_match && has_nsec3(ctx->conts) + && has_nodata(ctx->conts, name, 0, NULL, &where)) { if (has_nodata(ctx->conts, name, type, NULL, &where)) { LOG_INF(level, where, "NSEC3 NODATA proof found"); return KNOT_EOK; @@ -435,7 +477,8 @@ static int check_name(kdig_dnssec_ctx_t *ctx, const knot_dname_t *name, } } if (node_rrtype_exists(closest, KNOT_RRTYPE_DNAME)) { - const knot_dname_t *dname_tgt = knot_dname_target(node_rdataset(closest, KNOT_RRTYPE_DNAME)->rdata); + const knot_rdata_t *rdata_tmp = node_rdataset(closest, KNOT_RRTYPE_DNAME)->rdata; + const knot_dname_t *dname_tgt = knot_dname_target(rdata_tmp); size_t labels = knot_dname_labels(closest->owner, NULL); knot_dname_t *cname = knot_dname_replace_suffix(name, labels, dname_tgt, NULL); if (cname == NULL) { @@ -454,7 +497,8 @@ static int check_name(kdig_dnssec_ctx_t *ctx, const knot_dname_t *name, } } else { if (ctx->cname_visit > 0) { - LOG_INF(level, name, "CNAME/DNAME chain not returned whole, please re-query for the target"); + LOG_INF(level, name, + "CNAME/DNAME chain not returned whole, please re-query for the target"); return KNOT_EOK; // auth is not obligated to follow the chain whole } if (wc_match) { @@ -477,7 +521,8 @@ static int check_name(kdig_dnssec_ctx_t *ctx, const knot_dname_t *name, knot_dname_wildcard(encloser, wc, sizeof(wc)); if (has_opt_out && ctx->orig_rcode == KNOT_RCODE_NOERROR && zone_contents_find_node(ctx->conts, wc) == NULL) { - LOG_INF(level, wc, "this is empty non-terminal NODATA unprovable due to NSEC3 opt-out, skipping wildcard non-existence proof"); + LOG_INF(level, wc, "this is empty non-terminal NODATA unprovable due to NSEC3 opt-out, " + "skipping wildcard non-existence proof"); return KNOT_EOK; } LOG_INF(level, wc, "checking wildcard non/existence"); @@ -538,7 +583,10 @@ static int init_conts_from_pkt(knot_pkt_t *pkt, kdig_dnssec_ctx_t *ctx, const knot_rrset_t *some_nsec3 = find_first(pkt, KNOT_RRTYPE_NSEC3, KNOT_AUTHORITY); if (some_nsec3 != NULL) { - dnssec_binary_t nsec3rd = { .data = some_nsec3->rrs.rdata->data, .size = some_nsec3->rrs.rdata->len }; + dnssec_binary_t nsec3rd = { + .data = some_nsec3->rrs.rdata->data, + .size = some_nsec3->rrs.rdata->len, + }; ret = dnssec_nsec3_params_from_rdata(&ctx->conts->nsec3_params, &nsec3rd); if (ret != KNOT_EOK) { return knot_error_from_libdnssec(ret); @@ -548,9 +596,11 @@ static int init_conts_from_pkt(knot_pkt_t *pkt, kdig_dnssec_ctx_t *ctx, return KNOT_EOK; } -static int dv(knot_pkt_t *pkt, kdig_dnssec_ctx_t **dv_ctx, - kdig_validation_log_level_t level, - knot_dname_t zone_name[KNOT_DNAME_MAXLEN], uint16_t *type_needed) +static int dnssec_validate(knot_pkt_t *pkt, + kdig_dnssec_ctx_t **dv_ctx, + kdig_validation_log_level_t loglevel, + knot_dname_t zone_name[KNOT_DNAME_MAXLEN], + uint16_t *type_needed) { if (pkt == NULL || dv_ctx == NULL || zone_name == NULL || zone_name == NULL || type_needed == NULL) { @@ -563,20 +613,20 @@ static int dv(knot_pkt_t *pkt, kdig_dnssec_ctx_t **dv_ctx, return KNOT_ENOMEM; } - int ret = init_conts_from_pkt(pkt, *dv_ctx, level); + int ret = init_conts_from_pkt(pkt, *dv_ctx, loglevel); if (ret != KNOT_EOK) { return ret; - } else if (level >= KDIG_VALIDATION_LOG_INFOS) { + } else if (loglevel >= KDIG_VALIDATION_LOG_INFOS) { char zn[KNOT_DNAME_TXT_MAXLEN] = { 0 }; knot_dname_to_str(zn, (*dv_ctx)->conts->apex->owner, sizeof(zn)); - LOG_INF(level, NULL, "for zone: %s", zn); + LOG_INF(loglevel, NULL, "for zone: %s", zn); } } zone_contents_t *conts = (*dv_ctx)->conts; memcpy(zone_name, conts->apex->owner, knot_dname_size(conts->apex->owner)); - int ret = solve_missing_apex(pkt, KNOT_RRTYPE_DNSKEY, conts, level); + int ret = solve_missing_apex(pkt, KNOT_RRTYPE_DNSKEY, conts, loglevel); if (ret != KNOT_EOK) { // EAGAIN or failure *type_needed = KNOT_RRTYPE_DNSKEY; return ret; @@ -587,31 +637,35 @@ static int dv(knot_pkt_t *pkt, kdig_dnssec_ctx_t **dv_ctx, ret = zone_tree_delsafe_it_begin(conts->nodes, &it, false); while (ret == KNOT_EOK && !zone_tree_delsafe_it_finished(&it)) { zone_node_t *n = zone_tree_delsafe_it_val(&it); - knot_rrset_t cname = node_rrset(n, KNOT_RRTYPE_CNAME), rrsig = node_rrset(n, KNOT_RRTYPE_RRSIG); + knot_rrset_t cname = node_rrset(n, KNOT_RRTYPE_CNAME); + knot_rrset_t rrsig = node_rrset(n, KNOT_RRTYPE_RRSIG); if (!knot_rrset_empty(&cname) && parents_have_rrtype(n, KNOT_RRTYPE_DNAME)) { ret = zone_contents_remove_rr(conts, &cname, &n); zone_tree_delsafe_it_next(&it); continue; } - uint16_t rrsigcnt = 0, lbcnt = knot_dname_labels(n->owner, NULL), types[rrsig.rrs.count+1]; - ret = rrsig_types_lbcnt(&rrsig.rrs, (uint16_t *)&types, &rrsigcnt); + uint16_t nlabels = knot_dname_labels(n->owner, NULL); + uint16_t rrsig_nlabels = 0; + uint16_t types[rrsig.rrs.count + 1]; + ret = rrsig_types_labelcnt(&rrsig.rrs, (uint16_t *)&types, &rrsig_nlabels); types[rrsig.rrs.count] = KNOT_RRTYPE_RRSIG; - if (lbcnt > rrsigcnt && rrsigcnt > 0 && !knot_dname_is_wildcard(n->owner)) { + if (nlabels > rrsig_nlabels && rrsig_nlabels > 0 && !knot_dname_is_wildcard(n->owner)) { knot_dname_t wcbuf[knot_dname_size(n->owner)]; - knot_dname_t *wc = knot_dname_wildcard(knot_dname_next_labels(n->owner, lbcnt - rrsigcnt), wcbuf, sizeof(wcbuf)); + const knot_dname_t *stripped = knot_dname_next_labels(n->owner, nlabels - rrsig_nlabels); + knot_dname_t *wc = knot_dname_wildcard(stripped, wcbuf, sizeof(wcbuf)); assert(wc != NULL); for (int i = 0; i < rrsig.rrs.count + 1 && ret == KNOT_EOK; i++) { ret = move_rrset(conts, n, types[i], wc); } } - zone_tree_delsafe_it_next(&it); } zone_tree_delsafe_it_free(&it); if (ret == KNOT_EOK) { - ret = zone_adjust_contents(conts, adjust_cb_flags, adjust_cb_nsec3_flags, false, true, false, 1, NULL); + ret = zone_adjust_contents(conts, adjust_cb_flags, adjust_cb_nsec3_flags, false, + true, false, 1, NULL); } if (ret == KNOT_EOK) { ret = zone_tree_apply(conts->nodes, restore_orig_ttls, NULL); @@ -626,22 +680,22 @@ static int dv(knot_pkt_t *pkt, kdig_dnssec_ctx_t **dv_ctx, // NOTE at this point we have complete "contents" filled with the answer, DNSKEY and their RRSIGs knot_rcode_t expected_rcode = KNOT_RCODE_NOERROR; - tmp_ctx_t tmp = { .conts = conts, .level = level }; + tree_cb_ctx_t cb_ctx = { .conts = conts, .level = loglevel }; // check NSEC3 tree consistence - ret = zone_tree_apply(conts->nsec3_nodes, check_nsec3, &tmp); + ret = zone_tree_apply(conts->nsec3_nodes, check_nsec3, &cb_ctx); if (ret != KNOT_EOK) { // also '1' return ret; } // check the NSEC(3) proofs relevant for the queried name - ret = check_name(*dv_ctx, (*dv_ctx)->orig_qname, (*dv_ctx)->orig_qtype, level, &expected_rcode); + ret = check_name(*dv_ctx, (*dv_ctx)->orig_qname, (*dv_ctx)->orig_qtype, loglevel, &expected_rcode); if (ret != KNOT_EOK) { // also '1' return ret; } // check that any NSEC does not prove non-existence of anything existing - ret = zone_tree_apply(conts->nodes, check_existing_with_nsecs, &tmp); + ret = zone_tree_apply(conts->nodes, check_existing_with_nsecs, &cb_ctx); if (ret != KNOT_EOK) { // also '1' return ret; } @@ -659,28 +713,28 @@ static int dv(knot_pkt_t *pkt, kdig_dnssec_ctx_t **dv_ctx, if (ret == KNOT_DNSSEC_ENOSIG) { char type_txt[16] = { 0 }; (void)knot_rrtype_to_string(fake_up.validation_hint.rrtype, type_txt, sizeof(type_txt)); - LOG_ERROR(level, fake_up.validation_hint.node, "invalid or missing RRSIG for %s", type_txt); + LOG_ERROR(loglevel, fake_up.validation_hint.node, "invalid or missing RRSIG for %s", type_txt); return 1; } // check RCODE if (expected_rcode != (*dv_ctx)->orig_rcode) { const knot_lookup_t *item = knot_lookup_by_id(knot_rcode_names, expected_rcode); - LOG_ERROR(level, NULL, "expected RCODE was: %s", item->name); + LOG_ERROR(loglevel, NULL, "expected RCODE was: %s", item->name); return 1; } else { - LOG_INF(level, NULL, "correct RCODE found"); + LOG_INF(loglevel, NULL, "correct RCODE found"); } return ret; } -int kdig_dnssec_validate(knot_pkt_t *pkt, struct kdig_dnssec_ctx **dv_ctx, +int kdig_dnssec_validate(knot_pkt_t *pkt, kdig_dnssec_ctx_t **dv_ctx, kdig_validation_log_level_t level, knot_dname_t zone_name[KNOT_DNAME_MAXLEN], uint16_t *type_needed) { char type_txt[16] = { 0 }; - int ret = dv(pkt, dv_ctx, level, zone_name, type_needed); + int ret = dnssec_validate(pkt, dv_ctx, level, zone_name, type_needed); if (ret == 1) { LOG_OUTCOME(level, NULL, "NOK!"); ret = KNOT_EOK; -- GitLab From 951ac5b550fd58772f33af591bd09730f5f9bcac Mon Sep 17 00:00:00 2001 From: Libor Peltan <libor.peltan@nic.cz> Date: Thu, 10 Apr 2025 13:01:17 +0200 Subject: [PATCH 8/9] kdig/validation: fix empty-non-terminal wildcard match in NSEC zone... ...for example foo.not-star.phicoh.nl. matches empty-non-terminal wildcard --- src/utils/kdig/dnssec_validation.c | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/utils/kdig/dnssec_validation.c b/src/utils/kdig/dnssec_validation.c index 9100e1a56d..ba217179b1 100644 --- a/src/utils/kdig/dnssec_validation.c +++ b/src/utils/kdig/dnssec_validation.c @@ -202,11 +202,18 @@ static bool has_nodata(zone_contents_t *conts, const knot_dname_t *name, uint16_ const zone_node_t *from_node, const knot_dname_t **where) { if (!has_nsec3(conts)) { - const zone_node_t *node = zone_contents_find_node(conts, name); - knot_rrset_t nsec = node_rrset(node, KNOT_RRTYPE_NSEC); + const zone_node_t *node = zone_contents_find_node(conts, name), *prev = node; + while (prev->rrset_count == 0) { + prev = node_prev(prev); + } + knot_rrset_t nsec = node_rrset(prev, KNOT_RRTYPE_NSEC); if (where != NULL) { *where = nsec.owner; } + if (prev != node) { // seeking empty non-terminal proof + return !knot_rrset_empty(&nsec) + && nsec_covers_name(prev->owner, nsec.rrs.rdata, name); + } return !knot_rrset_empty(&nsec) && bitmap_covers(knot_nsec_bitmap(nsec.rrs.rdata), knot_nsec_bitmap_len(nsec.rrs.rdata), type, from_node); @@ -329,7 +336,7 @@ static int check_existing_with_nsecs(zone_node_t *node, void *data) } return 1; } - } else if (!(node->flags & NODE_FLAGS_NONAUTH)) { + } else if (!(node->flags & NODE_FLAGS_NONAUTH) && node->rrset_count > 0) { if (has_nodata(ctx->conts, node->owner, 0, node, NULL)) { LOG_ERROR(ctx->level, node->owner, "NSEC(3) wrongly proves NODATA"); return 1; @@ -366,6 +373,11 @@ int remove_cnames(zone_node_t *node, void *data) return KNOT_EOK; } +static zone_node_t *new_node_cb(const knot_dname_t *dname, void *ctx) +{ + return node_new_for_tree(dname, ctx, NULL); +} + static int rrsets_pkt2conts(knot_pkt_t *pkt, zone_contents_t *conts, knot_section_t limit, uint16_t type_only, kdig_validation_log_level_t level) @@ -403,6 +415,10 @@ static int rrsets_pkt2conts(knot_pkt_t *pkt, zone_contents_t *conts, LOG_INF(level, rr->owner, "WARNING: mismatched TTLs for type %s", rrtype); ret = KNOT_EOK; } + + if (rrcpy.type == KNOT_RRTYPE_NSEC && ret == KNOT_EOK) { + ret = zone_tree_add_node(conts->nodes, conts->apex, knot_nsec_next(rr->rrs.rdata), new_node_cb, conts->nodes, &inserted); + } } } return ret; @@ -534,7 +550,7 @@ static int check_name(kdig_dnssec_ctx_t *ctx, const knot_dname_t *name, return check_cname(ctx, knot_cname_name(cn->rdata), type, level, expected_rcode); } else if (!node_rrtype_exists(match, type)) { if (has_nodata(ctx->conts, match->owner, type, NULL, &where)) { - LOG_INF(level, match->owner, "NSEC NODATA proof found"); + LOG_INF(level, where, "NSEC NODATA proof found"); } else { LOG_ERROR(level, match->owner, "NODATA proof missing"); return 1; -- GitLab From 89a481ccb3aad482503e3c0351d78dde6c085b82 Mon Sep 17 00:00:00 2001 From: Libor Peltan <libor.peltan@nic.cz> Date: Thu, 10 Apr 2025 13:30:46 +0200 Subject: [PATCH 9/9] kdig/validation: ignore out-of-bailiwick junk in AUTHORITY --- src/utils/kdig/dnssec_validation.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/utils/kdig/dnssec_validation.c b/src/utils/kdig/dnssec_validation.c index ba217179b1..0096a02633 100644 --- a/src/utils/kdig/dnssec_validation.c +++ b/src/utils/kdig/dnssec_validation.c @@ -396,6 +396,10 @@ static int rrsets_pkt2conts(knot_pkt_t *pkt, zone_contents_t *conts, continue; } + if (i > KNOT_ANSWER && knot_dname_in_bailiwick(rr->owner, conts->apex->owner) < 0) { + continue; + } + uint16_t rr_pos = knot_pkt_rr_offset(&pkt->sections[i], j); knot_dname_storage_t owner; knot_dname_unpack(owner, pkt->wire + rr_pos, sizeof(owner), pkt->wire); -- GitLab