diff --git a/Makefile b/Makefile
index 35f78d361ffd07eb250fea4637e37bb64bed736d..b93e1a1ce03ae2df4aa04737ce4f250e110656cc 100644
--- a/Makefile
+++ b/Makefile
@@ -29,6 +29,7 @@ $(eval $(call find_bin,python))
 $(eval $(call find_lib,libmemcached,1.0))
 $(eval $(call find_lib,hiredis))
 $(eval $(call find_lib,socket_wrapper))
+$(eval $(call find_lib,libdnssec))
 
 # Work around luajit on OS X
 ifeq ($(PLATFORM), Darwin)
@@ -37,7 +38,7 @@ ifneq (,$(findstring luajit, $(lua_LIBS)))
 endif
 endif
 
-CFLAGS += $(libknot_CFLAGS) $(libuv_CFLAGS) $(cmocka_CFLAGS) $(python_CFLAGS) $(lua_CFLAGS)
+CFLAGS += $(libknot_CFLAGS) $(libuv_CFLAGS) $(cmocka_CFLAGS) $(python_CFLAGS) $(lua_CFLAGS) $(libdnssec_CFLAGS)
 
 # Sub-targets
 include help.mk
diff --git a/daemon/README.rst b/daemon/README.rst
index c9a0309e82e7883827ce5614d77df81c3d9def31..24c0c8b8881c1faf0ad92a22e34ff5828292958e 100644
--- a/daemon/README.rst
+++ b/daemon/README.rst
@@ -3,51 +3,85 @@
 Knot DNS Resolver daemon 
 ************************
 
-Requirements
-============
+The server is in the `daemon` directory, it works out of the box without any configuration.
 
-* libuv_ 1.0+ (a multi-platform support library with a focus on asynchronous I/O)
-* Lua_ 5.1+ (embeddable scripting language, LuaJIT_ is preferred)
+.. code-block:: bash
+
+   $ kresd -h # Get help
+   $ kresd -a ::1
 
-Running
-=======
+Enabling DNSSEC
+===============
 
-There is a separate resolver library in the `lib` directory, and a minimalistic daemon in
-the `daemon` directory.
+The resolver supports DNSSEC including :rfc:`5011` automated DNSSEC TA updates and :rfc:`7646` negative trust anchors.
+To enable it, you need to provide at least _one_ trust anchor. This step is not automatic, as you're supposed to obtain
+the trust anchor `using a secure channel <http://jpmens.net/2015/01/21/opendnssec-rfc-5011-bind-and-unbound/>`_.
+From there, the Knot DNS Resolver can perform automatic updates for you.
+
+1. Check the current TA published on `IANA website <https://data.iana.org/root-anchors/root-anchors.xml>`_
+2. Fetch current keys, verify
+3. Deploy them
 
 .. code-block:: bash
 
-	$ ./daemon/kresd -h
+   $ kdig DNSKEY . @a.root-servers.net +noall +answer | grep 257 > root.keys
+   $ ldns-key2ds -n root.keys
+   ... verify that digest matches TA published by IANA ...
+   $ kresd -k root.keys
 
-Interacting with the daemon
----------------------------
+You've just enabled DNSSEC!
 
-The daemon features a CLI interface if launched interactively, type ``help`` to see the list of available commands.
-You can load modules this way and use their properties to get information about statistics and such.
+CLI interface
+=============
+
+The daemon features a CLI interface, type ``help`` to see the list of available commands.
 
 .. code-block:: bash
 
-	$ kresd /var/run/knot-resolver
-	[system] started in interactive mode, type 'help()'
-	> cache.count()
-	53
+   $ kresd /var/run/knot-resolver
+   [system] started in interactive mode, type 'help()'
+   > cache.count()
+   53
 
 .. role:: lua(code)
    :language: lua
 
-Running in forked mode
-----------------------
+Verbose output
+--------------
+
+If the debug logging is compiled in, you can turn on verbose tracing of server operation with the ``-v`` option.
+You can also toggle it on runtime with ``verbose(true|false)`` command.
+
+.. code-block:: bash
+
+   $ kresd -v
+
+Scaling out
+===========
 
 The server can clone itself into multiple processes upon startup, this enables you to scale it on multiple cores.
+Multiple processes can serve different addresses, but still share the same working directory and cache.
+You can add start and stop processes on runtime based on the load.
 
 .. code-block:: bash
 
-	$ kresd -f 2 rundir > kresd.log
+	$ kresd -f 4 rundir > kresd.log &
+   $ kresd -f 2 rundir > kresd_2.log & # Extra instances
+   $ pstree $$ -g
+   bash(3533)─┬─kresd(19212)─┬─kresd(19212)
+              │              ├─kresd(19212)
+              │              └─kresd(19212)
+              ├─kresd(19399)───kresd(19399)
+              └─pstree(19411)
+   $ kill 19399 # Kill group 2, former will continue to run
+   bash(3533)─┬─kresd(19212)─┬─kresd(19212)
+              │              ├─kresd(19212)
+              │              └─kresd(19212)
+              └─pstree(19460)  
 
 .. note:: On recent Linux supporting ``SO_REUSEPORT`` (since 3.9, backported to RHEL 2.6.32) it is also able to bind to the same endpoint and distribute the load between the forked processes. If the kernel doesn't support it, you can still fork multiple processes on different ports, and do load balancing externally (on firewall or with `dnsdist <http://dnsdist.org/>`_).
 
-
-Notice it isn't interactive, but you can attach to the the consoles for each process, they are in ``rundir/tty/PID``.
+Notice the absence of an interactive CLI. You can attach to the the consoles for each process, they are in ``rundir/tty/PID``.
 
 .. code-block:: bash
 
@@ -77,11 +111,8 @@ comfortable in the current working directory.
 
 And you're good to go for most use cases! If you want to use modules or configure daemon behavior, read on.
 
-There are several choices on how you can configure the daemon, a RPC interface a CLI and a configuration file.
-Fortunately all share common syntax and are transparent to each other, e.g. changes made during the runtime are kept
-in the redo log and are immediately visible.
-
-.. warning:: Redo log is not yet implemented, changes are visible during the process lifetime only.
+There are several choices on how you can configure the daemon, a RPC interface, a CLI, and a configuration file.
+Fortunately all share common syntax and are transparent to each other.
 
 Configuration example
 ---------------------
@@ -90,9 +121,9 @@ Configuration example
 	-- 10MB cache
 	cache.size = 10*MB
 	-- load some modules
-	modules = { 'hints', 'cachectl' }
+	modules = { 'policy', 'cachectl' }
 	-- interfaces
-	net = { '127.0.0.1' }
+	net = { '127.0.0.1', '::1' }
 
 Configuration syntax
 --------------------
@@ -123,8 +154,6 @@ the modules use as the :ref:`input configuration <mod-properties>`.
 		}
 	}
 
-The possible simple data types are: string, integer or float, and boolean.
-
 .. tip:: The configuration and CLI syntax is Lua language, with which you may already be familiar with.
          If not, you can read the `Learn Lua in 15 minutes`_ for a syntax overview. Spending just a few minutes
          will allow you to break from static configuration, write more efficient configuration with iteration, and
@@ -170,7 +199,7 @@ to download cache from parent, to avoid cold-cache start.
 			sink = ltn12.sink.file(io.open('cache.mdb', 'w'))
 		}
 		-- reopen cache with 100M limit
-		cache.open(100*MB)
+		cache.size = 100*MB
 	end
 
 Events and services
@@ -241,6 +270,10 @@ Environment
 
    :return: Machine hostname.
 
+.. function:: verbose(true | false)
+
+   :return: Toggle verbose logging.
+
 Network configuration
 ^^^^^^^^^^^^^^^^^^^^^
 
@@ -320,6 +353,65 @@ For when listening on ``localhost`` just doesn't cut it.
 
    .. tip:: You can use ``net.<iface>`` as a shortcut for specific interface, e.g. ``net.eth0``
 
+.. function:: net.bufsize([udp_bufsize])
+
+   Get/set maximum EDNS payload available. Default is 1452 (the maximum unfragmented datagram size).
+   You cannot set less than 1220 (minimum size for DNSSEC) or more than 65535 octets.
+
+   Example output:
+
+   .. code-block:: lua
+
+	> net.bufsize(4096)
+	> net.bufsize()
+	4096
+
+Trust anchors and DNSSEC
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. function:: trust_anchors.config(keyfile)
+
+   :param string keyfile: File containing DNSKEY records, should be writeable.
+
+   You can use only DNSKEY records in managed mode. It is equivalent to CLI parameter `-k <keyfile>` or `trust_anchors.file = keyfile`.
+
+   Example output:
+
+   .. code-block:: lua
+
+   > trust_anchors.config('root.keys')
+   [trust_anchors] key: 19036 state: Valid
+
+.. function:: trust_anchors.set_insecure(nta_set)
+
+   :param table nta_list: List of domain names (text format) representing NTAs.
+
+   When you use a domain name as an NTA, DNSSEC validation will be turned off at/below these names.
+   Each function call replaces the previous NTA set. You can find the current active set in `trust_anchors.insecure` variable.
+
+   .. tip:: Use the `trust_anchors.negative = {}` alias for easier configuration.
+
+   Example output:
+
+   .. code-block:: lua
+
+   > trust_anchors.negative = { 'bad.boy', 'example.com' }
+   > trust_anchors.insecure
+   [1] => bad.boy
+   [2] => example.com
+
+.. function:: trust_anchors.add(rr_string)
+
+   :param string rr_string: DS/DNSKEY records in presentation format (e.g. `. 3600 IN DS 19036 8 2 49AAC11...`)
+
+   Inserts DS/DNSKEY record(s) into current keyset. These will not be managed or updated.
+
+   Example output:
+
+   .. code-block:: lua
+
+   > trust_anchors.add('. 3600 IN DS 19036 8 2 49AAC11...')
+
 Modules configuration
 ^^^^^^^^^^^^^^^^^^^^^
 
@@ -521,17 +613,15 @@ you can see the statistics or schedule new queries.
 
 	print(worker.stats().concurrent)
 
-.. function:: worker.resolve(qname, qtype[, qclass = kres.class.IN, options = 0])
+.. function:: worker.resolve(qname, qtype[, qclass = kres.class.IN, options = 0, callback = nil])
 
    :param string qname: Query name (e.g. 'com.')
    :param number qtype: Query type (e.g. ``kres.type.NS``)
    :param number qclass: Query class *(optional)* (e.g. ``kres.class.IN``)
    :param number options: Resolution options (see query flags)
+   :param function callback: Callback to be executed when resolution completes (e.g. `function cb (pkt) end`). The callback gets a packet containing the final answer and doesn't have to return anything.
    :return: boolean
 
-   Resolve a query, there is currently no callback when its finished, but you can track the query
-   progress in layers, just like any other query.
-
 .. _`JSON-encoded`: http://json.org/example
 .. _`Learn Lua in 15 minutes`: http://tylerneylon.com/a/learn-lua/
 .. _`PowerDNS Recursor`: https://doc.powerdns.com/md/recursor/scripting/
diff --git a/daemon/bindings.c b/daemon/bindings.c
index bef846f63351e690181996d2d650aa3a71d5f2fe..d5c20d7f52495a2f004eae967093399a6e042e4b 100644
--- a/daemon/bindings.c
+++ b/daemon/bindings.c
@@ -267,6 +267,24 @@ static int net_interfaces(lua_State *L)
 	return 1;
 }
 
+/** Set UDP maximum payload size. */
+static int net_bufsize(lua_State *L)
+{
+	struct engine *engine = engine_luaget(L);
+	knot_rrset_t *opt_rr = engine->resolver.opt_rr;
+	if (!lua_isnumber(L, 1)) {
+		lua_pushnumber(L, knot_edns_get_payload(opt_rr));
+		return 1;
+	}
+	int bufsize = lua_tointeger(L, 1);
+	if (bufsize < KNOT_EDNS_MIN_DNSSEC_PAYLOAD || bufsize > UINT16_MAX) {
+		format_error(L, "bufsize must be within <1220, 65535>");
+		lua_error(L);
+	}
+	knot_edns_set_payload(opt_rr, (uint16_t) bufsize);
+	return 0;
+}
+
 int lib_net(lua_State *L)
 {
 	static const luaL_Reg lib[] = {
@@ -274,6 +292,7 @@ int lib_net(lua_State *L)
 		{ "listen",     net_listen },
 		{ "close",      net_close },
 		{ "interfaces", net_interfaces },
+		{ "bufsize",    net_bufsize },
 		{ NULL, NULL }
 	};
 	register_lib(L, "net", lib);
@@ -433,23 +452,28 @@ static void event_free(uv_timer_t *timer)
 	free(timer);
 }
 
+static int execute_callback(lua_State *L, int argc)
+{
+	int ret = engine_pcall(L, argc);
+	if (ret != 0) {
+		fprintf(stderr, "error: %s\n", lua_tostring(L, -1));
+	}
+	/* Clear the stack, there may be event a/o enything returned */
+	lua_settop(L, 0);
+	lua_gc(L, LUA_GCCOLLECT, 0);
+	return ret;
+}
+
 static void event_callback(uv_timer_t *timer)
 {
 	struct worker_ctx *worker = timer->loop->data;
 	lua_State *L = worker->engine->L;
 
 	/* Retrieve callback and execute */
-	int top = lua_gettop(L);
 	lua_rawgeti(L, LUA_REGISTRYINDEX, (intptr_t) timer->data);
 	lua_rawgeti(L, -1, 1);
 	lua_pushinteger(L, (intptr_t) timer->data);
-	int ret = engine_pcall(L, 1);
-	if (ret != 0) {
-		fprintf(stderr, "error: %s\n", lua_tostring(L, -1));
-	}
-	/* Clear the stack, there may be event a/o enything returned */
-	lua_settop(L, top);
-	lua_gc(L, LUA_GCCOLLECT, 0);
+	int ret = execute_callback(L, 1);
 	/* Free callback if not recurrent or an error */
 	if (ret != 0 || uv_timer_get_repeat(timer) == 0) {
 		uv_close((uv_handle_t *)timer, (uv_close_cb) event_free);
@@ -522,15 +546,16 @@ static int event_cancel(lua_State *L)
 	/* Fetch event if it exists */
 	lua_rawgeti(L, LUA_REGISTRYINDEX, lua_tointeger(L, 1));
 	if (!lua_istable(L, -1)) {
-		format_error(L, "event not exists");
-		lua_error(L);
+		lua_pushboolean(L, false);
+		return 1;
 	}
 
 	/* Close the timer */
 	lua_rawgeti(L, -1, 2);
 	uv_handle_t *timer = lua_touserdata(L, -1);
 	uv_close(timer, (uv_close_cb) event_free);
-	return 0;
+	lua_pushboolean(L, true);
+	return 1;
 }
 
 int lib_event(lua_State *L)
@@ -553,6 +578,20 @@ static inline struct worker_ctx *wrk_luaget(lua_State *L) {
 	return worker;
 }
 
+/* @internal Call the Lua callback stored in baton. */
+static void resolve_callback(struct worker_ctx *worker, struct kr_request *req, void *baton)
+{
+	assert(worker);
+	assert(req);
+	assert(baton);
+	lua_State *L = worker->engine->L;
+	intptr_t cb_ref = (intptr_t) baton;
+	lua_rawgeti(L, LUA_REGISTRYINDEX, cb_ref);
+	luaL_unref(L, LUA_REGISTRYINDEX, cb_ref);
+	lua_pushlightuserdata(L, req->answer);
+	(void) execute_callback(L, 1);
+}
+
 static int wrk_resolve(lua_State *L)
 {
 	struct worker_ctx *worker = wrk_luaget(L);
@@ -580,18 +619,22 @@ static int wrk_resolve(lua_State *L)
 	knot_pkt_put_question(pkt, dname, rrclass, rrtype);
 	knot_wire_set_rd(pkt->wire);
 	/* Add OPT RR */
-	pkt->opt_rr = mm_alloc(&pkt->mm, sizeof(*pkt->opt_rr));
+	pkt->opt_rr = knot_rrset_copy(worker->engine->resolver.opt_rr, &pkt->mm);
 	if (!pkt->opt_rr) {
 		return kr_error(ENOMEM);
-	}
-	int ret = knot_edns_init(pkt->opt_rr, KR_EDNS_PAYLOAD, 0, KR_EDNS_VERSION, &pkt->mm);
-	if (ret != 0) {
-		knot_pkt_free(&pkt);
-		return 0;
-	}
-	/* Resolve it */
+	}	
+	/* Add completion callback */
+	int ret = 0;
 	unsigned options = lua_tointeger(L, 4);
-	ret = worker_resolve(worker, pkt, options);
+	if (lua_isfunction(L, 5)) {
+		/* Store callback in registry */
+		lua_pushvalue(L, 5);
+		int cb = luaL_ref(L, LUA_REGISTRYINDEX);
+		ret = worker_resolve(worker, pkt, options, resolve_callback, (void *) (intptr_t)cb);
+	} else {
+		ret = worker_resolve(worker, pkt, options, NULL, NULL);
+	}
+	
 	knot_pkt_free(&pkt);
 	lua_pushboolean(L, ret == 0);
 	return 1;
diff --git a/daemon/daemon.mk b/daemon/daemon.mk
index 7b15af7fc6ac3dbeb88782bb91266c2cd4653a1f..526c68a95705468af13c6c2b8866dc07d5bc51df 100644
--- a/daemon/daemon.mk
+++ b/daemon/daemon.mk
@@ -16,12 +16,12 @@ daemon/engine.o: daemon/lua/sandbox.inc daemon/lua/config.inc
 %.inc: %.lua
 	@$(call quiet,XXD,$<) $< > $@
 # Installed FFI bindings
-bindings-install: daemon/lua/kres.lua
-	$(INSTALL) $< $(PREFIX)/$(MODULEDIR)
+bindings-install: daemon/lua/kres.lua daemon/lua/trust_anchors.lua
+	$(INSTALL) $^ $(PREFIX)/$(MODULEDIR)
 
 # Dependencies
 kresd_DEPEND := $(libkres)
-kresd_LIBS := $(libkres_TARGET) $(libknot_LIBS) $(libuv_LIBS) $(lua_LIBS)
+kresd_LIBS := $(libkres_TARGET) $(libknot_LIBS) $(libdnssec_LIBS) $(libuv_LIBS) $(lua_LIBS)
 
 # Make binary
 ifeq ($(HAS_lua)|$(HAS_libuv), yes|yes)
diff --git a/daemon/engine.c b/daemon/engine.c
index 95a98365875a45cb594c5b38778da260218418db..af71bcef2e8529f08fea46233748f1449e730d3e 100644
--- a/daemon/engine.c
+++ b/daemon/engine.c
@@ -27,6 +27,7 @@
 #include "lib/nsrep.h"
 #include "lib/cache.h"
 #include "lib/defines.h"
+#include "lib/dnssec/ta.h"
 
 /** @internal Compatibility wrapper for Lua < 5.2 */
 #if LUA_VERSION_NUM < 502
@@ -52,6 +53,7 @@ static int l_help(lua_State *L)
 		"help()\n    show this help\n"
 		"quit()\n    quit\n"
 		"hostname()\n    hostname\n"
+		"verbose(true|false)\n    toggle verbose mode\n"
 		"option(opt[, new_val])\n    get/set server option\n"
 		;
 	lua_pushstring(L, help_str);
@@ -61,12 +63,20 @@ static int l_help(lua_State *L)
 /** Quit current executable. */
 static int l_quit(lua_State *L)
 {
-	/* Stop engine */
 	engine_stop(engine_luaget(L));
-	/* No results */
 	return 0;
 }
 
+/** Toggle verbose mode. */
+static int l_verbose(lua_State *L)
+{
+	if (lua_isboolean(L, 1) || lua_isnumber(L, 1)) {
+		log_debug_enable(lua_toboolean(L, 1));
+	}
+	lua_pushboolean(L, log_debug_status());
+	return 1;
+}
+
 /** Return hostname. */
 static int l_hostname(lua_State *L)
 {
@@ -96,7 +106,7 @@ static int l_option(lua_State *L)
 		}
 	}
 	/* Get or set */
-	if (lua_isboolean(L, 2)) {
+	if (lua_isboolean(L, 2) || lua_isnumber(L, 2)) {
 		if (lua_toboolean(L, 2)) {
 			engine->resolver.options |= opt_code;
 		} else {
@@ -234,6 +244,8 @@ void *namedb_lmdb_mkopts(const char *conf, size_t maxsize)
 static int init_resolver(struct engine *engine)
 {
 	/* Open resolution context */
+	engine->resolver.trust_anchors = map_make();
+	engine->resolver.negative_anchors = map_make();
 	engine->resolver.pool = engine->pool;
 	engine->resolver.modules = &engine->modules;
 	/* Create OPT RR */
@@ -257,6 +269,7 @@ static int init_resolver(struct engine *engine)
 
 	/* Load basic modules */
 	engine_register(engine, "iterate");
+	engine_register(engine, "validate");
 	engine_register(engine, "rrcache");
 	engine_register(engine, "pktcache");
 
@@ -283,10 +296,12 @@ static int init_state(struct engine *engine)
 	lua_setglobal(engine->L, "help");
 	lua_pushcfunction(engine->L, l_quit);
 	lua_setglobal(engine->L, "quit");
-	lua_pushcfunction(engine->L, l_option);
-	lua_setglobal(engine->L, "option");
 	lua_pushcfunction(engine->L, l_hostname);
 	lua_setglobal(engine->L, "hostname");
+	lua_pushcfunction(engine->L, l_verbose);
+	lua_setglobal(engine->L, "verbose");
+	lua_pushcfunction(engine->L, l_option);
+	lua_setglobal(engine->L, "option");
 	lua_pushlightuserdata(engine->L, engine);
 	lua_setglobal(engine->L, "__engine");
 	return kr_ok();
@@ -345,17 +360,19 @@ void engine_deinit(struct engine *engine)
 	lru_deinit(engine->resolver.cache_rtt);
 	lru_deinit(engine->resolver.cache_rep);
 
-	/* Unload modules. */
+	/* Unload modules and engine. */
 	for (size_t i = 0; i < engine->modules.len; ++i) {
 		engine_unload(engine, engine->modules.at[i]);
 	}
-	array_clear(engine->modules);
-	array_clear(engine->storage_registry);
-
 	if (engine->L) {
 		lua_close(engine->L);
 	}
 
+	/* Free data structures */
+	array_clear(engine->modules);
+	array_clear(engine->storage_registry);
+	kr_ta_clear(&engine->resolver.trust_anchors);
+	kr_ta_clear(&engine->resolver.negative_anchors);
 }
 
 int engine_pcall(lua_State *L, int argc)
@@ -390,6 +407,12 @@ int engine_cmd(struct engine *engine, const char *str)
 
 static int engine_loadconf(struct engine *engine)
 {
+	/* Use module path for including Lua scripts */
+	static const char l_paths[] = "package.path = package.path..';" PREFIX MODULEDIR "/?.lua'";
+	int ret = l_dobytecode(engine->L, l_paths, sizeof(l_paths) - 1, "");
+	if (ret != 0) {
+		lua_pop(engine->L, 1);
+	}
 	/* Init environment */
 	static const char sandbox_bytecode[] = {
 		#include "daemon/lua/sandbox.inc"
@@ -399,12 +422,6 @@ static int engine_loadconf(struct engine *engine)
 		lua_pop(engine->L, 1);
 		return kr_error(ENOEXEC);
 	}
-	/* Use module path for including Lua scripts */
-	int ret = engine_cmd(engine, "package.path = package.path..';" PREFIX MODULEDIR "/?.lua'");
-	if (ret > 0) {
-		lua_pop(engine->L, 1);
-	}
-
 	/* Load config file */
 	if(access("config", F_OK ) != -1 ) {
 		ret = l_dosandboxfile(engine->L, "config");
diff --git a/daemon/lua/config.lua b/daemon/lua/config.lua
index e23e0d3d6734f45b4f53d90b859076f2712ab813..028b88bb4cd4f8497fbbf9137dd340bff31b8405 100644
--- a/daemon/lua/config.lua
+++ b/daemon/lua/config.lua
@@ -1,5 +1,3 @@
--- Default configuration
-cache.open(10*MB)
 -- Listen on localhost
 if not next(net.list()) then
 	if not pcall(net.listen, '127.0.0.1') then
diff --git a/daemon/lua/kres.lua b/daemon/lua/kres.lua
index e548ad239802b014576e135e78b5646b7a4d9dc6..fdb37958208335f5e885826c32d46a5cec65b2c4 100644
--- a/daemon/lua/kres.lua
+++ b/daemon/lua/kres.lua
@@ -86,6 +86,13 @@ struct pkt_rcode {
 	static const int NOTZONE    = 10;
 	static const int BADVERS    = 16;
 };
+struct query_flag {
+	static const int NO_MINIMIZE = 1 << 0;
+	static const int CACHED      = 1 << 8;
+	static const int NO_CACHE    = 1 << 9;
+	static const int EXPIRING    = 1 << 10;
+};
+
 /*
  * Data structures
  */
@@ -97,13 +104,16 @@ struct sockaddr {
 };
 
 /* libknot */
+typedef int knot_section_t; /* Do not touch */
+typedef void knot_rrinfo_t; /* Do not touch */
 typedef struct node {
   struct node *next, *prev;
 } node_t;
 typedef uint8_t knot_dname_t;
+typedef uint8_t knot_rdata_t;
 typedef struct knot_rdataset {
 	uint16_t count;
-	uint8_t *data;
+	knot_rdata_t *data;
 } knot_rdataset_t;
 typedef struct knot_rrset {
 	knot_dname_t *_owner;
@@ -111,6 +121,11 @@ typedef struct knot_rrset {
 	uint16_t class;
 	knot_rdataset_t rr;
 } knot_rrset_t;
+typedef struct {
+	struct knot_pkt *pkt;
+	uint16_t pos;
+	uint16_t count;
+} knot_pktsection_t;
 typedef struct {
 	uint8_t *wire;
 	size_t size;
@@ -122,9 +137,24 @@ typedef struct {
 	uint16_t flags;
 	knot_rrset_t *opt;
 	knot_rrset_t *tsig;
+	knot_section_t _current;
+	knot_pktsection_t _sections[3];
+	size_t _rrset_allocd;
+	knot_rrinfo_t *_rr_info;
+	knot_rrset_t *_rr;
 	uint8_t _stub[]; /* Do not touch */
 } knot_pkt_t;
 
+/* generics */
+typedef void *(*map_alloc_f)(void *, size_t);
+typedef void (*map_free_f)(void *baton, void *ptr);
+typedef struct {
+	void *root;
+	map_alloc_f malloc;
+	map_free_f free;
+	void *baton;
+} map_t;
+
 /* libkres */
 struct kr_query {
 	node_t _node;
@@ -141,32 +171,53 @@ struct kr_rplan {
 	uint8_t _stub[]; /* Do not touch */
 };
 struct kr_request {
-	struct kr_context *_ctx;
+	struct kr_context *ctx;
 	knot_pkt_t *answer;
-    struct {
-        const knot_rrset_t *key;
-        const struct sockaddr *addr;
-    } qsource;
+	struct {
+		const knot_rrset_t *key;
+		const struct sockaddr *addr;
+	} qsource;
 	uint32_t options;
 	int state;
 	uint8_t _stub[]; /* Do not touch */
 };
+struct kr_context
+{	
+	uint32_t options;
+	knot_rrset_t *opt_rr;
+	map_t trust_anchors;
+	map_t negative_anchors;
+	uint8_t _stub[]; /* Do not touch */
+};
 
-/* libknot API
+/*
+ * libc APIs
  */
+void free(void *ptr);
 
+/*
+ * libknot APIs
+ */
 /* Domain names */
+int knot_dname_size(const knot_dname_t *name);
+knot_dname_t *knot_dname_from_str(uint8_t *dst, const char *name, size_t maxlen);
+char *knot_dname_to_str(char *dst, const knot_dname_t *name, size_t maxlen);
 /* Resource records */
+uint16_t knot_rdata_rdlen(const knot_rdata_t *rr);
+uint8_t *knot_rdata_data(const knot_rdata_t *rr);
+knot_rdata_t *knot_rdataset_at(const knot_rdataset_t *rrs, size_t pos);
+uint32_t knot_rrset_ttl(const knot_rrset_t *rrset);
 /* Packet */
 const knot_dname_t *knot_pkt_qname(const knot_pkt_t *pkt);
 uint16_t knot_pkt_qtype(const knot_pkt_t *pkt);
 uint16_t knot_pkt_qclass(const knot_pkt_t *pkt);
 int knot_pkt_begin(knot_pkt_t *pkt, int section_id);
 int knot_pkt_put_question(knot_pkt_t *pkt, const knot_dname_t *qname, uint16_t qclass, uint16_t qtype);
+const knot_rrset_t *knot_pkt_rr(const knot_pktsection_t *section, uint16_t i);
 
-/* libkres API
+/* 
+ * libkres API
  */
-
 /* Resolution request */
 struct kr_rplan *kr_resolve_plan(struct kr_request *request);
 /* Resolution plan */
@@ -178,6 +229,18 @@ int kr_pkt_put(knot_pkt_t *pkt, const knot_dname_t *name, uint32_t ttl,
                uint16_t rclass, uint16_t rtype, const uint8_t *rdata, uint16_t rdlen);
 const char *kr_inaddr(const struct sockaddr *addr);
 int kr_inaddr_len(const struct sockaddr *addr);
+/* Trust anchors */
+knot_rrset_t *kr_ta_get(map_t *trust_anchors, const knot_dname_t *name);
+int kr_ta_add(map_t *trust_anchors, const knot_dname_t *name, uint16_t type,
+               uint32_t ttl, const uint8_t *rdata, uint16_t rdlen);
+int kr_ta_del(map_t *trust_anchors, const knot_dname_t *name);
+void kr_ta_clear(map_t *trust_anchors);
+/* DNSSEC */
+bool kr_dnssec_key_ksk(const uint8_t *dnskey_rdata);
+bool kr_dnssec_key_revoked(const uint8_t *dnskey_rdata);
+int kr_dnssec_key_tag(uint16_t rrtype, const uint8_t *rdata, size_t rdlen);
+int kr_dnssec_key_match(const uint8_t *key_a_rdata, size_t key_a_rdlen,
+                        const uint8_t *key_b_rdata, size_t key_b_rdlen);
 ]]
 
 -- Metatype for sockaddr
@@ -193,7 +256,19 @@ ffi.metatype( sockaddr_t, {
 local knot_rrset_t = ffi.typeof('knot_rrset_t')
 ffi.metatype( knot_rrset_t, {
 	__index = {
-		owner = function(rr) return ffi.string(rr._owner) end,
+		owner = function(rr) return ffi.string(rr._owner, knot.knot_dname_size(rr._owner)) end,
+		ttl = function(rr) return tonumber(knot.knot_rrset_ttl(rr)) end,
+		rdata = function(rr, i)
+			local rdata = knot.knot_rdataset_at(rr.rr, i)
+			return ffi.string(knot.knot_rdata_data(rdata), knot.knot_rdata_rdlen(rdata))
+		end,
+		get = function(rr, i)
+			return {owner = rr:owner(),
+			        ttl = rr:ttl(),
+			        class = tonumber(rr.class),
+			        type = tonumber(rr.type),
+			        rdata = rr:rdata(i)}
+		end,
 	}
 })
 
@@ -201,7 +276,10 @@ ffi.metatype( knot_rrset_t, {
 local knot_pkt_t = ffi.typeof('knot_pkt_t')
 ffi.metatype( knot_pkt_t, {
 	__index = {
-		qname = function(pkt) return ffi.string(knot.knot_pkt_qname(pkt)) end,
+		qname = function(pkt)
+			local qname = knot.knot_pkt_qname(pkt)
+			return ffi.string(qname, knot.knot_dname_size(qname))
+		end,
 		qclass = function(pkt) return knot.knot_pkt_qclass(pkt) end,
 		qtype  = function(pkt) return knot.knot_pkt_qtype(pkt) end,
 		rcode = function (pkt, val)
@@ -212,6 +290,17 @@ ffi.metatype( knot_pkt_t, {
 			pkt.wire[2] = bor(pkt.wire[2], (val) and 0x02 or 0x00)
 			return band(pkt.wire[2], 0x02)
 		end,
+		section = function (pkt, section_id)
+			local records = {}
+			local section = pkt._sections[section_id]
+			for i = 0, section.count - 1 do
+				local rrset = knot.knot_pkt_rr(section, i)
+				for k = 0, rrset.rr.count - 1 do
+					table.insert(records, rrset:get(k))
+				end
+			end
+			return records
+		end, 
 		begin = function (pkt, section) return knot.knot_pkt_begin(pkt, section) end,
 		put = function (pkt, owner, ttl, rclass, rtype, rdata)
 			return C.kr_pkt_put(pkt, owner, ttl, rclass, rtype, rdata, string.len(rdata))
@@ -236,6 +325,24 @@ ffi.metatype( kr_request_t, {
 	},
 })
 
+-- Pretty print for domain name
+local function dname2str(dname)
+	return ffi.string(ffi.gc(C.knot_dname_to_str(nil, dname, 0), C.free))
+end
+
+-- Pretty print for RR
+local function rr2str(rr)
+	local function hex_encode(str)
+		return (str:gsub('.', function (c)
+			return string.format('%02X', string.byte(c))
+		end))
+	end
+	local rdata = hex_encode(rr.rdata)
+	return string.format('%s %d IN TYPE%d \\# %d %s',
+		dname2str(rr.owner), rr.ttl, rr.type, #rr.rdata, rdata)
+end
+
+
 -- Module API
 local kres = {
 	-- Constants
@@ -243,11 +350,16 @@ local kres = {
 	type = ffi.new('struct rr_type'),
 	section = ffi.new('struct pkt_section'),
 	rcode = ffi.new('struct pkt_rcode'),
+	query = ffi.new('struct query_flag'),
 	NOOP = 0, CONSUME = 1, PRODUCE = 2, DONE = 4, FAIL = 8,
 	-- Metatypes
 	pkt_t = function (udata) return ffi.cast('knot_pkt_t *', udata) end,
 	request_t = function (udata) return ffi.cast('struct kr_request *', udata) end,
 	-- Global API functions
+	str2dname = function(name) return ffi.string(ffi.gc(C.knot_dname_from_str(nil, name, 0), C.free)) end,
+	dname2str = dname2str,
+	rr2str = rr2str,
+	context = function () return ffi.cast('struct kr_context *', __engine) end,
 }
 
 return kres
\ No newline at end of file
diff --git a/daemon/lua/sandbox.lua b/daemon/lua/sandbox.lua
index 53559156ea1007b30c511a4cec6c239551a02266..fe9cba5d9aa857d0febaf9a100e7e972c014a99b 100644
--- a/daemon/lua/sandbox.lua
+++ b/daemon/lua/sandbox.lua
@@ -6,6 +6,11 @@ GB = 1024*MB
 sec = 1000
 minute = 60 * sec
 hour = 60 * minute
+day = 24 * hour
+
+-- Resolver bindings
+kres = require('kres')
+trust_anchors = require('trust_anchors')
 
 -- Function aliases
 -- `env.VAR returns os.getenv(VAR)`
@@ -63,6 +68,17 @@ setmetatable(cache, {
 		else   rawset(t, k, v) end
 	end
 })
+-- Defaults
+cache.size = 10 * MB
+
+-- Syntactic sugar for TA store
+setmetatable(trust_anchors, {
+	__newindex = function (t,k,v)
+	if     k == 'file' then t.config(v)
+	elseif k == 'negative' then t.set_insecure(v)
+	else   rawset(t, k, v) end
+	end,
+})
 
 -- Register module in Lua environment
 function modules_register(module)
@@ -85,7 +101,7 @@ end
 
 -- Make sandboxed environment
 local function make_sandbox(defined)
-	local __protected = { modules = true, cache = true, net = true }
+	local __protected = { modules = true, cache = true, net = true, trust_anchors = true }
 	return setmetatable({}, {
 		__index = defined,
 		__newindex = function (t, k, v)
@@ -135,22 +151,35 @@ function table_print (tt, indent, done)
 	done = done or {}
 	indent = indent or 0
 	result = ""
+	-- Convert to printable string (escape unprintable)
+	local function printable(value)
+		value = tostring(value)
+		local bytes = {}
+		for i = 1, #value do
+			local c = string.byte(value, i)
+			if c >= 0x20 and c < 0x7f then table.insert(bytes, string.char(c))
+			else                           table.insert(bytes, '\\'..tostring(c))
+			end
+			if i > 50 then table.insert(bytes, '...') break end
+		end
+		return table.concat(bytes)
+	end
 	if type(tt) == "table" then
 		for key, value in pairs (tt) do
 			result = result .. string.rep (" ", indent)
 			if type (value) == "table" and not done [value] then
 				done [value] = true
-				result = result .. string.format("[%s] => {\n", tostring (key))
+				result = result .. string.format("[%s] => {\n", printable (key))
 				result = result .. table_print (value, indent + 4, done)
 				result = result .. string.rep (" ", indent)
 				result = result .. "}\n"
 			else
 				result = result .. string.format("[%s] => %s\n",
-				         tostring (key), tostring(value))
+				         tostring (key), printable(value))
 			end
 		end
 	else
 		result = result .. tostring(tt) .. "\n"
 	end
 	return result
-end
+end
\ No newline at end of file
diff --git a/daemon/lua/trust_anchors.lua b/daemon/lua/trust_anchors.lua
new file mode 100644
index 0000000000000000000000000000000000000000..98fce1aa9d21b3f900165ab35992a852d6f7b2e7
--- /dev/null
+++ b/daemon/lua/trust_anchors.lua
@@ -0,0 +1,224 @@
+local kres = require('kres')
+local C = require('ffi').C
+
+-- RFC5011 state table
+local key_state = {
+	Start = 'Start', AddPend = 'AddPend', Valid = 'Valid',
+	Missing = 'Missing', Revoked = 'Revoked', Removed = 'Removed'
+}
+
+-- Find key in current keyset
+local function ta_find(keyset, rr)
+	for i = 1, #keyset do
+		local ta = keyset[i]
+		-- Match key owner and content
+		if ta.owner == rr.owner and
+		   C.kr_dnssec_key_match(ta.rdata, #ta.rdata, rr.rdata, #rr.rdata) == 0 then
+		   return ta
+		end
+	end
+	return nil
+end
+
+-- Evaluate TA status according to RFC5011
+local function ta_present(keyset, rr, hold_down_time, force)
+	if not C.kr_dnssec_key_ksk(rr.rdata) then
+		return false -- Ignore
+	end
+	-- Find the key in current key set and check its status
+	local now = os.time()
+	local key_revoked = C.kr_dnssec_key_revoked(rr.rdata)
+	local key_tag = C.kr_dnssec_key_tag(rr.type, rr.rdata, #rr.rdata)
+	local ta = ta_find(keyset, rr)
+	if ta then
+		-- Key reappears (KeyPres)
+		if ta.state == key_state.Missing then
+			ta.state = key_state.Valid
+			ta.timer = nil
+		end
+		-- Key is revoked (RevBit)
+		if ta.state == key_state.Valid or ta.state == key_state.Missing then
+			if key_revoked then
+				ta.state = key_state.Revoked
+				ta.timer = os.time() + hold_down_time
+			end
+		end
+		-- Remove hold-down timer expires (RemTime)
+		if ta.state == key_state.Revoked and os.difftime(ta.timer, now) <= 0 then
+			ta.state = key_state.Removed
+			ta.timer = nil
+		end
+		-- Add hold-down timer expires (AddTime)
+		if ta.state == key_state.AddPend and os.difftime(ta.timer, now) <= 0 then
+			ta.state = key_state.Valid
+			ta.timer = nil
+		end
+		print('[trust_anchors] key: '..key_tag..' state: '..ta.state)
+		return true
+	elseif not key_revoked then -- First time seen (NewKey)
+		rr.key_tag = key_tag
+		if force then
+			rr.state = key_state.Valid
+		else
+			rr.state = key_state.AddPend
+			rr.timer = now + hold_down_time
+		end
+		print('[trust_anchors] key: '..key_tag..' state: '..rr.state)
+		table.insert(keyset, rr)
+		return true
+	end
+	return false
+end
+
+-- TA is missing in the new key set
+local function ta_missing(keyset, ta, hold_down_time)
+	-- Key is removed (KeyRem)
+	local keep_ta = true
+	local key_tag = C.kr_dnssec_key_tag(ta.type, ta.rdata, #ta.rdata)
+	if ta.state == key_state.Valid then
+		ta.state = key_state.Missing
+		ta.timer = os.time() + hold_down_time
+	-- Purge pending key
+	elseif ta.state == key_state.AddPend then
+		print('[trust_anchors] key: '..key_tag..' purging')
+		keep_ta = false
+	end
+	print('[trust_anchors] key: '..key_tag..' state: '..ta.state)
+	return keep_ta
+end
+
+-- Plan refresh event and re-schedule itself based on the result of the callback
+local function refresh_plan(trust_anchors, timeout, refresh_cb)
+	if trust_anchors.refresh_ev ~= nil then event.cancel(trust_anchors.refresh_ev) end
+	trust_anchors.refresh_ev = event.after(timeout, function (ev)
+		worker.resolve('.', kres.type.DNSKEY, kres.class.IN, kres.query.NO_CACHE,
+		function (pkt)
+			-- Schedule itself with updated timeout
+			local next_time = refresh_cb(trust_anchors, kres.pkt_t(pkt))
+			if trust_anchors.refresh_time ~= nil then
+				next_time = math.min(next_time, trust_anchors.refresh_time)
+			end
+			print('[trust_anchors] next refresh: '..next_time)
+			refresh_plan(trust_anchors, next_time, refresh_cb)
+		end)
+	end)
+end
+
+-- Active refresh, return time of the next check
+local function active_refresh(trust_anchors, pkt)
+	local retry = true
+	if pkt:rcode() == kres.rcode.NOERROR then
+		local records = pkt:section(kres.section.ANSWER)
+		local keyset = {}
+		for i = 1, #records do
+			local rr = records[i]
+			if rr.type == kres.type.DNSKEY then
+				table.insert(keyset, rr)
+			end
+		end
+		trust_anchors.update(keyset, false)
+		retry = false
+	end
+	-- Calculate refresh/retry timer (RFC 5011, 2.3)
+	local min_ttl = retry and day or 15 * day
+	for i, rr in ipairs(trust_anchors.keyset) do -- 10 or 50% of the original TTL
+		min_ttl = math.min(min_ttl, (retry and 100 or 500) * rr.ttl)
+	end
+	return math.max(hour, min_ttl)
+end
+
+-- Write keyset to a file
+local function keyset_write(keyset, path)
+	local file = assert(io.open(path..'.lock', 'w'))
+	for i = 1, #keyset do
+		local ta = keyset[i]
+		local rr_str = string.format('%s ; %s\n', kres.rr2str(ta), ta.state)
+		if ta.state ~= key_state.Valid and ta.state ~= key_state.Missing then
+			rr_str = '; '..rr_str -- Invalidate key string
+		end
+		file:write(rr_str)
+	end
+	file:close()
+	os.rename(path..'.lock', path)
+end
+
+-- TA store management
+local trust_anchors = {
+	keyset = {},
+	insecure = {},
+	hold_down_time = 30 * day,
+	-- Update existing keyset
+	update = function (new_keys, initial)
+		if not new_keys then return false end
+		-- Filter TAs to be purged from the keyset (KeyRem)
+		local hold_down = trust_anchors.hold_down_time / 1000
+		local keyset_keep = {}
+		local keyset = trust_anchors.keyset
+		for i = 1, #keyset do
+			local ta = keyset[i]
+			local keep = true
+			if not ta_find(new_keys, ta) then
+				keep = ta_missing(trust_anchors, keyset, ta, hold_down)
+			end
+			if keep then
+				table.insert(keyset_keep, ta)
+			end
+		end
+		keyset = keyset_keep
+		-- Evaluate new TAs
+		for i = 1, #new_keys do
+			local rr = new_keys[i]
+			if rr.type == kres.type.DNSKEY then
+				ta_present(keyset, rr, hold_down, initial)
+			end
+		end
+		-- Publish active TAs
+		local store = kres.context().trust_anchors
+		C.kr_ta_clear(store)
+		if #keyset == 0 then return false end
+		for i = 1, #keyset do
+			local ta = keyset[i]
+			-- Key MAY be used as a TA only in these two states (RFC5011, 4.2)
+			if ta.state == key_state.Valid or ta.state == key_state.Missing then
+				C.kr_ta_add(store, ta.owner, ta.type, ta.ttl, ta.rdata, #ta.rdata)
+			end
+		end
+		trust_anchors.keyset = keyset
+		-- Store keyset in the file
+		if trust_anchors.file_current ~= nil then
+			keyset_write(keyset, trust_anchors.file_current)
+		end
+		return true
+	end,
+	-- Load keys from a file (managed)
+	config = function (path, is_unmanaged)
+		if path == trust_anchors.file_current then return end
+		local new_keys = require('zonefile').parse_file(path)
+		trust_anchors.file_current = path
+		if is_unmanaged then trust_anchors.file_current = nil end
+		trust_anchors.keyset = {}
+		if trust_anchors.update(new_keys, true) then
+			refresh_plan(trust_anchors, sec, active_refresh)
+		end
+	end,
+	-- Add DS/DNSKEY record(s) (unmanaged)
+	add = function (keystr)
+		local store = kres.context().trust_anchors
+		require('zonefile').parser(function (p)
+			local rr = p:current_rr()
+			C.kr_ta_add(store, rr.owner, rr.type, rr.ttl, rr.rdata, #rr.rdata)
+		end):read(keystr..'\n')
+	end,
+	-- Negative TA management
+	set_insecure = function (list)
+		local store = kres.context().negative_anchors
+		C.kr_ta_clear(store)
+		for i = 1, #list do
+			local dname = kres.str2dname(list[i])
+			C.kr_ta_add(store, dname, kres.type.DS, 0, nil, 0)
+		end
+		trust_anchors.insecure = list
+	end,
+}
+
+return trust_anchors
\ No newline at end of file
diff --git a/daemon/main.c b/daemon/main.c
index d0e2ba426860ab0b111bb931ca993f0d222e0ba5..bbebdab0b8ec5233f079cd8da7018c7838638859 100644
--- a/daemon/main.c
+++ b/daemon/main.c
@@ -23,6 +23,7 @@
 #include "contrib/ccan/asprintf/asprintf.h"
 #include "lib/defines.h"
 #include "lib/resolve.h"
+#include "lib/dnssec.h"
 #include "daemon/network.h"
 #include "daemon/worker.h"
 #include "daemon/engine.h"
@@ -61,8 +62,12 @@ static void tty_read(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf)
 		struct engine *engine = stream->data;
 		lua_State *L = engine->L;
 		int ret = engine_cmd(engine, cmd);
-		fprintf(ret ? outerr : out, "%s\n> ", lua_tostring(L, -1));
-		lua_pop(L, 1);
+		const char *message = "";
+		if (lua_gettop(L) > 0) {
+			message = lua_tostring(L, -1);
+		}
+		fprintf(ret ? outerr : out, "%s\n> ", message);
+		lua_settop(L, 0);
 		free(buf->base);
 	}
 	fflush(out);
@@ -120,12 +125,14 @@ static void help(int argc, char *argv[])
 {
 	printf("Usage: %s [parameters] [rundir]\n", argv[0]);
 	printf("\nParameters:\n"
-	       " -a, --addr=[addr]   Server address (default: localhost#53).\n"
-	       " -f, --forks=N       Start N forks sharing the configuration.\n"
-	       " -v, --version       Print version of the server.\n"
-	       " -h, --help          Print help and usage.\n"
+	       " -a, --addr=[addr]    Server address (default: localhost#53).\n"
+	       " -k, --keyfile=[path] File containing trust anchors (DS or DNSKEY).\n"
+	       " -f, --forks=N        Start N forks sharing the configuration.\n"
+	       " -v, --verbose        Run in verbose mode.\n"
+	       " -V, --version        Print version of the server.\n"
+	       " -h, --help           Print help and usage.\n"
 	       "Options:\n"
-	       " [rundir]            Path to the working directory (default: .)\n");
+	       " [rundir]             Path to the working directory (default: .)\n");
 }
 
 static struct worker_ctx *init_worker(uv_loop_t *loop, struct engine *engine, mm_ctx_t *pool, int worker_id)
@@ -178,47 +185,57 @@ static int run_worker(uv_loop_t *loop, struct engine *engine)
 		}
 	}
 	/* Run event loop */
-	int ret = engine_start(engine);
-	if (ret == 0) {
-		uv_run(loop, UV_RUN_DEFAULT);
-	}
+	uv_run(loop, UV_RUN_DEFAULT);
 	if (sock_file) {
 		unlink(sock_file);
 	}
-	return ret;
+	return kr_ok();
 }
 
 int main(int argc, char **argv)
 {
-	const char *addr = NULL;
-	int port = 53;
 	int forks = 1;
+	array_t(char*) addr_set;
+	array_init(addr_set);
+	const char *keyfile = NULL;
 
 	/* Long options. */
 	int c = 0, li = 0, ret = 0;
 	struct option opts[] = {
-		{"addr", required_argument, 0, 'a'},
-		{"forks",required_argument, 0, 'f'},
-		{"version",   no_argument,  0, 'v'},
-		{"help",      no_argument,  0, 'h'},
+		{"addr", required_argument,   0, 'a'},
+		{"keyfile",required_argument, 0, 'k'},
+		{"forks",required_argument,   0, 'f'},
+		{"verbose",    no_argument,   0, 'v'},
+		{"version",   no_argument,    0, 'V'},
+		{"help",      no_argument,    0, 'h'},
 		{0, 0, 0, 0}
 	};
-	while ((c = getopt_long(argc, argv, "a:f:vh", opts, &li)) != -1) {
+	while ((c = getopt_long(argc, argv, "a:f:k:vVh", opts, &li)) != -1) {
 		switch (c)
 		{
 		case 'a':
-			addr = set_addr(optarg, &port);
+			array_push(addr_set, optarg);
 			break;
 		case 'f':
 			g_interactive = 0;
 			forks = atoi(optarg);
 			if (forks == 0) {
-				fprintf(stderr, "[system] error '-f' requires number, not '%s'\n", optarg);
+				log_error("[system] error '-f' requires number, not '%s'\n", optarg);
+				return EXIT_FAILURE;
+			}
+			break;
+		case 'k':
+			keyfile = optarg;
+			if (access(optarg, R_OK) != 0) {
+				log_error("[system] keyfile '%s': not readable\n", optarg);
 				return EXIT_FAILURE;
 			}
 			break;
 		case 'v':
-			printf("%s, version %s\n", "Knot DNS Resolver", PACKAGE_VERSION);
+			log_debug_enable(true);
+			break;
+		case 'V':
+			log_info("%s, version %s\n", "Knot DNS Resolver", PACKAGE_VERSION);
 			return EXIT_SUCCESS;
 		case 'h':
 		case '?':
@@ -234,16 +251,18 @@ int main(int argc, char **argv)
 	if (optind < argc) {
 		const char *rundir = argv[optind];
 		if (access(rundir, W_OK) != 0) {
-			fprintf(stderr, "[system] rundir '%s': not writeable\n", rundir);
+			log_error("[system] rundir '%s': not writeable\n", rundir);
 			return EXIT_FAILURE;
 		}
 		ret = chdir(rundir);
 		if (ret != 0) {
-			fprintf(stderr, "[system] rundir '%s': %s\n", rundir, strerror(errno));
+			log_error("[system] rundir '%s': %s\n", rundir, strerror(errno));
 			return EXIT_FAILURE;
 		}
 	}
 
+	kr_crypto_init();
+
 	/* Fork subprocesses if requested */
 	while (--forks > 0) {
 		int pid = fork();
@@ -253,6 +272,7 @@ int main(int argc, char **argv)
 		}
 		/* Forked process */
 		if (pid == 0) {
+			kr_crypto_reinit();
 			break;
 		}
 	}
@@ -272,32 +292,50 @@ int main(int argc, char **argv)
 	struct engine engine;
 	ret = engine_init(&engine, &pool);
 	if (ret != 0) {
-		fprintf(stderr, "[system] failed to initialize engine: %s\n", kr_strerror(ret));
+		log_error("[system] failed to initialize engine: %s\n", kr_strerror(ret));
 		return EXIT_FAILURE;
 	}
 	/* Create worker */
 	struct worker_ctx *worker = init_worker(loop, &engine, &pool, forks);
 	if (!worker) {
-		fprintf(stderr, "[system] not enough memory\n");
+		log_error("[system] not enough memory\n");
 		return EXIT_FAILURE;
 	}
 	/* Bind to sockets and run */
-	if (addr != NULL) {
+	for (size_t i = 0; i < addr_set.len; ++i) {
+		int port = 53;
+		const char *addr = set_addr(addr_set.at[i], &port);
 		ret = network_listen(&engine.net, addr, (uint16_t)port, NET_UDP|NET_TCP);
 		if (ret != 0) {
-			fprintf(stderr, "[system] bind to '%s#%d' %s\n", addr, port, knot_strerror(ret));
+			log_error("[system] bind to '%s#%d' %s\n", addr, port, knot_strerror(ret));
 			ret = EXIT_FAILURE;
 		}
 	}
+	/* Start the scripting engine */
 	if (ret == 0) {
-		ret = run_worker(loop, &engine);
+		ret = engine_start(&engine);
+		if (ret == 0) {
+			if (keyfile) {
+				auto_free char *cmd = afmt("trust_anchors.file = '%s'", keyfile);
+				if (!cmd) {
+					log_error("[system] not enough memory\n");
+					return EXIT_FAILURE;
+				}
+				engine_cmd(&engine, cmd);
+				lua_settop(engine.L, 0);
+			}
+			/* Run the event loop */
+			ret = run_worker(loop, &engine);
+		}
 	}
 	/* Cleanup. */
+	array_clear(addr_set);
 	engine_deinit(&engine);
 	worker_reclaim(worker);
 	mp_delete(pool.ctx);
 	if (ret != 0) {
 		ret = EXIT_FAILURE;
 	}
+	kr_crypto_cleanup();
 	return ret;
 }
diff --git a/daemon/network.c b/daemon/network.c
index a7bd16a8f075d9ff5e43b56376ceb249507546b5..81a528fccff4eb61b0b43bb14104b9bd53e2679b 100644
--- a/daemon/network.c
+++ b/daemon/network.c
@@ -19,8 +19,7 @@
 #include "daemon/io.h"
 
 /* libuv 1.7.0+ is able to support SO_REUSEPORT for loadbalancing */
-#define UV_VERSION_NUM UV_VERSION_MAJOR ## UV_VERSION_MINOR ## UV_VERSION_PATCH
-#if (defined(ENABLE_REUSEPORT) || UV_VERSION_NUM >= 170) && (__linux__ && SO_REUSEPORT)
+#if (defined(ENABLE_REUSEPORT) || defined(UV_VERSION_HEX)) && (__linux__ && SO_REUSEPORT)
   #define handle_init(type, loop, handle, family) do { \
 	uv_ ## type ## _init_ex((loop), (handle), (family)); \
 	uv_os_fd_t fd = 0; \
diff --git a/daemon/worker.c b/daemon/worker.c
index abf5e4ab8dc39d3ff8fca596d4d56cfee3a18110..9900d65a44468edc509cd19343bf41ae0d8c4ce4 100644
--- a/daemon/worker.c
+++ b/daemon/worker.c
@@ -69,6 +69,8 @@ struct qr_task
 	uv_req_t *ioreq;
 	uv_handle_t *iohandle;
 	uv_timer_t timeout;
+	worker_cb_t on_complete;
+	void *baton;
 	struct {
 		union {
 			struct sockaddr_in ip4;
@@ -138,6 +140,7 @@ static struct qr_task *qr_task_create(struct worker_ctx *worker, uv_handle_t *ha
 	task->source.handle = handle;
 	uv_timer_init(worker->loop, &task->timeout);
 	task->timeout.data = task;
+	task->on_complete = NULL;
 	/* Remember query source addr */
 	if (addr) {
 		memcpy(&task->source.addr, addr, sockaddr_len(addr));
@@ -159,6 +162,11 @@ static struct qr_task *qr_task_create(struct worker_ctx *worker, uv_handle_t *ha
 static void qr_task_free(uv_handle_t *handle)
 {
 	struct qr_task *task = handle->data;
+	struct worker_ctx *worker = task->worker;
+	/* Run the completion callback. */
+	if (task->on_complete) {
+		task->on_complete(worker, &task->req, task->baton);
+	}
 	/* Return handle to the event loop in case
 	 * it was exclusively taken by this task. */
 	if (task->source.handle && !uv_has_ref(task->source.handle)) {
@@ -166,7 +174,6 @@ static void qr_task_free(uv_handle_t *handle)
 		io_start_read(task->source.handle);
 	}
 	/* Return mempool to ring or free it if it's full */
-	struct worker_ctx *worker = task->worker;
 	void *mp_context = task->req.pool.ctx;
 	if (worker->pools.len < MP_FREELIST_SIZE) {
 		mp_flush(mp_context);
@@ -416,9 +423,9 @@ int worker_exec(struct worker_ctx *worker, uv_handle_t *handle, knot_pkt_t *quer
 	return qr_task_step(task, query);
 }
 
-int worker_resolve(struct worker_ctx *worker, knot_pkt_t *query, unsigned options)
+int worker_resolve(struct worker_ctx *worker, knot_pkt_t *query, unsigned options, worker_cb_t on_complete, void *baton)
 {
-	if (!worker) {
+	if (!worker || !query) {
 		return kr_error(EINVAL);
 	}
 
@@ -427,6 +434,8 @@ int worker_resolve(struct worker_ctx *worker, knot_pkt_t *query, unsigned option
 	if (!task) {
 		return kr_error(ENOMEM);
 	}
+	task->baton = baton;
+	task->on_complete = on_complete;
 	task->req.options |= options;
 	return qr_task_step(task, query);
 }
diff --git a/daemon/worker.h b/daemon/worker.h
index f2cf3db5b2b10f7a691ef4ace56268cd60499a77..537281b61002a6a781e9e14cd3fdb3c66b37cc3a 100644
--- a/daemon/worker.h
+++ b/daemon/worker.h
@@ -48,6 +48,9 @@ struct worker_ctx {
 	mm_ctx_t pkt_pool;
 };
 
+/* Worker callback */
+typedef void (*worker_cb_t)(struct worker_ctx *worker, struct kr_request *req, void *baton);
+
 /**
  * Process incoming packet (query or answer to subrequest).
  * @return 0 or an error code
@@ -58,7 +61,7 @@ int worker_exec(struct worker_ctx *worker, uv_handle_t *handle, knot_pkt_t *quer
  * Schedule query for resolution.
  * @return 0 or an error code
  */
-int worker_resolve(struct worker_ctx *worker, knot_pkt_t *query, unsigned options);
+int worker_resolve(struct worker_ctx *worker, knot_pkt_t *query, unsigned options, worker_cb_t on_complete, void *baton);
 
 /** Reserve worker buffers */
 int worker_reserve(struct worker_ctx *worker, size_t ring_maxlen);
diff --git a/doc/build.rst b/doc/build.rst
index 32e04d5a25259bedd94b304ea6939c16986376d6..9ccd0c6ad062070c81ffbd2139edc0a0b9b7d6c9 100644
--- a/doc/build.rst
+++ b/doc/build.rst
@@ -30,7 +30,7 @@ The following is a list of software required to build Knot DNS Resolver from sou
    "`GNU Make`_ 3.80+", "*all*", "*(build only)*"
    "`pkg-config`_", "*all*", "*(build only)* [#]_"
    "C compiler", "*all*", "*(build only)* [#]_"
-   "libknot_ 2.0+", "*all*", "Knot DNS library."
+   "libknot_ 2.0+", "*all*", "Knot DNS library (requires autotools, GnuTLS and Jansson)."
    "LuaJIT_ 2.0+", "``daemon``", "Embedded scripting language (Lua_ 5.1+ with limitations)."
    "libuv_ 1.0+", "``daemon``", "Multiplatform I/O and services."
 
@@ -43,7 +43,6 @@ There are also *optional* packages that enable specific functionality in Knot DN
    "hiredis_", "``modules/redis``", "To build redis backend module."
    "cmocka_", "``unit tests``", "Unit testing framework."
    "Python_", "``integration tests``", "For test scripts."
-   "GCCGO_",  "``modules/go``", "For building Go modules, see modules documentation."
    "Doxygen_", "``documentation``", "Generating API documentation."
    "Sphinx_", "``documentation``", "Building this HTML/PDF documentation."
    "breathe_", "``documentation``", "Exposing Doxygen API doc to Sphinx."
@@ -85,7 +84,7 @@ When you have all the dependencies ready, you can build and install.
    $ make PREFIX="/usr/local"
    $ make install
 
-.. note:: Always build with ``PREFIX`` if you want to install, as it is hardcoded in the executable for module search path.
+.. note:: Always build with ``PREFIX`` if you want to install, as it is hardcoded in the executable for module search path. If you build the binary with ``-DNDEBUG``, verbose logging will be disabled as well.
 
 Alternatively you can build only specific parts of the project, i.e. ``library``.
 
@@ -96,17 +95,6 @@ Alternatively you can build only specific parts of the project, i.e. ``library``
 
 .. note:: Documentation is not built by default, run ``make doc`` to build it.
 
-Debug build
------------
-
-For debugging or tinkering purposes, it's useful to build the daemon with the debug messages enabled.
-
-.. code-block:: bash
-
-   $ CFLAGS="-O0 -g -DWITH_DEBUG" make
-
-.. warning:: If you want to track specific things like i.e. number of subrequests for given zone in production, use Lua modules or write custom layers rather then depending on debug output.
-
 Building dependencies
 ~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/doc/index.rst b/doc/index.rst
index 0cefc69db8e63be5f2dafd58daca0fa575021a3d..d5ac52459bfdcabe33c11602976f8140297c3e1b 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -13,6 +13,7 @@ Modular architecture of the library keeps the core tiny and efficient, and provi
    lib
    daemon
    modules
+   modules_api
 
 
 Indices and tables
diff --git a/doc/modules.rst b/doc/modules.rst
index 0ae3490d41bbb0aaa11bffa374175454e39aa508..491d469b730ac24682820f76b1e3f8e31f83232d 100644
--- a/doc/modules.rst
+++ b/doc/modules.rst
@@ -1,9 +1,8 @@
-.. include:: ../modules/README.rst
-
 .. _modules-implemented:
 
-Implemented modules
-===================
+*************************
+Knot DNS Resolver modules
+*************************
 
 .. contents::
    :depth: 1
@@ -18,4 +17,4 @@ Implemented modules
 .. include:: ../modules/kmemcached/README.rst
 .. include:: ../modules/redis/README.rst
 .. include:: ../modules/ketcd/README.rst
-.. include:: ../modules/cachectl/README.rst
+.. include:: ../modules/cachectl/README.rst
\ No newline at end of file
diff --git a/doc/modules_api.rst b/doc/modules_api.rst
new file mode 100644
index 0000000000000000000000000000000000000000..2f0f14738561e3f68e426358975509320ed529a4
--- /dev/null
+++ b/doc/modules_api.rst
@@ -0,0 +1 @@
+.. include:: ../modules/README.rst
\ No newline at end of file
diff --git a/help.mk b/help.mk
index 1ceef2f30b38fffece14fa10e55776d67c5f3e42..fc43b911487ba6e790aaf67baccaf249315a1d52 100644
--- a/help.mk
+++ b/help.mk
@@ -11,14 +11,17 @@ info:
 	$(info )
 	$(info Dependencies)
 	$(info ------------)
-	$(info [$(HAS_doxygen)] doxygen (doc))
 	$(info [$(HAS_libknot)] libknot (lib))
 	$(info [$(HAS_lua)] lua (daemon))
 	$(info [$(HAS_libuv)] libuv (daemon))
-	$(info [$(HAS_cmocka)] cmocka (tests/unit))
-	$(info [$(HAS_python)] Python (tests/integration))
+	$(info )
+	$(info Optional)
+	$(info --------)
+	$(info [$(HAS_doxygen)] doxygen (doc))
 	$(info [$(HAS_gccgo)] GCCGO (modules/go))
 	$(info [$(HAS_libmemcached)] libmemcached (modules/memcached))
 	$(info [$(HAS_hiredis)] hiredis (modules/redis))
+	$(info [$(HAS_cmocka)] cmocka (tests/unit))
+	$(info [$(HAS_python)] Python (tests/integration))
 	$(info [$(HAS_socket_wrapper)] socket_wrapper (lib))
 	$(info )
diff --git a/lib/cache.c b/lib/cache.c
index 4e168d95f8fa3ad1ddbd9c56aebf61ed037557f9..01fdb62cab313191605ee2ebb2c01b39f5f18ea8 100644
--- a/lib/cache.c
+++ b/lib/cache.c
@@ -324,3 +324,49 @@ int kr_cache_insert_rr(struct kr_cache_txn *txn, const knot_rrset_t *rr, uint32_
 	namedb_val_t data = { rr->rrs.data, knot_rdataset_size(&rr->rrs) };
 	return kr_cache_insert(txn, KR_CACHE_RR, rr->owner, rr->type, &header, data);
 }
+
+int kr_cache_peek_rrsig(struct kr_cache_txn *txn, knot_rrset_t *rr, uint32_t *timestamp)
+{
+	if (!txn || !rr || !timestamp) {
+		return kr_error(EINVAL);
+	}
+
+	/* Check if the RRSet is in the cache. */
+	struct kr_cache_entry *entry = NULL;
+	int ret = kr_cache_peek(txn, KR_CACHE_RRSIG, rr->owner, rr->type, &entry, timestamp);
+	if (ret != 0) {
+		return ret;
+	}
+	rr->type = KNOT_RRTYPE_RRSIG;
+	rr->rrs.rr_count = entry->count;
+	rr->rrs.data = entry->data;
+	return kr_ok();
+}
+
+int kr_cache_insert_rrsig(struct kr_cache_txn *txn, const knot_rrset_t *rr, uint16_t typec, uint32_t timestamp)
+{
+	if (!txn || !rr) {
+		return kr_error(EINVAL);
+	}
+
+	/* Ignore empty records */
+	if (knot_rrset_empty(rr)) {
+		return kr_ok();
+	}
+
+	/* Prepare header to write */
+	struct kr_cache_entry header = {
+		.timestamp = timestamp,
+		.ttl = 0,
+		.count = rr->rrs.rr_count
+	};
+	for (uint16_t i = 0; i < rr->rrs.rr_count; ++i) {
+		knot_rdata_t *rd = knot_rdataset_at(&rr->rrs, i);
+		if (knot_rdata_ttl(rd) > header.ttl) {
+			header.ttl = knot_rdata_ttl(rd);
+		}
+	}
+
+	namedb_val_t data = { rr->rrs.data, knot_rdataset_size(&rr->rrs) };
+	return kr_cache_insert(txn, KR_CACHE_RRSIG, rr->owner, typec, &header, data);
+}
diff --git a/lib/cache.h b/lib/cache.h
index c26ab150f3c260c2df19f68ca3ed1253ac922524..7edd42169de24c67cf9d2e7b8d385f719beec936 100644
--- a/lib/cache.h
+++ b/lib/cache.h
@@ -24,6 +24,7 @@ enum kr_cache_tag {
 	KR_CACHE_RR   = 'R',
 	KR_CACHE_PKT  = 'P',
 	KR_CACHE_SEC  = 'S',
+	KR_CACHE_RRSIG = 'G',
 	KR_CACHE_USER = 0x80
 };
 
@@ -173,3 +174,24 @@ int kr_cache_materialize(knot_rrset_t *dst, const knot_rrset_t *src, uint32_t dr
  * @return 0 or an errcode
  */
 int kr_cache_insert_rr(struct kr_cache_txn *txn, const knot_rrset_t *rr, uint32_t timestamp);
+
+/**
+ * Peek the cache for the given RRset signature (name, type)
+ * @note The RRset type must not be RRSIG but instead it must equal the type covered field of the sought RRSIG.
+ * @param txn transaction instance
+ * @param rr query RRSET (its rdataset and type may be changed depending on the result)
+ * @param timestamp current time (will be replaced with drift if successful)
+ * @return 0 or an errcode
+ */
+int kr_cache_peek_rrsig(struct kr_cache_txn *txn, knot_rrset_t *rr, uint32_t *timestamp);
+
+/**
+ * Insert the selected RRSIG RRSet of the selected type covered into cache, replacing any existing data.
+ * @note The RRSet must contain RRSIGS with only the specified type covered.
+ * @param txn transaction instance
+ * @param rr inserted RRSIG RRSet
+ * @param typec type covered of the RDATA
+ * @param timestamp current time
+ * @return 0 or an errcode
+ */
+int kr_cache_insert_rrsig(struct kr_cache_txn *txn, const knot_rrset_t *rr, uint16_t typec, uint32_t timestamp);
diff --git a/lib/dnssec.c b/lib/dnssec.c
new file mode 100644
index 0000000000000000000000000000000000000000..4359826a7ef2d24b93477c5a4560d86e5e578e35
--- /dev/null
+++ b/lib/dnssec.c
@@ -0,0 +1,379 @@
+/*  Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <dnssec/binary.h>
+#include <dnssec/crypto.h>
+#include <dnssec/error.h>
+#include <dnssec/key.h>
+#include <dnssec/sign.h>
+#include <libknot/descriptor.h>
+#include <libknot/packet/wire.h>
+#include <libknot/rdataset.h>
+#include <libknot/rrset.h>
+#include <libknot/rrtype/dnskey.h>
+#include <libknot/rrtype/nsec.h>
+#include <libknot/rrtype/rrsig.h>
+
+
+#include "lib/defines.h"
+#include "lib/dnssec/nsec.h"
+#include "lib/dnssec/nsec3.h"
+#include "lib/dnssec/signature.h"
+#include "lib/dnssec.h"
+
+#define DEBUG_MSG(fmt...) fprintf(stderr, fmt)
+
+void kr_crypto_init(void)
+{
+	dnssec_crypto_init();
+}
+
+void kr_crypto_cleanup(void)
+{
+	dnssec_crypto_cleanup();
+}
+
+void kr_crypto_reinit(void)
+{
+	dnssec_crypto_reinit();
+}
+
+#define FLG_WILDCARD_EXPANSION 0x01 /**< Possibly generated by using wildcard expansion. */
+
+/**
+ * Check the RRSIG RR validity according to RFC4035 5.3.1 .
+ * @param flags     The flags are going to be set according to validation result.
+ * @param covered   RRSet to be checked.
+ * @param rrsigs    RRSet containing the signatures.
+ * @param sig_pos   Specifies the signature within the RRSIG RRSet.
+ * @param keys      Associated DNSKEY RRSet.
+ * @param key_pos   Specifies the key within the DNSKEY RRSet,
+ * @param key       Parsed key (converted for direct usage).
+ * @param zone_name The name of the zone cut.
+ * @param timestamp Validation time.
+ */
+static int validate_rrsig_rr(int *flags, const knot_rrset_t *covered,
+                             const knot_rrset_t *rrsigs, size_t sig_pos,
+                             const knot_rrset_t *keys, size_t key_pos, const dnssec_key_t *key,
+                             const knot_dname_t *zone_name, uint32_t timestamp)
+{
+	if (!flags || !covered || !rrsigs || !keys || !key || !zone_name) {
+		return kr_error(EINVAL);
+	}
+	/* bullet 1 (presume same compression for the owner) */
+	if ((covered->rclass != rrsigs->rclass) || !knot_dname_is_equal(covered->owner, rrsigs->owner)) {
+		return kr_error(EINVAL);
+	}
+	/* bullet 2 */
+	const knot_dname_t *signer_name = knot_rrsig_signer_name(&rrsigs->rrs, sig_pos);
+	if (signer_name == NULL) {
+		return kr_error(EINVAL);
+	}
+	if (knot_dname_cmp(signer_name, zone_name) != 0) {
+		return kr_error(EINVAL);
+	}
+	/* bullet 3 */
+	uint16_t tcovered = knot_rrsig_type_covered(&rrsigs->rrs, sig_pos);
+	if (tcovered != covered->type) {
+		return kr_error(EINVAL);
+	}
+	/* bullet 4 */
+	{
+		int rrsig_labels = knot_rrsig_labels(&rrsigs->rrs, sig_pos);
+		int dname_labels = knot_dname_labels(covered->owner, NULL);
+		if (knot_dname_is_wildcard(covered->owner)) {
+			/* The asterisk does not count, RFC4034 3.1.3, paragraph 3. */
+			--dname_labels;
+		}
+		if (rrsig_labels > dname_labels) {
+			return kr_error(EINVAL);
+		}
+		if (rrsig_labels < dname_labels) {
+			*flags |= FLG_WILDCARD_EXPANSION;
+		}
+	}
+	/* bullet 5 */
+	if (knot_rrsig_sig_expiration(&rrsigs->rrs, sig_pos) < timestamp) {
+		return kr_error(EINVAL);
+	}
+	/* bullet 6 */
+	if (knot_rrsig_sig_inception(&rrsigs->rrs, sig_pos) > timestamp) {
+		return kr_error(EINVAL);
+	}
+	/* bullet 7 */
+	if ((knot_dname_cmp(keys->owner, signer_name) != 0) ||
+	    (knot_dnskey_alg(&keys->rrs, key_pos) != knot_rrsig_algorithm(&rrsigs->rrs, sig_pos)) ||
+	    (dnssec_key_get_keytag(key) != knot_rrsig_key_tag(&rrsigs->rrs, sig_pos))) {
+		return kr_error(EINVAL);
+	}
+	/* bullet 8 */
+	/* Checked somewhere else. */
+	/* bullet 9 and 10 */
+	/* One of the requirements should be always fulfilled. */
+
+	return kr_ok();
+}
+
+/**
+ * Returns the number of labels that have been added by wildcard expansion.
+ * @param expanded Expanded wildcard.
+ * @param rrsigs   RRSet containing the signatures.
+ * @param sig_pos  Specifies the signature within the RRSIG RRSet.
+ * @return         Number of added labels, -1 on error.
+ */
+static int wildcard_radix_len_diff(const knot_dname_t *expanded,
+                                   const knot_rrset_t *rrsigs, size_t sig_pos)
+{
+	if (!expanded || !rrsigs) {
+		return -1;
+	}
+
+	return knot_dname_labels(expanded, NULL) - knot_rrsig_labels(&rrsigs->rrs, sig_pos);
+}
+
+int kr_rrset_validate(const knot_pkt_t *pkt, knot_section_t section_id,
+                      const knot_rrset_t *covered, const knot_rrset_t *keys,
+                      const knot_dname_t *zone_name, uint32_t timestamp,
+                      bool has_nsec3)
+{
+	if (!pkt || !covered || !keys || !zone_name) {
+		return kr_error(EINVAL);
+	}
+
+	int ret = kr_error(ENOENT);
+	for (unsigned i = 0; i < keys->rrs.rr_count; ++i) {
+		ret = kr_rrset_validate_with_key(pkt, section_id, covered, keys, i, NULL, zone_name, timestamp, has_nsec3);
+		if (ret == 0) {
+			break;
+		}
+	}
+
+	return ret;
+}
+
+int kr_rrset_validate_with_key(const knot_pkt_t *pkt, knot_section_t section_id,
+                               const knot_rrset_t *covered, const knot_rrset_t *keys,
+                               size_t key_pos, const struct dseckey *key,
+                               const knot_dname_t *zone_name, uint32_t timestamp,
+                               bool has_nsec3)
+{
+	int ret;
+	int val_flgs;
+	struct dseckey *created_key = NULL;
+	int trim_labels;
+	if (key == NULL) {
+		const knot_rdata_t *krr = knot_rdataset_at(&keys->rrs, key_pos);
+		ret = kr_dnssec_key_from_rdata(&created_key, keys->owner,
+			                       knot_rdata_data(krr), knot_rdata_rdlen(krr));
+		if (ret != 0) {
+			return ret;
+		}
+		key = created_key;
+	}
+
+	ret = kr_error(ENOENT);
+	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
+	for (unsigned i = 0; i < sec->count; ++i) {
+		/* Try every RRSIG. */
+		const knot_rrset_t *rrsig = knot_pkt_rr(sec, i);
+		if (rrsig->type != KNOT_RRTYPE_RRSIG) {
+			continue;
+		}
+		for (uint16_t j = 0; j < rrsig->rrs.rr_count; ++j) {
+			val_flgs = 0;
+			trim_labels = 0;
+			if (validate_rrsig_rr(&val_flgs, covered, rrsig, j,
+			                      keys, key_pos, (dnssec_key_t *) key,
+			                      zone_name, timestamp) != 0) {
+				continue;
+			}
+			if (val_flgs & FLG_WILDCARD_EXPANSION) {
+				trim_labels = wildcard_radix_len_diff(covered->owner, rrsig, j);
+				if (trim_labels < 0) {
+					break;
+				}
+			}
+			if (kr_check_signature(rrsig, j, (dnssec_key_t *) key, covered, trim_labels) != 0) {
+				continue;
+			}
+			if (val_flgs & FLG_WILDCARD_EXPANSION) {
+				if (!has_nsec3) {
+					ret = kr_nsec_wildcard_answer_response_check(pkt, KNOT_AUTHORITY, covered->owner);
+				} else {
+					ret = kr_nsec3_wildcard_answer_response_check(pkt, KNOT_AUTHORITY, covered->owner, trim_labels - 1);
+				}
+				if (ret != 0) {
+					continue;
+				}
+			}
+			ret = kr_ok();
+			break;
+		}
+		if (ret == kr_ok()) {
+			break;
+		}
+	}
+
+	kr_dnssec_key_free(&created_key);
+	return ret;
+}
+
+int kr_dnskeys_trusted(const knot_pkt_t *pkt, knot_section_t section_id, const knot_rrset_t *keys,
+                       const knot_rrset_t *ta, const knot_dname_t *zone_name, uint32_t timestamp,
+                       bool has_nsec3)
+{
+	if (!pkt || !keys || !ta) {
+		return kr_error(EINVAL);
+	}
+
+	/* RFC4035 5.2, bullet 1
+	 * The supplied DS record has been authenticated.
+	 * It has been validated or is part of a configured trust anchor.
+	 */
+	for (uint16_t i = 0; i < keys->rrs.rr_count; ++i) {
+		/* RFC4035 5.3.1, bullet 8 */ /* ZSK */
+		const knot_rdata_t *krr = knot_rdataset_at(&keys->rrs, i);
+		const uint8_t *key_data = knot_rdata_data(krr);
+		if (!kr_dnssec_key_zsk(key_data) || kr_dnssec_key_revoked(key_data)) {
+			continue;
+		}
+		
+		struct dseckey *key;
+		if (kr_dnssec_key_from_rdata(&key, keys->owner, key_data, knot_rdata_rdlen(krr)) != 0) {
+			continue;
+		}
+		if (kr_authenticate_referral(ta, (dnssec_key_t *) key) != 0) {
+			kr_dnssec_key_free(&key);
+			continue;
+		}
+		if (kr_rrset_validate_with_key(pkt, section_id, keys, keys, i, key, zone_name, timestamp, has_nsec3) != 0) {
+			kr_dnssec_key_free(&key);
+			continue;
+		}
+		kr_dnssec_key_free(&key);
+		return kr_ok();
+	}
+	/* No useable key found */
+	return kr_error(ENOENT);
+}
+
+bool kr_dnssec_key_zsk(const uint8_t *dnskey_rdata)
+{
+	return wire_read_u16(dnskey_rdata) & 0x0100;
+}
+
+bool kr_dnssec_key_ksk(const uint8_t *dnskey_rdata)
+{
+	return wire_read_u16(dnskey_rdata) & 0x0001;
+}
+
+/** Return true if the DNSKEY is revoked. */
+bool kr_dnssec_key_revoked(const uint8_t *dnskey_rdata)
+{
+	return wire_read_u16(dnskey_rdata) & 0x0080;
+}
+
+int kr_dnssec_key_tag(uint16_t rrtype, const uint8_t *rdata, size_t rdlen)
+{
+	if (!rdata || rdlen == 0 || (rrtype != KNOT_RRTYPE_DS && rrtype != KNOT_RRTYPE_DNSKEY)) {
+		return kr_error(EINVAL);
+	}
+	if (rrtype == KNOT_RRTYPE_DS) {
+		return wire_read_u16(rdata);
+	} else if (rrtype == KNOT_RRTYPE_DNSKEY) {
+		struct dseckey *key = NULL;
+		int ret = kr_dnssec_key_from_rdata(&key, NULL, rdata, rdlen);
+		if (ret != 0) {
+			return ret;
+		}
+		uint16_t keytag = dnssec_key_get_keytag((dnssec_key_t *)key);
+		kr_dnssec_key_free(&key);
+		return keytag;
+	} else {
+		return kr_error(EINVAL);
+	}
+}
+
+int kr_dnssec_key_match(const uint8_t *key_a_rdata, size_t key_a_rdlen,
+                        const uint8_t *key_b_rdata, size_t key_b_rdlen)
+{
+	dnssec_key_t *key_a = NULL, *key_b = NULL;
+	int ret = kr_dnssec_key_from_rdata((struct dseckey **)&key_a, NULL, key_a_rdata, key_a_rdlen);
+	if (ret != 0) {
+		return ret;
+	}
+	ret = kr_dnssec_key_from_rdata((struct dseckey **)&key_b, NULL, key_b_rdata, key_b_rdlen);
+	if (ret != 0) {
+		dnssec_key_free(key_a);
+		return ret;
+	}
+	/* If the algorithm and the public key match, we can be sure
+	 * that they are the same key. */
+	ret = kr_error(ENOENT);
+	dnssec_binary_t pk_a, pk_b;
+	if (dnssec_key_get_algorithm(key_a) == dnssec_key_get_algorithm(key_b) &&
+	    dnssec_key_get_pubkey(key_a, &pk_a) == DNSSEC_EOK &&
+	    dnssec_key_get_pubkey(key_b, &pk_b) == DNSSEC_EOK) {
+		if (pk_a.size == pk_b.size && memcmp(pk_a.data, pk_b.data, pk_a.size) == 0) {
+			ret = 0;
+		}
+	}
+	dnssec_key_free(key_a);
+	dnssec_key_free(key_b);
+	return ret;
+}
+
+int kr_dnssec_key_from_rdata(struct dseckey **key, const knot_dname_t *kown, const uint8_t *rdata, size_t rdlen)
+{
+	if (!key || !rdata || rdlen == 0) {
+		return kr_error(EINVAL);
+	}
+
+	dnssec_key_t *new_key = NULL;
+	const dnssec_binary_t binary_key = {
+		.size = rdlen,
+		.data = (uint8_t *)rdata
+	};
+
+	int ret = dnssec_key_new(&new_key);
+	if (ret != DNSSEC_EOK) {
+		return kr_error(ENOMEM);
+	}
+	ret = dnssec_key_set_rdata(new_key, &binary_key);
+	if (ret != DNSSEC_EOK) {
+		dnssec_key_free(new_key);
+		return kr_error(ENOMEM);
+	}
+	if (kown) {
+		ret = dnssec_key_set_dname(new_key, kown);
+		if (ret != DNSSEC_EOK) {
+			dnssec_key_free(new_key);
+			return kr_error(ENOMEM);
+		}
+	}
+
+	*key = (struct dseckey *) new_key;
+	return kr_ok();
+}
+
+void kr_dnssec_key_free(struct dseckey **key)
+{
+	assert(key);
+
+	dnssec_key_free((dnssec_key_t *) *key);
+	*key = NULL;
+}
diff --git a/lib/dnssec.h b/lib/dnssec.h
new file mode 100644
index 0000000000000000000000000000000000000000..7679f730a2a41d7a410248687b09b376bda29d3f
--- /dev/null
+++ b/lib/dnssec.h
@@ -0,0 +1,132 @@
+/*  Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <libknot/internal/consts.h>
+#include <libknot/packet/pkt.h>
+
+/**
+ * Initialise cryptographic back-end.
+ */
+void kr_crypto_init(void);
+
+/**
+ * De-initialise cryptographic back-end.
+ */
+void kr_crypto_cleanup(void);
+
+/**
+ * Re-initialise cryptographic back-end.
+ * @note Must be called after fork() in the child.
+ */
+void kr_crypto_reinit(void);
+
+/** Opaque DNSSEC key pointer. */
+struct dseckey;
+
+/**
+ * Validate RRSet.
+ * @param pkt        Packet to be validated.
+ * @param section_id Section to work with.
+ * @param covered    RRSet covered by a signature. It must be in canonical format.
+ * @param keys       DNSKEY RRSet.
+ * @param zone_name  Name of the zone containing the RRSIG RRSet.
+ * @param timestamp  Validation time.
+ * @param has_nsec3  Whether to use NSEC3 validation.
+ * @return           0 or error code.
+ */
+int kr_rrset_validate(const knot_pkt_t *pkt, knot_section_t section_id,
+                      const knot_rrset_t *covered, const knot_rrset_t *keys,
+                      const knot_dname_t *zone_name, uint32_t timestamp,
+                      bool has_nsec3);
+
+/**
+ * Validate RRSet using a specific key.
+ * @param pkt        Packet to be validated.
+ * @param section_id Section to work with.
+ * @param covered    RRSet covered by a signature. It must be in canonical format.
+ * @param keys       DNSKEY RRSet.
+ * @param key_pos    Position of the key to be validated with.
+ * @param key        Key to be used to validate. If NULL, then key from DNSKEY RRSet is used.
+ * @param zone_name  Name of the zone containing the RRSIG RRSet.
+ * @param timestamp  Validation time.
+ * @param has_nsec3  Whether to use NSEC3 validation.
+ * @return           0 or error code.
+ */
+int kr_rrset_validate_with_key(const knot_pkt_t *pkt, knot_section_t section_id,
+                               const knot_rrset_t *covered, const knot_rrset_t *keys,
+                               size_t key_pos, const struct dseckey *key,
+                               const knot_dname_t *zone_name, uint32_t timestamp,
+                               bool has_nsec3);
+
+/**
+ * Check whether the DNSKEY rrset matches the supplied trust anchor RRSet.
+ * @param pkt        Packet to be validated.
+ * @param section_id Section to work with.
+ * @param keys       DNSKEY RRSet to check.
+ * @param ta         Trust anchor RRSet against which to validate the DNSKEY RRSet.
+ * @param zone_name  Name of the zone containing the RRSet.
+ * @param timestamp  Time stamp.
+ * @param has_nsec3  Whether to use NSEC3 validation.
+ * @return     0 or error code.
+ */
+int kr_dnskeys_trusted(const knot_pkt_t *pkt, knot_section_t section_id, const knot_rrset_t *keys,
+                       const knot_rrset_t *ta, const knot_dname_t *zone_name, uint32_t timestamp,
+                       bool has_nsec3);
+
+/** Return true if the DNSKEY can be used as a ZSK.  */
+bool kr_dnssec_key_zsk(const uint8_t *dnskey_rdata);
+
+/** Return true if the DNSKEY indicates being KSK (=> has SEP).  */
+bool kr_dnssec_key_ksk(const uint8_t *dnskey_rdata);
+
+/** Return true if the DNSKEY is revoked. */
+bool kr_dnssec_key_revoked(const uint8_t *dnskey_rdata);
+
+/** Return DNSKEY tag.
+  * @param rrtype RR type (either DS or DNSKEY are supported)
+  * @param rdata  Key/digest RDATA.
+  * @param rdlen  RDATA length.
+  * @return Key tag (positive number), or an error code
+  */
+int kr_dnssec_key_tag(uint16_t rrtype, const uint8_t *rdata, size_t rdlen);
+
+/** Return 0 if the two keys are identical.
+  * @note This compares RDATA only, algorithm and public key must match.
+  * @param key_a_rdata First key RDATA
+  * @param key_a_rdlen First key RDATA length
+  * @param key_b_rdata Second key RDATA
+  * @param key_b_rdlen Second key RDATA length
+  * @return 0 if they match or an error code
+  */
+int kr_dnssec_key_match(const uint8_t *key_a_rdata, size_t key_a_rdlen,
+                        const uint8_t *key_b_rdata, size_t key_b_rdlen);
+
+/**
+ * Construct a DNSSEC key.
+ * @param key   Pointer to be set to newly created DNSSEC key.
+ * @param kown  DNSKEY owner name.
+ * @param rdata DNSKEY RDATA
+ * @param rdlen DNSKEY RDATA length
+ */
+int kr_dnssec_key_from_rdata(struct dseckey **key, const knot_dname_t *kown, const uint8_t *rdata, size_t rdlen);
+
+/**
+ * Frees the DNSSEC key.
+ * @param key Pointer to freed key.
+ */
+void kr_dnssec_key_free(struct dseckey **key);
diff --git a/lib/dnssec/nsec.c b/lib/dnssec/nsec.c
new file mode 100644
index 0000000000000000000000000000000000000000..1f82482701a08c345c3df02d6f80edda0db7032a
--- /dev/null
+++ b/lib/dnssec/nsec.c
@@ -0,0 +1,471 @@
+/*  Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include <libknot/descriptor.h>
+#include <libknot/dname.h>
+#include <libknot/packet/wire.h>
+#include <libknot/rrset.h>
+#include <libknot/rrtype/nsec.h>
+#include <libknot/rrtype/rrsig.h>
+
+#include "lib/defines.h"
+#include "lib/dnssec/nsec.h"
+
+bool kr_nsec_bitmap_contains_type(const uint8_t *bm, uint16_t bm_size, uint16_t type)
+{
+	if (!bm && !bm_size) {
+		return false;
+	}
+
+	uint8_t sought_win = (type >> 8 ) & 0xff;
+	uint8_t bitmap_idx = (type >> 3) & 0x1f;
+	uint8_t bitmap_bit_mask = 1 << (7 - (type & 0x07));
+
+	size_t bm_pos = 0;
+	while (bm_pos < bm_size) {
+		uint8_t win = bm[bm_pos++];
+		uint8_t win_size = bm[bm_pos++];
+
+		if (win == sought_win) {
+			if (win_size >= bitmap_idx) {
+				return bm[bm_pos + bitmap_idx] & bitmap_bit_mask;
+			}
+			return false;
+		}
+
+		bm_pos += win_size;
+	}
+
+	return false;
+}
+
+/**
+ * Check whether the NSEC RR proves that there is no closer match for <SNAME, SCLASS>.
+ * @param nsec  NSEC RRSet.
+ * @param sname Searched name.
+ * @return      0 or error code.
+ */
+static int nsec_nonamematch(const knot_rrset_t *nsec, const knot_dname_t *sname)
+{
+	assert(nsec && sname);
+
+	const knot_dname_t *next = knot_nsec_next(&nsec->rrs);
+
+	if ((knot_dname_cmp(nsec->owner, sname) < 0) &&
+	    (knot_dname_cmp(sname, next) < 0)) {
+		return kr_ok();
+	} else {
+		return kr_error(EINVAL);
+	}
+}
+
+#define FLG_NOEXIST_RRTYPE (1 << 0) /**< <SNAME, SCLASS> exists, <SNAME, SCLASS, STYPE> does not exist. */
+#define FLG_NOEXIST_RRSET  (1 << 1) /**< <SNAME, SCLASS> does not exist. */
+#define FLG_NOEXIST_WILDCARD (1 << 2) /**< No wildcard covering <SNAME, SCLASS> exists. */
+#define FLG_NOEXIST_CLOSER (1 << 3) /**< Wildcard covering <SNAME, SCLASS> exists, but doesn't match STYPE. */
+
+/**
+ * According to set flags determine whether authenticated denial of existence has been proven.
+ * @param f Flags to inspect.
+ * @return  True if denial of existence proven.
+ */
+#define kr_nsec_existence_denied(f) \
+	(((f) & (FLG_NOEXIST_RRTYPE | FLG_NOEXIST_RRSET)) && ((f) & FLG_NOEXIST_WILDCARD))
+
+/**
+ * Name error response check (RFC4035 3.1.3.2; RFC4035 5.4, bullet 2).
+ * @note Returned flags must be checked in order to prove denial.
+ * @param flags Flags to be set according to check outcome.
+ * @param nsec  NSEC RR.
+ * @param name  Name to be checked.
+ * @param pool
+ * @return      0 or error code.
+ */
+static int name_error_response_check_rr(int *flags, const knot_rrset_t *nsec,
+                                        const knot_dname_t *name, mm_ctx_t *pool)
+{
+	assert(flags && nsec && name);
+
+	if (nsec_nonamematch(nsec, name) == 0) {
+		*flags |= FLG_NOEXIST_RRSET;
+	}
+
+	knot_dname_t *name_copy = knot_dname_copy(name, pool);
+	if (!name_copy) {
+		return kr_error(ENOMEM);
+	}
+	knot_dname_t *ptr = name_copy;
+	while (ptr[0]) {
+		/* Remove leftmost label and replace it with '*.'. */
+		ptr = (uint8_t *) knot_wire_next_label(ptr, NULL);
+		*(--ptr) = '*';
+		*(--ptr) = 1;
+
+		if (nsec_nonamematch(nsec, ptr) == 0) {
+			*flags |= FLG_NOEXIST_WILDCARD;
+			break;
+		}
+
+		/* Remove added leftmost asterisk. */
+		ptr += 2;
+	}
+
+	knot_dname_free(&name_copy, pool);
+	return kr_ok();
+}
+
+int kr_nsec_name_error_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                      const knot_dname_t *sname, mm_ctx_t *pool)
+{
+	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
+	if (!sec || !sname) {
+		return kr_error(EINVAL);
+	}
+
+	int ret = kr_error(ENOENT);
+	int flags = 0;
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
+		if (rrset->type != KNOT_RRTYPE_NSEC) {
+			continue;
+		}
+		ret = name_error_response_check_rr(&flags, rrset, sname, pool);
+		if (ret != 0) {
+			return ret;
+		}
+	}
+
+	return kr_nsec_existence_denied(flags) ? kr_ok() : kr_error(ENOENT);
+}
+
+/**
+ * Returns the labels from the covering RRSIG RRs.
+ * @note The number must be the same in all covering RRSIGs.
+ * @param nsec NSEC RR.
+ * @param sec  Packet section.
+ * @param      Number of labels or (negative) error code.
+ */
+static int coverign_rrsig_labels(const knot_rrset_t *nsec, const knot_pktsection_t *sec)
+{
+	assert(nsec && sec);
+
+	int ret = kr_error(ENOENT);
+
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
+		if ((rrset->type != KNOT_RRTYPE_RRSIG) ||
+		    (!knot_dname_is_equal(rrset->owner, nsec->owner))) {
+			continue;
+		}
+
+		for (uint16_t j = 0; j < rrset->rrs.rr_count; ++j) {
+			if (knot_rrsig_type_covered(&rrset->rrs, j) != KNOT_RRTYPE_NSEC) {
+				continue;
+			}
+
+			if (ret < 0) {
+				ret = knot_rrsig_labels(&rrset->rrs, j);
+			} else {
+				if (ret != knot_rrsig_labels(&rrset->rrs, j)) {
+					return kr_error(EINVAL);
+				}
+			}
+		}
+	}
+
+	return ret;
+}
+
+/**
+ * Perform check of RR type existence denial according to RFC4035 5.4, bullet 1.
+ * @param flags Flags to be set according to check outcome.
+ * @param nsec  NSEC RR.
+ * @param type  Type to be checked.
+ * @return      0 or error code.
+ */
+static int no_data_response_check_rrtype(int *flags, const knot_rrset_t *nsec,
+                                         uint16_t type)
+{
+	assert(flags && nsec);
+
+	uint8_t *bm = NULL;
+	uint16_t bm_size;
+	knot_nsec_bitmap(&nsec->rrs, &bm, &bm_size);
+	if (!bm) {
+		return kr_error(EINVAL);
+	}
+
+	if (!kr_nsec_bitmap_contains_type(bm, bm_size, type)) {
+		/* The type is not listed in the NSEC bitmap. */
+		*flags |= FLG_NOEXIST_RRTYPE;
+	}
+
+	return kr_ok();
+}
+
+/**
+ * Perform check for RR type wildcard existence denial according to RFC4035 5.4, bullet 1.
+ * @param flags Flags to be set according to check outcome.
+ * @param nsec  NSEC RR.
+ * @param sec   Packet section to work with.
+ * @return      0 or error code.
+ */
+static int no_data_wildcard_existence_check(int *flags, const knot_rrset_t *nsec,
+                                            const knot_pktsection_t *sec)
+{
+	assert(flags && nsec && sec);
+
+	int rrsig_labels = coverign_rrsig_labels(nsec, sec);
+	if (rrsig_labels < 0) {
+		return rrsig_labels;
+	}
+	int nsec_labels = knot_dname_labels(nsec->owner, NULL);
+	if (nsec_labels < 0) {
+		return nsec_labels;
+	}
+
+	if (rrsig_labels == nsec_labels) {
+		*flags |= FLG_NOEXIST_WILDCARD;
+	}
+
+	return kr_ok();
+}
+
+int kr_nsec_no_data_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                   const knot_dname_t *sname, uint16_t stype)
+{
+	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
+	if (!sec || !sname) {
+		return kr_error(EINVAL);
+	}
+
+	int ret = kr_error(ENOENT);
+	int flags = 0;
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
+		if (rrset->type != KNOT_RRTYPE_NSEC) {
+			continue;
+		}
+		if (knot_dname_is_equal(rrset->owner, sname)) {
+			ret = no_data_response_check_rrtype(&flags, rrset, stype);
+			if (ret != 0) {
+				return ret;
+			}
+		}
+	}
+
+	return (flags & FLG_NOEXIST_RRTYPE) ? kr_ok() : kr_error(ENOENT);
+}
+
+/**
+ * Wildcard no data response check (RFC4035 3.1.3.4).
+ * @param flags Flags to be set according to check outcome.
+ * @param nsec  NSEC RR.
+ * @param name Name to be checked.
+ * @param type Type to be checked.
+ * @return      0 or error code.
+ */
+static int wildcard_no_data_response_check(int *flags, const knot_rrset_t *nsec,
+                                           const knot_dname_t *name, uint16_t type)
+{
+	assert(flags && nsec && name);
+
+	if (nsec_nonamematch(nsec, name) == 0) {
+		*flags |= FLG_NOEXIST_RRSET;
+	}
+
+	const knot_dname_t *nsec_own = nsec->owner;
+	if (knot_dname_is_wildcard(nsec_own)) {
+		nsec_own = knot_wire_next_label(nsec_own, NULL);
+
+		if (knot_dname_is_sub(name, nsec_own)) {
+			uint8_t *bm = NULL;
+			uint16_t bm_size;
+			knot_nsec_bitmap(&nsec->rrs, &bm, &bm_size);
+			if (!bm) {
+				return kr_error(EINVAL);
+			}
+
+			if (!kr_nsec_bitmap_contains_type(bm, bm_size, type)) {
+				*flags |= FLG_NOEXIST_CLOSER;
+			}
+		}
+	}
+
+	return kr_ok();
+}
+
+int kr_nsec_wildcard_no_data_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                            const knot_dname_t *sname, uint16_t stype)
+{
+	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
+	if (!sec || !sname) {
+		return kr_error(EINVAL);
+	}
+
+	int ret = kr_error(ENOENT);
+	int flags = 0;
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
+		if (rrset->type != KNOT_RRTYPE_NSEC) {
+			continue;
+		}
+		ret = wildcard_no_data_response_check(&flags, rrset, sname, stype);
+		if (ret != 0) {
+			return ret;
+		}
+	}
+
+	return ((flags & FLG_NOEXIST_RRSET) && (flags & FLG_NOEXIST_CLOSER)) ? kr_ok() : kr_error(ENOENT);
+}
+
+int kr_nsec_wildcard_answer_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                           const knot_dname_t *sname)
+{
+	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
+	if (!sec || !sname) {
+		return kr_error(EINVAL);
+	}
+
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
+		if (rrset->type != KNOT_RRTYPE_NSEC) {
+			continue;
+		}
+		if (nsec_nonamematch(rrset, sname) == 0) {
+			return kr_ok();
+		}
+	}
+
+	return kr_error(ENOENT);
+}
+
+/**
+ * Check whether the NSEC RR proves that there is a empty non-terminal.
+ * @param nsec  NSEC RRSet.
+ * @param sname Searched name.
+ * @return      0 or error code.
+ */
+static int nsec_empty_nonterminal(const knot_rrset_t *nsec, const knot_dname_t *sname)
+{
+	assert(nsec && sname);
+
+	int ret = nsec_nonamematch(nsec, sname);
+	if (ret != 0) {
+		return ret;
+	}
+
+	const knot_dname_t *next = knot_nsec_next(&nsec->rrs);
+
+	if (knot_dname_in(sname, next)) {
+		return kr_ok();
+	} else {
+		return kr_error(EINVAL);
+	}
+}
+
+int kr_nsec_empty_nonterminal_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                             const knot_dname_t *sname)
+{
+	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
+	if (!sec || !sname) {
+		return kr_error(EINVAL);
+	}
+
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
+		if (rrset->type != KNOT_RRTYPE_NSEC) {
+			continue;
+		}
+		if (nsec_empty_nonterminal(rrset, sname) == 0) {
+			return kr_ok();
+		}
+	}
+
+	return kr_error(ENOENT);
+}
+
+int kr_nsec_no_data(const knot_pkt_t *pkt, knot_section_t section_id,
+                    const knot_dname_t *sname, uint16_t stype)
+{
+	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
+	if (!sec || !sname) {
+		return kr_error(EINVAL);
+	}
+
+	int ret;
+	int flags = 0;
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
+		if (rrset->type != KNOT_RRTYPE_NSEC) {
+			continue;
+		}
+
+		/* No data. */
+		if (knot_dname_is_equal(rrset->owner, sname)) {
+			ret = no_data_response_check_rrtype(&flags, rrset, stype);
+			if (ret != 0) {
+				return ret;
+			}
+		}
+		if (flags & FLG_NOEXIST_RRTYPE) {
+			return kr_ok();
+		}
+
+		/* Empty non-terminal. */
+		if (nsec_empty_nonterminal(rrset, sname) == 0) {
+			return kr_ok();
+		}
+
+		/* Wild card no data. */
+		ret = wildcard_no_data_response_check(&flags, rrset, sname, stype);
+		if (ret != 0) {
+			return ret;
+		}
+		if ((flags & FLG_NOEXIST_RRSET) && (flags & FLG_NOEXIST_CLOSER)) {
+			return kr_ok();
+		}
+	}
+
+	return kr_error(ENOENT);
+}
+
+int kr_nsec_existence_denial(const knot_pkt_t *pkt, knot_section_t section_id,
+                             const knot_dname_t *sname, uint16_t stype, mm_ctx_t *pool)
+{
+	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
+	if (!sec || !sname) {
+		return kr_error(EINVAL);
+	}
+
+	int flags = 0;
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
+		if (rrset->type != KNOT_RRTYPE_NSEC) {
+			continue;
+		}
+		if (knot_dname_is_equal(rrset->owner, sname)) {
+			no_data_response_check_rrtype(&flags, rrset, stype);
+			no_data_wildcard_existence_check(&flags, rrset, sec);
+		} else {
+			name_error_response_check_rr(&flags, rrset, sname, pool);
+		}
+	}
+
+	return kr_nsec_existence_denied(flags) ? kr_ok() : kr_error(ENOENT);
+}
diff --git a/lib/dnssec/nsec.h b/lib/dnssec/nsec.h
new file mode 100644
index 0000000000000000000000000000000000000000..8a74b4ff3d862d499f5357a1ee5aceac6b6bb347
--- /dev/null
+++ b/lib/dnssec/nsec.h
@@ -0,0 +1,109 @@
+/*  Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <libknot/internal/consts.h>
+#include <libknot/internal/mempattern.h>
+#include <libknot/packet/pkt.h>
+
+/**
+ * Check whether bitmap contains given type.
+ * @param bm      Bitmap.
+ * @param bm_size Bitmap size.
+ * @param type    RR type to search for.
+ * @return        True if bitmap contains type.
+ */
+bool kr_nsec_bitmap_contains_type(const uint8_t *bm, uint16_t bm_size, uint16_t type);
+
+/**
+ * Name error response check (RFC4035 3.1.3.2; RFC4035 5.4, bullet 2).
+ * @note No RRSIGs are validated.
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param sname      Name to be checked.
+ * @param pool
+ * @return           0 or error code.
+ */
+int kr_nsec_name_error_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                      const knot_dname_t *sname, mm_ctx_t *pool);
+
+/**
+ * No data response check (RFC4035 3.1.3.1; RFC4035 5.4, bullet 1).
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param sname      Name to be checked.
+ * @param stype      Type to be checked.
+ * @return           0 or error code.
+ */
+int kr_nsec_no_data_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                   const knot_dname_t *sname, uint16_t stype);
+
+/**
+ * Wildcard no data response check (RFC4035 3.1.3.4).
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param sname      Name to be checked.
+ * @param stype      Type to be checked.
+ * @return           0 or error code.
+ */
+int kr_nsec_wildcard_no_data_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                            const knot_dname_t *sname, uint16_t stype);
+
+/**
+ * Wildcard answer response check (RFC4035 3.1.3.3).
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param sname      Name to be checked.
+ * @return           0 or error code.
+ */
+int kr_nsec_wildcard_answer_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                           const knot_dname_t *sname);
+
+/**
+ * Empty non-terminal response.
+ * @note There are no NSEC records for empty non-terminals. The existence of
+ *     the domain is inferred from the covering NSEC record.
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param sname      Name to be checked.
+ * @return           0 or error code.
+ */
+int kr_nsec_empty_nonterminal_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                             const knot_dname_t *sname);
+
+/**
+ * Authenticated denial of existence according to RFC4035 3.1.3.1 and 3.1.3.4.
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param sname      Name to be checked.
+ * @param stype      Type to be checked.
+ * @return           0 or error code.
+ */
+int kr_nsec_no_data(const knot_pkt_t *pkt, knot_section_t section_id,
+                    const knot_dname_t *sname, uint16_t stype);
+
+/**
+ * Authenticated denial of existence according to RFC4035 5.4.
+ * @note No RRSIGs are validated.
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param sname      Queried domain name.
+ * @param stype      Queried type.
+ * @return           0 or error code.
+ */
+int kr_nsec_existence_denial(const knot_pkt_t *pkt, knot_section_t section_id,
+                             const knot_dname_t *sname, uint16_t stype, mm_ctx_t *pool);
diff --git a/lib/dnssec/nsec3.c b/lib/dnssec/nsec3.c
new file mode 100644
index 0000000000000000000000000000000000000000..fdc4d5b8ff9e21772b63274e9e5096953cfb9f22
--- /dev/null
+++ b/lib/dnssec/nsec3.c
@@ -0,0 +1,735 @@
+/*  Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <string.h>
+
+#include <dnssec/binary.h>
+#include <dnssec/error.h>
+#include <dnssec/nsec.h>
+#include <libknot/descriptor.h>
+#include <libknot/internal/base32hex.h>
+#include <libknot/rrset.h>
+#include <libknot/rrtype/nsec3.h>
+
+#include "lib/defines.h"
+#include "lib/dnssec/nsec.h"
+#include "lib/dnssec/nsec3.h"
+
+#define OPT_OUT_BIT 0x01
+
+//#define FLG_CLOSEST_ENCLOSER (1 << 0)
+#define FLG_CLOSEST_PROVABLE_ENCLOSER (1 << 1)
+#define FLG_NAME_COVERED (1 << 2)
+#define FLG_NAME_MATCHED (1 << 3)
+#define FLG_TYPE_BIT_MISSING (1 << 4)
+#define FLG_CNAME_BIT_MISSING (1 << 5)
+
+/**
+ * Obtains NSEC3 parameters from RR.
+ * @param params NSEC3 parameters structure to be set.
+ * @param nsec3  NSEC3 RR containing the parameters.
+ * @return       0 or error code.
+ */
+static int nsec3_parameters(dnssec_nsec3_params_t *params, const knot_rrset_t *nsec3)
+{
+#define SALT_OFFSET 5
+	assert(params && nsec3);
+
+	const knot_rdata_t *rr = knot_rdataset_at(&nsec3->rrs, 0);
+	assert(rr);
+
+	/* Every NSEC3 RR contains data from NSEC3PARAMS. */
+	dnssec_binary_t rdata = {0, };
+	rdata.size = SALT_OFFSET + (size_t) knot_nsec3_salt_length(&nsec3->rrs, 0);
+	rdata.data = knot_rdata_data(rr);
+
+	int ret = dnssec_nsec3_params_from_rdata(params, &rdata);
+	if (ret != DNSSEC_EOK) {
+		return kr_error(EINVAL);
+	}
+
+	return kr_ok();
+#undef SALT_OFFSET
+}
+
+/**
+ * Computes a hash of a given domain name.
+ * @param hash   Resulting hash, must be freed.
+ * @param params NSEC3 parameters.
+ * @param name   Domain name to be hashed.
+ * @return       0 or error code.
+ */
+static int hash_name(dnssec_binary_t *hash, const dnssec_nsec3_params_t *params,
+                     const knot_dname_t *name)
+{
+	assert(hash && params && name);
+
+	dnssec_binary_t dname = {0, };
+	dname.size = knot_dname_size(name);
+	dname.data = (uint8_t *) name;
+
+	int ret = dnssec_nsec3_hash(&dname, params, hash);
+	if (ret != DNSSEC_EOK) {
+		return kr_error(EINVAL);
+	}
+
+	return kr_ok();
+}
+
+/**
+ * Read hash from NSEC3 owner name and store its binary form.
+ * @param hash          Buffer to be written.
+ * @param max_hash_size Maximal has size.
+ * @param nsec3         NSEC3 RR.
+ * @return              0 or error code.
+ */
+static int read_owner_hash(dnssec_binary_t *hash, size_t max_hash_size, const knot_rrset_t *nsec3)
+{
+	assert(hash && nsec3);
+	assert(hash->data);
+
+	int32_t ret = base32hex_decode(nsec3->owner + 1, nsec3->owner[0], hash->data, max_hash_size);
+	if (ret < 0) {
+		return ret;
+	}
+	hash->size = ret;
+
+	return kr_ok();
+}
+
+#define MAX_HASH_BYTES 64
+/**
+ * Closest (provable) encloser match (RFC5155 7.2.1, bullet 1).
+ * @param flags   Flags to be set according to check outcome.
+ * @param nsec3   NSEC3 RR.
+ * @param name    Name to be checked.
+ * @param skipped Number of skipped labels to find closest (provable) match.
+ * @return        0 or error code.
+ */
+static int closest_encloser_match(int *flags, const knot_rrset_t *nsec3,
+                                  const knot_dname_t *name, unsigned *skipped)
+{
+	assert(flags && nsec3 && name && skipped);
+
+	dnssec_binary_t owner_hash = {0, };
+	uint8_t hash_data[MAX_HASH_BYTES] = {0, };
+	owner_hash.data = hash_data;
+	dnssec_nsec3_params_t params = {0, };
+	dnssec_binary_t name_hash = {0, };
+
+	int ret = read_owner_hash(&owner_hash, MAX_HASH_BYTES, nsec3);
+	if (ret != 0) {
+		goto fail;
+	}
+
+	ret = nsec3_parameters(&params, nsec3);
+	if (ret != 0) {
+		goto fail;
+	}
+
+	const knot_dname_t *encloser = knot_wire_next_label(name, NULL);
+	*skipped = 1;
+
+	do {
+		ret = hash_name(&name_hash, &params, encloser);
+		if (ret != 0) {
+			goto fail;
+		}
+
+		if ((owner_hash.size == name_hash.size) &&
+		    (memcmp(owner_hash.data, name_hash.data, owner_hash.size) == 0)) {
+			dnssec_binary_free(&name_hash);
+			*flags |= FLG_CLOSEST_PROVABLE_ENCLOSER;
+			break;
+		}
+
+		dnssec_binary_free(&name_hash);
+
+		encloser = knot_wire_next_label(encloser, NULL);
+		++(*skipped);
+	} while (encloser && (encloser[0] != '\0'));
+
+	ret = kr_ok();
+
+fail:
+	if (params.salt.data) {
+		dnssec_nsec3_params_free(&params);
+	}
+	if (name_hash.data) {
+		dnssec_binary_free(&name_hash);
+	}
+	return ret;
+}
+
+/**
+ * Checks whether NSEC3 RR covers the supplied name (RFC5155 7.2.1, bullet 2).
+ * @param flags Flags to be set according to check outcome.
+ * @param nsec3 NSEC3 RR.
+ * @param name  Name to be checked.
+ * @return      0 or error code.
+ */
+static int covers_name(int *flags, const knot_rrset_t *nsec3, const knot_dname_t *name)
+{
+	assert(flags && nsec3 && name);
+
+	dnssec_binary_t owner_hash = {0, };
+	uint8_t hash_data[MAX_HASH_BYTES] = {0, };
+	owner_hash.data = hash_data;
+	dnssec_nsec3_params_t params = {0, };
+	dnssec_binary_t name_hash = {0, };
+
+	int ret = read_owner_hash(&owner_hash, MAX_HASH_BYTES, nsec3);
+	if (ret != 0) {
+		goto fail;
+	}
+
+	ret = nsec3_parameters(&params, nsec3);
+	if (ret != 0) {
+		goto fail;
+	}
+
+	ret = hash_name(&name_hash, &params, name);
+	if (ret != 0) {
+		goto fail;
+	}
+
+	uint8_t next_size = 0;
+	uint8_t *next_hash = NULL;
+	knot_nsec3_next_hashed(&nsec3->rrs, 0, &next_hash, &next_size);
+
+	if ((owner_hash.size != next_size) || (name_hash.size != next_size)) {
+		/* All hash lengths must be same. */
+		goto fail;
+	}
+
+	const uint8_t *ownrd = owner_hash.data;
+	const uint8_t *nextd = next_hash;
+	if (memcmp(ownrd, nextd, next_size) < 0) {
+		/*
+		 * 0 (...) owner ... next (...) MAX
+		 *                ^
+		 *                name
+		 * ==>
+		 * (owner < name) && (name < next)
+		 */
+		if ((memcmp(ownrd, name_hash.data, next_size) >= 0) ||
+		    (memcmp(name_hash.data, nextd, next_size) >= 0)) {
+			goto fail;
+		}
+	} else {
+		/*
+		 * owner ... MAX, 0 ... next
+		 *        ^     ^    ^
+		 *        name  name name
+		 * =>
+		 * (owner < name) || (name < next)
+		 */
+		if ((memcmp(ownrd, name_hash.data, next_size) >= 0) &&
+		    (memcmp(name_hash.data, nextd, next_size) >= 0)) {
+			goto fail;
+		}
+	}
+
+	*flags |= FLG_NAME_COVERED;
+
+	uint8_t nsec3_flags = knot_nsec3_flags(&nsec3->rrs, 0);
+	if (nsec3_flags & ~OPT_OUT_BIT) {
+		/* RFC5155 3.1.2 */
+		ret = kr_error(EINVAL);
+	}
+
+	ret = kr_ok();
+
+fail:
+	if (params.salt.data) {
+		dnssec_nsec3_params_free(&params);
+	}
+	if (name_hash.data) {
+		dnssec_binary_free(&name_hash);
+	}
+	return ret;
+}
+
+/**
+ * Checks whether NSEC3 RR has the opt-out bit set.
+ * @param flags Flags to be set according to check outcome.
+ * @param nsec3 NSEC3 RR.
+ * @param name  Name to be checked.
+ * @return      0 or error code.
+ */
+static bool has_optout(const knot_rrset_t *nsec3)
+{
+	if (!nsec3) {
+		return false;
+	}
+
+	uint8_t nsec3_flags = knot_nsec3_flags(&nsec3->rrs, 0);
+	if (nsec3_flags & ~OPT_OUT_BIT) {
+		/* RFC5155 3.1.2 */
+		return false;
+	}
+
+	return nsec3_flags & OPT_OUT_BIT;
+}
+
+/**
+ * Checks whether NSEC3 RR matches the supplied name.
+ * @param flags Flags to be set according to check outcome.
+ * @param nsec3 NSEC3 RR.
+ * @param name  Name to be checked.
+ * @return      0 or error code.
+ */
+static int matches_name(int *flags, const knot_rrset_t *nsec3, const knot_dname_t *name)
+{
+	assert(flags && nsec3 && name);
+
+	dnssec_binary_t owner_hash = {0, };
+	uint8_t hash_data[MAX_HASH_BYTES] = {0, };
+	owner_hash.data = hash_data;
+	dnssec_nsec3_params_t params = {0, };
+	dnssec_binary_t name_hash = {0, };
+
+	int ret = read_owner_hash(&owner_hash, MAX_HASH_BYTES, nsec3);
+	if (ret != 0) {
+		goto fail;
+	}
+
+	ret = nsec3_parameters(&params, nsec3);
+	if (ret != 0) {
+		goto fail;
+	}
+
+	ret = hash_name(&name_hash, &params, name);
+	if (ret != 0) {
+		goto fail;
+	}
+
+	if ((owner_hash.size != name_hash.size) ||
+	    (memcmp(owner_hash.data, name_hash.data, owner_hash.size) != 0)) {
+		goto fail;
+	}
+
+	*flags |= FLG_NAME_MATCHED;
+	ret = kr_ok();
+
+fail:
+	if (params.salt.data) {
+		dnssec_nsec3_params_free(&params);
+	}
+	if (name_hash.data) {
+		dnssec_binary_free(&name_hash);
+	}
+	return ret;
+}
+#undef MAX_HASH_BYTES
+
+/**
+ * Prepends an asterisk label to given name.
+ *
+ * @param tgt  Target buffer to write domain name into.
+ * @param name Name to be added to the asterisk.
+ * @return     0 or error code
+ */
+int prepend_asterisk(uint8_t tgt[KNOT_DNAME_MAXLEN], const knot_dname_t *name)
+{
+	tgt[0] = 1;
+	tgt[1] = '*';
+	tgt[2] = 0;
+	int name_len = knot_dname_size(name);
+	if (name_len < 0) {
+		return name_len;
+	}
+	memcpy(tgt + 2, name, name_len);
+	return 0;
+}
+
+/**
+ * Closest encloser proof (RFC5155 7.2.1).
+ * @note No RRSIGs are validated.
+ * @param pkt                    Packet structure to be processed.
+ * @param section_id             Packet section to be processed.
+ * @param sname                  Name to be checked.
+ * @param encloser_name          Returned matching encloser name, if found.
+ * @param matching_ecloser_nsec3 Pointer to matching encloser NSEC RRSet.
+ * @param covering_next_nsec3    Pointer to covering next closer NSEC3 RRSet.
+ * @return                       0 or error code.
+ */
+static int closest_encloser_proof(const knot_pkt_t *pkt, knot_section_t section_id,
+                                  const knot_dname_t *sname, const knot_dname_t **encloser_name,
+                                  const knot_rrset_t **matching_ecloser_nsec3, const knot_rrset_t **covering_next_nsec3)
+{
+	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
+	if (!sec || !sname) {
+		return kr_error(EINVAL);
+	}
+
+	const knot_rrset_t *matching = NULL;
+	const knot_rrset_t *covering = NULL;
+
+	int ret = kr_error(ENOENT);
+	int flags;
+	const knot_dname_t *next_closer = NULL;
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
+		if (rrset->type != KNOT_RRTYPE_NSEC3) {
+			continue;
+		}
+		unsigned skipped = 0;
+		flags = 0;
+		ret = closest_encloser_match(&flags, rrset, sname, &skipped);
+		if (ret != 0) {
+			return ret;
+		}
+		if (!(flags & FLG_CLOSEST_PROVABLE_ENCLOSER)) {
+			continue;
+		}
+		matching = rrset;
+		--skipped;
+		next_closer = sname;
+		for (unsigned j = 0; j < skipped; ++j) {
+			next_closer = knot_wire_next_label(next_closer, NULL);
+		}
+		for (unsigned j = 0; j < sec->count; ++j) {
+			const knot_rrset_t *rrset = knot_pkt_rr(sec, j);
+			if (rrset->type != KNOT_RRTYPE_NSEC3) {
+				continue;
+			}
+			ret = covers_name(&flags, rrset, next_closer);
+			if (ret != 0) {
+				return ret;
+			}
+			if (flags & FLG_NAME_COVERED) {
+				covering = rrset;
+				break;
+			}
+		}
+		if (flags & FLG_NAME_COVERED) {
+			break;
+		}
+		flags = 0; //
+	}
+
+	if ((flags & FLG_CLOSEST_PROVABLE_ENCLOSER) &&
+	    (flags & FLG_NAME_COVERED)) {
+		if (encloser_name) {
+			*encloser_name = knot_wire_next_label(next_closer, NULL);
+		}
+		if (matching_ecloser_nsec3) {
+			*matching_ecloser_nsec3 = matching;
+		}
+		if (covering_next_nsec3) {
+			*covering_next_nsec3 = covering;
+		}
+		return kr_ok();
+	}
+
+	return kr_error(ENOENT);
+}
+
+/**
+ * Check whether any NSEC3 RR covers a wildcard RR at the closer encloser.
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param encloser   Closest (provable) encloser domain name.
+ * @return           0 or error code.
+ */
+static int covers_closest_encloser_wildcard(const knot_pkt_t *pkt, knot_section_t section_id,
+                                            const knot_dname_t *encloser)
+{
+	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
+	if (!sec || !encloser) {
+		return kr_error(EINVAL);
+	}
+
+	uint8_t wildcard[KNOT_DNAME_MAXLEN];
+	wildcard[0] = 1;
+	wildcard[1] = '*';
+	int encloser_len = knot_dname_size(encloser);
+	if (encloser_len < 0) {
+		return encloser_len;
+	}
+	memcpy(wildcard + 2, encloser, encloser_len);
+
+	int flags = 0;
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
+		if (rrset->type != KNOT_RRTYPE_NSEC3) {
+			continue;
+		}
+		int ret = covers_name(&flags, rrset, wildcard);
+		if (ret != 0) {
+			return ret;
+		}
+		if (flags & FLG_NAME_COVERED) {
+			return kr_ok();
+		}
+	}
+
+	return kr_error(ENOENT);
+}
+
+int kr_nsec3_name_error_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                       const knot_dname_t *sname)
+{
+	const knot_dname_t *encloser = NULL;
+	int ret = closest_encloser_proof(pkt, section_id, sname, &encloser, NULL, NULL);
+	if (ret != 0) {
+		return ret;
+	}
+	return covers_closest_encloser_wildcard(pkt, section_id, encloser);
+}
+
+/**
+ * Checks whether supplied NSEC3 RR matches the supplied name and type.
+ * @param flags Flags to be set according to check outcome.
+ * @param nsec3 NSEC3 RR.
+ * @param name  Name to be checked.
+ * @param type  Type to be checked.
+ * @return      0 or error code.
+ */
+static int maches_name_and_type(int *flags, const knot_rrset_t *nsec3,
+                                const knot_dname_t *name, uint16_t type)
+{
+	assert(flags && nsec3 && name);
+
+	int ret = matches_name(flags, nsec3, name);
+	if (ret != 0) {
+		return ret;
+	}
+
+	if (!(*flags & FLG_NAME_MATCHED)) {
+		return kr_ok();
+	}
+
+	uint8_t *bm = NULL;
+	uint16_t bm_size;
+	knot_nsec3_bitmap(&nsec3->rrs, 0, &bm, &bm_size);
+	if (!bm) {
+		return kr_error(EINVAL);
+	}
+
+	if (!kr_nsec_bitmap_contains_type(bm, bm_size, type)) {
+		*flags |= FLG_TYPE_BIT_MISSING;
+		if (type == KNOT_RRTYPE_CNAME) {
+			*flags |= FLG_CNAME_BIT_MISSING;
+		}
+	}
+
+	if ((type != KNOT_RRTYPE_CNAME) &&
+	    !kr_nsec_bitmap_contains_type(bm, bm_size, KNOT_RRTYPE_CNAME)) {
+		*flags |= FLG_CNAME_BIT_MISSING;
+	}
+
+	return kr_ok();
+}
+
+/**
+ * No data response check, no DS (RFC5155 7.2.3).
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param sname      Name to be checked.
+ * @param stype      Type to be checked.
+ * @return           0 or error code.
+ */
+static int no_data_response_no_ds(const knot_pkt_t *pkt, knot_section_t section_id,
+                                  const knot_dname_t *sname, uint16_t stype)
+{
+	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
+	if (!sec || !sname) {
+		return kr_error(EINVAL);
+	}
+
+	int flags;
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
+		if (rrset->type != KNOT_RRTYPE_NSEC3) {
+			continue;
+		}
+		flags = 0;
+
+		int ret = maches_name_and_type(&flags, rrset, sname, stype);
+		if (ret != 0) {
+			return ret;
+		}
+
+		if ((flags & FLG_NAME_MATCHED) &&
+		    (flags & FLG_TYPE_BIT_MISSING) &&
+		    (flags & FLG_CNAME_BIT_MISSING)) {
+			return kr_ok();
+		}
+	}
+
+	return kr_error(ENOENT);
+}
+
+/**
+ * No data response check, DS (RFC5155 7.2.4, 2nd paragraph).
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param sname      Name to be checked.
+ * @param stype      Type to be checked.
+ * @return           0 or error code.
+ */
+static int no_data_response_ds(const knot_pkt_t *pkt, knot_section_t section_id,
+                               const knot_dname_t *sname, uint16_t stype)
+{
+	assert(pkt && sname);
+	if (stype != KNOT_RRTYPE_DS) {
+		return kr_error(EINVAL);
+	}
+
+	const knot_rrset_t *covering_nsec3 = NULL;
+	int ret = closest_encloser_proof(pkt, section_id, sname, NULL, NULL, &covering_nsec3);
+	if (ret != 0) {
+		return ret;
+	}
+
+	if (has_optout(covering_nsec3)) {
+		return kr_ok();
+	}
+
+	return kr_error(ENOENT);
+}
+
+int kr_nsec3_no_data_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                    const knot_dname_t *sname, uint16_t stype)
+{
+	/* DS record may be matched by an existing NSEC3 RR. */
+	int ret = no_data_response_no_ds(pkt, section_id, sname, stype);
+	if ((ret == 0) || (stype != KNOT_RRTYPE_DS)) {
+		return ret;
+	}
+	/* Closest provable encloser proof must be performed else. */
+	return no_data_response_ds(pkt, section_id, sname, stype);
+}
+
+/**
+ * Check whether NSEC3 RR matches a wildcard at the closest encloser and has given type bit missing.
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param encloser   Closest (provable) encloser domain name.
+ * @param stype      Type to be checked.
+ * @return           0 or error code.
+ */
+static int matches_closest_encloser_wildcard(const knot_pkt_t *pkt, knot_section_t section_id,
+                                             const knot_dname_t *encloser, uint16_t stype)
+{
+	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
+	if (!sec || !encloser) {
+		return kr_error(EINVAL);
+	}
+
+	uint8_t wildcard[KNOT_DNAME_MAXLEN];
+	int ret = prepend_asterisk(wildcard, encloser);
+	if (ret != 0) {
+		return ret;
+	}
+
+	int flags;
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
+		if (rrset->type != KNOT_RRTYPE_NSEC3) {
+			continue;
+		}
+		flags = 0;
+
+		int ret = maches_name_and_type(&flags, rrset, wildcard, stype);
+		if (ret != 0) {
+			return ret;
+		}
+
+		/* TODO -- The loop resembles no_data_response_no_ds() exept
+		 * the following condition.
+		 */
+		if ((flags & FLG_NAME_MATCHED) && (flags & FLG_TYPE_BIT_MISSING)) {
+			return kr_ok();
+		}
+	}
+
+	return kr_error(ENOENT);
+}
+
+int kr_nsec3_wildcard_no_data_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                             const knot_dname_t *sname, uint16_t stype)
+{
+	const knot_dname_t *encloser = NULL;
+	int ret = closest_encloser_proof(pkt, section_id, sname, &encloser, NULL, NULL);
+	if (ret != 0) {
+		return ret;
+	}
+	return matches_closest_encloser_wildcard(pkt, section_id, encloser, stype);
+}
+
+int kr_nsec3_wildcard_answer_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                            const knot_dname_t *sname, int trim_to_next)
+{
+	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
+	if (!sec || !sname) {
+		return kr_error(EINVAL);
+	}
+
+	/* Compute the next closer name. */
+	for (int i = 0; i < trim_to_next; ++i) {
+		sname = knot_wire_next_label(sname, NULL);
+	}
+
+	int flags = 0;
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
+		if (rrset->type != KNOT_RRTYPE_NSEC3) {
+			continue;
+		}
+		int ret = covers_name(&flags, rrset, sname);
+		if (ret != 0) {
+			return ret;
+		}
+		if (flags & FLG_NAME_COVERED) {
+			return kr_ok();
+		}
+	}
+
+	return kr_error(ENOENT);
+}
+
+int kr_nsec3_no_data(const knot_pkt_t *pkt, knot_section_t section_id,
+                     const knot_dname_t *sname, uint16_t stype)
+{
+	/* DS record may be also matched by an existing NSEC3 RR. */
+	int ret = no_data_response_no_ds(pkt, section_id, sname, stype);
+	if (ret == 0) {
+		/* Satisfies RFC5155 8.5 and 8.6, first paragraph. */
+		return ret;
+	}
+
+	/* Find closest provable encloser. */
+	const knot_dname_t *encloser_name = NULL;
+	const knot_rrset_t *covering_next_nsec3 = NULL;
+	ret = closest_encloser_proof(pkt, section_id, sname, &encloser_name,
+                                     NULL, &covering_next_nsec3);
+	if (ret != 0) {
+		return ret;
+	}
+
+	assert(encloser_name && covering_next_nsec3);
+	if ((stype == KNOT_RRTYPE_DS) && has_optout(covering_next_nsec3)) {
+		/* Satisfies RFC5155 8.6, second paragraph. */
+		return 0;
+	}
+
+	return matches_closest_encloser_wildcard(pkt, section_id,
+	                                         encloser_name, stype);
+}
diff --git a/lib/dnssec/nsec3.h b/lib/dnssec/nsec3.h
new file mode 100644
index 0000000000000000000000000000000000000000..b4a50405c5121881a69f2052eb158e95b4150747
--- /dev/null
+++ b/lib/dnssec/nsec3.h
@@ -0,0 +1,77 @@
+/*  Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <libknot/internal/consts.h>
+#include <libknot/internal/mempattern.h>
+#include <libknot/packet/pkt.h>
+
+/**
+ * Name error response check (RFC5155 7.2.2).
+ * @note No RRSIGs are validated.
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param sname      Name to be checked.
+ * @return           0 or error code.
+ */
+int kr_nsec3_name_error_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                       const knot_dname_t *sname);
+
+/**
+ * No data response check (RFC5155 7.2.3 and 7.2.4).
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param sname      Name to be checked.
+ * @param stype      Type to be checked.
+ * @return           0 or error code.
+ */
+int kr_nsec3_no_data_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                    const knot_dname_t *sname, uint16_t stype);
+
+/**
+ * Wildcard no data response check (RFC5155 7.2.5).
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param sname      Name to be checked.
+ * @param stype      Type to be checked.
+ * @return           0 or error code.
+ */
+int kr_nsec3_wildcard_no_data_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                             const knot_dname_t *sname, uint16_t stype);
+
+/**
+ * Wildcard answer response check (RFC5155 7.2.6).
+ * @param pkt          Packet structure to be processed.
+ * @param section_id   Packet section to be processed.
+ * @param sname        Name to be checked.
+ * @param trim_to_next Number of labels to remove to obtain next closer name.
+ * @return             0 or error code.
+ */
+int kr_nsec3_wildcard_answer_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+                                            const knot_dname_t *sname, int trim_to_next);
+
+/**
+ * Authenticated denial of existence according to RFC5155 8.5, 8.6 and 8.7.
+ * @note No RRSIGs are validated.
+ * @param pkt        Packet structure to be processed.
+ * @param section_id Packet section to be processed.
+ * @param sname      Queried domain name.
+ * @param stype      Queried type.
+ * @return           0 or error code.
+ */
+int kr_nsec3_no_data(const knot_pkt_t *pkt, knot_section_t section_id,
+                     const knot_dname_t *sname, uint16_t stype);
diff --git a/lib/dnssec/packet/pkt.c b/lib/dnssec/packet/pkt.c
new file mode 100644
index 0000000000000000000000000000000000000000..91088aba197958da11ac5de5cf5072a351f048de
--- /dev/null
+++ b/lib/dnssec/packet/pkt.c
@@ -0,0 +1,56 @@
+/*  Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <libknot/internal/consts.h>
+
+#include "lib/dnssec/packet/pkt.h"
+
+/**
+ * Search in section for given type.
+ * @param sec  Packet section.
+ * @param type Type to search for.
+ * @return     True if found.
+ */
+static bool section_has_type(const knot_pktsection_t *sec, uint16_t type)
+{
+	if (!sec) {
+		return false;
+	}
+
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rr = knot_pkt_rr(sec, i);
+		if (rr->type == type) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+bool _knot_pkt_has_type(const knot_pkt_t *pkt, uint16_t type)
+{
+	if (!pkt) {
+		return false;
+	}
+
+	if (section_has_type(knot_pkt_section(pkt, KNOT_ANSWER), type)) {
+		return true;
+	}
+	if (section_has_type(knot_pkt_section(pkt, KNOT_AUTHORITY), type)) {
+		return true;
+	}
+	return section_has_type(knot_pkt_section(pkt, KNOT_ADDITIONAL), type);
+}
diff --git a/lib/dnssec/packet/pkt.h b/lib/dnssec/packet/pkt.h
new file mode 100644
index 0000000000000000000000000000000000000000..429ad0fb882716aa216186208198c838e6f5aac7
--- /dev/null
+++ b/lib/dnssec/packet/pkt.h
@@ -0,0 +1,27 @@
+/*  Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <libknot/packet/pkt.h>
+
+/**
+ * Check whether packet contains given type.
+ * @param pkt  Packet to seek through.
+ * @param type RR type to search for.
+ * @return     True if found.
+ */
+bool _knot_pkt_has_type(const knot_pkt_t *pkt, uint16_t type);
diff --git a/lib/dnssec/rrtype/ds.h b/lib/dnssec/rrtype/ds.h
new file mode 100644
index 0000000000000000000000000000000000000000..32e131c310679259e25e44a3066e8b40b80becd7
--- /dev/null
+++ b/lib/dnssec/rrtype/ds.h
@@ -0,0 +1,50 @@
+/*  Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <libknot/rdataset.h>
+
+static inline
+uint16_t _knot_ds_ktag(const knot_rdataset_t *rrs, size_t pos)
+{
+	KNOT_RDATASET_CHECK(rrs, pos, return 0);
+	return wire_read_u16(knot_rdata_offset(rrs, pos, 0));
+}
+
+static inline
+uint8_t _knot_ds_alg(const knot_rdataset_t *rrs, size_t pos)
+{
+	KNOT_RDATASET_CHECK(rrs, pos, return 0);
+	return *knot_rdata_offset(rrs, pos, 2);
+}
+
+static inline
+uint8_t _knot_ds_dtype(const knot_rdataset_t *rrs, size_t pos)
+{
+	KNOT_RDATASET_CHECK(rrs, pos, return 0);
+	return *knot_rdata_offset(rrs, pos, 3);
+}
+
+static inline
+void _knot_ds_digest(const knot_rdataset_t *rrs, size_t pos,
+                    uint8_t **digest, uint16_t *digest_size)
+{
+	KNOT_RDATASET_CHECK(rrs, pos, return);
+	*digest = knot_rdata_offset(rrs, pos, 4);
+	const knot_rdata_t *rr = knot_rdataset_at(rrs, pos);
+	*digest_size = knot_rdata_rdlen(rr) - 4;
+}
diff --git a/lib/dnssec/signature.c b/lib/dnssec/signature.c
new file mode 100644
index 0000000000000000000000000000000000000000..a6cc321c7deb6e8986907bdb65ea22aa3b6e1697
--- /dev/null
+++ b/lib/dnssec/signature.c
@@ -0,0 +1,278 @@
+/*  Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <arpa/inet.h>
+#include <assert.h>
+#include <string.h>
+
+#include <dnssec/error.h>
+#include <dnssec/key.h>
+#include <dnssec/sign.h>
+#include <libknot/descriptor.h>
+#include <libknot/packet/rrset-wire.h>
+#include <libknot/packet/wire.h>
+#include <libknot/rrset.h>
+#include <libknot/rrtype/rrsig.h>
+
+#include "lib/defines.h"
+#include "lib/dnssec/rrtype/ds.h"
+#include "lib/dnssec/signature.h"
+
+int kr_authenticate_referral(const knot_rrset_t *ref, const dnssec_key_t *key)
+{
+	assert(ref && key);
+	if (ref->type != KNOT_RRTYPE_DS) {
+		assert(0);
+		return kr_error(EINVAL);
+	}
+
+	int ret = 0;
+	dnssec_binary_t orig_ds_rdata;
+	dnssec_binary_t generated_ds_rdata = {0, };
+
+	{
+		/* Obtain RDATA of the supplied DS. */
+		const knot_rdata_t *rr = knot_rdataset_at(&ref->rrs, 0);
+		orig_ds_rdata.size = knot_rdata_rdlen(rr);
+		orig_ds_rdata.data = knot_rdata_data(rr);
+	}
+
+	/* Compute DS RDATA from the DNSKEY. */
+	ret = dnssec_key_create_ds(key, _knot_ds_dtype(&ref->rrs, 0), &generated_ds_rdata);
+	if (ret != DNSSEC_EOK) {
+		ret = kr_error(ENOMEM);
+		goto fail;
+	}
+
+	/* DS records contain algorithm, key tag and the digest.
+	 * Therefore the comparison of the two DS is sufficient.
+	 */
+	ret = (orig_ds_rdata.size == generated_ds_rdata.size) &&
+	    (memcmp(orig_ds_rdata.data, generated_ds_rdata.data, orig_ds_rdata.size) == 0);
+	ret = ret ? kr_ok() : kr_error(ENOENT);
+
+fail:
+	dnssec_binary_free(&generated_ds_rdata);
+	return ret;
+}
+
+/**
+ * Adjust TTL in wire format.
+ * @param wire      RR Set in wire format.
+ * @param wire_size Size of the wire data portion.
+ * @param new_ttl   TTL value to be set for all RRs.
+ * @return          0 or error code.
+ */
+static int adjust_wire_ttl(uint8_t *wire, size_t wire_size, uint32_t new_ttl)
+{
+	assert(wire);
+	assert(sizeof(uint16_t) == 2);
+	assert(sizeof(uint32_t) == 4);
+	uint16_t rdlen;
+
+	int ret;
+
+	new_ttl = htonl(new_ttl);
+
+	size_t i = 0;
+	/* RR wire format in RFC1035 3.2.1 */
+	while(i < wire_size) {
+		ret = knot_dname_size(wire + i);
+		if (ret < 0) {
+			return ret;
+		}
+		i += ret + 4;
+		memcpy(wire + i, &new_ttl, sizeof(uint32_t));
+		i += sizeof(uint32_t);
+
+		memcpy(&rdlen, wire + i, sizeof(uint16_t));
+		rdlen = ntohs(rdlen);
+		i += sizeof(uint16_t) + rdlen;
+
+		assert(i <= wire_size);
+	}
+
+	return kr_ok();
+}
+
+/*!
+ * \brief Add RRSIG RDATA without signature to signing context.
+ *
+ * Requires signer name in RDATA in canonical form.
+ *
+ * \param ctx   Signing context.
+ * \param rdata Pointer to RRSIG RDATA.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+#define RRSIG_RDATA_SIGNER_OFFSET 18
+static int sign_ctx_add_self(dnssec_sign_ctx_t *ctx, const uint8_t *rdata)
+{
+	assert(ctx);
+	assert(rdata);
+
+	int result;
+
+	// static header
+
+	dnssec_binary_t header = { 0 };
+	header.data = (uint8_t *)rdata;
+	header.size = RRSIG_RDATA_SIGNER_OFFSET;
+
+	result = dnssec_sign_add(ctx, &header);
+	if (result != DNSSEC_EOK) {
+		return result;
+	}
+
+	// signer name
+
+	const uint8_t *rdata_signer = rdata + RRSIG_RDATA_SIGNER_OFFSET;
+	dnssec_binary_t signer = { 0 };
+	signer.data = knot_dname_copy(rdata_signer, NULL);
+	signer.size = knot_dname_size(signer.data);
+
+	result = dnssec_sign_add(ctx, &signer);
+	free(signer.data);
+
+	return result;
+}
+#undef RRSIG_RDATA_SIGNER_OFFSET
+
+/*!
+ * \brief Add covered RRs to signing context.
+ *
+ * Requires all DNAMEs in canonical form and all RRs ordered canonically.
+ *
+ * \param ctx      Signing context.
+ * \param covered  Covered RRs.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+static int sign_ctx_add_records(dnssec_sign_ctx_t *ctx, const knot_rrset_t *covered,
+                                uint32_t orig_ttl, int trim_labels)
+{
+	// huge block of rrsets can be optionally created
+	uint8_t *rrwf = malloc(KNOT_WIRE_MAX_PKTSIZE);
+	if (!rrwf) {
+		return KNOT_ENOMEM;
+	}
+
+	int written = knot_rrset_to_wire(covered, rrwf, KNOT_WIRE_MAX_PKTSIZE, NULL);
+	if (written < 0) {
+		free(rrwf);
+		return written;
+	}
+
+	/* Set original ttl. */
+	int ret = adjust_wire_ttl(rrwf, written, orig_ttl);
+	if (ret != 0) {
+		return ret;
+	}
+
+	/* RFC4035 5.3.2
+	 * Remove leftmost labels and replace them with '*.'.
+	 */
+	uint8_t *owner = rrwf;
+	if (trim_labels > 0) {
+		/**/
+		for (int i = 0; i < trim_labels; ++i) {
+			owner = (uint8_t *) knot_wire_next_label(owner, NULL);
+		}
+		*(--owner) = '*';
+		*(--owner) = 1;
+	}
+
+	dnssec_binary_t rrset_wire = { 0 };
+	rrset_wire.size = written - (owner - rrwf);
+	rrset_wire.data = owner;
+	int result = dnssec_sign_add(ctx, &rrset_wire);
+	free(rrwf);
+
+	return result;
+}
+
+/*!
+ * \brief Add all data covered by signature into signing context.
+ *
+ * RFC 4034: The signature covers RRSIG RDATA field (excluding the signature)
+ * and all matching RR records, which are ordered canonically.
+ *
+ * Requires all DNAMEs in canonical form and all RRs ordered canonically.
+ *
+ * \param ctx          Signing context.
+ * \param rrsig_rdata  RRSIG RDATA with populated fields except signature.
+ * \param covered      Covered RRs.
+ *
+ * \return Error code, KNOT_EOK if successful.
+ */
+/* TODO -- Taken from knot/src/knot/dnssec/rrset-sign.c. Re-write for better fit needed. */
+static int sign_ctx_add_data(dnssec_sign_ctx_t *ctx, const uint8_t *rrsig_rdata,
+                             const knot_rrset_t *covered, uint32_t orig_ttl, int trim_labels)
+{
+	int result = sign_ctx_add_self(ctx, rrsig_rdata);
+	if (result != KNOT_EOK) {
+		return result;
+	}
+
+	return sign_ctx_add_records(ctx, covered, orig_ttl, trim_labels);
+}
+
+int kr_check_signature(const knot_rrset_t *rrsigs, size_t pos,
+                       const dnssec_key_t *key, const knot_rrset_t *covered,
+                       int trim_labels)
+{
+	if (!rrsigs || !key || !dnssec_key_can_verify(key)) {
+		return kr_error(EINVAL);
+	}
+
+	int ret;
+	dnssec_sign_ctx_t *sign_ctx = NULL;
+	dnssec_binary_t signature = {0, };
+
+	knot_rrsig_signature(&rrsigs->rrs, pos, &signature.data, &signature.size);
+	if (!signature.data || !signature.size) {
+		ret = kr_error(EINVAL);
+		goto fail;
+	}
+
+	ret = dnssec_sign_new(&sign_ctx, key);
+	if (ret != DNSSEC_EOK) {
+		ret = kr_error(ENOMEM);
+		goto fail;
+	}
+
+	uint32_t orig_ttl = knot_rrsig_original_ttl(&rrsigs->rrs, pos);
+	const knot_rdata_t *rr_data = knot_rdataset_at(&rrsigs->rrs, pos);
+	uint8_t *rdata = knot_rdata_data(rr_data);
+
+	ret = sign_ctx_add_data(sign_ctx, rdata, covered, orig_ttl, trim_labels);
+	if (ret != KNOT_EOK) {
+		ret = kr_error(ENOMEM);
+		goto fail;
+	}
+
+	ret = dnssec_sign_verify(sign_ctx, &signature);
+	if (ret != KNOT_EOK) {
+		ret = kr_error(EBADMSG);
+		goto fail;
+	}
+
+	ret = kr_ok();
+
+fail:
+	dnssec_sign_free(sign_ctx);
+	return ret;
+}
diff --git a/lib/dnssec/signature.h b/lib/dnssec/signature.h
new file mode 100644
index 0000000000000000000000000000000000000000..c31be36663e8d125504da68d6278a3a08aecb522
--- /dev/null
+++ b/lib/dnssec/signature.h
@@ -0,0 +1,41 @@
+/*  Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <dnssec/key.h>
+#include <libknot/rrset.h>
+
+/**
+ * Performs referral authentication according to RFC4035 5.2, bullet 2
+ * @param ref Referral RRSet. Currently only DS can be used.
+ * @param key Already parsed key.
+ * @return    0 or error code.
+ */
+int kr_authenticate_referral(const knot_rrset_t *ref, const dnssec_key_t *key);
+
+/**
+ * Check the signature of the supplied RRSet.
+ * @param rrsigs      RRSet containing signatures.
+ * @param pos         Index of the signature record in the signature RRSet.
+ * @param key         Key to be used to validate the signature.
+ * @param covered     The covered RRSet.
+ * @param trim_labels Number of the leftmost labels to be removed and replaced with '*.'.
+ * @return            0 if signature valid, error code else.
+ */
+int kr_check_signature(const knot_rrset_t *rrsigs, size_t pos,
+                       const dnssec_key_t *key, const knot_rrset_t *covered,
+                       int trim_labels);
diff --git a/lib/dnssec/ta.c b/lib/dnssec/ta.c
new file mode 100644
index 0000000000000000000000000000000000000000..7e3daa2b24350a2087c80e6eba55370d3905bbec
--- /dev/null
+++ b/lib/dnssec/ta.c
@@ -0,0 +1,146 @@
+/*  Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <libknot/descriptor.h>
+#include <libknot/rdataset.h>
+#include <libknot/rrset.h>
+#include <libknot/packet/wire.h>
+#include <dnssec/key.h>
+#include <dnssec/error.h>
+
+#include "lib/defines.h"
+#include "lib/dnssec/ta.h"
+
+knot_rrset_t *kr_ta_get(map_t *trust_anchors, const knot_dname_t *name)
+{
+	return map_get(trust_anchors, (const char *)name);
+}
+
+/* @internal Create DS from DNSKEY, caller MUST free dst if successful. */
+static int dnskey2ds(dnssec_binary_t *dst, const knot_dname_t *owner, const uint8_t *rdata, uint16_t rdlen)
+{
+	dnssec_key_t *key = NULL;
+	int ret = dnssec_key_new(&key);
+	if (ret != DNSSEC_EOK) {
+		return kr_error(ENOMEM);
+	}
+	/* Create DS from DNSKEY and reinsert */
+	const dnssec_binary_t key_data = { .size = rdlen, .data = (uint8_t *)rdata };
+	ret = dnssec_key_set_rdata(key, &key_data);
+	if (ret == DNSSEC_EOK) {
+		/* Accept only KSK (257) to TA store */
+		if (dnssec_key_get_flags(key) == 257)  {
+			ret = dnssec_key_set_dname(key, owner);
+		} else {
+			ret = DNSSEC_EINVAL;
+		}
+		if (ret == DNSSEC_EOK) {
+			ret = dnssec_key_create_ds(key, DNSSEC_KEY_DIGEST_SHA256, dst);
+		}
+	}
+	dnssec_key_free(key);
+	/* Pick some sane error code */
+	if (ret != DNSSEC_EOK) {
+		return kr_error(ENOMEM);
+	}
+	return kr_ok();
+}
+
+/* @internal Insert new TA to trust anchor set, rdata MUST be of DS type. */
+static int insert_ta(map_t *trust_anchors, const knot_dname_t *name,
+                     uint32_t ttl, const uint8_t *rdata, uint16_t rdlen)
+{
+	bool is_new_key = false;
+	knot_rrset_t *ta_rr = kr_ta_get(trust_anchors, name);
+	if (!ta_rr) {
+		ta_rr = knot_rrset_new(name, KNOT_RRTYPE_DS, KNOT_CLASS_IN, NULL);
+		is_new_key = true;
+	}
+	/* Merge-in new key data */
+	if (!ta_rr || (rdlen > 0 && knot_rrset_add_rdata(ta_rr, rdata, rdlen, ttl, NULL) != 0)) {
+		knot_rrset_free(&ta_rr, NULL);
+		return kr_error(ENOMEM);
+	}
+	if (is_new_key) {
+		return map_set(trust_anchors, (const char *)name, ta_rr);
+	}
+	return kr_ok();	
+}
+
+int kr_ta_add(map_t *trust_anchors, const knot_dname_t *name, uint16_t type,
+              uint32_t ttl, const uint8_t *rdata, uint16_t rdlen)
+{
+	if (!trust_anchors || !name) {
+		return kr_error(EINVAL);
+	}
+
+	/* DS/DNSEY types are accepted, for DNSKEY we
+	 * need to compute a DS digest. */
+	if (type == KNOT_RRTYPE_DS) {
+		return insert_ta(trust_anchors, name, ttl, rdata, rdlen);
+	} else if (type == KNOT_RRTYPE_DNSKEY) {
+		dnssec_binary_t ds_rdata = { 0, };
+		int ret = dnskey2ds(&ds_rdata, name, rdata, rdlen);
+		if (ret != 0) {
+			return ret;
+		}
+		ret = insert_ta(trust_anchors, name, ttl, ds_rdata.data, ds_rdata.size);
+		dnssec_binary_free(&ds_rdata);
+		return ret;
+	} else { /* Invalid type for TA */
+		return kr_error(EINVAL);
+	}
+}
+
+int kr_ta_covers(map_t *trust_anchors, const knot_dname_t *name)
+{
+	while(name) {
+		if (kr_ta_get(trust_anchors, name)) {
+			return true;
+		}
+		if (name[0] == '\0') {
+			return false;
+		}
+		name = knot_wire_next_label(name, NULL);
+	}
+	return false;
+}
+
+/* Delete record data */
+static int del_record(const char *k, void *v, void *ext)
+{
+	knot_rrset_t *ta_rr = v;
+	if (ta_rr) {
+		knot_rrset_free(&ta_rr, NULL);
+	}
+	return 0;
+}
+
+int kr_ta_del(map_t *trust_anchors, const knot_dname_t *name)
+{
+	knot_rrset_t *ta_rr = kr_ta_get(trust_anchors, name);
+	if (ta_rr) {
+		del_record(NULL, ta_rr, NULL);
+		map_del(trust_anchors, (const char *)name);
+	}
+	return kr_ok();
+}
+
+void kr_ta_clear(map_t *trust_anchors)
+{
+	map_walk(trust_anchors, del_record, NULL);
+	map_clear(trust_anchors);
+}
diff --git a/lib/dnssec/ta.h b/lib/dnssec/ta.h
new file mode 100644
index 0000000000000000000000000000000000000000..6c712a0db162205b985d01c66d837ea8e77f8a4d
--- /dev/null
+++ b/lib/dnssec/ta.h
@@ -0,0 +1,64 @@
+/*  Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "lib/generic/map.h"
+#include <libknot/rrset.h>
+
+/**
+ * Find TA RRSet by name.
+ * @param  trust_anchors trust store
+ * @param  name          name of the TA
+ * @return non-empty RRSet or NULL
+ */
+knot_rrset_t *kr_ta_get(map_t *trust_anchors, const knot_dname_t *name);
+
+/**
+ * Add TA to trust store. DS or DNSKEY types are supported.
+ * @param  trust_anchors trust store
+ * @param  name          name of the TA
+ * @param  type          RR type of the TA (DS or DNSKEY)
+ * @param  ttl           
+ * @param  rdata         
+ * @param  rdlen         
+ * @return 0 or an error
+ */
+int kr_ta_add(map_t *trust_anchors, const knot_dname_t *name, uint16_t type,
+               uint32_t ttl, const uint8_t *rdata, uint16_t rdlen);
+
+/**
+ * Return true if the name is below/at any TA in the store.
+ * This can be useful to check if it's possible to validate a name beforehand.
+ * @param  trust_anchors trust store
+ * @param  name          name of the TA
+ * @return boolean
+ */
+int kr_ta_covers(map_t *trust_anchors, const knot_dname_t *name);
+
+/**
+ * Remove TA from trust store.
+ * @param  trust_anchors trust store
+ * @param  name          name of the TA
+ * @return 0 or an error
+ */
+int kr_ta_del(map_t *trust_anchors, const knot_dname_t *name);
+
+/**
+ * Clear trust store.
+ * @param trust_anchors trust store
+ */
+void kr_ta_clear(map_t *trust_anchors);
diff --git a/lib/generic/map.h b/lib/generic/map.h
index 7d40f9b528bb4f19718fc0a19c14a5d491fa3027..dc459f5ef9f271c9bff8bb2d1fd426aab16cc10a 100644
--- a/lib/generic/map.h
+++ b/lib/generic/map.h
@@ -18,10 +18,11 @@
  *      map.malloc = &mymalloc;
  *      map.baton  = &mymalloc_context;
  *
- *      // Insert keys
- *      if (map_set(&map, "princess") != 0 ||
- *          map_set(&map, "prince")   != 0 ||
- *          map_set(&map, "leia")     != 0) {
+ *      // Insert k-v pairs
+ *      int values = { 42, 53, 64 };
+ *      if (map_set(&map, "princess", &values[0]) != 0 ||
+ *          map_set(&map, "prince", &values[1])   != 0 ||
+ *          map_set(&map, "leia", &values[2])     != 0) {
  *          fail();
  *      }
  *
diff --git a/lib/layer.h b/lib/layer.h
index db358c7e949b0e37dd471c2637be9dd9cdb895b1..827118dfe58241080020f140be4cc5c72c285f5b 100644
--- a/lib/layer.h
+++ b/lib/layer.h
@@ -17,15 +17,15 @@
 #pragma once
 
 #include "lib/defines.h"
+#include "lib/utils.h"
 #include "lib/resolve.h"
 
-#ifdef WITH_DEBUG
-/** @internal Print a debug message related to resolution. */
+#ifndef NDEBUG
+ /** @internal Print a debug message related to resolution. */
  #define QRDEBUG(query, cls, fmt, ...) do { \
     unsigned _ind = 0; \
     for (struct kr_query *q = (query); q; q = q->parent, _ind += 2); \
-    fprintf(stdout, "[%s] %*s" fmt, cls, _ind, "", ##  __VA_ARGS__); \
-    fflush(stdout); \
+    log_debug("[%s] %*s" fmt, cls, _ind, "", ##  __VA_ARGS__); \
     } while (0)
 #else
  #define QRDEBUG(query, cls, fmt, ...)
diff --git a/lib/layer/iterate.c b/lib/layer/iterate.c
index 98e0c6212e0a54b5bc60355dfd6cf745e0697bd9..aa320c279c9a7b580f8d8b834bbe1902463d6507 100644
--- a/lib/layer/iterate.c
+++ b/lib/layer/iterate.c
@@ -63,6 +63,7 @@ static bool is_paired_to_query(const knot_pkt_t *answer, struct kr_query *query)
 	const knot_dname_t *qname = minimized_qname(query, &qtype);
 
 	return query->id      == knot_wire_get_id(answer->wire) &&
+	       knot_wire_get_qdcount(answer->wire) > 0 &&
 	       (query->sclass == KNOT_CLASS_ANY || query->sclass  == knot_pkt_qclass(answer)) &&
 	       qtype          == knot_pkt_qtype(answer) &&
 	       knot_dname_is_equal(qname, knot_pkt_qname(answer));
@@ -163,7 +164,14 @@ static int update_parent(const knot_rrset_t *rr, struct kr_request *req)
 
 static int update_answer(const knot_rrset_t *rr, unsigned hint, struct kr_request *req)
 {
+	/* Scrub DNSSEC records when not requested. */
 	knot_pkt_t *answer = req->answer;
+	if (!knot_pkt_has_dnssec(answer)) {
+		if (rr->type != knot_pkt_qtype(answer) && knot_rrtype_is_dnssec(rr->type)) {
+			return KNOT_STATE_DONE; /* Scrub */
+		}
+	}
+
 	int ret = knot_pkt_put(answer, hint, rr, 0);
 	if (ret != KNOT_EOK) {
 		knot_wire_set_tc(answer->wire);
@@ -187,7 +195,7 @@ static void fetch_glue(knot_pkt_t *pkt, const knot_dname_t *ns, struct kr_query
 }
 
 /** Attempt to find glue for given nameserver name (best effort). */
-static int has_glue(knot_pkt_t *pkt, const knot_dname_t *ns, struct kr_request *req)
+static int has_glue(knot_pkt_t *pkt, const knot_dname_t *ns)
 {
 	for (knot_section_t i = KNOT_ANSWER; i <= KNOT_ADDITIONAL; ++i) {
 		const knot_pktsection_t *sec = knot_pkt_section(pkt, i);
@@ -204,8 +212,8 @@ static int has_glue(knot_pkt_t *pkt, const knot_dname_t *ns, struct kr_request *
 
 static int update_cut(knot_pkt_t *pkt, const knot_rrset_t *rr, struct kr_request *req)
 {
-	struct kr_query *query = kr_rplan_current(&req->rplan);	
-	struct kr_zonecut *cut = &query->zone_cut;
+	struct kr_query *qry = kr_rplan_current(&req->rplan);
+	struct kr_zonecut *cut = &qry->zone_cut;
 	int state = KNOT_STATE_CONSUME;
 
 	/* Authority MUST be at/below the authority of the nameserver, otherwise
@@ -224,14 +232,14 @@ static int update_cut(knot_pkt_t *pkt, const knot_rrset_t *rr, struct kr_request
 	/* Fetch glue for each NS */
 	for (unsigned i = 0; i < rr->rrs.rr_count; ++i) {
 		const knot_dname_t *ns_name = knot_ns_name(&rr->rrs, i);
-		int glue_records = has_glue(pkt, ns_name, req);
+		int glue_records = has_glue(pkt, ns_name);
 		/* Glue is mandatory for NS below zone */
 		if (!glue_records && knot_dname_in(rr->owner, ns_name)) {
 			DEBUG_MSG("<= authority: missing mandatory glue, rejecting\n");
 			continue;
 		}
 		kr_zonecut_add(cut, ns_name, NULL);
-		fetch_glue(pkt, ns_name, query);
+		fetch_glue(pkt, ns_name, qry);
 	}
 
 	return state;
@@ -240,6 +248,7 @@ static int update_cut(knot_pkt_t *pkt, const knot_rrset_t *rr, struct kr_request
 static int process_authority(knot_pkt_t *pkt, struct kr_request *req)
 {
 	int result = KNOT_STATE_CONSUME;
+	struct kr_query *qry = kr_rplan_current(&req->rplan);
 	const knot_pktsection_t *ns = knot_pkt_section(pkt, KNOT_AUTHORITY);
 
 #ifdef STRICT_MODE
@@ -268,6 +277,14 @@ static int process_authority(knot_pkt_t *pkt, struct kr_request *req)
 			case KNOT_STATE_FAIL: return state; break;
 			default:              /* continue */ break;
 			}
+		} else if (rr->type == KNOT_RRTYPE_SOA) {
+			/* SOA below cut in authority indicates different authority, but same NS set. */
+			if (knot_dname_is_sub(rr->owner, qry->zone_cut.name)) {
+				qry->zone_cut.name = knot_dname_copy(rr->owner, &req->pool);
+				if (qry->flags & QUERY_DNSSEC_WANT) { /* Treat as a referral */
+					return KNOT_STATE_DONE;
+				}
+			}
 		}
 	}
 
@@ -312,7 +329,7 @@ static int process_answer(knot_pkt_t *pkt, struct kr_request *req)
 	    (pkt_class & (PKT_NOERROR|PKT_NXDOMAIN|PKT_REFUSED|PKT_NODATA))) {
 		DEBUG_MSG("<= found cut, retrying with non-minimized name\n");
 		query->flags |= QUERY_NO_MINIMIZE;
-		return KNOT_STATE_DONE;
+		return KNOT_STATE_CONSUME;
 	}
 
 	/* This answer didn't improve resolution chain, therefore must be authoritative (relaxed to negative). */
@@ -469,10 +486,10 @@ static int resolve(knot_layer_t *ctx, knot_pkt_t *pkt)
 			}
 			query->flags |= QUERY_TCP;
 		}
-		return KNOT_STATE_DONE;
+		return KNOT_STATE_CONSUME;
 	}
 
-#ifdef WITH_DEBUG
+#ifndef NDEBUG
 	lookup_table_t *rcode = lookup_by_id(knot_rcode_names, knot_wire_get_rcode(pkt->wire));
 #endif
 
diff --git a/lib/layer/pktcache.c b/lib/layer/pktcache.c
index 0193772f902cf6a2addf78f90f0941b9625c5ad1..46f5a3177db3751496346a93a4d8e21ef7409e2d 100644
--- a/lib/layer/pktcache.c
+++ b/lib/layer/pktcache.c
@@ -26,9 +26,9 @@
 #define DEFAULT_MAXTTL (15 * 60)
 #define DEFAULT_NOTTL (5) /* Short-time "no data" retention to avoid bursts */
 
-static inline uint8_t get_tag(knot_pkt_t *pkt)
+static inline uint8_t get_tag(struct kr_query *qry)
 {
-	return knot_pkt_has_dnssec(pkt) ? KR_CACHE_SEC : KR_CACHE_PKT;
+	return (qry->flags & QUERY_DNSSEC_WANT) ? KR_CACHE_SEC : KR_CACHE_PKT;
 }
 
 static uint32_t limit_ttl(uint32_t ttl)
@@ -97,11 +97,11 @@ static int peek(knot_layer_t *ctx, knot_pkt_t *pkt)
 	struct kr_request *req = ctx->data;
 	struct kr_rplan *rplan = &req->rplan;
 	struct kr_query *qry = kr_rplan_current(rplan);
-	if (!qry || ctx->state & (KNOT_STATE_DONE|KNOT_STATE_FAIL)) {
+	if (ctx->state & (KNOT_STATE_FAIL|KNOT_STATE_DONE) || (qry->flags & QUERY_NO_CACHE)) {
 		return ctx->state; /* Already resolved/failed */
 	}
-	if (!(qry->flags & QUERY_AWAIT_CUT)) {
-		return ctx->state; /* Only lookup on first iteration */
+	if (qry->ns.addr.ip.sa_family != AF_UNSPEC) {
+		return ctx->state; /* Only lookup before asking a query */
 	}
 	if (knot_pkt_qclass(pkt) != KNOT_CLASS_IN) {
 		return ctx->state; /* Only IN class */
@@ -115,7 +115,7 @@ static int peek(knot_layer_t *ctx, knot_pkt_t *pkt)
 	}
 
 	/* Fetch either answer to original or minimized query */
-	uint8_t tag = get_tag(req->answer);
+	uint8_t tag = get_tag(qry);
 	int ret = loot_cache(&txn, pkt, tag, qry);
 	kr_cache_txn_abort(&txn);
 	if (ret == 0) {
@@ -197,7 +197,7 @@ static int stash(knot_layer_t *ctx, knot_pkt_t *pkt)
 	};
 
 	/* Stash answer in the cache */
-	int ret = kr_cache_insert(&txn, get_tag(pkt), qname, qtype, &header, data);	
+	int ret = kr_cache_insert(&txn, get_tag(qry), qname, qtype, &header, data);	
 	if (ret != 0) {
 		kr_cache_txn_abort(&txn);
 	} else {
diff --git a/lib/layer/rrcache.c b/lib/layer/rrcache.c
index 1025d434ed2f171245e91eac8ab7403c6eabe648..d440fdd47505249f68849eb149d65a4ce4dfe205 100644
--- a/lib/layer/rrcache.c
+++ b/lib/layer/rrcache.c
@@ -17,6 +17,7 @@
 #include <libknot/descriptor.h>
 #include <libknot/errcode.h>
 #include <libknot/rrset.h>
+#include <libknot/rrtype/rrsig.h>
 #include <libknot/rrtype/rdname.h>
 #include <ucw/config.h>
 #include <ucw/lib.h>
@@ -24,6 +25,7 @@
 #include "lib/layer/iterate.h"
 #include "lib/cache.h"
 #include "lib/module.h"
+#include "lib/rrset_stash.h"
 
 #define DEBUG_MSG(fmt...) QRDEBUG(kr_rplan_current(rplan), " rc ",  fmt)
 #define DEFAULT_MINTTL (5) /* Short-time "no data" retention to avoid bursts */
@@ -35,24 +37,25 @@ static inline bool is_expiring(const knot_rrset_t *rr, uint32_t drift)
 }
 
 static int loot_rr(struct kr_cache_txn *txn, knot_pkt_t *pkt, const knot_dname_t *name,
-                  uint16_t rrclass, uint16_t rrtype, struct kr_query *qry)
+                  uint16_t rrclass, uint16_t rrtype, struct kr_query *qry, bool fetch_rrsig)
 {
 	/* Check if record exists in cache */
+	int ret = 0;
 	uint32_t drift = qry->timestamp.tv_sec;
 	knot_rrset_t cache_rr;
 	knot_rrset_init(&cache_rr, (knot_dname_t *)name, rrtype, rrclass);
-	int ret = kr_cache_peek_rr(txn, &cache_rr, &drift);
+	if (fetch_rrsig) {
+		ret = kr_cache_peek_rrsig(txn, &cache_rr, &drift);
+	} else {
+		ret = kr_cache_peek_rr(txn, &cache_rr, &drift);	
+	}
 	if (ret != 0) {
 		return ret;
 	}
 
 	/* Mark as expiring if it has less than 1% TTL (or less than 5s) */
 	if (is_expiring(&cache_rr, drift)) {
-		if (qry->flags & QUERY_NO_EXPIRING) {
-			return kr_error(ENOENT);
-		} else {
-			qry->flags |= QUERY_EXPIRING;
-		}
+		qry->flags |= QUERY_EXPIRING;
 	}
 
 	/* Update packet question */
@@ -74,7 +77,7 @@ static int loot_rr(struct kr_cache_txn *txn, knot_pkt_t *pkt, const knot_dname_t
 }
 
 /** @internal Try to find a shortcut directly to searched record. */
-static int loot_cache(struct kr_cache *cache, knot_pkt_t *pkt, struct kr_query *qry)
+static int loot_cache(struct kr_cache *cache, knot_pkt_t *pkt, struct kr_query *qry, bool dobit)
 {
 	struct kr_cache_txn txn;
 	int ret = kr_cache_txn_begin(cache, &txn, NAMEDB_RDONLY);
@@ -82,9 +85,15 @@ static int loot_cache(struct kr_cache *cache, knot_pkt_t *pkt, struct kr_query *
 		return ret;
 	}
 	/* Lookup direct match first */
-	ret = loot_rr(&txn, pkt, qry->sname, qry->sclass, qry->stype, qry);
-	if (ret != 0 && qry->stype != KNOT_RRTYPE_CNAME) { /* Chase CNAME if no direct hit */
-		ret = loot_rr(&txn, pkt, qry->sname, qry->sclass, KNOT_RRTYPE_CNAME, qry);
+	uint16_t rrtype = qry->stype;
+	ret = loot_rr(&txn, pkt, qry->sname, qry->sclass, rrtype, qry, 0);
+	if (ret != 0 && rrtype != KNOT_RRTYPE_CNAME) { /* Chase CNAME if no direct hit */
+		rrtype = KNOT_RRTYPE_CNAME;
+		ret = loot_rr(&txn, pkt, qry->sname, qry->sclass, rrtype, qry, 0);
+	}
+	/* Loot RRSIG if matched. */
+	if (ret == 0 && dobit) {
+		ret = loot_rr(&txn, pkt, qry->sname, qry->sclass, rrtype, qry, true);
 	}
 	kr_cache_txn_abort(&txn);
 	return ret;
@@ -95,11 +104,11 @@ static int peek(knot_layer_t *ctx, knot_pkt_t *pkt)
 	struct kr_request *req = ctx->data;
 	struct kr_rplan *rplan = &req->rplan;
 	struct kr_query *qry = kr_rplan_current(rplan);
-	if (!qry || ctx->state & (KNOT_STATE_FAIL|KNOT_STATE_DONE)) {
+	if (ctx->state & (KNOT_STATE_FAIL|KNOT_STATE_DONE) || (qry->flags & QUERY_NO_CACHE)) {
 		return ctx->state; /* Already resolved/failed */
 	}
-	if (!(qry->flags & QUERY_AWAIT_CUT)) {
-		return ctx->state; /* Only lookup on first iteration */
+	if (qry->ns.addr.ip.sa_family != AF_UNSPEC) {
+		return ctx->state; /* Only lookup before asking a query */
 	}
 
 	/* Reconstruct the answer from the cache,
@@ -107,7 +116,7 @@ static int peek(knot_layer_t *ctx, knot_pkt_t *pkt)
 	 * Only one step of the chain is resolved at a time.
 	 */
 	struct kr_cache *cache = &req->ctx->cache;
-	int ret = loot_cache(cache, pkt, qry);
+	int ret = loot_cache(cache, pkt, qry, (qry->flags & QUERY_DNSSEC_WANT));
 	if (ret == 0) {
 		DEBUG_MSG("=> satisfied from cache\n");
 		qry->flags |= QUERY_CACHED|QUERY_NO_MINIMIZE;
@@ -122,11 +131,30 @@ static int peek(knot_layer_t *ctx, knot_pkt_t *pkt)
 /** @internal Baton for stash_commit */
 struct stash_baton
 {
+	struct kr_request *req;
+	struct kr_query *qry;
 	struct kr_cache_txn *txn;
 	unsigned timestamp;
 	uint32_t min_ttl;
 };
 
+static int commit_rrsig(struct stash_baton *baton, knot_rrset_t *rr)
+{
+	/* If not doing secure resolution, ignore (unvalidated) RRSIGs. */
+	if (!(baton->qry->flags & QUERY_DNSSEC_WANT)) {
+		return kr_ok();
+	}
+	/* Commit covering RRSIG to a separate cache namespace. */
+	uint16_t covered = knot_rrsig_type_covered(&rr->rrs, 0);
+	unsigned drift = baton->timestamp;
+	knot_rrset_t query_rrsig;
+	knot_rrset_init(&query_rrsig, rr->owner, covered, rr->rclass);
+	if (kr_cache_peek_rrsig(baton->txn, &query_rrsig, &drift) == 0) {
+		return kr_ok();
+	}
+	return kr_cache_insert_rrsig(baton->txn, rr, covered, baton->timestamp);
+}
+
 static int commit_rr(const char *key, void *val, void *data)
 {
 	knot_rrset_t *rr = val;
@@ -140,59 +168,32 @@ static int commit_rr(const char *key, void *val, void *data)
 		rd = kr_rdataset_next(rd);
 	}
 
+	/* Save RRSIG in a special cache. */
+	if (KEY_COVERING_RRSIG(key)) {
+		return commit_rrsig(baton, rr);
+	}
+
 	/* Check if already cached */
 	/** @todo This should check if less trusted data is in the cache,
 	          for that the cache would need to trace data trust level.
 	   */
-	unsigned drift = baton->timestamp;
 	knot_rrset_t query_rr;
 	knot_rrset_init(&query_rr, rr->owner, rr->type, rr->rclass);
-	if (kr_cache_peek_rr(baton->txn, &query_rr, &drift) == 0) {
-		/* Allow replace if RRSet in the cache is about to expire. */
-		if (!is_expiring(&query_rr, drift)) {
-		        return kr_ok();
-		}
-	}
 	return kr_cache_insert_rr(baton->txn, rr, baton->timestamp);
 }
 
-static int stash_commit(map_t *stash, unsigned timestamp, struct kr_cache_txn *txn)
+static int stash_commit(map_t *stash, struct kr_query *qry, struct kr_cache_txn *txn, struct kr_request *req)
 {
 	struct stash_baton baton = {
+		.req = req,
+		.qry = qry,
 		.txn = txn,
-		.timestamp = timestamp,
+		.timestamp = qry->timestamp.tv_sec,
 		.min_ttl = DEFAULT_MINTTL
 	};
 	return map_walk(stash, &commit_rr, &baton);
 }
 
-static int stash_add(map_t *stash, const knot_rrset_t *rr, mm_ctx_t *pool)
-{
-	/* Stash key = {[1-255] owner, [1-5] type, [1] \x00 } */
-	char key[8 + KNOT_DNAME_MAXLEN];
-	int ret = knot_dname_to_wire((uint8_t *)key, rr->owner, KNOT_DNAME_MAXLEN);
-	if (ret <= 0) {
-		return ret;
-	}
-	knot_dname_to_lower((uint8_t *)key);
-	ret = snprintf(key + ret - 1, sizeof(key) - KNOT_DNAME_MAXLEN, "%hu", rr->type);
-	if (ret <= 0 || ret >= KNOT_DNAME_MAXLEN) {
-		return kr_error(EILSEQ);
-	}
-	
-	/* Check if already exists */
-	knot_rrset_t *stashed = map_get(stash, key);
-	if (!stashed) {
-		stashed = knot_rrset_copy(rr, pool);
-		if (!stashed) {
-			return kr_error(ENOMEM);
-		}
-		return map_set(stash, key, stashed);
-	}
-	/* Merge rdataset */
-	return knot_rdataset_merge(&stashed->rrs, &rr->rrs, pool);
-}
-
 static void stash_glue(map_t *stash, knot_pkt_t *pkt, const knot_dname_t *ns_name, mm_ctx_t *pool)
 {
 	const knot_pktsection_t *additional = knot_pkt_section(pkt, KNOT_ADDITIONAL);
@@ -202,7 +203,19 @@ static void stash_glue(map_t *stash, knot_pkt_t *pkt, const knot_dname_t *ns_nam
 		    !knot_dname_is_equal(rr->owner, ns_name)) {
 			continue;
 		}
-		stash_add(stash, rr, pool);
+		stash_add(pkt, stash, rr, pool);
+	}
+}
+
+/* @internal DS is special and is present only parent-side */
+static void stash_ds(struct kr_query *qry, knot_pkt_t *pkt, map_t *stash, mm_ctx_t *pool)
+{
+	const knot_pktsection_t *authority = knot_pkt_section(pkt, KNOT_AUTHORITY);
+	for (unsigned i = 0; i < authority->count; ++i) {
+		const knot_rrset_t *rr = knot_pkt_rr(authority, i);
+		if (rr->type == KNOT_RRTYPE_DS || rr->type == KNOT_RRTYPE_RRSIG) {
+			stash_add(pkt, stash, rr, pool);
+		}
 	}
 }
 
@@ -220,7 +233,7 @@ static int stash_authority(struct kr_query *qry, knot_pkt_t *pkt, map_t *stash,
 			stash_glue(stash, pkt, knot_ns_name(&rr->rrs, 0), pool);
 		}
 		/* Stash record */
-		stash_add(stash, rr, pool);
+		stash_add(pkt, stash, rr, pool);
 	}
 	return kr_ok();
 }
@@ -230,12 +243,14 @@ static int stash_answer(struct kr_query *qry, knot_pkt_t *pkt, map_t *stash, mm_
 	const knot_dname_t *cname = qry->sname;
 	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
 	for (unsigned i = 0; i < answer->count; ++i) {
-		/* Stash direct answers (equal to current QNAME/CNAME) */
+		/* Stash direct answers (equal to current QNAME/CNAME),
+		 * accept out-of-order RRSIGS. */
 		const knot_rrset_t *rr = knot_pkt_rr(answer, i);
-		if (!knot_dname_is_equal(rr->owner, cname)) {
+		if (!knot_dname_is_equal(rr->owner, cname)
+		    && rr->type != KNOT_RRTYPE_RRSIG) {
 			continue;
 		}
-		stash_add(stash, rr, pool);
+		stash_add(pkt, stash, rr, pool);
 		/* Follow CNAME chain */
 		if (rr->type == KNOT_RRTYPE_CNAME) {
 			cname = knot_cname_name(&rr->rrs);
@@ -273,13 +288,17 @@ static int stash(knot_layer_t *ctx, knot_pkt_t *pkt)
 	if (!is_auth || qry != HEAD(rplan->pending)) {
 		ret = stash_authority(qry, pkt, &stash, rplan->pool);
 	}
+	/* Cache DS records in referrals */
+	if (!is_auth && knot_pkt_has_dnssec(pkt)) {
+		stash_ds(qry, pkt, &stash, rplan->pool);
+	}
 	/* Cache stashed records */
 	if (ret == 0) {
 		/* Open write transaction */
 		struct kr_cache *cache = &req->ctx->cache;
 		struct kr_cache_txn txn;
 		if (kr_cache_txn_begin(cache, &txn, 0) == 0) {
-			ret = stash_commit(&stash, qry->timestamp.tv_sec, &txn);
+			ret = stash_commit(&stash, qry, &txn, req);
 			if (ret == 0) {
 				kr_cache_txn_commit(&txn);
 			} else {
diff --git a/lib/layer/validate.c b/lib/layer/validate.c
new file mode 100644
index 0000000000000000000000000000000000000000..f282317e49fc94c2d9ef0dfd1ca0ab3d3bfe5230
--- /dev/null
+++ b/lib/layer/validate.c
@@ -0,0 +1,441 @@
+/*  Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <sys/time.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <libknot/packet/wire.h>
+#include <libknot/rrtype/rdname.h>
+#include <libknot/rrtype/rrsig.h>
+
+#include "lib/dnssec/nsec.h"
+#include "lib/dnssec/nsec3.h"
+#include "lib/dnssec/packet/pkt.h"
+#include "lib/dnssec.h"
+#include "lib/layer.h"
+#include "lib/resolve.h"
+#include "lib/rplan.h"
+#include "lib/rrset_stash.h"
+#include "lib/defines.h"
+#include "lib/module.h"
+
+#define DEBUG_MSG(qry, fmt...) QRDEBUG(qry, "vldr", fmt)
+
+/** @internal Baton for validate_section */
+struct stash_baton {
+	const knot_pkt_t *pkt;
+	knot_section_t section_id;
+	const knot_rrset_t *keys;
+	const knot_dname_t *zone_name;
+	uint32_t timestamp;
+	bool has_nsec3;
+	int result;
+};
+
+static int validate_rrset(const char *key, void *val, void *data)
+{
+	knot_rrset_t *rr = val;
+	struct stash_baton *baton = data;
+
+	if (baton->result != 0) {
+		return baton->result;
+	}
+	baton->result = kr_rrset_validate(baton->pkt, baton->section_id, rr,
+	                                  baton->keys, baton->zone_name,
+	                                  baton->timestamp, baton->has_nsec3);
+	return baton->result;
+}
+
+static int validate_section(struct kr_query *qry, knot_pkt_t *answer,
+                            knot_section_t section_id, mm_ctx_t *pool,
+                            bool has_nsec3)
+{
+	const knot_pktsection_t *sec = knot_pkt_section(answer, section_id);
+	if (!sec) {
+		return kr_ok();
+	}
+
+	int ret = kr_ok();
+
+	map_t stash = map_make();
+	stash.malloc = (map_alloc_f) mm_alloc;
+	stash.free = (map_free_f) mm_free;
+	stash.baton = pool;
+
+	/* Determine RR types contained in the section. */
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rr = knot_pkt_rr(sec, i);
+		if (rr->type == KNOT_RRTYPE_RRSIG) {
+			continue;
+		}
+		if ((rr->type == KNOT_RRTYPE_NS) && (section_id == KNOT_AUTHORITY)) {
+			continue;
+		}
+		ret = stash_add(answer, &stash, rr, pool);
+		if (ret != 0) {
+			goto fail;
+		}
+	}
+
+	struct stash_baton baton = {
+		.pkt = answer,
+		.section_id = section_id,
+		.keys = qry->zone_cut.key,
+		/* Can't use qry->zone_cut.name directly, as this name can
+		 * change when updating cut information before validation.
+		 */
+		.zone_name = qry->zone_cut.key ? qry->zone_cut.key->owner : NULL,
+		.timestamp = qry->timestamp.tv_sec,
+		.has_nsec3 = has_nsec3,
+		.result = 0
+	};
+
+	ret = map_walk(&stash, &validate_rrset, &baton);
+	if (ret != 0) {
+		return ret;
+	}
+	ret = baton.result;
+
+fail:
+	return ret;
+}
+
+static int validate_records(struct kr_query *qry, knot_pkt_t *answer, mm_ctx_t *pool, bool has_nsec3)
+{
+	if (!qry->zone_cut.key) {
+		DEBUG_MSG(qry, "<= no DNSKEY, can't validate\n");
+		return kr_error(EBADMSG);
+	}
+
+	int ret = validate_section(qry, answer, KNOT_ANSWER, pool, has_nsec3);
+	if (ret != 0) {
+		return ret;
+	}
+
+	return validate_section(qry, answer, KNOT_AUTHORITY, pool, has_nsec3);
+}
+
+static int validate_keyset(struct kr_query *qry, knot_pkt_t *answer, bool has_nsec3)
+{
+	/* Merge DNSKEY records from answer that are below/at current cut. */
+	const knot_pktsection_t *an = knot_pkt_section(answer, KNOT_ANSWER);
+	for (unsigned i = 0; i < an->count; ++i) {
+		const knot_rrset_t *rr = knot_pkt_rr(an, i);
+		if ((rr->type != KNOT_RRTYPE_DNSKEY) || !knot_dname_in(qry->zone_cut.name, rr->owner)) {
+			continue;
+		}
+		/* Merge with zone cut (or replace ancestor key). */
+		if (!qry->zone_cut.key || !knot_dname_is_equal(qry->zone_cut.key->owner, rr->owner)) {
+			qry->zone_cut.key = knot_rrset_copy(rr, qry->zone_cut.pool);
+			if (!qry->zone_cut.key) {
+				return kr_error(ENOMEM);
+			}
+		} else {
+			int ret = knot_rdataset_merge(&qry->zone_cut.key->rrs,
+			                              &rr->rrs, qry->zone_cut.pool);
+			if (ret != 0) {
+				knot_rrset_free(&qry->zone_cut.key, qry->zone_cut.pool);
+				return ret;
+			}
+		}
+	}
+	if (!qry->zone_cut.key) {
+		return kr_error(EBADMSG);
+	}
+
+	/* Check if there's a key for current TA. */
+	if (!(qry->flags & QUERY_CACHED)) {
+		int ret = kr_dnskeys_trusted(answer, KNOT_ANSWER, qry->zone_cut.key,
+		                             qry->zone_cut.trust_anchor, qry->zone_cut.name,
+		                             qry->timestamp.tv_sec, has_nsec3);
+		if (ret != 0) {
+			knot_rrset_free(&qry->zone_cut.key, qry->zone_cut.pool);
+			return ret;
+		}
+	}
+	return kr_ok();
+}
+
+static const knot_dname_t *section_first_signer_name(knot_pkt_t *pkt, knot_section_t section_id)
+{
+	const knot_dname_t *sname = NULL;
+	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
+	if (!sec) {
+		return sname;
+	}
+
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rr = knot_pkt_rr(sec, i);
+		if (rr->type != KNOT_RRTYPE_RRSIG) {
+			continue;
+		}
+
+		sname = knot_rrsig_signer_name(&rr->rrs, 0);
+		break;
+	}
+
+	return sname;
+}
+
+static const knot_dname_t *first_rrsig_signer_name(knot_pkt_t *answer)
+{
+	const knot_dname_t *ans_sname = section_first_signer_name(answer, KNOT_ANSWER);
+	const knot_dname_t *auth_sname = section_first_signer_name(answer, KNOT_AUTHORITY);
+
+	if (!ans_sname) {
+		return auth_sname;
+	} else if (!auth_sname) {
+		return ans_sname;
+	} else if (knot_dname_is_equal(ans_sname, auth_sname)) {
+		return ans_sname;
+	} else {
+		return NULL;
+	}
+}
+
+static knot_rrset_t *update_ds(struct kr_zonecut *cut, const knot_pktsection_t *sec)
+{
+	/* Aggregate DS records (if using multiple keys) */
+	knot_rrset_t *new_ds = NULL;
+	for (unsigned i = 0; i < sec->count; ++i) {
+		const knot_rrset_t *rr = knot_pkt_rr(sec, i);
+		if (rr->type != KNOT_RRTYPE_DS) {
+			continue;
+		}
+		int ret = 0;
+		if (new_ds) {
+			ret = knot_rdataset_merge(&new_ds->rrs, &rr->rrs, cut->pool);
+		} else {
+			new_ds = knot_rrset_copy(rr, cut->pool);
+			if (!new_ds) {
+				return NULL;
+			}
+		}
+		if (ret != 0) {
+			knot_rrset_free(&new_ds, cut->pool);
+			return NULL;
+		}
+	}
+	return new_ds;	
+}
+
+static int update_parent(struct kr_query *qry, uint16_t answer_type)
+{
+	struct kr_query *parent = qry->parent;
+	assert(parent);
+	switch(answer_type) {
+	case KNOT_RRTYPE_DNSKEY:
+		DEBUG_MSG(qry, "<= parent: updating DNSKEY\n");
+		parent->zone_cut.key = knot_rrset_copy(qry->zone_cut.key, parent->zone_cut.pool);
+		if (!parent->zone_cut.key) {
+			return KNOT_STATE_FAIL;
+		}
+		break;
+	case KNOT_RRTYPE_DS:
+		DEBUG_MSG(qry, "<= parent: updating DS\n");
+		parent->zone_cut.trust_anchor = knot_rrset_copy(qry->zone_cut.trust_anchor, parent->zone_cut.pool);
+		if (!parent->zone_cut.trust_anchor) {
+			return KNOT_STATE_FAIL;
+		}
+		break;
+	default: break;
+	}
+	return kr_ok();
+}
+
+static int update_delegation(struct kr_request *req, struct kr_query *qry, knot_pkt_t *answer, bool has_nsec3)
+{
+	struct kr_zonecut *cut = &qry->zone_cut;
+
+	/* RFC4035 3.1.4. authoritative must send either DS or proof of non-existence.
+	 * If it contains neither, the referral is bogus (or an attempted downgrade attack).
+	 */
+
+	/* Aggregate DS records (if using multiple keys) */
+	unsigned section = KNOT_ANSWER;
+	if (!knot_wire_get_aa(answer->wire)) { /* Referral */
+		section = KNOT_AUTHORITY;
+	} else if (knot_pkt_qtype(answer) == KNOT_RRTYPE_DS) { /* Subrequest */
+		section = KNOT_ANSWER;
+	} else { /* N/A */
+		return kr_ok();
+	}
+
+	/* No DS provided, check for proof of non-existence. */
+	int ret = 0;
+	knot_rrset_t *new_ds = update_ds(cut, knot_pkt_section(answer, section));
+	if (!new_ds) {
+		if (has_nsec3) {
+			ret = kr_nsec3_no_data_response_check(answer, section,
+			      knot_pkt_qname(answer), KNOT_RRTYPE_DS);
+		} else {
+			ret = kr_nsec_no_data_response_check(answer, section,
+			      knot_pkt_qname(answer), KNOT_RRTYPE_DS);
+		}
+		if (ret != 0) {
+			DEBUG_MSG(qry, "<= bogus proof of DS non-existence\n");
+			qry->flags |= QUERY_DNSSEC_BOGUS;
+		} else {
+			DEBUG_MSG(qry, "<= DS doesn't exist, going insecure\n");
+			qry->flags &= ~QUERY_DNSSEC_WANT;
+		}
+		return ret;
+	}
+
+	/* Extend trust anchor */
+	DEBUG_MSG(qry, "<= DS: OK\n");
+	cut->trust_anchor = new_ds;
+	return ret;
+}
+
+static int validate(knot_layer_t *ctx, knot_pkt_t *pkt)
+{
+	int ret = 0;
+	struct kr_request *req = ctx->data;
+	struct kr_query *qry = kr_rplan_current(&req->rplan);
+	/* Ignore faulty or unprocessed responses. */
+	if (ctx->state & (KNOT_STATE_FAIL|KNOT_STATE_CONSUME)) {
+		return ctx->state;
+	}
+
+	/* Pass-through if user doesn't want secure answer. */
+	if (!(qry->flags & QUERY_DNSSEC_WANT)) {
+		return ctx->state;
+	}
+	/* Answer for RRSIG may not set DO=1, but all records MUST still validate. */
+	bool use_signatures = (knot_pkt_qtype(pkt) != KNOT_RRTYPE_RRSIG);
+	/* @todo do not cache RRSIG answers until RFC2181 credibility is implemented */
+	if (!use_signatures) {
+		knot_wire_set_rcode(pkt->wire, KNOT_RCODE_SERVFAIL); /* Prevent caching */
+	}
+	if (!(qry->flags & QUERY_CACHED) && !knot_pkt_has_dnssec(pkt) && !use_signatures) {
+		DEBUG_MSG(qry, "<= got insecure response\n");
+		qry->flags |= QUERY_DNSSEC_BOGUS;
+		return KNOT_STATE_FAIL;
+	}
+
+	/* Check if this is a DNSKEY answer, check trust chain and store. */
+	uint16_t qtype = knot_pkt_qtype(pkt);
+	bool has_nsec3 = _knot_pkt_has_type(pkt, KNOT_RRTYPE_NSEC3);
+	if (qtype == KNOT_RRTYPE_DNSKEY) {
+		ret = validate_keyset(qry, pkt, has_nsec3);
+		if (ret != 0) {
+			DEBUG_MSG(qry, "<= bad keys, broken trust chain\n");
+			qry->flags |= QUERY_DNSSEC_BOGUS;
+			return KNOT_STATE_FAIL;
+		}
+	}
+
+	/* Check whether the current zone cut holds keys that can be used
+	 * for validation (i.e. RRSIG signer name matches key owner).
+	 */
+	const knot_dname_t *key_own = qry->zone_cut.key ? qry->zone_cut.key->owner : NULL;
+	const knot_dname_t *sig_name = first_rrsig_signer_name(pkt);
+	if (use_signatures && key_own && sig_name && !knot_dname_is_equal(key_own, sig_name)) {
+		DEBUG_MSG(qry, ">< cut changed, needs revalidation\n");
+		knot_wire_set_rcode(pkt->wire, KNOT_RCODE_SERVFAIL); /* Prevent caching */
+		qry->flags &= ~QUERY_RESOLVED;
+		return KNOT_STATE_CONSUME;
+	}
+
+	uint8_t pkt_rcode = knot_wire_get_rcode(pkt->wire);
+
+	/* Validate non-existence proof if not positive answer. */
+	if (pkt_rcode == KNOT_RCODE_NXDOMAIN) {
+		/* @todo If knot_pkt_qname(pkt) is used instead of qry->sname then the tests crash. */
+		if (!has_nsec3) {
+			ret = kr_nsec_name_error_response_check(pkt, KNOT_AUTHORITY, qry->sname, &req->pool);
+		} else {
+			ret = kr_nsec3_name_error_response_check(pkt, KNOT_AUTHORITY, qry->sname);
+		}
+		if (ret != 0) {
+			DEBUG_MSG(qry, "<= bad NXDOMAIN proof\n");
+			qry->flags |= QUERY_DNSSEC_BOGUS;
+			return KNOT_STATE_FAIL;
+		}
+	}
+
+	/* @todo WTH, this needs API that just tries to find a proof and the caller
+	 * doesn't have to worry about NSEC/NSEC3
+	 * @todo rework this */
+	{
+		const knot_pktsection_t *sec = knot_pkt_section(pkt, KNOT_ANSWER);
+		uint16_t answer_count = sec ? sec->count : 0;
+
+		/* Validate no data response. */
+		if ((pkt_rcode == KNOT_RCODE_NOERROR) && (!answer_count) &&
+		    (KNOT_WIRE_AA_MASK & knot_wire_get_flags1(pkt->wire))) {
+			/* @todo
+			 * ? quick mechanism to determine which check to preform first
+			 * ? merge the functionality together to share code/resources
+			 */
+			if (!has_nsec3) {
+				ret = kr_nsec_no_data(pkt, KNOT_AUTHORITY, knot_pkt_qname(pkt), knot_pkt_qtype(pkt));
+			} else {
+				ret = kr_nsec3_no_data(pkt, KNOT_AUTHORITY, knot_pkt_qname(pkt), knot_pkt_qtype(pkt));
+			}
+			if (ret != 0) {
+				DEBUG_MSG(qry, "<= bad no data response proof\n");
+				qry->flags |= QUERY_DNSSEC_BOGUS;
+				return KNOT_STATE_FAIL;
+			}
+		}
+	}
+
+	/* Validate all records, fail as bogus if it doesn't match.
+	 * Do not revalidate data from cache, as it's already trusted. */
+	if (!(qry->flags & QUERY_CACHED)) {
+		ret = validate_records(qry, pkt, req->rplan.pool, has_nsec3);
+		if (ret != 0) {
+			DEBUG_MSG(qry, "<= couldn't validate RRSIGs\n");
+			qry->flags |= QUERY_DNSSEC_BOGUS;
+			return KNOT_STATE_FAIL;
+		}
+	}
+
+	/* Check and update current delegation point security status. */
+	ret = update_delegation(req, qry, pkt, has_nsec3);
+	if (ret != 0) {
+		return KNOT_STATE_FAIL;
+	}
+	/* Update parent query zone cut */
+	if (qry->parent) {
+		if (update_parent(qry, qtype) != 0) {
+			return KNOT_STATE_FAIL;
+		}
+	}
+	DEBUG_MSG(qry, "<= answer valid, OK\n");
+	return ctx->state;
+}
+/** Module implementation. */
+const knot_layer_api_t *validate_layer(struct kr_module *module)
+{
+	static const knot_layer_api_t _layer = {
+		.consume = &validate,
+	};
+	/* Store module reference */
+	return &_layer;
+}
+
+int validate_init(struct kr_module *module)
+{
+	return kr_ok();
+}
+
+KR_MODULE_EXPORT(validate)
diff --git a/lib/lib.mk b/lib/lib.mk
index 41067c7ec593a0d8045447bfd6f6b66f2960d1b2..3a1f8e04d86728cac2f4cb83a0a23651b27023ce 100644
--- a/lib/lib.mk
+++ b/lib/lib.mk
@@ -1,6 +1,7 @@
 ccan_EMBED := \
 	contrib/ccan/ilog/ilog.c \
 	contrib/ccan/isaac/isaac.c \
+	contrib/ccan/json/json.c \
 	contrib/ucw/mempool.c \
 	contrib/murmurhash3/murmurhash3.c
 
@@ -8,12 +9,20 @@ libkres_SOURCES := \
 	$(ccan_EMBED)          \
 	lib/generic/map.c      \
 	lib/layer/iterate.c    \
+	lib/layer/validate.c   \
 	lib/layer/rrcache.c    \
 	lib/layer/pktcache.c   \
+	lib/dnssec/nsec.c      \
+	lib/dnssec/nsec3.c     \
+	lib/dnssec/packet/pkt.c \
+	lib/dnssec/signature.c \
+	lib/dnssec/ta.c        \
+	lib/dnssec.c           \
 	lib/utils.c            \
 	lib/nsrep.c            \
 	lib/module.c           \
 	lib/resolve.c          \
+	lib/rrset_stash.c      \
 	lib/zonecut.c          \
 	lib/rplan.c            \
 	lib/cache.c
@@ -23,17 +32,25 @@ libkres_HEADERS := \
 	lib/generic/map.h      \
 	lib/generic/set.h      \
 	lib/layer.h            \
+	lib/dnssec/nsec.h      \
+	lib/dnssec/nsec3.h     \
+	lib/dnssec/packet/pkt.h \
+	lib/dnssec/rrtype/ds.h \
+	lib/dnssec/signature.h \
+	lib/dnssec/ta.h        \
+	lib/dnssec.h           \
 	lib/utils.h            \
 	lib/nsrep.h            \
 	lib/module.h           \
 	lib/resolve.h          \
+	lib/rrset_stash.h      \
 	lib/zonecut.h          \
 	lib/rplan.h            \
 	lib/cache.h
 
 # Dependencies
 libkres_DEPEND := 
-libkres_LIBS := $(libknot_LIBS)
+libkres_LIBS := $(libknot_LIBS) $(libdnssec_LIBS)
 libkres_TARGET := -Wl,-rpath,lib -Llib -lkres
 
 # Make library
diff --git a/lib/module.c b/lib/module.c
index acfb9e2812936b785fba45a9edbeae3d0ad69f63..071394940611722b5cb47b4441e181ed2a8e0097 100644
--- a/lib/module.c
+++ b/lib/module.c
@@ -25,10 +25,12 @@
 
 /* List of embedded modules */
 const knot_layer_api_t *iterate_layer(struct kr_module *module);
+const knot_layer_api_t *validate_layer(struct kr_module *module);
 const knot_layer_api_t *rrcache_layer(struct kr_module *module);
 const knot_layer_api_t *pktcache_layer(struct kr_module *module);
 static const struct kr_module embedded_modules[] = {
 	{ "iterate",  NULL, NULL, NULL, iterate_layer, NULL, NULL, NULL },
+	{ "validate", NULL, NULL, NULL, validate_layer, NULL, NULL, NULL },
 	{ "rrcache",  NULL, NULL, NULL, rrcache_layer, NULL, NULL, NULL },
 	{ "pktcache", NULL, NULL, NULL, pktcache_layer, NULL, NULL, NULL },
 };
diff --git a/lib/resolve.c b/lib/resolve.c
index 4d5b69a417072f513cd967eefb7a79c697c316da..8a24a48b03f9a4266a86effa687842bbe26a6379 100644
--- a/lib/resolve.c
+++ b/lib/resolve.c
@@ -24,6 +24,7 @@
 #include "lib/layer.h"
 #include "lib/rplan.h"
 #include "lib/layer/iterate.h"
+#include "lib/dnssec/ta.h"
 
 #define DEBUG_MSG(fmt...) QRDEBUG(kr_rplan_current(rplan), "resl",  fmt)
 
@@ -74,22 +75,25 @@ static int invalidate_ns(struct kr_rplan *rplan, struct kr_query *qry)
 	}
 }
 
-static int ns_fetch_cut(struct kr_query *qry, struct kr_request *req)
+static int ns_fetch_cut(struct kr_query *qry, struct kr_request *req, bool secured)
 {
-	struct kr_cache_txn txn;
 	int ret = 0;
 
-	/* If at/subdomain of parent zone cut, start top-down search */
-	struct kr_query *parent = qry->parent;
-	if (parent && knot_dname_in(parent->zone_cut.name, qry->sname)) {
-		return kr_zonecut_set_sbelt(req->ctx, &qry->zone_cut);
-	}
 	/* Find closest zone cut from cache */
-	if (kr_cache_txn_begin(&req->ctx->cache, &txn, NAMEDB_RDONLY) != 0) {
-		ret = kr_zonecut_set_sbelt(req->ctx, &qry->zone_cut);
-	} else {
-		ret = kr_zonecut_find_cached(req->ctx, &qry->zone_cut, qry->sname, &txn, qry->timestamp.tv_sec);
+	struct kr_cache_txn txn;
+	if (kr_cache_txn_begin(&req->ctx->cache, &txn, NAMEDB_RDONLY) == 0) {
+		/* If at/subdomain of parent zone cut, start from its encloser.
+		 * This is for case when we get to a dead end (and need glue from parent), or DS refetch. */
+		struct kr_query *parent = qry->parent;
+		if (parent && qry->sname[0] != '\0' && knot_dname_in(parent->zone_cut.name, qry->sname)) {
+			const knot_dname_t *encloser = knot_wire_next_label(parent->zone_cut.name, NULL);
+			ret = kr_zonecut_find_cached(req->ctx, &qry->zone_cut, encloser, &txn, qry->timestamp.tv_sec, secured);
+		} else {
+			ret = kr_zonecut_find_cached(req->ctx, &qry->zone_cut, qry->sname, &txn, qry->timestamp.tv_sec, secured);
+		}
 		kr_cache_txn_abort(&txn);
+	} else {
+		ret = kr_zonecut_set_sbelt(req->ctx, &qry->zone_cut);
 	}
 	return ret;
 }
@@ -153,10 +157,6 @@ static int edns_put(knot_pkt_t *pkt)
 static int edns_create(knot_pkt_t *pkt, knot_pkt_t *template, struct kr_request *req)
 {
 	pkt->opt_rr = knot_rrset_copy(req->ctx->opt_rr, &pkt->mm);
-	/* Set DO bit if set (DNSSEC requested). */
-	if (knot_pkt_has_dnssec(template)) {
-		knot_edns_set_do(pkt->opt_rr);
-	}
 	return knot_pkt_reserve(pkt, knot_edns_wire_size(pkt->opt_rr));
 }
 
@@ -174,24 +174,41 @@ static int answer_prepare(knot_pkt_t *answer, knot_pkt_t *query, struct kr_reque
 		if (ret != 0){
 			return ret;
 		}
+		/* Set DO bit if set (DNSSEC requested). */
+		if (knot_pkt_has_dnssec(query)) {
+			knot_edns_set_do(answer->opt_rr);
+		}
 	}
 	return kr_ok();
 }
 
-static int answer_finalize(knot_pkt_t *answer)
+static int answer_finalize(struct kr_request *request, int state)
 {
+	/* Write EDNS information */
+	knot_pkt_t *answer = request->answer;
 	knot_pkt_begin(answer, KNOT_ADDITIONAL);
 	if (answer->opt_rr) {
-		return edns_put(answer);
-
+		int ret = edns_put(answer);
+		if (ret != 0) {
+			return ret;
+		}
+	}
+	/* Set AD=1 if succeeded and requested secured answer. */
+	struct kr_rplan *rplan = &request->rplan;
+	if (state == KNOT_STATE_DONE && !EMPTY_LIST(rplan->resolved)) {
+		struct kr_query *last = TAIL(rplan->resolved);
+		/* Do not set AD for RRSIG query, as we can't validate it. */
+		if ((last->flags & QUERY_DNSSEC_WANT) && knot_pkt_has_dnssec(answer) &&
+			knot_pkt_qtype(answer) != KNOT_RRTYPE_RRSIG) {
+			knot_wire_set_ad(answer->wire);
+		}
 	}
 	return kr_ok();
 }
 
-static int query_finalize(struct kr_request *request, knot_pkt_t *pkt)
+static int query_finalize(struct kr_request *request, struct kr_query *qry, knot_pkt_t *pkt)
 {
 	/* Randomize query case (if not in safemode) */
-	struct kr_query *qry = kr_rplan_current(&request->rplan);
 	qry->secret = (qry->flags & QUERY_SAFEMODE) ? 0 : kr_rand_uint(UINT32_MAX);
 	knot_dname_t *qname_raw = (knot_dname_t *)knot_pkt_qname(pkt);
 	randomized_qname_case(qname_raw, qry->secret);
@@ -200,7 +217,11 @@ static int query_finalize(struct kr_request *request, knot_pkt_t *pkt)
 	knot_pkt_begin(pkt, KNOT_ADDITIONAL);
 	if (!(qry->flags & QUERY_SAFEMODE)) {
 		ret = edns_create(pkt, request->answer, request);
-		if (ret == 0) {
+		if (ret == 0) { /* Enable DNSSEC for query. */
+			if (qry->flags & QUERY_DNSSEC_WANT) {
+				knot_edns_set_do(pkt->opt_rr);
+				knot_wire_set_cd(pkt->wire);
+			}
 			ret = edns_put(pkt);
 		}
 	}
@@ -220,9 +241,12 @@ int kr_resolve_begin(struct kr_request *request, struct kr_context *ctx, knot_pk
 	return KNOT_STATE_CONSUME;
 }
 
-int kr_resolve_query(struct kr_request *request, const knot_dname_t *qname, uint16_t qclass, uint16_t qtype)
+static int resolve_query(struct kr_request *request, const knot_pkt_t *packet)
 {
 	struct kr_rplan *rplan = &request->rplan;
+	const knot_dname_t *qname = knot_pkt_qname(packet);
+	uint16_t qclass = knot_pkt_qclass(packet);
+	uint16_t qtype = knot_pkt_qtype(packet);
 	struct kr_query *qry = kr_rplan_push(rplan, NULL, qname, qclass, qtype);
 	if (!qry) {
 		return KNOT_STATE_FAIL;
@@ -230,6 +254,11 @@ int kr_resolve_query(struct kr_request *request, const knot_dname_t *qname, uint
 
 	/* Deferred zone cut lookup for this query. */
 	qry->flags |= QUERY_AWAIT_CUT;
+	/* Want DNSSEC if it's posible to secure this name (e.g. is covered by any TA) */
+	map_t *trust_anchors = &request->ctx->trust_anchors;
+	if (knot_pkt_has_dnssec(packet) && kr_ta_covers(trust_anchors, qname)) {
+		qry->flags |= QUERY_DNSSEC_WANT;
+	}
 
 	/* Initialize answer packet */
 	knot_pkt_t *answer = request->answer;
@@ -257,17 +286,14 @@ int kr_resolve_consume(struct kr_request *request, knot_pkt_t *packet)
 		if (answer_prepare(request->answer, packet, request) != 0) {
 			return KNOT_STATE_FAIL;
 		}
-		/* Start query resolution */
-		const knot_dname_t *qname = knot_pkt_qname(packet);
-		uint16_t qclass = knot_pkt_qclass(packet);
-		uint16_t qtype = knot_pkt_qtype(packet);
-		return kr_resolve_query(request, qname, qclass, qtype);
+		return resolve_query(request, packet);
 	}
 
 	/* Different processing for network error */
+	bool tried_tcp = (qry->flags & QUERY_TCP);
 	if (!packet || packet->size == 0) {
 		/* Network error, retry over TCP. */
-		if (!(qry->flags & QUERY_TCP)) {
+		if (!tried_tcp) {
 			DEBUG_MSG("=> NS unreachable, retrying over TCP\n");
 			qry->flags |= QUERY_TCP;
 			return KNOT_STATE_PRODUCE;
@@ -286,6 +312,7 @@ int kr_resolve_consume(struct kr_request *request, knot_pkt_t *packet)
 	if (request->state == KNOT_STATE_FAIL) {
 		kr_nsrep_update_rtt(&qry->ns, KR_NS_TIMEOUT, ctx->cache_rtt);
 		invalidate_ns(rplan, qry);
+		qry->flags &= ~QUERY_RESOLVED;
 	/* Track RTT for iterative answers */
 	} else if (!(qry->flags & QUERY_CACHED)) {
 		struct timeval now;
@@ -298,14 +325,113 @@ int kr_resolve_consume(struct kr_request *request, knot_pkt_t *packet)
 	/* Pop query if resolved. */
 	if (qry->flags & QUERY_RESOLVED) {
 		kr_rplan_pop(rplan, qry);
+	} else if (!tried_tcp && (qry->flags & QUERY_TCP)) {
+		return KNOT_STATE_PRODUCE; /* Requery over TCP */
 	} else { /* Clear query flags for next attempt */
 		qry->flags &= ~(QUERY_CACHED|QUERY_TCP);
 	}
 
 	ITERATE_LAYERS(request, reset);
+
+	/* Do not finish with bogus answer. */
+	if (qry->flags & QUERY_DNSSEC_BOGUS)  {
+		return KNOT_STATE_FAIL;
+	}
+
 	return kr_rplan_empty(&request->rplan) ? KNOT_STATE_DONE : KNOT_STATE_PRODUCE;
 }
 
+/** @internal Spawn subrequest in current zone cut (no minimization or lookup). */
+static struct kr_query *zone_cut_subreq(struct kr_rplan *rplan, struct kr_query *parent,
+                           const knot_dname_t *qname, uint16_t qtype)
+{
+	struct kr_query *next = kr_rplan_push(rplan, parent, qname, parent->sclass, qtype);
+	if (!next) {
+		return NULL;
+	}
+	kr_zonecut_set(&next->zone_cut, parent->zone_cut.name);
+	if (kr_zonecut_copy(&next->zone_cut, &parent->zone_cut) != 0 ||
+	    kr_zonecut_copy_trust(&next->zone_cut, &parent->zone_cut) != 0) {
+		return NULL;
+	}
+	next->flags |= QUERY_NO_MINIMIZE;
+	if (parent->flags & QUERY_DNSSEC_WANT) {
+		next->flags |= QUERY_DNSSEC_WANT;
+	}
+	return next;
+}
+
+/** @internal Check current zone cut status and credibility, spawn subrequests if needed. */
+static int zone_cut_check(struct kr_request *request, struct kr_query *qry, knot_pkt_t *packet)
+{
+	struct kr_rplan *rplan = &request->rplan;
+	map_t *trust_anchors = &request->ctx->trust_anchors;
+	map_t *negative_anchors = &request->ctx->negative_anchors;
+
+	/* The query wasn't resolved from cache,
+	 * now it's the time to look up closest zone cut from cache. */
+	if (qry->flags & QUERY_AWAIT_CUT) {
+		/* Want DNSSEC if it's posible to secure this name (e.g. is covered by any TA) */
+		if (!kr_ta_covers(negative_anchors, qry->zone_cut.name) &&
+		    kr_ta_covers(trust_anchors, qry->zone_cut.name)) {
+			qry->flags |= QUERY_DNSSEC_WANT;
+		}
+		int ret = ns_fetch_cut(qry, request, (qry->flags & QUERY_DNSSEC_WANT));
+		if (ret != 0) {
+			return KNOT_STATE_FAIL;
+		}
+		/* Update minimized QNAME if zone cut changed */
+		if (qry->zone_cut.name[0] != '\0' && !(qry->flags & QUERY_NO_MINIMIZE)) {
+			if (kr_make_query(qry, packet) != 0) {
+				return KNOT_STATE_FAIL;
+			}
+		}
+		qry->flags &= ~QUERY_AWAIT_CUT;
+	}
+	/* Disable DNSSEC if it enters NTA. */
+	if (kr_ta_get(negative_anchors, qry->zone_cut.name)){
+		DEBUG_MSG(">< negative TA, going insecure\n");
+		qry->flags &= ~QUERY_DNSSEC_WANT;
+	}
+	/* Enable DNSSEC if enters a new island of trust. */
+	bool want_secured = (qry->flags & QUERY_DNSSEC_WANT);
+	if (!want_secured && kr_ta_get(trust_anchors, qry->zone_cut.name)) {
+		qry->flags |= QUERY_DNSSEC_WANT;
+		want_secured = true;
+		WITH_DEBUG {
+		char qname_str[KNOT_DNAME_MAXLEN];
+		knot_dname_to_str(qname_str, qry->zone_cut.name, sizeof(qname_str));
+		DEBUG_MSG(">< TA: using '%s'\n", qname_str);
+		}
+	}
+	if (want_secured && !qry->zone_cut.trust_anchor) {
+		knot_rrset_t *ta_rr = kr_ta_get(trust_anchors, qry->zone_cut.name);
+		qry->zone_cut.trust_anchor = knot_rrset_copy(ta_rr, qry->zone_cut.pool);
+	}
+	/* Try to fetch missing DS (from above the cut). */
+	bool refetch_ta = !qry->zone_cut.trust_anchor || !knot_dname_is_equal(qry->zone_cut.name, qry->zone_cut.trust_anchor->owner);
+	if (want_secured && refetch_ta) {
+		/* @todo we could fetch the information from the parent cut, but we don't remember that now */
+		struct kr_query *next = kr_rplan_push(rplan, qry, qry->zone_cut.name, qry->sclass, KNOT_RRTYPE_DS);
+		if (!next) {
+			return KNOT_STATE_FAIL;
+		}
+		next->flags |= QUERY_AWAIT_CUT|QUERY_DNSSEC_WANT;
+		return KNOT_STATE_DONE;
+	}
+	/* Try to fetch missing DNSKEY (either missing or above current cut). */
+	bool refetch_key = !qry->zone_cut.key || !knot_dname_is_equal(qry->zone_cut.name, qry->zone_cut.key->owner);
+	if (want_secured && qry->zone_cut.trust_anchor && refetch_key && qry->stype != KNOT_RRTYPE_DNSKEY) {
+		struct kr_query *next = zone_cut_subreq(rplan, qry, qry->zone_cut.name, KNOT_RRTYPE_DNSKEY);
+		if (!next) {
+			return KNOT_STATE_FAIL;
+		}
+		return KNOT_STATE_DONE;
+	}
+
+	return KNOT_STATE_PRODUCE;	
+}
+
 int kr_resolve_produce(struct kr_request *request, struct sockaddr **dst, int *type, knot_pkt_t *packet)
 {
 	struct kr_rplan *rplan = &request->rplan;
@@ -326,7 +452,7 @@ int kr_resolve_produce(struct kr_request *request, struct sockaddr **dst, int *t
 		ITERATE_LAYERS(request, consume, packet);
 	}
 	switch(request->state) {
-	case KNOT_STATE_FAIL: return request->state; break;
+	case KNOT_STATE_FAIL: return request->state;
 	case KNOT_STATE_CONSUME: break;
 	case KNOT_STATE_DONE:
 	default: /* Current query is done */
@@ -337,21 +463,12 @@ int kr_resolve_produce(struct kr_request *request, struct sockaddr **dst, int *t
 		return kr_rplan_empty(rplan) ? KNOT_STATE_DONE : KNOT_STATE_PRODUCE;
 	}
 
-	/* The query wasn't resolved from cache,
-	 * now it's the time to look up closest zone cut from cache.
-	 */
-	if (qry->flags & QUERY_AWAIT_CUT) {
-		int ret = ns_fetch_cut(qry, request);
-		if (ret != 0) {
-			return KNOT_STATE_FAIL;
-		}
-		qry->flags &= ~QUERY_AWAIT_CUT;
-		/* Update minimized QNAME if zone cut changed */
-		if (qry->zone_cut.name[0] != '\0' && !(qry->flags & QUERY_NO_MINIMIZE)) {
-			if (kr_make_query(qry, packet) != 0) {
-				return KNOT_STATE_FAIL;
-			}
-		}
+	/* Update zone cut, spawn new subrequests. */
+	int state = zone_cut_check(request, qry, packet);
+	switch(state) {
+	case KNOT_STATE_FAIL: return KNOT_STATE_FAIL;
+	case KNOT_STATE_DONE: return KNOT_STATE_PRODUCE;
+	default: break;
 	}
 
 ns_election:
@@ -360,7 +477,7 @@ ns_election:
 	 * elect best address only, otherwise elect a completely new NS.
 	 */
 	if(++ns_election_iter >= KR_ITER_LIMIT) {
-		DEBUG_MSG("=> couldn't agree NS decision, report this\n");
+		DEBUG_MSG("=> couldn't converge NS selection, bail out\n");
 		return KNOT_STATE_FAIL;
 	}
 	if (qry->flags & (QUERY_AWAIT_IPV4|QUERY_AWAIT_IPV6)) {
@@ -386,19 +503,21 @@ ns_election:
 	}
 
 	/* Prepare additional query */
-	int ret = query_finalize(request, packet);
+	int ret = query_finalize(request, qry, packet);
 	if (ret != 0) {
 		return KNOT_STATE_FAIL;
 	}
 
-#ifdef WITH_DEBUG
-	char qname_str[KNOT_DNAME_MAXLEN], zonecut_str[KNOT_DNAME_MAXLEN], ns_str[SOCKADDR_STRLEN];
+	WITH_DEBUG {
+	char qname_str[KNOT_DNAME_MAXLEN], zonecut_str[KNOT_DNAME_MAXLEN], ns_str[SOCKADDR_STRLEN], type_str[16];
 	knot_dname_to_str(qname_str, knot_pkt_qname(packet), sizeof(qname_str));
 	struct sockaddr *addr = &qry->ns.addr.ip;
 	inet_ntop(addr->sa_family, kr_nsrep_inaddr(qry->ns.addr), ns_str, sizeof(ns_str));
 	knot_dname_to_str(zonecut_str, qry->zone_cut.name, sizeof(zonecut_str));
-	DEBUG_MSG("=> querying: '%s' score: %u zone cut: '%s' m12n: '%s'\n", ns_str, qry->ns.score, zonecut_str, qname_str);
-#endif
+	knot_rrtype_to_string(knot_pkt_qtype(packet), type_str, sizeof(type_str));
+	DEBUG_MSG("=> querying: '%s' score: %u zone cut: '%s' m12n: '%s' type: '%s'\n",
+		ns_str, qry->ns.score, zonecut_str, qname_str, type_str);
+	}
 
 	gettimeofday(&qry->timestamp, NULL);
 	*dst = &qry->ns.addr.ip;
@@ -408,12 +527,11 @@ ns_election:
 
 int kr_resolve_finish(struct kr_request *request, int state)
 {
-#ifdef WITH_DEBUG
+#ifndef NDEBUG
 	struct kr_rplan *rplan = &request->rplan;
-	DEBUG_MSG("finished: %d, mempool: %zu B\n", state, (size_t) mp_total_size(request->pool.ctx));
 #endif
 	/* Finalize answer */
-	if (answer_finalize(request->answer) != 0) {
+	if (answer_finalize(request, state) != 0) {
 		state = KNOT_STATE_FAIL;
 	}
 	/* Error during procesing, internal failure */
@@ -423,7 +541,9 @@ int kr_resolve_finish(struct kr_request *request, int state)
 			knot_wire_set_rcode(answer->wire, KNOT_RCODE_SERVFAIL);
 		}
 	}
+
 	ITERATE_LAYERS(request, finish);
+	DEBUG_MSG("finished: %d, mempool: %zu B\n", state, (size_t) mp_total_size(request->pool.ctx));
 	return KNOT_STATE_DONE;
 }
 
diff --git a/lib/resolve.h b/lib/resolve.h
index a6afb2ba65b65415c71573c858c3dcd99675bb1a..deaaa9e18fd09f0dcc87f9266250b500ca874e1e 100644
--- a/lib/resolve.h
+++ b/lib/resolve.h
@@ -20,6 +20,7 @@
 #include <libknot/processing/layer.h>
 #include <libknot/packet/pkt.h>
 
+#include "lib/generic/map.h"
 #include "lib/generic/array.h"
 #include "lib/nsrep.h"
 #include "lib/rplan.h"
@@ -42,8 +43,10 @@
  * 		.alloc = (mm_alloc_t) mp_alloc
  * 	}
  * };
- * kr_resolve_begin(&req, ctx, answer);
- * int state = kr_resolve_query(&req, qname, qclass, qtype);
+ *
+ * // Setup and provide input query
+ * int state = kr_resolve_begin(&req, ctx, final_answer);
+ * state = kr_resolve_consume(&req, query);
  *
  * // Generate answer
  * while (state == KNOT_STATE_PRODUCE) {
@@ -79,15 +82,17 @@ typedef array_t(struct kr_module *) module_array_t;
  *       be shared between threads.
  */
 struct kr_context
-{
-	mm_ctx_t *pool;
+{	
+	uint32_t options;
+	knot_rrset_t *opt_rr;
+	map_t trust_anchors;
+	map_t negative_anchors;
 	struct kr_zonecut root_hints;
 	struct kr_cache cache;
 	kr_nsrep_lru_t *cache_rtt;
 	kr_nsrep_lru_t *cache_rep;
 	module_array_t *modules;
-	knot_rrset_t *opt_rr;
-	uint32_t options;
+	mm_ctx_t *pool;
 };
 
 /**
@@ -126,16 +131,6 @@ struct kr_request {
  */
 int kr_resolve_begin(struct kr_request *request, struct kr_context *ctx, knot_pkt_t *answer);
 
-/**
- * Push new query for resolution to the state.
- * @param  request request state (if already has a question, this will be resolved first)
- * @param  qname
- * @param  qclass
- * @param  qtype
- * @return         PRODUCE|FAIL
- */
-int kr_resolve_query(struct kr_request *request, const knot_dname_t *qname, uint16_t qclass, uint16_t qtype);
-
 /**
  * Consume input packet (may be either first query or answer to query originated from kr_resolve_produce())
  *
diff --git a/lib/rplan.c b/lib/rplan.c
index 8aa640ef677aa1778dc1f0ba9dc392fd37536980..57caf37f0fd6144fa4bc4b1c66ed22af86dbf6e2 100644
--- a/lib/rplan.c
+++ b/lib/rplan.c
@@ -119,16 +119,17 @@ struct kr_query *kr_rplan_push(struct kr_rplan *rplan, struct kr_query *parent,
 	qry->stype = type;
 	qry->flags = rplan->request->options;
 	qry->parent = parent;
+	qry->ns.addr.ip.sa_family = AF_UNSPEC;
 	gettimeofday(&qry->timestamp, NULL);
 	add_tail(&rplan->pending, &qry->node);
 	kr_zonecut_init(&qry->zone_cut, (const uint8_t *)"", rplan->pool);
 
-#ifdef WITH_DEBUG
+	WITH_DEBUG {
 	char name_str[KNOT_DNAME_MAXLEN], type_str[16];
 	knot_dname_to_str(name_str, name, sizeof(name_str));
 	knot_rrtype_to_string(type, type_str, sizeof(type_str));
 	DEBUG_MSG(parent, "plan '%s' type '%s'\n", name_str, type_str);
-#endif
+	}
 	return qry;
 }
 
diff --git a/lib/rplan.h b/lib/rplan.h
index ab4febf41e357b2ff50c265d00c6b4761c3fc569..b6ff573d360e81319e82cc9cc3cf370cac7b658f 100644
--- a/lib/rplan.h
+++ b/lib/rplan.h
@@ -36,9 +36,11 @@
 	X(AWAIT_CUT  , 1 << 6) /**< Query is waiting for zone cut lookup */ \
 	X(SAFEMODE   , 1 << 7) /**< Don't use fancy stuff (EDNS...) */ \
 	X(CACHED     , 1 << 8) /**< Query response is cached. */ \
-	X(EXPIRING   , 1 << 9) /**< Query response is cached, but expiring. */ \
-	X(NO_EXPIRING, 1 << 10) /**< Do not use expiring cached records. */ \
-	X(ALLOW_LOCAL, 1 << 11) /**< Allow queries to local or private address ranges. */
+	X(NO_CACHE   , 1 << 9) /**< Do not use expiring cache for lookup. */ \
+	X(EXPIRING   , 1 << 10) /**< Query response is cached, but expiring. */ \
+	X(ALLOW_LOCAL, 1 << 11) /**< Allow queries to local or private address ranges. */ \
+	X(DNSSEC_WANT , 1 << 12) /**< Want DNSSEC secured answer. */ \
+	X(DNSSEC_BOGUS , 1 << 13) /**< Query response is DNSSEC bogus. */ \
 
 /** Query flags */
 enum kr_query_flag {
diff --git a/lib/rrset_stash.c b/lib/rrset_stash.c
new file mode 100644
index 0000000000000000000000000000000000000000..6cb021b7791d784fec1e478cb1d2c94e05600484
--- /dev/null
+++ b/lib/rrset_stash.c
@@ -0,0 +1,64 @@
+/*  Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <libknot/descriptor.h>
+#include <libknot/dname.h>
+#include <libknot/rrtype/rrsig.h>
+#include <stdio.h>
+
+#include "lib/defines.h"
+#include "lib/rrset_stash.h"
+
+int stash_add(const knot_pkt_t *pkt, map_t *stash, const knot_rrset_t *rr, mm_ctx_t *pool)
+{
+	(void) pkt;
+
+	/* Stash key = {[1] flags, [1-255] owner, [1-5] type, [1] \x00 } */
+	char key[9 + KNOT_DNAME_MAXLEN];
+	uint16_t rrtype = rr->type;
+	KEY_FLAG_SET(key, KEY_FLAG_NO);
+
+	/* Stash RRSIGs in a special cache, flag them and set type to its covering RR.
+	 * This way it the stash won't merge RRSIGs together. */
+	if (rr->type == KNOT_RRTYPE_RRSIG) {
+		rrtype = knot_rrsig_type_covered(&rr->rrs, 0);
+		KEY_FLAG_SET(key, KEY_FLAG_RRSIG);
+	}
+
+	uint8_t *key_buf = (uint8_t *)key + 1;
+	int ret = knot_dname_to_wire(key_buf, rr->owner, KNOT_DNAME_MAXLEN);
+	if (ret <= 0) {
+		return ret;
+	}
+	knot_dname_to_lower(key_buf);
+	/* Must convert to string, as the key must not contain 0x00 */
+	ret = snprintf((char *)key_buf + ret - 1, sizeof(key) - KNOT_DNAME_MAXLEN, "%hu", rrtype);
+	if (ret <= 0 || ret >= KNOT_DNAME_MAXLEN) {
+		return kr_error(EILSEQ);
+	}
+
+	/* Check if already exists */
+	knot_rrset_t *stashed = map_get(stash, key);
+	if (!stashed) {
+		stashed = knot_rrset_copy(rr, pool);
+		if (!stashed) {
+			return kr_error(ENOMEM);
+		}
+		return map_set(stash, key, stashed);
+	}
+	/* Merge rdataset */
+	return knot_rdataset_merge(&stashed->rrs, &rr->rrs, pool);
+}
diff --git a/lib/rrset_stash.h b/lib/rrset_stash.h
new file mode 100644
index 0000000000000000000000000000000000000000..3e6387f1ede68582ae0c0885dcf4e09974bff039
--- /dev/null
+++ b/lib/rrset_stash.h
@@ -0,0 +1,40 @@
+/*  Copyright (C) 2015 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 <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <libknot/internal/mempattern.h>
+#include <libknot/packet/pkt.h>
+#include <libknot/rrset.h>
+
+#include "lib/generic/map.h"
+
+/* Stash key flags */
+#define KEY_FLAG_NO 0x01
+#define KEY_FLAG_RRSIG 0x02
+#define KEY_FLAG_SET(key, flag) key[0] = (flag);
+#define KEY_COVERING_RRSIG(key) (key[0] & KEY_FLAG_RRSIG)
+
+/**
+ * Merges RRSets with matching owner name and type together.
+ * @note RRSIG RRSets are merged according the type covered fields.
+ * @param pkt   Packet which the rset belongs to.
+ * @param stash Holds the merged RRSets.
+ * @param rr    RRSet to be added.
+ * @param pool  Memory pool.
+ * @return      0 or an error
+ */
+int stash_add(const knot_pkt_t *pkt, map_t *stash, const knot_rrset_t *rr, mm_ctx_t *pool);
diff --git a/lib/utils.c b/lib/utils.c
index 5d9373ece3dbad57fc810600eb0df31979abb577..b2779f61c93b11d3b1d89ca5213aaacccc050b42 100644
--- a/lib/utils.c
+++ b/lib/utils.c
@@ -28,6 +28,9 @@
 #include "lib/generic/array.h"
 #include "lib/nsrep.h"
 
+/* Logging & debugging */
+bool _env_debug = false;
+
 /** @internal CSPRNG context */
 static isaac_ctx ISAAC;
 static bool isaac_seeded = false;
diff --git a/lib/utils.h b/lib/utils.h
index c122e580fd14baeaaa3cfffd97fb6b0469d52841..ab1fa264a7947b81f014ff90d6704d22a8e4e042 100644
--- a/lib/utils.h
+++ b/lib/utils.h
@@ -34,8 +34,27 @@ extern void _cleanup_fclose(FILE **p);
 /* @endcond */
 
 /*
- * Defines.
+ * Logging and debugging.
  */
+#define log_info(fmt, ...) printf((fmt), ## __VA_ARGS__)
+#define log_error(fmt, ...) fprintf(stderr, (fmt), ## __VA_ARGS__)
+#ifndef NDEBUG
+extern bool _env_debug; /* @internal cond variable */
+/* Toggle debug messages */
+#define log_debug_enable(x) _env_debug = (x)
+#define log_debug_status() _env_debug
+/* Message logging */
+#define log_debug(fmt, ...) do { \
+    if (_env_debug) { printf((fmt), ## __VA_ARGS__); fflush(stdout); } \
+    } while (0)
+/* Debug block */
+#define WITH_DEBUG if(__builtin_expect(_env_debug, 0))
+#else
+#define log_debug_status() false
+#define log_debug_enable(x)
+#define log_debug(fmt, ...)
+#define WITH_DEBUG if(0)
+#endif
 
 /** Return time difference in miliseconds.
   * @note based on the _BSD_SOURCE timersub() macro */
diff --git a/lib/zonecut.c b/lib/zonecut.c
index 73b49e0b5b60f35633ab6028996e562e4cd51675..0366210e27c74b3a99b6409b77d0ea4c8106ee6a 100644
--- a/lib/zonecut.c
+++ b/lib/zonecut.c
@@ -70,6 +70,8 @@ int kr_zonecut_init(struct kr_zonecut *cut, const knot_dname_t *name, mm_ctx_t *
 
 	cut->name = knot_dname_copy(name, pool);
 	cut->pool = pool;
+	cut->key  = NULL;
+	cut->trust_anchor = NULL;
 	cut->nsset = map_make();
 	cut->nsset.malloc = (map_alloc_f) mm_alloc;
 	cut->nsset.free = (map_free_f) mm_free;
@@ -93,6 +95,8 @@ void kr_zonecut_deinit(struct kr_zonecut *cut)
 	mm_free(cut->pool, cut->name);
 	map_walk(&cut->nsset, free_addr_set, cut->pool);
 	map_clear(&cut->nsset);
+	knot_rrset_free(&cut->key, cut->pool);
+	knot_rrset_free(&cut->trust_anchor, cut->pool);
 }
 
 void kr_zonecut_set(struct kr_zonecut *cut, const knot_dname_t *name)
@@ -100,8 +104,13 @@ void kr_zonecut_set(struct kr_zonecut *cut, const knot_dname_t *name)
 	if (!cut || !name) {
 		return;
 	}
+	knot_rrset_t *key, *ta;
+	key = cut->key; cut->key = NULL;
+	ta = cut->trust_anchor; cut->trust_anchor = NULL;
 	kr_zonecut_deinit(cut);
 	kr_zonecut_init(cut, name, cut->pool);
+	cut->key = key;
+	cut->trust_anchor = ta;
 }
 
 static int copy_addr_set(const char *k, void *v, void *baton)
@@ -143,7 +152,33 @@ int kr_zonecut_copy(struct kr_zonecut *dst, const struct kr_zonecut *src)
 	return map_walk((map_t *)&src->nsset, copy_addr_set, dst);
 }
 
+int kr_zonecut_copy_trust(struct kr_zonecut *dst, const struct kr_zonecut *src)
+{
+	knot_rrset_t *key_copy = NULL;
+	knot_rrset_t *ta_copy = NULL;
+
+	if (src->key) {
+		key_copy = knot_rrset_copy(src->key, dst->pool);
+		if (!key_copy) {
+			return kr_error(ENOMEM);
+		}
+	}
+
+	if (src->trust_anchor) {
+		ta_copy = knot_rrset_copy(src->trust_anchor, dst->pool);
+		if (!ta_copy) {
+			knot_rrset_free(&key_copy, dst->pool);
+			return kr_error(ENOMEM);
+		}
+	}
 
+	knot_rrset_free(&dst->key, dst->pool);
+	dst->key = key_copy;
+	knot_rrset_free(&dst->trust_anchor, dst->pool);
+	dst->trust_anchor = ta_copy;
+
+	return kr_ok();
+}
 
 int kr_zonecut_add(struct kr_zonecut *cut, const knot_dname_t *ns, const knot_rdata_t *rdata)
 {
@@ -225,6 +260,9 @@ int kr_zonecut_set_sbelt(struct kr_context *ctx, struct kr_zonecut *cut)
 	/* Copy root hints from resolution context. */
 	if (ctx->root_hints.nsset.root) {
 		int ret = kr_zonecut_copy(cut, &ctx->root_hints);
+		if (ret == 0) {
+			ret = kr_zonecut_copy_trust(cut, &ctx->root_hints);
+		}
 		if (ret == 0) {
 			return ret;
 		}
@@ -241,6 +279,8 @@ int kr_zonecut_set_sbelt(struct kr_context *ctx, struct kr_zonecut *cut)
 		}
 	}
 
+	/* Set trust anchor. */
+	knot_rrset_free(&cut->trust_anchor, cut->pool);
 	return kr_ok();
 }
 
@@ -297,8 +337,57 @@ static int fetch_ns(struct kr_context *ctx, struct kr_zonecut *cut, const knot_d
 	return kr_ok();
 }
 
+/**
+ * Fetch RRSet of given type.
+ */
+static int fetch_rrset(knot_rrset_t **rr, const knot_dname_t *owner, uint16_t type,
+                       struct kr_cache_txn *txn, mm_ctx_t *pool, uint32_t timestamp)
+{
+	if (!rr) {
+		return kr_error(ENOENT);
+	}
+
+	uint32_t drift = timestamp;
+	knot_rrset_t cached_rr;
+
+	knot_rrset_init(&cached_rr, (knot_dname_t *)owner, type, KNOT_CLASS_IN);
+	int ret = kr_cache_peek_rr(txn, &cached_rr, &drift);
+	if (ret != 0) {
+		return ret;
+	}
+
+	knot_rrset_free(rr, pool);
+	*rr = mm_alloc(pool, sizeof(knot_rrset_t));
+	if (*rr == NULL) {
+		return kr_error(ENOMEM);
+	}
+
+	ret = kr_cache_materialize(*rr, &cached_rr, drift, pool);
+	if (ret != 0) {
+		knot_rrset_free(rr, pool);
+		return ret;
+	}
+
+	return kr_ok();
+}
+
+/**
+ * Fetch trust anchors for zone cut.
+ * @note The trust anchor can theoretically be a DNSKEY but for now lets use only DS.
+ */
+static int fetch_ta(struct kr_zonecut *cut, const knot_dname_t *name, struct kr_cache_txn *txn, uint32_t timestamp)
+{
+	return fetch_rrset(&cut->trust_anchor, name, KNOT_RRTYPE_DS, txn, cut->pool, timestamp);
+}
+
+/** Fetch DNSKEY for zone cut. */
+static int fetch_dnskey(struct kr_zonecut *cut, const knot_dname_t *name, struct kr_cache_txn *txn, uint32_t timestamp)
+{
+	return fetch_rrset(&cut->key, name, KNOT_RRTYPE_DNSKEY, txn, cut->pool, timestamp);
+}
+
 int kr_zonecut_find_cached(struct kr_context *ctx, struct kr_zonecut *cut, const knot_dname_t *name,
-                           struct kr_cache_txn *txn, uint32_t timestamp)
+                           struct kr_cache_txn *txn, uint32_t timestamp, bool secured)
 {
 	if (!ctx || !cut || !name) {
 		return kr_error(EINVAL);
@@ -306,7 +395,11 @@ int kr_zonecut_find_cached(struct kr_context *ctx, struct kr_zonecut *cut, const
 
 	/* Start at QNAME parent. */
 	while (txn && name) {
-		if (fetch_ns(ctx, cut, name, txn, timestamp) == 0) {
+		bool has_ta = !secured || fetch_ta(cut, name, txn, timestamp) == 0;
+		if (secured) {
+			fetch_dnskey(cut, name, txn, timestamp);
+		}
+		if (has_ta && fetch_ns(ctx, cut, name, txn, timestamp) == 0) {
 			update_cut_name(cut, name);
 			return kr_ok();
 		}
diff --git a/lib/zonecut.h b/lib/zonecut.h
index c9be6b327666526c4bc130598179908b23583428..a2662e5bb49a0f51dec7045101bb33404171687f 100644
--- a/lib/zonecut.h
+++ b/lib/zonecut.h
@@ -28,8 +28,10 @@ struct kr_context;
 */
 struct kr_zonecut {
 	knot_dname_t *name; /**< Zone cut name. */
-	mm_ctx_t *pool;     /**< Memory pool. */
 	map_t nsset;        /**< Map of nameserver => address_set. */
+	knot_rrset_t* key;  /**< Zone cut DNSKEY. */
+	knot_rrset_t* trust_anchor; /**< Current trust anchor. */
+	mm_ctx_t *pool;     /**< Memory pool. */
 };
 
 /**
@@ -49,20 +51,28 @@ void kr_zonecut_deinit(struct kr_zonecut *cut);
 
 /**
  * Reset zone cut to given name and clear address list.
- * @note This clears the address list even if the name doesn't change.
+ * @note This clears the address list even if the name doesn't change. TA and DNSKEY don't change.
  * @param cut  zone cut to be set
  * @param name new zone cut name
  */
 void kr_zonecut_set(struct kr_zonecut *cut, const knot_dname_t *name);
 
 /** 
- * Copy zone cut, including all data.
+ * Copy zone cut, including all data. Does not copy keys and trust anchor.
  * @param dst destination zone cut
  * @param src source zone cut
  * @return 0 or an error code
  */
 int kr_zonecut_copy(struct kr_zonecut *dst, const struct kr_zonecut *src);
 
+/**
+ * Copy zone trust anchor and keys.
+ * @param dst destination zone cut
+ * @param src source zone cut
+ * @return 0 or an error code
+ */
+int kr_zonecut_copy_trust(struct kr_zonecut *dst, const struct kr_zonecut *src);
+
 /**
  * Add address record to the zone cut.
  *
@@ -114,7 +124,8 @@ int kr_zonecut_set_sbelt(struct kr_context *ctx, struct kr_zonecut *cut);
  * @param name      QNAME to start finding zone cut for
  * @param txn       cache transaction (read)
  * @param timestamp transaction timestamp
+ * @param secured   search nearest containing a DNSKEY
  * @return 0 or error code
  */
 int kr_zonecut_find_cached(struct kr_context *ctx, struct kr_zonecut *cut, const knot_dname_t *name,
-                           struct kr_cache_txn *txn, uint32_t timestamp);
+                           struct kr_cache_txn *txn, uint32_t timestamp, bool secured);
diff --git a/modules/README.rst b/modules/README.rst
index d4991304b95c5d5ced656e589d2ca7b8810b73cd..47b1435b992fc46ceaed6601f9ebdc3983273cb0 100644
--- a/modules/README.rst
+++ b/modules/README.rst
@@ -1,16 +1,15 @@
-****************************
-Knot DNS Resolver extensions
-****************************
+.. _modules-api:
 
-Writing extensions
-==================
+*********************
+Modules API reference
+*********************
 
 .. contents::
-   :depth: 2
+   :depth: 1
    :local:
 
 Supported languages
--------------------
+===================
 
 Currently modules written in C and Lua are supported.
 There is also a rudimentary support for writing modules in Go |---|
@@ -19,7 +18,7 @@ There is also a rudimentary support for writing modules in Go |---|
 (3) no coroutines and no garbage collecting thread, as the Go code is called from C threads.
 
 The anatomy of an extension
----------------------------
+===========================
 
 A module is a shared object or script defining specific functions, here's an overview.
 
@@ -45,61 +44,8 @@ This doesn't apply for Go, as it for now always implements `main` and requires c
    
    If the module exports a layer implementation, it is automatically discovered by :c:func:`kr_resolver` on resolution init and plugged in. The order in which the modules are registered corresponds to the call order of layers.
 
-Writing a module in C
----------------------
-
-As almost all the functions are optional, the minimal module looks like this:
-
-.. code-block:: c
-
-	#include "lib/module.h"
-	/* Convenience macro to declare module API. */
-	KR_MODULE_EXPORT(mymodule);
-
-
-Let's define an observer thread for the module as well. It's going to be stub for the sake of brevity,
-but you can for example create a condition, and notify the thread from query processing by declaring
-module layer (see the :ref:`Writing layers <lib-layers>`).
-
-.. code-block:: c
-
-	static void* observe(void *arg)
-	{
-		/* ... do some observing ... */
-	}
-
-	int mymodule_init(struct kr_module *module)
-	{
-		/* Create a thread and start it in the background. */
-		pthread_t thr_id;
-		int ret = pthread_create(&thr_id, NULL, &observe, NULL);
-		if (ret != 0) {
-			return kr_error(errno);
-		}
-
-		/* Keep it in the thread */
-		module->data = thr_id;
-		return kr_ok();
-	}
-
-	int mymodule_deinit(struct kr_module *module)
-	{
-		/* ... signalize cancellation ... */
-		void *res = NULL;
-		pthread_t thr_id = (pthread_t) module->data;
-		int ret = pthread_join(thr_id, res);
-		if (ret != 0) {
-			return kr_error(errno);
-		}
-
-		return kr_ok();
-	}
-
-This example shows how a module can run in the background, this enables you to, for example, observe
-and publish data about query resolution.
-
 Writing a module in Lua
------------------------
+=======================
 
 The probably most convenient way of writing modules is Lua since you can use already installed modules
 from system and have first-class access to the scripting engine. You can also tap to all the events, that
@@ -179,8 +125,61 @@ Since the modules are like any other Lua modules, you can interact with them thr
 
 .. tip:: The module can be placed anywhere in the Lua search path, in the working directory or in the MODULESDIR.
 
+Writing a module in C
+=====================
+
+As almost all the functions are optional, the minimal module looks like this:
+
+.. code-block:: c
+
+	#include "lib/module.h"
+	/* Convenience macro to declare module API. */
+	KR_MODULE_EXPORT(mymodule);
+
+
+Let's define an observer thread for the module as well. It's going to be stub for the sake of brevity,
+but you can for example create a condition, and notify the thread from query processing by declaring
+module layer (see the :ref:`Writing layers <lib-layers>`).
+
+.. code-block:: c
+
+	static void* observe(void *arg)
+	{
+		/* ... do some observing ... */
+	}
+
+	int mymodule_init(struct kr_module *module)
+	{
+		/* Create a thread and start it in the background. */
+		pthread_t thr_id;
+		int ret = pthread_create(&thr_id, NULL, &observe, NULL);
+		if (ret != 0) {
+			return kr_error(errno);
+		}
+
+		/* Keep it in the thread */
+		module->data = thr_id;
+		return kr_ok();
+	}
+
+	int mymodule_deinit(struct kr_module *module)
+	{
+		/* ... signalize cancellation ... */
+		void *res = NULL;
+		pthread_t thr_id = (pthread_t) module->data;
+		int ret = pthread_join(thr_id, res);
+		if (ret != 0) {
+			return kr_error(errno);
+		}
+
+		return kr_ok();
+	}
+
+This example shows how a module can run in the background, this enables you to, for example, observe
+and publish data about query resolution.
+
 Writing a module in Go
-----------------------
+======================
 
 .. note:: At the moment only a limited subset of Go is supported. The reason is that the Go functions must run inside the goroutines, and *presume* the garbage collector and scheduler are running in the background. `GCCGO`_ compiler can build dynamic libraries, and also allow us to bootstrap basic Go runtime, including a trampoline to call Go functions. The problem with the ``layer()`` and callbacks is that they're called from C threads, that Go runtime has no knowledge of. Thus neither garbage collection or spawning routines can work. The solution could be to register C threads to Go runtime, or have each module to run inside its world loop and use IPC instead of callbacks |---| alas neither is implemented at the moment, but may be in the future.
 
@@ -253,14 +252,14 @@ Now we can add the implementations for the ``Begin`` and ``Finish`` functions, a
 See the CGO_ for more information about type conversions and interoperability between the C/Go.
 
 Configuring modules
--------------------
+===================
 
 There is a callback ``X_config()`` that you can implement, see hints module.
 
 .. _mod-properties:
 
 Exposing C/Go module properties
--------------------------------
+===============================
 
 A module can offer NULL-terminated list of *properties*, each property is essentially a callable with free-form JSON input/output.
 JSON was chosen as an interchangeable format that doesn't require any schema beforehand, so you can do two things - query the module properties
@@ -309,7 +308,8 @@ Here's an example how a module can expose its property:
 
 	KR_MODULE_EXPORT(cache)
 
-Once you load the module, you can call the module property from the interactive console:
+Once you load the module, you can call the module property from the interactive console.
+*Note* |---| the JSON output will be transparently converted to Lua tables.
 
 .. code-block:: bash
 
@@ -318,7 +318,7 @@ Once you load the module, you can call the module property from the interactive
 	[system] started in interactive mode, type 'help()'
 	> modules.load('cached')
 	> cached.get_size()
-	{ "size": 53 }
+	[size] => 53
 
 *Note* |---| this relies on function pointers, so the same ``static inline`` trick as for the ``Layer()`` is required for C/Go.
 
@@ -328,9 +328,6 @@ Special properties
 If the module declares properties ``get`` or ``set``, they can be used in the Lua interpreter as
 regular tables.
 
-.. warning:: This is not yet completely implemented, as the module I/O format may change to map_t a/o
-             embedded JSON tokenizer.
-
 .. _`not present in Go`: http://blog.golang.org/gos-declaration-syntax
 .. _CGO: http://golang.org/cmd/cgo/
 .. _GCCGO: https://golang.org/doc/install/gccgo
diff --git a/modules/policy/policy.lua b/modules/policy/policy.lua
index 84276cb6ee8a2bf250beaf291767baea28ced431..4a8b28c390b60ae31cc1b2be76738cd8d9c35b86 100644
--- a/modules/policy/policy.lua
+++ b/modules/policy/policy.lua
@@ -148,9 +148,7 @@ end
 -- Convert list of string names to domain names
 function policy.to_domains(names)
 	for i, v in ipairs(names) do
-		names[i] = v:gsub('([^.]*%.)', function (x)
-			return string.format('%s%s', string.char(x:len()-1), x:sub(1,-2))
-		end)
+		names[i] = kres.str2dname(v)
 	end
 end
 
diff --git a/modules/predict/predict.lua b/modules/predict/predict.lua
index bc80d11c8b9a77a0f97846beb1c1828a4feb6d35..d462e0ac28b76fb5675e106280fdf56da9cce491 100644
--- a/modules/predict/predict.lua
+++ b/modules/predict/predict.lua
@@ -30,7 +30,7 @@ end
 function predict.drain(ev)
 	local deleted = 0
 	for key, val in pairs(predict.queue) do
-		worker.resolve(string.sub(key, 2), string.byte(key), 1, kres.query.NO_EXPIRING)
+		worker.resolve(string.sub(key, 2), string.byte(key), 1, kres.query.NO_CACHE)
 		predict.queue[key] = nil
 		deleted = deleted + 1
 		if deleted >= predict.batch then
diff --git a/scripts/bootstrap-depends.sh b/scripts/bootstrap-depends.sh
index 21b811fee4006d7ff0b0d7e44043643e7eda3ad9..85b69115c3d542812187aed234d8ba55f1d46191 100755
--- a/scripts/bootstrap-depends.sh
+++ b/scripts/bootstrap-depends.sh
@@ -5,7 +5,7 @@ CMOCKA_TAG="cmocka-0.4.1"
 CMOCKA_URL="git://git.cryptomilk.org/projects/cmocka.git"
 LIBUV_TAG="v1.x"
 LIBUV_URL="https://github.com/libuv/libuv.git"
-KNOT_TAG="edc125bc"
+KNOT_TAG="33c23ed3f54316b0642fc58a4455ad4d4424d121"
 KNOT_URL="https://github.com/CZ-NIC/knot.git"
 GMP_TAG="6.0.0"
 GMP_URL="https://gmplib.org/download/gmp/gmp-${GMP_TAG}.tar.xz"
diff --git a/tests/integration.mk b/tests/integration.mk
index 7c9307b6322cacb286e323343775e7d65f176ed4..8f879585975e4b3e4f1fe9990ef3dd92a94696ff 100644
--- a/tests/integration.mk
+++ b/tests/integration.mk
@@ -27,6 +27,6 @@ $(libfaketime): $(libfaketime_DIR)/Makefile
 	@CFLAGS="" $(MAKE) -C $(libfaketime_DIR)
 
 check-integration: $(libfaketime)
-	$(preload_LIBS) $(preload_syms) tests/test_integration.py $(TESTS) $(abspath daemon/kresd) ./kresd.j2 config
+	@$(preload_LIBS) $(preload_syms) python tests/test_integration.py $(TESTS) $(abspath daemon/kresd) ./kresd.j2 config
 
 .PHONY: check-integration
diff --git a/tests/kresd.j2 b/tests/kresd.j2
index 14a637f785d76b9c52f7f89e8e92b90a9dc54b3f..2f787b5cf5da8a90ee5170d3391c6434430530f0 100644
--- a/tests/kresd.j2
+++ b/tests/kresd.j2
@@ -1,9 +1,11 @@
-net.listen('{{SELF_ADDR}}',53)
-cache.size = 1*MB
+net = { '{{SELF_ADDR}}' }
 modules = {'stats', 'policy', 'hints'}
+cache.size = 1*MB
 hints.root({['k.root-servers.net'] = '{{ROOT_ADDR}}'})
 option('NO_MINIMIZE', {{NO_MINIMIZE}})
 option('ALLOW_LOCAL', true)
+trust_anchors.add('{{TRUST_ANCHOR}}')
+verbose(true)
 
 -- Self-checks on globals
 assert(help() ~= nil)
diff --git a/tests/pydnstest/scenario.py b/tests/pydnstest/scenario.py
index 6768907839daba6cf6470cebcce0cdc35d4d8d88..3ccf310d9be0c3d02f4c4368b88a2592977b1324 100644
--- a/tests/pydnstest/scenario.py
+++ b/tests/pydnstest/scenario.py
@@ -9,6 +9,11 @@ import itertools
 import time
 from datetime import datetime
 
+def dprint(msg):
+    """ Verbose logging (if enabled). """
+    if 'VERBOSE' in os.environ:
+        print(msg)
+
 class Entry:
     """
     Data entry represents scripted message and extra metadata, notably match criteria and reply adjustments.
@@ -25,7 +30,7 @@ class Entry:
         self.adjust_fields = ['copy_id']
         self.origin = '.'
         self.message = dns.message.Message()
-        self.message.use_edns(edns = 0)
+        self.message.use_edns(edns = 0, payload = 4096)
         self.sections = []
         self.is_raw_data_entry = False
         self.raw_data_pending = False
@@ -86,7 +91,7 @@ class Entry:
         if raw_value is not None:
             got = binascii.hexlify(raw_value)
         if expected != got:
-            print "expected '",expected,"', got '",got,"'"
+            print("expected '",expected,"', got '",got,"'")
             raise Exception("comparsion failed")
 
     def set_match(self, fields):
@@ -99,7 +104,9 @@ class Entry:
         answer.use_edns(query.edns, query.ednsflags)
         if 'copy_id' in self.adjust_fields:
             answer.id = query.id
-            answer.question[0].name = query.question[0].name
+            # Copy letter-case if the template has QD
+            if len(answer.question) > 0:
+                answer.question[0].name = query.question[0].name
         if 'copy_query' in self.adjust_fields:
             answer.question = query.question
         return answer
@@ -273,7 +280,9 @@ class Step:
 
     def play(self, ctx, peeraddr):
         """ Play one step from a scenario. """
+        dprint('[ STEP %03d ] %s' % (self.id, self.type))
         if self.type == 'QUERY':
+            dprint(self.data[0].message.to_text())
             return self.__query(ctx, peeraddr)
         elif self.type == 'CHECK_OUT_QUERY':
              pass # Ignore
@@ -292,14 +301,14 @@ class Step:
             raise Exception("response definition required")
         expected = self.data[0]
         if expected.is_raw_data_entry is True:
-            expected.cmp_raw(ctx.last_raw_answer);
+            dprint(ctx.last_raw_answer.to_text())
+            expected.cmp_raw(ctx.last_raw_answer)
         else:
-            if ctx.last_answer is None :
+            if ctx.last_answer is None:
                 raise Exception("no answer from preceding query")
+            dprint(ctx.last_answer.to_text())
             expected.match(ctx.last_answer)
 
-
-
     def __query(self, ctx, peeraddr):
         """ Resolve a query. """
         if len(self.data) == 0:
@@ -307,9 +316,8 @@ class Step:
         if self.data[0].is_raw_data_entry is True:
             data_to_wire = self.data[0].raw_data
         else:
-            msg = self.data[0].message
-            msg.use_edns(edns = 1)
-            data_to_wire = msg.to_wire()
+            # Don't use a message copy as the EDNS data portion is not copied.
+            data_to_wire = self.data[0].message.to_wire()
         # Send query to client and wait for response
         while True:
             try:
@@ -334,15 +342,19 @@ class Step:
 
     def __time_passes(self, ctx):
         """ Modify system time. """
-        ctx.time += int(self.args[1])
+        time_file = open(os.environ["FAKETIME_TIMESTAMP_FILE"], 'r')
+        line = time_file.readline().strip()
+        time_file.close()
+        t = time.mktime(datetime.strptime(line, '%Y-%m-%d %H:%M:%S').timetuple())
+        t += int(self.args[1])
         time_file = open(os.environ["FAKETIME_TIMESTAMP_FILE"], 'w')
-        time_file.write(datetime.fromtimestamp(ctx.time).strftime('%Y-%m-%d %H:%M:%S') + "\n")
+        time_file.write(datetime.fromtimestamp(t).strftime('%Y-%m-%d %H:%M:%S') + "\n")
+        time_file.close()
 
 class Scenario:
     def __init__(self, info):
         """ Initialize scenario with description. """
         self.info = info
-        self.time = 0
         self.ranges = []
         self.steps = []
         self.current_step = None
@@ -382,7 +394,7 @@ class Scenario:
     def play(self, saddr, paddr):
         """ Play given scenario. """
         self.child_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-        self.child_sock.settimeout(1000)
+        self.child_sock.settimeout(2)
         self.child_sock.connect((paddr, 53))
 
         step = None
@@ -395,5 +407,5 @@ class Scenario:
         except Exception as e:
             raise Exception('step #%d %s' % (step.id, str(e)))
         finally:
-        	self.child_sock.close()
-        	self.child_sock = None
+            self.child_sock.close()
+            self.child_sock = None
diff --git a/tests/pydnstest/test.py b/tests/pydnstest/test.py
index 9d47306926c8ccac8c232f9ad2195b65488a09dd..54b94bec7861cd32a8037f218e2950c903d2ee7a 100644
--- a/tests/pydnstest/test.py
+++ b/tests/pydnstest/test.py
@@ -1,4 +1,6 @@
 #!/usr/bin/env python
+import os
+import traceback
 
 class Test:
     """ Small library to imitate CMocka output. """
@@ -26,6 +28,8 @@ class Test:
                 print('[       OK ] %s' % name)
             except Exception as e:
                 print('[     FAIL ] %s (%s)' % (name, str(e)))
+                if 'VERBOSE' in os.environ:
+                    print(traceback.format_exc())
 
         # Clear test set
         self.tests = []
diff --git a/tests/pydnstest/testserver.py b/tests/pydnstest/testserver.py
index ab131e84b0a4402b4fd59ff5e4cbf3728afca149..f96c5bb39de36f918ebf45b4d768d80ec9fa0cc8 100644
--- a/tests/pydnstest/testserver.py
+++ b/tests/pydnstest/testserver.py
@@ -102,7 +102,11 @@ class TestServer:
                  addr_local = am.local
                  new_entry = False
         if addr_local is None:
-            addr_local = get_local_addr_str(family, iface)
+            # Do not remap addresses already in local range
+            if addr.startswith('127.0.0.') or addr.startswith('::'):
+                addr_local = addr
+            else:
+                addr_local = get_local_addr_str(family, iface)
             am = AddrMapInfo(family,addr_local,addr_external)
             self.addr_map.append(am)
             new_entry = True
@@ -228,7 +232,7 @@ class TestServer:
                 address = "::1"
         else:
             raise Exception("[start_srv] unsupported socket type {sock_type}".format(sock_type=type))
-	if port == 0 or port is None:
+        if port == 0 or port is None:
             port = 53
 
         if (self.thread is None):
diff --git a/tests/test.h b/tests/test.h
index 213f03016b542d99d8ed9c996d85c020612fbe4d..ceb43e0f95784eb06415b7c7113c171605f7cb59 100644
--- a/tests/test.h
+++ b/tests/test.h
@@ -27,6 +27,9 @@
 #include <unistd.h>
 #include <cmocka.h>
 
+/* Silence clang/GCC warnings when using cmocka 1.0 */
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+
 #include "lib/defines.h"
 #include <libknot/internal/mempattern.h>
 #include <libknot/descriptor.h>
diff --git a/tests/test_integration.py b/tests/test_integration.py
index dbd2989c08c64cad0e5c36bc4038030e908d2b18..f641ba3f7440fa522bec01dbfeeb28d3470f17a9 100755
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -167,6 +167,11 @@ def find_objects(path):
             result.append(path)
     return result
 
+def write_timestamp_file(path, tst):
+    time_file = open(path, 'w')
+    time_file.write(datetime.fromtimestamp(tst).strftime('%Y-%m-%d %H:%M:%S'))
+    time_file.close()
+
 def setup_env(child_env, config, config_name, j2template):
     """ Set up test environment and config """
     # Clear test directory
@@ -174,19 +179,30 @@ def setup_env(child_env, config, config_name, j2template):
     # Set up libfaketime
     os.environ["FAKETIME_NO_CACHE"] = "1"
     os.environ["FAKETIME_TIMESTAMP_FILE"] = '%s/.time' % TMPDIR
-    time_file = open(os.environ["FAKETIME_TIMESTAMP_FILE"], 'w')
-    time_file.write(datetime.fromtimestamp(0).strftime('%Y-%m-%d %H:%M:%S'))
-    time_file.close()
+    child_env["FAKETIME_NO_CACHE"] = "1"
+    child_env["FAKETIME_TIMESTAMP_FILE"] = '%s/.time' % TMPDIR
+    write_timestamp_file(child_env["FAKETIME_TIMESTAMP_FILE"], 0)
     # Set up child process env() 
     child_env["SOCKET_WRAPPER_DEFAULT_IFACE"] = "%i" % CHILD_IFACE
     child_env["SOCKET_WRAPPER_DIR"] = TMPDIR
     no_minimize = "true"
+    trust_anchor_str = ""
+    stub_addr = ""
     for k,v in config:
         # Enable selectively for some tests
         if k == 'query-minimization' and str2bool(v):
             no_minimize = "false"
-            break
-    selfaddr = testserver.get_local_addr_str(socket.AF_INET, DEFAULT_IFACE)
+        elif k == 'trust-anchor':
+            trust_anchor_str = v.strip('"\'')
+        elif k == 'val-override-date':
+            override_date_str = v.strip('"\'')
+            write_timestamp_file(child_env["FAKETIME_TIMESTAMP_FILE"], int(override_date_str))
+        elif k == 'stub-addr':
+            stub_addr = v.strip('"\'')
+    if stub_addr.startswith('127.0.0.') or stub_addr.startswith('::'):
+        selfaddr = stub_addr
+    else:
+        selfaddr = testserver.get_local_addr_str(socket.AF_INET, DEFAULT_IFACE)
     childaddr = testserver.get_local_addr_str(socket.AF_INET, CHILD_IFACE)
     # Prebind to sockets to create necessary files
     # @TODO: this is probably a workaround for socket_wrapper bug
@@ -201,6 +217,7 @@ def setup_env(child_env, config, config_name, j2template):
         "ROOT_ADDR" : selfaddr,
         "SELF_ADDR" : childaddr,
         "NO_MINIMIZE" : no_minimize,
+        "TRUST_ANCHOR" : trust_anchor_str,
         "WORKING_DIR" : TMPDIR,
     }
     cfg_rendered = j2template.render(j2template_ctx)
@@ -235,6 +252,7 @@ def play_object(path, binary_name, config_name, j2template, binary_additional_pa
     # Wait until the server accepts TCP clients
     sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     while True:
+    	time.sleep(0.1)
         if daemon_proc.poll() != None:
             print(open('%s/server.log' % TMPDIR).read())
             raise Exception('process died "%s", logs in "%s"' % (os.path.basename(binary_name), TMPDIR))
@@ -251,6 +269,8 @@ def play_object(path, binary_name, config_name, j2template, binary_additional_pa
         server.stop()
         daemon_proc.terminate()
         daemon_proc.wait()
+        if 'VERBOSE' in os.environ:
+            print('[ LOG      ]\n%s' % open('%s/server.log' % TMPDIR).read())
     # Do not clear files if the server crashed (for analysis)
     del_files(TMPDIR)
 
@@ -269,6 +289,7 @@ if __name__ == '__main__':
         print "\t<additional> - additional parameters for <binary>"
         sys.exit(0)
 
+    test_platform()
     path_to_scenario = ""
     binary_name = ""
     template_name = ""
@@ -288,15 +309,10 @@ if __name__ == '__main__':
     j2template_env = jinja2.Environment(loader=j2template_loader)
     j2template = j2template_env.get_template(template_name)
 
-    # Self-tests first
+    # Scan for scenarios
     test = test.Test()
-    test.add('integration/platform', test_platform)
-    if test.run() != 0:
-        sys.exit(1)
-    else:
-        # Scan for scenarios
-        for arg in [path_to_scenario]:
-            objects = find_objects(arg)
-            for path in objects:
-                test.add(path, play_object, path, binary_name, config_name, j2template, binary_additional_pars)
-        sys.exit(test.run())
+    for arg in [path_to_scenario]:
+        objects = find_objects(arg)
+        for path in objects:
+            test.add(path, play_object, path, binary_name, config_name, j2template, binary_additional_pars)
+    sys.exit(test.run())
diff --git a/tests/test_zonecut.c b/tests/test_zonecut.c
index 7158788429a6cc40a127286a23a499dda08153c8..e090b6c447d13d2acf11ca80f53c76b793e19e9c 100644
--- a/tests/test_zonecut.c
+++ b/tests/test_zonecut.c
@@ -32,7 +32,7 @@ static void test_zonecut_params(void **state)
 	assert_null((void *)kr_zonecut_find(NULL, NULL));
 	assert_null((void *)kr_zonecut_find(&cut, NULL));
 	assert_int_not_equal(kr_zonecut_set_sbelt(NULL, NULL), 0);
-	assert_int_not_equal(kr_zonecut_find_cached(NULL, NULL, NULL, NULL, 0), 0);
+	assert_int_not_equal(kr_zonecut_find_cached(NULL, NULL, NULL, NULL, 0, 0), 0);
 }
 
 static void test_zonecut_copy(void **state)
diff --git a/tests/testdata/iter_minim_a.rpl b/tests/testdata/iter_minim_a.rpl
index 612c716dc0ea824018609beeb7eac9830b9016cf..f97d445e4f8100a4b99f00fd2ef1ab1d1d252b1e 100644
--- a/tests/testdata/iter_minim_a.rpl
+++ b/tests/testdata/iter_minim_a.rpl
@@ -2,14 +2,14 @@
 	target-fetch-policy: "0 0 0 0 0"
 	query-minimization: on
 	name: "."
-	stub-addr: 193.0.14.129 	# K.ROOT-SERVERS.NET.
+	stub-addr: 127.0.0.10 	# K.ROOT-SERVERS.NET.
 CONFIG_END
 
 SCENARIO_BEGIN Test basic query minimization www.example.com.
 
 ; K.ROOT-SERVERS.NET.
 RANGE_BEGIN 0 100
-	ADDRESS 193.0.14.129 
+	ADDRESS 127.0.0.10
 ENTRY_BEGIN
 MATCH opcode qtype qname
 ADJUST copy_id
@@ -19,7 +19,7 @@ SECTION QUESTION
 SECTION ANSWER
 . IN NS	K.ROOT-SERVERS.NET.
 SECTION ADDITIONAL
-K.ROOT-SERVERS.NET.	IN	A	193.0.14.129
+K.ROOT-SERVERS.NET.	IN	A	127.0.0.10
 ENTRY_END
 
 ENTRY_BEGIN
diff --git a/tests/testdata_notimpl/iter_req_qname.rpl b/tests/testdata/iter_req_qname.rpl
similarity index 100%
rename from tests/testdata_notimpl/iter_req_qname.rpl
rename to tests/testdata/iter_req_qname.rpl
diff --git a/tests/testdata/iter_validate.rpl b/tests/testdata/iter_validate.rpl
new file mode 100644
index 0000000000000000000000000000000000000000..6983ea599ba27aa78988581ce92bb12911a4661a
--- /dev/null
+++ b/tests/testdata/iter_validate.rpl
@@ -0,0 +1,184 @@
+; config options
+server:
+	trust-anchor: ". 3600 IN DS 19036 8 2 49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5"
+	val-override-date: "1437625000"
+
+stub-zone:
+	name: "."
+	stub-addr: 198.41.0.4 	# a.root-servers.net.
+CONFIG_END
+
+SCENARIO_BEGIN Test basic validation of NS cz. (two levels)
+
+; K.ROOT-SERVERS.NET.
+RANGE_BEGIN 0 100
+	ADDRESS 198.41.0.4
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+.			518400	IN	NS	a.root-servers.net.
+.			518400	IN	NS	b.root-servers.net.
+.			518400	IN	NS	c.root-servers.net.
+.			518400	IN	NS	d.root-servers.net.
+.			518400	IN	NS	e.root-servers.net.
+.			518400	IN	NS	f.root-servers.net.
+.			518400	IN	NS	g.root-servers.net.
+.			518400	IN	NS	h.root-servers.net.
+.			518400	IN	NS	i.root-servers.net.
+.			518400	IN	NS	j.root-servers.net.
+.			518400	IN	NS	k.root-servers.net.
+.			518400	IN	NS	l.root-servers.net.
+.			518400	IN	NS	m.root-servers.net.
+.			518400	IN	RRSIG	NS 8 0 518400 20150802050000 20150723040000 1518 . JSoL4/wQXh7vzoY/m98WYbpr2/S66u4RQi/UhkSrR3JmPZaWRRERDFm6 RRrFY6GWt4CP61X9rvshuVT+0OhluXqYpEatoHEDgur+PKf3+dTAmcgQ 4RzsahwhQ42Y9fDgJ2nNVMcN97HEIH+qMv0FWjU9b7wJ2iYlDL1ZoAVu TKE=
+SECTION ADDITIONAL
+a.root-servers.net.	518400	IN	A	198.41.0.4
+ENTRY_END
+
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN DNSKEY
+SECTION ANSWER
+.			172800	IN	DNSKEY	256 3 8 AwEAAa67bQck1JjopOOFc+iMISFcp/osWrEst2wbKbuQSUWu77QC9UHL ipiHgWN7JlqVAEjKITZz49hhkLmOpmLK55pTq+RD2kwoyNWk9cvpc+tS nIxT7i93O+3oVeLYjMWrkDAz7K45rObbHDuSBwYZKrcSIUCZnCpNMUtn PFl/04cb
+.			172800	IN	DNSKEY	257 3 8 AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjF FVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoX bfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaD X6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpz W5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relS Qageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulq QxA+Uk1ihz0=
+.			172800	IN	RRSIG	DNSKEY 8 0 172800 20150804235959 20150721000000 19036 . n9FwNj80Zik2Rr2zTB4F17ydFpiZfUIv8v/XAz4EbSgRxQgFT+TCz3FW i4O7tW5REXUVNHtULiS7fxKLsHZNDPev8DA20DXAw3eEIDi9pDi01O/e 4GnljpkPnP8d5zA62Dob4cxgmhjjFTvhIjtDsH5Dd4jmyHsgBboy4grZ uJNdsez76gD4Ad6WlosZn5Hj5JwqaxZlRph/6I3va4rkp4c32w5DwaQ7 WSne8ffMHX9r7Dn6EbT3FfvnXFDNPE1P6r+qzTzC0t+M/F4R3H+VOdqg cRJcBG6zGCh9ZErhAeoiJh1WAfpjpzx+TUMzqxZCjSC/XL+l2YMKVHtF 8WNg/w==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+cz. IN NS
+SECTION AUTHORITY
+cz.			172800	IN	NS	a.ns.nic.cz.
+cz.			86400	IN	DS	54576 10 2 397E50C85EDE9CDE33F363A9E66FD1B216D788F8DD438A57A423A386 869C8F06
+cz.			86400	IN	RRSIG	DS 8 1 86400 20150802050000 20150723040000 1518 . fEz3NpYRzgeBjKrLMpht3KFOQ0t6U2wikIaOt1HcmFvurxtPkZVvqdb0 QBQfvh8DoEXDbvpcikzMIO9XYLzzs10X/m91ybGiWzcTVcU+prVGZJP9 zZrvYAIWrpxoC4deKD+vOoNZXGnLfffi6lmGn7QRZaH0LVKjn33cIaPQ 9EM=
+SECTION ADDITIONAL
+a.ns.nic.cz.		172800	IN	A	194.0.12.1
+ENTRY_END
+
+; fake, this can't be validated anyway
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+cz. IN RRSIG
+SECTION ANSWER
+cz.			18000	IN	RRSIG	NS 10 1 18000 20150802132511 20150721120844 39788 cz. fEz3NpYRzgeBjKrLMpht3KFOQ0t6U2wikIaOt1HcmFvurxtPkZVvqdb0 QBQfvh8DoEXDbvpcikzMIO9XYLzzs10X/m91ybGiWzcTVcU+prVGZJP9 zZrvYAIWrpxoC4deKD+vOoNZXGnLfffi6lmGn7QRZaH0LVKjn33cIaPQ 9EM=
+cz.			86400	IN	RRSIG	DS 8 1 86400 20150802050000 20150723040000 1518 . pf5UzinUesHzGQTav/1NxGW0AifCmzLW3S8X9tWDRwx7XSKGac7QVXgp nMNyb/NiSho9oj+ZTaQpBZQaTri+brHT4W/nE0TofqZlyYiaABb9xgxJ LgjLkt+OVcJsM3a+q+QEGSt+skNlZVDQeR+sztbuORiZXAqhxumxD8iy zZ8=
+ENTRY_END
+RANGE_END
+
+;a.ns.nic.cz.
+RANGE_BEGIN 0 100
+	ADDRESS 194.0.12.1
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+cz. IN DNSKEY
+SECTION ANSWER
+cz.			18000	IN	DNSKEY	256 3 10 AwEAAbwKeyKB5fuLe16/N5MR6OoG/PO8uxEob7HoIjK0w0wNjwINYb2w edLtzhVlA4HJ0AUUBuZiNj41hlJ474SOBlsAA7BQdtbL1V0Ksk8IC5Z8 3ldU9Mp+ynkj9p9Cl2UOBmoVFYfkbwz0BsOptcXruYA52Ayc9rHrmDPI /0Y8gZAL
+cz.			18000	IN	DNSKEY	257 3 10 AwEAAay0hi4HN2r/BqMQTpIPIVDyjmyF+9ZWvr5Lewx+q+947o/GrRv4 FGFfkZxf9CFfYVUf0jG5Yq4i06pGVNwJl81HS9Ux2oeHRXUvgtLnl5He RVLL+zgI5byx9HSNr4bPO8ZEn5OjoayhkNyGSFr4VWrzQk/K02vLP4d1 cCEzUQy30eyZto2/tG5ZwCU/iRkS1PJOcOW98hiFIfFDZv1XjbEpqEYh T2PATs6rt+BKwSHKGISmg1PNdg+y0rItemYMWr1f9BGAdtTWoPCPCYPj OZMPoIyA4tMscD+ww54Jf/QNoHccY4hO1yHiuAXG7SUn8jo0IKQ9W7JJ xES0aqFCX/0=
+cz.			18000	IN	RRSIG	DNSKEY 10 1 18000 20150802000000 20150719000000 54576 cz. K04ONpLX3wseqHhUu2QLBY7wzSUszVlut5mC6jpCAqbfhgIvGMnyoWP5 lKwSvCLmjie0j1HSv8Q4OmoYGz8L+P/FGAzK4LhMturHrDtHkpuGvQJ6 //UsHQhf4iwCg5tEeHI4ZvaMmqRZI3FhBnSh0OyFjGO73FRbBU9nDrOM sPB1iCUfRfZhQU0sB/rj82ykBUma280sO1aRp3gmQHc/SVNbFfCL1Z8D htBP6sy4Jh0z3Z40d4CFZ8ZCBsIloHO44/GvXGePtr2dW4gJsoU1619B Jz+6cuTRh5RJBiweUNb/nwjBP8fNRkzH1CbjomC2FpDMnBXw7jE1GUiY vLW9Gg==
+cz.			18000	IN	RRSIG	DNSKEY 10 1 18000 20150805131929 20150723140842 39788 cz. KhyRPt4TYVYH7VAsfn39tY66+5P8bgZhG83d33oogLuqQEPgsxt/tu0c snrUA11Ub+4wOK3MslD5/gTyBuDtT9dk4FbRr3WeUZ4DNn5laYO3AcYx SAU3Vn3dZ8orWFxEwTKNhH5QthPdHj8p8097KRHiPo/DGEnFpYdocEws WJ4=
+ENTRY_END
+
+; a.ns.nic.cz.
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+cz. IN NS
+SECTION ANSWER
+cz.			18000	IN	NS	a.ns.nic.cz.
+cz.			18000	IN	NS	b.ns.nic.cz.
+cz.			18000	IN	NS	c.ns.nic.cz.
+cz.			18000	IN	NS	d.ns.nic.cz.
+cz.			18000	IN	RRSIG	NS 10 1 18000 20150802132511 20150721120844 39788 cz. pf5UzinUesHzGQTav/1NxGW0AifCmzLW3S8X9tWDRwx7XSKGac7QVXgp nMNyb/NiSho9oj+ZTaQpBZQaTri+brHT4W/nE0TofqZlyYiaABb9xgxJ LgjLkt+OVcJsM3a+q+QEGSt+skNlZVDQeR+sztbuORiZXAqhxumxD8iy zZ8=
+SECTION ADDITIONAL
+a.ns.nic.cz.		18000	IN	A	194.0.12.1
+b.ns.nic.cz.		18000	IN	A	194.0.12.1
+c.ns.nic.cz.		18000	IN	A	194.0.12.1
+d.ns.nic.cz.		18000	IN	A	194.0.12.1
+ENTRY_END
+
+RANGE_END
+
+;STEP 0 TIME_PASSES ELAPSE 1000
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+cz. IN RRSIG
+ENTRY_END
+
+; check that it answers a query for RRSIG (unauthenticated)
+; digests are swapped, i.e. signatures are invalid, server shouldn't use them later
+STEP 2 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+cz. IN RRSIG
+SECTION ANSWER
+cz.			18000	IN	RRSIG	NS 10 1 18000 20150802132511 20150721120844 39788 cz. fEz3NpYRzgeBjKrLMpht3KFOQ0t6U2wikIaOt1HcmFvurxtPkZVvqdb0 QBQfvh8DoEXDbvpcikzMIO9XYLzzs10X/m91ybGiWzcTVcU+prVGZJP9 zZrvYAIWrpxoC4deKD+vOoNZXGnLfffi6lmGn7QRZaH0LVKjn33cIaPQ 9EM=
+cz.			86400	IN	RRSIG	DS 8 1 86400 20150802050000 20150723040000 1518 . pf5UzinUesHzGQTav/1NxGW0AifCmzLW3S8X9tWDRwx7XSKGac7QVXgp nMNyb/NiSho9oj+ZTaQpBZQaTri+brHT4W/nE0TofqZlyYiaABb9xgxJ LgjLkt+OVcJsM3a+q+QEGSt+skNlZVDQeR+sztbuORiZXAqhxumxD8iy zZ8=
+ENTRY_END
+
+STEP 3 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+cz. IN NS
+ENTRY_END
+
+; check that it answers a plain query
+STEP 4 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+cz. IN NS
+SECTION ANSWER
+cz.			18000	IN	NS	a.ns.nic.cz.
+cz.			18000	IN	NS	b.ns.nic.cz.
+cz.			18000	IN	NS	c.ns.nic.cz.
+cz.			18000	IN	NS	d.ns.nic.cz.
+ENTRY_END
+
+STEP 5 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+cz. IN NS
+ENTRY_END
+
+; recursion happens here.
+STEP 6 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA AD NOERROR
+SECTION QUESTION
+cz. IN NS
+SECTION ANSWER
+cz.			18000	IN	NS	a.ns.nic.cz.
+cz.			18000	IN	NS	b.ns.nic.cz.
+cz.			18000	IN	NS	c.ns.nic.cz.
+cz.			18000	IN	NS	d.ns.nic.cz.
+cz.			18000	IN	RRSIG	NS 10 1 18000 20150802132511 20150721120844 39788 cz. pf5UzinUesHzGQTav/1NxGW0AifCmzLW3S8X9tWDRwx7XSKGac7QVXgp nMNyb/NiSho9oj+ZTaQpBZQaTri+brHT4W/nE0TofqZlyYiaABb9xgxJ LgjLkt+OVcJsM3a+q+QEGSt+skNlZVDQeR+sztbuORiZXAqhxumxD8iy zZ8=
+ENTRY_END
+
+SCENARIO_END
diff --git a/tests/testdata/iter_validate_child_zone_noaddr.rpl b/tests/testdata/iter_validate_child_zone_noaddr.rpl
new file mode 100644
index 0000000000000000000000000000000000000000..408defde8247644d67bc9b591da89e08e25cfc3e
--- /dev/null
+++ b/tests/testdata/iter_validate_child_zone_noaddr.rpl
@@ -0,0 +1,173 @@
+; config options
+server:
+	trust-anchor: ". 3600 IN DS 19036 8 2 49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5"
+	val-override-date: "1441892800"
+
+stub-zone:
+	name: "."
+	stub-addr: 198.41.0.4 	# a.root-servers.net.
+CONFIG_END
+
+SCENARIO_BEGIN Test basic validation of AAAA nic.cz.
+
+; K.ROOT-SERVERS.NET.
+RANGE_BEGIN 0 100
+	ADDRESS 198.41.0.4
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+.			518400	IN	NS	l.root-servers.net.
+.			518400	IN	NS	i.root-servers.net.
+.			518400	IN	NS	m.root-servers.net.
+.			518400	IN	NS	f.root-servers.net.
+.			518400	IN	NS	d.root-servers.net.
+.			518400	IN	NS	k.root-servers.net.
+.			518400	IN	NS	e.root-servers.net.
+.			518400	IN	NS	h.root-servers.net.
+.			518400	IN	NS	b.root-servers.net.
+.			518400	IN	NS	j.root-servers.net.
+.			518400	IN	NS	c.root-servers.net.
+.			518400	IN	NS	a.root-servers.net.
+.			518400	IN	NS	g.root-servers.net.
+.			518400	IN	RRSIG	NS 8 0 518400 20150920050000 20150910040000 1518 . ZCytFZO9aWv+135mNVaH+qdlXz1t2VyhaOx4GVbydiRuEMVKvjauxXMb OfnCK451G95AjxaL00eCi68Z19B3+pa6Ud8X81M69fHeB4/Eh+KIjl+d YvmUw3DxVQJknj/sHBVihjgsiMsiw03lE+dX+g2ms9TQbOo5VohLPgpC 82A=
+SECTION ADDITIONAL
+a.root-servers.net.	518400	IN	A	198.41.0.4
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN DNSKEY
+SECTION ANSWER
+.			172800	IN	DNSKEY	257 3 8 AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjF FVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoX bfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaD X6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpz W5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relS Qageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulq QxA+Uk1ihz0=
+.			172800	IN	DNSKEY	256 3 8 AwEAAa67bQck1JjopOOFc+iMISFcp/osWrEst2wbKbuQSUWu77QC9UHL ipiHgWN7JlqVAEjKITZz49hhkLmOpmLK55pTq+RD2kwoyNWk9cvpc+tS nIxT7i93O+3oVeLYjMWrkDAz7K45rObbHDuSBwYZKrcSIUCZnCpNMUtn PFl/04cb
+.			172800	IN	RRSIG	DNSKEY 8 0 172800 20150924000000 20150909000000 19036 . XGBRtnftNzxfk4LFyMzQXv9ZSV//SuiHlUYfnK8i0Hg3bHuOR2oEJ+JN P5HBlg+BGLTYHYBTuQYwn0FZd81gF7nVPDcQmHPwPzgwPWH00RDt46dK J1LwJ5KsAbNT5FOVuYRO2Rm15eajwaYGtJHSOyxHEegzuklvMgVVSiBr rPbNTF2/1Qi4c1y1gPXuxkifENbxlbHMvWxcVnG0v2xko/MazQnzSStv i1TtKUKDNT/jLyAv24wALWsPhOcNoVl1uRr9IJX7Ov9wbvSVCoEuBeKC hgy0KO1lRffnqR1YRRqjabKXB161T/fepLkwkxqa0Uidk+rRL3jxulJa nL2TMQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+nic.cz. IN MX
+SECTION AUTHORITY
+cz.			172800	IN	NS	a.ns.nic.cz.
+cz.			86400	IN	DS	54576 10 2 397E50C85EDE9CDE33F363A9E66FD1B216D788F8DD438A57A423A386 869C8F06
+cz.			86400	IN	RRSIG	DS 8 1 86400 20150920050000 20150910040000 1518 . LRx9WQ8KhcUHOCe+eY7jvw1QIm1aRrin02Qn9YtImOGf4V1MVhf1ZYoF mP7GOBDXAbAJhrb5fPKumLsuRLgmA+5VyFhBMmzgqwRjdec1Tu7mWHoQ EukoZp4y2Mmw4NuAs1pBJQOZzLxhYUk+vbjK9mZm5u+mTtt/EFUu8QfG bp8=
+SECTION ADDITIONAL
+a.ns.nic.cz.		172800	IN	A	194.0.12.1
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+nic.cz. IN DS
+SECTION AUTHORITY
+cz.			172800	IN	NS	a.ns.nic.cz.
+cz.			86400	IN	DS	54576 10 2 397E50C85EDE9CDE33F363A9E66FD1B216D788F8DD438A57A423A386 869C8F06
+cz.			86400	IN	RRSIG	DS 8 1 86400 20150920050000 20150910040000 1518 . LRx9WQ8KhcUHOCe+eY7jvw1QIm1aRrin02Qn9YtImOGf4V1MVhf1ZYoF mP7GOBDXAbAJhrb5fPKumLsuRLgmA+5VyFhBMmzgqwRjdec1Tu7mWHoQ EukoZp4y2Mmw4NuAs1pBJQOZzLxhYUk+vbjK9mZm5u+mTtt/EFUu8QfG bp8=
+SECTION ADDITIONAL
+a.ns.nic.cz.		172800	IN	A	194.0.12.1
+ENTRY_END
+
+RANGE_END
+
+;a.ns.nic.cz.
+RANGE_BEGIN 0 100
+	ADDRESS 194.0.12.1
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+cz. IN DNSKEY
+SECTION ANSWER
+cz.			18000	IN	DNSKEY	256 3 10 AwEAAdORJsCVmI4NZRmgtDDRoULmnP6JsA/wR68Z5gO8XD/awSiqsKEB 6BXNC2jvBiPFA94oXroeLXxCjLN+GS/fE1zCKklKfdY5wOHNIlfekWOO 4rbgJtmDzL3IuTbGmNSSIZ0TJkk5NVzpo+Zon9peX2nPdacytQ36hHup GlJMKTxH
+cz.			18000	IN	DNSKEY	257 3 10 AwEAAay0hi4HN2r/BqMQTpIPIVDyjmyF+9ZWvr5Lewx+q+947o/GrRv4 FGFfkZxf9CFfYVUf0jG5Yq4i06pGVNwJl81HS9Ux2oeHRXUvgtLnl5He RVLL+zgI5byx9HSNr4bPO8ZEn5OjoayhkNyGSFr4VWrzQk/K02vLP4d1 cCEzUQy30eyZto2/tG5ZwCU/iRkS1PJOcOW98hiFIfFDZv1XjbEpqEYh T2PATs6rt+BKwSHKGISmg1PNdg+y0rItemYMWr1f9BGAdtTWoPCPCYPj OZMPoIyA4tMscD+ww54Jf/QNoHccY4hO1yHiuAXG7SUn8jo0IKQ9W7JJ xES0aqFCX/0=
+cz.			18000	IN	RRSIG	DNSKEY 10 1 18000 20150917000000 20150903000000 54576 cz. Ei0P45gSw4Vp4u4/H74vm58ehU5JlB4SGLnXXw1U6qVq8EwYrRHv3gV6 9RrUt2GgCqfUlvlJr1Q4WYAJkkiW5zhXJAzCzamtHGuxo3lZuqV1oyw2 zzL4khvmzT0wMxm13TaeSqjbrAEth/00oHIJPqDzrhYlJX74V1q49mD/ 2VoMIVctnTOyE4A+swlyMLOBD8mmjXr47+a5VuwE3bkzBKn1rdHiePl5 MJQjT9Es+qcyMEFZb31/ZOa6MWci1+P28bKFG6mKLVyyiK8sDCkqw/l3 1CTlxRyFdxQc6cBc5KrZwsfApNi5bXXuaJvuOW/YSRbI72HGUNtbbN2v ttsiBw==
+cz.			18000	IN	RRSIG	DNSKEY 10 1 18000 20150922144030 20150910120939 45182 cz. CajFqhmkoOnO9S1HG/o5TTz2nk8fuaKYSZw6aW7vBcVsUAu3PB2fBCpj zRA2JNtX9ebwXPP4WQR+DPgh+hkOneSUK0hNkp5CguUUr+kiJy8a2IXm 3mmvt7yldkq3Xr3Ygqk9yGW5Sd7NiXT4jOXSMmBueNJFnPf9WThEPpqV zc4=
+ENTRY_END
+
+; a.ns.nic.cz.
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nic.cz. IN MX
+SECTION ANSWER
+nic.cz.			1800	IN	MX	20 mx.nic.cz.
+nic.cz.			1800	IN	MX	30 bh.nic.cz.
+nic.cz.			1800	IN	MX	10 mail.nic.cz.
+nic.cz.			1800	IN	RRSIG	MX 5 2 1800 20150917230532 20150904125503 46296 nic.cz. qzyjltVcO33Jisn5RVxSAy8D8QHv71hpKgX9D4TBKe/Yrr7aI7rB6tLQ JCLJlYdq7m0w2N+QZCczV67OK3ZTDPErl/N0IKbxK84EVp5/NqgzKivl h95Z1T1jRf9iGdauDMjz8QTFpnOs62/CuOuEJwAIXeIuH2eT25AoBRDe sXM=
+SECTION AUTHORITY
+nic.cz.			1800	IN	NS	b.ns.nic.cz.
+nic.cz.			1800	IN	NS	a.ns.nic.cz.
+nic.cz.			1800	IN	NS	d.ns.nic.cz.
+nic.cz.			1800	IN	RRSIG	NS 5 2 1800 20150917134944 20150904125503 46296 nic.cz. gPCmMHvHl+76p6ERWuKS9tH/xwD5Or0ZON866yRy1hM0YCzOO0lIsSU9 fxHTKlx3gx0pgz4EGH3Doi54lT9XRZDyp/XiZ6j4+q+583cFJ05ISQHM Sp4QTMqAYCN1XchH2li+YWCgZqUqK1C+D+OO4Zbfu3YVTEaox82+OkCg 0Uo=
+SECTION ADDITIONAL
+a.ns.nic.cz.		18000	IN	A	194.0.12.1
+b.ns.nic.cz.		18000	IN	A	194.0.12.1
+d.ns.nic.cz.		18000	IN	A	194.0.12.1
+ENTRY_END
+
+; a.ns.nic.cz.
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nic.cz. IN DS
+SECTION ANSWER
+nic.cz.			18000	IN	DS	59916 5 1 144130216E45C4EC2BB8595E817916E8B060D87B
+nic.cz.			18000	IN	RRSIG	DS 10 2 18000 20150918193553 20150905063845 45182 cz. leMkrTPUUrO6CmBU9pTMTT2f+0V4DV8P/uY8ZyDa0YHSUZVoFzW4cXZy xzZfgE0y/Q6eJYOeqPOPOoFKs4g8JhdcmFwOrf6Pnmfk5eOhgJVtg5nX xW+j1G3n24a2H4u6ITEzheTcYj00/l8tfPPzS+JW+2yyPALxmQqx4pKP N6I=
+ENTRY_END
+
+;a.ns.nic.cz.
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nic.cz. IN DNSKEY
+SECTION ANSWER
+nic.cz.			1800	IN	DNSKEY	257 3 5 BQEAAAABt3LenoCVTV0okqKYPDnnVJqvwCD9MKJNXg8fcOCdLQYncyoe hpwM5RK2UkZDcDxWkMo7yMa35ej+Mhpaji9si4xXD+Syl4Q06LFiFkdN /5GlVlrIdE3GW7zC7Z4sS14Vz8FbYfcRmhsh19Ob718jGZneGfw2UPbv kyxUR8wD7mguZn02fQ6tjj/Ktp4uSW9tpz3bjGMo2rX+iZk4xgbPaesA OlR/AaHdatGZsWC9CPon8mnLZeu6czm8CBDgBmnf3PE8c5+uyWj1Pw4p p0VQmnX5UrnuGpErg7qXhJm7wY2CRVRMcLX3zmjVWXW1uT9JFh2G+/pZ zxnASfKKltZpuw==
+nic.cz.			1800	IN	DNSKEY	256 3 5 AwEAAcrwqogrgLUrCRzhbXoMEBPy/55Zeg+yyOYodLGT9ts9UhGYRSgI BJQq6aX/6QOf042niK809bglBRid56ioSV7BlMQeuJx9+87AEiqEupS1 Zg5CzLHx/JLGloCWeCT5bwL4hAmT8gCu8xjHvLFqQjr4QX71Et4MfrJG rOgGpcRJ
+nic.cz.			1800	IN	RRSIG	DNSKEY 5 2 1800 20150918013842 20150904125503 46296 nic.cz. J92bm8JmPiF3JVyqlmXpBMgVkiIxLfZq+M1fpRMTiemSrC/S5Fj8RHXU 4qVy/wbToegcd6ivqxKNa4IjIrjQ7L5FSfBjcVkBxarWgXp70k7UEc23 K1ZvLHGwa6efmb/RILjdi2YptQzA52f43mwF5qHfWFkEWVXVgNbqm+Zu arY=
+nic.cz.			1800	IN	RRSIG	DNSKEY 5 2 1800 20150918082556 20150904125503 59916 nic.cz. Zc5XVLTa41lxbOhRkL+PsY3HIpBe43yTrvr9qKRMMuZeyhWsN8YDzUho Otsq3ujf6HyRhJVZrh9Y0Eh2yrvvi+lVSc1ez1kMaqmB+MZxx+d7/f3E 150jW4nGm6T09pyLcKR0sOPV2dRcdgLeeI29wA1S99jr+FWc4AWmQ8/c jjWOW+78EFlDhSu76gUEHi+R/VYhhzW97R4kqaKzqITLhG4luql2s/5E MuHX3MAbFFq6bq5RqS4rxuqxj59VvUSGSWhsxS0HGDrJIsYsrqvRFcg/ yWiEIhTPr/RvmDkNf57p+wWEbI9GkUDE4K53RSKz2jg+z8kcw/FiO731 yMvdcQ==
+ENTRY_END
+
+RANGE_END
+
+;STEP 0 TIME_PASSES ELAPSE 1000
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+nic.cz. IN MX
+ENTRY_END
+
+; recursion happens here.
+STEP 10 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA AD NOERROR
+SECTION QUESTION
+nic.cz. IN MX
+SECTION ANSWER
+nic.cz.			1800	IN	MX	20 mx.nic.cz.
+nic.cz.			1800	IN	MX	30 bh.nic.cz.
+nic.cz.			1800	IN	MX	10 mail.nic.cz.
+nic.cz.			1800	IN	RRSIG	MX 5 2 1800 20150917230532 20150904125503 46296 nic.cz. qzyjltVcO33Jisn5RVxSAy8D8QHv71hpKgX9D4TBKe/Yrr7aI7rB6tLQ JCLJlYdq7m0w2N+QZCczV67OK3ZTDPErl/N0IKbxK84EVp5/NqgzKivl h95Z1T1jRf9iGdauDMjz8QTFpnOs62/CuOuEJwAIXeIuH2eT25AoBRDe sXM=
+ENTRY_END
+
+SCENARIO_END
diff --git a/tests/testdata/iter_validate_nsec_nxdomain.rpl b/tests/testdata/iter_validate_nsec_nxdomain.rpl
new file mode 100644
index 0000000000000000000000000000000000000000..54a2ffd64d87bd4b0191940d2d78e480f6465026
--- /dev/null
+++ b/tests/testdata/iter_validate_nsec_nxdomain.rpl
@@ -0,0 +1,151 @@
+; config options
+server:
+	trust-anchor: ". 3600 IN DS 19036 8 2 49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5"
+	val-override-date: "1438783903"
+
+stub-zone:
+	name: "."
+	stub-addr: 198.41.0.4 	# a.root-servers.net.
+CONFIG_END
+
+SCENARIO_BEGIN Test basic validation of MX xxx.nic.cz.
+
+; K.ROOT-SERVERS.NET.
+RANGE_BEGIN 0 100
+	ADDRESS 198.41.0.4
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+.			518400	IN	NS	a.root-servers.net.
+.			518400	IN	NS	b.root-servers.net.
+.			518400	IN	NS	c.root-servers.net.
+.			518400	IN	NS	d.root-servers.net.
+.			518400	IN	NS	e.root-servers.net.
+.			518400	IN	NS	f.root-servers.net.
+.			518400	IN	NS	g.root-servers.net.
+.			518400	IN	NS	h.root-servers.net.
+.			518400	IN	NS	i.root-servers.net.
+.			518400	IN	NS	j.root-servers.net.
+.			518400	IN	NS	k.root-servers.net.
+.			518400	IN	NS	l.root-servers.net.
+.			518400	IN	NS	m.root-servers.net.
+.			518400	IN	RRSIG	NS 8 0 518400 20150809050000 20150730040000 1518 . ntWgyA7SjlVedxDStbRA6fXl0Hq5pyBgVtBb6l+LbqgLs8/2mwPhzaEw A/BMM+wr7KQLvNSyxTl/SZny94uMVu7o2fnI6+bCP5C+lo7PWni/GvMU yj3JSq2hPv3iO/D1ch8yaKddtYL/NCwPBn9CgpW0jWIWp8FvwwCR4RAs GzA=
+SECTION ADDITIONAL
+a.root-servers.net.	518400	IN	A	198.41.0.4
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN DNSKEY
+SECTION ANSWER
+.			172800	IN	DNSKEY	256 3 8 AwEAAa67bQck1JjopOOFc+iMISFcp/osWrEst2wbKbuQSUWu77QC9UHL ipiHgWN7JlqVAEjKITZz49hhkLmOpmLK55pTq+RD2kwoyNWk9cvpc+tS nIxT7i93O+3oVeLYjMWrkDAz7K45rObbHDuSBwYZKrcSIUCZnCpNMUtn PFl/04cb
+.			172800	IN	DNSKEY	257 3 8 AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjF FVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoX bfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaD X6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpz W5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relS Qageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulq QxA+Uk1ihz0=
+.			172800	IN	RRSIG	DNSKEY 8 0 172800 20150814235959 20150731000000 19036 . GW5z3/PgUahqXvFy4UKqc+gxl6b1T4MwHP6E08PUd1KSyFAy/7cltOP6 dfavtYwP9HWIadti7w0GkK560vWEe0aneJCqn9VvSWLI7wrrTLTDd03v WRFk0qxEaVZ22MxqA2AxHMEnEgbLJ9oTJL8eUZDRetKeCgk3w8zypq4f 3xnh0QO7p5F8mBUlAcrCy8B20ZqItvq9irdeeWOSvvJWs35XnPY497xz WVLrF4hOLQnhmgxJpIDwNRGlkqmbNAmVIICOkrG9S9mvZdhhQqogzHhn k6T7Ws1ZQ2FTYBLc5/QA3urEAn8H6TCm5D+wURcfy5x++hXBOIxipkyh 4yfsgw==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+missing.nic.cz. IN MX
+SECTION AUTHORITY
+cz.			172800	IN	NS	a.ns.nic.cz.
+cz.			86400	IN	DS	54576 10 2 397E50C85EDE9CDE33F363A9E66FD1B216D788F8DD438A57A423A386 869C8F06
+cz.			86400	IN	RRSIG	DS 8 1 86400 20150815050000 20150805040000 1518 . jeryA8jj+yf2X9exz5Ka/Nfifr+k5++Se1klItsut3Jvy1d0X6TI5pjr ABzXbhOUGz6M4cUKhLjM3XDTRspu/VT4DhJUE2pRITKBzeAabDN6dkO/ KHbB/Klrc5DjSeq3RNA3zj39U/TxT+gO8F/fzn6FQKIGkcxwSzSD4Xov K5Q=
+SECTION ADDITIONAL
+a.ns.nic.cz.		172800	IN	A	194.0.12.1
+ENTRY_END
+RANGE_END
+
+;a.ns.nic.cz.
+RANGE_BEGIN 0 100
+	ADDRESS 194.0.12.1
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+cz. IN DNSKEY
+SECTION ANSWER
+cz.			18000	IN	DNSKEY	256 3 10 AwEAAbwKeyKB5fuLe16/N5MR6OoG/PO8uxEob7HoIjK0w0wNjwINYb2w edLtzhVlA4HJ0AUUBuZiNj41hlJ474SOBlsAA7BQdtbL1V0Ksk8IC5Z8 3ldU9Mp+ynkj9p9Cl2UOBmoVFYfkbwz0BsOptcXruYA52Ayc9rHrmDPI /0Y8gZAL
+cz.			18000	IN	DNSKEY	257 3 10 AwEAAay0hi4HN2r/BqMQTpIPIVDyjmyF+9ZWvr5Lewx+q+947o/GrRv4 FGFfkZxf9CFfYVUf0jG5Yq4i06pGVNwJl81HS9Ux2oeHRXUvgtLnl5He RVLL+zgI5byx9HSNr4bPO8ZEn5OjoayhkNyGSFr4VWrzQk/K02vLP4d1 cCEzUQy30eyZto2/tG5ZwCU/iRkS1PJOcOW98hiFIfFDZv1XjbEpqEYh T2PATs6rt+BKwSHKGISmg1PNdg+y0rItemYMWr1f9BGAdtTWoPCPCYPj OZMPoIyA4tMscD+ww54Jf/QNoHccY4hO1yHiuAXG7SUn8jo0IKQ9W7JJ xES0aqFCX/0=
+cz.			18000	IN	RRSIG	DNSKEY 10 1 18000 20150814000000 20150731000000 54576 cz. T/q1zripqILs8CdStXtf/GRpDhgbduHC6AEoGfnK+lfxk9okn6amwhho j464OUtH1wGlS9pikWQ02O6BX5CRaaaZjgMeIJugj3w4MZMPbSk1tV5y JaRXaec/uZI5h91iJtQzNAP5rbMj5liIYQV02nrN8+5SVBwxnrJ9JvQ/ tOetsoP2eh1wlgb+Tu+GgrYVrO/4EwOUk+5RUuMVKofGvY+vyYaEuRip rr6pSjH+dhjKegMv2IQ9rBEI1MKWcFA3+6ZqaMazNShgeEJgBI3GKPog AFiZFijDl5Pd5+4/HftGYpXnUlon266ilvCCS1RzE3pynnHPFFRVBmd6 Q38sIQ==
+cz.			18000	IN	RRSIG	DNSKEY 10 1 18000 20150818181624 20150805140846 39788 cz. dmYD4pzcswSWyVEqEaCKXN1a58uP2b7/fscNn24wAhQ891sTZi3kNhS8 BvoYIncoAppi+Kkw9vRfXNB26YhBgalCDBxHdwg1vxMD/uHiTrQ1KFFM ZjeM+CYTmULK6PY06NN8IyauL87gcx8k2/r9GVr71yUC1nNjNum4ZRZD EiQ=
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NXDOMAIN
+SECTION QUESTION
+missing.nic.cz. IN MX
+SECTION AUTHORITY
+nic.cz.			1800	IN	SOA	a.ns.nic.cz. hostmaster.nic.cz. 1438786503 10800 3600 1209600 7200
+nic.cz.			1800	IN	RRSIG	SOA 5 2 1800 20150819080542 20150805135503 24582 nic.cz. TDX7klpEuTI2vUQdmNzacxND0p828AD1HNxzTop0MJqKTehn8XgPoSK0 ZRWITLIMTtmC9UFLh/nb9I06HqUVdirxCKWvSzO840wjoVF7SjLWZysB 3VhPn84NA/9m8N8dmpTIt2IpN2N/T0lZVIIg/SSeru/dPNKH1uNtKekv 1eg=
+nic.cz.			7200	IN	NSEC	6to4.nic.cz. A NS SOA MX AAAA RRSIG NSEC DNSKEY
+nic.cz.			7200	IN	RRSIG	NSEC 5 2 7200 20150818164706 20150805135503 24582 nic.cz. dqZiT0kHOtrap/aNq/M4u6KCGAS+f1yrAa6aDVBnqIopJ4DJJjnh67WF LedeHae5JLDNwuZV8SlkicIwW5K2ET4//2bJ6FCJcw89s40s3h/QnVxB +wV6hNhqfVnOjtHO4TWK96uNhf/B+A4N/voSpA21zYBMTV8mFvynj7oy ozk=
+*.mirrors.nic.cz.	7200	IN	NSEC	akuma.mnt.nic.cz. CNAME RRSIG NSEC
+*.mirrors.nic.cz.	7200	IN	RRSIG	NSEC 5 3 7200 20150819054509 20150805135503 24582 nic.cz. BDT/Rw1F7/QJd76/KWL0jdkdHkzWNxxZ2Hdgba6o1okc0mgqz2ag1P3s BkYtzWMwql4U7Au/KcLtq6P8X2/T9xytqmYfpn2O1dCaBzHubeTiz/aP wcPaYEC+jPR7JwmHpZlxs+KirS4yo7aVF1OPP7ZjtuqDeNLvmF4W7mHl r/A=
+ENTRY_END
+
+; a.ns.nic.cz.
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nic.cz. IN DS
+SECTION ANSWER
+nic.cz.			18000	IN	DS	59916 5 1 144130216E45C4EC2BB8595E817916E8B060D87B
+nic.cz.			18000	IN	RRSIG	DS 10 2 18000 20150817213510 20150804233901 39788 cz. sxyJEb5Aqpk65VNqxI6bIbyB2UweVjAMf4YvyMJh6MAOGeii8tRbHoJN CntFpaW8sDrw1dgv/xQMFB04Yl3B518n1vMHspweuT3GX5MVV8dEED+9 MdDC0LvhrcPhcoY7ZEz4koywHN39J51tzeSiAPIyQPpMv/b1E3YLwPou lOQ=
+ENTRY_END
+
+;a.ns.nic.cz.
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nic.cz. IN DNSKEY
+SECTION ANSWER
+nic.cz.			1800	IN	DNSKEY	256 3 5 AwEAAatglK6e9CpmATmEDtjQfOMqSEGB2KT3xciP+ZuBH3qYQDggHxao Hk9cfL5uXHxEr8AqmiTNA2CJ9oVm2wlZFCTUtQS2Vxz+i30XQpIPoh1y 0fU/XHKmrVmdVGzc9OdgDyZT+8CbUInAyUin+tWM9M7ekUXAllUw5bd8 VC4SJO3z
+nic.cz.			1800	IN	DNSKEY	257 3 5 BQEAAAABt3LenoCVTV0okqKYPDnnVJqvwCD9MKJNXg8fcOCdLQYncyoe hpwM5RK2UkZDcDxWkMo7yMa35ej+Mhpaji9si4xXD+Syl4Q06LFiFkdN /5GlVlrIdE3GW7zC7Z4sS14Vz8FbYfcRmhsh19Ob718jGZneGfw2UPbv kyxUR8wD7mguZn02fQ6tjj/Ktp4uSW9tpz3bjGMo2rX+iZk4xgbPaesA OlR/AaHdatGZsWC9CPon8mnLZeu6czm8CBDgBmnf3PE8c5+uyWj1Pw4p p0VQmnX5UrnuGpErg7qXhJm7wY2CRVRMcLX3zmjVWXW1uT9JFh2G+/pZ zxnASfKKltZpuw==
+nic.cz.			1800	IN	RRSIG	DNSKEY 5 2 1800 20150818153605 20150805135503 59916 nic.cz. iDP/U4c8zJF085/V/CAlRU4Hs1RugPkzJdaYVCXwnpZ5vjArAY5wzUtx 88626FvBgVD/hnCUrSoN8eNz8ISSsyk3Ql3bTp5Cmxi+hgIqWd1Q3H3u RY3TlsMM0rpsVBalz3f77pia8s7e3kFsjee2z7iadj/ILSfzYQTghSaO 0B6pDaWoUhhwbGWR1Fz0YdhaiYO21Tvxa5/DD3R3fsTWZQ773GENhNhE 1LM6L82770F+VGfbIhG/wBqiRM1FXiikPixvbmgRco2dff/3w/ns2WXI yFjlr7WwaHlrlyIhgI30CYDs3Xe3jI+sxwNG64XqnwVgBIaN7GVbUulb 9RFxkg==
+nic.cz.			1800	IN	RRSIG	DNSKEY 5 2 1800 20150818221347 20150805135503 24582 nic.cz. nrW0y70a7urskICTXafO/39Dd+sWU9gY/xQeeMLHuOTyJc5xPeKfPY61 6xUrfZveYy/dj3quDP+RB5hZCxK2gBDjRDBoDfJayaWmGoPJ4ima42KF wpyR8MMbcnha2Z+hP82Q/pVs7DsC3rJFg9Q5VHP5qzyQcRONYXWBjnM7 y54=
+ENTRY_END
+RANGE_END
+
+;STEP 0 TIME_PASSES ELAPSE 1000
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+missing.nic.cz. IN MX
+ENTRY_END
+
+; recursion happens here.
+STEP 10 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA AD NXDOMAIN
+SECTION QUESTION
+missing.nic.cz. IN MX
+SECTION AUTHORITY
+nic.cz.			1800	IN	SOA	a.ns.nic.cz. hostmaster.nic.cz. 1438786503 10800 3600 1209600 7200
+nic.cz.			1800	IN	RRSIG	SOA 5 2 1800 20150819080542 20150805135503 24582 nic.cz. TDX7klpEuTI2vUQdmNzacxND0p828AD1HNxzTop0MJqKTehn8XgPoSK0 ZRWITLIMTtmC9UFLh/nb9I06HqUVdirxCKWvSzO840wjoVF7SjLWZysB 3VhPn84NA/9m8N8dmpTIt2IpN2N/T0lZVIIg/SSeru/dPNKH1uNtKekv 1eg=
+nic.cz.			7200	IN	NSEC	6to4.nic.cz. A NS SOA MX AAAA RRSIG NSEC DNSKEY
+nic.cz.			7200	IN	RRSIG	NSEC 5 2 7200 20150818164706 20150805135503 24582 nic.cz. dqZiT0kHOtrap/aNq/M4u6KCGAS+f1yrAa6aDVBnqIopJ4DJJjnh67WF LedeHae5JLDNwuZV8SlkicIwW5K2ET4//2bJ6FCJcw89s40s3h/QnVxB +wV6hNhqfVnOjtHO4TWK96uNhf/B+A4N/voSpA21zYBMTV8mFvynj7oy ozk=
+*.mirrors.nic.cz.	7200	IN	NSEC	akuma.mnt.nic.cz. CNAME RRSIG NSEC
+*.mirrors.nic.cz.	7200	IN	RRSIG	NSEC 5 3 7200 20150819054509 20150805135503 24582 nic.cz. BDT/Rw1F7/QJd76/KWL0jdkdHkzWNxxZ2Hdgba6o1okc0mgqz2ag1P3s BkYtzWMwql4U7Au/KcLtq6P8X2/T9xytqmYfpn2O1dCaBzHubeTiz/aP wcPaYEC+jPR7JwmHpZlxs+KirS4yo7aVF1OPP7ZjtuqDeNLvmF4W7mHl r/A=
+ENTRY_END
+
+SCENARIO_END
diff --git a/tests/testdata/nsec3_name_error_response.rpl b/tests/testdata/nsec3_name_error_response.rpl
new file mode 100644
index 0000000000000000000000000000000000000000..c521781844cbc95dde636fdb0a4647e59db20731
--- /dev/null
+++ b/tests/testdata/nsec3_name_error_response.rpl
@@ -0,0 +1,271 @@
+; config options
+server:
+	trust-anchor: ". 3600 IN DS 17272 13 4 B87AD8C76DC2244E7AA57285057BF533F2E248CC8D7E1A071D8A3837A711A5EA705C4707E6E8911DA653BE1AE019927B"
+	val-override-date: "1442323400"
+
+stub-zone:
+	name: "."
+	stub-addr: 127.0.0.1 	# ns.
+CONFIG_END
+
+SCENARIO_BEGIN Test validation of NSEC3 name error responses.
+
+; ns.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.1
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+.                       3600    IN      NS      ns.
+.                       3600    IN      RRSIG   NS 13 0 3600 20151014142315 20150914142315 17272 . aEIYUS4S8Hd7vAVYvHwFyV97lKx4xt2PgAUbM4A7JUXHkTJDHUQEDVQh LWGxK6e+AUeuq4qlDo4vSz3IedmOBQ==
+SECTION ADDITIONAL
+ns.                     3600    IN      A       127.0.0.1
+ns.                     3600    IN      RRSIG   A 13 1 3600 20151014142315 20150914142315 17272 . 27h0pFJyb5t/2cZsFjynp0TRIdUlQwPYcAwCer2UbXTiBBaD8n15hfh8 PFU0if8X0ikqHusz6rCNTx/aBraYdQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN DNSKEY
+SECTION ANSWER
+.                       3600    IN      DNSKEY  256 3 13 qKlBZ0TvdY8C8+7bTcdnQdrLZxEwvxEwlGmIOTd/ccL5Jiei1whNktoE /Qzo1lJ0cXfVssy4EVMaqEdzIa+pkA==
+.                       3600    IN      RRSIG   DNSKEY 13 0 3600 20151014142315 20150914142315 17272 . FaY+kslqSPIRZsk65z8SrROt7kfx+RGUEBGbVgLQxKruJxc9+MMrl4e4 +RefYIlwpecj4jXwb75RTbT0g7OGGg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+aaa.nsec3.example. IN MX
+SECTION AUTHORITY
+example.                3600    IN      NS      ns.example.
+example.                3600    IN      DS      11225 13 4 B4BDAB0B3751300BFB9D0D240649279B4BA0E67A308E1B0BFE2931D9 47F7FD71A2BD807D84CDE24286D955A35752484F
+example.                3600    IN      RRSIG   DS 13 1 3600 20151014143533 20150914143533 17272 . b0+fXKmsBBXkzf+Myr5eRsXWDvY75oMlr4Yi5j+3iF7cOviVGKz3Dw8u bfKW+OmyHiuTeL71gez/84P+vHEvHA==
+SECTION ADDITIONAL
+ns.example.             3600    IN      A       127.0.0.2
+ENTRY_END
+
+RANGE_END
+
+; ns.example.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.2
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+example. IN NS
+SECTION ANSWER
+example.                3600    IN      NS      ns.example.
+example.                3600    IN      RRSIG   NS 13 1 3600 20151014143225 20150914143225 11225 example. C6KOyVJzeRh/3KL9BxSVOVZN0RIyBhlBmmmnVEFT5qPUrn3m5FjcIBtI hi7cAl2FeY1rqstztvKAY6UOBE0kGQ==
+SECTION ADDITIONAL
+ns.example.             3600    IN      A       127.0.0.2
+ns.example.             3600    IN      RRSIG   A 13 2 3600 20151014143225 20150914143225 11225 example. fM/mwUOtyIbKTxgxaekZf5A8kV3qYIFADtvhcQi0TUh09nfkHQtUqhew zVBXCEtjKMnYFvNhWF6PyiirtOeM8w==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+example. IN DNSKEY
+SECTION ANSWER
+example.                3600    IN      DNSKEY  256 3 13 d9Qb4Tj90Y2cvdWcZfu45clfoLKqGbJn2vQKqZv07nc4FMf2oRkrNXtP fixVTLfbbWAFtbbFf3mhCNUsetRUVQ==
+example.                3600    IN      RRSIG   DNSKEY 13 1 3600 20151015124839 20150915124839 11225 example. 4DemFjvys9Gfq+gG1i8IB6GPBUw9lIv3F082JwW7O8tqNIn45n2z14gg ieeJTRhU9xXOVIfj6amITZWbjvGyFA==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+aaa.nsec3.example. IN MX
+SECTION AUTHORITY
+nsec3.example.          3600    IN      NS      ns.nsec3.example.
+nsec3.example.          3600    IN      DS      29913 13 4 9CCAE2E2369F5CE2725B0CCED256E746D4CB8CDA8A1A936C852A3810 1680F6F0D311476C891A5107DEA71165F72DAD01
+nsec3.example.          3600    IN      RRSIG   DS 13 2 3600 20151015124611 20150915124611 11225 example. Ur24uOPBVJV8dXlVzsEYq50QDzsKgzwdQ197/JR1Pjpo6hm+Q5qqGN5v TP57rCIDvqNY0L7ZnYjFDa1HlUie1A==
+SECTION ADDITIONAL
+ns.nsec3.example.        3600    IN      A       127.0.0.3
+ENTRY_END
+
+RANGE_END
+
+; ns.nsec3.example.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.3
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec3.example. IN NS
+SECTION ANSWER
+nsec3.example.          3600    IN      NS      ns.nsec3.example.
+nsec3.example.          3600    IN      RRSIG   NS 13 2 3600 20151015124917 20150915124917 29913 nsec3.example. 5CtvbOiUnPE4yVM9tRDWAjHT0X1X5M8tTez1ZkGVd+c9iwwX+PJV+tWW Q40UIqMVEDW1BG39uzGi82XINdvt4Q==
+SECTION ADDITIONAL
+ns.nsec3.example.       3600    IN      A       127.0.0.3
+ns.nsec3.example.       3600    IN      RRSIG   A 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. CYnl5b4taGLcGp2cZhlB93zbf4CfAEQPVBJVKQXIHlGCshMdakBKCJ5a o9wB+HfnA6VW0/2YgKxdv3j45qVWwQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec3.example. IN DNSKEY
+SECTION ANSWER
+nsec3.example.          3600    IN      DNSKEY  256 3 13 p2ZjJQmvVGYMkFIzttVodmLNuxBqSbIjk/U9vsiWjWkYhgAHbCVDyH2I rKx3aqidW7Wnuaj//zDrXGxKu99WPQ==
+nsec3.example.          3600    IN      RRSIG   DNSKEY 13 2 3600 20151015124917 20150915124917 29913 nsec3.example. M+WnKqDgEcgKxWi6n6hEKm61/OOqeZXVUOj+v8v9pRA/Kp0XVWL5G+hc tbQgGaUv7OFV5doYEI5XwuMazHzumg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NXDOMAIN
+SECTION QUESTION
+aaa.nsec3.example. IN MX
+SECTION AUTHORITY
+nsec3.example.          3600    IN      SOA     ns.nsec3.example. root.nsec3.example. 2 60 60 120 3600
+t6komava61p95c35og3u595fmadcdba3.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 7VGF2JRLB1LROS7VFCD14UQCTLH2SELH A NS SOA MX AAAA RRSIG DNSKEY NSEC3PARAM
+nsec3.example.          3600    IN      RRSIG   SOA 13 2 3600 20151015124917 20150915124917 29913 nsec3.example. mJ+v4DsslLKlxMOAOEx552+Q4hzU/Q/RlyzBrw5uZ6ypOP9FmsIgtGnC cyHLOIpCS+RSSRzZKo2b5ok7y2m8Nw==
+t6komava61p95c35og3u595fmadcdba3.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. 8qLXCvLG80qeMd2HXluT/3CT4TsiW8RrBMrJPYUQbmlld82gZc4mg/0q Lnxr6eA2c5Dyl7FDbVoVbsAsfe0zJw==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NXDOMAIN
+SECTION QUESTION
+missing3.nsec3.example. IN MX
+SECTION AUTHORITY
+nsec3.example.          3600    IN      SOA     ns.nsec3.example. root.nsec3.example. 2 60 60 120 3600
+t6komava61p95c35og3u595fmadcdba3.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 7VGF2JRLB1LROS7VFCD14UQCTLH2SELH A NS SOA MX AAAA RRSIG DNSKEY NSEC3PARAM
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 Q1VKA08M8TAP08SMQN0BU8HAI8BU51NB 
+nsec3.example.          3600    IN      RRSIG   SOA 13 2 3600 20151015124917 20150915124917 29913 nsec3.example. mJ+v4DsslLKlxMOAOEx552+Q4hzU/Q/RlyzBrw5uZ6ypOP9FmsIgtGnC cyHLOIpCS+RSSRzZKo2b5ok7y2m8Nw==
+t6komava61p95c35og3u595fmadcdba3.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. 8qLXCvLG80qeMd2HXluT/3CT4TsiW8RrBMrJPYUQbmlld82gZc4mg/0q Lnxr6eA2c5Dyl7FDbVoVbsAsfe0zJw==
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. YAAp004WrK5E/tgiq93/5UUx4Ze8mHibu3is2jogALKFsRbJPvYcbUoR AQu9KNK/ym2Mjk9VShNSSQPjtqrdVQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NXDOMAIN
+SECTION QUESTION
+missing2.nsec3.example. IN MX
+SECTION AUTHORITY
+nsec3.example.          3600    IN      SOA     ns.nsec3.example. root.nsec3.example. 2 60 60 120 3600
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 Q1VKA08M8TAP08SMQN0BU8HAI8BU51NB 
+nsec3.example.          3600    IN      RRSIG   SOA 13 2 3600 20151015124917 20150915124917 29913 nsec3.example. mJ+v4DsslLKlxMOAOEx552+Q4hzU/Q/RlyzBrw5uZ6ypOP9FmsIgtGnC cyHLOIpCS+RSSRzZKo2b5ok7y2m8Nw==
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. YAAp004WrK5E/tgiq93/5UUx4Ze8mHibu3is2jogALKFsRbJPvYcbUoR AQu9KNK/ym2Mjk9VShNSSQPjtqrdVQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NXDOMAIN
+SECTION QUESTION
+missing22.nsec3.example. IN MX
+SECTION AUTHORITY
+nsec3.example.          3600    IN      SOA     ns.nsec3.example. root.nsec3.example. 2 60 60 120 3600
+t6komava61p95c35og3u595fmadcdba3.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 7VGF2JRLB1LROS7VFCD14UQCTLH2SELH A NS SOA MX AAAA RRSIG DNSKEY NSEC3PARAM
+nsec3.example.          3600    IN      RRSIG   SOA 13 2 3600 20151015124917 20150915124917 29913 nsec3.example. mJ+v4DsslLKlxMOAOEx552+Q4hzU/Q/RlyzBrw5uZ6ypOP9FmsIgtGnC cyHLOIpCS+RSSRzZKo2b5ok7y2m8Nw==
+t6komava61p95c35og3u595fmadcdba3.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. 8qLXCvLG80qeMd2HXluT/3CT4TsiW8RrBMrJPYUQbmlld82gZc4mg/0q Lnxr6eA2c5Dyl7FDbVoVbsAsfe0zJw==
+ENTRY_END
+
+RANGE_END
+
+;STEP 0 TIME_PASSES ELAPSE 1000
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+aaa.nsec3.example. IN MX
+ENTRY_END
+
+; recursion happens here.
+STEP 2 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD NXDOMAIN
+SECTION QUESTION
+aaa.nsec3.example. IN MX
+SECTION AUTHORITY
+nsec3.example.          3600    IN      SOA     ns.nsec3.example. root.nsec3.example. 2 60 60 120 3600
+t6komava61p95c35og3u595fmadcdba3.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 7VGF2JRLB1LROS7VFCD14UQCTLH2SELH A NS SOA MX AAAA RRSIG DNSKEY NSEC3PARAM
+nsec3.example.          3600    IN      RRSIG   SOA 13 2 3600 20151015124917 20150915124917 29913 nsec3.example. mJ+v4DsslLKlxMOAOEx552+Q4hzU/Q/RlyzBrw5uZ6ypOP9FmsIgtGnC cyHLOIpCS+RSSRzZKo2b5ok7y2m8Nw==
+t6komava61p95c35og3u595fmadcdba3.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. 8qLXCvLG80qeMd2HXluT/3CT4TsiW8RrBMrJPYUQbmlld82gZc4mg/0q Lnxr6eA2c5Dyl7FDbVoVbsAsfe0zJw==
+ENTRY_END
+
+STEP 3 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+missing3.nsec3.example. IN MX
+ENTRY_END
+
+STEP 4 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD NXDOMAIN
+SECTION QUESTION
+missing3.nsec3.example. IN MX
+SECTION AUTHORITY
+missing3.nsec3.example. IN MX
+SECTION AUTHORITY
+nsec3.example.          3600    IN      SOA     ns.nsec3.example. root.nsec3.example. 2 60 60 120 3600
+t6komava61p95c35og3u595fmadcdba3.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 7VGF2JRLB1LROS7VFCD14UQCTLH2SELH A NS SOA MX AAAA RRSIG DNSKEY NSEC3PARAM
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 Q1VKA08M8TAP08SMQN0BU8HAI8BU51NB 
+nsec3.example.          3600    IN      RRSIG   SOA 13 2 3600 20151015124917 20150915124917 29913 nsec3.example. mJ+v4DsslLKlxMOAOEx552+Q4hzU/Q/RlyzBrw5uZ6ypOP9FmsIgtGnC cyHLOIpCS+RSSRzZKo2b5ok7y2m8Nw==
+t6komava61p95c35og3u595fmadcdba3.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. 8qLXCvLG80qeMd2HXluT/3CT4TsiW8RrBMrJPYUQbmlld82gZc4mg/0q Lnxr6eA2c5Dyl7FDbVoVbsAsfe0zJw==
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. YAAp004WrK5E/tgiq93/5UUx4Ze8mHibu3is2jogALKFsRbJPvYcbUoR AQu9KNK/ym2Mjk9VShNSSQPjtqrdVQ==
+ENTRY_END
+
+STEP 5 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+missing2.nsec3.example. IN MX
+SECTION AUTHORITY
+ENTRY_END
+
+STEP 6 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD SERVFAIL
+SECTION QUESTION
+missing2.nsec3.example. IN MX
+ENTRY_END
+
+STEP 7 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+missing22.nsec3.example. IN MX
+SECTION AUTHORITY
+ENTRY_END
+
+STEP 8 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD SERVFAIL
+SECTION QUESTION
+missing22.nsec3.example. IN MX
+ENTRY_END
+
+SCENARIO_END
diff --git a/tests/testdata/nsec3_wildcard_answer_response.rpl b/tests/testdata/nsec3_wildcard_answer_response.rpl
new file mode 100644
index 0000000000000000000000000000000000000000..20020d24c9f9c43379a8374e16da825467c33e83
--- /dev/null
+++ b/tests/testdata/nsec3_wildcard_answer_response.rpl
@@ -0,0 +1,203 @@
+; config options
+server:
+	trust-anchor: ". 3600 IN DS 17272 13 4 B87AD8C76DC2244E7AA57285057BF533F2E248CC8D7E1A071D8A3837A711A5EA705C4707E6E8911DA653BE1AE019927B"
+	val-override-date: "1442489540"
+
+stub-zone:
+	name: "."
+	stub-addr: 127.0.0.1 	# ns.
+CONFIG_END
+
+SCENARIO_BEGIN Test validation of NSEC wildcard answer response.
+
+; ns.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.1
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+.                       3600    IN      NS      ns.
+.                       3600    IN      RRSIG   NS 13 0 3600 20151014142315 20150914142315 17272 . aEIYUS4S8Hd7vAVYvHwFyV97lKx4xt2PgAUbM4A7JUXHkTJDHUQEDVQh LWGxK6e+AUeuq4qlDo4vSz3IedmOBQ==
+SECTION ADDITIONAL
+ns.                     3600    IN      A       127.0.0.1
+ns.                     3600    IN      RRSIG   A 13 1 3600 20151014142315 20150914142315 17272 . 27h0pFJyb5t/2cZsFjynp0TRIdUlQwPYcAwCer2UbXTiBBaD8n15hfh8 PFU0if8X0ikqHusz6rCNTx/aBraYdQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN DNSKEY
+SECTION ANSWER
+.                       3600    IN      DNSKEY  256 3 13 qKlBZ0TvdY8C8+7bTcdnQdrLZxEwvxEwlGmIOTd/ccL5Jiei1whNktoE /Qzo1lJ0cXfVssy4EVMaqEdzIa+pkA==
+.                       3600    IN      RRSIG   DNSKEY 13 0 3600 20151014142315 20150914142315 17272 . FaY+kslqSPIRZsk65z8SrROt7kfx+RGUEBGbVgLQxKruJxc9+MMrl4e4 +RefYIlwpecj4jXwb75RTbT0g7OGGg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+aaa.local.nsec3.example. IN A
+SECTION AUTHORITY
+example.                3600    IN      NS      ns.example.
+example.                3600    IN      DS      11225 13 4 B4BDAB0B3751300BFB9D0D240649279B4BA0E67A308E1B0BFE2931D9 47F7FD71A2BD807D84CDE24286D955A35752484F
+example.                3600    IN      RRSIG   DS 13 1 3600 20151014143533 20150914143533 17272 . b0+fXKmsBBXkzf+Myr5eRsXWDvY75oMlr4Yi5j+3iF7cOviVGKz3Dw8u bfKW+OmyHiuTeL71gez/84P+vHEvHA==
+SECTION ADDITIONAL
+ns.example.             3600    IN      A       127.0.0.2
+ENTRY_END
+
+RANGE_END
+
+; ns.example.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.2
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+example. IN NS
+SECTION ANSWER
+example.                3600    IN      NS      ns.example.
+example.                3600    IN      RRSIG   NS 13 1 3600 20151014143225 20150914143225 11225 example. C6KOyVJzeRh/3KL9BxSVOVZN0RIyBhlBmmmnVEFT5qPUrn3m5FjcIBtI hi7cAl2FeY1rqstztvKAY6UOBE0kGQ==
+SECTION ADDITIONAL
+ns.example.             3600    IN      A       127.0.0.2
+ns.example.             3600    IN      RRSIG   A 13 2 3600 20151014143225 20150914143225 11225 example. fM/mwUOtyIbKTxgxaekZf5A8kV3qYIFADtvhcQi0TUh09nfkHQtUqhew zVBXCEtjKMnYFvNhWF6PyiirtOeM8w==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+example. IN DNSKEY
+SECTION ANSWER
+example.                3600    IN      DNSKEY  256 3 13 d9Qb4Tj90Y2cvdWcZfu45clfoLKqGbJn2vQKqZv07nc4FMf2oRkrNXtP fixVTLfbbWAFtbbFf3mhCNUsetRUVQ==
+example.                3600    IN      RRSIG   DNSKEY 13 1 3600 20151015124839 20150915124839 11225 example. 4DemFjvys9Gfq+gG1i8IB6GPBUw9lIv3F082JwW7O8tqNIn45n2z14gg ieeJTRhU9xXOVIfj6amITZWbjvGyFA==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+aaa.local.nsec3.example. IN A
+SECTION AUTHORITY
+nsec3.example.          3600    IN      NS      ns.nsec3.example.
+nsec3.example.          3600    IN      DS      29913 13 4 9CCAE2E2369F5CE2725B0CCED256E746D4CB8CDA8A1A936C852A3810 1680F6F0D311476C891A5107DEA71165F72DAD01
+nsec3.example.          3600    IN      RRSIG   DS 13 2 3600 20151015124611 20150915124611 11225 example. Ur24uOPBVJV8dXlVzsEYq50QDzsKgzwdQ197/JR1Pjpo6hm+Q5qqGN5v TP57rCIDvqNY0L7ZnYjFDa1HlUie1A==
+SECTION ADDITIONAL
+ns.nsec3.example.        3600    IN      A       127.0.0.3
+ENTRY_END
+
+RANGE_END
+
+; ns.nsec3.example.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.3
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec3.example. IN NS
+SECTION ANSWER
+nsec3.example.          3600    IN      NS      ns.nsec3.example.
+nsec3.example.          3600    IN      RRSIG   NS 13 2 3600 20151015124917 20150915124917 29913 nsec3.example. 5CtvbOiUnPE4yVM9tRDWAjHT0X1X5M8tTez1ZkGVd+c9iwwX+PJV+tWW Q40UIqMVEDW1BG39uzGi82XINdvt4Q==
+SECTION ADDITIONAL
+ns.nsec3.example.       3600    IN      A       127.0.0.3
+ns.nsec3.example.       3600    IN      RRSIG   A 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. CYnl5b4taGLcGp2cZhlB93zbf4CfAEQPVBJVKQXIHlGCshMdakBKCJ5a o9wB+HfnA6VW0/2YgKxdv3j45qVWwQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec3.example. IN DNSKEY
+SECTION ANSWER
+nsec3.example.          3600    IN      DNSKEY  256 3 13 p2ZjJQmvVGYMkFIzttVodmLNuxBqSbIjk/U9vsiWjWkYhgAHbCVDyH2I rKx3aqidW7Wnuaj//zDrXGxKu99WPQ==
+nsec3.example.          3600    IN      RRSIG   DNSKEY 13 2 3600 20151015124917 20150915124917 29913 nsec3.example. M+WnKqDgEcgKxWi6n6hEKm61/OOqeZXVUOj+v8v9pRA/Kp0XVWL5G+hc tbQgGaUv7OFV5doYEI5XwuMazHzumg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+aaa.local.nsec3.example. IN A
+SECTION ANSWER
+aaa.local.nsec3.example. 3600   IN      A       127.0.0.3
+aaa.local.nsec3.example. 3600   IN      RRSIG   A 13 3 3600 20151017113144 20150917113144 29913 nsec3.example. Lu3/XpqnhkiTPTWdPFeANaVlmo7pFYMRL+z4p/G6/jHwDC087w8++fPv Ppr79whH3MWnVY5tKTvnABRck4odzQ==
+SECTION AUTHORITY
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 Q1VKA08M8TAP08SMQN0BU8HAI8BU51NB 
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. YAAp004WrK5E/tgiq93/5UUx4Ze8mHibu3is2jogALKFsRbJPvYcbUoR AQu9KNK/ym2Mjk9VShNSSQPjtqrdVQ==
+ENTRY_END
+
+; Missing NSEC covering the wildcard.
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+bbb.local.nsec3.example. IN A
+SECTION ANSWER
+bbb.local.nsec3.example. 3600   IN      A       127.0.0.3
+bbb.local.nsec3.example. 3600   IN      RRSIG   A 13 3 3600 20151017113144 20150917113144 29913 nsec3.example. Lu3/XpqnhkiTPTWdPFeANaVlmo7pFYMRL+z4p/G6/jHwDC087w8++fPv Ppr79whH3MWnVY5tKTvnABRck4odzQ==
+SECTION AUTHORITY
+ENTRY_END
+
+RANGE_END
+
+;STEP 0 TIME_PASSES ELAPSE 1000
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+aaa.local.nsec3.example. IN A
+ENTRY_END
+
+; recursion happens here.
+STEP 2 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD NXDOMAIN
+SECTION QUESTION
+aaa.local.nsec3.example. IN A
+SECTION ANSWER
+SECTION ANSWER
+aaa.local.nsec3.example. 3600   IN      A       127.0.0.3
+aaa.local.nsec3.example. 3600   IN      RRSIG   A 13 3 3600 20151017113144 20150917113144 29913 nsec3.example. Lu3/XpqnhkiTPTWdPFeANaVlmo7pFYMRL+z4p/G6/jHwDC087w8++fPv Ppr79whH3MWnVY5tKTvnABRck4odzQ==
+SECTION AUTHORITY
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 Q1VKA08M8TAP08SMQN0BU8HAI8BU51NB 
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. YAAp004WrK5E/tgiq93/5UUx4Ze8mHibu3is2jogALKFsRbJPvYcbUoR AQu9KNK/ym2Mjk9VShNSSQPjtqrdVQ==
+ENTRY_END
+
+STEP 3 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+bbb.local.nsec3.example. IN A
+ENTRY_END
+
+STEP 4 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD SERVFAIL
+SECTION QUESTION
+bbb.local.nsec3.example. IN A
+ENTRY_END
+
+SCENARIO_END
diff --git a/tests/testdata/nsec3_wildcard_no_data_response.rpl b/tests/testdata/nsec3_wildcard_no_data_response.rpl
new file mode 100644
index 0000000000000000000000000000000000000000..8199ad7d89551950003d9055dd12cfc73b690449
--- /dev/null
+++ b/tests/testdata/nsec3_wildcard_no_data_response.rpl
@@ -0,0 +1,248 @@
+; config options
+server:
+	trust-anchor: ". 3600 IN DS 17272 13 4 B87AD8C76DC2244E7AA57285057BF533F2E248CC8D7E1A071D8A3837A711A5EA705C4707E6E8911DA653BE1AE019927B"
+	val-override-date: "1442839270"
+
+stub-zone:
+	name: "."
+	stub-addr: 127.0.0.1 	# ns.
+CONFIG_END
+
+SCENARIO_BEGIN Test validation of NSEC3 name error responses.
+
+; ns.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.1
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+.                       3600    IN      NS      ns.
+.                       3600    IN      RRSIG   NS 13 0 3600 20151014142315 20150914142315 17272 . aEIYUS4S8Hd7vAVYvHwFyV97lKx4xt2PgAUbM4A7JUXHkTJDHUQEDVQh LWGxK6e+AUeuq4qlDo4vSz3IedmOBQ==
+SECTION ADDITIONAL
+ns.                     3600    IN      A       127.0.0.1
+ns.                     3600    IN      RRSIG   A 13 1 3600 20151014142315 20150914142315 17272 . 27h0pFJyb5t/2cZsFjynp0TRIdUlQwPYcAwCer2UbXTiBBaD8n15hfh8 PFU0if8X0ikqHusz6rCNTx/aBraYdQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN DNSKEY
+SECTION ANSWER
+.                       3600    IN      DNSKEY  256 3 13 qKlBZ0TvdY8C8+7bTcdnQdrLZxEwvxEwlGmIOTd/ccL5Jiei1whNktoE /Qzo1lJ0cXfVssy4EVMaqEdzIa+pkA==
+.                       3600    IN      RRSIG   DNSKEY 13 0 3600 20151014142315 20150914142315 17272 . FaY+kslqSPIRZsk65z8SrROt7kfx+RGUEBGbVgLQxKruJxc9+MMrl4e4 +RefYIlwpecj4jXwb75RTbT0g7OGGg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+aaa.local.nsec3.example. IN CNAME
+SECTION AUTHORITY
+example.                3600    IN      NS      ns.example.
+example.                3600    IN      DS      11225 13 4 B4BDAB0B3751300BFB9D0D240649279B4BA0E67A308E1B0BFE2931D9 47F7FD71A2BD807D84CDE24286D955A35752484F
+example.                3600    IN      RRSIG   DS 13 1 3600 20151014143533 20150914143533 17272 . b0+fXKmsBBXkzf+Myr5eRsXWDvY75oMlr4Yi5j+3iF7cOviVGKz3Dw8u bfKW+OmyHiuTeL71gez/84P+vHEvHA==
+SECTION ADDITIONAL
+ns.example.             3600    IN      A       127.0.0.2
+ENTRY_END
+
+RANGE_END
+
+; ns.example.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.2
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+example. IN NS
+SECTION ANSWER
+example.                3600    IN      NS      ns.example.
+example.                3600    IN      RRSIG   NS 13 1 3600 20151014143225 20150914143225 11225 example. C6KOyVJzeRh/3KL9BxSVOVZN0RIyBhlBmmmnVEFT5qPUrn3m5FjcIBtI hi7cAl2FeY1rqstztvKAY6UOBE0kGQ==
+SECTION ADDITIONAL
+ns.example.             3600    IN      A       127.0.0.2
+ns.example.             3600    IN      RRSIG   A 13 2 3600 20151014143225 20150914143225 11225 example. fM/mwUOtyIbKTxgxaekZf5A8kV3qYIFADtvhcQi0TUh09nfkHQtUqhew zVBXCEtjKMnYFvNhWF6PyiirtOeM8w==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+example. IN DNSKEY
+SECTION ANSWER
+example.                3600    IN      DNSKEY  256 3 13 d9Qb4Tj90Y2cvdWcZfu45clfoLKqGbJn2vQKqZv07nc4FMf2oRkrNXtP fixVTLfbbWAFtbbFf3mhCNUsetRUVQ==
+example.                3600    IN      RRSIG   DNSKEY 13 1 3600 20151015124839 20150915124839 11225 example. 4DemFjvys9Gfq+gG1i8IB6GPBUw9lIv3F082JwW7O8tqNIn45n2z14gg ieeJTRhU9xXOVIfj6amITZWbjvGyFA==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+aaa.local.nsec3.example. IN CNAME
+SECTION AUTHORITY
+nsec3.example.          3600    IN      NS      ns.nsec3.example.
+nsec3.example.          3600    IN      DS      29913 13 4 9CCAE2E2369F5CE2725B0CCED256E746D4CB8CDA8A1A936C852A3810 1680F6F0D311476C891A5107DEA71165F72DAD01
+nsec3.example.          3600    IN      RRSIG   DS 13 2 3600 20151015124611 20150915124611 11225 example. Ur24uOPBVJV8dXlVzsEYq50QDzsKgzwdQ197/JR1Pjpo6hm+Q5qqGN5v TP57rCIDvqNY0L7ZnYjFDa1HlUie1A==
+SECTION ADDITIONAL
+ns.nsec3.example.        3600    IN      A       127.0.0.3
+ENTRY_END
+
+RANGE_END
+
+; ns.nsec3.example.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.3
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec3.example. IN NS
+SECTION ANSWER
+nsec3.example.          3600    IN      NS      ns.nsec3.example.
+nsec3.example.          3600    IN      RRSIG   NS 13 2 3600 20151015124917 20150915124917 29913 nsec3.example. 5CtvbOiUnPE4yVM9tRDWAjHT0X1X5M8tTez1ZkGVd+c9iwwX+PJV+tWW Q40UIqMVEDW1BG39uzGi82XINdvt4Q==
+SECTION ADDITIONAL
+ns.nsec3.example.       3600    IN      A       127.0.0.3
+ns.nsec3.example.       3600    IN      AAAA    ::3
+ns.nsec3.example.       3600    IN      RRSIG   A 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. CYnl5b4taGLcGp2cZhlB93zbf4CfAEQPVBJVKQXIHlGCshMdakBKCJ5a o9wB+HfnA6VW0/2YgKxdv3j45qVWwQ==
+ns.nsec3.example.       3600    IN      RRSIG   AAAA 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. OVKZYrbuwz46eXMt3HMsu3WICcloYkok26g4lbTY/4nslnwluiE7rFEX r5EI9MSt4fF82cghN5McnkjHLhI+rw==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec3.example. IN DNSKEY
+SECTION ANSWER
+nsec3.example.          3600    IN      DNSKEY  256 3 13 p2ZjJQmvVGYMkFIzttVodmLNuxBqSbIjk/U9vsiWjWkYhgAHbCVDyH2I rKx3aqidW7Wnuaj//zDrXGxKu99WPQ==
+nsec3.example.          3600    IN      RRSIG   DNSKEY 13 2 3600 20151015124917 20150915124917 29913 nsec3.example. M+WnKqDgEcgKxWi6n6hEKm61/OOqeZXVUOj+v8v9pRA/Kp0XVWL5G+hc tbQgGaUv7OFV5doYEI5XwuMazHzumg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+local.nsec3.example. IN NS
+SECTION AUTHORITY
+nsec3.example.          3600    IN      SOA     ns.nsec3.example. root.nsec3.example. 6 60 60 120 3600
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 Q1VKA08M8TAP08SMQN0BU8HAI8BU51NB 
+nsec3.example.          3600    IN      RRSIG   SOA 13 2 3600 20151017113144 20150917113144 29913 nsec3.example. iO305HC3tic/x7noFt0JdWr33fcm7ZE3+s4OGXslAA0e47bMlyZnJ0Mu 6WCRNSSq11ZBDuGWTDnqx09YiXHFBw==
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. YAAp004WrK5E/tgiq93/5UUx4Ze8mHibu3is2jogALKFsRbJPvYcbUoR AQu9KNK/ym2Mjk9VShNSSQPjtqrdVQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+aaa.local.nsec3.example. IN CNAME
+SECTION AUTHORITY
+nsec3.example.          3600    IN      SOA     ns.nsec3.example. root.nsec3.example. 6 60 60 120 3600
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 Q1VKA08M8TAP08SMQN0BU8HAI8BU51NB 
+7vgf2jrlb1lros7vfcd14uqctlh2selh.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 CNMT1N5RMPKOL8DOHQ4ATJ62468PLNU6 A AAAA RRSIG
+nsec3.example.          3600    IN      RRSIG   SOA 13 2 3600 20151017113144 20150917113144 29913 nsec3.example. iO305HC3tic/x7noFt0JdWr33fcm7ZE3+s4OGXslAA0e47bMlyZnJ0Mu 6WCRNSSq11ZBDuGWTDnqx09YiXHFBw==
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. YAAp004WrK5E/tgiq93/5UUx4Ze8mHibu3is2jogALKFsRbJPvYcbUoR AQu9KNK/ym2Mjk9VShNSSQPjtqrdVQ==
+7vgf2jrlb1lros7vfcd14uqctlh2selh.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151017113144 20150917113144 29913 nsec3.example. AXFTisbCKigPpK4JLsb3QWwKr5Qd2KucDxowUvY+ycLcH9Z6VjTE4KXu 3YKIFo1Sy9uBYzNfgMEjkCDPwPZDHg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+bbb1.local.nsec3.example. IN CNAME
+SECTION AUTHORITY
+nsec3.example.          3600    IN      SOA     ns.nsec3.example. root.nsec3.example. 6 60 60 120 3600
+7vgf2jrlb1lros7vfcd14uqctlh2selh.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 CNMT1N5RMPKOL8DOHQ4ATJ62468PLNU6 A AAAA RRSIG
+nsec3.example.          3600    IN      RRSIG   SOA 13 2 3600 20151017113144 20150917113144 29913 nsec3.example. iO305HC3tic/x7noFt0JdWr33fcm7ZE3+s4OGXslAA0e47bMlyZnJ0Mu 6WCRNSSq11ZBDuGWTDnqx09YiXHFBw==
+7vgf2jrlb1lros7vfcd14uqctlh2selh.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151017113144 20150917113144 29913 nsec3.example. AXFTisbCKigPpK4JLsb3QWwKr5Qd2KucDxowUvY+ycLcH9Z6VjTE4KXu 3YKIFo1Sy9uBYzNfgMEjkCDPwPZDHg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+bbb11.local.nsec3.example. IN CNAME
+SECTION AUTHORITY
+nsec3.example.          3600    IN      SOA     ns.nsec3.example. root.nsec3.example. 6 60 60 120 3600
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 Q1VKA08M8TAP08SMQN0BU8HAI8BU51NB 
+nsec3.example.          3600    IN      RRSIG   SOA 13 2 3600 20151017113144 20150917113144 29913 nsec3.example. iO305HC3tic/x7noFt0JdWr33fcm7ZE3+s4OGXslAA0e47bMlyZnJ0Mu 6WCRNSSq11ZBDuGWTDnqx09YiXHFBw==
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. YAAp004WrK5E/tgiq93/5UUx4Ze8mHibu3is2jogALKFsRbJPvYcbUoR AQu9KNK/ym2Mjk9VShNSSQPjtqrdVQ==
+ENTRY_END
+
+RANGE_END
+
+;STEP 0 TIME_PASSES ELAPSE 1000
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+aaa.local.nsec3.example. IN CNAME
+ENTRY_END
+
+; recursion happens here.
+STEP 2 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD NOERROR
+SECTION QUESTION
+aaa.local.nsec3.example. IN CNAME
+SECTION AUTHORITY
+nsec3.example.          3600    IN      SOA     ns.nsec3.example. root.nsec3.example. 6 60 60 120 3600
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 Q1VKA08M8TAP08SMQN0BU8HAI8BU51NB 
+7vgf2jrlb1lros7vfcd14uqctlh2selh.nsec3.example. 3600 IN NSEC3 1 0 10 ED699434FF4B1D16 CNMT1N5RMPKOL8DOHQ4ATJ62468PLNU6 A AAAA RRSIG
+nsec3.example.          3600    IN      RRSIG   SOA 13 2 3600 20151017113144 20150917113144 29913 nsec3.example. iO305HC3tic/x7noFt0JdWr33fcm7ZE3+s4OGXslAA0e47bMlyZnJ0Mu 6WCRNSSq11ZBDuGWTDnqx09YiXHFBw==
+ivmjat46u4s0qgn5ouqtsut9mtut0230.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151015124917 20150915124917 29913 nsec3.example. YAAp004WrK5E/tgiq93/5UUx4Ze8mHibu3is2jogALKFsRbJPvYcbUoR AQu9KNK/ym2Mjk9VShNSSQPjtqrdVQ==
+7vgf2jrlb1lros7vfcd14uqctlh2selh.nsec3.example. 3600 IN RRSIG NSEC3 13 3 3600 20151017113144 20150917113144 29913 nsec3.example. AXFTisbCKigPpK4JLsb3QWwKr5Qd2KucDxowUvY+ycLcH9Z6VjTE4KXu 3YKIFo1Sy9uBYzNfgMEjkCDPwPZDHg==
+ENTRY_END
+
+STEP 3 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+bbb1.local.nsec3.example. IN CNAME
+ENTRY_END
+
+STEP 4 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD SERVFAIL
+SECTION QUESTION
+bbb1.local.nsec3.example. IN CNAME
+ENTRY_END
+
+STEP 5 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+bbb11.local.nsec3.example. IN CNAME
+ENTRY_END
+
+STEP 6 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD SERVFAIL
+SECTION QUESTION
+bbb11.local.nsec3.example. IN CNAME
+ENTRY_END
+
+SCENARIO_END
diff --git a/tests/testdata/nsec_name_error_response.rpl b/tests/testdata/nsec_name_error_response.rpl
new file mode 100644
index 0000000000000000000000000000000000000000..1d622aeca2ff500964822c73520e7200afcef526
--- /dev/null
+++ b/tests/testdata/nsec_name_error_response.rpl
@@ -0,0 +1,269 @@
+; config options
+server:
+	trust-anchor: ". 3600 IN DS 17272 13 4 B87AD8C76DC2244E7AA57285057BF533F2E248CC8D7E1A071D8A3837A711A5EA705C4707E6E8911DA653BE1AE019927B"
+	val-override-date: "1442323400"
+
+stub-zone:
+	name: "."
+	stub-addr: 127.0.0.1 	# ns.
+CONFIG_END
+
+SCENARIO_BEGIN Test validation of NSEC name error responses.
+
+; ns.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.1
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+.                       3600    IN      NS      ns.
+.                       3600    IN      RRSIG   NS 13 0 3600 20151014142315 20150914142315 17272 . aEIYUS4S8Hd7vAVYvHwFyV97lKx4xt2PgAUbM4A7JUXHkTJDHUQEDVQh LWGxK6e+AUeuq4qlDo4vSz3IedmOBQ==
+SECTION ADDITIONAL
+ns.                     3600    IN      A       127.0.0.1
+ns.                     3600    IN      RRSIG   A 13 1 3600 20151014142315 20150914142315 17272 . 27h0pFJyb5t/2cZsFjynp0TRIdUlQwPYcAwCer2UbXTiBBaD8n15hfh8 PFU0if8X0ikqHusz6rCNTx/aBraYdQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN DNSKEY
+SECTION ANSWER
+.                       3600    IN      DNSKEY  256 3 13 qKlBZ0TvdY8C8+7bTcdnQdrLZxEwvxEwlGmIOTd/ccL5Jiei1whNktoE /Qzo1lJ0cXfVssy4EVMaqEdzIa+pkA==
+.                       3600    IN      RRSIG   DNSKEY 13 0 3600 20151014142315 20150914142315 17272 . FaY+kslqSPIRZsk65z8SrROt7kfx+RGUEBGbVgLQxKruJxc9+MMrl4e4 +RefYIlwpecj4jXwb75RTbT0g7OGGg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+aaa.nsec.example. IN MX
+SECTION AUTHORITY
+example.                3600    IN      NS      ns.example.
+example.                3600    IN      DS      11225 13 4 B4BDAB0B3751300BFB9D0D240649279B4BA0E67A308E1B0BFE2931D9 47F7FD71A2BD807D84CDE24286D955A35752484F
+example.                3600    IN      RRSIG   DS 13 1 3600 20151014143533 20150914143533 17272 . b0+fXKmsBBXkzf+Myr5eRsXWDvY75oMlr4Yi5j+3iF7cOviVGKz3Dw8u bfKW+OmyHiuTeL71gez/84P+vHEvHA==
+SECTION ADDITIONAL
+ns.example.             3600    IN      A       127.0.0.2
+ENTRY_END
+
+RANGE_END
+
+; ns.example.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.2
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+example. IN NS
+SECTION ANSWER
+example.                3600    IN      NS      ns.example.
+example.                3600    IN      RRSIG   NS 13 1 3600 20151014143225 20150914143225 11225 example. C6KOyVJzeRh/3KL9BxSVOVZN0RIyBhlBmmmnVEFT5qPUrn3m5FjcIBtI hi7cAl2FeY1rqstztvKAY6UOBE0kGQ==
+SECTION ADDITIONAL
+ns.example.             3600    IN      A       127.0.0.2
+ns.example.             3600    IN      RRSIG   A 13 2 3600 20151014143225 20150914143225 11225 example. fM/mwUOtyIbKTxgxaekZf5A8kV3qYIFADtvhcQi0TUh09nfkHQtUqhew zVBXCEtjKMnYFvNhWF6PyiirtOeM8w==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+example. IN DNSKEY
+SECTION ANSWER
+example.                3600    IN      DNSKEY  256 3 13 d9Qb4Tj90Y2cvdWcZfu45clfoLKqGbJn2vQKqZv07nc4FMf2oRkrNXtP fixVTLfbbWAFtbbFf3mhCNUsetRUVQ==
+example.                3600    IN      RRSIG   DNSKEY 13 1 3600 20151015124839 20150915124839 11225 example. 4DemFjvys9Gfq+gG1i8IB6GPBUw9lIv3F082JwW7O8tqNIn45n2z14gg ieeJTRhU9xXOVIfj6amITZWbjvGyFA==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+aaa.nsec.example. IN MX
+SECTION AUTHORITY
+nsec.example.           3600    IN      NS      ns.nsec.example.
+nsec.example.           3600    IN      DS      54343 13 4 90ABD4FB9F053CF67F6D838DD2437FB16104B8BF127319706223004F 2ED72AF2872B4E507EB483A303BF60BF08C87364
+nsec.example.           3600    IN      RRSIG   DS 13 2 3600 20151015124611 20150915124611 11225 example. HYzlEdyYugggsEwUVyyY4XHFVUZZ8yiIh4vnuViGBQQJP+yryYh1aLyN ap2Q51nkmSG1fXDb2IySiAYuqUJyLw==
+SECTION ADDITIONAL
+ns.nsec.example.        3600    IN      A       127.0.0.3
+ENTRY_END
+
+RANGE_END
+
+; ns.nsec.example.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.3
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec.example. IN NS
+SECTION ANSWER
+nsec.example.           3600    IN      NS      ns.nsec.example.
+nsec.example.           3600    IN      RRSIG   NS 13 2 3600 20151015124917 20150915124917 54343 nsec.example. 6s75LEuylIKAxqAbcPmmnkOMC7jxF6cPZGW5EFbhOOeR63ENyh642GE1 71WtJc7Ta4Y/PsnAT+/dTv8NSTDCHQ==
+SECTION ADDITIONAL
+ns.nsec.example.        3600    IN      A       127.0.0.3
+ns.nsec.example.        3600    IN      RRSIG   A 13 3 3600 20151015124917 20150915124917 54343 nsec.example. oJpF87bjXR0DjIoNvEAo+Wu+p9jF+URX5lxi+g53OFCX1Q1lxqj5ujGd KOPsNAbKvTCsoFFW4tQyhCYJYD1HlQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec.example. IN DNSKEY
+SECTION ANSWER
+nsec.example.           3600    IN      DNSKEY  256 3 13 HA6nKf+X7/mYkmmRO8qS2tIKT0B60P7COAiRs25xKs/rAP+tDtGWkrkG NQx2D3ajccC9whjRaKz2JVS3ItTFQg==
+nsec.example.           3600    IN      RRSIG   DNSKEY 13 2 3600 20151015124917 20150915124917 54343 nsec.example. 965Mfxs1QtgxwzyhfxXyKyOZ9iT1DXpvypBBR10sLyjHe/w7cRhgcyev Cza6K+2jJwHJBmbknc3Qhi+1dd+AJw==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NXDOMAIN
+SECTION QUESTION
+aaa.nsec.example. IN MX
+SECTION AUTHORITY
+nsec.example.           3600    IN      SOA     ns.nsec.example. root.nsec.example. 2 60 60 120 3600
+nsec.example.           3600    IN      NSEC    alias.nsec.example. A NS SOA MX AAAA RRSIG NSEC DNSKEY
+nsec.example.           3600    IN      RRSIG   SOA 13 2 3600 20151015124917 20150915124917 54343 nsec.example. AcjIOhRgJMRILo06O2yl/G4Q6gTuA0NIGpnejpgcoVHg8kZy6xmURhTc kYf//qbx/WPB9k+8j+ymmQPe1phJCQ==
+nsec.example.           3600    IN      RRSIG   NSEC 13 2 3600 20151015124917 20150915124917 54343 nsec.example. STcV7Lc1a794i9DTgflI+d0N0KXTMws0G8VGc0Wo4tVI8lvFJcG1SFXW /jJaXkQstdZ2EM63fIs/u1hhBaV2Gw==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NXDOMAIN
+SECTION QUESTION
+missing.nsec.example. IN MX
+SECTION AUTHORITY
+nsec.example.           3600    IN      SOA     ns.nsec.example. root.nsec.example. 2 60 60 120 3600
+mail.nsec.example.      3600    IN      NSEC    multiple.nsec.example. A AAAA RRSIG NSEC
+nsec.example.           3600    IN      NSEC    alias.nsec.example. A NS SOA MX AAAA RRSIG NSEC DNSKEY
+nsec.example.           3600    IN      RRSIG   SOA 13 2 3600 20151015124917 20150915124917 54343 nsec.example. AcjIOhRgJMRILo06O2yl/G4Q6gTuA0NIGpnejpgcoVHg8kZy6xmURhTc kYf//qbx/WPB9k+8j+ymmQPe1phJCQ==
+mail.nsec.example.      3600    IN      RRSIG   NSEC 13 3 3600 20151015124917 20150915124917 54343 nsec.example. kM+Z63RDn377szwbOqPPinkH98BuCljY7hoeM8jGJcnQ90fA3NFi72Jg k/0T1bo4r0cNMn6lm9OUotawa6BOqw==
+nsec.example.           3600    IN      RRSIG   NSEC 13 2 3600 20151015124917 20150915124917 54343 nsec.example. STcV7Lc1a794i9DTgflI+d0N0KXTMws0G8VGc0Wo4tVI8lvFJcG1SFXW /jJaXkQstdZ2EM63fIs/u1hhBaV2Gw==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NXDOMAIN
+SECTION QUESTION
+missing1.nsec.example. IN MX
+SECTION AUTHORITY
+nsec.example.           3600    IN      SOA     ns.nsec.example. root.nsec.example. 2 60 60 120 3600
+nsec.example.           3600    IN      NSEC    alias.nsec.example. A NS SOA MX AAAA RRSIG NSEC DNSKEY
+nsec.example.           3600    IN      RRSIG   SOA 13 2 3600 20151015124917 20150915124917 54343 nsec.example. AcjIOhRgJMRILo06O2yl/G4Q6gTuA0NIGpnejpgcoVHg8kZy6xmURhTc kYf//qbx/WPB9k+8j+ymmQPe1phJCQ==
+nsec.example.           3600    IN      RRSIG   NSEC 13 2 3600 20151015124917 20150915124917 54343 nsec.example. STcV7Lc1a794i9DTgflI+d0N0KXTMws0G8VGc0Wo4tVI8lvFJcG1SFXW /jJaXkQstdZ2EM63fIs/u1hhBaV2Gw==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NXDOMAIN
+SECTION QUESTION
+missing2.nsec.example. IN MX
+SECTION AUTHORITY
+nsec.example.           3600    IN      SOA     ns.nsec.example. root.nsec.example. 2 60 60 120 3600
+mail.nsec.example.      3600    IN      NSEC    multiple.nsec.example. A AAAA RRSIG NSEC
+nsec.example.           3600    IN      RRSIG   SOA 13 2 3600 20151015124917 20150915124917 54343 nsec.example. AcjIOhRgJMRILo06O2yl/G4Q6gTuA0NIGpnejpgcoVHg8kZy6xmURhTc kYf//qbx/WPB9k+8j+ymmQPe1phJCQ==
+mail.nsec.example.      3600    IN      RRSIG   NSEC 13 3 3600 20151015124917 20150915124917 54343 nsec.example. kM+Z63RDn377szwbOqPPinkH98BuCljY7hoeM8jGJcnQ90fA3NFi72Jg k/0T1bo4r0cNMn6lm9OUotawa6BOqw==
+ENTRY_END
+
+RANGE_END
+
+;STEP 0 TIME_PASSES ELAPSE 1000
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+aaa.nsec.example. IN MX
+ENTRY_END
+
+; recursion happens here.
+STEP 2 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD NXDOMAIN
+SECTION QUESTION
+aaa.nsec.example. IN MX
+SECTION AUTHORITY
+nsec.example.           3600    IN      SOA     ns.nsec.example. root.nsec.example. 2 60 60 120 3600
+nsec.example.           3600    IN      NSEC    alias.nsec.example. A NS SOA MX AAAA RRSIG NSEC DNSKEY
+nsec.example.           3600    IN      RRSIG   SOA 13 2 3600 20151015124917 20150915124917 54343 nsec.example. AcjIOhRgJMRILo06O2yl/G4Q6gTuA0NIGpnejpgcoVHg8kZy6xmURhTc kYf//qbx/WPB9k+8j+ymmQPe1phJCQ==
+nsec.example.           3600    IN      RRSIG   NSEC 13 2 3600 20151015124917 20150915124917 54343 nsec.example. STcV7Lc1a794i9DTgflI+d0N0KXTMws0G8VGc0Wo4tVI8lvFJcG1SFXW /jJaXkQstdZ2EM63fIs/u1hhBaV2Gw==
+ENTRY_END
+
+STEP 3 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+missing.nsec.example. IN MX
+ENTRY_END
+
+STEP 4 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD NXDOMAIN
+SECTION QUESTION
+missing.nsec.example. IN MX
+SECTION AUTHORITY
+nsec.example.           3600    IN      SOA     ns.nsec.example. root.nsec.example. 2 60 60 120 3600
+mail.nsec.example.      3600    IN      NSEC    multiple.nsec.example. A AAAA RRSIG NSEC
+nsec.example.           3600    IN      NSEC    alias.nsec.example. A NS SOA MX AAAA RRSIG NSEC DNSKEY
+nsec.example.           3600    IN      RRSIG   SOA 13 2 3600 20151015124917 20150915124917 54343 nsec.example. AcjIOhRgJMRILo06O2yl/G4Q6gTuA0NIGpnejpgcoVHg8kZy6xmURhTc kYf//qbx/WPB9k+8j+ymmQPe1phJCQ==
+mail.nsec.example.      3600    IN      RRSIG   NSEC 13 3 3600 20151015124917 20150915124917 54343 nsec.example. kM+Z63RDn377szwbOqPPinkH98BuCljY7hoeM8jGJcnQ90fA3NFi72Jg k/0T1bo4r0cNMn6lm9OUotawa6BOqw==
+nsec.example.           3600    IN      RRSIG   NSEC 13 2 3600 20151015124917 20150915124917 54343 nsec.example. STcV7Lc1a794i9DTgflI+d0N0KXTMws0G8VGc0Wo4tVI8lvFJcG1SFXW /jJaXkQstdZ2EM63fIs/u1hhBaV2Gw==
+ENTRY_END
+
+STEP 5 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+missing1.nsec.example. IN MX
+ENTRY_END
+
+STEP 6 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA SERVFAIL
+SECTION QUESTION
+missing1.nsec.example. IN MX
+SECTION AUTHORITY
+ENTRY_END
+
+STEP 7 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+missing2.nsec.example. IN MX
+ENTRY_END
+
+STEP 8 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA SERVFAIL
+SECTION QUESTION
+missing2.nsec.example. IN MX
+SECTION AUTHORITY
+ENTRY_END
+
+SCENARIO_END
diff --git a/tests/testdata/nsec_no_data_response.rpl b/tests/testdata/nsec_no_data_response.rpl
new file mode 100644
index 0000000000000000000000000000000000000000..4118d88faae9c4e2d54e18b908d796485eeec66c
--- /dev/null
+++ b/tests/testdata/nsec_no_data_response.rpl
@@ -0,0 +1,199 @@
+; config options
+server:
+	trust-anchor: ". 3600 IN DS 17272 13 4 B87AD8C76DC2244E7AA57285057BF533F2E248CC8D7E1A071D8A3837A711A5EA705C4707E6E8911DA653BE1AE019927B"
+	val-override-date: "1442839270"
+
+stub-zone:
+	name: "."
+	stub-addr: 127.0.0.1 	# ns.
+CONFIG_END
+
+SCENARIO_BEGIN Test validation of NSEC3 name error responses.
+
+; ns.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.1
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+.                       3600    IN      NS      ns.
+.                       3600    IN      RRSIG   NS 13 0 3600 20151014142315 20150914142315 17272 . aEIYUS4S8Hd7vAVYvHwFyV97lKx4xt2PgAUbM4A7JUXHkTJDHUQEDVQh LWGxK6e+AUeuq4qlDo4vSz3IedmOBQ==
+SECTION ADDITIONAL
+ns.                     3600    IN      A       127.0.0.1
+ns.                     3600    IN      RRSIG   A 13 1 3600 20151014142315 20150914142315 17272 . 27h0pFJyb5t/2cZsFjynp0TRIdUlQwPYcAwCer2UbXTiBBaD8n15hfh8 PFU0if8X0ikqHusz6rCNTx/aBraYdQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN DNSKEY
+SECTION ANSWER
+.                       3600    IN      DNSKEY  256 3 13 qKlBZ0TvdY8C8+7bTcdnQdrLZxEwvxEwlGmIOTd/ccL5Jiei1whNktoE /Qzo1lJ0cXfVssy4EVMaqEdzIa+pkA==
+.                       3600    IN      RRSIG   DNSKEY 13 0 3600 20151014142315 20150914142315 17272 . FaY+kslqSPIRZsk65z8SrROt7kfx+RGUEBGbVgLQxKruJxc9+MMrl4e4 +RefYIlwpecj4jXwb75RTbT0g7OGGg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+nsec.example. IN CNAME
+SECTION AUTHORITY
+example.                3600    IN      NS      ns.example.
+example.                3600    IN      DS      11225 13 4 B4BDAB0B3751300BFB9D0D240649279B4BA0E67A308E1B0BFE2931D9 47F7FD71A2BD807D84CDE24286D955A35752484F
+example.                3600    IN      RRSIG   DS 13 1 3600 20151014143533 20150914143533 17272 . b0+fXKmsBBXkzf+Myr5eRsXWDvY75oMlr4Yi5j+3iF7cOviVGKz3Dw8u bfKW+OmyHiuTeL71gez/84P+vHEvHA==
+SECTION ADDITIONAL
+ns.example.             3600    IN      A       127.0.0.2
+ENTRY_END
+
+RANGE_END
+
+; ns.example.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.2
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+example. IN NS
+SECTION ANSWER
+example.                3600    IN      NS      ns.example.
+example.                3600    IN      RRSIG   NS 13 1 3600 20151014143225 20150914143225 11225 example. C6KOyVJzeRh/3KL9BxSVOVZN0RIyBhlBmmmnVEFT5qPUrn3m5FjcIBtI hi7cAl2FeY1rqstztvKAY6UOBE0kGQ==
+SECTION ADDITIONAL
+ns.example.             3600    IN      A       127.0.0.2
+ns.example.             3600    IN      RRSIG   A 13 2 3600 20151014143225 20150914143225 11225 example. fM/mwUOtyIbKTxgxaekZf5A8kV3qYIFADtvhcQi0TUh09nfkHQtUqhew zVBXCEtjKMnYFvNhWF6PyiirtOeM8w==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+example. IN DNSKEY
+SECTION ANSWER
+example.                3600    IN      DNSKEY  256 3 13 d9Qb4Tj90Y2cvdWcZfu45clfoLKqGbJn2vQKqZv07nc4FMf2oRkrNXtP fixVTLfbbWAFtbbFf3mhCNUsetRUVQ==
+example.                3600    IN      RRSIG   DNSKEY 13 1 3600 20151015124839 20150915124839 11225 example. 4DemFjvys9Gfq+gG1i8IB6GPBUw9lIv3F082JwW7O8tqNIn45n2z14gg ieeJTRhU9xXOVIfj6amITZWbjvGyFA==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+nsec.example. IN CNAME
+SECTION AUTHORITY
+nsec.example.           3600    IN      NS      ns.nsec.example.
+nsec.example.           3600    IN      DS      54343 13 4 90ABD4FB9F053CF67F6D838DD2437FB16104B8BF127319706223004F 2ED72AF2872B4E507EB483A303BF60BF08C87364
+nsec.example.           3600    IN      RRSIG   DS 13 2 3600 20151015124611 20150915124611 11225 example. HYzlEdyYugggsEwUVyyY4XHFVUZZ8yiIh4vnuViGBQQJP+yryYh1aLyN ap2Q51nkmSG1fXDb2IySiAYuqUJyLw==
+SECTION ADDITIONAL
+ns.nsec.example.        3600    IN      A       127.0.0.3
+ENTRY_END
+
+RANGE_END
+
+; ns.nsec.example.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.3
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec.example. IN NS
+SECTION ANSWER
+nsec.example.           3600    IN      NS      ns.nsec.example.
+nsec.example.           3600    IN      RRSIG   NS 13 2 3600 20151015124917 20150915124917 54343 nsec.example. 6s75LEuylIKAxqAbcPmmnkOMC7jxF6cPZGW5EFbhOOeR63ENyh642GE1 71WtJc7Ta4Y/PsnAT+/dTv8NSTDCHQ==
+SECTION ADDITIONAL
+ns.nsec.example.        3600    IN      A       127.0.0.3
+ns.nsec.example.        3600    IN      RRSIG   A 13 3 3600 20151015124917 20150915124917 54343 nsec.example. oJpF87bjXR0DjIoNvEAo+Wu+p9jF+URX5lxi+g53OFCX1Q1lxqj5ujGd KOPsNAbKvTCsoFFW4tQyhCYJYD1HlQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec.example. IN DNSKEY
+SECTION ANSWER
+nsec.example.           3600    IN      DNSKEY  256 3 13 HA6nKf+X7/mYkmmRO8qS2tIKT0B60P7COAiRs25xKs/rAP+tDtGWkrkG NQx2D3ajccC9whjRaKz2JVS3ItTFQg==
+nsec.example.           3600    IN      RRSIG   DNSKEY 13 2 3600 20151015124917 20150915124917 54343 nsec.example. 965Mfxs1QtgxwzyhfxXyKyOZ9iT1DXpvypBBR10sLyjHe/w7cRhgcyev Cza6K+2jJwHJBmbknc3Qhi+1dd+AJw==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec.example. IN CNAME
+SECTION AUTHORITY
+nsec.example.           3600    IN      SOA     ns.nsec.example. root.nsec.example. 6 60 60 120 3600
+nsec.example.           3600    IN      NSEC    alias.nsec.example. A NS SOA MX AAAA RRSIG NSEC DNSKEY
+nsec.example.           3600    IN      RRSIG   SOA 13 2 3600 20151017113144 20150917113144 54343 nsec.example. /3orb3cezQbBCZsFP9rx6Col9AB2QxHQtzQ32BYe09MfN7YZxtTE/HZJ aSXGWD3D7sLBdEkg8TGP8JPQtbW2yQ==
+nsec.example.           3600    IN      RRSIG   NSEC 13 2 3600 20151015124917 20150915124917 54343 nsec.example. STcV7Lc1a794i9DTgflI+d0N0KXTMws0G8VGc0Wo4tVI8lvFJcG1SFXW /jJaXkQstdZ2EM63fIs/u1hhBaV2Gw==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec.example. IN TYPE1000
+SECTION AUTHORITY
+nsec.example.           3600    IN      SOA     ns.nsec.example. root.nsec.example. 6 60 60 120 3600
+nsec.example.           3600    IN      RRSIG   SOA 13 2 3600 20151017113144 20150917113144 54343 nsec.example. /3orb3cezQbBCZsFP9rx6Col9AB2QxHQtzQ32BYe09MfN7YZxtTE/HZJ aSXGWD3D7sLBdEkg8TGP8JPQtbW2yQ==
+ENTRY_END
+
+RANGE_END
+
+;STEP 0 TIME_PASSES ELAPSE 1000
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+nsec.example. IN CNAME
+ENTRY_END
+
+; recursion happens here.
+STEP 2 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD NOERROR
+SECTION QUESTION
+nsec.example. IN CNAME
+SECTION AUTHORITY
+nsec.example.           3600    IN      SOA     ns.nsec.example. root.nsec.example. 6 60 60 120 3600
+nsec.example.           3600    IN      NSEC    alias.nsec.example. A NS SOA MX AAAA RRSIG NSEC DNSKEY
+nsec.example.           3600    IN      RRSIG   SOA 13 2 3600 20151017113144 20150917113144 54343 nsec.example. /3orb3cezQbBCZsFP9rx6Col9AB2QxHQtzQ32BYe09MfN7YZxtTE/HZJ aSXGWD3D7sLBdEkg8TGP8JPQtbW2yQ==
+nsec.example.           3600    IN      RRSIG   NSEC 13 2 3600 20151015124917 20150915124917 54343 nsec.example. STcV7Lc1a794i9DTgflI+d0N0KXTMws0G8VGc0Wo4tVI8lvFJcG1SFXW /jJaXkQstdZ2EM63fIs/u1hhBaV2Gw==
+ENTRY_END
+
+STEP 3 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+nsec.example. IN TYPE1000
+ENTRY_END
+
+STEP 4 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD SERVFAIL
+SECTION QUESTION
+nsec.example. IN TYPE1000
+SECTION AUTHORITY
+ENTRY_END
+
+SCENARIO_END
diff --git a/tests/testdata/nsec_wildcard_answer_response.rpl b/tests/testdata/nsec_wildcard_answer_response.rpl
new file mode 100644
index 0000000000000000000000000000000000000000..1db95bffe2d872d026caaa552f739644508c6a95
--- /dev/null
+++ b/tests/testdata/nsec_wildcard_answer_response.rpl
@@ -0,0 +1,202 @@
+; config options
+server:
+	trust-anchor: ". 3600 IN DS 17272 13 4 B87AD8C76DC2244E7AA57285057BF533F2E248CC8D7E1A071D8A3837A711A5EA705C4707E6E8911DA653BE1AE019927B"
+	val-override-date: "1442489440"
+
+stub-zone:
+	name: "."
+	stub-addr: 127.0.0.1 	# ns.
+CONFIG_END
+
+SCENARIO_BEGIN Test validation of NSEC wildcard answer response.
+
+; ns.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.1
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+.                       3600    IN      NS      ns.
+.                       3600    IN      RRSIG   NS 13 0 3600 20151014142315 20150914142315 17272 . aEIYUS4S8Hd7vAVYvHwFyV97lKx4xt2PgAUbM4A7JUXHkTJDHUQEDVQh LWGxK6e+AUeuq4qlDo4vSz3IedmOBQ==
+SECTION ADDITIONAL
+ns.                     3600    IN      A       127.0.0.1
+ns.                     3600    IN      RRSIG   A 13 1 3600 20151014142315 20150914142315 17272 . 27h0pFJyb5t/2cZsFjynp0TRIdUlQwPYcAwCer2UbXTiBBaD8n15hfh8 PFU0if8X0ikqHusz6rCNTx/aBraYdQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN DNSKEY
+SECTION ANSWER
+.                       3600    IN      DNSKEY  256 3 13 qKlBZ0TvdY8C8+7bTcdnQdrLZxEwvxEwlGmIOTd/ccL5Jiei1whNktoE /Qzo1lJ0cXfVssy4EVMaqEdzIa+pkA==
+.                       3600    IN      RRSIG   DNSKEY 13 0 3600 20151014142315 20150914142315 17272 . FaY+kslqSPIRZsk65z8SrROt7kfx+RGUEBGbVgLQxKruJxc9+MMrl4e4 +RefYIlwpecj4jXwb75RTbT0g7OGGg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+aaa.local.nsec.example. IN A
+SECTION AUTHORITY
+example.                3600    IN      NS      ns.example.
+example.                3600    IN      DS      11225 13 4 B4BDAB0B3751300BFB9D0D240649279B4BA0E67A308E1B0BFE2931D9 47F7FD71A2BD807D84CDE24286D955A35752484F
+example.                3600    IN      RRSIG   DS 13 1 3600 20151014143533 20150914143533 17272 . b0+fXKmsBBXkzf+Myr5eRsXWDvY75oMlr4Yi5j+3iF7cOviVGKz3Dw8u bfKW+OmyHiuTeL71gez/84P+vHEvHA==
+SECTION ADDITIONAL
+ns.example.             3600    IN      A       127.0.0.2
+ENTRY_END
+
+RANGE_END
+
+; ns.example.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.2
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+example. IN NS
+SECTION ANSWER
+example.                3600    IN      NS      ns.example.
+example.                3600    IN      RRSIG   NS 13 1 3600 20151014143225 20150914143225 11225 example. C6KOyVJzeRh/3KL9BxSVOVZN0RIyBhlBmmmnVEFT5qPUrn3m5FjcIBtI hi7cAl2FeY1rqstztvKAY6UOBE0kGQ==
+SECTION ADDITIONAL
+ns.example.             3600    IN      A       127.0.0.2
+ns.example.             3600    IN      RRSIG   A 13 2 3600 20151014143225 20150914143225 11225 example. fM/mwUOtyIbKTxgxaekZf5A8kV3qYIFADtvhcQi0TUh09nfkHQtUqhew zVBXCEtjKMnYFvNhWF6PyiirtOeM8w==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+example. IN DNSKEY
+SECTION ANSWER
+example.                3600    IN      DNSKEY  256 3 13 d9Qb4Tj90Y2cvdWcZfu45clfoLKqGbJn2vQKqZv07nc4FMf2oRkrNXtP fixVTLfbbWAFtbbFf3mhCNUsetRUVQ==
+example.                3600    IN      RRSIG   DNSKEY 13 1 3600 20151015124839 20150915124839 11225 example. 4DemFjvys9Gfq+gG1i8IB6GPBUw9lIv3F082JwW7O8tqNIn45n2z14gg ieeJTRhU9xXOVIfj6amITZWbjvGyFA==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+aaa.local.nsec.example. IN A
+SECTION AUTHORITY
+nsec.example.           3600    IN      NS      ns.nsec.example.
+nsec.example.           3600    IN      DS      54343 13 4 90ABD4FB9F053CF67F6D838DD2437FB16104B8BF127319706223004F 2ED72AF2872B4E507EB483A303BF60BF08C87364
+nsec.example.           3600    IN      RRSIG   DS 13 2 3600 20151015124611 20150915124611 11225 example. HYzlEdyYugggsEwUVyyY4XHFVUZZ8yiIh4vnuViGBQQJP+yryYh1aLyN ap2Q51nkmSG1fXDb2IySiAYuqUJyLw==
+SECTION ADDITIONAL
+ns.nsec.example.        3600    IN      A       127.0.0.3
+ENTRY_END
+
+RANGE_END
+
+; ns.nsec.example.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.3
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec.example. IN NS
+SECTION ANSWER
+nsec.example.           3600    IN      NS      ns.nsec.example.
+nsec.example.           3600    IN      RRSIG   NS 13 2 3600 20151015124917 20150915124917 54343 nsec.example. 6s75LEuylIKAxqAbcPmmnkOMC7jxF6cPZGW5EFbhOOeR63ENyh642GE1 71WtJc7Ta4Y/PsnAT+/dTv8NSTDCHQ==
+SECTION ADDITIONAL
+ns.nsec.example.        3600    IN      A       127.0.0.3
+ns.nsec.example.        3600    IN      RRSIG   A 13 3 3600 20151015124917 20150915124917 54343 nsec.example. oJpF87bjXR0DjIoNvEAo+Wu+p9jF+URX5lxi+g53OFCX1Q1lxqj5ujGd KOPsNAbKvTCsoFFW4tQyhCYJYD1HlQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec.example. IN DNSKEY
+SECTION ANSWER
+nsec.example.           3600    IN      DNSKEY  256 3 13 HA6nKf+X7/mYkmmRO8qS2tIKT0B60P7COAiRs25xKs/rAP+tDtGWkrkG NQx2D3ajccC9whjRaKz2JVS3ItTFQg==
+nsec.example.           3600    IN      RRSIG   DNSKEY 13 2 3600 20151015124917 20150915124917 54343 nsec.example. 965Mfxs1QtgxwzyhfxXyKyOZ9iT1DXpvypBBR10sLyjHe/w7cRhgcyev Cza6K+2jJwHJBmbknc3Qhi+1dd+AJw==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+aaa.local.nsec.example. IN A
+SECTION ANSWER
+aaa.local.nsec.example. 3600    IN      A       127.0.0.3
+aaa.local.nsec.example. 3600    IN      RRSIG   A 13 3 3600 20151017113004 20150917113004 54343 nsec.example. 5hYzdWDetMJ5h9nrbgFBOQliFc+HH7QAsS32CzpXGHd2rpbr3OvzNDqP bvvWD/9BPJ8nVFLnrgh+xfBqtYhUzA==
+SECTION AUTHORITY
+*.local.nsec.example.   3600    IN      NSEC    loop.nsec.example. A AAAA RRSIG NSEC
+*.local.nsec.example.   3600    IN      RRSIG   NSEC 13 3 3600 20151017113004 20150917113004 54343 nsec.example. iMTPQUvd9v3W5qOMGZaTHBwjDpnb14S3FwZ2B1ry4G5ZEQzNC/mLGoex XqY2zBLFhs37KKSrmmWQZMOonTcYNw==
+ENTRY_END
+
+; Missing NSEC covering the wildcard.
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+bbb.local.nsec.example. IN A
+SECTION ANSWER
+bbb.local.nsec.example. 3600    IN      A       127.0.0.3
+bbb.local.nsec.example. 3600    IN      RRSIG   A 13 3 3600 20151017113004 20150917113004 54343 nsec.example. 5hYzdWDetMJ5h9nrbgFBOQliFc+HH7QAsS32CzpXGHd2rpbr3OvzNDqP bvvWD/9BPJ8nVFLnrgh+xfBqtYhUzA==
+SECTION AUTHORITY
+ENTRY_END
+
+RANGE_END
+
+;STEP 0 TIME_PASSES ELAPSE 1000
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+aaa.local.nsec.example. IN A
+ENTRY_END
+
+; recursion happens here.
+STEP 2 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD NXDOMAIN
+SECTION QUESTION
+aaa.local.nsec.example. IN A
+SECTION ANSWER
+aaa.local.nsec.example. 3600    IN      A       127.0.0.3
+aaa.local.nsec.example. 3600    IN      RRSIG   A 13 3 3600 20151017113004 20150917113004 54343 nsec.example. 5hYzdWDetMJ5h9nrbgFBOQliFc+HH7QAsS32CzpXGHd2rpbr3OvzNDqP bvvWD/9BPJ8nVFLnrgh+xfBqtYhUzA==
+SECTION AUTHORITY
+*.local.nsec.example.   3600    IN      NSEC    loop.nsec.example. A AAAA RRSIG NSEC
+*.local.nsec.example.   3600    IN      RRSIG   NSEC 13 3 3600 20151017113004 20150917113004 54343 nsec.example. iMTPQUvd9v3W5qOMGZaTHBwjDpnb14S3FwZ2B1ry4G5ZEQzNC/mLGoex XqY2zBLFhs37KKSrmmWQZMOonTcYNw==
+ENTRY_END
+
+STEP 3 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+bbb.local.nsec.example. IN A
+ENTRY_END
+
+STEP 4 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD SERVFAIL
+SECTION QUESTION
+bbb.local.nsec.example. IN A
+ENTRY_END
+
+SCENARIO_END
diff --git a/tests/testdata/nsec_wildcard_no_data_response.rpl b/tests/testdata/nsec_wildcard_no_data_response.rpl
new file mode 100644
index 0000000000000000000000000000000000000000..43fcfb4c7ed142751c1a0770444fa5d3fa08f20e
--- /dev/null
+++ b/tests/testdata/nsec_wildcard_no_data_response.rpl
@@ -0,0 +1,211 @@
+; config options
+server:
+	trust-anchor: ". 3600 IN DS 17272 13 4 B87AD8C76DC2244E7AA57285057BF533F2E248CC8D7E1A071D8A3837A711A5EA705C4707E6E8911DA653BE1AE019927B"
+	val-override-date: "1442839270"
+
+stub-zone:
+	name: "."
+	stub-addr: 127.0.0.1 	# ns.
+CONFIG_END
+
+SCENARIO_BEGIN Test validation of NSEC3 name error responses.
+
+; ns.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.1
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+.                       3600    IN      NS      ns.
+.                       3600    IN      RRSIG   NS 13 0 3600 20151014142315 20150914142315 17272 . aEIYUS4S8Hd7vAVYvHwFyV97lKx4xt2PgAUbM4A7JUXHkTJDHUQEDVQh LWGxK6e+AUeuq4qlDo4vSz3IedmOBQ==
+SECTION ADDITIONAL
+ns.                     3600    IN      A       127.0.0.1
+ns.                     3600    IN      RRSIG   A 13 1 3600 20151014142315 20150914142315 17272 . 27h0pFJyb5t/2cZsFjynp0TRIdUlQwPYcAwCer2UbXTiBBaD8n15hfh8 PFU0if8X0ikqHusz6rCNTx/aBraYdQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+. IN DNSKEY
+SECTION ANSWER
+.                       3600    IN      DNSKEY  256 3 13 qKlBZ0TvdY8C8+7bTcdnQdrLZxEwvxEwlGmIOTd/ccL5Jiei1whNktoE /Qzo1lJ0cXfVssy4EVMaqEdzIa+pkA==
+.                       3600    IN      RRSIG   DNSKEY 13 0 3600 20151014142315 20150914142315 17272 . FaY+kslqSPIRZsk65z8SrROt7kfx+RGUEBGbVgLQxKruJxc9+MMrl4e4 +RefYIlwpecj4jXwb75RTbT0g7OGGg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+aaa.local.nsec.example. IN CNAME
+SECTION AUTHORITY
+example.                3600    IN      NS      ns.example.
+example.                3600    IN      DS      11225 13 4 B4BDAB0B3751300BFB9D0D240649279B4BA0E67A308E1B0BFE2931D9 47F7FD71A2BD807D84CDE24286D955A35752484F
+example.                3600    IN      RRSIG   DS 13 1 3600 20151014143533 20150914143533 17272 . b0+fXKmsBBXkzf+Myr5eRsXWDvY75oMlr4Yi5j+3iF7cOviVGKz3Dw8u bfKW+OmyHiuTeL71gez/84P+vHEvHA==
+SECTION ADDITIONAL
+ns.example.             3600    IN      A       127.0.0.2
+ENTRY_END
+
+RANGE_END
+
+; ns.example.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.2
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+example. IN NS
+SECTION ANSWER
+example.                3600    IN      NS      ns.example.
+example.                3600    IN      RRSIG   NS 13 1 3600 20151014143225 20150914143225 11225 example. C6KOyVJzeRh/3KL9BxSVOVZN0RIyBhlBmmmnVEFT5qPUrn3m5FjcIBtI hi7cAl2FeY1rqstztvKAY6UOBE0kGQ==
+SECTION ADDITIONAL
+ns.example.             3600    IN      A       127.0.0.2
+ns.example.             3600    IN      RRSIG   A 13 2 3600 20151014143225 20150914143225 11225 example. fM/mwUOtyIbKTxgxaekZf5A8kV3qYIFADtvhcQi0TUh09nfkHQtUqhew zVBXCEtjKMnYFvNhWF6PyiirtOeM8w==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+example. IN DNSKEY
+SECTION ANSWER
+example.                3600    IN      DNSKEY  256 3 13 d9Qb4Tj90Y2cvdWcZfu45clfoLKqGbJn2vQKqZv07nc4FMf2oRkrNXtP fixVTLfbbWAFtbbFf3mhCNUsetRUVQ==
+example.                3600    IN      RRSIG   DNSKEY 13 1 3600 20151015124839 20150915124839 11225 example. 4DemFjvys9Gfq+gG1i8IB6GPBUw9lIv3F082JwW7O8tqNIn45n2z14gg ieeJTRhU9xXOVIfj6amITZWbjvGyFA==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+aaa.local.nsec.example. IN CNAME
+SECTION AUTHORITY
+nsec.example.           3600    IN      NS      ns.nsec.example.
+nsec.example.           3600    IN      DS      54343 13 4 90ABD4FB9F053CF67F6D838DD2437FB16104B8BF127319706223004F 2ED72AF2872B4E507EB483A303BF60BF08C87364
+nsec.example.           3600    IN      RRSIG   DS 13 2 3600 20151015124611 20150915124611 11225 example. HYzlEdyYugggsEwUVyyY4XHFVUZZ8yiIh4vnuViGBQQJP+yryYh1aLyN ap2Q51nkmSG1fXDb2IySiAYuqUJyLw==
+SECTION ADDITIONAL
+ns.nsec.example.        3600    IN      A       127.0.0.3
+ENTRY_END
+
+RANGE_END
+
+; ns.nsec.example.
+RANGE_BEGIN 0 100
+	ADDRESS 127.0.0.3
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec.example. IN NS
+SECTION ANSWER
+nsec.example.           3600    IN      NS      ns.nsec.example.
+nsec.example.           3600    IN      RRSIG   NS 13 2 3600 20151015124917 20150915124917 54343 nsec.example. 6s75LEuylIKAxqAbcPmmnkOMC7jxF6cPZGW5EFbhOOeR63ENyh642GE1 71WtJc7Ta4Y/PsnAT+/dTv8NSTDCHQ==
+SECTION ADDITIONAL
+ns.nsec.example.        3600    IN      A       127.0.0.3
+ns.nsec.example.        3600    IN      RRSIG   A 13 3 3600 20151015124917 20150915124917 54343 nsec.example. oJpF87bjXR0DjIoNvEAo+Wu+p9jF+URX5lxi+g53OFCX1Q1lxqj5ujGd KOPsNAbKvTCsoFFW4tQyhCYJYD1HlQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+nsec.example. IN DNSKEY
+SECTION ANSWER
+nsec.example.           3600    IN      DNSKEY  256 3 13 HA6nKf+X7/mYkmmRO8qS2tIKT0B60P7COAiRs25xKs/rAP+tDtGWkrkG NQx2D3ajccC9whjRaKz2JVS3ItTFQg==
+nsec.example.           3600    IN      RRSIG   DNSKEY 13 2 3600 20151015124917 20150915124917 54343 nsec.example. 965Mfxs1QtgxwzyhfxXyKyOZ9iT1DXpvypBBR10sLyjHe/w7cRhgcyev Cza6K+2jJwHJBmbknc3Qhi+1dd+AJw==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+local.nsec.example. IN NS
+SECTION AUTHORITY
+nsec.example.           3600    IN      SOA     ns.nsec.example. root.nsec.example. 6 60 60 120 3600
+alias.nsec.example.     3600    IN      NSEC    *.local.nsec.example. CNAME RRSIG NSEC
+nsec.example.           3600    IN      RRSIG   SOA 13 2 3600 20151017113144 20150917113144 54343 nsec.example. /3orb3cezQbBCZsFP9rx6Col9AB2QxHQtzQ32BYe09MfN7YZxtTE/HZJ aSXGWD3D7sLBdEkg8TGP8JPQtbW2yQ==
+alias.nsec.example.     3600    IN      RRSIG   NSEC 13 3 3600 20151015124917 20150915124917 54343 nsec.example. isUjSpDPDEYmSdbWzIpU7+m/Xa9S00TruxYv68FwRK1JVlta9OkUXRDb e4UMH9Mz8HsntYIa5NK+uCJr+i+o/g==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+aaa.local.nsec.example. IN CNAME
+SECTION AUTHORITY
+nsec.example.           3600    IN      SOA     ns.nsec.example. root.nsec.example. 6 60 60 120 3600
+*.local.nsec.example.   3600    IN      NSEC    loop.nsec.example. A AAAA RRSIG NSEC
+nsec.example.           3600    IN      RRSIG   SOA 13 2 3600 20151017113144 20150917113144 54343 nsec.example. /3orb3cezQbBCZsFP9rx6Col9AB2QxHQtzQ32BYe09MfN7YZxtTE/HZJ aSXGWD3D7sLBdEkg8TGP8JPQtbW2yQ==
+*.local.nsec.example.   3600    IN      RRSIG   NSEC 13 3 3600 20151017113004 20150917113004 54343 nsec.example. iMTPQUvd9v3W5qOMGZaTHBwjDpnb14S3FwZ2B1ry4G5ZEQzNC/mLGoex XqY2zBLFhs37KKSrmmWQZMOonTcYNw==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+bbb.local.nsec.example. IN CNAME
+SECTION AUTHORITY
+nsec.example.           3600    IN      SOA     ns.nsec.example. root.nsec.example. 6 60 60 120 3600
+nsec.example.           3600    IN      RRSIG   SOA 13 2 3600 20151017113144 20150917113144 54343 nsec.example. /3orb3cezQbBCZsFP9rx6Col9AB2QxHQtzQ32BYe09MfN7YZxtTE/HZJ aSXGWD3D7sLBdEkg8TGP8JPQtbW2yQ==
+ENTRY_END
+
+RANGE_END
+
+;STEP 0 TIME_PASSES ELAPSE 1000
+
+STEP 1 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+aaa.local.nsec.example. IN CNAME
+ENTRY_END
+
+; recursion happens here.
+STEP 2 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD NOERROR
+SECTION QUESTION
+aaa.local.nsec.example. IN CNAME
+SECTION AUTHORITY
+nsec.example.           3600    IN      SOA     ns.nsec.example. root.nsec.example. 6 60 60 120 3600
+*.local.nsec.example.   3600    IN      NSEC    loop.nsec.example. A AAAA RRSIG NSEC
+nsec.example.           3600    IN      RRSIG   SOA 13 2 3600 20151017113144 20150917113144 54343 nsec.example. /3orb3cezQbBCZsFP9rx6Col9AB2QxHQtzQ32BYe09MfN7YZxtTE/HZJ aSXGWD3D7sLBdEkg8TGP8JPQtbW2yQ==
+*.local.nsec.example.   3600    IN      RRSIG   NSEC 13 3 3600 20151017113004 20150917113004 54343 nsec.example. iMTPQUvd9v3W5qOMGZaTHBwjDpnb14S3FwZ2B1ry4G5ZEQzNC/mLGoex XqY2zBLFhs37KKSrmmWQZMOonTcYNw==
+ENTRY_END
+
+STEP 3 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+bbb.local.nsec.example. IN CNAME
+ENTRY_END
+
+STEP 4 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA AD SERVFAIL
+SECTION QUESTION
+bbb.local.nsec.example. IN CNAME
+ENTRY_END
+
+SCENARIO_END
diff --git a/tests/testdata_notimpl/iter_dnsseclame_bug.rpl b/tests/testdata_notimpl/iter_dnsseclame_bug.rpl
index a22dc96bc55178172dfb98b2c21accc21eb47804..0210e2d9074c9c9bbd4dd09226a3cefe4cf0bd69 100644
--- a/tests/testdata_notimpl/iter_dnsseclame_bug.rpl
+++ b/tests/testdata_notimpl/iter_dnsseclame_bug.rpl
@@ -1,7 +1,7 @@
 ; config options
 server:
 	trust-anchor: "example.com.    3600    IN      DS      2854 3 1 46e4ffc6e9a4793b488954bd3f0cc6af0dfb201b"
-	val-override-date: "20070916134226"
+	val-override-date: "1189950146"
 
 stub-zone:
 	name: "."
diff --git a/tests/testdata_notimpl/iter_dnsseclame_ds.rpl b/tests/testdata_notimpl/iter_dnsseclame_ds.rpl
index 0e8405db94e99b6aa52fc656b632f01ebe60c429..8b91100a4068600ce31c731511b4f95093cd7eb9 100644
--- a/tests/testdata_notimpl/iter_dnsseclame_ds.rpl
+++ b/tests/testdata_notimpl/iter_dnsseclame_ds.rpl
@@ -1,7 +1,7 @@
 ; config options
 server:
 	trust-anchor: "example.com.    3600    IN      DS      2854 3 1 46e4ffc6e9a4793b488954bd3f0cc6af0dfb201b"
-	val-override-date: "20070916134226"
+	val-override-date: "1189950146"
 
 stub-zone:
 	name: "."
diff --git a/tests/testdata_notimpl/iter_dnsseclame_ds_ok.rpl b/tests/testdata_notimpl/iter_dnsseclame_ds_ok.rpl
index 0ff322cd42bcd8bdd122a9b5632fdd4bceca3f0d..8b7edebc297ad587718154ce04106052321e3f84 100644
--- a/tests/testdata_notimpl/iter_dnsseclame_ds_ok.rpl
+++ b/tests/testdata_notimpl/iter_dnsseclame_ds_ok.rpl
@@ -1,7 +1,7 @@
 ; config options
 server:
 	trust-anchor: "example.com.    3600    IN      DS      2854 3 1 46e4ffc6e9a4793b488954bd3f0cc6af0dfb201b"
-	val-override-date: "20070916134226"
+	val-override-date: "1189950146"
 	target-fetch-policy: "0 0 0 0 0"
 
 stub-zone:
diff --git a/tests/testdata_notimpl/iter_dnsseclame_ta.rpl b/tests/testdata_notimpl/iter_dnsseclame_ta.rpl
index 9472dcc1a1e6b18f8862b16e07621e88528fa9da..d17940159fdb50980737385eee5d1992d712004c 100644
--- a/tests/testdata_notimpl/iter_dnsseclame_ta.rpl
+++ b/tests/testdata_notimpl/iter_dnsseclame_ta.rpl
@@ -1,7 +1,7 @@
 ; config options
 server:
 	trust-anchor: "example.com.    3600    IN      DS      2854 3 1 46e4ffc6e9a4793b488954bd3f0cc6af0dfb201b"
-	val-override-date: "20070916134226"
+	val-override-date: "1189950146"
 
 stub-zone:
 	name: "."
diff --git a/tests/testdata_notimpl/iter_dnsseclame_ta_ok.rpl b/tests/testdata_notimpl/iter_dnsseclame_ta_ok.rpl
index e794b54fdaa86dce6a52ecca222cd2dda74e786d..d53377bebeb1816a48c26dfac3834ecb1ee39866 100644
--- a/tests/testdata_notimpl/iter_dnsseclame_ta_ok.rpl
+++ b/tests/testdata_notimpl/iter_dnsseclame_ta_ok.rpl
@@ -1,7 +1,7 @@
 ; config options
 server:
 	trust-anchor: "example.com.    3600    IN      DS      2854 3 1 46e4ffc6e9a4793b488954bd3f0cc6af0dfb201b"
-	val-override-date: "20070916134226"
+	val-override-date: "1189950146"
 	target-fetch-policy: "0 0 0 0 0"
 
 stub-zone:
diff --git a/tests/testdata_notimpl/iter_emptydp.rpl b/tests/testdata_notimpl/iter_emptydp.rpl
index 07dbed7c4c40ca26e45166fcd3c2d7f93d306dca..dc179865b981268499dd0129b0488dcd096439c4 100644
--- a/tests/testdata_notimpl/iter_emptydp.rpl
+++ b/tests/testdata_notimpl/iter_emptydp.rpl
@@ -2,7 +2,7 @@
 ; The island of trust is at example.com
 server:
 	trust-anchor: "example.com.    3600    IN      DS      2854 3 1 46e4ffc6e9a4793b488954bd3f0cc6af0dfb201b"
-	val-override-date: "20070916134226"
+	val-override-date: "1189950146"
 	target-fetch-policy: "3 2 1 0 0" # make sure it fetches for test
 
 stub-zone:
diff --git a/tests/testdata_notimpl/iter_emptydp_for_glue.rpl b/tests/testdata_notimpl/iter_emptydp_for_glue.rpl
index 59ccf9eb347541ee31f27ce16e7a31dfbe970640..d469bc2842f000adba374aab1a3a8c3f651b88ed 100644
--- a/tests/testdata_notimpl/iter_emptydp_for_glue.rpl
+++ b/tests/testdata_notimpl/iter_emptydp_for_glue.rpl
@@ -2,7 +2,7 @@
 ; The island of trust is at example.com
 server:
 	trust-anchor: "example.com.    3600    IN      DS      2854 3 1 46e4ffc6e9a4793b488954bd3f0cc6af0dfb201b"
-	val-override-date: "20070916134226"
+	val-override-date: "1189950146"
 	target-fetch-policy: "3 2 1 0 0" # make sure it fetches for test
 
 stub-zone:
diff --git a/tests/testdata/iter_pcdiff.rpl b/tests/testdata_notimpl/iter_pcdiff.rpl
similarity index 100%
rename from tests/testdata/iter_pcdiff.rpl
rename to tests/testdata_notimpl/iter_pcdiff.rpl
diff --git a/tests/testdata/iter_pcnamerec.rpl b/tests/testdata_notimpl/iter_pcnamerec.rpl
similarity index 100%
rename from tests/testdata/iter_pcnamerec.rpl
rename to tests/testdata_notimpl/iter_pcnamerec.rpl
diff --git a/tests/tests.mk b/tests/tests.mk
index 870f749ba21236f19aa81ae81696936bd522511c..b0fe33b9ad2889de43ebc2fb2656b4b75a866b9c 100644
--- a/tests/tests.mk
+++ b/tests/tests.mk
@@ -1,10 +1,10 @@
 # Preload libraries
 preload_PATH := $(abspath contrib/libfaketime/src)
 ifeq ($(PLATFORM),Darwin)
-	preload_LIBS := @DYLD_FORCE_FLAT_NAMESPACE=1 \
+	preload_LIBS := DYLD_FORCE_FLAT_NAMESPACE=1 \
 	                DYLD_LIBRARY_PATH="$(preload_PATH):${DYLD_LIBRARY_PATH}"
 else
-	preload_LIBS := @LD_LIBRARY_PATH="$(preload_PATH):${LD_LIBRARY_PATH}"
+	preload_LIBS := LD_LIBRARY_PATH="$(preload_PATH):${LD_LIBRARY_PATH}"
 endif
 
 # Unit tests
diff --git a/tests/unit.mk b/tests/unit.mk
index d944d5967799390103f4213614faafd8cdfadb47..6893c5b96c5d369d68762effaf6dd98c6cadece2 100644
--- a/tests/unit.mk
+++ b/tests/unit.mk
@@ -29,13 +29,13 @@ $(1)_SOURCES := tests/$(1).c
 $(1)_LIBS := $(tests_LIBS)
 $(1)_DEPEND := $(tests_DEPEND)
 $(call make_bin,$(1),tests)
-$(1)-run: $$($(1))
-	$(call preload_LIBS) $$<
-.PHONY: $(1)-run
+$(1): $$($(1))
+	@$$<
+.PHONY: $(1)
 endef
 
 # Targets
 $(foreach test,$(tests_BIN),$(eval $(call make_test,$(test))))
-check-unit: $(foreach test,$(tests_BIN),$(test)-run)
+check-unit: $(foreach test,$(tests_BIN),$(test))
 
 .PHONY: check-unit