]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
kdig: implemented DNSSEC validation (+validate)
authorLibor Peltan <libor.peltan@nic.cz>
Fri, 21 Feb 2025 19:51:31 +0000 (20:51 +0100)
committerLibor Peltan <libor.peltan@nic.cz>
Thu, 16 Apr 2026 10:20:09 +0000 (12:20 +0200)
21 files changed:
Knot.files
configure.ac
doc/man_kdig.rst
src/knot/dnssec/nsec-chain.c
src/knot/dnssec/nsec3-chain.c
src/knot/dnssec/zone-events.c
src/knot/updates/zone-update.c
src/knot/zone/adjust.c
src/knot/zone/adjust.h
src/knot/zone/node.c
src/knot/zone/node.h
src/knot/zone/zonefile.c
src/utils/Makefile.inc
src/utils/kdig/dnssec_validation.c [new file with mode: 0644]
src/utils/kdig/dnssec_validation.h [new file with mode: 0644]
src/utils/kdig/kdig_exec.c
src/utils/kdig/kdig_params.c
src/utils/kdig/kdig_params.h
tests/.gitignore
tests/Makefile.am
tests/utils/test_kdig_validate.in [new file with mode: 0644]

index 9410df885998760f45e5121f97a294b18f5d35a3..675b8bf29b5107c9d95cef3ff2435bcf778c8dd4 100644 (file)
@@ -615,6 +615,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
index 1514966d832808aecc38c6e8f2738530018df785..9a707467e73886304bb2ee36041fbae075fb4bff 100644 (file)
@@ -629,6 +629,11 @@ AC_ARG_WITH(libnghttp2,
   with_libnghttp2=yes
 )
 
+dnl DNSSEC validation in kdig
+AC_ARG_ENABLE([kdig_validation],
+  AS_HELP_STRING([--enable-kdig-validation=yes|no], [DNSSEC validation in kdig [default=yes]]),
+    [enable_kdig_validation="$enableval"], [enable_kdig_validation=yes])
+
 AS_IF([test "$enable_utilities" = "yes"], [
   AS_IF([test "$with_libidn" != "no"], [
     PKG_CHECK_MODULES([libidn2], [libidn2 >= 2.0.0], [
@@ -650,6 +655,12 @@ AS_IF([test "$enable_utilities" = "yes"], [
     ])
   ])
 
+  AS_IF([test "$enable_daemon" = "no"], [enable_kdig_validation=no])
+  AS_IF([test "$enable_kdig_validation" != "no"], [
+    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_xdp" != "no"], [
     PKG_CHECK_MODULES([libmnl], [libmnl], [], [
       AC_MSG_ERROR([libmnl not found])
@@ -824,6 +835,7 @@ result_msg_base="
     Utilities with IDN:     ${with_libidn}
     Utilities with DoH:     ${with_libnghttp2}
     Utilities with Dnstap:  ${enable_dnstap}
+    Kdig DNSSEC validation: ${enable_kdig_validation}
     MaxMind DB support:     ${enable_maxminddb}
     Systemd integration:    ${enable_systemd}
     D-Bus support:          ${enable_dbus}
index f21e2871c36fa2bca5d8d89dafe0bf9ae4ed00d6..a73009903475122c161cd7d171501f4291cb3baa 100644 (file)
@@ -177,6 +177,14 @@ Options
 **+**\ [\ **no**\ ]\ **dnssec**
   Same as **+**\ [\ **no**\ ]\ **doflag**
 
+**+**\ [\ **no**\ ]\ **validate**\[\ =\ *LEVEL*\]
+  Also query for DNSKEY, validate DNSSEC in the answer. Implies DO flag.
+  Optional argument specifies verbosity (1-3, default 3).
+
+  NOTICE: this is not a security feature, rather a debugging tool. Kdig
+  doesn't attempt to obtain nor validate the whole trust chain, therefore
+  an attacker can in theory spoof everything and Kdig concludes "valid".
+
 **+**\ [\ **no**\ ]\ **all**
   Show all packet sections.
 
index aeb1279fb28181ee2429401e17657eda5915a608..09567c3c4e3f26159b3f890725a9bc8d0260bf91 100644 (file)
@@ -734,7 +734,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;
        }
index 621cc7eaea135160e2206421928c7068049bb45f..41ddd44dedc932bfc1398ae8ec8f209f70d44a38 100644 (file)
@@ -665,7 +665,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;
        }
index 059fa1ef4bd6173a9774e4a6db97f2a65c5d0dde..32e528ea243dca5e22926ca4fb9b2e438b009c7b 100644 (file)
@@ -230,7 +230,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;
        }
@@ -367,7 +367,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;
        }
index 8476bed1ad38d325268b53f46a3642d417de2cda..ecaafcd758265c4a6f57f10b4716b3ebc01e2e84 100644 (file)
@@ -983,7 +983,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;
        }
index d9208bf0e6ad4edb6f64d129253638481ae2a744..a11e5a37186802584349c02dec616a415aadbb3b 100644 (file)
@@ -467,10 +467,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)",
@@ -538,10 +538,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);
@@ -578,11 +578,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);
index 779087e313e103517f28aea40a91cdc74b787b2b..2f76d4355f9911fbc5084713720df9075a85e212 100644 (file)
@@ -63,13 +63,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);
 
 /*!
index 4ce1433a28bfed5ca31b9470215d84a5be160daa..87f683c4c385ea7138d23be1add4c762ff57c7d8 100644 (file)
@@ -434,6 +434,21 @@ bool node_rrtype_is_signed(const zone_node_t *node, uint16_t type)
        return false;
 }
 
+void node_set_ttl(zone_node_t *node, uint16_t type, uint32_t ttl)
+{
+       if (node == NULL) {
+               return;
+       }
+
+       int remain = node->rrset_count;
+       while (--remain >= 0) {
+               if (node->rrs[remain].type == type) {
+                       node->rrs[remain].ttl = ttl;
+                       break;
+               }
+       }
+}
+
 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) {
index 3f5071aeb8cadadc999650fb996350c8d55a1759..52679986dbd5b8f216b0ac5509296f78cf44d447 100644 (file)
@@ -372,6 +372,15 @@ 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.
+ */
+void 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.
index eec9b64c068df01468330636fff64fcf16ee5b2f..5d96f44c5edf2132fb3de2924e57d56f923e1cf5 100644 (file)
@@ -248,7 +248,7 @@ zone_contents_t *zonefile_load(zloader_t *loader, uint16_t threads)
        }
 
        ret = zone_adjust_contents(loader->contents, adjust_cb_flags_and_nsec3,
-                                  adjust_cb_nsec3_flags, true, true, 1, NULL);
+                                  adjust_cb_nsec3_flags, true, true, true, 1, NULL);
        if (ret != KNOT_EOK) {
                ERROR(zname, "failed to finalize zone contents (%s)", knot_strerror(ret));
                goto fail;
@@ -265,7 +265,7 @@ zone_contents_t *zonefile_load(zloader_t *loader, uint16_t threads)
        /* The contents will now change possibly messing up NSEC3 tree, it will
           be adjusted again at zone_update_commit. */
        ret = zone_adjust_contents(loader->contents, unadjust_cb_point_to_nsec3,
-                                  NULL, false, false, 1, NULL);
+                                  NULL, false, false, true, 1, NULL);
        if (ret != KNOT_EOK) {
                ERROR(zname, "failed to finalize zone contents (%s)", knot_strerror(ret));
                goto fail;
index 4d180a42501c042275382c1853e913376a021617..63035034a757fc34e34f73bed956a2e19e44a037 100644 (file)
@@ -7,8 +7,7 @@ noinst_LTLIBRARIES += libknotus.la
 libknotus_la_CPPFLAGS = $(embedded_libngtcp2_CFLAGS) \
                         $(AM_CPPFLAGS) $(CFLAG_VISIBILITY) $(gnutls_CFLAGS) \
                         $(libedit_CFLAGS) $(libidn2_CFLAGS) $(libidn_CFLAGS) \
-                        $(libkqueue_CFLAGS) $(libnghttp2_CFLAGS) $(libngtcp2_CFLAGS) \
-                        $(lmdb_CFLAGS)
+                        $(libkqueue_CFLAGS) $(libnghttp2_CFLAGS) $(libngtcp2_CFLAGS)
 libknotus_la_LDFLAGS  = $(AM_LDFLAGS) $(LDFLAG_EXCLUDE_LIBS)
 libknotus_la_LIBADD   = $(libidn2_LIBS) $(libidn_LIBS) $(libnghttp2_LIBS) $(libngtcp2_LIBS)
 libknotus_LIBS        = libknotus.la libknot.la $(libcontrib_LIBS) \
@@ -59,6 +58,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                  \
@@ -81,8 +86,9 @@ knsupdate_SOURCES = \
        utils/knsupdate/knsupdate_params.h
 
 kdig_CPPFLAGS          = $(libknotus_la_CPPFLAGS)
-kdig_LDADD             = $(libknotus_LIBS)
-khost_CPPFLAGS         = $(libknotus_la_CPPFLAGS)
+kdig_LDADD             = $(libknotd_LIBS) $(libknotus_LIBS)
+kdig_LDFLAGS           = $(AM_LDFLAGS)
+khost_CPPFLAGS         = $(libknotus_la_CPPFLAGS) -DNO_DNSSEC_VALIDATION
 khost_LDADD            = $(libknotus_LIBS)
 knsec3hash_CPPFLAGS    = $(libknotus_la_CPPFLAGS)
 knsec3hash_LDADD       = libknot.la $(libcontrib_LIBS)
diff --git a/src/utils/kdig/dnssec_validation.c b/src/utils/kdig/dnssec_validation.c
new file mode 100644 (file)
index 0000000..4b62b24
--- /dev/null
@@ -0,0 +1,767 @@
+/*  Copyright (C) CZ.NIC, z.s.p.o. and contributors
+ *  SPDX-License-Identifier: GPL-2.0-or-later
+ *  For more information, see <https://www.knot-dns.cz/>
+ */
+
+#include <string.h>
+
+#include "utils/kdig/dnssec_validation.h"
+#include "knot/dnssec/zone-nsec.h"
+#include "knot/dnssec/zone-sign.h"
+#include "knot/zone/adjust.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;
+} tree_cb_ctx_t;
+
+static void kdv_log(kdig_validation_log_level_t log_level, kdig_validation_log_level_t set_level,
+                    const char *prefix, const knot_dname_t *at, const char *msg, ...)
+{
+       if (set_level >= log_level) {
+               fprintf(stdout, ";; %s: ", prefix);
+               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, "OUTCOME", at, msg, ##__VA_ARGS__)
+#define LOG_ERROR(level, at, msg, ...)   kdv_log(KDIG_VALIDATION_LOG_ERRORS,    level, "ERROR",   at, msg, ##__VA_ARGS__)
+#define LOG_FOUND(level, at, msg, ...)   kdv_log(KDIG_VALIDATION_LOG_INFOS,     level, "FOUND",   at, msg, ##__VA_ARGS__)
+#define LOG_INF(level, at, msg, ...)     kdv_log(KDIG_VALIDATION_LOG_INFOS,     level, "INFO",    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 int dname_wildcard(const knot_dname_t *from, knot_dname_t *dest, size_t dest_len)
+{
+       size_t from_size = knot_dname_size(from);
+       if (from_size + 2 > dest_len) {
+               return KNOT_ERANGE;
+       }
+       memcpy(dest, "\x01*", 2);
+       memcpy(dest + 2, from, from_size);
+       return KNOT_EOK;
+}
+
+static const knot_dname_t *dname_next_labels(const knot_dname_t *name, unsigned nlabels)
+{
+       return name + knot_dname_prefixlen(name, nlabels);
+}
+
+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)
+{
+       tree_cb_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_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++) {
+               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, _unused_ 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++) {
+                       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_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_nlabels > apex_nlabels; name_nlabels--) {
+               if (has_nodata(conts, *encloser, 0, NULL, &enc_where) ||
+                   // 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_nlabels <= apex_nlabels) {
+               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_FOUND(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)
+{
+       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);
+               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;
+                       }
+
+                       if (i > KNOT_ANSWER && knot_dname_in_bailiwick(rrcpy.owner, conts->apex->owner) < 0) {
+                               continue;
+                       }
+
+                       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, "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;
+       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;
+       }
+       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_FOUND(level, closest->owner, "secure delegation, DS found");
+                       return KNOT_EOK;
+               } else if (has_nodata(ctx->conts, closest->owner, KNOT_RRTYPE_DS, NULL, &where)) {
+                       LOG_FOUND(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_FOUND(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_FOUND(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_FOUND(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_FOUND(level, where, "wildcard non-existence proven");
+                       } else {
+                               LOG_FOUND(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_storage_t wc;
+                       ret = dname_wildcard(encloser, wc, sizeof(wc));
+                       if (ret != KNOT_EOK) {
+                               return ret;
+                       }
+                       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_FOUND(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_FOUND(level, where, "NSEC NODATA proof found");
+               } else {
+                       LOG_ERROR(level, match->owner, "NODATA proof missing");
+                       return 1;
+               }
+       } else {
+               LOG_FOUND(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 (%d) of CNAME/DNAME chain reached, giving up", CNAME_LIMIT);
+               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) {
+               // no need to free anything, sorted by kdig_dnssec_free in any case
+               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 ret;
+               }
+       }
+
+       return KNOT_EOK;
+}
+
+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 || 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, loglevel);
+               if (ret != KNOT_EOK) {
+                       return ret;
+               } 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(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, loglevel);
+       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);
+               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 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, &types[0], &rrsig_nlabels);
+               types[rrsig.rrs.count] = KNOT_RRTYPE_RRSIG;
+               if (nlabels > rrsig_nlabels && rrsig_nlabels > 0 && !knot_dname_is_wildcard(n->owner)) {
+                       knot_dname_storage_t wc;
+                       const knot_dname_t *stripped = dname_next_labels(n->owner, nlabels - rrsig_nlabels);
+                       ret = dname_wildcard(stripped, wc, sizeof(wc));
+                       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, DNSKEY and their RRSIGs
+
+       knot_rcode_t expected_rcode = KNOT_RCODE_NOERROR;
+       tree_cb_ctx_t cb_ctx = { .conts = conts, .level = loglevel };
+
+       // check NSEC3 tree consistence
+       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, 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, &cb_ctx);
+       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, 1);
+       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(loglevel, fake_up.validation_hint.node, "invalid or missing RRSIG for %s", type_txt);
+               return 1;
+       } else if (ret == KNOT_EOK) {
+               LOG_FOUND(loglevel, NULL, "all RRSIGs present and valid");
+       }
+
+       // 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(loglevel, NULL, "expected RCODE was: %s", item->name);
+               return 1;
+       } else {
+               LOG_FOUND(loglevel, NULL, "correct RCODE found");
+       }
+
+       return ret;
+}
+
+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)
+{
+       int ret = dnssec_validate(pkt, dv_ctx, level, zone_name, type_needed);
+       if (ret == 1) {
+               LOG_OUTCOME(level, NULL, "Invalid!");
+               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, "Invalid!");
+               ret = KNOT_EOK;
+       } else if (ret == KNOT_EOK) {
+               LOG_OUTCOME(level, NULL, "Valid!");
+       }
+
+       if (ret == KNOT_EAGAIN) {
+               char type_txt[16] = { 0 };
+               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 {
+               kdig_dnssec_free(*dv_ctx);
+               *dv_ctx = NULL;
+       }
+       return ret;
+}
+
+void kdig_dnssec_free(kdig_dnssec_ctx_t *dv_ctx)
+{
+       if (dv_ctx != NULL) {
+               zone_contents_deep_free(dv_ctx->conts);
+               free(dv_ctx->orig_qname);
+               free(dv_ctx);
+       }
+}
diff --git a/src/utils/kdig/dnssec_validation.h b/src/utils/kdig/dnssec_validation.h
new file mode 100644 (file)
index 0000000..7573e84
--- /dev/null
@@ -0,0 +1,41 @@
+/*  Copyright (C) CZ.NIC, z.s.p.o. and contributors
+ *  SPDX-License-Identifier: GPL-2.0-or-later
+ *  For more information, see <https://www.knot-dns.cz/>
+ */
+
+#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);
+
+/*!
+ * \brief Free DNSSEC validation context.
+ *
+ * \param dv_ctx         Context structure to free.
+ */
+void kdig_dnssec_free(struct kdig_dnssec_ctx *dv_ctx);
index c0f46d07936752212670bc5952c5d2aef8a0ad9e..141d4d0f0fee31c80ee0b8001f929f5e16e160b9 100644 (file)
@@ -8,6 +8,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"
@@ -798,6 +799,55 @@ static int process_query_packet(const knot_pkt_t      *query,
                return ret;
        }
 
+       if (query_ctx->dnssec_validation > 0) {
+#if defined(HAVE_KDIG_VALIDATION) && !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;
+
+               if (dv_ctx == NULL) {
+                       printf("\n;; DNSSEC VALIDATION:\n");
+               }
+
+               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
+                       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) {
+                               kdig_dnssec_free(dv_ctx);
+                               net_close_keepopen(net, query_ctx);
+                               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);
+                       if (new_query == NULL) {
+                               free(new_ctx.owner);
+                               kdig_dnssec_free(dv_ctx);
+                               net_close_keepopen(net, query_ctx);
+                               return KNOT_ENOMEM;
+                       }
+                       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));
+               }
+#endif // HAVE_KDIG_VALIDATION && !NO_DNSSEC_VALIDATION
+       }
+
        knot_pkt_free(reply);
        net_close_keepopen(net, query_ctx);
 
index 742b28af99950c6f67cf79471581dad992cb9d83..feacd280c32a006aeec48a0064136fb194eba85a 100644 (file)
@@ -275,6 +275,37 @@ static int opt_nodoflag(const char *arg, void *query)
        return KNOT_EOK;
 }
 
+static int opt_validate(const char *arg, void *query)
+{
+#if defined(HAVE_KDIG_VALIDATION) && !defined(NO_DNSSEC_VALIDATION)
+       query_t *q = query;
+
+       q->dnssec_validation = 3;
+       if (arg != NULL) {
+               if (!is_digit(arg[0]) || arg[0] < '1' || arg[0] > '3') {
+                       ERR("invalid +validation=%s", arg);
+                       return KNOT_EINVAL;
+               }
+               q->dnssec_validation = arg[0] - '0';
+       }
+       q->flags.do_flag = true;
+
+       return KNOT_EOK;
+#else
+       ERR("DNSSEC validation support not compiled");
+       return KNOT_ENOTSUP;
+#endif // HAVE_KDIG_VALIDATION && !NO_DNSSEC_VALIDATION
+}
+
+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;
@@ -1550,6 +1581,9 @@ static const param_t kdig_opts2[] = {
        { "dnssec",         ARG_NONE,     opt_doflag },   // Alias.
        { "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 },
 
@@ -2377,6 +2411,7 @@ static void print_help(void)
               "       +[no]cdflag                Set CD flag.\n"
               "       +[no]doflag                Set DO flag.\n"
               "       +[no]dnssec                Same as +[no]doflag.\n"
+              "       +[no]validate[=LEVEL]      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"
index 869ae12e7dda22a95caa47f0c7173d78f6ebeda3..0fb54210a962a4842c327b759fb70dd05e9c4e0e 100644 (file)
@@ -130,6 +130,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;
index a4a59795fb3aafe8ee64c9775eaa804feb16f287..abb5933545db933c8a02994bd280fcd24ac25d2c 100644 (file)
@@ -97,3 +97,4 @@
 /modules/test_rrl
 
 /utils/test_lookup
+/utils/test_kdig_validate
index fc506fd4d9f057e9c0a84e62513897b12372e017..f93de3c9d10f2ab816ec729d2654c8e152743754 100644 (file)
@@ -30,7 +30,8 @@ EXTRA_DIST = \
        knot/test_semantic_check.in             \
        libzscanner/data                        \
        libzscanner/test_zscanner.in            \
-       libzscanner/TESTS
+       libzscanner/TESTS                       \
+       utils/test_kdig_validate.in
 
 check_LTLIBRARIES = libtap.la
 
@@ -45,6 +46,8 @@ libtap_la_SOURCES = \
 
 EXTRA_PROGRAMS = tap/runtests
 
+check_SCRIPTS =
+
 check_PROGRAMS = \
        contrib/test_base32hex                  \
        contrib/test_base64                     \
@@ -164,6 +167,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
@@ -220,7 +233,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 100644 (file)
index 0000000..120825d
--- /dev/null
@@ -0,0 +1,126 @@
+#!/bin/sh
+# Copyright (C) CZ.NIC, z.s.p.o. and contributors
+# SPDX-License-Identifier: GPL-2.0-or-later
+# For more information, see <https://www.knot-dns.cz/>
+
+BUILDROOT="@top_builddir@"
+SRCROOT="@top_srcdir@"
+
+. "@top_srcdir@/tests/tap/libtap.sh"
+
+TMPDIR=$(cd $(test_tmpdir) && pwd)
+if [ -f /etc/crypto-policies/config ] || [ ${#TMPDIR} -gt 85 ]; then
+    diag "Test not compatible with strict crypto policy or too long unix socket paths"
+    skip_all
+    exit 0
+fi
+
+SCN=$TMPDIR/scenario.txt
+CONF=$TMPDIR/knot.conf
+RUNDIR=$TMPDIR/run
+LISTEN=$RUNDIR/listen
+LISTEN_CTL=$RUNDIR/ctl
+VALGRIND=
+
+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.*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
+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:
+    rundir: $RUNDIR
+    listen: $LISTEN
+    tcp-workers: 1
+    udp-workers: 1
+    background-workers: 1
+control:
+    listen: $LISTEN_CTL
+database:
+    storage: $RUNDIR
+    timer-db-sync: never
+zone:
+  - domain: example.com.
+    storage: $RUNDIR
+    file: example.com.zone
+log:
+  - target: stdout
+    any: debug
+EOF
+
+plan $(( $(cat "$SCN" | wc -l) * 4 ))
+
+q() {
+    QN="$2"
+    OUTCOME=$(echo "$4" | sed 's/NOK/Invalid/;s/OK/Valid/;s/FAILED/VALIDATION failed to proceed/')
+    case "$QN" in
+        *.) ;;
+        *) QN="$QN.example.com." ;;
+    esac
+    CMD="$VALGRIND $BUILDROOT/src/kdig @$LISTEN +tcp +validate +nocrypto $QN -t $3"
+    echo "$1 $CMD" >&2
+    RESP=$(sh -c "$CMD" 2>&1)
+    echo "$RESP" >&2
+    echo "$RESP" | grep -q "$OUTCOME"
+    ok "$1 outcome '$OUTCOME'" test $? -eq 0
+    echo "$RESP" | grep -q "$5"
+    ok "$1 point '$5'" 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
+    sleep 0.02
+    continue
+done
+
+i=0
+while read ZFILE QNAME QTYPE OUT POINT QNAME2 QTYPE2 OUT2 POINT2; do
+    i=$((i+1))
+    if [ -n "$1" ] && [ "$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 $LISTEN_CTL -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