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; irep->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: 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; ientry.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 #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; inow, 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; ian_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+1msg_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; ian_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; irrset_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; ian_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; ian_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; ian_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; ian_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; ian_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; irep->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: 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; ientry.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 #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; inow, 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; ian_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+1msg_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; ian_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; irrset_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; ian_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; ian_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; ian_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; ian_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; ian_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 */