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