]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
WIP checkpoint
authorVladimír Čunát <vladimir.cunat@nic.cz>
Thu, 3 May 2018 17:06:33 +0000 (19:06 +0200)
committerVladimír Čunát <vladimir.cunat@nic.cz>
Thu, 3 May 2018 17:06:45 +0000 (19:06 +0200)
contrib/base32hex.c
contrib/base32hex.h
lib/cache/api.c
lib/cache/impl.h
lib/cache/nsec1.c
lib/cache/nsec3.c

index 0fc7fd8692d009123751a4e81584aef5979af4e6..1da9fa47ff2e9953c11d17719cd541d28ebef0c9 100644 (file)
@@ -205,3 +205,85 @@ int32_t base32hex_decode(const uint8_t  *in,
 
        return (bin - out);
 }
+
+int32_t base32hex_encode(const uint8_t  *in,
+                         const uint32_t in_len,
+                         uint8_t        *out,
+                         const uint32_t out_len)
+{
+       // Checking inputs.
+       if (in == NULL || out == NULL) {
+               return -1;
+       }
+       if (in_len > MAX_BIN_DATA_LEN || out_len < ((in_len + 4) / 5) * 8) {
+               return -1;
+       }
+
+       uint8_t         rest_len = in_len % 5;
+       const uint8_t   *stop = in + in_len - rest_len;
+       uint8_t         *text = out;
+
+       // Encoding loop takes 5 bytes and creates 8 characters.
+       while (in < stop) {
+               text[0] = base32hex_enc[in[0] >> 3];
+               text[1] = base32hex_enc[(in[0] & 0x07) << 2 | in[1] >> 6];
+               text[2] = base32hex_enc[(in[1] & 0x3E) >> 1];
+               text[3] = base32hex_enc[(in[1] & 0x01) << 4 | in[2] >> 4];
+               text[4] = base32hex_enc[(in[2] & 0x0F) << 1 | in[3] >> 7];
+               text[5] = base32hex_enc[(in[3] & 0x7C) >> 2];
+               text[6] = base32hex_enc[(in[3] & 0x03) << 3 | in[4] >> 5];
+               text[7] = base32hex_enc[in[4] & 0x1F];
+               text += 8;
+               in += 5;
+       }
+
+       // Processing of padding, if any.
+       switch (rest_len) {
+       case 4:
+               text[0] = base32hex_enc[in[0] >> 3];
+               text[1] = base32hex_enc[(in[0] & 0x07) << 2 | in[1] >> 6];
+               text[2] = base32hex_enc[(in[1] & 0x3E) >> 1];
+               text[3] = base32hex_enc[(in[1] & 0x01) << 4 | in[2] >> 4];
+               text[4] = base32hex_enc[(in[2] & 0x0F) << 1 | in[3] >> 7];
+               text[5] = base32hex_enc[(in[3] & 0x7C) >> 2];
+               text[6] = base32hex_enc[(in[3] & 0x03) << 3];
+               text[7] = base32hex_pad;
+               text += 8;
+               break;
+       case 3:
+               text[0] = base32hex_enc[in[0] >> 3];
+               text[1] = base32hex_enc[(in[0] & 0x07) << 2 | in[1] >> 6];
+               text[2] = base32hex_enc[(in[1] & 0x3E) >> 1];
+               text[3] = base32hex_enc[(in[1] & 0x01) << 4 | in[2] >> 4];
+               text[4] = base32hex_enc[(in[2] & 0x0F) << 1];
+               text[5] = base32hex_pad;
+               text[6] = base32hex_pad;
+               text[7] = base32hex_pad;
+               text += 8;
+               break;
+       case 2:
+               text[0] = base32hex_enc[in[0] >> 3];
+               text[1] = base32hex_enc[(in[0] & 0x07) << 2 | in[1] >> 6];
+               text[2] = base32hex_enc[(in[1] & 0x3E) >> 1];
+               text[3] = base32hex_enc[(in[1] & 0x01) << 4];
+               text[4] = base32hex_pad;
+               text[5] = base32hex_pad;
+               text[6] = base32hex_pad;
+               text[7] = base32hex_pad;
+               text += 8;
+               break;
+       case 1:
+               text[0] = base32hex_enc[in[0] >> 3];
+               text[1] = base32hex_enc[(in[0] & 0x07) << 2];
+               text[2] = base32hex_pad;
+               text[3] = base32hex_pad;
+               text[4] = base32hex_pad;
+               text[5] = base32hex_pad;
+               text[6] = base32hex_pad;
+               text[7] = base32hex_pad;
+               text += 8;
+               break;
+       }
+
+       return (text - out);
+}
index ca5835be6d193cc6a8406857c9f7deb5d868f2c6..ed4e723eaa20d4f38bee8647bb0dcb185c1fa7a6 100644 (file)
@@ -49,4 +49,24 @@ int32_t base32hex_decode(const uint8_t  *in,
                          uint8_t        *out,
                          const uint32_t out_len);
 
+
+/*!
+ * \brief Encodes binary data using Base32hex.
+ *
+ * \note Output data buffer contains Base32hex text string which isn't
+ *       terminated with '\0'!
+ *
+ * \param in           Input binary data.
+ * \param in_len       Length of input data.
+ * \param out          Output data buffer.
+ * \param out_len      Size of output buffer.
+ *
+ * \retval >=0         length of output string.
+ * \retval <0          if error.
+ */
+int32_t base32hex_encode(const uint8_t  *in,
+                         const uint32_t in_len,
+                         uint8_t        *out,
+                         const uint32_t out_len);
+
 /*! @} */
index fbb0f32c04cbf6c15087a55f43315be084807c18..60c36aea2e51bfb38ec950de2d3181be9e04141e 100644 (file)
@@ -781,8 +781,14 @@ static ssize_t stash_rrset(struct kr_cache *cache, const struct kr_query *qry, c
        struct key k_storage, *k = &k_storage;
        knot_db_val_t key;
        switch (rr->type) {
-       case KNOT_RRTYPE_NSEC:
        case KNOT_RRTYPE_NSEC3:
+               if (rr->rrs.rr_count != 1
+                   || (KNOT_NSEC3_FLAG_OPT_OUT & knot_nsec3_flags(&rr->rrs, 0))) {
+                       /* Skip "suspicious" or opt-out NSEC3 sets. */
+                       return kr_ok();
+               }
+               /* fall through */
+       case KNOT_RRTYPE_NSEC:
                if (!kr_rank_test(rank, KR_RANK_SECURE)) {
                        /* Skip any NSEC*s that aren't validated. */
                        return kr_ok();
@@ -805,7 +811,7 @@ static ssize_t stash_rrset(struct kr_cache *cache, const struct kr_query *qry, c
                assert(rr->type == KNOT_RRTYPE_NSEC3);
                const knot_rdata_t *np_data = knot_rdata_data(rr->rrs.data);
                const int np_dlen = nsec_p_rdlen(np_data);
-               key = key_NSEC3(k, encloser, nsec_p_hash(np_data));
+               key = key_NSEC3(k, encloser, nsec_p_mkHash(np_data));
                if (npp && !*npp) {
                        *npp = mm_alloc(&qry->request->pool, np_dlen);
                        if (!*npp) {
index 9d438f1f3de008a7e04bca7406bef4ce2c46893c..c79ec992b63361cd631e5850c2308e628ffaa214 100644 (file)
@@ -116,11 +116,29 @@ struct key {
        uint8_t buf[KR_CACHE_KEY_MAXLEN];
 };
 
+/** Hash of NSEC3 parameters, used as a tag to separate different chains for same zone. */
+typedef uint32_t nsec_p_hash_t;
+static inline nsec_p_hash_t nsec_p_mkHash(const uint8_t *nsec_p)
+{
+       assert(nsec_p && !(KNOT_NSEC3_FLAG_OPT_OUT & nsec_p[1]));
+       return hash((const char *)nsec_p, nsec_p_rdlen(nsec_p));
+}
 static inline size_t key_nwz_off(const struct key *k)
 {
-       /* CACHE_KEY_DEF: zone name lf + 0 '1' + name within zone */
+       /* CACHE_KEY_DEF: zone name lf + 0 ('1' or '3').
+        * NSEC '1' case continues just with the name within zone. */
        return k->zlf_len + 2;
 }
+static inline size_t key_nsec3_hash_off(const struct key *k)
+{
+       /* CACHE_KEY_DEF NSEC3: tag (nsec_p_hash_t) + 20 bytes NSEC3 name hash) */
+       return key_nwz_off(k) + sizeof(nsec_p_hash_t);
+}
+/** Hash is always SHA1; I see no plans to standardize anything else.
+ * https://www.iana.org/assignments/dnssec-nsec3-parameters/dnssec-nsec3-parameters.xhtml#dnssec-nsec3-parameters-3
+ */
+static const int NSEC3_HASH_LEN = 20,
+                NSEC3_HASH_TXT_LEN = 32;
 
 /** Finish constructing string key for for exact search.
  * It's assumed that kr_dname_lf(k->buf, owner, *) had been ran.
@@ -233,7 +251,7 @@ struct answer {
 enum {
        AR_ANSWER = 0,  /**< Positive answer record.  It might be wildcard-expanded. */
        AR_SOA,         /**< SOA record. */
-       AR_NSEC,        /**< NSEC* covering the SNAME. */
+       AR_NSEC,        /**< NSEC* covering the SNAME (next closer name in NSEC3 case). */
        AR_WILD,        /**< NSEC* covering or matching the source of synthesis. */
        AR_CPE,         /**< NSEC3 matching the closest provable encloser. */
 };
@@ -289,13 +307,6 @@ int nsec1_src_synth(struct key *k, struct answer *ans, const knot_dname_t *clenc
 
 /* NSEC3 stuff.  Implementation in ./nsec3.c */
 
-typedef uint32_t nsec_p_hash_t;
-/** \note We assume it's not opt-out. */
-static inline nsec_p_hash_t nsec_p_hash(const uint8_t *nsec_p)
-{
-       assert(nsec_p);
-       return hash((const char *)nsec_p, nsec_p_rdlen(nsec_p));
-}
 
 
 /** Construct a string key for for NSEC3 predecessor-search, from an NSEC3 name.
@@ -322,3 +333,22 @@ int nsec3_src_synth(struct key *k, struct answer *ans, const knot_dname_t *clenc
 /** Shorthand for operations on cache backend */
 #define cache_op(cache, op, ...) (cache)->api->op((cache)->db, ## __VA_ARGS__)
 
+
+
+/** Consistency check, ATM common for NSEC and NSEC3. */
+static inline struct entry_h * entry_h_consistent_NSEC(knot_db_val_t data)
+{
+       /* ATM it's enough to just extend the checks for exact entries. */
+       const struct entry_h *eh = entry_h_consistent(data, KNOT_RRTYPE_NSEC);
+       bool ok = eh != NULL;
+       ok = ok && !eh->is_packet && !eh->has_optout;
+       return ok ? /*const-cast*/(struct entry_h *)eh : NULL;
+}
+
+static inline uint16_t get_uint16(const void *address)
+{
+       uint16_t tmp;
+       memcpy(&tmp, address, sizeof(tmp));
+       return tmp;
+}
+
index 8096d0cccd831fbe50a73e67b4cb2c08fc7ef672..ec363f7b38da87fe30638233a5287ae434e5eb95 100644 (file)
@@ -26,7 +26,6 @@
 /** Reconstruct a name into a buffer (assuming length at least KNOT_DNAME_MAXLEN). */
 static int dname_wire_reconstruct(knot_dname_t *buf, const struct key *k,
                 knot_db_val_t kwz)
-/* TODO: probably move to a place shared with with NSEC3, perhaps with key_NSEC* */
 {
        /* Reconstruct from key: first the ending, then zone name. */
        int ret = knot_dname_lf2wire(buf, kwz.len, kwz.data);
@@ -127,14 +126,6 @@ static int kwz_between(knot_db_val_t k1, knot_db_val_t k2, knot_db_val_t k4)
        }
 }
 
-static struct entry_h * entry_h_consistent_NSEC(knot_db_val_t data)
-{
-       /* ATM it's enough to just extend the checks for exact entries. */
-       const struct entry_h *eh = entry_h_consistent(data, KNOT_RRTYPE_NSEC);
-       bool ok = eh != NULL;
-       ok = ok && !(eh->is_packet || eh->has_optout);
-       return ok ? /*const-cast*/(struct entry_h *)eh : NULL;
-}
 
 /** NSEC1 range search.
  *
@@ -218,7 +209,7 @@ static const char * find_leq_NSEC1(struct kr_cache *cache, const struct kr_query
        /* it's *full* name ATM */
        const knot_dname_t *next = eh->data + KR_CACHE_RR_COUNT_SIZE
                                 + 2 /* RDLENGTH from rfc1034 */;
-       if (!eh->data[0]) {
+       if (KR_CACHE_RR_COUNT_SIZE != 2 || get_uint16(eh->data) == 0) {
                assert(false);
                return "ERROR";
                /* TODO: more checks?  Also, `next` computation is kinda messy. */
@@ -296,8 +287,6 @@ int nsec1_encloser(struct key *k, struct answer *ans,
                return ESKIP;
        }
 
-       const struct entry_h *nsec_eh = val.data;
-       const void *nsec_eh_bound = val.data + val.len;
 
        /* Get owner name of the record. */
        const knot_dname_t *owner;
@@ -311,6 +300,8 @@ int nsec1_encloser(struct key *k, struct answer *ans,
        }
        /* Basic checks OK -> materialize data. */
        {
+               const struct entry_h *nsec_eh = val.data;
+               const void *nsec_eh_bound = val.data + val.len;
                int ret = entry2answer(ans, AR_NSEC, nsec_eh, nsec_eh_bound,
                                        owner, KNOT_RRTYPE_NSEC, new_ttl);
                if (ret) return kr_error(ret);
index ead68828b4b326858d334e18f7ac1d3b1219ec35..68cf026883a317328898ad4823938d146bf6a9ad 100644 (file)
@@ -21,6 +21,9 @@
 #include "lib/cache/impl.h"
 
 #include "contrib/base32hex.h"
+#include "lib/dnssec/nsec.h"
+#include "lib/layer/iterate.h"
+
 #include <dnssec/error.h>
 #include <dnssec/nsec.h>
 
@@ -39,7 +42,7 @@ static knot_db_val_t key_NSEC3_common(struct key *k, const knot_dname_t *zname,
        }
 
        /* CACHE_KEY_DEF: key == zone's dname_lf + '\0' + '3' + nsec_p hash (4B)
-        *                      + NSEC3 hash (20B binary!)
+        *                      + NSEC3 hash (20B == NSEC3_HASH_LEN binary!)
         * LATER(optim.) nsec_p hash: perhaps 2B would give a sufficient probability
         * of avoiding collisions.
         */
@@ -62,7 +65,7 @@ knot_db_val_t key_NSEC3(struct key *k, const knot_dname_t *nsec3_name,
        if (!val.data) return val;
        int len = base32hex_decode(nsec3_name + 1, nsec3_name[0], val.data + val.len,
                                   KR_CACHE_KEY_MAXLEN - val.len);
-       if (len != 20) {
+       if (len != NSEC3_HASH_LEN) {
                assert(false); // FIXME: just debug, possible bogus input in real life
                return VAL_EMPTY;
        }
@@ -74,10 +77,10 @@ knot_db_val_t key_NSEC3(struct key *k, const knot_dname_t *nsec3_name,
  * \note k->zlf_len and k->zname are assumed to have been correctly set */
 static knot_db_val_t key_NSEC3_name(struct key *k, const knot_dname_t *name,
                const bool add_wildcard,
-               const nsec_p_hash_t nsec_p_hash, const uint8_t *nsec_p)
+               const nsec_p_hash_t nsec_p_hash, const dnssec_nsec3_params_t *nsec3_p)
 {
        knot_db_val_t val = key_NSEC3_common(k, k->zname, nsec_p_hash);
-       const bool ok = val.data && nsec_p;
+       const bool ok = val.data && nsec3_p;
        if (!ok) return VAL_EMPTY;
 
        /* Make `name` point to correctly wildcarded owner name. */
@@ -93,15 +96,6 @@ static knot_db_val_t key_NSEC3_name(struct key *k, const knot_dname_t *name,
                name_len = knot_dname_size(name);
        }
        /* Append the NSEC3 hash. */
-       dnssec_nsec3_params_t params;
-       {
-               const dnssec_binary_t rdata = {
-                       .size = nsec_p_rdlen(nsec_p),
-                       .data = (uint8_t *)/*const-cast*/nsec_p,
-               };
-               int ret = dnssec_nsec3_params_from_rdata(&params, &rdata);
-               if (ret != DNSSEC_EOK) return VAL_EMPTY;
-       }
        const dnssec_binary_t dname = {
                .size = knot_dname_size(name),
                .data = (uint8_t *)/*const-cast*/name,
@@ -111,37 +105,271 @@ static knot_db_val_t key_NSEC3_name(struct key *k, const knot_dname_t *name,
                .data = val.data + val.len,
        };
                /* FIXME: vv this requires a patched libdnssec - tries to realloc() */
-       int ret = dnssec_nsec3_hash(&dname, &params, &hash);
+       int ret = dnssec_nsec3_hash(&dname, nsec3_p, &hash);
        if (ret != DNSSEC_EOK) return VAL_EMPTY;
        val.len += hash.size;
        return val;
 }
 
+/** Return h1 < h2, semantically on NSEC3 hashes. */
+static inline bool nsec3_hash_ordered(const uint8_t *h1, const uint8_t *h2)
+{
+       return memcmp(h1, h2, NSEC3_HASH_LEN) < 0;
+}
+
+/** NSEC3 range search.
+ *
+ * \param key Pass output of key_NSEC3(k, ...)
+ * \param value[out] The raw data of the NSEC cache record (optional; consistency checked).
+ * \param exact_match[out] Whether the key was matched exactly or just covered (optional).
+ * \param hash_low[out] Output the low end hash of covering NSEC3, pointing within DB (optional).
+ * \param new_ttl[out] New TTL of the NSEC3 (optional).
+ * \return Error message or NULL.
+ * \note The function itself does *no* bitmap checks, e.g. RFC 6840 sec. 4.
+ */
+static const char * find_leq_NSEC3(struct kr_cache *cache, const struct kr_query *qry,
+                       const knot_db_val_t key, const struct key *k, knot_db_val_t *value,
+                       bool *exact_match, const uint8_t **hash_low,
+                       uint32_t *new_ttl)
+{
+       /* Do the cache operation. */
+       const size_t hash_off = key_nsec3_hash_off(k);
+       if (!key.data || key.len < hash_off) {
+               assert(false);
+               return "range search ERROR";
+       }
+       knot_db_val_t key_found = key;
+       knot_db_val_t val = { NULL, 0 };
+       int ret = cache_op(cache, read_leq, &key_found, &val);
+               /* ^^ LATER(optim.): incrementing key and doing less-than search
+                * would probably be slightly more efficient with LMDB,
+                * but the code complexity would grow considerably. */
+       if (ret < 0) {
+               if (ret == kr_error(ENOENT)) {
+                       return "range search miss";
+               } else {
+                       assert(false);
+                       return "range search ERROR";
+               }
+       }
+       if (value) {
+               *value = val;
+       }
+       /* Check consistency, TTL, rank. */
+       const bool is_exact = (ret == 0);
+       if (exact_match) {
+               *exact_match = is_exact;
+       }
+       const struct entry_h *eh = entry_h_consistent_NSEC(val);
+       if (!eh) {
+               /* This might be just finding something else than NSEC3 entry,
+                * in case we searched before the very first one in the zone. */
+               return "range search found inconsistent entry";
+       }
+       /* FIXME(stale): passing just zone name instead of owner, as we don't
+        * have it reconstructed at this point. */
+       int32_t new_ttl_ = get_new_ttl(eh, qry, k->zname, KNOT_RRTYPE_NSEC3, qry->timestamp.tv_sec);
+       if (new_ttl_ < 0 || !kr_rank_test_noassert(eh->rank, KR_RANK_SECURE)) {
+               return "range search found stale or insecure entry";
+               /* TODO: remove the stale record *and* retry,
+                * in case we haven't run off.  Perhaps start by in_zone check. */
+       }
+       if (new_ttl) {
+               *new_ttl = new_ttl_;
+       }
+       if (hash_low) {
+               *hash_low = key_found.data + hash_off;
+       }
+       if (is_exact) {
+               /* Nothing else to do. */
+               return NULL;
+       }
+       /* The NSEC3 starts strictly before our target name;
+        * now check that it still belongs into that zone and chain. */
+       const bool same_chain = key_found.len == hash_off + NSEC3_HASH_LEN
+               /* CACHE_KEY_DEF */
+               && memcmp(key.data, key_found.data, hash_off) == 0;
+               /* FIXME: just probabilistic; also compare the nsec parameters exactly! */
+       if (!same_chain) {
+               return "range search miss (!same_chain)";
+       }
+       /* We know it starts before sname, so let's check the other end.
+        * A. find the next hash and check its length. */
+       const uint8_t *next_rdata = eh->data + KR_CACHE_RR_COUNT_SIZE
+                                 + 2 /* RDLENGTH from rfc1034 */;
+       if (KR_CACHE_RR_COUNT_SIZE != 2 || get_uint16(eh->data) == 0) {
+               assert(false);
+               return "ERROR";
+               /* TODO: more checks?  Also, `next` computation is kinda messy. */
+       }
+       const uint8_t *hash_next = next_rdata + nsec_p_rdlen(next_rdata)
+                                + sizeof(uint16_t) /* hash length from rfc5155 */;
+       if (get_uint16(hash_next - sizeof(uint16_t)) != NSEC3_HASH_LEN) {
+               assert(false); // FIXME: just debug, possible bogus input in real life
+               return "unexpected next hash length";
+       }
+       /* B. do the actual range check. */
+       const uint8_t * const hash_searched = key.data + hash_off;
+       bool covers = /* we know for sure that the low end is before the searched name */
+               nsec3_hash_ordered(hash_searched, hash_next)
+               /* and the wrap-around case */
+               || nsec3_hash_ordered(hash_next, key_found.data + hash_off);
+       if (!covers) {
+               return "range search miss (!covers)";
+       }
+       return NULL;
+}
+
+static int dname_wire_reconstruct(knot_dname_t *buf, const knot_dname_t *zname,
+                                 const uint8_t *hash_low)
+{
+       int len = base32hex_encode(hash_low, NSEC3_HASH_LEN, buf + 1, KNOT_DNAME_MAXLEN);
+       if (len != NSEC3_HASH_TXT_LEN) {
+               assert(false);
+               return kr_error(EINVAL);
+       }
+       buf[0] = len;
+       return knot_dname_to_wire(buf + 1 + len, zname, KNOT_DNAME_MAXLEN - 1 - len);
+}
+
+static void nsec3_hash2text(const knot_dname_t *owner, char *text)
+{
+       assert(owner[0] == NSEC3_HASH_TXT_LEN);
+       memcpy(text, owner + 1, MIN(owner[0], NSEC3_HASH_TXT_LEN));
+       text[NSEC3_HASH_TXT_LEN] = '\0';
+}
+
 int nsec3_encloser(struct key *k, struct answer *ans,
                   const int sname_labels, int *clencl_labels,
                   knot_db_val_t *cover_low_kwz, knot_db_val_t *cover_hi_kwz,
                   const struct kr_query *qry, struct kr_cache *cache)
 {
+       static const int ESKIP = ABS(ENOENT);
        /* Basic sanity check. */
-       const bool ok = k && ans && clencl_labels && cover_low_kwz && cover_hi_kwz
+       const bool ok = k && k->zname && ans && clencl_labels
+                       && cover_low_kwz && cover_hi_kwz
                        && qry && cache;
        if (!ok) {
                assert(!EINVAL);
                return kr_error(EINVAL);
        }
+
        // FIXME get *nsec_p - possibly just add to the parameter list
+       const uint8_t *nsec_p = NULL;
+       const nsec_p_hash_t nsec_p_hash = nsec_p_mkHash(nsec_p);
+       /* Convert NSEC3 params to another format. */
+       dnssec_nsec3_params_t nsec3_p;
+       {
+               const dnssec_binary_t rdata = {
+                       .size = nsec_p_rdlen(nsec_p),
+                       .data = (uint8_t *)/*const-cast*/nsec_p,
+               };
+               int ret = dnssec_nsec3_params_from_rdata(&nsec3_p, &rdata);
+               if (ret != DNSSEC_EOK) return kr_error(ret);
+       }
 
        /*** Find and cover the next closer name - cycle: name starting at sname,
-        * proceeding while longer than zname, shortening by one label on step.
-        * Each iteration: */
-               /*** 1. compute NSEC3 hash of the name with nsec_p. */
-               /*** 2. find a previous-or-equal NSEC3 in cache covering the name,
+        * proceeding while longer than zname, shortening by one label on step. */
+       const int zname_labels = knot_dname_labels(k->zname, NULL);
+       const knot_dname_t *name = qry->sname;
+       for (int name_labels = sname_labels; name_labels > zname_labels;
+                                       --name_labels, name += 1 + name[0]) {
+               /*** Find a previous-or-equal NSEC3 in cache covering the name,
                 * checking TTL etc.
                 * Exit if match is found (may have NODATA proof if the first iteration),
                 * break if cover is found. */
+               const knot_db_val_t key =
+                       key_NSEC3_name(k, name, false, nsec_p_hash, &nsec3_p);
+               if (!key.data) continue;
+               knot_db_val_t val = { NULL, 0 };
+               bool exact_match;
+               uint32_t new_ttl;
+               const uint8_t *hash_low;
+               const char *err = find_leq_NSEC3(cache, qry, key, k, &val,
+                                                &exact_match, &hash_low, &new_ttl);
+               if (err) {
+                       WITH_VERBOSE(qry) {
+                               auto_free char *name_str = kr_dname_text(name);
+                               VERBOSE_MSG(qry, "=> NSEC3 encloser error for %s: %s\n",
+                                               name_str, err);
+                       }
+                       continue;
+               }
+               if (exact_match && name_labels != sname_labels) {
+                       /* This name exists (checked rank and TTL), so we do not
+                        * continue trying to prove non-existence above this name. */
+                       VERBOSE_MSG(qry,
+                               "=> NSEC3 encloser: only found existence of an ancestor\n");
+                       return ESKIP;
+               }
+
+               /* FIXME XXX: rethink delegation/ancestor checks */
+
+               /* Get owner name of the record. */
+               const knot_dname_t *owner;
+               knot_dname_t owner_buf[KNOT_DNAME_MAXLEN];
+               if (exact_match) {
+                       owner = name;
+               } else {
+                       int ret = dname_wire_reconstruct(owner_buf, k->zname, hash_low);
+                       if (unlikely(ret)) {
+                               assert(false); // FIXME: just debug, possible long zname in real life
+                               continue;
+                       }
+                       owner = owner_buf;
+               }
+               /* Basic checks OK -> materialize data. */
+               {
+                       const struct entry_h *nsec_eh = val.data;
+                       const void *nsec_eh_bound = val.data + val.len;
+                       int ret = entry2answer(ans, AR_NSEC, nsec_eh, nsec_eh_bound,
+                                               owner, KNOT_RRTYPE_NSEC3, new_ttl);
+                       if (ret) return kr_error(ret);
+               }
+               /* Final checks, split for matching vs. covering our name. */
+               const knot_rrset_t *nsec_rr = ans->rrsets[AR_NSEC].set.rr;
+               uint8_t *bm = NULL;
+               uint16_t bm_size = 0;
+               knot_nsec_bitmap(&nsec_rr->rrs, &bm, &bm_size);
+               if (exact_match) {
+                       assert(name_labels == sname_labels);
+                       if (kr_nsec_bitmap_nodata_check(bm, bm_size, qry->stype,
+                                                       nsec_rr->owner) != 0) {
+                               assert(bm);
+                               VERBOSE_MSG(qry,
+                                       "=> NSEC3 sname: match but failed type check\n");
+                               return ESKIP;
+                       }
+                       /* NODATA proven; just need to add SOA+RRSIG later */
+                       VERBOSE_MSG(qry,
+                               "=> NSEC3 sname: match proved NODATA, new TTL %d\n",
+                               new_ttl);
+                       ans->rcode = PKT_NODATA;
+                       return kr_ok();
+               } /* else */
+
+               /* Inexact match.  First check if name is delegated by that NSEC3. */
+               if (kr_nsec_children_in_zone_check(bm, bm_size) != 0
+                   && knot_dname_matched_labels()) {
+                       VERBOSE_MSG(qry,
+                               "=> NSEC3 sname: covered but delegated (or error)\n");
+                       return ESKIP;
+               }
+
+               /*** One more step but searching for match this time
+                * - that's the closest (provable) encloser. */
+
+               /* Non-existence proven *except* for wildcards. */
+               WITH_VERBOSE(qry) {
+                       char hash_low_txt[NSEC3_HASH_TXT_LEN + 1];
+                       nsec3_hash2text(owner, hash_low_txt);
+                       VERBOSE_MSG(qry,
+                               "=> NSEC3 sname: covered by: %s -> TODO, new TTL %d\n",
+                               hash_low_txt, new_ttl);
+               }
+       }
+
 
-       /*** One more step but searching for match this time
-        * - that's the closest (provable) encloser. */
 
        //assert(false);
        return -ENOSYS;