untrusted comment: verify with openbsd-74-base.pub
RWRoyQmAD08ajZlu6KWgRmQMDfX0+KI04ocNAFbtzzWw7R3EAoo2gfys8Yz8NgNIjqvQkly4v3rEu2tvEiDMiNtfzrzTdjxNSQM=

OpenBSD 7.4 errata 013, February 13, 2024:

DNSSEC protocol vulnerabilities have been discovered that render
various DNSSEC validators victims of Denial Of Service while trying
to validate specially crafted DNSSEC responses.
Fix CVE-2023-50387 and CVE-2023-50868 in unwind(8) and unbound(8).

Apply by doing:
   signify -Vep /etc/signify/openbsd-74-base.pub -x 013_unbound.patch.sig \
       -m - | (cd /usr/src && patch -p0)

And then rebuild and install unwind and unbound:
   cd /usr/src/sbin/unwind
   make obj
   make
   make install
   cd /usr/src/usr.sbin/unbound
   make -f Makefile.bsd-wrapper obj
   make -f Makefile.bsd-wrapper
   make -f Makefile.bsd-wrapper install

Index: sbin/unwind/libunbound/services/authzone.c
===================================================================
RCS file: /cvs/src/sbin/unwind/libunbound/services/authzone.c,v
diff -u -p -r1.16 authzone.c
--- sbin/unwind/libunbound/services/authzone.c  5 Sep 2023 15:44:01 -0000       1.16
+++ sbin/unwind/libunbound/services/authzone.c  12 Feb 2024 21:08:02 -0000
@@ -7771,6 +7771,7 @@ static int zonemd_dnssec_verify_rrset(st
       enum sec_status sec;
       struct val_env* ve;
       int m;
+       int verified = 0;
       m = modstack_find(mods, "validator");
       if(m == -1) {
               auth_zone_log(z->name, VERB_ALGO, "zonemd dnssec verify: have "
@@ -7794,7 +7795,7 @@ static int zonemd_dnssec_verify_rrset(st
                       "zonemd: verify %s RRset with DNSKEY", typestr);
       }
       sec = dnskeyset_verify_rrset(env, ve, &pk, dnskey, sigalg, why_bogus, NULL,
-               LDNS_SECTION_ANSWER, NULL);
+               LDNS_SECTION_ANSWER, NULL, &verified);
       if(sec == sec_status_secure) {
               return 1;
       }
Index: sbin/unwind/libunbound/services/cache/dns.c
===================================================================
RCS file: /cvs/src/sbin/unwind/libunbound/services/cache/dns.c,v
diff -u -p -r1.10 dns.c
--- sbin/unwind/libunbound/services/cache/dns.c 5 Sep 2023 15:44:02 -0000       1.10
+++ sbin/unwind/libunbound/services/cache/dns.c 12 Feb 2024 21:08:02 -0000
@@ -690,6 +690,28 @@ tomsg(struct module_env* env, struct que
       return msg;
}

+struct dns_msg*
+dns_msg_deepcopy_region(struct dns_msg* origin, struct regional* region)
+{
+       size_t i;
+       struct dns_msg* res = NULL;
+       res = gen_dns_msg(region, &origin->qinfo, origin->rep->rrset_count);
+       if(!res) return NULL;
+       *res->rep = *origin->rep;
+       if(origin->rep->reason_bogus_str) {
+               res->rep->reason_bogus_str = regional_strdup(region,
+                       origin->rep->reason_bogus_str);
+       }
+       for(i=0; i<res->rep->rrset_count; i++) {
+               res->rep->rrsets[i] = packed_rrset_copy_region(
+                       origin->rep->rrsets[i], region, 0);
+               if(!res->rep->rrsets[i]) {
+                       return NULL;
+               }
+       }
+       return res;
+}
+
/** synthesize RRset-only response from cached RRset item */
static struct dns_msg*
rrset_msg(struct ub_packed_rrset_key* rrset, struct regional* region,
Index: sbin/unwind/libunbound/services/cache/dns.h
===================================================================
RCS file: /cvs/src/sbin/unwind/libunbound/services/cache/dns.h,v
diff -u -p -r1.4 dns.h
--- sbin/unwind/libunbound/services/cache/dns.h 30 Aug 2022 05:46:51 -0000      1.4
+++ sbin/unwind/libunbound/services/cache/dns.h 12 Feb 2024 21:08:02 -0000
@@ -164,6 +164,15 @@ struct dns_msg* tomsg(struct module_env*
       struct reply_info* r, struct regional* region, time_t now,
       int allow_expired, struct regional* scratch);

+/**
+ * Deep copy a dns_msg to a region.
+ * @param origin: the dns_msg to copy.
+ * @param region: the region to copy all the data to.
+ * @return the new dns_msg or NULL on malloc error.
+ */
+struct dns_msg* dns_msg_deepcopy_region(struct dns_msg* origin,
+       struct regional* region);
+
/**
 * Find cached message
 * @param env: module environment with the DNS cache.
Index: sbin/unwind/libunbound/util/fptr_wlist.c
===================================================================
RCS file: /cvs/src/sbin/unwind/libunbound/util/fptr_wlist.c,v
diff -u -p -r1.11 fptr_wlist.c
--- sbin/unwind/libunbound/util/fptr_wlist.c    5 Sep 2023 15:44:02 -0000       1.11
+++ sbin/unwind/libunbound/util/fptr_wlist.c    12 Feb 2024 21:08:02 -0000
@@ -131,6 +131,7 @@ fptr_whitelist_comm_timer(void (*fptr)(v
       else if(fptr == &pending_udp_timer_delay_cb) return 1;
       else if(fptr == &worker_stat_timer_cb) return 1;
       else if(fptr == &worker_probe_timer_cb) return 1;
+       else if(fptr == &validate_suspend_timer_cb) return 1;
#ifdef UB_ON_WINDOWS
       else if(fptr == &wsvc_cron_cb) return 1;
#endif
Index: sbin/unwind/libunbound/validator/val_nsec.c
===================================================================
RCS file: /cvs/src/sbin/unwind/libunbound/validator/val_nsec.c,v
diff -u -p -r1.5 val_nsec.c
--- sbin/unwind/libunbound/validator/val_nsec.c 5 Sep 2023 15:44:02 -0000       1.5
+++ sbin/unwind/libunbound/validator/val_nsec.c 12 Feb 2024 21:08:02 -0000
@@ -181,6 +181,7 @@ nsec_verify_rrset(struct module_env* env
{
       struct packed_rrset_data* d = (struct packed_rrset_data*)
               nsec->entry.data;
+       int verified = 0;
       if(!d) return 0;
       if(d->security == sec_status_secure)
               return 1;
@@ -188,7 +189,7 @@ nsec_verify_rrset(struct module_env* env
       if(d->security == sec_status_secure)
               return 1;
       d->security = val_verify_rrset_entry(env, ve, nsec, kkey, reason,
-               reason_bogus, LDNS_SECTION_AUTHORITY, qstate);
+               reason_bogus, LDNS_SECTION_AUTHORITY, qstate, &verified);
       if(d->security == sec_status_secure) {
               rrset_update_sec_status(env->rrset_cache, nsec, *env->now);
               return 1;
Index: sbin/unwind/libunbound/validator/val_nsec3.c
===================================================================
RCS file: /cvs/src/sbin/unwind/libunbound/validator/val_nsec3.c,v
diff -u -p -r1.2 val_nsec3.c
--- sbin/unwind/libunbound/validator/val_nsec3.c        18 Jun 2022 16:20:14 -0000      1.2
+++ sbin/unwind/libunbound/validator/val_nsec3.c        12 Feb 2024 21:08:02 -0000
@@ -57,6 +57,19 @@
/* we include nsec.h for the bitmap_has_type function */
#include "validator/val_nsec.h"
#include "sldns/sbuffer.h"
+#include "util/config_file.h"
+
+/**
+ * Max number of NSEC3 calculations at once, suspend query for later.
+ * 8 is low enough and allows for cases where multiple proofs are needed.
+ */
+#define MAX_NSEC3_CALCULATIONS 8
+/**
+ * When all allowed NSEC3 calculations at once resulted in error treat as
+ * bogus. NSEC3 hash errors are not cached and this helps breaks loops with
+ * erroneous data.
+ */
+#define MAX_NSEC3_ERRORS -1

/**
 * This function we get from ldns-compat or from base system
@@ -532,6 +545,17 @@ nsec3_hash_cmp(const void* c1, const voi
       return memcmp(s1, s2, s1len);
}

+int
+nsec3_cache_table_init(struct nsec3_cache_table* ct, struct regional* region)
+{
+       if(ct->ct) return 1;
+       ct->ct = (rbtree_type*)regional_alloc(region, sizeof(*ct->ct));
+       if(!ct->ct) return 0;
+       ct->region = region;
+       rbtree_init(ct->ct, &nsec3_hash_cmp);
+       return 1;
+}
+
size_t
nsec3_get_hashed(sldns_buffer* buf, uint8_t* nm, size_t nmlen, int algo,
       size_t iter, uint8_t* salt, size_t saltlen, uint8_t* res, size_t max)
@@ -646,7 +670,7 @@ nsec3_hash_name(rbtree_type* table, stru
       c = (struct nsec3_cached_hash*)rbtree_search(table, &looki);
       if(c) {
               *hash = c;
-               return 1;
+               return 2;
       }
       /* create a new entry */
       c = (struct nsec3_cached_hash*)regional_alloc(region, sizeof(*c));
@@ -658,10 +682,10 @@ nsec3_hash_name(rbtree_type* table, stru
       c->dname_len = dname_len;
       r = nsec3_calc_hash(region, buf, c);
       if(r != 1)
-               return r;
+               return r;  /* returns -1 or 0 */
       r = nsec3_calc_b32(region, buf, c);
       if(r != 1)
-               return r;
+               return r;  /* returns 0 */
#ifdef UNBOUND_DEBUG
       n =
#else
@@ -704,6 +728,7 @@ nsec3_hash_matches_owner(struct nsec3_fi
       struct nsec3_cached_hash* hash, struct ub_packed_rrset_key* s)
{
       uint8_t* nm = s->rk.dname;
+       if(!hash) return 0; /* please clang */
       /* compare, does hash of name based on params in this NSEC3
        * match the owner name of this NSEC3?
        * name must be: <hashlength>base32 . zone name
@@ -730,34 +755,50 @@ nsec3_hash_matches_owner(struct nsec3_fi
 * @param nmlen: length of name.
 * @param rrset: nsec3 that matches is returned here.
 * @param rr: rr number in nsec3 rrset that matches.
+ * @param calculations: current hash calculations.
 * @return true if a matching NSEC3 is found, false if not.
 */
static int
find_matching_nsec3(struct module_env* env, struct nsec3_filter* flt,
-       rbtree_type* ct, uint8_t* nm, size_t nmlen,
-       struct ub_packed_rrset_key** rrset, int* rr)
+       struct nsec3_cache_table* ct, uint8_t* nm, size_t nmlen,
+       struct ub_packed_rrset_key** rrset, int* rr,
+       int* calculations)
{
       size_t i_rs;
       int i_rr;
       struct ub_packed_rrset_key* s;
       struct nsec3_cached_hash* hash = NULL;
       int r;
+       int calc_errors = 0;

       /* this loop skips other-zone and unknown NSEC3s, also non-NSEC3 RRs */
       for(s=filter_first(flt, &i_rs, &i_rr); s;
               s=filter_next(flt, &i_rs, &i_rr)) {
+               /* check if we are allowed more calculations */
+               if(*calculations >= MAX_NSEC3_CALCULATIONS) {
+                       if(calc_errors == *calculations) {
+                               *calculations = MAX_NSEC3_ERRORS;
+                       }
+                       break;
+               }
               /* get name hashed for this NSEC3 RR */
-               r = nsec3_hash_name(ct, env->scratch, env->scratch_buffer,
+               r = nsec3_hash_name(ct->ct, ct->region, env->scratch_buffer,
                       s, i_rr, nm, nmlen, &hash);
               if(r == 0) {
                       log_err("nsec3: malloc failure");
                       break; /* alloc failure */
-               } else if(r != 1)
-                       continue; /* malformed NSEC3 */
-               else if(nsec3_hash_matches_owner(flt, hash, s)) {
-                       *rrset = s; /* rrset with this name */
-                       *rr = i_rr; /* matches hash with these parameters */
-                       return 1;
+               } else if(r < 0) {
+                       /* malformed NSEC3 */
+                       calc_errors++;
+                       (*calculations)++;
+                       continue;
+               } else {
+                       if(r == 1) (*calculations)++;
+                       if(nsec3_hash_matches_owner(flt, hash, s)) {
+                               *rrset = s; /* rrset with this name */
+                               *rr = i_rr; /* matches hash with these parameters */
+                               return 1;
+                       }
               }
       }
       *rrset = NULL;
@@ -775,6 +816,7 @@ nsec3_covers(uint8_t* zone, struct nsec3
       if(!nsec3_get_nextowner(rrset, rr, &next, &nextlen))
               return 0; /* malformed RR proves nothing */

+       if(!hash) return 0; /* please clang */
       /* check the owner name is a hashed value . apex
        * base32 encoded values must have equal length.
        * hash_value and next hash value must have equal length. */
@@ -823,35 +865,51 @@ nsec3_covers(uint8_t* zone, struct nsec3
 * @param nmlen: length of name.
 * @param rrset: covering NSEC3 rrset is returned here.
 * @param rr: rr of cover is returned here.
+ * @param calculations: current hash calculations.
 * @return true if a covering NSEC3 is found, false if not.
 */
static int
find_covering_nsec3(struct module_env* env, struct nsec3_filter* flt,
-        rbtree_type* ct, uint8_t* nm, size_t nmlen,
-       struct ub_packed_rrset_key** rrset, int* rr)
+       struct nsec3_cache_table* ct, uint8_t* nm, size_t nmlen,
+       struct ub_packed_rrset_key** rrset, int* rr,
+       int* calculations)
{
       size_t i_rs;
       int i_rr;
       struct ub_packed_rrset_key* s;
       struct nsec3_cached_hash* hash = NULL;
       int r;
+       int calc_errors = 0;

       /* this loop skips other-zone and unknown NSEC3s, also non-NSEC3 RRs */
       for(s=filter_first(flt, &i_rs, &i_rr); s;
               s=filter_next(flt, &i_rs, &i_rr)) {
+               /* check if we are allowed more calculations */
+               if(*calculations >= MAX_NSEC3_CALCULATIONS) {
+                       if(calc_errors == *calculations) {
+                               *calculations = MAX_NSEC3_ERRORS;
+                       }
+                       break;
+               }
               /* get name hashed for this NSEC3 RR */
-               r = nsec3_hash_name(ct, env->scratch, env->scratch_buffer,
+               r = nsec3_hash_name(ct->ct, ct->region, env->scratch_buffer,
                       s, i_rr, nm, nmlen, &hash);
               if(r == 0) {
                       log_err("nsec3: malloc failure");
                       break; /* alloc failure */
-               } else if(r != 1)
-                       continue; /* malformed NSEC3 */
-               else if(nsec3_covers(flt->zone, hash, s, i_rr,
-                       env->scratch_buffer)) {
-                       *rrset = s; /* rrset with this name */
-                       *rr = i_rr; /* covers hash with these parameters */
-                       return 1;
+               } else if(r < 0) {
+                       /* malformed NSEC3 */
+                       calc_errors++;
+                       (*calculations)++;
+                       continue;
+               } else {
+                       if(r == 1) (*calculations)++;
+                       if(nsec3_covers(flt->zone, hash, s, i_rr,
+                               env->scratch_buffer)) {
+                               *rrset = s; /* rrset with this name */
+                               *rr = i_rr; /* covers hash with these parameters */
+                               return 1;
+                       }
               }
       }
       *rrset = NULL;
@@ -869,11 +927,13 @@ find_covering_nsec3(struct module_env* e
 * @param ct: cached hashes table.
 * @param qinfo: query that is verified for.
 * @param ce: closest encloser information is returned in here.
+ * @param calculations: current hash calculations.
 * @return true if a closest encloser candidate is found, false if not.
 */
static int
-nsec3_find_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
-       rbtree_type* ct, struct query_info* qinfo, struct ce_response* ce)
+nsec3_find_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
+       struct nsec3_cache_table* ct, struct query_info* qinfo,
+       struct ce_response* ce, int* calculations)
{
       uint8_t* nm = qinfo->qname;
       size_t nmlen = qinfo->qname_len;
@@ -888,8 +948,12 @@ nsec3_find_closest_encloser(struct modul
        * may be the case. */

       while(dname_subdomain_c(nm, flt->zone)) {
+               if(*calculations >= MAX_NSEC3_CALCULATIONS ||
+                       *calculations == MAX_NSEC3_ERRORS) {
+                       return 0;
+               }
               if(find_matching_nsec3(env, flt, ct, nm, nmlen,
-                       &ce->ce_rrset, &ce->ce_rr)) {
+                       &ce->ce_rrset, &ce->ce_rr, calculations)) {
                       ce->ce = nm;
                       ce->ce_len = nmlen;
                       return 1;
@@ -933,22 +997,38 @@ next_closer(uint8_t* qname, size_t qname
 *     If set true, and the return value is true, then you can be
 *     certain that the ce.nc_rrset and ce.nc_rr are set properly.
 * @param ce: closest encloser information is returned in here.
+ * @param calculations: pointer to the current NSEC3 hash calculations.
 * @return bogus if no closest encloser could be proven.
 *     secure if a closest encloser could be proven, ce is set.
 *     insecure if the closest-encloser candidate turns out to prove
 *             that an insecure delegation exists above the qname.
+ *     unchecked if no more hash calculations are allowed at this point.
 */
static enum sec_status
-nsec3_prove_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
-       rbtree_type* ct, struct query_info* qinfo, int prove_does_not_exist,
-       struct ce_response* ce)
+nsec3_prove_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
+       struct nsec3_cache_table* ct, struct query_info* qinfo,
+       int prove_does_not_exist, struct ce_response* ce, int* calculations)
{
       uint8_t* nc;
       size_t nc_len;
       /* robust: clean out ce, in case it gets abused later */
       memset(ce, 0, sizeof(*ce));

-       if(!nsec3_find_closest_encloser(env, flt, ct, qinfo, ce)) {
+       if(!nsec3_find_closest_encloser(env, flt, ct, qinfo, ce, calculations)) {
+               if(*calculations == MAX_NSEC3_ERRORS) {
+                       verbose(VERB_ALGO, "nsec3 proveClosestEncloser: could "
+                               "not find a candidate for the closest "
+                               "encloser; all attempted hash calculations "
+                               "were erroneous; bogus");
+                       return sec_status_bogus;
+               } else if(*calculations >= MAX_NSEC3_CALCULATIONS) {
+                       verbose(VERB_ALGO, "nsec3 proveClosestEncloser: could "
+                               "not find a candidate for the closest "
+                               "encloser; reached MAX_NSEC3_CALCULATIONS "
+                               "(%d); unchecked still",
+                               MAX_NSEC3_CALCULATIONS);
+                       return sec_status_unchecked;
+               }
               verbose(VERB_ALGO, "nsec3 proveClosestEncloser: could "
                       "not find a candidate for the closest encloser.");
               return sec_status_bogus;
@@ -989,9 +1069,23 @@ nsec3_prove_closest_encloser(struct modu
       /* Otherwise, we need to show that the next closer name is covered. */
       next_closer(qinfo->qname, qinfo->qname_len, ce->ce, &nc, &nc_len);
       if(!find_covering_nsec3(env, flt, ct, nc, nc_len,
-               &ce->nc_rrset, &ce->nc_rr)) {
+               &ce->nc_rrset, &ce->nc_rr, calculations)) {
+               if(*calculations == MAX_NSEC3_ERRORS) {
+                       verbose(VERB_ALGO, "nsec3: Could not find proof that the "
+                               "candidate encloser was the closest encloser; "
+                               "all attempted hash calculations were "
+                               "erroneous; bogus");
+                       return sec_status_bogus;
+               } else if(*calculations >= MAX_NSEC3_CALCULATIONS) {
+                       verbose(VERB_ALGO, "nsec3: Could not find proof that the "
+                               "candidate encloser was the closest encloser; "
+                               "reached MAX_NSEC3_CALCULATIONS (%d); "
+                               "unchecked still",
+                               MAX_NSEC3_CALCULATIONS);
+                       return sec_status_unchecked;
+               }
               verbose(VERB_ALGO, "nsec3: Could not find proof that the "
-                         "candidate encloser was the closest encloser");
+                       "candidate encloser was the closest encloser");
               return sec_status_bogus;
       }
       return sec_status_secure;
@@ -1019,8 +1113,8 @@ nsec3_ce_wildcard(struct regional* regio

/** Do the name error proof */
static enum sec_status
-nsec3_do_prove_nameerror(struct module_env* env, struct nsec3_filter* flt,
-       rbtree_type* ct, struct query_info* qinfo)
+nsec3_do_prove_nameerror(struct module_env* env, struct nsec3_filter* flt,
+       struct nsec3_cache_table* ct, struct query_info* qinfo, int* calc)
{
       struct ce_response ce;
       uint8_t* wc;
@@ -1032,11 +1126,15 @@ nsec3_do_prove_nameerror(struct module_e
       /* First locate and prove the closest encloser to qname. We will
        * use the variant that fails if the closest encloser turns out
        * to be qname. */
-       sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce);
+       sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce, calc);
       if(sec != sec_status_secure) {
               if(sec == sec_status_bogus)
                       verbose(VERB_ALGO, "nsec3 nameerror proof: failed "
                               "to prove a closest encloser");
+               else if(sec == sec_status_unchecked)
+                       verbose(VERB_ALGO, "nsec3 nameerror proof: will "
+                               "continue proving closest encloser after "
+                               "suspend");
               else    verbose(VERB_ALGO, "nsec3 nameerror proof: closest "
                               "nsec3 is an insecure delegation");
               return sec;
@@ -1046,9 +1144,27 @@ nsec3_do_prove_nameerror(struct module_e
       /* At this point, we know that qname does not exist. Now we need
        * to prove that the wildcard does not exist. */
       log_assert(ce.ce);
-       wc = nsec3_ce_wildcard(env->scratch, ce.ce, ce.ce_len, &wclen);
-       if(!wc || !find_covering_nsec3(env, flt, ct, wc, wclen,
-               &wc_rrset, &wc_rr)) {
+       wc = nsec3_ce_wildcard(ct->region, ce.ce, ce.ce_len, &wclen);
+       if(!wc) {
+               verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
+                       "that the applicable wildcard did not exist.");
+               return sec_status_bogus;
+       }
+       if(!find_covering_nsec3(env, flt, ct, wc, wclen, &wc_rrset, &wc_rr, calc)) {
+               if(*calc == MAX_NSEC3_ERRORS) {
+                       verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
+                               "that the applicable wildcard did not exist; "
+                               "all attempted hash calculations were "
+                               "erroneous; bogus");
+                       return sec_status_bogus;
+               } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
+                       verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
+                               "that the applicable wildcard did not exist; "
+                               "reached MAX_NSEC3_CALCULATIONS (%d); "
+                               "unchecked still",
+                               MAX_NSEC3_CALCULATIONS);
+                       return sec_status_unchecked;
+               }
               verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
                       "that the applicable wildcard did not exist.");
               return sec_status_bogus;
@@ -1064,14 +1180,13 @@ nsec3_do_prove_nameerror(struct module_e
enum sec_status
nsec3_prove_nameerror(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey)
+       struct query_info* qinfo, struct key_entry_key* kkey,
+       struct nsec3_cache_table* ct, int* calc)
{
-       rbtree_type ct;
       struct nsec3_filter flt;

       if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
               return sec_status_bogus; /* no valid NSEC3s, bogus */
-       rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
       filter_init(&flt, list, num, qinfo); /* init RR iterator */
       if(!flt.zone)
               return sec_status_bogus; /* no RRs */
@@ -1079,7 +1194,7 @@ nsec3_prove_nameerror(struct module_env*
               return sec_status_insecure; /* iteration count too high */
       log_nametypeclass(VERB_ALGO, "start nsec3 nameerror proof, zone",
               flt.zone, 0, 0);
-       return nsec3_do_prove_nameerror(env, &flt, &ct, qinfo);
+       return nsec3_do_prove_nameerror(env, &flt, ct, qinfo, calc);
}

/*
@@ -1089,8 +1204,9 @@ nsec3_prove_nameerror(struct module_env*

/** Do the nodata proof */
static enum sec_status
-nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
-       rbtree_type* ct, struct query_info* qinfo)
+nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
+       struct nsec3_cache_table* ct, struct query_info* qinfo,
+       int* calc)
{
       struct ce_response ce;
       uint8_t* wc;
@@ -1100,7 +1216,7 @@ nsec3_do_prove_nodata(struct module_env*
       enum sec_status sec;

       if(find_matching_nsec3(env, flt, ct, qinfo->qname, qinfo->qname_len,
-               &rrset, &rr)) {
+               &rrset, &rr, calc)) {
               /* cases 1 and 2 */
               if(nsec3_has_type(rrset, rr, qinfo->qtype)) {
                       verbose(VERB_ALGO, "proveNodata: Matching NSEC3 "
@@ -1144,11 +1260,23 @@ nsec3_do_prove_nodata(struct module_env*
               }
               return sec_status_secure;
       }
+       if(*calc == MAX_NSEC3_ERRORS) {
+               verbose(VERB_ALGO, "proveNodata: all attempted hash "
+                       "calculations were erroneous while finding a matching "
+                       "NSEC3, bogus");
+               return sec_status_bogus;
+       } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
+               verbose(VERB_ALGO, "proveNodata: reached "
+                       "MAX_NSEC3_CALCULATIONS (%d) while finding a "
+                       "matching NSEC3; unchecked still",
+                       MAX_NSEC3_CALCULATIONS);
+               return sec_status_unchecked;
+       }

       /* For cases 3 - 5, we need the proven closest encloser, and it
        * can't match qname. Although, at this point, we know that it
        * won't since we just checked that. */
-       sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce);
+       sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce, calc);
       if(sec == sec_status_bogus) {
               verbose(VERB_ALGO, "proveNodata: did not match qname, "
                         "nor found a proven closest encloser.");
@@ -1157,14 +1285,17 @@ nsec3_do_prove_nodata(struct module_env*
               verbose(VERB_ALGO, "proveNodata: closest nsec3 is insecure "
                         "delegation.");
               return sec_status_insecure;
+       } else if(sec==sec_status_unchecked) {
+               return sec_status_unchecked;
       }

       /* Case 3: removed */

       /* Case 4: */
       log_assert(ce.ce);
-       wc = nsec3_ce_wildcard(env->scratch, ce.ce, ce.ce_len, &wclen);
-       if(wc && find_matching_nsec3(env, flt, ct, wc, wclen, &rrset, &rr)) {
+       wc = nsec3_ce_wildcard(ct->region, ce.ce, ce.ce_len, &wclen);
+       if(wc && find_matching_nsec3(env, flt, ct, wc, wclen, &rrset, &rr,
+               calc)) {
               /* found wildcard */
               if(nsec3_has_type(rrset, rr, qinfo->qtype)) {
                       verbose(VERB_ALGO, "nsec3 nodata proof: matching "
@@ -1195,6 +1326,18 @@ nsec3_do_prove_nodata(struct module_env*
               }
               return sec_status_secure;
       }
+       if(*calc == MAX_NSEC3_ERRORS) {
+               verbose(VERB_ALGO, "nsec3 nodata proof: all attempted hash "
+                       "calculations were erroneous while matching "
+                       "wildcard, bogus");
+               return sec_status_bogus;
+       } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
+               verbose(VERB_ALGO, "nsec3 nodata proof: reached "
+                       "MAX_NSEC3_CALCULATIONS (%d) while matching "
+                       "wildcard, unchecked still",
+                       MAX_NSEC3_CALCULATIONS);
+               return sec_status_unchecked;
+       }

       /* Case 5: */
       /* Due to forwarders, cnames, and other collating effects, we
@@ -1223,28 +1366,27 @@ nsec3_do_prove_nodata(struct module_env*
enum sec_status
nsec3_prove_nodata(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey)
+       struct query_info* qinfo, struct key_entry_key* kkey,
+       struct nsec3_cache_table* ct, int* calc)
{
-       rbtree_type ct;
       struct nsec3_filter flt;

       if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
               return sec_status_bogus; /* no valid NSEC3s, bogus */
-       rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
       filter_init(&flt, list, num, qinfo); /* init RR iterator */
       if(!flt.zone)
               return sec_status_bogus; /* no RRs */
       if(nsec3_iteration_count_high(ve, &flt, kkey))
               return sec_status_insecure; /* iteration count too high */
-       return nsec3_do_prove_nodata(env, &flt, &ct, qinfo);
+       return nsec3_do_prove_nodata(env, &flt, ct, qinfo, calc);
}

enum sec_status
nsec3_prove_wildcard(struct module_env* env, struct val_env* ve,
        struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc)
+       struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc,
+       struct nsec3_cache_table* ct, int* calc)
{
-       rbtree_type ct;
       struct nsec3_filter flt;
       struct ce_response ce;
       uint8_t* nc;
@@ -1254,7 +1396,6 @@ nsec3_prove_wildcard(struct module_env*

       if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
               return sec_status_bogus; /* no valid NSEC3s, bogus */
-       rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
       filter_init(&flt, list, num, qinfo); /* init RR iterator */
       if(!flt.zone)
               return sec_status_bogus; /* no RRs */
@@ -1272,8 +1413,22 @@ nsec3_prove_wildcard(struct module_env*
       /* Now we still need to prove that the original data did not exist.
        * Otherwise, we need to show that the next closer name is covered. */
       next_closer(qinfo->qname, qinfo->qname_len, ce.ce, &nc, &nc_len);
-       if(!find_covering_nsec3(env, &flt, &ct, nc, nc_len,
-               &ce.nc_rrset, &ce.nc_rr)) {
+       if(!find_covering_nsec3(env, &flt, ct, nc, nc_len,
+               &ce.nc_rrset, &ce.nc_rr, calc)) {
+               if(*calc == MAX_NSEC3_ERRORS) {
+                       verbose(VERB_ALGO, "proveWildcard: did not find a "
+                               "covering NSEC3 that covered the next closer "
+                               "name; all attempted hash calculations were "
+                               "erroneous; bogus");
+                       return sec_status_bogus;
+               } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
+                       verbose(VERB_ALGO, "proveWildcard: did not find a "
+                               "covering NSEC3 that covered the next closer "
+                               "name; reached MAX_NSEC3_CALCULATIONS "
+                               "(%d); unchecked still",
+                               MAX_NSEC3_CALCULATIONS);
+                       return sec_status_unchecked;
+               }
               verbose(VERB_ALGO, "proveWildcard: did not find a covering "
                       "NSEC3 that covered the next closer name.");
               return sec_status_bogus;
@@ -1294,6 +1449,7 @@ list_is_secure(struct module_env* env, s
{
       struct packed_rrset_data* d;
       size_t i;
+       int verified = 0;
       for(i=0; i<num; i++) {
               d = (struct packed_rrset_data*)list[i]->entry.data;
               if(list[i]->rk.type != htons(LDNS_RR_TYPE_NSEC3))
@@ -1304,7 +1460,8 @@ list_is_secure(struct module_env* env, s
               if(d->security == sec_status_secure)
                       continue;
               d->security = val_verify_rrset_entry(env, ve, list[i], kkey,
-                       reason, reason_bogus, LDNS_SECTION_AUTHORITY, qstate);
+                       reason, reason_bogus, LDNS_SECTION_AUTHORITY, qstate,
+                       &verified);
               if(d->security != sec_status_secure) {
                       verbose(VERB_ALGO, "NSEC3 did not verify");
                       return 0;
@@ -1318,13 +1475,16 @@ enum sec_status
nsec3_prove_nods(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
       struct query_info* qinfo, struct key_entry_key* kkey, char** reason,
-       sldns_ede_code* reason_bogus, struct module_qstate* qstate)
+       sldns_ede_code* reason_bogus, struct module_qstate* qstate,
+       struct nsec3_cache_table* ct)
{
-       rbtree_type ct;
       struct nsec3_filter flt;
       struct ce_response ce;
       struct ub_packed_rrset_key* rrset;
       int rr;
+       int calc = 0;
+       enum sec_status sec;
+
       log_assert(qinfo->qtype == LDNS_RR_TYPE_DS);

       if(!list || num == 0 || !kkey || !key_entry_isgood(kkey)) {
@@ -1335,7 +1495,6 @@ nsec3_prove_nods(struct module_env* env,
               *reason = "not all NSEC3 records secure";
               return sec_status_bogus; /* not all NSEC3 records secure */
       }
-       rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
       filter_init(&flt, list, num, qinfo); /* init RR iterator */
       if(!flt.zone) {
               *reason = "no NSEC3 records";
@@ -1346,8 +1505,8 @@ nsec3_prove_nods(struct module_env* env,

       /* Look for a matching NSEC3 to qname -- this is the normal
        * NODATA case. */
-       if(find_matching_nsec3(env, &flt, &ct, qinfo->qname, qinfo->qname_len,
-               &rrset, &rr)) {
+       if(find_matching_nsec3(env, &flt, ct, qinfo->qname, qinfo->qname_len,
+               &rrset, &rr, &calc)) {
               /* If the matching NSEC3 has the SOA bit set, it is from
                * the wrong zone (the child instead of the parent). If
                * it has the DS bit set, then we were lied to. */
@@ -1370,10 +1529,24 @@ nsec3_prove_nods(struct module_env* env,
               /* Otherwise, this proves no DS. */
               return sec_status_secure;
       }
+       if(calc == MAX_NSEC3_ERRORS) {
+               verbose(VERB_ALGO, "nsec3 provenods: all attempted hash "
+                       "calculations were erroneous while finding a matching "
+                       "NSEC3, bogus");
+               return sec_status_bogus;
+       } else if(calc >= MAX_NSEC3_CALCULATIONS) {
+               verbose(VERB_ALGO, "nsec3 provenods: reached "
+                       "MAX_NSEC3_CALCULATIONS (%d) while finding a "
+                       "matching NSEC3, unchecked still",
+                       MAX_NSEC3_CALCULATIONS);
+               return sec_status_unchecked;
+       }

       /* Otherwise, we are probably in the opt-out case. */
-       if(nsec3_prove_closest_encloser(env, &flt, &ct, qinfo, 1, &ce)
-               != sec_status_secure) {
+       sec = nsec3_prove_closest_encloser(env, &flt, ct, qinfo, 1, &ce, &calc);
+       if(sec == sec_status_unchecked) {
+               return sec_status_unchecked;
+       } else if(sec != sec_status_secure) {
               /* an insecure delegation *above* the qname does not prove
                * anything about this qname exactly, and bogus is bogus */
               verbose(VERB_ALGO, "nsec3 provenods: did not match qname, "
@@ -1407,17 +1580,16 @@ nsec3_prove_nods(struct module_env* env,

enum sec_status
nsec3_prove_nxornodata(struct module_env* env, struct val_env* ve,
-       struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey, int* nodata)
+       struct ub_packed_rrset_key** list, size_t num,
+       struct query_info* qinfo, struct key_entry_key* kkey, int* nodata,
+       struct  nsec3_cache_table* ct, int* calc)
{
       enum sec_status sec, secnx;
-       rbtree_type ct;
       struct nsec3_filter flt;
       *nodata = 0;

       if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
               return sec_status_bogus; /* no valid NSEC3s, bogus */
-       rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
       filter_init(&flt, list, num, qinfo); /* init RR iterator */
       if(!flt.zone)
               return sec_status_bogus; /* no RRs */
@@ -1427,16 +1599,20 @@ nsec3_prove_nxornodata(struct module_env
       /* try nxdomain and nodata after another, while keeping the
        * hash cache intact */

-       secnx = nsec3_do_prove_nameerror(env, &flt, &ct, qinfo);
+       secnx = nsec3_do_prove_nameerror(env, &flt, ct, qinfo, calc);
       if(secnx==sec_status_secure)
               return sec_status_secure;
-       sec = nsec3_do_prove_nodata(env, &flt, &ct, qinfo);
+       else if(secnx == sec_status_unchecked)
+               return sec_status_unchecked;
+       sec = nsec3_do_prove_nodata(env, &flt, ct, qinfo, calc);
       if(sec==sec_status_secure) {
               *nodata = 1;
       } else if(sec == sec_status_insecure) {
               *nodata = 1;
       } else if(secnx == sec_status_insecure) {
               sec = sec_status_insecure;
+       } else if(sec == sec_status_unchecked) {
+               return sec_status_unchecked;
       }
       return sec;
}
Index: sbin/unwind/libunbound/validator/val_nsec3.h
===================================================================
RCS file: /cvs/src/sbin/unwind/libunbound/validator/val_nsec3.h,v
diff -u -p -r1.2 val_nsec3.h
--- sbin/unwind/libunbound/validator/val_nsec3.h        18 Jun 2022 16:20:14 -0000      1.2
+++ sbin/unwind/libunbound/validator/val_nsec3.h        12 Feb 2024 21:08:02 -0000
@@ -99,6 +99,15 @@ struct sldns_buffer;
#define NSEC3_HASH_SHA1        0x01

/**
+* Cache table for NSEC3 hashes.
+* It keeps a *pointer* to the region its items are allocated.
+*/
+struct nsec3_cache_table {
+       rbtree_type* ct;
+       struct regional* region;
+};
+
+/**
 * Determine if the set of NSEC3 records provided with a response prove NAME
 * ERROR. This means that the NSEC3s prove a) the closest encloser exists,
 * b) the direct child of the closest encloser towards qname doesn't exist,
@@ -110,14 +119,18 @@ struct sldns_buffer;
 * @param num: number of RRsets in the array to examine.
 * @param qinfo: query that is verified for.
 * @param kkey: key entry that signed the NSEC3s.
+ * @param ct: cached hashes table.
+ * @param calc: current hash calculations.
 * @return:
 *     sec_status SECURE of the Name Error is proven by the NSEC3 RRs,
- *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
+ *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
+ *     UNCHECKED if no more hash calculations are allowed at this point.
 */
enum sec_status
nsec3_prove_nameerror(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey);
+       struct query_info* qinfo, struct key_entry_key* kkey,
+       struct nsec3_cache_table* ct, int* calc);

/**
 * Determine if the NSEC3s provided in a response prove the NOERROR/NODATA
@@ -144,15 +157,18 @@ nsec3_prove_nameerror(struct module_env*
 * @param num: number of RRsets in the array to examine.
 * @param qinfo: query that is verified for.
 * @param kkey: key entry that signed the NSEC3s.
+ * @param ct: cached hashes table.
+ * @param calc: current hash calculations.
 * @return:
 *     sec_status SECURE of the proposition is proven by the NSEC3 RRs,
- *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
+ *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
+ *     UNCHECKED if no more hash calculations are allowed at this point.
 */
enum sec_status
nsec3_prove_nodata(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey);
-
+       struct query_info* qinfo, struct key_entry_key* kkey,
+       struct nsec3_cache_table* ct, int* calc);

/**
 * Prove that a positive wildcard match was appropriate (no direct match
@@ -166,14 +182,18 @@ nsec3_prove_nodata(struct module_env* en
 * @param kkey: key entry that signed the NSEC3s.
 * @param wc: The purported wildcard that matched. This is the wildcard name
 *     as *.wildcard.name., with the *. label already removed.
+ * @param ct: cached hashes table.
+ * @param calc: current hash calculations.
 * @return:
 *     sec_status SECURE of the proposition is proven by the NSEC3 RRs,
- *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
+ *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
+ *     UNCHECKED if no more hash calculations are allowed at this point.
 */
enum sec_status
nsec3_prove_wildcard(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc);
+       struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc,
+       struct nsec3_cache_table* ct, int* calc);

/**
 * Prove that a DS response either had no DS, or wasn't a delegation point.
@@ -189,17 +209,20 @@ nsec3_prove_wildcard(struct module_env*
 * @param reason: string for bogus result.
 * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
 * @param qstate: qstate with region.
+ * @param ct: cached hashes table.
 * @return:
 *     sec_status SECURE of the proposition is proven by the NSEC3 RRs,
 *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
 *     or if there was no DS in an insecure (i.e., opt-in) way,
- *     INDETERMINATE if it was clear that this wasn't a delegation point.
+ *     INDETERMINATE if it was clear that this wasn't a delegation point,
+ *     UNCHECKED if no more hash calculations are allowed at this point.
 */
enum sec_status
nsec3_prove_nods(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
       struct query_info* qinfo, struct key_entry_key* kkey, char** reason,
-       sldns_ede_code* reason_bogus, struct module_qstate* qstate);
+       sldns_ede_code* reason_bogus, struct module_qstate* qstate,
+       struct nsec3_cache_table* ct);

/**
 * Prove NXDOMAIN or NODATA.
@@ -212,14 +235,18 @@ nsec3_prove_nods(struct module_env* env,
 * @param kkey: key entry that signed the NSEC3s.
 * @param nodata: if return value is secure, this indicates if nodata or
 *     nxdomain was proven.
+ * @param ct: cached hashes table.
+ * @param calc: current hash calculations.
 * @return:
 *     sec_status SECURE of the proposition is proven by the NSEC3 RRs,
- *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
+ *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
+ *     UNCHECKED if no more hash calculations are allowed at this point.
 */
enum sec_status
nsec3_prove_nxornodata(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey, int* nodata);
+       struct query_info* qinfo, struct key_entry_key* kkey, int* nodata,
+       struct nsec3_cache_table* ct, int* calc);

/**
 * The NSEC3 hash result storage.
@@ -257,6 +284,14 @@ struct nsec3_cached_hash {
int nsec3_hash_cmp(const void* c1, const void* c2);

/**
+ * Initialise the NSEC3 cache table.
+ * @param ct: the nsec3 cache table.
+ * @param region: the region where allocations for the table will happen.
+ * @return true on success, false on malloc error.
+ */
+int nsec3_cache_table_init(struct nsec3_cache_table* ct, struct regional* region);
+
+/**
 * Obtain the hash of an owner name.
 * Used internally by the nsec3 proof functions in this file.
 * published to enable unit testing of hash algorithms and cache.
@@ -272,7 +307,8 @@ int nsec3_hash_cmp(const void* c1, const
 * @param dname_len: the length of the name.
 * @param hash: the hash node is returned on success.
 * @return:
- *     1 on success, either from cache or newly hashed hash is returned.
+ *     2 on success, hash from cache is returned.
+ *     1 on success, newly computed hash is returned.
 *     0 on a malloc failure.
 *     -1 if the NSEC3 rr was badly formatted (i.e. formerr).
 */
Index: sbin/unwind/libunbound/validator/val_sigcrypt.c
===================================================================
RCS file: /cvs/src/sbin/unwind/libunbound/validator/val_sigcrypt.c,v
diff -u -p -r1.7 val_sigcrypt.c
--- sbin/unwind/libunbound/validator/val_sigcrypt.c     5 Sep 2023 15:44:02 -0000       1.7
+++ sbin/unwind/libunbound/validator/val_sigcrypt.c     12 Feb 2024 21:08:02 -0000
@@ -79,6 +79,9 @@
#include <openssl/engine.h>
#endif

+/** Maximum number of RRSIG validations for an RRset. */
+#define MAX_VALIDATE_RRSIGS 8
+
/** return number of rrs in an rrset */
static size_t
rrset_get_count(struct ub_packed_rrset_key* rrset)
@@ -542,6 +545,8 @@ int algo_needs_missing(struct algo_needs
 * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
 * @param section: section of packet where this rrset comes from.
 * @param qstate: qstate with region.
+ * @param numverified: incremented when the number of RRSIG validations
+ *     increases.
 * @return secure if any key signs *this* signature. bogus if no key signs it,
 *     unchecked on error, or indeterminate if all keys are not supported by
 *     the crypto library (openssl3+ only).
@@ -552,7 +557,8 @@ dnskeyset_verify_rrset_sig(struct module
       struct ub_packed_rrset_key* dnskey, size_t sig_idx,
       struct rbtree_type** sortree,
       char** reason, sldns_ede_code *reason_bogus,
-       sldns_pkt_section section, struct module_qstate* qstate)
+       sldns_pkt_section section, struct module_qstate* qstate,
+       int* numverified)
{
       /* find matching keys and check them */
       enum sec_status sec = sec_status_bogus;
@@ -576,6 +582,7 @@ dnskeyset_verify_rrset_sig(struct module
                       tag != dnskey_calc_keytag(dnskey, i))
                       continue;
               numchecked ++;
+               (*numverified)++;

               /* see if key verifies */
               sec = dnskey_verify_rrset_sig(env->scratch,
@@ -586,6 +593,13 @@ dnskeyset_verify_rrset_sig(struct module
                       return sec;
               else if(sec == sec_status_indeterminate)
                       numindeterminate ++;
+               if(*numverified > MAX_VALIDATE_RRSIGS) {
+                       *reason = "too many RRSIG validations";
+                       if(reason_bogus)
+                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
+                       verbose(VERB_ALGO, "verify sig: too many RRSIG validations");
+                       return sec_status_bogus;
+               }
       }
       if(numchecked == 0) {
               *reason = "signatures from unknown keys";
@@ -609,7 +623,7 @@ enum sec_status
dnskeyset_verify_rrset(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key* rrset, struct ub_packed_rrset_key* dnskey,
       uint8_t* sigalg, char** reason, sldns_ede_code *reason_bogus,
-       sldns_pkt_section section, struct module_qstate* qstate)
+       sldns_pkt_section section, struct module_qstate* qstate, int* verified)
{
       enum sec_status sec;
       size_t i, num;
@@ -617,6 +631,7 @@ dnskeyset_verify_rrset(struct module_env
       /* make sure that for all DNSKEY algorithms there are valid sigs */
       struct algo_needs needs;
       int alg;
+       *verified = 0;

       num = rrset_get_sigcount(rrset);
       if(num == 0) {
@@ -641,7 +656,7 @@ dnskeyset_verify_rrset(struct module_env
       for(i=0; i<num; i++) {
               sec = dnskeyset_verify_rrset_sig(env, ve, *env->now, rrset,
                       dnskey, i, &sortree, reason, reason_bogus,
-                       section, qstate);
+                       section, qstate, verified);
               /* see which algorithm has been fixed up */
               if(sec == sec_status_secure) {
                       if(!sigalg)
@@ -653,6 +668,13 @@ dnskeyset_verify_rrset(struct module_env
                       algo_needs_set_bogus(&needs,
                               (uint8_t)rrset_get_sig_algo(rrset, i));
               }
+               if(*verified > MAX_VALIDATE_RRSIGS) {
+                       verbose(VERB_QUERY, "rrset failed to verify, too many RRSIG validations");
+                       *reason = "too many RRSIG validations";
+                       if(reason_bogus)
+                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
+                       return sec_status_bogus;
+               }
       }
       if(sigalg && (alg=algo_needs_missing(&needs)) != 0) {
               verbose(VERB_ALGO, "rrset failed to verify: "
@@ -691,6 +713,7 @@ dnskey_verify_rrset(struct module_env* e
       int buf_canon = 0;
       uint16_t tag = dnskey_calc_keytag(dnskey, dnskey_idx);
       int algo = dnskey_get_algo(dnskey, dnskey_idx);
+       int numverified = 0;

       num = rrset_get_sigcount(rrset);
       if(num == 0) {
@@ -714,8 +737,16 @@ dnskey_verify_rrset(struct module_env* e
               if(sec == sec_status_secure)
                       return sec;
               numchecked ++;
+               numverified ++;
               if(sec == sec_status_indeterminate)
                       numindeterminate ++;
+               if(numverified > MAX_VALIDATE_RRSIGS) {
+                       verbose(VERB_QUERY, "rrset failed to verify, too many RRSIG validations");
+                       *reason = "too many RRSIG validations";
+                       if(reason_bogus)
+                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
+                       return sec_status_bogus;
+               }
       }
       verbose(VERB_ALGO, "rrset failed to verify: all signatures are bogus");
       if(!numchecked) {
Index: sbin/unwind/libunbound/validator/val_sigcrypt.h
===================================================================
RCS file: /cvs/src/sbin/unwind/libunbound/validator/val_sigcrypt.h,v
diff -u -p -r1.3 val_sigcrypt.h
--- sbin/unwind/libunbound/validator/val_sigcrypt.h     18 Jun 2022 16:20:14 -0000      1.3
+++ sbin/unwind/libunbound/validator/val_sigcrypt.h     12 Feb 2024 21:08:02 -0000
@@ -260,6 +260,7 @@ uint16_t dnskey_get_flags(struct ub_pack
 * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
 * @param section: section of packet where this rrset comes from.
 * @param qstate: qstate with region.
+ * @param verified: if not NULL the number of RRSIG validations is returned.
 * @return SECURE if one key in the set verifies one rrsig.
 *     UNCHECKED on allocation errors, unsupported algorithms, malformed data,
 *     and BOGUS on verification failures (no keys match any signatures).
@@ -268,7 +269,7 @@ enum sec_status dnskeyset_verify_rrset(s
       struct val_env* ve, struct ub_packed_rrset_key* rrset,
       struct ub_packed_rrset_key* dnskey, uint8_t* sigalg,
       char** reason, sldns_ede_code *reason_bogus,
-       sldns_pkt_section section, struct module_qstate* qstate);
+       sldns_pkt_section section, struct module_qstate* qstate, int* verified);


/**
Index: sbin/unwind/libunbound/validator/val_utils.c
===================================================================
RCS file: /cvs/src/sbin/unwind/libunbound/validator/val_utils.c,v
diff -u -p -r1.6 val_utils.c
--- sbin/unwind/libunbound/validator/val_utils.c        5 Sep 2023 15:44:02 -0000       1.6
+++ sbin/unwind/libunbound/validator/val_utils.c        12 Feb 2024 21:08:03 -0000
@@ -58,6 +58,10 @@
#include "sldns/wire2str.h"
#include "sldns/parseutil.h"

+/** Maximum allowed digest match failures per DS, for DNSKEYs with the same
+ *  properties */
+#define MAX_DS_MATCH_FAILURES 4
+
enum val_classification
val_classify_response(uint16_t query_flags, struct query_info* origqinf,
       struct query_info* qinf, struct reply_info* rep, size_t skip)
@@ -336,7 +340,8 @@ static enum sec_status
val_verify_rrset(struct module_env* env, struct val_env* ve,
        struct ub_packed_rrset_key* rrset, struct ub_packed_rrset_key* keys,
       uint8_t* sigalg, char** reason, sldns_ede_code *reason_bogus,
-       sldns_pkt_section section, struct module_qstate* qstate)
+       sldns_pkt_section section, struct module_qstate* qstate,
+       int *verified)
{
       enum sec_status sec;
       struct packed_rrset_data* d = (struct packed_rrset_data*)rrset->
@@ -346,6 +351,7 @@ val_verify_rrset(struct module_env* env,
               log_nametypeclass(VERB_ALGO, "verify rrset cached",
                       rrset->rk.dname, ntohs(rrset->rk.type),
                       ntohs(rrset->rk.rrset_class));
+               *verified = 0;
               return d->security;
       }
       /* check in the cache if verification has already been done */
@@ -354,12 +360,13 @@ val_verify_rrset(struct module_env* env,
               log_nametypeclass(VERB_ALGO, "verify rrset from cache",
                       rrset->rk.dname, ntohs(rrset->rk.type),
                       ntohs(rrset->rk.rrset_class));
+               *verified = 0;
               return d->security;
       }
       log_nametypeclass(VERB_ALGO, "verify rrset", rrset->rk.dname,
               ntohs(rrset->rk.type), ntohs(rrset->rk.rrset_class));
       sec = dnskeyset_verify_rrset(env, ve, rrset, keys, sigalg, reason,
-               reason_bogus, section, qstate);
+               reason_bogus, section, qstate, verified);
       verbose(VERB_ALGO, "verify result: %s", sec_status_to_string(sec));
       regional_free_all(env->scratch);

@@ -393,7 +400,8 @@ enum sec_status
val_verify_rrset_entry(struct module_env* env, struct val_env* ve,
        struct ub_packed_rrset_key* rrset, struct key_entry_key* kkey,
       char** reason, sldns_ede_code *reason_bogus,
-       sldns_pkt_section section, struct module_qstate* qstate)
+       sldns_pkt_section section, struct module_qstate* qstate,
+       int* verified)
{
       /* temporary dnskey rrset-key */
       struct ub_packed_rrset_key dnskey;
@@ -407,7 +415,7 @@ val_verify_rrset_entry(struct module_env
       dnskey.entry.key = &dnskey;
       dnskey.entry.data = kd->rrset_data;
       sec = val_verify_rrset(env, ve, rrset, &dnskey, kd->algo, reason,
-               reason_bogus, section, qstate);
+               reason_bogus, section, qstate, verified);
       return sec;
}

@@ -439,6 +447,12 @@ verify_dnskeys_with_ds_rr(struct module_
               if(!ds_digest_match_dnskey(env, dnskey_rrset, i, ds_rrset,
                       ds_idx)) {
                       verbose(VERB_ALGO, "DS match attempt failed");
+                       if(numchecked > numhashok + MAX_DS_MATCH_FAILURES) {
+                               verbose(VERB_ALGO, "DS match attempt reached "
+                                       "MAX_DS_MATCH_FAILURES (%d); bogus",
+                                       MAX_DS_MATCH_FAILURES);
+                               return sec_status_bogus;
+                       }
                       continue;
               }
               numhashok++;
Index: sbin/unwind/libunbound/validator/val_utils.h
===================================================================
RCS file: /cvs/src/sbin/unwind/libunbound/validator/val_utils.h,v
diff -u -p -r1.2 val_utils.h
--- sbin/unwind/libunbound/validator/val_utils.h        18 Jun 2022 16:20:14 -0000      1.2
+++ sbin/unwind/libunbound/validator/val_utils.h        12 Feb 2024 21:08:03 -0000
@@ -124,12 +124,14 @@ void val_find_signer(enum val_classifica
 * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
 * @param section: section of packet where this rrset comes from.
 * @param qstate: qstate with region.
+ * @param verified: if not NULL, the number of RRSIG validations is returned.
 * @return security status of verification.
 */
enum sec_status val_verify_rrset_entry(struct module_env* env,
       struct val_env* ve, struct ub_packed_rrset_key* rrset,
       struct key_entry_key* kkey, char** reason, sldns_ede_code *reason_bogus,
-       sldns_pkt_section section, struct module_qstate* qstate);
+       sldns_pkt_section section, struct module_qstate* qstate,
+       int* verified);

/**
 * Verify DNSKEYs with DS rrset. Like val_verify_new_DNSKEYs but
Index: sbin/unwind/libunbound/validator/validator.c
===================================================================
RCS file: /cvs/src/sbin/unwind/libunbound/validator/validator.c,v
diff -u -p -r1.10 validator.c
--- sbin/unwind/libunbound/validator/validator.c        5 Sep 2023 15:44:02 -0000       1.10
+++ sbin/unwind/libunbound/validator/validator.c        12 Feb 2024 21:08:03 -0000
@@ -64,10 +64,15 @@
#include "sldns/wire2str.h"
#include "sldns/str2wire.h"

+/** Max number of RRSIGs to validate at once, suspend query for later. */
+#define MAX_VALIDATE_AT_ONCE 8
+/** Max number of validation suspends allowed, error out otherwise. */
+#define MAX_VALIDATION_SUSPENDS 16
+
/* forward decl for cache response and normal super inform calls of a DS */
static void process_ds_response(struct module_qstate* qstate,
       struct val_qstate* vq, int id, int rcode, struct dns_msg* msg,
-       struct query_info* qinfo, struct sock_list* origin);
+       struct query_info* qinfo, struct sock_list* origin, int* suspend);


/* Updates the suplied EDE (RFC8914) code selectively so we don't lose
@@ -281,6 +286,21 @@ val_new(struct module_qstate* qstate, in
       return val_new_getmsg(qstate, vq);
}

+/** reset validator query state for query restart */
+static void
+val_restart(struct val_qstate* vq)
+{
+       struct comm_timer* temp_timer;
+       int restart_count;
+       if(!vq) return;
+       temp_timer = vq->suspend_timer;
+       restart_count = vq->restart_count+1;
+       memset(vq, 0, sizeof(*vq));
+       vq->suspend_timer = temp_timer;
+       vq->restart_count = restart_count;
+       vq->state = VAL_INIT_STATE;
+}
+
/**
 * Exit validation with an error status
 *
@@ -587,30 +607,42 @@ prime_trust_anchor(struct module_qstate*
 * completed.
 *
 * @param qstate: query state.
+ * @param vq: validator query state.
 * @param env: module env for verify.
 * @param ve: validator env for verify.
 * @param qchase: query that was made.
 * @param chase_reply: answer to validate.
 * @param key_entry: the key entry, which is trusted, and which matches
 *     the signer of the answer. The key entry isgood().
+ * @param suspend: returned true if the task takes too long and needs to
+ *     suspend to continue the effort later.
 * @return false if any of the rrsets in the an or ns sections of the message
 *     fail to verify. The message is then set to bogus.
 */
static int
-validate_msg_signatures(struct module_qstate* qstate, struct module_env* env,
-       struct val_env* ve, struct query_info* qchase,
-       struct reply_info* chase_reply, struct key_entry_key* key_entry)
+validate_msg_signatures(struct module_qstate* qstate, struct val_qstate* vq,
+       struct module_env* env, struct val_env* ve, struct query_info* qchase,
+       struct reply_info* chase_reply, struct key_entry_key* key_entry,
+       int* suspend)
{
       uint8_t* sname;
       size_t i, slen;
       struct ub_packed_rrset_key* s;
       enum sec_status sec;
-       int dname_seen = 0;
+       int dname_seen = 0, num_verifies = 0, verified, have_state = 0;
       char* reason = NULL;
       sldns_ede_code reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
+       *suspend = 0;
+       if(vq->msg_signatures_state) {
+               /* Pick up the state, and reset it, may not be needed now. */
+               vq->msg_signatures_state = 0;
+               have_state = 1;
+       }

       /* validate the ANSWER section */
       for(i=0; i<chase_reply->an_numrrsets; i++) {
+               if(have_state && i <= vq->msg_signatures_index)
+                       continue;
               s = chase_reply->rrsets[i];
               /* Skip the CNAME following a (validated) DNAME.
                * Because of the normalization routines in the iterator,
@@ -629,7 +661,7 @@ validate_msg_signatures(struct module_qs

               /* Verify the answer rrset */
               sec = val_verify_rrset_entry(env, ve, s, key_entry, &reason,
-                       &reason_bogus, LDNS_SECTION_ANSWER, qstate);
+                       &reason_bogus, LDNS_SECTION_ANSWER, qstate, &verified);
               /* If the (answer) rrset failed to validate, then this
                * message is BAD. */
               if(sec != sec_status_secure) {
@@ -654,14 +686,33 @@ validate_msg_signatures(struct module_qs
                       ntohs(s->rk.type) == LDNS_RR_TYPE_DNAME) {
                       dname_seen = 1;
               }
+               num_verifies += verified;
+               if(num_verifies > MAX_VALIDATE_AT_ONCE &&
+                       i+1 < (env->cfg->val_clean_additional?
+                       chase_reply->an_numrrsets+chase_reply->ns_numrrsets:
+                       chase_reply->rrset_count)) {
+                       /* If the number of RRSIGs exceeds the maximum in
+                        * one go, suspend. Only suspend if there is a next
+                        * rrset to verify, i+1<loopmax. Store where to
+                        * continue later. */
+                       *suspend = 1;
+                       vq->msg_signatures_state = 1;
+                       vq->msg_signatures_index = i;
+                       verbose(VERB_ALGO, "msg signature validation "
+                               "suspended");
+                       return 0;
+               }
       }

       /* validate the AUTHORITY section */
       for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
               chase_reply->ns_numrrsets; i++) {
+               if(have_state && i <= vq->msg_signatures_index)
+                       continue;
               s = chase_reply->rrsets[i];
               sec = val_verify_rrset_entry(env, ve, s, key_entry, &reason,
-                       &reason_bogus, LDNS_SECTION_AUTHORITY, qstate);
+                       &reason_bogus, LDNS_SECTION_AUTHORITY, qstate,
+                       &verified);
               /* If anything in the authority section fails to be secure,
                * we have a bad message. */
               if(sec != sec_status_secure) {
@@ -675,6 +726,18 @@ validate_msg_signatures(struct module_qs
                       update_reason_bogus(chase_reply, reason_bogus);
                       return 0;
               }
+               num_verifies += verified;
+               if(num_verifies > MAX_VALIDATE_AT_ONCE &&
+                       i+1 < (env->cfg->val_clean_additional?
+                       chase_reply->an_numrrsets+chase_reply->ns_numrrsets:
+                       chase_reply->rrset_count)) {
+                       *suspend = 1;
+                       vq->msg_signatures_state = 1;
+                       vq->msg_signatures_index = i;
+                       verbose(VERB_ALGO, "msg signature validation "
+                               "suspended");
+                       return 0;
+               }
       }

       /* If set, the validator should clean the additional section of
@@ -684,22 +747,103 @@ validate_msg_signatures(struct module_qs
       /* attempt to validate the ADDITIONAL section rrsets */
       for(i=chase_reply->an_numrrsets+chase_reply->ns_numrrsets;
               i<chase_reply->rrset_count; i++) {
+               if(have_state && i <= vq->msg_signatures_index)
+                       continue;
               s = chase_reply->rrsets[i];
               /* only validate rrs that have signatures with the key */
               /* leave others unchecked, those get removed later on too */
               val_find_rrset_signer(s, &sname, &slen);

+               verified = 0;
               if(sname && query_dname_compare(sname, key_entry->name)==0)
                       (void)val_verify_rrset_entry(env, ve, s, key_entry,
-                               &reason, NULL, LDNS_SECTION_ADDITIONAL, qstate);
+                               &reason, NULL, LDNS_SECTION_ADDITIONAL, qstate,
+                               &verified);
               /* the additional section can fail to be secure,
                * it is optional, check signature in case we need
                * to clean the additional section later. */
+               num_verifies += verified;
+               if(num_verifies > MAX_VALIDATE_AT_ONCE &&
+                       i+1 < chase_reply->rrset_count) {
+                       *suspend = 1;
+                       vq->msg_signatures_state = 1;
+                       vq->msg_signatures_index = i;
+                       verbose(VERB_ALGO, "msg signature validation "
+                               "suspended");
+                       return 0;
+               }
       }

       return 1;
}

+void
+validate_suspend_timer_cb(void* arg)
+{
+       struct module_qstate* qstate = (struct module_qstate*)arg;
+       verbose(VERB_ALGO, "validate_suspend timer, continue");
+       mesh_run(qstate->env->mesh, qstate->mesh_info, module_event_pass,
+               NULL);
+}
+
+/** Setup timer to continue validation of msg signatures later */
+static int
+validate_suspend_setup_timer(struct module_qstate* qstate,
+       struct val_qstate* vq, int id, enum val_state resume_state)
+{
+       struct timeval tv;
+       int usec, slack, base;
+       if(vq->suspend_count >= MAX_VALIDATION_SUSPENDS) {
+               verbose(VERB_ALGO, "validate_suspend timer: "
+                       "reached MAX_VALIDATION_SUSPENDS (%d); error out",
+                       MAX_VALIDATION_SUSPENDS);
+               errinf(qstate, "max validation suspends reached, "
+                       "too many RRSIG validations");
+               return 0;
+       }
+       verbose(VERB_ALGO, "validate_suspend timer, set for suspend");
+       vq->state = resume_state;
+       qstate->ext_state[id] = module_wait_reply;
+       if(!vq->suspend_timer) {
+               vq->suspend_timer = comm_timer_create(
+                       qstate->env->worker_base,
+                       validate_suspend_timer_cb, qstate);
+               if(!vq->suspend_timer) {
+                       log_err("validate_suspend_setup_timer: "
+                               "out of memory for comm_timer_create");
+                       return 0;
+               }
+       }
+       /* The timer is activated later, after other events in the event
+        * loop have been processed. The query state can also be deleted,
+        * when the list is full and query states are dropped. */
+       /* Extend wait time if there are a lot of queries or if this one
+        * is taking long, to keep around cpu time for ordinary queries. */
+       usec = 50000; /* 50 msec */
+       slack = 0;
+       if(qstate->env->mesh->all.count >= qstate->env->mesh->max_reply_states)
+               slack += 3;
+       else if(qstate->env->mesh->all.count >= qstate->env->mesh->max_reply_states/2)
+               slack += 2;
+       else if(qstate->env->mesh->all.count >= qstate->env->mesh->max_reply_states/4)
+               slack += 1;
+       if(vq->suspend_count > 3)
+               slack += 3;
+       else if(vq->suspend_count > 0)
+               slack += vq->suspend_count;
+       if(slack != 0 && slack <= 12 /* No numeric overflow. */) {
+               usec = usec << slack;
+       }
+       /* Spread such timeouts within 90%-100% of the original timer. */
+       base = usec * 9/10;
+       usec = base + ub_random_max(qstate->env->rnd, usec-base);
+       tv.tv_usec = (usec % 1000000);
+       tv.tv_sec = (usec / 1000000);
+       vq->suspend_count ++;
+       comm_timer_set(vq->suspend_timer, &tv);
+       return 1;
+}
+
/**
 * Detect wrong truncated response (say from BIND 9.6.1 that is forwarding
 * and saw the NS record without signatures from a referral).
@@ -798,11 +942,17 @@ remove_spurious_authority(struct reply_i
 * @param chase_reply: answer to that query to validate.
 * @param kkey: the key entry, which is trusted, and which matches
 *     the signer of the answer. The key entry isgood().
+ * @param qstate: query state for the region.
+ * @param vq: validator state for the nsec3 cache table.
+ * @param nsec3_calculations: current nsec3 hash calculations.
+ * @param suspend: returned true if the task takes too long and needs to
+ *     suspend to continue the effort later.
 */
static void
validate_positive_response(struct module_env* env, struct val_env* ve,
       struct query_info* qchase, struct reply_info* chase_reply,
-       struct key_entry_key* kkey)
+       struct key_entry_key* kkey, struct module_qstate* qstate,
+       struct val_qstate* vq, int* nsec3_calculations, int* suspend)
{
       uint8_t* wc = NULL;
       size_t wl;
@@ -811,6 +961,7 @@ validate_positive_response(struct module
       int nsec3s_seen = 0;
       size_t i;
       struct ub_packed_rrset_key* s;
+       *suspend = 0;

       /* validate the ANSWER section - this will be the answer itself */
       for(i=0; i<chase_reply->an_numrrsets; i++) {
@@ -862,17 +1013,23 @@ validate_positive_response(struct module
       /* If this was a positive wildcard response that we haven't already
        * proven, and we have NSEC3 records, try to prove it using the NSEC3
        * records. */
-       if(wc != NULL && !wc_NSEC_ok && nsec3s_seen) {
-               enum sec_status sec = nsec3_prove_wildcard(env, ve,
+       if(wc != NULL && !wc_NSEC_ok && nsec3s_seen &&
+               nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
+               enum sec_status sec = nsec3_prove_wildcard(env, ve,
                       chase_reply->rrsets+chase_reply->an_numrrsets,
-                       chase_reply->ns_numrrsets, qchase, kkey, wc);
+                       chase_reply->ns_numrrsets, qchase, kkey, wc,
+                       &vq->nsec3_cache_table, nsec3_calculations);
               if(sec == sec_status_insecure) {
                       verbose(VERB_ALGO, "Positive wildcard response is "
                               "insecure");
                       chase_reply->security = sec_status_insecure;
                       return;
-               } else if(sec == sec_status_secure)
+               } else if(sec == sec_status_secure) {
                       wc_NSEC_ok = 1;
+               } else if(sec == sec_status_unchecked) {
+                       *suspend = 1;
+                       return;
+               }
       }

       /* If after all this, we still haven't proven the positive wildcard
@@ -904,11 +1061,17 @@ validate_positive_response(struct module
 * @param chase_reply: answer to that query to validate.
 * @param kkey: the key entry, which is trusted, and which matches
 *     the signer of the answer. The key entry isgood().
+ * @param qstate: query state for the region.
+ * @param vq: validator state for the nsec3 cache table.
+ * @param nsec3_calculations: current nsec3 hash calculations.
+ * @param suspend: returned true if the task takes too long and needs to
+ *     suspend to continue the effort later.
 */
static void
validate_nodata_response(struct module_env* env, struct val_env* ve,
       struct query_info* qchase, struct reply_info* chase_reply,
-       struct key_entry_key* kkey)
+       struct key_entry_key* kkey, struct module_qstate* qstate,
+       struct val_qstate* vq, int* nsec3_calculations, int* suspend)
{
       /* Since we are here, there must be nothing in the ANSWER section to
        * validate. */
@@ -925,6 +1088,7 @@ validate_nodata_response(struct module_e
       int nsec3s_seen = 0; /* nsec3s seen */
       struct ub_packed_rrset_key* s;
       size_t i;
+       *suspend = 0;

       for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
               chase_reply->ns_numrrsets; i++) {
@@ -963,16 +1127,23 @@ validate_nodata_response(struct module_e
               }
       }

-       if(!has_valid_nsec && nsec3s_seen) {
+       if(!has_valid_nsec && nsec3s_seen &&
+               nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
               enum sec_status sec = nsec3_prove_nodata(env, ve,
                       chase_reply->rrsets+chase_reply->an_numrrsets,
-                       chase_reply->ns_numrrsets, qchase, kkey);
+                       chase_reply->ns_numrrsets, qchase, kkey,
+                       &vq->nsec3_cache_table, nsec3_calculations);
               if(sec == sec_status_insecure) {
                       verbose(VERB_ALGO, "NODATA response is insecure");
                       chase_reply->security = sec_status_insecure;
                       return;
-               } else if(sec == sec_status_secure)
+               } else if(sec == sec_status_secure) {
                       has_valid_nsec = 1;
+               } else if(sec == sec_status_unchecked) {
+                       /* check is incomplete; suspend */
+                       *suspend = 1;
+                       return;
+               }
       }

       if(!has_valid_nsec) {
@@ -1004,11 +1175,18 @@ validate_nodata_response(struct module_e
 * @param kkey: the key entry, which is trusted, and which matches
 *     the signer of the answer. The key entry isgood().
 * @param rcode: adjusted RCODE, in case of RCODE/proof mismatch leniency.
+ * @param qstate: query state for the region.
+ * @param vq: validator state for the nsec3 cache table.
+ * @param nsec3_calculations: current nsec3 hash calculations.
+ * @param suspend: returned true if the task takes too long and needs to
+ *     suspend to continue the effort later.
 */
static void
validate_nameerror_response(struct module_env* env, struct val_env* ve,
       struct query_info* qchase, struct reply_info* chase_reply,
-       struct key_entry_key* kkey, int* rcode)
+       struct key_entry_key* kkey, int* rcode,
+       struct module_qstate* qstate, struct val_qstate* vq,
+       int* nsec3_calculations, int* suspend)
{
       int has_valid_nsec = 0;
       int has_valid_wnsec = 0;
@@ -1018,6 +1196,7 @@ validate_nameerror_response(struct modul
       uint8_t* ce;
       int ce_labs = 0;
       int prev_ce_labs = 0;
+       *suspend = 0;

       for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
               chase_reply->ns_numrrsets; i++) {
@@ -1047,13 +1226,18 @@ validate_nameerror_response(struct modul
                       nsec3s_seen = 1;
       }

-       if((!has_valid_nsec || !has_valid_wnsec) && nsec3s_seen) {
+       if((!has_valid_nsec || !has_valid_wnsec) && nsec3s_seen &&
+               nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
               /* use NSEC3 proof, both answer and auth rrsets, in case
                * NSEC3s end up in the answer (due to qtype=NSEC3 or so) */
               chase_reply->security = nsec3_prove_nameerror(env, ve,
                       chase_reply->rrsets, chase_reply->an_numrrsets+
-                       chase_reply->ns_numrrsets, qchase, kkey);
-               if(chase_reply->security != sec_status_secure) {
+                       chase_reply->ns_numrrsets, qchase, kkey,
+                       &vq->nsec3_cache_table, nsec3_calculations);
+               if(chase_reply->security == sec_status_unchecked) {
+                       *suspend = 1;
+                       return;
+               } else if(chase_reply->security != sec_status_secure) {
                       verbose(VERB_QUERY, "NameError response failed nsec, "
                               "nsec3 proof was %s", sec_status_to_string(
                               chase_reply->security));
@@ -1065,26 +1249,34 @@ validate_nameerror_response(struct modul

       /* If the message fails to prove either condition, it is bogus. */
       if(!has_valid_nsec) {
+               validate_nodata_response(env, ve, qchase, chase_reply, kkey,
+                       qstate, vq, nsec3_calculations, suspend);
+               if(*suspend) return;
               verbose(VERB_QUERY, "NameError response has failed to prove: "
                         "qname does not exist");
-               chase_reply->security = sec_status_bogus;
-               update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
               /* Be lenient with RCODE in NSEC NameError responses */
-               validate_nodata_response(env, ve, qchase, chase_reply, kkey);
-               if (chase_reply->security == sec_status_secure)
+               if(chase_reply->security == sec_status_secure) {
                       *rcode = LDNS_RCODE_NOERROR;
+               } else {
+                       chase_reply->security = sec_status_bogus;
+                       update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
+               }
               return;
       }

       if(!has_valid_wnsec) {
+               validate_nodata_response(env, ve, qchase, chase_reply, kkey,
+                       qstate, vq, nsec3_calculations, suspend);
+               if(*suspend) return;
               verbose(VERB_QUERY, "NameError response has failed to prove: "
                         "covering wildcard does not exist");
-               chase_reply->security = sec_status_bogus;
-               update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
               /* Be lenient with RCODE in NSEC NameError responses */
-               validate_nodata_response(env, ve, qchase, chase_reply, kkey);
-               if (chase_reply->security == sec_status_secure)
+               if (chase_reply->security == sec_status_secure) {
                       *rcode = LDNS_RCODE_NOERROR;
+               } else {
+                       chase_reply->security = sec_status_bogus;
+                       update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
+               }
               return;
       }

@@ -1144,11 +1336,17 @@ validate_referral_response(struct reply_
 * @param chase_reply: answer to that query to validate.
 * @param kkey: the key entry, which is trusted, and which matches
 *     the signer of the answer. The key entry isgood().
+ * @param qstate: query state for the region.
+ * @param vq: validator state for the nsec3 cache table.
+ * @param nsec3_calculations: current nsec3 hash calculations.
+ * @param suspend: returned true if the task takes too long and needs to
+ *     suspend to continue the effort later.
 */
static void
validate_any_response(struct module_env* env, struct val_env* ve,
       struct query_info* qchase, struct reply_info* chase_reply,
-       struct key_entry_key* kkey)
+       struct key_entry_key* kkey, struct module_qstate* qstate,
+       struct val_qstate* vq, int* nsec3_calculations, int* suspend)
{
       /* all answer and auth rrsets already verified */
       /* but check if a wildcard response is given, then check NSEC/NSEC3
@@ -1159,6 +1357,7 @@ validate_any_response(struct module_env*
       int nsec3s_seen = 0;
       size_t i;
       struct ub_packed_rrset_key* s;
+       *suspend = 0;

       if(qchase->qtype != LDNS_RR_TYPE_ANY) {
               log_err("internal error: ANY validation called for non-ANY");
@@ -1213,19 +1412,25 @@ validate_any_response(struct module_env*
       /* If this was a positive wildcard response that we haven't already
        * proven, and we have NSEC3 records, try to prove it using the NSEC3
        * records. */
-       if(wc != NULL && !wc_NSEC_ok && nsec3s_seen) {
+       if(wc != NULL && !wc_NSEC_ok && nsec3s_seen &&
+               nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
               /* look both in answer and auth section for NSEC3s */
-               enum sec_status sec = nsec3_prove_wildcard(env, ve,
+               enum sec_status sec = nsec3_prove_wildcard(env, ve,
                       chase_reply->rrsets,
-                       chase_reply->an_numrrsets+chase_reply->ns_numrrsets,
-                       qchase, kkey, wc);
+                       chase_reply->an_numrrsets+chase_reply->ns_numrrsets,
+                       qchase, kkey, wc, &vq->nsec3_cache_table,
+                       nsec3_calculations);
               if(sec == sec_status_insecure) {
                       verbose(VERB_ALGO, "Positive ANY wildcard response is "
                               "insecure");
                       chase_reply->security = sec_status_insecure;
                       return;
-               } else if(sec == sec_status_secure)
+               } else if(sec == sec_status_secure) {
                       wc_NSEC_ok = 1;
+               } else if(sec == sec_status_unchecked) {
+                       *suspend = 1;
+                       return;
+               }
       }

       /* If after all this, we still haven't proven the positive wildcard
@@ -1258,11 +1463,17 @@ validate_any_response(struct module_env*
 * @param chase_reply: answer to that query to validate.
 * @param kkey: the key entry, which is trusted, and which matches
 *     the signer of the answer. The key entry isgood().
+ * @param qstate: query state for the region.
+ * @param vq: validator state for the nsec3 cache table.
+ * @param nsec3_calculations: current nsec3 hash calculations.
+ * @param suspend: returned true if the task takes too long and needs to
+ *     suspend to continue the effort later.
 */
static void
validate_cname_response(struct module_env* env, struct val_env* ve,
       struct query_info* qchase, struct reply_info* chase_reply,
-       struct key_entry_key* kkey)
+       struct key_entry_key* kkey, struct module_qstate* qstate,
+       struct val_qstate* vq, int* nsec3_calculations, int* suspend)
{
       uint8_t* wc = NULL;
       size_t wl;
@@ -1270,6 +1481,7 @@ validate_cname_response(struct module_en
       int nsec3s_seen = 0;
       size_t i;
       struct ub_packed_rrset_key* s;
+       *suspend = 0;

       /* validate the ANSWER section - this will be the CNAME (+DNAME) */
       for(i=0; i<chase_reply->an_numrrsets; i++) {
@@ -1334,17 +1546,23 @@ validate_cname_response(struct module_en
       /* If this was a positive wildcard response that we haven't already
        * proven, and we have NSEC3 records, try to prove it using the NSEC3
        * records. */
-       if(wc != NULL && !wc_NSEC_ok && nsec3s_seen) {
-               enum sec_status sec = nsec3_prove_wildcard(env, ve,
+       if(wc != NULL && !wc_NSEC_ok && nsec3s_seen &&
+               nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
+               enum sec_status sec = nsec3_prove_wildcard(env, ve,
                       chase_reply->rrsets+chase_reply->an_numrrsets,
-                       chase_reply->ns_numrrsets, qchase, kkey, wc);
+                       chase_reply->ns_numrrsets, qchase, kkey, wc,
+                       &vq->nsec3_cache_table, nsec3_calculations);
               if(sec == sec_status_insecure) {
                       verbose(VERB_ALGO, "wildcard CNAME response is "
                               "insecure");
                       chase_reply->security = sec_status_insecure;
                       return;
-               } else if(sec == sec_status_secure)
+               } else if(sec == sec_status_secure) {
                       wc_NSEC_ok = 1;
+               } else if(sec == sec_status_unchecked) {
+                       *suspend = 1;
+                       return;
+               }
       }

       /* If after all this, we still haven't proven the positive wildcard
@@ -1375,11 +1593,17 @@ validate_cname_response(struct module_en
 * @param chase_reply: answer to that query to validate.
 * @param kkey: the key entry, which is trusted, and which matches
 *     the signer of the answer. The key entry isgood().
+ * @param qstate: query state for the region.
+ * @param vq: validator state for the nsec3 cache table.
+ * @param nsec3_calculations: current nsec3 hash calculations.
+ * @param suspend: returned true if the task takes too long and needs to
+ *     suspend to continue the effort later.
 */
static void
validate_cname_noanswer_response(struct module_env* env, struct val_env* ve,
       struct query_info* qchase, struct reply_info* chase_reply,
-       struct key_entry_key* kkey)
+       struct key_entry_key* kkey, struct module_qstate* qstate,
+       struct val_qstate* vq, int* nsec3_calculations, int* suspend)
{
       int nodata_valid_nsec = 0; /* If true, then NODATA has been proven.*/
       uint8_t* ce = NULL; /* for wildcard nodata responses. This is the
@@ -1393,6 +1617,7 @@ validate_cname_noanswer_response(struct
       uint8_t* nsec_ce; /* Used to find the NSEC with the longest ce */
       int ce_labs = 0;
       int prev_ce_labs = 0;
+       *suspend = 0;

       /* the AUTHORITY section */
       for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
@@ -1458,11 +1683,13 @@ validate_cname_noanswer_response(struct
               update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
               return;
       }
-       if(!nodata_valid_nsec && !nxdomain_valid_nsec && nsec3s_seen) {
+       if(!nodata_valid_nsec && !nxdomain_valid_nsec && nsec3s_seen &&
+               nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
               int nodata;
               enum sec_status sec = nsec3_prove_nxornodata(env, ve,
                       chase_reply->rrsets+chase_reply->an_numrrsets,
-                       chase_reply->ns_numrrsets, qchase, kkey, &nodata);
+                       chase_reply->ns_numrrsets, qchase, kkey, &nodata,
+                       &vq->nsec3_cache_table, nsec3_calculations);
               if(sec == sec_status_insecure) {
                       verbose(VERB_ALGO, "CNAMEchain to noanswer response "
                               "is insecure");
@@ -1472,6 +1699,9 @@ validate_cname_noanswer_response(struct
                       if(nodata)
                               nodata_valid_nsec = 1;
                       else    nxdomain_valid_nsec = 1;
+               } else if(sec == sec_status_unchecked) {
+                       *suspend = 1;
+                       return;
               }
       }

@@ -1815,13 +2045,37 @@ processFindKey(struct module_qstate* qst
                * Uses negative cache for NSEC3 lookup of DS responses. */
               /* only if cache not blacklisted, of course */
               struct dns_msg* msg;
-               if(!qstate->blacklist && !vq->chain_blacklist &&
+               int suspend;
+               if(vq->sub_ds_msg) {
+                       /* We have a suspended DS reply from a sub-query;
+                        * process it. */
+                       verbose(VERB_ALGO, "Process suspended sub DS response");
+                       msg = vq->sub_ds_msg;
+                       process_ds_response(qstate, vq, id, LDNS_RCODE_NOERROR,
+                               msg, &msg->qinfo, NULL, &suspend);
+                       if(suspend) {
+                               /* we'll come back here later to continue */
+                               if(!validate_suspend_setup_timer(qstate, vq,
+                                       id, VAL_FINDKEY_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
+                       vq->sub_ds_msg = NULL;
+                       return 1; /* continue processing ds-response results */
+               } else if(!qstate->blacklist && !vq->chain_blacklist &&
                       (msg=val_find_DS(qstate->env, target_key_name,
                       target_key_len, vq->qchase.qclass, qstate->region,
                       vq->key_entry->name)) ) {
                       verbose(VERB_ALGO, "Process cached DS response");
                       process_ds_response(qstate, vq, id, LDNS_RCODE_NOERROR,
-                               msg, &msg->qinfo, NULL);
+                               msg, &msg->qinfo, NULL, &suspend);
+                       if(suspend) {
+                               /* we'll come back here later to continue */
+                               if(!validate_suspend_setup_timer(qstate, vq,
+                                       id, VAL_FINDKEY_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
                       return 1; /* continue processing ds-response results */
               }
               if(!generate_request(qstate, id, target_key_name,
@@ -1864,7 +2118,7 @@ processValidate(struct module_qstate* qs
       struct val_env* ve, int id)
{
       enum val_classification subtype;
-       int rcode;
+       int rcode, suspend, nsec3_calculations = 0;

       if(!vq->key_entry) {
               verbose(VERB_ALGO, "validate: no key entry, failed");
@@ -1921,8 +2175,14 @@ processValidate(struct module_qstate* qs

       /* check signatures in the message;
        * answer and authority must be valid, additional is only checked. */
-       if(!validate_msg_signatures(qstate, qstate->env, ve, &vq->qchase,
-               vq->chase_reply, vq->key_entry)) {
+       if(!validate_msg_signatures(qstate, vq, qstate->env, ve, &vq->qchase,
+               vq->chase_reply, vq->key_entry, &suspend)) {
+               if(suspend) {
+                       if(!validate_suspend_setup_timer(qstate, vq,
+                               id, VAL_VALIDATE_STATE))
+                               return val_error(qstate, id);
+                       return 0;
+               }
               /* workaround bad recursor out there that truncates (even
                * with EDNS4k) to 512 by removing RRSIG from auth section
                * for positive replies*/
@@ -1951,7 +2211,14 @@ processValidate(struct module_qstate* qs
               case VAL_CLASS_POSITIVE:
                       verbose(VERB_ALGO, "Validating a positive response");
                       validate_positive_response(qstate->env, ve,
-                               &vq->qchase, vq->chase_reply, vq->key_entry);
+                               &vq->qchase, vq->chase_reply, vq->key_entry,
+                               qstate, vq, &nsec3_calculations, &suspend);
+                       if(suspend) {
+                               if(!validate_suspend_setup_timer(qstate,
+                                       vq, id, VAL_VALIDATE_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
                       verbose(VERB_DETAIL, "validate(positive): %s",
                               sec_status_to_string(
                               vq->chase_reply->security));
@@ -1960,7 +2227,14 @@ processValidate(struct module_qstate* qs
               case VAL_CLASS_NODATA:
                       verbose(VERB_ALGO, "Validating a nodata response");
                       validate_nodata_response(qstate->env, ve,
-                               &vq->qchase, vq->chase_reply, vq->key_entry);
+                               &vq->qchase, vq->chase_reply, vq->key_entry,
+                               qstate, vq, &nsec3_calculations, &suspend);
+                       if(suspend) {
+                               if(!validate_suspend_setup_timer(qstate,
+                                       vq, id, VAL_VALIDATE_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
                       verbose(VERB_DETAIL, "validate(nodata): %s",
                               sec_status_to_string(
                               vq->chase_reply->security));
@@ -1970,7 +2244,14 @@ processValidate(struct module_qstate* qs
                       rcode = (int)FLAGS_GET_RCODE(vq->orig_msg->rep->flags);
                       verbose(VERB_ALGO, "Validating a nxdomain response");
                       validate_nameerror_response(qstate->env, ve,
-                               &vq->qchase, vq->chase_reply, vq->key_entry, &rcode);
+                               &vq->qchase, vq->chase_reply, vq->key_entry, &rcode,
+                               qstate, vq, &nsec3_calculations, &suspend);
+                       if(suspend) {
+                               if(!validate_suspend_setup_timer(qstate,
+                                       vq, id, VAL_VALIDATE_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
                       verbose(VERB_DETAIL, "validate(nxdomain): %s",
                               sec_status_to_string(
                               vq->chase_reply->security));
@@ -1981,7 +2262,14 @@ processValidate(struct module_qstate* qs
               case VAL_CLASS_CNAME:
                       verbose(VERB_ALGO, "Validating a cname response");
                       validate_cname_response(qstate->env, ve,
-                               &vq->qchase, vq->chase_reply, vq->key_entry);
+                               &vq->qchase, vq->chase_reply, vq->key_entry,
+                               qstate, vq, &nsec3_calculations, &suspend);
+                       if(suspend) {
+                               if(!validate_suspend_setup_timer(qstate,
+                                       vq, id, VAL_VALIDATE_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
                       verbose(VERB_DETAIL, "validate(cname): %s",
                               sec_status_to_string(
                               vq->chase_reply->security));
@@ -1991,7 +2279,14 @@ processValidate(struct module_qstate* qs
                       verbose(VERB_ALGO, "Validating a cname noanswer "
                               "response");
                       validate_cname_noanswer_response(qstate->env, ve,
-                               &vq->qchase, vq->chase_reply, vq->key_entry);
+                               &vq->qchase, vq->chase_reply, vq->key_entry,
+                               qstate, vq, &nsec3_calculations, &suspend);
+                       if(suspend) {
+                               if(!validate_suspend_setup_timer(qstate,
+                                       vq, id, VAL_VALIDATE_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
                       verbose(VERB_DETAIL, "validate(cname_noanswer): %s",
                               sec_status_to_string(
                               vq->chase_reply->security));
@@ -2008,8 +2303,15 @@ processValidate(struct module_qstate* qs
               case VAL_CLASS_ANY:
                       verbose(VERB_ALGO, "Validating a positive ANY "
                               "response");
-                       validate_any_response(qstate->env, ve, &vq->qchase,
-                               vq->chase_reply, vq->key_entry);
+                       validate_any_response(qstate->env, ve, &vq->qchase,
+                               vq->chase_reply, vq->key_entry, qstate, vq,
+                               &nsec3_calculations, &suspend);
+                       if(suspend) {
+                               if(!validate_suspend_setup_timer(qstate,
+                                       vq, id, VAL_VALIDATE_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
                       verbose(VERB_DETAIL, "validate(positive_any): %s",
                               sec_status_to_string(
                               vq->chase_reply->security));
@@ -2118,16 +2420,13 @@ processFinished(struct module_qstate* qs
       if(vq->orig_msg->rep->security == sec_status_bogus) {
               /* see if we can try again to fetch data */
               if(vq->restart_count < ve->max_restart) {
-                       int restart_count = vq->restart_count+1;
                       verbose(VERB_ALGO, "validation failed, "
                               "blacklist and retry to fetch data");
                       val_blacklist(&qstate->blacklist, qstate->region,
                               qstate->reply_origin, 0);
                       qstate->reply_origin = NULL;
                       qstate->errinf = NULL;
-                       memset(vq, 0, sizeof(*vq));
-                       vq->restart_count = restart_count;
-                       vq->state = VAL_INIT_STATE;
+                       val_restart(vq);
                       verbose(VERB_ALGO, "pass back to next module");
                       qstate->ext_state[id] = module_restart_next;
                       return 0;
@@ -2454,7 +2753,10 @@ primeResponseToKE(struct ub_packed_rrset
 *     DS response indicated an end to secure space, is_good if the DS
 *     validated. It returns ke=NULL if the DS response indicated that the
 *     request wasn't a delegation point.
- * @return 0 on servfail error (malloc failure).
+ * @return
+ *     0 on success,
+ *     1 on servfail error (malloc failure),
+ *     2 on NSEC3 suspend.
 */
static int
ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
@@ -2465,6 +2767,7 @@ ds_response_to_ke(struct module_qstate*
       char* reason = NULL;
       sldns_ede_code reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
       enum val_classification subtype;
+       int verified;
       if(rcode != LDNS_RCODE_NOERROR) {
               char rc[16];
               rc[0]=0;
@@ -2495,7 +2798,7 @@ ds_response_to_ke(struct module_qstate*
               /* Verify only returns BOGUS or SECURE. If the rrset is
                * bogus, then we are done. */
               sec = val_verify_rrset_entry(qstate->env, ve, ds,
-                       vq->key_entry, &reason, &reason_bogus, LDNS_SECTION_ANSWER, qstate);
+                       vq->key_entry, &reason, &reason_bogus, LDNS_SECTION_ANSWER, qstate, &verified);
               if(sec != sec_status_secure) {
                       verbose(VERB_DETAIL, "DS rrset in DS response did "
                               "not verify");
@@ -2513,7 +2816,7 @@ ds_response_to_ke(struct module_qstate*
                               ub_packed_rrset_ttl(ds),
                               LDNS_EDE_UNSUPPORTED_DS_DIGEST, NULL,
                               *qstate->env->now);
-                       return (*ke) != NULL;
+                       return (*ke) == NULL;
               }

               /* Otherwise, we return the positive response. */
@@ -2521,7 +2824,7 @@ ds_response_to_ke(struct module_qstate*
               *ke = key_entry_create_rrset(qstate->region,
                       qinfo->qname, qinfo->qname_len, qinfo->qclass, ds,
                       NULL, LDNS_EDE_NONE, NULL, *qstate->env->now);
-               return (*ke) != NULL;
+               return (*ke) == NULL;
       } else if(subtype == VAL_CLASS_NODATA ||
               subtype == VAL_CLASS_NAMEERROR) {
               /* NODATA means that the qname exists, but that there was
@@ -2555,12 +2858,12 @@ ds_response_to_ke(struct module_qstate*
                                       qinfo->qclass, proof_ttl,
                                       LDNS_EDE_NONE, NULL,
                                       *qstate->env->now);
-                               return (*ke) != NULL;
+                               return (*ke) == NULL;
                       case sec_status_insecure:
                               verbose(VERB_DETAIL, "NSEC RRset for the "
                                 "referral proved not a delegation point");
                               *ke = NULL;
-                               return 1;
+                               return 0;
                       case sec_status_bogus:
                               verbose(VERB_DETAIL, "NSEC RRset for the "
                                       "referral did not prove no DS.");
@@ -2572,10 +2875,17 @@ ds_response_to_ke(struct module_qstate*
                               break;
               }

+               if(!nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
+                       log_err("malloc failure in ds_response_to_ke for "
+                               "NSEC3 cache");
+                       reason = "malloc failure";
+                       errinf_ede(qstate, reason, 0);
+                       goto return_bogus;
+               }
               sec = nsec3_prove_nods(qstate->env, ve,
                       msg->rep->rrsets + msg->rep->an_numrrsets,
                       msg->rep->ns_numrrsets, qinfo, vq->key_entry, &reason,
-                       &reason_bogus, qstate);
+                       &reason_bogus, qstate, &vq->nsec3_cache_table);
               switch(sec) {
                       case sec_status_insecure:
                               /* case insecure also continues to unsigned
@@ -2589,18 +2899,19 @@ ds_response_to_ke(struct module_qstate*
                                       qinfo->qclass, proof_ttl,
                                       LDNS_EDE_NONE, NULL,
                                       *qstate->env->now);
-                               return (*ke) != NULL;
+                               return (*ke) == NULL;
                       case sec_status_indeterminate:
                               verbose(VERB_DETAIL, "NSEC3s for the "
                                 "referral proved no delegation");
                               *ke = NULL;
-                               return 1;
+                               return 0;
                       case sec_status_bogus:
                               verbose(VERB_DETAIL, "NSEC3s for the "
                                       "referral did not prove no DS.");
                               errinf_ede(qstate, reason, reason_bogus);
                               goto return_bogus;
                       case sec_status_unchecked:
+                               return 2;
                       default:
                               /* NSEC3 proof did not work */
                               break;
@@ -2641,13 +2952,13 @@ ds_response_to_ke(struct module_qstate*
               }
               sec = val_verify_rrset_entry(qstate->env, ve, cname,
                       vq->key_entry, &reason, &reason_bogus,
-                       LDNS_SECTION_ANSWER, qstate);
+                       LDNS_SECTION_ANSWER, qstate, &verified);
               if(sec == sec_status_secure) {
                       verbose(VERB_ALGO, "CNAME validated, "
                               "proof that DS does not exist");
                       /* and that it is not a referral point */
                       *ke = NULL;
-                       return 1;
+                       return 0;
               }
               errinf(qstate, "CNAME in DS response was not secure.");
               errinf_ede(qstate, reason, reason_bogus);
@@ -2671,7 +2982,7 @@ return_bogus:
       *ke = key_entry_create_bad(qstate->region, qinfo->qname,
               qinfo->qname_len, qinfo->qclass, BOGUS_KEY_TTL,
               reason_bogus, reason, *qstate->env->now);
-       return (*ke) != NULL;
+       return (*ke) == NULL;
}

/**
@@ -2692,17 +3003,31 @@ return_bogus:
static void
process_ds_response(struct module_qstate* qstate, struct val_qstate* vq,
       int id, int rcode, struct dns_msg* msg, struct query_info* qinfo,
-       struct sock_list* origin)
+       struct sock_list* origin, int* suspend)
{
       struct val_env* ve = (struct val_env*)qstate->env->modinfo[id];
       struct key_entry_key* dske = NULL;
       uint8_t* olds = vq->empty_DS_name;
+       int ret;
+       *suspend = 0;
       vq->empty_DS_name = NULL;
-       if(!ds_response_to_ke(qstate, vq, id, rcode, msg, qinfo, &dske)) {
+       ret = ds_response_to_ke(qstate, vq, id, rcode, msg, qinfo, &dske);
+       if(ret != 0) {
+               switch(ret) {
+               case 1:
                       log_err("malloc failure in process_ds_response");
                       vq->key_entry = NULL; /* make it error */
                       vq->state = VAL_VALIDATE_STATE;
                       return;
+               case 2:
+                       *suspend = 1;
+                       return;
+               default:
+                       log_err("unhandled error value for ds_response_to_ke");
+                       vq->key_entry = NULL; /* make it error */
+                       vq->state = VAL_VALIDATE_STATE;
+                       return;
+               }
       }
       if(dske == NULL) {
               vq->empty_DS_name = regional_alloc_init(qstate->region,
@@ -2954,9 +3279,26 @@ val_inform_super(struct module_qstate* q
               return;
       }
       if(qstate->qinfo.qtype == LDNS_RR_TYPE_DS) {
+               int suspend;
               process_ds_response(super, vq, id, qstate->return_rcode,
-                       qstate->return_msg, &qstate->qinfo,
-                       qstate->reply_origin);
+                       qstate->return_msg, &qstate->qinfo,
+                       qstate->reply_origin, &suspend);
+               /* If NSEC3 was needed during validation, NULL the NSEC3 cache;
+                * it will be re-initiated if needed later on.
+                * Validation (and the cache table) are happening/allocated in
+                * the super qstate whilst the RRs are allocated (and pointed
+                * to) in this sub qstate. */
+               if(vq->nsec3_cache_table.ct) {
+                       vq->nsec3_cache_table.ct = NULL;
+               }
+               if(suspend) {
+                       /* deep copy the return_msg to vq->sub_ds_msg; it will
+                        * be resumed later in the super state with the caveat
+                        * that the initial calculations will be re-caclulated
+                        * and re-suspended there before continuing. */
+                       vq->sub_ds_msg = dns_msg_deepcopy_region(
+                               qstate->return_msg, super->region);
+               }
               return;
       } else if(qstate->qinfo.qtype == LDNS_RR_TYPE_DNSKEY) {
               process_dnskey_response(super, vq, id, qstate->return_rcode,
@@ -2970,8 +3312,15 @@ val_inform_super(struct module_qstate* q
void
val_clear(struct module_qstate* qstate, int id)
{
+       struct val_qstate* vq;
       if(!qstate)
               return;
+       vq = (struct val_qstate*)qstate->minfo[id];
+       if(vq) {
+               if(vq->suspend_timer) {
+                       comm_timer_delete(vq->suspend_timer);
+               }
+       }
       /* everything is allocated in the region, so assign NULL */
       qstate->minfo[id] = NULL;
}
Index: sbin/unwind/libunbound/validator/validator.h
===================================================================
RCS file: /cvs/src/sbin/unwind/libunbound/validator/validator.h,v
diff -u -p -r1.5 validator.h
--- sbin/unwind/libunbound/validator/validator.h        18 Jun 2022 16:20:14 -0000      1.5
+++ sbin/unwind/libunbound/validator/validator.h        12 Feb 2024 21:08:03 -0000
@@ -45,11 +45,13 @@
#include "util/module.h"
#include "util/data/msgreply.h"
#include "validator/val_utils.h"
+#include "validator/val_nsec3.h"
struct val_anchors;
struct key_cache;
struct key_entry_key;
struct val_neg_cache;
struct config_strlist;
+struct comm_timer;

/**
 * This is the TTL to use when a trust anchor fails to prime. A trust anchor
@@ -215,6 +217,19 @@ struct val_qstate {

       /** true if this state is waiting to prime a trust anchor */
       int wait_prime_ta;
+
+       /** State to continue with RRSIG validation in a message later */
+       int msg_signatures_state;
+       /** The rrset index for the msg signatures to continue from */
+       size_t msg_signatures_index;
+       /** Cache table for NSEC3 hashes */
+       struct nsec3_cache_table nsec3_cache_table;
+       /** DS message from sub if it got suspended from NSEC3 calculations */
+       struct dns_msg* sub_ds_msg;
+       /** The timer to resume processing msg signatures */
+       struct comm_timer* suspend_timer;
+       /** Number of suspends */
+       int suspend_count;
};

/**
@@ -261,5 +276,8 @@ void val_clear(struct module_qstate* qst
 * @return memory in use in bytes.
 */
size_t val_get_mem(struct module_env* env, int id);
+
+/** Timer callback for msg signatures continue timer */
+void validate_suspend_timer_cb(void* arg);

#endif /* VALIDATOR_VALIDATOR_H */
Index: usr.sbin/unbound/services/authzone.c
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/services/authzone.c,v
diff -u -p -r1.23 authzone.c
--- usr.sbin/unbound/services/authzone.c        5 Sep 2023 11:12:10 -0000       1.23
+++ usr.sbin/unbound/services/authzone.c        12 Feb 2024 21:08:03 -0000
@@ -7771,6 +7771,7 @@ static int zonemd_dnssec_verify_rrset(st
       enum sec_status sec;
       struct val_env* ve;
       int m;
+       int verified = 0;
       m = modstack_find(mods, "validator");
       if(m == -1) {
               auth_zone_log(z->name, VERB_ALGO, "zonemd dnssec verify: have "
@@ -7794,7 +7795,7 @@ static int zonemd_dnssec_verify_rrset(st
                       "zonemd: verify %s RRset with DNSKEY", typestr);
       }
       sec = dnskeyset_verify_rrset(env, ve, &pk, dnskey, sigalg, why_bogus, NULL,
-               LDNS_SECTION_ANSWER, NULL);
+               LDNS_SECTION_ANSWER, NULL, &verified);
       if(sec == sec_status_secure) {
               return 1;
       }
Index: usr.sbin/unbound/services/cache/dns.c
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/services/cache/dns.c,v
diff -u -p -r1.21 dns.c
--- usr.sbin/unbound/services/cache/dns.c       5 Sep 2023 11:12:10 -0000       1.21
+++ usr.sbin/unbound/services/cache/dns.c       12 Feb 2024 21:08:03 -0000
@@ -690,6 +690,28 @@ tomsg(struct module_env* env, struct que
       return msg;
}

+struct dns_msg*
+dns_msg_deepcopy_region(struct dns_msg* origin, struct regional* region)
+{
+       size_t i;
+       struct dns_msg* res = NULL;
+       res = gen_dns_msg(region, &origin->qinfo, origin->rep->rrset_count);
+       if(!res) return NULL;
+       *res->rep = *origin->rep;
+       if(origin->rep->reason_bogus_str) {
+               res->rep->reason_bogus_str = regional_strdup(region,
+                       origin->rep->reason_bogus_str);
+       }
+       for(i=0; i<res->rep->rrset_count; i++) {
+               res->rep->rrsets[i] = packed_rrset_copy_region(
+                       origin->rep->rrsets[i], region, 0);
+               if(!res->rep->rrsets[i]) {
+                       return NULL;
+               }
+       }
+       return res;
+}
+
/** synthesize RRset-only response from cached RRset item */
static struct dns_msg*
rrset_msg(struct ub_packed_rrset_key* rrset, struct regional* region,
Index: usr.sbin/unbound/services/cache/dns.h
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/services/cache/dns.h,v
diff -u -p -r1.10 dns.h
--- usr.sbin/unbound/services/cache/dns.h       29 Aug 2022 16:05:00 -0000      1.10
+++ usr.sbin/unbound/services/cache/dns.h       12 Feb 2024 21:08:03 -0000
@@ -164,6 +164,15 @@ struct dns_msg* tomsg(struct module_env*
       struct reply_info* r, struct regional* region, time_t now,
       int allow_expired, struct regional* scratch);

+/**
+ * Deep copy a dns_msg to a region.
+ * @param origin: the dns_msg to copy.
+ * @param region: the region to copy all the data to.
+ * @return the new dns_msg or NULL on malloc error.
+ */
+struct dns_msg* dns_msg_deepcopy_region(struct dns_msg* origin,
+       struct regional* region);
+
/**
 * Find cached message
 * @param env: module environment with the DNS cache.
Index: usr.sbin/unbound/testcode/unitverify.c
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/testcode/unitverify.c,v
diff -u -p -r1.1.1.3 unitverify.c
--- usr.sbin/unbound/testcode/unitverify.c      7 Jun 2022 15:40:02 -0000       1.1.1.3
+++ usr.sbin/unbound/testcode/unitverify.c      12 Feb 2024 21:08:03 -0000
@@ -180,6 +180,7 @@ verifytest_rrset(struct module_env* env,
       enum sec_status sec;
       char* reason = NULL;
       uint8_t sigalg[ALGO_NEEDS_MAX+1];
+       int verified = 0;
       if(vsig) {
               log_nametypeclass(VERB_QUERY, "verify of rrset",
                       rrset->rk.dname, ntohs(rrset->rk.type),
@@ -188,7 +189,7 @@ verifytest_rrset(struct module_env* env,
       setup_sigalg(dnskey, sigalg); /* check all algorithms in the dnskey */
       /* ok to give null as qstate here, won't be used for answer section. */
       sec = dnskeyset_verify_rrset(env, ve, rrset, dnskey, sigalg, &reason, NULL,
-               LDNS_SECTION_ANSWER, NULL);
+               LDNS_SECTION_ANSWER, NULL, &verified);
       if(vsig) {
               printf("verify outcome is: %s %s\n", sec_status_to_string(sec),
                       reason?reason:"");
@@ -442,9 +443,9 @@ nsec3_hash_test_entry(struct entry* e, r

       ret = nsec3_hash_name(ct, region, buf, nsec3, 0, qname,
               qinfo.qname_len, &hash);
-       if(ret != 1) {
+       if(ret < 1) {
               printf("Bad nsec3_hash_name retcode %d\n", ret);
-               unit_assert(ret == 1);
+               unit_assert(ret == 1 || ret == 2);
       }
       unit_assert(hash->dname && hash->hash && hash->hash_len &&
               hash->b32 && hash->b32_len);
Index: usr.sbin/unbound/util/fptr_wlist.c
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/util/fptr_wlist.c,v
diff -u -p -r1.24 fptr_wlist.c
--- usr.sbin/unbound/util/fptr_wlist.c  5 Sep 2023 11:12:10 -0000       1.24
+++ usr.sbin/unbound/util/fptr_wlist.c  12 Feb 2024 21:08:03 -0000
@@ -131,6 +131,7 @@ fptr_whitelist_comm_timer(void (*fptr)(v
       else if(fptr == &pending_udp_timer_delay_cb) return 1;
       else if(fptr == &worker_stat_timer_cb) return 1;
       else if(fptr == &worker_probe_timer_cb) return 1;
+       else if(fptr == &validate_suspend_timer_cb) return 1;
#ifdef UB_ON_WINDOWS
       else if(fptr == &wsvc_cron_cb) return 1;
#endif
Index: usr.sbin/unbound/validator/val_nsec.c
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/validator/val_nsec.c,v
diff -u -p -r1.10 val_nsec.c
--- usr.sbin/unbound/validator/val_nsec.c       5 Sep 2023 11:12:11 -0000       1.10
+++ usr.sbin/unbound/validator/val_nsec.c       12 Feb 2024 21:08:03 -0000
@@ -181,6 +181,7 @@ nsec_verify_rrset(struct module_env* env
{
       struct packed_rrset_data* d = (struct packed_rrset_data*)
               nsec->entry.data;
+       int verified = 0;
       if(!d) return 0;
       if(d->security == sec_status_secure)
               return 1;
@@ -188,7 +189,7 @@ nsec_verify_rrset(struct module_env* env
       if(d->security == sec_status_secure)
               return 1;
       d->security = val_verify_rrset_entry(env, ve, nsec, kkey, reason,
-               reason_bogus, LDNS_SECTION_AUTHORITY, qstate);
+               reason_bogus, LDNS_SECTION_AUTHORITY, qstate, &verified);
       if(d->security == sec_status_secure) {
               rrset_update_sec_status(env->rrset_cache, nsec, *env->now);
               return 1;
Index: usr.sbin/unbound/validator/val_nsec3.c
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/validator/val_nsec3.c,v
diff -u -p -r1.7 val_nsec3.c
--- usr.sbin/unbound/validator/val_nsec3.c      7 Jun 2022 15:42:54 -0000       1.7
+++ usr.sbin/unbound/validator/val_nsec3.c      12 Feb 2024 21:08:03 -0000
@@ -57,6 +57,19 @@
/* we include nsec.h for the bitmap_has_type function */
#include "validator/val_nsec.h"
#include "sldns/sbuffer.h"
+#include "util/config_file.h"
+
+/**
+ * Max number of NSEC3 calculations at once, suspend query for later.
+ * 8 is low enough and allows for cases where multiple proofs are needed.
+ */
+#define MAX_NSEC3_CALCULATIONS 8
+/**
+ * When all allowed NSEC3 calculations at once resulted in error treat as
+ * bogus. NSEC3 hash errors are not cached and this helps breaks loops with
+ * erroneous data.
+ */
+#define MAX_NSEC3_ERRORS -1

/**
 * This function we get from ldns-compat or from base system
@@ -532,6 +545,17 @@ nsec3_hash_cmp(const void* c1, const voi
       return memcmp(s1, s2, s1len);
}

+int
+nsec3_cache_table_init(struct nsec3_cache_table* ct, struct regional* region)
+{
+       if(ct->ct) return 1;
+       ct->ct = (rbtree_type*)regional_alloc(region, sizeof(*ct->ct));
+       if(!ct->ct) return 0;
+       ct->region = region;
+       rbtree_init(ct->ct, &nsec3_hash_cmp);
+       return 1;
+}
+
size_t
nsec3_get_hashed(sldns_buffer* buf, uint8_t* nm, size_t nmlen, int algo,
       size_t iter, uint8_t* salt, size_t saltlen, uint8_t* res, size_t max)
@@ -646,7 +670,7 @@ nsec3_hash_name(rbtree_type* table, stru
       c = (struct nsec3_cached_hash*)rbtree_search(table, &looki);
       if(c) {
               *hash = c;
-               return 1;
+               return 2;
       }
       /* create a new entry */
       c = (struct nsec3_cached_hash*)regional_alloc(region, sizeof(*c));
@@ -658,10 +682,10 @@ nsec3_hash_name(rbtree_type* table, stru
       c->dname_len = dname_len;
       r = nsec3_calc_hash(region, buf, c);
       if(r != 1)
-               return r;
+               return r;  /* returns -1 or 0 */
       r = nsec3_calc_b32(region, buf, c);
       if(r != 1)
-               return r;
+               return r;  /* returns 0 */
#ifdef UNBOUND_DEBUG
       n =
#else
@@ -704,6 +728,7 @@ nsec3_hash_matches_owner(struct nsec3_fi
       struct nsec3_cached_hash* hash, struct ub_packed_rrset_key* s)
{
       uint8_t* nm = s->rk.dname;
+       if(!hash) return 0; /* please clang */
       /* compare, does hash of name based on params in this NSEC3
        * match the owner name of this NSEC3?
        * name must be: <hashlength>base32 . zone name
@@ -730,34 +755,50 @@ nsec3_hash_matches_owner(struct nsec3_fi
 * @param nmlen: length of name.
 * @param rrset: nsec3 that matches is returned here.
 * @param rr: rr number in nsec3 rrset that matches.
+ * @param calculations: current hash calculations.
 * @return true if a matching NSEC3 is found, false if not.
 */
static int
find_matching_nsec3(struct module_env* env, struct nsec3_filter* flt,
-       rbtree_type* ct, uint8_t* nm, size_t nmlen,
-       struct ub_packed_rrset_key** rrset, int* rr)
+       struct nsec3_cache_table* ct, uint8_t* nm, size_t nmlen,
+       struct ub_packed_rrset_key** rrset, int* rr,
+       int* calculations)
{
       size_t i_rs;
       int i_rr;
       struct ub_packed_rrset_key* s;
       struct nsec3_cached_hash* hash = NULL;
       int r;
+       int calc_errors = 0;

       /* this loop skips other-zone and unknown NSEC3s, also non-NSEC3 RRs */
       for(s=filter_first(flt, &i_rs, &i_rr); s;
               s=filter_next(flt, &i_rs, &i_rr)) {
+               /* check if we are allowed more calculations */
+               if(*calculations >= MAX_NSEC3_CALCULATIONS) {
+                       if(calc_errors == *calculations) {
+                               *calculations = MAX_NSEC3_ERRORS;
+                       }
+                       break;
+               }
               /* get name hashed for this NSEC3 RR */
-               r = nsec3_hash_name(ct, env->scratch, env->scratch_buffer,
+               r = nsec3_hash_name(ct->ct, ct->region, env->scratch_buffer,
                       s, i_rr, nm, nmlen, &hash);
               if(r == 0) {
                       log_err("nsec3: malloc failure");
                       break; /* alloc failure */
-               } else if(r != 1)
-                       continue; /* malformed NSEC3 */
-               else if(nsec3_hash_matches_owner(flt, hash, s)) {
-                       *rrset = s; /* rrset with this name */
-                       *rr = i_rr; /* matches hash with these parameters */
-                       return 1;
+               } else if(r < 0) {
+                       /* malformed NSEC3 */
+                       calc_errors++;
+                       (*calculations)++;
+                       continue;
+               } else {
+                       if(r == 1) (*calculations)++;
+                       if(nsec3_hash_matches_owner(flt, hash, s)) {
+                               *rrset = s; /* rrset with this name */
+                               *rr = i_rr; /* matches hash with these parameters */
+                               return 1;
+                       }
               }
       }
       *rrset = NULL;
@@ -775,6 +816,7 @@ nsec3_covers(uint8_t* zone, struct nsec3
       if(!nsec3_get_nextowner(rrset, rr, &next, &nextlen))
               return 0; /* malformed RR proves nothing */

+       if(!hash) return 0; /* please clang */
       /* check the owner name is a hashed value . apex
        * base32 encoded values must have equal length.
        * hash_value and next hash value must have equal length. */
@@ -823,35 +865,51 @@ nsec3_covers(uint8_t* zone, struct nsec3
 * @param nmlen: length of name.
 * @param rrset: covering NSEC3 rrset is returned here.
 * @param rr: rr of cover is returned here.
+ * @param calculations: current hash calculations.
 * @return true if a covering NSEC3 is found, false if not.
 */
static int
find_covering_nsec3(struct module_env* env, struct nsec3_filter* flt,
-        rbtree_type* ct, uint8_t* nm, size_t nmlen,
-       struct ub_packed_rrset_key** rrset, int* rr)
+       struct nsec3_cache_table* ct, uint8_t* nm, size_t nmlen,
+       struct ub_packed_rrset_key** rrset, int* rr,
+       int* calculations)
{
       size_t i_rs;
       int i_rr;
       struct ub_packed_rrset_key* s;
       struct nsec3_cached_hash* hash = NULL;
       int r;
+       int calc_errors = 0;

       /* this loop skips other-zone and unknown NSEC3s, also non-NSEC3 RRs */
       for(s=filter_first(flt, &i_rs, &i_rr); s;
               s=filter_next(flt, &i_rs, &i_rr)) {
+               /* check if we are allowed more calculations */
+               if(*calculations >= MAX_NSEC3_CALCULATIONS) {
+                       if(calc_errors == *calculations) {
+                               *calculations = MAX_NSEC3_ERRORS;
+                       }
+                       break;
+               }
               /* get name hashed for this NSEC3 RR */
-               r = nsec3_hash_name(ct, env->scratch, env->scratch_buffer,
+               r = nsec3_hash_name(ct->ct, ct->region, env->scratch_buffer,
                       s, i_rr, nm, nmlen, &hash);
               if(r == 0) {
                       log_err("nsec3: malloc failure");
                       break; /* alloc failure */
-               } else if(r != 1)
-                       continue; /* malformed NSEC3 */
-               else if(nsec3_covers(flt->zone, hash, s, i_rr,
-                       env->scratch_buffer)) {
-                       *rrset = s; /* rrset with this name */
-                       *rr = i_rr; /* covers hash with these parameters */
-                       return 1;
+               } else if(r < 0) {
+                       /* malformed NSEC3 */
+                       calc_errors++;
+                       (*calculations)++;
+                       continue;
+               } else {
+                       if(r == 1) (*calculations)++;
+                       if(nsec3_covers(flt->zone, hash, s, i_rr,
+                               env->scratch_buffer)) {
+                               *rrset = s; /* rrset with this name */
+                               *rr = i_rr; /* covers hash with these parameters */
+                               return 1;
+                       }
               }
       }
       *rrset = NULL;
@@ -869,11 +927,13 @@ find_covering_nsec3(struct module_env* e
 * @param ct: cached hashes table.
 * @param qinfo: query that is verified for.
 * @param ce: closest encloser information is returned in here.
+ * @param calculations: current hash calculations.
 * @return true if a closest encloser candidate is found, false if not.
 */
static int
-nsec3_find_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
-       rbtree_type* ct, struct query_info* qinfo, struct ce_response* ce)
+nsec3_find_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
+       struct nsec3_cache_table* ct, struct query_info* qinfo,
+       struct ce_response* ce, int* calculations)
{
       uint8_t* nm = qinfo->qname;
       size_t nmlen = qinfo->qname_len;
@@ -888,8 +948,12 @@ nsec3_find_closest_encloser(struct modul
        * may be the case. */

       while(dname_subdomain_c(nm, flt->zone)) {
+               if(*calculations >= MAX_NSEC3_CALCULATIONS ||
+                       *calculations == MAX_NSEC3_ERRORS) {
+                       return 0;
+               }
               if(find_matching_nsec3(env, flt, ct, nm, nmlen,
-                       &ce->ce_rrset, &ce->ce_rr)) {
+                       &ce->ce_rrset, &ce->ce_rr, calculations)) {
                       ce->ce = nm;
                       ce->ce_len = nmlen;
                       return 1;
@@ -933,22 +997,38 @@ next_closer(uint8_t* qname, size_t qname
 *     If set true, and the return value is true, then you can be
 *     certain that the ce.nc_rrset and ce.nc_rr are set properly.
 * @param ce: closest encloser information is returned in here.
+ * @param calculations: pointer to the current NSEC3 hash calculations.
 * @return bogus if no closest encloser could be proven.
 *     secure if a closest encloser could be proven, ce is set.
 *     insecure if the closest-encloser candidate turns out to prove
 *             that an insecure delegation exists above the qname.
+ *     unchecked if no more hash calculations are allowed at this point.
 */
static enum sec_status
-nsec3_prove_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
-       rbtree_type* ct, struct query_info* qinfo, int prove_does_not_exist,
-       struct ce_response* ce)
+nsec3_prove_closest_encloser(struct module_env* env, struct nsec3_filter* flt,
+       struct nsec3_cache_table* ct, struct query_info* qinfo,
+       int prove_does_not_exist, struct ce_response* ce, int* calculations)
{
       uint8_t* nc;
       size_t nc_len;
       /* robust: clean out ce, in case it gets abused later */
       memset(ce, 0, sizeof(*ce));

-       if(!nsec3_find_closest_encloser(env, flt, ct, qinfo, ce)) {
+       if(!nsec3_find_closest_encloser(env, flt, ct, qinfo, ce, calculations)) {
+               if(*calculations == MAX_NSEC3_ERRORS) {
+                       verbose(VERB_ALGO, "nsec3 proveClosestEncloser: could "
+                               "not find a candidate for the closest "
+                               "encloser; all attempted hash calculations "
+                               "were erroneous; bogus");
+                       return sec_status_bogus;
+               } else if(*calculations >= MAX_NSEC3_CALCULATIONS) {
+                       verbose(VERB_ALGO, "nsec3 proveClosestEncloser: could "
+                               "not find a candidate for the closest "
+                               "encloser; reached MAX_NSEC3_CALCULATIONS "
+                               "(%d); unchecked still",
+                               MAX_NSEC3_CALCULATIONS);
+                       return sec_status_unchecked;
+               }
               verbose(VERB_ALGO, "nsec3 proveClosestEncloser: could "
                       "not find a candidate for the closest encloser.");
               return sec_status_bogus;
@@ -989,9 +1069,23 @@ nsec3_prove_closest_encloser(struct modu
       /* Otherwise, we need to show that the next closer name is covered. */
       next_closer(qinfo->qname, qinfo->qname_len, ce->ce, &nc, &nc_len);
       if(!find_covering_nsec3(env, flt, ct, nc, nc_len,
-               &ce->nc_rrset, &ce->nc_rr)) {
+               &ce->nc_rrset, &ce->nc_rr, calculations)) {
+               if(*calculations == MAX_NSEC3_ERRORS) {
+                       verbose(VERB_ALGO, "nsec3: Could not find proof that the "
+                               "candidate encloser was the closest encloser; "
+                               "all attempted hash calculations were "
+                               "erroneous; bogus");
+                       return sec_status_bogus;
+               } else if(*calculations >= MAX_NSEC3_CALCULATIONS) {
+                       verbose(VERB_ALGO, "nsec3: Could not find proof that the "
+                               "candidate encloser was the closest encloser; "
+                               "reached MAX_NSEC3_CALCULATIONS (%d); "
+                               "unchecked still",
+                               MAX_NSEC3_CALCULATIONS);
+                       return sec_status_unchecked;
+               }
               verbose(VERB_ALGO, "nsec3: Could not find proof that the "
-                         "candidate encloser was the closest encloser");
+                       "candidate encloser was the closest encloser");
               return sec_status_bogus;
       }
       return sec_status_secure;
@@ -1019,8 +1113,8 @@ nsec3_ce_wildcard(struct regional* regio

/** Do the name error proof */
static enum sec_status
-nsec3_do_prove_nameerror(struct module_env* env, struct nsec3_filter* flt,
-       rbtree_type* ct, struct query_info* qinfo)
+nsec3_do_prove_nameerror(struct module_env* env, struct nsec3_filter* flt,
+       struct nsec3_cache_table* ct, struct query_info* qinfo, int* calc)
{
       struct ce_response ce;
       uint8_t* wc;
@@ -1032,11 +1126,15 @@ nsec3_do_prove_nameerror(struct module_e
       /* First locate and prove the closest encloser to qname. We will
        * use the variant that fails if the closest encloser turns out
        * to be qname. */
-       sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce);
+       sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce, calc);
       if(sec != sec_status_secure) {
               if(sec == sec_status_bogus)
                       verbose(VERB_ALGO, "nsec3 nameerror proof: failed "
                               "to prove a closest encloser");
+               else if(sec == sec_status_unchecked)
+                       verbose(VERB_ALGO, "nsec3 nameerror proof: will "
+                               "continue proving closest encloser after "
+                               "suspend");
               else    verbose(VERB_ALGO, "nsec3 nameerror proof: closest "
                               "nsec3 is an insecure delegation");
               return sec;
@@ -1046,9 +1144,27 @@ nsec3_do_prove_nameerror(struct module_e
       /* At this point, we know that qname does not exist. Now we need
        * to prove that the wildcard does not exist. */
       log_assert(ce.ce);
-       wc = nsec3_ce_wildcard(env->scratch, ce.ce, ce.ce_len, &wclen);
-       if(!wc || !find_covering_nsec3(env, flt, ct, wc, wclen,
-               &wc_rrset, &wc_rr)) {
+       wc = nsec3_ce_wildcard(ct->region, ce.ce, ce.ce_len, &wclen);
+       if(!wc) {
+               verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
+                       "that the applicable wildcard did not exist.");
+               return sec_status_bogus;
+       }
+       if(!find_covering_nsec3(env, flt, ct, wc, wclen, &wc_rrset, &wc_rr, calc)) {
+               if(*calc == MAX_NSEC3_ERRORS) {
+                       verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
+                               "that the applicable wildcard did not exist; "
+                               "all attempted hash calculations were "
+                               "erroneous; bogus");
+                       return sec_status_bogus;
+               } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
+                       verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
+                               "that the applicable wildcard did not exist; "
+                               "reached MAX_NSEC3_CALCULATIONS (%d); "
+                               "unchecked still",
+                               MAX_NSEC3_CALCULATIONS);
+                       return sec_status_unchecked;
+               }
               verbose(VERB_ALGO, "nsec3 nameerror proof: could not prove "
                       "that the applicable wildcard did not exist.");
               return sec_status_bogus;
@@ -1064,14 +1180,13 @@ nsec3_do_prove_nameerror(struct module_e
enum sec_status
nsec3_prove_nameerror(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey)
+       struct query_info* qinfo, struct key_entry_key* kkey,
+       struct nsec3_cache_table* ct, int* calc)
{
-       rbtree_type ct;
       struct nsec3_filter flt;

       if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
               return sec_status_bogus; /* no valid NSEC3s, bogus */
-       rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
       filter_init(&flt, list, num, qinfo); /* init RR iterator */
       if(!flt.zone)
               return sec_status_bogus; /* no RRs */
@@ -1079,7 +1194,7 @@ nsec3_prove_nameerror(struct module_env*
               return sec_status_insecure; /* iteration count too high */
       log_nametypeclass(VERB_ALGO, "start nsec3 nameerror proof, zone",
               flt.zone, 0, 0);
-       return nsec3_do_prove_nameerror(env, &flt, &ct, qinfo);
+       return nsec3_do_prove_nameerror(env, &flt, ct, qinfo, calc);
}

/*
@@ -1089,8 +1204,9 @@ nsec3_prove_nameerror(struct module_env*

/** Do the nodata proof */
static enum sec_status
-nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
-       rbtree_type* ct, struct query_info* qinfo)
+nsec3_do_prove_nodata(struct module_env* env, struct nsec3_filter* flt,
+       struct nsec3_cache_table* ct, struct query_info* qinfo,
+       int* calc)
{
       struct ce_response ce;
       uint8_t* wc;
@@ -1100,7 +1216,7 @@ nsec3_do_prove_nodata(struct module_env*
       enum sec_status sec;

       if(find_matching_nsec3(env, flt, ct, qinfo->qname, qinfo->qname_len,
-               &rrset, &rr)) {
+               &rrset, &rr, calc)) {
               /* cases 1 and 2 */
               if(nsec3_has_type(rrset, rr, qinfo->qtype)) {
                       verbose(VERB_ALGO, "proveNodata: Matching NSEC3 "
@@ -1144,11 +1260,23 @@ nsec3_do_prove_nodata(struct module_env*
               }
               return sec_status_secure;
       }
+       if(*calc == MAX_NSEC3_ERRORS) {
+               verbose(VERB_ALGO, "proveNodata: all attempted hash "
+                       "calculations were erroneous while finding a matching "
+                       "NSEC3, bogus");
+               return sec_status_bogus;
+       } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
+               verbose(VERB_ALGO, "proveNodata: reached "
+                       "MAX_NSEC3_CALCULATIONS (%d) while finding a "
+                       "matching NSEC3; unchecked still",
+                       MAX_NSEC3_CALCULATIONS);
+               return sec_status_unchecked;
+       }

       /* For cases 3 - 5, we need the proven closest encloser, and it
        * can't match qname. Although, at this point, we know that it
        * won't since we just checked that. */
-       sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce);
+       sec = nsec3_prove_closest_encloser(env, flt, ct, qinfo, 1, &ce, calc);
       if(sec == sec_status_bogus) {
               verbose(VERB_ALGO, "proveNodata: did not match qname, "
                         "nor found a proven closest encloser.");
@@ -1157,14 +1285,17 @@ nsec3_do_prove_nodata(struct module_env*
               verbose(VERB_ALGO, "proveNodata: closest nsec3 is insecure "
                         "delegation.");
               return sec_status_insecure;
+       } else if(sec==sec_status_unchecked) {
+               return sec_status_unchecked;
       }

       /* Case 3: removed */

       /* Case 4: */
       log_assert(ce.ce);
-       wc = nsec3_ce_wildcard(env->scratch, ce.ce, ce.ce_len, &wclen);
-       if(wc && find_matching_nsec3(env, flt, ct, wc, wclen, &rrset, &rr)) {
+       wc = nsec3_ce_wildcard(ct->region, ce.ce, ce.ce_len, &wclen);
+       if(wc && find_matching_nsec3(env, flt, ct, wc, wclen, &rrset, &rr,
+               calc)) {
               /* found wildcard */
               if(nsec3_has_type(rrset, rr, qinfo->qtype)) {
                       verbose(VERB_ALGO, "nsec3 nodata proof: matching "
@@ -1195,6 +1326,18 @@ nsec3_do_prove_nodata(struct module_env*
               }
               return sec_status_secure;
       }
+       if(*calc == MAX_NSEC3_ERRORS) {
+               verbose(VERB_ALGO, "nsec3 nodata proof: all attempted hash "
+                       "calculations were erroneous while matching "
+                       "wildcard, bogus");
+               return sec_status_bogus;
+       } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
+               verbose(VERB_ALGO, "nsec3 nodata proof: reached "
+                       "MAX_NSEC3_CALCULATIONS (%d) while matching "
+                       "wildcard, unchecked still",
+                       MAX_NSEC3_CALCULATIONS);
+               return sec_status_unchecked;
+       }

       /* Case 5: */
       /* Due to forwarders, cnames, and other collating effects, we
@@ -1223,28 +1366,27 @@ nsec3_do_prove_nodata(struct module_env*
enum sec_status
nsec3_prove_nodata(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey)
+       struct query_info* qinfo, struct key_entry_key* kkey,
+       struct nsec3_cache_table* ct, int* calc)
{
-       rbtree_type ct;
       struct nsec3_filter flt;

       if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
               return sec_status_bogus; /* no valid NSEC3s, bogus */
-       rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
       filter_init(&flt, list, num, qinfo); /* init RR iterator */
       if(!flt.zone)
               return sec_status_bogus; /* no RRs */
       if(nsec3_iteration_count_high(ve, &flt, kkey))
               return sec_status_insecure; /* iteration count too high */
-       return nsec3_do_prove_nodata(env, &flt, &ct, qinfo);
+       return nsec3_do_prove_nodata(env, &flt, ct, qinfo, calc);
}

enum sec_status
nsec3_prove_wildcard(struct module_env* env, struct val_env* ve,
        struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc)
+       struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc,
+       struct nsec3_cache_table* ct, int* calc)
{
-       rbtree_type ct;
       struct nsec3_filter flt;
       struct ce_response ce;
       uint8_t* nc;
@@ -1254,7 +1396,6 @@ nsec3_prove_wildcard(struct module_env*

       if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
               return sec_status_bogus; /* no valid NSEC3s, bogus */
-       rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
       filter_init(&flt, list, num, qinfo); /* init RR iterator */
       if(!flt.zone)
               return sec_status_bogus; /* no RRs */
@@ -1272,8 +1413,22 @@ nsec3_prove_wildcard(struct module_env*
       /* Now we still need to prove that the original data did not exist.
        * Otherwise, we need to show that the next closer name is covered. */
       next_closer(qinfo->qname, qinfo->qname_len, ce.ce, &nc, &nc_len);
-       if(!find_covering_nsec3(env, &flt, &ct, nc, nc_len,
-               &ce.nc_rrset, &ce.nc_rr)) {
+       if(!find_covering_nsec3(env, &flt, ct, nc, nc_len,
+               &ce.nc_rrset, &ce.nc_rr, calc)) {
+               if(*calc == MAX_NSEC3_ERRORS) {
+                       verbose(VERB_ALGO, "proveWildcard: did not find a "
+                               "covering NSEC3 that covered the next closer "
+                               "name; all attempted hash calculations were "
+                               "erroneous; bogus");
+                       return sec_status_bogus;
+               } else if(*calc >= MAX_NSEC3_CALCULATIONS) {
+                       verbose(VERB_ALGO, "proveWildcard: did not find a "
+                               "covering NSEC3 that covered the next closer "
+                               "name; reached MAX_NSEC3_CALCULATIONS "
+                               "(%d); unchecked still",
+                               MAX_NSEC3_CALCULATIONS);
+                       return sec_status_unchecked;
+               }
               verbose(VERB_ALGO, "proveWildcard: did not find a covering "
                       "NSEC3 that covered the next closer name.");
               return sec_status_bogus;
@@ -1294,6 +1449,7 @@ list_is_secure(struct module_env* env, s
{
       struct packed_rrset_data* d;
       size_t i;
+       int verified = 0;
       for(i=0; i<num; i++) {
               d = (struct packed_rrset_data*)list[i]->entry.data;
               if(list[i]->rk.type != htons(LDNS_RR_TYPE_NSEC3))
@@ -1304,7 +1460,8 @@ list_is_secure(struct module_env* env, s
               if(d->security == sec_status_secure)
                       continue;
               d->security = val_verify_rrset_entry(env, ve, list[i], kkey,
-                       reason, reason_bogus, LDNS_SECTION_AUTHORITY, qstate);
+                       reason, reason_bogus, LDNS_SECTION_AUTHORITY, qstate,
+                       &verified);
               if(d->security != sec_status_secure) {
                       verbose(VERB_ALGO, "NSEC3 did not verify");
                       return 0;
@@ -1318,13 +1475,16 @@ enum sec_status
nsec3_prove_nods(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
       struct query_info* qinfo, struct key_entry_key* kkey, char** reason,
-       sldns_ede_code* reason_bogus, struct module_qstate* qstate)
+       sldns_ede_code* reason_bogus, struct module_qstate* qstate,
+       struct nsec3_cache_table* ct)
{
-       rbtree_type ct;
       struct nsec3_filter flt;
       struct ce_response ce;
       struct ub_packed_rrset_key* rrset;
       int rr;
+       int calc = 0;
+       enum sec_status sec;
+
       log_assert(qinfo->qtype == LDNS_RR_TYPE_DS);

       if(!list || num == 0 || !kkey || !key_entry_isgood(kkey)) {
@@ -1335,7 +1495,6 @@ nsec3_prove_nods(struct module_env* env,
               *reason = "not all NSEC3 records secure";
               return sec_status_bogus; /* not all NSEC3 records secure */
       }
-       rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
       filter_init(&flt, list, num, qinfo); /* init RR iterator */
       if(!flt.zone) {
               *reason = "no NSEC3 records";
@@ -1346,8 +1505,8 @@ nsec3_prove_nods(struct module_env* env,

       /* Look for a matching NSEC3 to qname -- this is the normal
        * NODATA case. */
-       if(find_matching_nsec3(env, &flt, &ct, qinfo->qname, qinfo->qname_len,
-               &rrset, &rr)) {
+       if(find_matching_nsec3(env, &flt, ct, qinfo->qname, qinfo->qname_len,
+               &rrset, &rr, &calc)) {
               /* If the matching NSEC3 has the SOA bit set, it is from
                * the wrong zone (the child instead of the parent). If
                * it has the DS bit set, then we were lied to. */
@@ -1370,10 +1529,24 @@ nsec3_prove_nods(struct module_env* env,
               /* Otherwise, this proves no DS. */
               return sec_status_secure;
       }
+       if(calc == MAX_NSEC3_ERRORS) {
+               verbose(VERB_ALGO, "nsec3 provenods: all attempted hash "
+                       "calculations were erroneous while finding a matching "
+                       "NSEC3, bogus");
+               return sec_status_bogus;
+       } else if(calc >= MAX_NSEC3_CALCULATIONS) {
+               verbose(VERB_ALGO, "nsec3 provenods: reached "
+                       "MAX_NSEC3_CALCULATIONS (%d) while finding a "
+                       "matching NSEC3, unchecked still",
+                       MAX_NSEC3_CALCULATIONS);
+               return sec_status_unchecked;
+       }

       /* Otherwise, we are probably in the opt-out case. */
-       if(nsec3_prove_closest_encloser(env, &flt, &ct, qinfo, 1, &ce)
-               != sec_status_secure) {
+       sec = nsec3_prove_closest_encloser(env, &flt, ct, qinfo, 1, &ce, &calc);
+       if(sec == sec_status_unchecked) {
+               return sec_status_unchecked;
+       } else if(sec != sec_status_secure) {
               /* an insecure delegation *above* the qname does not prove
                * anything about this qname exactly, and bogus is bogus */
               verbose(VERB_ALGO, "nsec3 provenods: did not match qname, "
@@ -1407,17 +1580,16 @@ nsec3_prove_nods(struct module_env* env,

enum sec_status
nsec3_prove_nxornodata(struct module_env* env, struct val_env* ve,
-       struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey, int* nodata)
+       struct ub_packed_rrset_key** list, size_t num,
+       struct query_info* qinfo, struct key_entry_key* kkey, int* nodata,
+       struct  nsec3_cache_table* ct, int* calc)
{
       enum sec_status sec, secnx;
-       rbtree_type ct;
       struct nsec3_filter flt;
       *nodata = 0;

       if(!list || num == 0 || !kkey || !key_entry_isgood(kkey))
               return sec_status_bogus; /* no valid NSEC3s, bogus */
-       rbtree_init(&ct, &nsec3_hash_cmp); /* init names-to-hash cache */
       filter_init(&flt, list, num, qinfo); /* init RR iterator */
       if(!flt.zone)
               return sec_status_bogus; /* no RRs */
@@ -1427,16 +1599,20 @@ nsec3_prove_nxornodata(struct module_env
       /* try nxdomain and nodata after another, while keeping the
        * hash cache intact */

-       secnx = nsec3_do_prove_nameerror(env, &flt, &ct, qinfo);
+       secnx = nsec3_do_prove_nameerror(env, &flt, ct, qinfo, calc);
       if(secnx==sec_status_secure)
               return sec_status_secure;
-       sec = nsec3_do_prove_nodata(env, &flt, &ct, qinfo);
+       else if(secnx == sec_status_unchecked)
+               return sec_status_unchecked;
+       sec = nsec3_do_prove_nodata(env, &flt, ct, qinfo, calc);
       if(sec==sec_status_secure) {
               *nodata = 1;
       } else if(sec == sec_status_insecure) {
               *nodata = 1;
       } else if(secnx == sec_status_insecure) {
               sec = sec_status_insecure;
+       } else if(sec == sec_status_unchecked) {
+               return sec_status_unchecked;
       }
       return sec;
}
Index: usr.sbin/unbound/validator/val_nsec3.h
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/validator/val_nsec3.h,v
diff -u -p -r1.5 val_nsec3.h
--- usr.sbin/unbound/validator/val_nsec3.h      7 Jun 2022 15:42:54 -0000       1.5
+++ usr.sbin/unbound/validator/val_nsec3.h      12 Feb 2024 21:08:03 -0000
@@ -99,6 +99,15 @@ struct sldns_buffer;
#define NSEC3_HASH_SHA1        0x01

/**
+* Cache table for NSEC3 hashes.
+* It keeps a *pointer* to the region its items are allocated.
+*/
+struct nsec3_cache_table {
+       rbtree_type* ct;
+       struct regional* region;
+};
+
+/**
 * Determine if the set of NSEC3 records provided with a response prove NAME
 * ERROR. This means that the NSEC3s prove a) the closest encloser exists,
 * b) the direct child of the closest encloser towards qname doesn't exist,
@@ -110,14 +119,18 @@ struct sldns_buffer;
 * @param num: number of RRsets in the array to examine.
 * @param qinfo: query that is verified for.
 * @param kkey: key entry that signed the NSEC3s.
+ * @param ct: cached hashes table.
+ * @param calc: current hash calculations.
 * @return:
 *     sec_status SECURE of the Name Error is proven by the NSEC3 RRs,
- *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
+ *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
+ *     UNCHECKED if no more hash calculations are allowed at this point.
 */
enum sec_status
nsec3_prove_nameerror(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey);
+       struct query_info* qinfo, struct key_entry_key* kkey,
+       struct nsec3_cache_table* ct, int* calc);

/**
 * Determine if the NSEC3s provided in a response prove the NOERROR/NODATA
@@ -144,15 +157,18 @@ nsec3_prove_nameerror(struct module_env*
 * @param num: number of RRsets in the array to examine.
 * @param qinfo: query that is verified for.
 * @param kkey: key entry that signed the NSEC3s.
+ * @param ct: cached hashes table.
+ * @param calc: current hash calculations.
 * @return:
 *     sec_status SECURE of the proposition is proven by the NSEC3 RRs,
- *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
+ *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
+ *     UNCHECKED if no more hash calculations are allowed at this point.
 */
enum sec_status
nsec3_prove_nodata(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey);
-
+       struct query_info* qinfo, struct key_entry_key* kkey,
+       struct nsec3_cache_table* ct, int* calc);

/**
 * Prove that a positive wildcard match was appropriate (no direct match
@@ -166,14 +182,18 @@ nsec3_prove_nodata(struct module_env* en
 * @param kkey: key entry that signed the NSEC3s.
 * @param wc: The purported wildcard that matched. This is the wildcard name
 *     as *.wildcard.name., with the *. label already removed.
+ * @param ct: cached hashes table.
+ * @param calc: current hash calculations.
 * @return:
 *     sec_status SECURE of the proposition is proven by the NSEC3 RRs,
- *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
+ *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
+ *     UNCHECKED if no more hash calculations are allowed at this point.
 */
enum sec_status
nsec3_prove_wildcard(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc);
+       struct query_info* qinfo, struct key_entry_key* kkey, uint8_t* wc,
+       struct nsec3_cache_table* ct, int* calc);

/**
 * Prove that a DS response either had no DS, or wasn't a delegation point.
@@ -189,17 +209,20 @@ nsec3_prove_wildcard(struct module_env*
 * @param reason: string for bogus result.
 * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
 * @param qstate: qstate with region.
+ * @param ct: cached hashes table.
 * @return:
 *     sec_status SECURE of the proposition is proven by the NSEC3 RRs,
 *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
 *     or if there was no DS in an insecure (i.e., opt-in) way,
- *     INDETERMINATE if it was clear that this wasn't a delegation point.
+ *     INDETERMINATE if it was clear that this wasn't a delegation point,
+ *     UNCHECKED if no more hash calculations are allowed at this point.
 */
enum sec_status
nsec3_prove_nods(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
       struct query_info* qinfo, struct key_entry_key* kkey, char** reason,
-       sldns_ede_code* reason_bogus, struct module_qstate* qstate);
+       sldns_ede_code* reason_bogus, struct module_qstate* qstate,
+       struct nsec3_cache_table* ct);

/**
 * Prove NXDOMAIN or NODATA.
@@ -212,14 +235,18 @@ nsec3_prove_nods(struct module_env* env,
 * @param kkey: key entry that signed the NSEC3s.
 * @param nodata: if return value is secure, this indicates if nodata or
 *     nxdomain was proven.
+ * @param ct: cached hashes table.
+ * @param calc: current hash calculations.
 * @return:
 *     sec_status SECURE of the proposition is proven by the NSEC3 RRs,
- *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored.
+ *     BOGUS if not, INSECURE if all of the NSEC3s could be validly ignored,
+ *     UNCHECKED if no more hash calculations are allowed at this point.
 */
enum sec_status
nsec3_prove_nxornodata(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key** list, size_t num,
-       struct query_info* qinfo, struct key_entry_key* kkey, int* nodata);
+       struct query_info* qinfo, struct key_entry_key* kkey, int* nodata,
+       struct nsec3_cache_table* ct, int* calc);

/**
 * The NSEC3 hash result storage.
@@ -257,6 +284,14 @@ struct nsec3_cached_hash {
int nsec3_hash_cmp(const void* c1, const void* c2);

/**
+ * Initialise the NSEC3 cache table.
+ * @param ct: the nsec3 cache table.
+ * @param region: the region where allocations for the table will happen.
+ * @return true on success, false on malloc error.
+ */
+int nsec3_cache_table_init(struct nsec3_cache_table* ct, struct regional* region);
+
+/**
 * Obtain the hash of an owner name.
 * Used internally by the nsec3 proof functions in this file.
 * published to enable unit testing of hash algorithms and cache.
@@ -272,7 +307,8 @@ int nsec3_hash_cmp(const void* c1, const
 * @param dname_len: the length of the name.
 * @param hash: the hash node is returned on success.
 * @return:
- *     1 on success, either from cache or newly hashed hash is returned.
+ *     2 on success, hash from cache is returned.
+ *     1 on success, newly computed hash is returned.
 *     0 on a malloc failure.
 *     -1 if the NSEC3 rr was badly formatted (i.e. formerr).
 */
Index: usr.sbin/unbound/validator/val_sigcrypt.c
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/validator/val_sigcrypt.c,v
diff -u -p -r1.12 val_sigcrypt.c
--- usr.sbin/unbound/validator/val_sigcrypt.c   5 Sep 2023 11:12:11 -0000       1.12
+++ usr.sbin/unbound/validator/val_sigcrypt.c   12 Feb 2024 21:08:03 -0000
@@ -79,6 +79,9 @@
#include <openssl/engine.h>
#endif

+/** Maximum number of RRSIG validations for an RRset. */
+#define MAX_VALIDATE_RRSIGS 8
+
/** return number of rrs in an rrset */
static size_t
rrset_get_count(struct ub_packed_rrset_key* rrset)
@@ -542,6 +545,8 @@ int algo_needs_missing(struct algo_needs
 * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
 * @param section: section of packet where this rrset comes from.
 * @param qstate: qstate with region.
+ * @param numverified: incremented when the number of RRSIG validations
+ *     increases.
 * @return secure if any key signs *this* signature. bogus if no key signs it,
 *     unchecked on error, or indeterminate if all keys are not supported by
 *     the crypto library (openssl3+ only).
@@ -552,7 +557,8 @@ dnskeyset_verify_rrset_sig(struct module
       struct ub_packed_rrset_key* dnskey, size_t sig_idx,
       struct rbtree_type** sortree,
       char** reason, sldns_ede_code *reason_bogus,
-       sldns_pkt_section section, struct module_qstate* qstate)
+       sldns_pkt_section section, struct module_qstate* qstate,
+       int* numverified)
{
       /* find matching keys and check them */
       enum sec_status sec = sec_status_bogus;
@@ -576,6 +582,7 @@ dnskeyset_verify_rrset_sig(struct module
                       tag != dnskey_calc_keytag(dnskey, i))
                       continue;
               numchecked ++;
+               (*numverified)++;

               /* see if key verifies */
               sec = dnskey_verify_rrset_sig(env->scratch,
@@ -586,6 +593,13 @@ dnskeyset_verify_rrset_sig(struct module
                       return sec;
               else if(sec == sec_status_indeterminate)
                       numindeterminate ++;
+               if(*numverified > MAX_VALIDATE_RRSIGS) {
+                       *reason = "too many RRSIG validations";
+                       if(reason_bogus)
+                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
+                       verbose(VERB_ALGO, "verify sig: too many RRSIG validations");
+                       return sec_status_bogus;
+               }
       }
       if(numchecked == 0) {
               *reason = "signatures from unknown keys";
@@ -609,7 +623,7 @@ enum sec_status
dnskeyset_verify_rrset(struct module_env* env, struct val_env* ve,
       struct ub_packed_rrset_key* rrset, struct ub_packed_rrset_key* dnskey,
       uint8_t* sigalg, char** reason, sldns_ede_code *reason_bogus,
-       sldns_pkt_section section, struct module_qstate* qstate)
+       sldns_pkt_section section, struct module_qstate* qstate, int* verified)
{
       enum sec_status sec;
       size_t i, num;
@@ -617,6 +631,7 @@ dnskeyset_verify_rrset(struct module_env
       /* make sure that for all DNSKEY algorithms there are valid sigs */
       struct algo_needs needs;
       int alg;
+       *verified = 0;

       num = rrset_get_sigcount(rrset);
       if(num == 0) {
@@ -641,7 +656,7 @@ dnskeyset_verify_rrset(struct module_env
       for(i=0; i<num; i++) {
               sec = dnskeyset_verify_rrset_sig(env, ve, *env->now, rrset,
                       dnskey, i, &sortree, reason, reason_bogus,
-                       section, qstate);
+                       section, qstate, verified);
               /* see which algorithm has been fixed up */
               if(sec == sec_status_secure) {
                       if(!sigalg)
@@ -653,6 +668,13 @@ dnskeyset_verify_rrset(struct module_env
                       algo_needs_set_bogus(&needs,
                               (uint8_t)rrset_get_sig_algo(rrset, i));
               }
+               if(*verified > MAX_VALIDATE_RRSIGS) {
+                       verbose(VERB_QUERY, "rrset failed to verify, too many RRSIG validations");
+                       *reason = "too many RRSIG validations";
+                       if(reason_bogus)
+                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
+                       return sec_status_bogus;
+               }
       }
       if(sigalg && (alg=algo_needs_missing(&needs)) != 0) {
               verbose(VERB_ALGO, "rrset failed to verify: "
@@ -691,6 +713,7 @@ dnskey_verify_rrset(struct module_env* e
       int buf_canon = 0;
       uint16_t tag = dnskey_calc_keytag(dnskey, dnskey_idx);
       int algo = dnskey_get_algo(dnskey, dnskey_idx);
+       int numverified = 0;

       num = rrset_get_sigcount(rrset);
       if(num == 0) {
@@ -714,8 +737,16 @@ dnskey_verify_rrset(struct module_env* e
               if(sec == sec_status_secure)
                       return sec;
               numchecked ++;
+               numverified ++;
               if(sec == sec_status_indeterminate)
                       numindeterminate ++;
+               if(numverified > MAX_VALIDATE_RRSIGS) {
+                       verbose(VERB_QUERY, "rrset failed to verify, too many RRSIG validations");
+                       *reason = "too many RRSIG validations";
+                       if(reason_bogus)
+                               *reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
+                       return sec_status_bogus;
+               }
       }
       verbose(VERB_ALGO, "rrset failed to verify: all signatures are bogus");
       if(!numchecked) {
Index: usr.sbin/unbound/validator/val_sigcrypt.h
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/validator/val_sigcrypt.h,v
diff -u -p -r1.5 val_sigcrypt.h
--- usr.sbin/unbound/validator/val_sigcrypt.h   7 Jun 2022 15:42:54 -0000       1.5
+++ usr.sbin/unbound/validator/val_sigcrypt.h   12 Feb 2024 21:08:03 -0000
@@ -260,6 +260,7 @@ uint16_t dnskey_get_flags(struct ub_pack
 * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
 * @param section: section of packet where this rrset comes from.
 * @param qstate: qstate with region.
+ * @param verified: if not NULL the number of RRSIG validations is returned.
 * @return SECURE if one key in the set verifies one rrsig.
 *     UNCHECKED on allocation errors, unsupported algorithms, malformed data,
 *     and BOGUS on verification failures (no keys match any signatures).
@@ -268,7 +269,7 @@ enum sec_status dnskeyset_verify_rrset(s
       struct val_env* ve, struct ub_packed_rrset_key* rrset,
       struct ub_packed_rrset_key* dnskey, uint8_t* sigalg,
       char** reason, sldns_ede_code *reason_bogus,
-       sldns_pkt_section section, struct module_qstate* qstate);
+       sldns_pkt_section section, struct module_qstate* qstate, int* verified);


/**
Index: usr.sbin/unbound/validator/val_utils.c
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/validator/val_utils.c,v
diff -u -p -r1.13 val_utils.c
--- usr.sbin/unbound/validator/val_utils.c      5 Sep 2023 11:12:11 -0000       1.13
+++ usr.sbin/unbound/validator/val_utils.c      12 Feb 2024 21:08:03 -0000
@@ -58,6 +58,10 @@
#include "sldns/wire2str.h"
#include "sldns/parseutil.h"

+/** Maximum allowed digest match failures per DS, for DNSKEYs with the same
+ *  properties */
+#define MAX_DS_MATCH_FAILURES 4
+
enum val_classification
val_classify_response(uint16_t query_flags, struct query_info* origqinf,
       struct query_info* qinf, struct reply_info* rep, size_t skip)
@@ -336,7 +340,8 @@ static enum sec_status
val_verify_rrset(struct module_env* env, struct val_env* ve,
        struct ub_packed_rrset_key* rrset, struct ub_packed_rrset_key* keys,
       uint8_t* sigalg, char** reason, sldns_ede_code *reason_bogus,
-       sldns_pkt_section section, struct module_qstate* qstate)
+       sldns_pkt_section section, struct module_qstate* qstate,
+       int *verified)
{
       enum sec_status sec;
       struct packed_rrset_data* d = (struct packed_rrset_data*)rrset->
@@ -346,6 +351,7 @@ val_verify_rrset(struct module_env* env,
               log_nametypeclass(VERB_ALGO, "verify rrset cached",
                       rrset->rk.dname, ntohs(rrset->rk.type),
                       ntohs(rrset->rk.rrset_class));
+               *verified = 0;
               return d->security;
       }
       /* check in the cache if verification has already been done */
@@ -354,12 +360,13 @@ val_verify_rrset(struct module_env* env,
               log_nametypeclass(VERB_ALGO, "verify rrset from cache",
                       rrset->rk.dname, ntohs(rrset->rk.type),
                       ntohs(rrset->rk.rrset_class));
+               *verified = 0;
               return d->security;
       }
       log_nametypeclass(VERB_ALGO, "verify rrset", rrset->rk.dname,
               ntohs(rrset->rk.type), ntohs(rrset->rk.rrset_class));
       sec = dnskeyset_verify_rrset(env, ve, rrset, keys, sigalg, reason,
-               reason_bogus, section, qstate);
+               reason_bogus, section, qstate, verified);
       verbose(VERB_ALGO, "verify result: %s", sec_status_to_string(sec));
       regional_free_all(env->scratch);

@@ -393,7 +400,8 @@ enum sec_status
val_verify_rrset_entry(struct module_env* env, struct val_env* ve,
        struct ub_packed_rrset_key* rrset, struct key_entry_key* kkey,
       char** reason, sldns_ede_code *reason_bogus,
-       sldns_pkt_section section, struct module_qstate* qstate)
+       sldns_pkt_section section, struct module_qstate* qstate,
+       int* verified)
{
       /* temporary dnskey rrset-key */
       struct ub_packed_rrset_key dnskey;
@@ -407,7 +415,7 @@ val_verify_rrset_entry(struct module_env
       dnskey.entry.key = &dnskey;
       dnskey.entry.data = kd->rrset_data;
       sec = val_verify_rrset(env, ve, rrset, &dnskey, kd->algo, reason,
-               reason_bogus, section, qstate);
+               reason_bogus, section, qstate, verified);
       return sec;
}

@@ -439,6 +447,12 @@ verify_dnskeys_with_ds_rr(struct module_
               if(!ds_digest_match_dnskey(env, dnskey_rrset, i, ds_rrset,
                       ds_idx)) {
                       verbose(VERB_ALGO, "DS match attempt failed");
+                       if(numchecked > numhashok + MAX_DS_MATCH_FAILURES) {
+                               verbose(VERB_ALGO, "DS match attempt reached "
+                                       "MAX_DS_MATCH_FAILURES (%d); bogus",
+                                       MAX_DS_MATCH_FAILURES);
+                               return sec_status_bogus;
+                       }
                       continue;
               }
               numhashok++;
Index: usr.sbin/unbound/validator/val_utils.h
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/validator/val_utils.h,v
diff -u -p -r1.7 val_utils.h
--- usr.sbin/unbound/validator/val_utils.h      7 Jun 2022 15:42:54 -0000       1.7
+++ usr.sbin/unbound/validator/val_utils.h      12 Feb 2024 21:08:03 -0000
@@ -124,12 +124,14 @@ void val_find_signer(enum val_classifica
 * @param reason_bogus: EDE (RFC8914) code paired with the reason of failure.
 * @param section: section of packet where this rrset comes from.
 * @param qstate: qstate with region.
+ * @param verified: if not NULL, the number of RRSIG validations is returned.
 * @return security status of verification.
 */
enum sec_status val_verify_rrset_entry(struct module_env* env,
       struct val_env* ve, struct ub_packed_rrset_key* rrset,
       struct key_entry_key* kkey, char** reason, sldns_ede_code *reason_bogus,
-       sldns_pkt_section section, struct module_qstate* qstate);
+       sldns_pkt_section section, struct module_qstate* qstate,
+       int* verified);

/**
 * Verify DNSKEYs with DS rrset. Like val_verify_new_DNSKEYs but
Index: usr.sbin/unbound/validator/validator.c
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/validator/validator.c,v
diff -u -p -r1.20 validator.c
--- usr.sbin/unbound/validator/validator.c      5 Sep 2023 11:12:11 -0000       1.20
+++ usr.sbin/unbound/validator/validator.c      12 Feb 2024 21:08:03 -0000
@@ -64,10 +64,15 @@
#include "sldns/wire2str.h"
#include "sldns/str2wire.h"

+/** Max number of RRSIGs to validate at once, suspend query for later. */
+#define MAX_VALIDATE_AT_ONCE 8
+/** Max number of validation suspends allowed, error out otherwise. */
+#define MAX_VALIDATION_SUSPENDS 16
+
/* forward decl for cache response and normal super inform calls of a DS */
static void process_ds_response(struct module_qstate* qstate,
       struct val_qstate* vq, int id, int rcode, struct dns_msg* msg,
-       struct query_info* qinfo, struct sock_list* origin);
+       struct query_info* qinfo, struct sock_list* origin, int* suspend);


/* Updates the suplied EDE (RFC8914) code selectively so we don't lose
@@ -281,6 +286,21 @@ val_new(struct module_qstate* qstate, in
       return val_new_getmsg(qstate, vq);
}

+/** reset validator query state for query restart */
+static void
+val_restart(struct val_qstate* vq)
+{
+       struct comm_timer* temp_timer;
+       int restart_count;
+       if(!vq) return;
+       temp_timer = vq->suspend_timer;
+       restart_count = vq->restart_count+1;
+       memset(vq, 0, sizeof(*vq));
+       vq->suspend_timer = temp_timer;
+       vq->restart_count = restart_count;
+       vq->state = VAL_INIT_STATE;
+}
+
/**
 * Exit validation with an error status
 *
@@ -587,30 +607,42 @@ prime_trust_anchor(struct module_qstate*
 * completed.
 *
 * @param qstate: query state.
+ * @param vq: validator query state.
 * @param env: module env for verify.
 * @param ve: validator env for verify.
 * @param qchase: query that was made.
 * @param chase_reply: answer to validate.
 * @param key_entry: the key entry, which is trusted, and which matches
 *     the signer of the answer. The key entry isgood().
+ * @param suspend: returned true if the task takes too long and needs to
+ *     suspend to continue the effort later.
 * @return false if any of the rrsets in the an or ns sections of the message
 *     fail to verify. The message is then set to bogus.
 */
static int
-validate_msg_signatures(struct module_qstate* qstate, struct module_env* env,
-       struct val_env* ve, struct query_info* qchase,
-       struct reply_info* chase_reply, struct key_entry_key* key_entry)
+validate_msg_signatures(struct module_qstate* qstate, struct val_qstate* vq,
+       struct module_env* env, struct val_env* ve, struct query_info* qchase,
+       struct reply_info* chase_reply, struct key_entry_key* key_entry,
+       int* suspend)
{
       uint8_t* sname;
       size_t i, slen;
       struct ub_packed_rrset_key* s;
       enum sec_status sec;
-       int dname_seen = 0;
+       int dname_seen = 0, num_verifies = 0, verified, have_state = 0;
       char* reason = NULL;
       sldns_ede_code reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
+       *suspend = 0;
+       if(vq->msg_signatures_state) {
+               /* Pick up the state, and reset it, may not be needed now. */
+               vq->msg_signatures_state = 0;
+               have_state = 1;
+       }

       /* validate the ANSWER section */
       for(i=0; i<chase_reply->an_numrrsets; i++) {
+               if(have_state && i <= vq->msg_signatures_index)
+                       continue;
               s = chase_reply->rrsets[i];
               /* Skip the CNAME following a (validated) DNAME.
                * Because of the normalization routines in the iterator,
@@ -629,7 +661,7 @@ validate_msg_signatures(struct module_qs

               /* Verify the answer rrset */
               sec = val_verify_rrset_entry(env, ve, s, key_entry, &reason,
-                       &reason_bogus, LDNS_SECTION_ANSWER, qstate);
+                       &reason_bogus, LDNS_SECTION_ANSWER, qstate, &verified);
               /* If the (answer) rrset failed to validate, then this
                * message is BAD. */
               if(sec != sec_status_secure) {
@@ -654,14 +686,33 @@ validate_msg_signatures(struct module_qs
                       ntohs(s->rk.type) == LDNS_RR_TYPE_DNAME) {
                       dname_seen = 1;
               }
+               num_verifies += verified;
+               if(num_verifies > MAX_VALIDATE_AT_ONCE &&
+                       i+1 < (env->cfg->val_clean_additional?
+                       chase_reply->an_numrrsets+chase_reply->ns_numrrsets:
+                       chase_reply->rrset_count)) {
+                       /* If the number of RRSIGs exceeds the maximum in
+                        * one go, suspend. Only suspend if there is a next
+                        * rrset to verify, i+1<loopmax. Store where to
+                        * continue later. */
+                       *suspend = 1;
+                       vq->msg_signatures_state = 1;
+                       vq->msg_signatures_index = i;
+                       verbose(VERB_ALGO, "msg signature validation "
+                               "suspended");
+                       return 0;
+               }
       }

       /* validate the AUTHORITY section */
       for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
               chase_reply->ns_numrrsets; i++) {
+               if(have_state && i <= vq->msg_signatures_index)
+                       continue;
               s = chase_reply->rrsets[i];
               sec = val_verify_rrset_entry(env, ve, s, key_entry, &reason,
-                       &reason_bogus, LDNS_SECTION_AUTHORITY, qstate);
+                       &reason_bogus, LDNS_SECTION_AUTHORITY, qstate,
+                       &verified);
               /* If anything in the authority section fails to be secure,
                * we have a bad message. */
               if(sec != sec_status_secure) {
@@ -675,6 +726,18 @@ validate_msg_signatures(struct module_qs
                       update_reason_bogus(chase_reply, reason_bogus);
                       return 0;
               }
+               num_verifies += verified;
+               if(num_verifies > MAX_VALIDATE_AT_ONCE &&
+                       i+1 < (env->cfg->val_clean_additional?
+                       chase_reply->an_numrrsets+chase_reply->ns_numrrsets:
+                       chase_reply->rrset_count)) {
+                       *suspend = 1;
+                       vq->msg_signatures_state = 1;
+                       vq->msg_signatures_index = i;
+                       verbose(VERB_ALGO, "msg signature validation "
+                               "suspended");
+                       return 0;
+               }
       }

       /* If set, the validator should clean the additional section of
@@ -684,22 +747,103 @@ validate_msg_signatures(struct module_qs
       /* attempt to validate the ADDITIONAL section rrsets */
       for(i=chase_reply->an_numrrsets+chase_reply->ns_numrrsets;
               i<chase_reply->rrset_count; i++) {
+               if(have_state && i <= vq->msg_signatures_index)
+                       continue;
               s = chase_reply->rrsets[i];
               /* only validate rrs that have signatures with the key */
               /* leave others unchecked, those get removed later on too */
               val_find_rrset_signer(s, &sname, &slen);

+               verified = 0;
               if(sname && query_dname_compare(sname, key_entry->name)==0)
                       (void)val_verify_rrset_entry(env, ve, s, key_entry,
-                               &reason, NULL, LDNS_SECTION_ADDITIONAL, qstate);
+                               &reason, NULL, LDNS_SECTION_ADDITIONAL, qstate,
+                               &verified);
               /* the additional section can fail to be secure,
                * it is optional, check signature in case we need
                * to clean the additional section later. */
+               num_verifies += verified;
+               if(num_verifies > MAX_VALIDATE_AT_ONCE &&
+                       i+1 < chase_reply->rrset_count) {
+                       *suspend = 1;
+                       vq->msg_signatures_state = 1;
+                       vq->msg_signatures_index = i;
+                       verbose(VERB_ALGO, "msg signature validation "
+                               "suspended");
+                       return 0;
+               }
       }

       return 1;
}

+void
+validate_suspend_timer_cb(void* arg)
+{
+       struct module_qstate* qstate = (struct module_qstate*)arg;
+       verbose(VERB_ALGO, "validate_suspend timer, continue");
+       mesh_run(qstate->env->mesh, qstate->mesh_info, module_event_pass,
+               NULL);
+}
+
+/** Setup timer to continue validation of msg signatures later */
+static int
+validate_suspend_setup_timer(struct module_qstate* qstate,
+       struct val_qstate* vq, int id, enum val_state resume_state)
+{
+       struct timeval tv;
+       int usec, slack, base;
+       if(vq->suspend_count >= MAX_VALIDATION_SUSPENDS) {
+               verbose(VERB_ALGO, "validate_suspend timer: "
+                       "reached MAX_VALIDATION_SUSPENDS (%d); error out",
+                       MAX_VALIDATION_SUSPENDS);
+               errinf(qstate, "max validation suspends reached, "
+                       "too many RRSIG validations");
+               return 0;
+       }
+       verbose(VERB_ALGO, "validate_suspend timer, set for suspend");
+       vq->state = resume_state;
+       qstate->ext_state[id] = module_wait_reply;
+       if(!vq->suspend_timer) {
+               vq->suspend_timer = comm_timer_create(
+                       qstate->env->worker_base,
+                       validate_suspend_timer_cb, qstate);
+               if(!vq->suspend_timer) {
+                       log_err("validate_suspend_setup_timer: "
+                               "out of memory for comm_timer_create");
+                       return 0;
+               }
+       }
+       /* The timer is activated later, after other events in the event
+        * loop have been processed. The query state can also be deleted,
+        * when the list is full and query states are dropped. */
+       /* Extend wait time if there are a lot of queries or if this one
+        * is taking long, to keep around cpu time for ordinary queries. */
+       usec = 50000; /* 50 msec */
+       slack = 0;
+       if(qstate->env->mesh->all.count >= qstate->env->mesh->max_reply_states)
+               slack += 3;
+       else if(qstate->env->mesh->all.count >= qstate->env->mesh->max_reply_states/2)
+               slack += 2;
+       else if(qstate->env->mesh->all.count >= qstate->env->mesh->max_reply_states/4)
+               slack += 1;
+       if(vq->suspend_count > 3)
+               slack += 3;
+       else if(vq->suspend_count > 0)
+               slack += vq->suspend_count;
+       if(slack != 0 && slack <= 12 /* No numeric overflow. */) {
+               usec = usec << slack;
+       }
+       /* Spread such timeouts within 90%-100% of the original timer. */
+       base = usec * 9/10;
+       usec = base + ub_random_max(qstate->env->rnd, usec-base);
+       tv.tv_usec = (usec % 1000000);
+       tv.tv_sec = (usec / 1000000);
+       vq->suspend_count ++;
+       comm_timer_set(vq->suspend_timer, &tv);
+       return 1;
+}
+
/**
 * Detect wrong truncated response (say from BIND 9.6.1 that is forwarding
 * and saw the NS record without signatures from a referral).
@@ -798,11 +942,17 @@ remove_spurious_authority(struct reply_i
 * @param chase_reply: answer to that query to validate.
 * @param kkey: the key entry, which is trusted, and which matches
 *     the signer of the answer. The key entry isgood().
+ * @param qstate: query state for the region.
+ * @param vq: validator state for the nsec3 cache table.
+ * @param nsec3_calculations: current nsec3 hash calculations.
+ * @param suspend: returned true if the task takes too long and needs to
+ *     suspend to continue the effort later.
 */
static void
validate_positive_response(struct module_env* env, struct val_env* ve,
       struct query_info* qchase, struct reply_info* chase_reply,
-       struct key_entry_key* kkey)
+       struct key_entry_key* kkey, struct module_qstate* qstate,
+       struct val_qstate* vq, int* nsec3_calculations, int* suspend)
{
       uint8_t* wc = NULL;
       size_t wl;
@@ -811,6 +961,7 @@ validate_positive_response(struct module
       int nsec3s_seen = 0;
       size_t i;
       struct ub_packed_rrset_key* s;
+       *suspend = 0;

       /* validate the ANSWER section - this will be the answer itself */
       for(i=0; i<chase_reply->an_numrrsets; i++) {
@@ -862,17 +1013,23 @@ validate_positive_response(struct module
       /* If this was a positive wildcard response that we haven't already
        * proven, and we have NSEC3 records, try to prove it using the NSEC3
        * records. */
-       if(wc != NULL && !wc_NSEC_ok && nsec3s_seen) {
-               enum sec_status sec = nsec3_prove_wildcard(env, ve,
+       if(wc != NULL && !wc_NSEC_ok && nsec3s_seen &&
+               nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
+               enum sec_status sec = nsec3_prove_wildcard(env, ve,
                       chase_reply->rrsets+chase_reply->an_numrrsets,
-                       chase_reply->ns_numrrsets, qchase, kkey, wc);
+                       chase_reply->ns_numrrsets, qchase, kkey, wc,
+                       &vq->nsec3_cache_table, nsec3_calculations);
               if(sec == sec_status_insecure) {
                       verbose(VERB_ALGO, "Positive wildcard response is "
                               "insecure");
                       chase_reply->security = sec_status_insecure;
                       return;
-               } else if(sec == sec_status_secure)
+               } else if(sec == sec_status_secure) {
                       wc_NSEC_ok = 1;
+               } else if(sec == sec_status_unchecked) {
+                       *suspend = 1;
+                       return;
+               }
       }

       /* If after all this, we still haven't proven the positive wildcard
@@ -904,11 +1061,17 @@ validate_positive_response(struct module
 * @param chase_reply: answer to that query to validate.
 * @param kkey: the key entry, which is trusted, and which matches
 *     the signer of the answer. The key entry isgood().
+ * @param qstate: query state for the region.
+ * @param vq: validator state for the nsec3 cache table.
+ * @param nsec3_calculations: current nsec3 hash calculations.
+ * @param suspend: returned true if the task takes too long and needs to
+ *     suspend to continue the effort later.
 */
static void
validate_nodata_response(struct module_env* env, struct val_env* ve,
       struct query_info* qchase, struct reply_info* chase_reply,
-       struct key_entry_key* kkey)
+       struct key_entry_key* kkey, struct module_qstate* qstate,
+       struct val_qstate* vq, int* nsec3_calculations, int* suspend)
{
       /* Since we are here, there must be nothing in the ANSWER section to
        * validate. */
@@ -925,6 +1088,7 @@ validate_nodata_response(struct module_e
       int nsec3s_seen = 0; /* nsec3s seen */
       struct ub_packed_rrset_key* s;
       size_t i;
+       *suspend = 0;

       for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
               chase_reply->ns_numrrsets; i++) {
@@ -963,16 +1127,23 @@ validate_nodata_response(struct module_e
               }
       }

-       if(!has_valid_nsec && nsec3s_seen) {
+       if(!has_valid_nsec && nsec3s_seen &&
+               nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
               enum sec_status sec = nsec3_prove_nodata(env, ve,
                       chase_reply->rrsets+chase_reply->an_numrrsets,
-                       chase_reply->ns_numrrsets, qchase, kkey);
+                       chase_reply->ns_numrrsets, qchase, kkey,
+                       &vq->nsec3_cache_table, nsec3_calculations);
               if(sec == sec_status_insecure) {
                       verbose(VERB_ALGO, "NODATA response is insecure");
                       chase_reply->security = sec_status_insecure;
                       return;
-               } else if(sec == sec_status_secure)
+               } else if(sec == sec_status_secure) {
                       has_valid_nsec = 1;
+               } else if(sec == sec_status_unchecked) {
+                       /* check is incomplete; suspend */
+                       *suspend = 1;
+                       return;
+               }
       }

       if(!has_valid_nsec) {
@@ -1004,11 +1175,18 @@ validate_nodata_response(struct module_e
 * @param kkey: the key entry, which is trusted, and which matches
 *     the signer of the answer. The key entry isgood().
 * @param rcode: adjusted RCODE, in case of RCODE/proof mismatch leniency.
+ * @param qstate: query state for the region.
+ * @param vq: validator state for the nsec3 cache table.
+ * @param nsec3_calculations: current nsec3 hash calculations.
+ * @param suspend: returned true if the task takes too long and needs to
+ *     suspend to continue the effort later.
 */
static void
validate_nameerror_response(struct module_env* env, struct val_env* ve,
       struct query_info* qchase, struct reply_info* chase_reply,
-       struct key_entry_key* kkey, int* rcode)
+       struct key_entry_key* kkey, int* rcode,
+       struct module_qstate* qstate, struct val_qstate* vq,
+       int* nsec3_calculations, int* suspend)
{
       int has_valid_nsec = 0;
       int has_valid_wnsec = 0;
@@ -1018,6 +1196,7 @@ validate_nameerror_response(struct modul
       uint8_t* ce;
       int ce_labs = 0;
       int prev_ce_labs = 0;
+       *suspend = 0;

       for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
               chase_reply->ns_numrrsets; i++) {
@@ -1047,13 +1226,18 @@ validate_nameerror_response(struct modul
                       nsec3s_seen = 1;
       }

-       if((!has_valid_nsec || !has_valid_wnsec) && nsec3s_seen) {
+       if((!has_valid_nsec || !has_valid_wnsec) && nsec3s_seen &&
+               nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
               /* use NSEC3 proof, both answer and auth rrsets, in case
                * NSEC3s end up in the answer (due to qtype=NSEC3 or so) */
               chase_reply->security = nsec3_prove_nameerror(env, ve,
                       chase_reply->rrsets, chase_reply->an_numrrsets+
-                       chase_reply->ns_numrrsets, qchase, kkey);
-               if(chase_reply->security != sec_status_secure) {
+                       chase_reply->ns_numrrsets, qchase, kkey,
+                       &vq->nsec3_cache_table, nsec3_calculations);
+               if(chase_reply->security == sec_status_unchecked) {
+                       *suspend = 1;
+                       return;
+               } else if(chase_reply->security != sec_status_secure) {
                       verbose(VERB_QUERY, "NameError response failed nsec, "
                               "nsec3 proof was %s", sec_status_to_string(
                               chase_reply->security));
@@ -1065,26 +1249,34 @@ validate_nameerror_response(struct modul

       /* If the message fails to prove either condition, it is bogus. */
       if(!has_valid_nsec) {
+               validate_nodata_response(env, ve, qchase, chase_reply, kkey,
+                       qstate, vq, nsec3_calculations, suspend);
+               if(*suspend) return;
               verbose(VERB_QUERY, "NameError response has failed to prove: "
                         "qname does not exist");
-               chase_reply->security = sec_status_bogus;
-               update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
               /* Be lenient with RCODE in NSEC NameError responses */
-               validate_nodata_response(env, ve, qchase, chase_reply, kkey);
-               if (chase_reply->security == sec_status_secure)
+               if(chase_reply->security == sec_status_secure) {
                       *rcode = LDNS_RCODE_NOERROR;
+               } else {
+                       chase_reply->security = sec_status_bogus;
+                       update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
+               }
               return;
       }

       if(!has_valid_wnsec) {
+               validate_nodata_response(env, ve, qchase, chase_reply, kkey,
+                       qstate, vq, nsec3_calculations, suspend);
+               if(*suspend) return;
               verbose(VERB_QUERY, "NameError response has failed to prove: "
                         "covering wildcard does not exist");
-               chase_reply->security = sec_status_bogus;
-               update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
               /* Be lenient with RCODE in NSEC NameError responses */
-               validate_nodata_response(env, ve, qchase, chase_reply, kkey);
-               if (chase_reply->security == sec_status_secure)
+               if (chase_reply->security == sec_status_secure) {
                       *rcode = LDNS_RCODE_NOERROR;
+               } else {
+                       chase_reply->security = sec_status_bogus;
+                       update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
+               }
               return;
       }

@@ -1144,11 +1336,17 @@ validate_referral_response(struct reply_
 * @param chase_reply: answer to that query to validate.
 * @param kkey: the key entry, which is trusted, and which matches
 *     the signer of the answer. The key entry isgood().
+ * @param qstate: query state for the region.
+ * @param vq: validator state for the nsec3 cache table.
+ * @param nsec3_calculations: current nsec3 hash calculations.
+ * @param suspend: returned true if the task takes too long and needs to
+ *     suspend to continue the effort later.
 */
static void
validate_any_response(struct module_env* env, struct val_env* ve,
       struct query_info* qchase, struct reply_info* chase_reply,
-       struct key_entry_key* kkey)
+       struct key_entry_key* kkey, struct module_qstate* qstate,
+       struct val_qstate* vq, int* nsec3_calculations, int* suspend)
{
       /* all answer and auth rrsets already verified */
       /* but check if a wildcard response is given, then check NSEC/NSEC3
@@ -1159,6 +1357,7 @@ validate_any_response(struct module_env*
       int nsec3s_seen = 0;
       size_t i;
       struct ub_packed_rrset_key* s;
+       *suspend = 0;

       if(qchase->qtype != LDNS_RR_TYPE_ANY) {
               log_err("internal error: ANY validation called for non-ANY");
@@ -1213,19 +1412,25 @@ validate_any_response(struct module_env*
       /* If this was a positive wildcard response that we haven't already
        * proven, and we have NSEC3 records, try to prove it using the NSEC3
        * records. */
-       if(wc != NULL && !wc_NSEC_ok && nsec3s_seen) {
+       if(wc != NULL && !wc_NSEC_ok && nsec3s_seen &&
+               nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
               /* look both in answer and auth section for NSEC3s */
-               enum sec_status sec = nsec3_prove_wildcard(env, ve,
+               enum sec_status sec = nsec3_prove_wildcard(env, ve,
                       chase_reply->rrsets,
-                       chase_reply->an_numrrsets+chase_reply->ns_numrrsets,
-                       qchase, kkey, wc);
+                       chase_reply->an_numrrsets+chase_reply->ns_numrrsets,
+                       qchase, kkey, wc, &vq->nsec3_cache_table,
+                       nsec3_calculations);
               if(sec == sec_status_insecure) {
                       verbose(VERB_ALGO, "Positive ANY wildcard response is "
                               "insecure");
                       chase_reply->security = sec_status_insecure;
                       return;
-               } else if(sec == sec_status_secure)
+               } else if(sec == sec_status_secure) {
                       wc_NSEC_ok = 1;
+               } else if(sec == sec_status_unchecked) {
+                       *suspend = 1;
+                       return;
+               }
       }

       /* If after all this, we still haven't proven the positive wildcard
@@ -1258,11 +1463,17 @@ validate_any_response(struct module_env*
 * @param chase_reply: answer to that query to validate.
 * @param kkey: the key entry, which is trusted, and which matches
 *     the signer of the answer. The key entry isgood().
+ * @param qstate: query state for the region.
+ * @param vq: validator state for the nsec3 cache table.
+ * @param nsec3_calculations: current nsec3 hash calculations.
+ * @param suspend: returned true if the task takes too long and needs to
+ *     suspend to continue the effort later.
 */
static void
validate_cname_response(struct module_env* env, struct val_env* ve,
       struct query_info* qchase, struct reply_info* chase_reply,
-       struct key_entry_key* kkey)
+       struct key_entry_key* kkey, struct module_qstate* qstate,
+       struct val_qstate* vq, int* nsec3_calculations, int* suspend)
{
       uint8_t* wc = NULL;
       size_t wl;
@@ -1270,6 +1481,7 @@ validate_cname_response(struct module_en
       int nsec3s_seen = 0;
       size_t i;
       struct ub_packed_rrset_key* s;
+       *suspend = 0;

       /* validate the ANSWER section - this will be the CNAME (+DNAME) */
       for(i=0; i<chase_reply->an_numrrsets; i++) {
@@ -1334,17 +1546,23 @@ validate_cname_response(struct module_en
       /* If this was a positive wildcard response that we haven't already
        * proven, and we have NSEC3 records, try to prove it using the NSEC3
        * records. */
-       if(wc != NULL && !wc_NSEC_ok && nsec3s_seen) {
-               enum sec_status sec = nsec3_prove_wildcard(env, ve,
+       if(wc != NULL && !wc_NSEC_ok && nsec3s_seen &&
+               nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
+               enum sec_status sec = nsec3_prove_wildcard(env, ve,
                       chase_reply->rrsets+chase_reply->an_numrrsets,
-                       chase_reply->ns_numrrsets, qchase, kkey, wc);
+                       chase_reply->ns_numrrsets, qchase, kkey, wc,
+                       &vq->nsec3_cache_table, nsec3_calculations);
               if(sec == sec_status_insecure) {
                       verbose(VERB_ALGO, "wildcard CNAME response is "
                               "insecure");
                       chase_reply->security = sec_status_insecure;
                       return;
-               } else if(sec == sec_status_secure)
+               } else if(sec == sec_status_secure) {
                       wc_NSEC_ok = 1;
+               } else if(sec == sec_status_unchecked) {
+                       *suspend = 1;
+                       return;
+               }
       }

       /* If after all this, we still haven't proven the positive wildcard
@@ -1375,11 +1593,17 @@ validate_cname_response(struct module_en
 * @param chase_reply: answer to that query to validate.
 * @param kkey: the key entry, which is trusted, and which matches
 *     the signer of the answer. The key entry isgood().
+ * @param qstate: query state for the region.
+ * @param vq: validator state for the nsec3 cache table.
+ * @param nsec3_calculations: current nsec3 hash calculations.
+ * @param suspend: returned true if the task takes too long and needs to
+ *     suspend to continue the effort later.
 */
static void
validate_cname_noanswer_response(struct module_env* env, struct val_env* ve,
       struct query_info* qchase, struct reply_info* chase_reply,
-       struct key_entry_key* kkey)
+       struct key_entry_key* kkey, struct module_qstate* qstate,
+       struct val_qstate* vq, int* nsec3_calculations, int* suspend)
{
       int nodata_valid_nsec = 0; /* If true, then NODATA has been proven.*/
       uint8_t* ce = NULL; /* for wildcard nodata responses. This is the
@@ -1393,6 +1617,7 @@ validate_cname_noanswer_response(struct
       uint8_t* nsec_ce; /* Used to find the NSEC with the longest ce */
       int ce_labs = 0;
       int prev_ce_labs = 0;
+       *suspend = 0;

       /* the AUTHORITY section */
       for(i=chase_reply->an_numrrsets; i<chase_reply->an_numrrsets+
@@ -1458,11 +1683,13 @@ validate_cname_noanswer_response(struct
               update_reason_bogus(chase_reply, LDNS_EDE_DNSSEC_BOGUS);
               return;
       }
-       if(!nodata_valid_nsec && !nxdomain_valid_nsec && nsec3s_seen) {
+       if(!nodata_valid_nsec && !nxdomain_valid_nsec && nsec3s_seen &&
+               nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
               int nodata;
               enum sec_status sec = nsec3_prove_nxornodata(env, ve,
                       chase_reply->rrsets+chase_reply->an_numrrsets,
-                       chase_reply->ns_numrrsets, qchase, kkey, &nodata);
+                       chase_reply->ns_numrrsets, qchase, kkey, &nodata,
+                       &vq->nsec3_cache_table, nsec3_calculations);
               if(sec == sec_status_insecure) {
                       verbose(VERB_ALGO, "CNAMEchain to noanswer response "
                               "is insecure");
@@ -1472,6 +1699,9 @@ validate_cname_noanswer_response(struct
                       if(nodata)
                               nodata_valid_nsec = 1;
                       else    nxdomain_valid_nsec = 1;
+               } else if(sec == sec_status_unchecked) {
+                       *suspend = 1;
+                       return;
               }
       }

@@ -1815,13 +2045,37 @@ processFindKey(struct module_qstate* qst
                * Uses negative cache for NSEC3 lookup of DS responses. */
               /* only if cache not blacklisted, of course */
               struct dns_msg* msg;
-               if(!qstate->blacklist && !vq->chain_blacklist &&
+               int suspend;
+               if(vq->sub_ds_msg) {
+                       /* We have a suspended DS reply from a sub-query;
+                        * process it. */
+                       verbose(VERB_ALGO, "Process suspended sub DS response");
+                       msg = vq->sub_ds_msg;
+                       process_ds_response(qstate, vq, id, LDNS_RCODE_NOERROR,
+                               msg, &msg->qinfo, NULL, &suspend);
+                       if(suspend) {
+                               /* we'll come back here later to continue */
+                               if(!validate_suspend_setup_timer(qstate, vq,
+                                       id, VAL_FINDKEY_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
+                       vq->sub_ds_msg = NULL;
+                       return 1; /* continue processing ds-response results */
+               } else if(!qstate->blacklist && !vq->chain_blacklist &&
                       (msg=val_find_DS(qstate->env, target_key_name,
                       target_key_len, vq->qchase.qclass, qstate->region,
                       vq->key_entry->name)) ) {
                       verbose(VERB_ALGO, "Process cached DS response");
                       process_ds_response(qstate, vq, id, LDNS_RCODE_NOERROR,
-                               msg, &msg->qinfo, NULL);
+                               msg, &msg->qinfo, NULL, &suspend);
+                       if(suspend) {
+                               /* we'll come back here later to continue */
+                               if(!validate_suspend_setup_timer(qstate, vq,
+                                       id, VAL_FINDKEY_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
                       return 1; /* continue processing ds-response results */
               }
               if(!generate_request(qstate, id, target_key_name,
@@ -1864,7 +2118,7 @@ processValidate(struct module_qstate* qs
       struct val_env* ve, int id)
{
       enum val_classification subtype;
-       int rcode;
+       int rcode, suspend, nsec3_calculations = 0;

       if(!vq->key_entry) {
               verbose(VERB_ALGO, "validate: no key entry, failed");
@@ -1921,8 +2175,14 @@ processValidate(struct module_qstate* qs

       /* check signatures in the message;
        * answer and authority must be valid, additional is only checked. */
-       if(!validate_msg_signatures(qstate, qstate->env, ve, &vq->qchase,
-               vq->chase_reply, vq->key_entry)) {
+       if(!validate_msg_signatures(qstate, vq, qstate->env, ve, &vq->qchase,
+               vq->chase_reply, vq->key_entry, &suspend)) {
+               if(suspend) {
+                       if(!validate_suspend_setup_timer(qstate, vq,
+                               id, VAL_VALIDATE_STATE))
+                               return val_error(qstate, id);
+                       return 0;
+               }
               /* workaround bad recursor out there that truncates (even
                * with EDNS4k) to 512 by removing RRSIG from auth section
                * for positive replies*/
@@ -1951,7 +2211,14 @@ processValidate(struct module_qstate* qs
               case VAL_CLASS_POSITIVE:
                       verbose(VERB_ALGO, "Validating a positive response");
                       validate_positive_response(qstate->env, ve,
-                               &vq->qchase, vq->chase_reply, vq->key_entry);
+                               &vq->qchase, vq->chase_reply, vq->key_entry,
+                               qstate, vq, &nsec3_calculations, &suspend);
+                       if(suspend) {
+                               if(!validate_suspend_setup_timer(qstate,
+                                       vq, id, VAL_VALIDATE_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
                       verbose(VERB_DETAIL, "validate(positive): %s",
                               sec_status_to_string(
                               vq->chase_reply->security));
@@ -1960,7 +2227,14 @@ processValidate(struct module_qstate* qs
               case VAL_CLASS_NODATA:
                       verbose(VERB_ALGO, "Validating a nodata response");
                       validate_nodata_response(qstate->env, ve,
-                               &vq->qchase, vq->chase_reply, vq->key_entry);
+                               &vq->qchase, vq->chase_reply, vq->key_entry,
+                               qstate, vq, &nsec3_calculations, &suspend);
+                       if(suspend) {
+                               if(!validate_suspend_setup_timer(qstate,
+                                       vq, id, VAL_VALIDATE_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
                       verbose(VERB_DETAIL, "validate(nodata): %s",
                               sec_status_to_string(
                               vq->chase_reply->security));
@@ -1970,7 +2244,14 @@ processValidate(struct module_qstate* qs
                       rcode = (int)FLAGS_GET_RCODE(vq->orig_msg->rep->flags);
                       verbose(VERB_ALGO, "Validating a nxdomain response");
                       validate_nameerror_response(qstate->env, ve,
-                               &vq->qchase, vq->chase_reply, vq->key_entry, &rcode);
+                               &vq->qchase, vq->chase_reply, vq->key_entry, &rcode,
+                               qstate, vq, &nsec3_calculations, &suspend);
+                       if(suspend) {
+                               if(!validate_suspend_setup_timer(qstate,
+                                       vq, id, VAL_VALIDATE_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
                       verbose(VERB_DETAIL, "validate(nxdomain): %s",
                               sec_status_to_string(
                               vq->chase_reply->security));
@@ -1981,7 +2262,14 @@ processValidate(struct module_qstate* qs
               case VAL_CLASS_CNAME:
                       verbose(VERB_ALGO, "Validating a cname response");
                       validate_cname_response(qstate->env, ve,
-                               &vq->qchase, vq->chase_reply, vq->key_entry);
+                               &vq->qchase, vq->chase_reply, vq->key_entry,
+                               qstate, vq, &nsec3_calculations, &suspend);
+                       if(suspend) {
+                               if(!validate_suspend_setup_timer(qstate,
+                                       vq, id, VAL_VALIDATE_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
                       verbose(VERB_DETAIL, "validate(cname): %s",
                               sec_status_to_string(
                               vq->chase_reply->security));
@@ -1991,7 +2279,14 @@ processValidate(struct module_qstate* qs
                       verbose(VERB_ALGO, "Validating a cname noanswer "
                               "response");
                       validate_cname_noanswer_response(qstate->env, ve,
-                               &vq->qchase, vq->chase_reply, vq->key_entry);
+                               &vq->qchase, vq->chase_reply, vq->key_entry,
+                               qstate, vq, &nsec3_calculations, &suspend);
+                       if(suspend) {
+                               if(!validate_suspend_setup_timer(qstate,
+                                       vq, id, VAL_VALIDATE_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
                       verbose(VERB_DETAIL, "validate(cname_noanswer): %s",
                               sec_status_to_string(
                               vq->chase_reply->security));
@@ -2008,8 +2303,15 @@ processValidate(struct module_qstate* qs
               case VAL_CLASS_ANY:
                       verbose(VERB_ALGO, "Validating a positive ANY "
                               "response");
-                       validate_any_response(qstate->env, ve, &vq->qchase,
-                               vq->chase_reply, vq->key_entry);
+                       validate_any_response(qstate->env, ve, &vq->qchase,
+                               vq->chase_reply, vq->key_entry, qstate, vq,
+                               &nsec3_calculations, &suspend);
+                       if(suspend) {
+                               if(!validate_suspend_setup_timer(qstate,
+                                       vq, id, VAL_VALIDATE_STATE))
+                                       return val_error(qstate, id);
+                               return 0;
+                       }
                       verbose(VERB_DETAIL, "validate(positive_any): %s",
                               sec_status_to_string(
                               vq->chase_reply->security));
@@ -2118,16 +2420,13 @@ processFinished(struct module_qstate* qs
       if(vq->orig_msg->rep->security == sec_status_bogus) {
               /* see if we can try again to fetch data */
               if(vq->restart_count < ve->max_restart) {
-                       int restart_count = vq->restart_count+1;
                       verbose(VERB_ALGO, "validation failed, "
                               "blacklist and retry to fetch data");
                       val_blacklist(&qstate->blacklist, qstate->region,
                               qstate->reply_origin, 0);
                       qstate->reply_origin = NULL;
                       qstate->errinf = NULL;
-                       memset(vq, 0, sizeof(*vq));
-                       vq->restart_count = restart_count;
-                       vq->state = VAL_INIT_STATE;
+                       val_restart(vq);
                       verbose(VERB_ALGO, "pass back to next module");
                       qstate->ext_state[id] = module_restart_next;
                       return 0;
@@ -2454,7 +2753,10 @@ primeResponseToKE(struct ub_packed_rrset
 *     DS response indicated an end to secure space, is_good if the DS
 *     validated. It returns ke=NULL if the DS response indicated that the
 *     request wasn't a delegation point.
- * @return 0 on servfail error (malloc failure).
+ * @return
+ *     0 on success,
+ *     1 on servfail error (malloc failure),
+ *     2 on NSEC3 suspend.
 */
static int
ds_response_to_ke(struct module_qstate* qstate, struct val_qstate* vq,
@@ -2465,6 +2767,7 @@ ds_response_to_ke(struct module_qstate*
       char* reason = NULL;
       sldns_ede_code reason_bogus = LDNS_EDE_DNSSEC_BOGUS;
       enum val_classification subtype;
+       int verified;
       if(rcode != LDNS_RCODE_NOERROR) {
               char rc[16];
               rc[0]=0;
@@ -2495,7 +2798,7 @@ ds_response_to_ke(struct module_qstate*
               /* Verify only returns BOGUS or SECURE. If the rrset is
                * bogus, then we are done. */
               sec = val_verify_rrset_entry(qstate->env, ve, ds,
-                       vq->key_entry, &reason, &reason_bogus, LDNS_SECTION_ANSWER, qstate);
+                       vq->key_entry, &reason, &reason_bogus, LDNS_SECTION_ANSWER, qstate, &verified);
               if(sec != sec_status_secure) {
                       verbose(VERB_DETAIL, "DS rrset in DS response did "
                               "not verify");
@@ -2513,7 +2816,7 @@ ds_response_to_ke(struct module_qstate*
                               ub_packed_rrset_ttl(ds),
                               LDNS_EDE_UNSUPPORTED_DS_DIGEST, NULL,
                               *qstate->env->now);
-                       return (*ke) != NULL;
+                       return (*ke) == NULL;
               }

               /* Otherwise, we return the positive response. */
@@ -2521,7 +2824,7 @@ ds_response_to_ke(struct module_qstate*
               *ke = key_entry_create_rrset(qstate->region,
                       qinfo->qname, qinfo->qname_len, qinfo->qclass, ds,
                       NULL, LDNS_EDE_NONE, NULL, *qstate->env->now);
-               return (*ke) != NULL;
+               return (*ke) == NULL;
       } else if(subtype == VAL_CLASS_NODATA ||
               subtype == VAL_CLASS_NAMEERROR) {
               /* NODATA means that the qname exists, but that there was
@@ -2555,12 +2858,12 @@ ds_response_to_ke(struct module_qstate*
                                       qinfo->qclass, proof_ttl,
                                       LDNS_EDE_NONE, NULL,
                                       *qstate->env->now);
-                               return (*ke) != NULL;
+                               return (*ke) == NULL;
                       case sec_status_insecure:
                               verbose(VERB_DETAIL, "NSEC RRset for the "
                                 "referral proved not a delegation point");
                               *ke = NULL;
-                               return 1;
+                               return 0;
                       case sec_status_bogus:
                               verbose(VERB_DETAIL, "NSEC RRset for the "
                                       "referral did not prove no DS.");
@@ -2572,10 +2875,17 @@ ds_response_to_ke(struct module_qstate*
                               break;
               }

+               if(!nsec3_cache_table_init(&vq->nsec3_cache_table, qstate->region)) {
+                       log_err("malloc failure in ds_response_to_ke for "
+                               "NSEC3 cache");
+                       reason = "malloc failure";
+                       errinf_ede(qstate, reason, 0);
+                       goto return_bogus;
+               }
               sec = nsec3_prove_nods(qstate->env, ve,
                       msg->rep->rrsets + msg->rep->an_numrrsets,
                       msg->rep->ns_numrrsets, qinfo, vq->key_entry, &reason,
-                       &reason_bogus, qstate);
+                       &reason_bogus, qstate, &vq->nsec3_cache_table);
               switch(sec) {
                       case sec_status_insecure:
                               /* case insecure also continues to unsigned
@@ -2589,18 +2899,19 @@ ds_response_to_ke(struct module_qstate*
                                       qinfo->qclass, proof_ttl,
                                       LDNS_EDE_NONE, NULL,
                                       *qstate->env->now);
-                               return (*ke) != NULL;
+                               return (*ke) == NULL;
                       case sec_status_indeterminate:
                               verbose(VERB_DETAIL, "NSEC3s for the "
                                 "referral proved no delegation");
                               *ke = NULL;
-                               return 1;
+                               return 0;
                       case sec_status_bogus:
                               verbose(VERB_DETAIL, "NSEC3s for the "
                                       "referral did not prove no DS.");
                               errinf_ede(qstate, reason, reason_bogus);
                               goto return_bogus;
                       case sec_status_unchecked:
+                               return 2;
                       default:
                               /* NSEC3 proof did not work */
                               break;
@@ -2641,13 +2952,13 @@ ds_response_to_ke(struct module_qstate*
               }
               sec = val_verify_rrset_entry(qstate->env, ve, cname,
                       vq->key_entry, &reason, &reason_bogus,
-                       LDNS_SECTION_ANSWER, qstate);
+                       LDNS_SECTION_ANSWER, qstate, &verified);
               if(sec == sec_status_secure) {
                       verbose(VERB_ALGO, "CNAME validated, "
                               "proof that DS does not exist");
                       /* and that it is not a referral point */
                       *ke = NULL;
-                       return 1;
+                       return 0;
               }
               errinf(qstate, "CNAME in DS response was not secure.");
               errinf_ede(qstate, reason, reason_bogus);
@@ -2671,7 +2982,7 @@ return_bogus:
       *ke = key_entry_create_bad(qstate->region, qinfo->qname,
               qinfo->qname_len, qinfo->qclass, BOGUS_KEY_TTL,
               reason_bogus, reason, *qstate->env->now);
-       return (*ke) != NULL;
+       return (*ke) == NULL;
}

/**
@@ -2692,17 +3003,31 @@ return_bogus:
static void
process_ds_response(struct module_qstate* qstate, struct val_qstate* vq,
       int id, int rcode, struct dns_msg* msg, struct query_info* qinfo,
-       struct sock_list* origin)
+       struct sock_list* origin, int* suspend)
{
       struct val_env* ve = (struct val_env*)qstate->env->modinfo[id];
       struct key_entry_key* dske = NULL;
       uint8_t* olds = vq->empty_DS_name;
+       int ret;
+       *suspend = 0;
       vq->empty_DS_name = NULL;
-       if(!ds_response_to_ke(qstate, vq, id, rcode, msg, qinfo, &dske)) {
+       ret = ds_response_to_ke(qstate, vq, id, rcode, msg, qinfo, &dske);
+       if(ret != 0) {
+               switch(ret) {
+               case 1:
                       log_err("malloc failure in process_ds_response");
                       vq->key_entry = NULL; /* make it error */
                       vq->state = VAL_VALIDATE_STATE;
                       return;
+               case 2:
+                       *suspend = 1;
+                       return;
+               default:
+                       log_err("unhandled error value for ds_response_to_ke");
+                       vq->key_entry = NULL; /* make it error */
+                       vq->state = VAL_VALIDATE_STATE;
+                       return;
+               }
       }
       if(dske == NULL) {
               vq->empty_DS_name = regional_alloc_init(qstate->region,
@@ -2954,9 +3279,26 @@ val_inform_super(struct module_qstate* q
               return;
       }
       if(qstate->qinfo.qtype == LDNS_RR_TYPE_DS) {
+               int suspend;
               process_ds_response(super, vq, id, qstate->return_rcode,
-                       qstate->return_msg, &qstate->qinfo,
-                       qstate->reply_origin);
+                       qstate->return_msg, &qstate->qinfo,
+                       qstate->reply_origin, &suspend);
+               /* If NSEC3 was needed during validation, NULL the NSEC3 cache;
+                * it will be re-initiated if needed later on.
+                * Validation (and the cache table) are happening/allocated in
+                * the super qstate whilst the RRs are allocated (and pointed
+                * to) in this sub qstate. */
+               if(vq->nsec3_cache_table.ct) {
+                       vq->nsec3_cache_table.ct = NULL;
+               }
+               if(suspend) {
+                       /* deep copy the return_msg to vq->sub_ds_msg; it will
+                        * be resumed later in the super state with the caveat
+                        * that the initial calculations will be re-caclulated
+                        * and re-suspended there before continuing. */
+                       vq->sub_ds_msg = dns_msg_deepcopy_region(
+                               qstate->return_msg, super->region);
+               }
               return;
       } else if(qstate->qinfo.qtype == LDNS_RR_TYPE_DNSKEY) {
               process_dnskey_response(super, vq, id, qstate->return_rcode,
@@ -2970,8 +3312,15 @@ val_inform_super(struct module_qstate* q
void
val_clear(struct module_qstate* qstate, int id)
{
+       struct val_qstate* vq;
       if(!qstate)
               return;
+       vq = (struct val_qstate*)qstate->minfo[id];
+       if(vq) {
+               if(vq->suspend_timer) {
+                       comm_timer_delete(vq->suspend_timer);
+               }
+       }
       /* everything is allocated in the region, so assign NULL */
       qstate->minfo[id] = NULL;
}
Index: usr.sbin/unbound/validator/validator.h
===================================================================
RCS file: /cvs/src/usr.sbin/unbound/validator/validator.h,v
diff -u -p -r1.8 validator.h
--- usr.sbin/unbound/validator/validator.h      7 Jun 2022 15:42:54 -0000       1.8
+++ usr.sbin/unbound/validator/validator.h      12 Feb 2024 21:08:03 -0000
@@ -45,11 +45,13 @@
#include "util/module.h"
#include "util/data/msgreply.h"
#include "validator/val_utils.h"
+#include "validator/val_nsec3.h"
struct val_anchors;
struct key_cache;
struct key_entry_key;
struct val_neg_cache;
struct config_strlist;
+struct comm_timer;

/**
 * This is the TTL to use when a trust anchor fails to prime. A trust anchor
@@ -215,6 +217,19 @@ struct val_qstate {

       /** true if this state is waiting to prime a trust anchor */
       int wait_prime_ta;
+
+       /** State to continue with RRSIG validation in a message later */
+       int msg_signatures_state;
+       /** The rrset index for the msg signatures to continue from */
+       size_t msg_signatures_index;
+       /** Cache table for NSEC3 hashes */
+       struct nsec3_cache_table nsec3_cache_table;
+       /** DS message from sub if it got suspended from NSEC3 calculations */
+       struct dns_msg* sub_ds_msg;
+       /** The timer to resume processing msg signatures */
+       struct comm_timer* suspend_timer;
+       /** Number of suspends */
+       int suspend_count;
};

/**
@@ -261,5 +276,8 @@ void val_clear(struct module_qstate* qst
 * @return memory in use in bytes.
 */
size_t val_get_mem(struct module_env* env, int id);
+
+/** Timer callback for msg signatures continue timer */
+void validate_suspend_timer_cb(void* arg);

#endif /* VALIDATOR_VALIDATOR_H */