]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Backport of Keytrap to rec-4.8.x 13784/head rec-4.8.6
authorOtto Moerbeek <otto.moerbeek@open-xchange.com>
Mon, 22 Jan 2024 11:19:16 +0000 (12:19 +0100)
committerOtto Moerbeek <otto.moerbeek@open-xchange.com>
Tue, 6 Feb 2024 08:47:54 +0000 (09:47 +0100)
15 files changed:
pdns/pdns_recursor.cc
pdns/recursordist/aggressive_nsec.cc
pdns/recursordist/aggressive_nsec.hh
pdns/recursordist/rec-main.cc
pdns/recursordist/rec-zonetocache.cc
pdns/recursordist/test-aggressive_nsec_cc.cc
pdns/recursordist/test-syncres_cc.cc
pdns/recursordist/test-syncres_cc4.cc
pdns/recursordist/test-syncres_cc5.cc
pdns/recursordist/test-syncres_cc8.cc
pdns/syncres.cc
pdns/syncres.hh
pdns/validate.cc
pdns/validate.hh
regression-tests.recursor-dnssec/test_AggressiveNSECCache.py

index 858dc28b9d9975661f93a4649f90ffe909dcfd80..0e70d14a857a1dd8fb75b64bf1684d40d4dd9b34 100644 (file)
@@ -551,6 +551,15 @@ static PolicyResult handlePolicyHit(const DNSFilterEngine::Policy& appliedPolicy
           res = RCode::ServFail;
           break;
         }
+        catch (const pdns::validation::TooManySEC3IterationsException& e) {
+          if (g_logCommonErrors) {
+            SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << dc->getRemote() << " during resolve of the custom filter policy '" << appliedPolicy.getName() << "' while resolving '" << dc->d_mdp.d_qname << "' because: " << e.what() << endl,
+                 sr.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during resolve of the custom filter policy",
+                                  "policyName", Logging::Loggable(appliedPolicy.getName()), "exception", Logging::Loggable("TooManySEC3IterationsException")));
+          }
+          res = RCode::ServFail;
+          break;
+        }
         catch (const PolicyHitException& e) {
           if (g_logCommonErrors) {
             SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << dc->getRemote() << " during resolve of the custom filter policy '" << appliedPolicy.getName() << "' while resolving '" << dc->d_mdp.d_qname << "' because another RPZ policy was hit" << endl,
@@ -1197,6 +1206,13 @@ void startDoResolve(void* p)
         }
         res = RCode::ServFail;
       }
+      catch (const pdns::validation::TooManySEC3IterationsException& e) {
+        if (g_logCommonErrors) {
+          SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << dc->getRemote() << " during resolve of '" << dc->d_mdp.d_qname << "' because: " << e.what() << endl,
+               sr.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during resolve"));
+        }
+        res = RCode::ServFail;
+      }
       catch (const SendTruncatedAnswerException& e) {
         ret.clear();
         res = RCode::NoError;
@@ -1379,6 +1395,13 @@ void startDoResolve(void* p)
                  sr.d_slog->error(Logr::Notice, e.reason, "Sending SERVFAIL during validation", "exception", Logging::Loggable("ImmediateServFailException")));
           goto sendit;
         }
+        catch (const pdns::validation::TooManySEC3IterationsException& e) {
+          if (g_logCommonErrors) {
+            SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << dc->getRemote() << " during validation of '" << dc->d_mdp.d_qname << "|" << QType(dc->d_mdp.d_qtype) << "' because: " << e.what() << endl,
+                 sr.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during validation", "exception", Logging::Loggable("TooManySEC3IterationsException")));
+          }
+          goto sendit; // NOLINT(cppcoreguidelines-avoid-goto)
+        }
       }
 
       if (ret.size()) {
index c0b1d6e1822b2024dc4cb8ed77cb1ff0f7274ced..3b6e344f42738e8450b0ce4b820a2123cef1578e 100644 (file)
@@ -28,6 +28,7 @@
 #include "validate.hh"
 
 std::unique_ptr<AggressiveNSECCache> g_aggressiveNSECCache{nullptr};
+uint64_t AggressiveNSECCache::s_nsec3DenialProofMaxCost{0};
 
 /* this is defined in syncres.hh and we are not importing that here */
 extern std::unique_ptr<MemRecursorCache> g_recCache;
@@ -514,7 +515,7 @@ bool AggressiveNSECCache::synthesizeFromNSECWildcard(time_t now, const DNSName&
   return true;
 }
 
-bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded<AggressiveNSECCache::ZoneEntry>>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC)
+bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded<AggressiveNSECCache::ZoneEntry>>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, pdns::validation::ValidationContext& validationContext)
 {
   DNSName zone;
   std::string salt;
@@ -530,7 +531,17 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded
     iterations = entry->d_iterations;
   }
 
-  auto nameHash = DNSName(toBase32Hex(hashQNameWithSalt(salt, iterations, name))) + zone;
+  const auto zoneLabelsCount = zone.countLabels();
+  if (s_nsec3DenialProofMaxCost != 0) {
+    const auto worstCaseIterations = getNSEC3DenialProofWorstCaseIterationsCount(name.countLabels() - zoneLabelsCount, iterations, salt.length());
+    if (worstCaseIterations > s_nsec3DenialProofMaxCost) {
+      // skip NSEC3 aggressive cache for expensive NSEC3 parameters: "if you want us to take the pain of PRSD away from you, you need to make it cheap for us to do so"
+      LOG(name << ": Skipping aggressive use of the NSEC3 cache since the zone parameters are too expensive" << endl);
+      return false;
+    }
+  }
+
+  auto nameHash = DNSName(toBase32Hex(getHashFromNSEC3(name, iterations, salt, validationContext))) + zone;
 
   ZoneEntry::CacheEntry exactNSEC3;
   if (getNSEC3(now, zoneEntry, nameHash, exactNSEC3)) {
@@ -577,8 +588,10 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded
   DNSName closestEncloser(name);
   bool found = false;
   ZoneEntry::CacheEntry closestNSEC3;
-  while (!found && closestEncloser.chopOff()) {
-    auto closestHash = DNSName(toBase32Hex(hashQNameWithSalt(salt, iterations, closestEncloser))) + zone;
+  auto remainingLabels = closestEncloser.countLabels() - 1;
+  while (!found && closestEncloser.chopOff() && remainingLabels >= zoneLabelsCount) {
+    auto closestHash = DNSName(toBase32Hex(getHashFromNSEC3(closestEncloser, iterations, salt, validationContext))) + zone;
+    remainingLabels--;
 
     if (getNSEC3(now, zoneEntry, closestHash, closestNSEC3)) {
       LOG("Found closest encloser at " << closestEncloser << " (" << closestHash << ")" << endl);
@@ -739,7 +752,7 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded
   return true;
 }
 
-bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, const ComboAddress& who, const boost::optional<std::string>& routingTag, bool doDNSSEC)
+bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, const ComboAddress& who, const boost::optional<std::string>& routingTag, bool doDNSSEC, pdns::validation::ValidationContext& validationContext)
 {
   std::shared_ptr<LockGuarded<ZoneEntry>> zoneEntry;
   if (type == QType::DS) {
@@ -779,7 +792,7 @@ bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType
   }
 
   if (nsec3) {
-    return getNSEC3Denial(now, zoneEntry, soaSet, soaSignatures, name, type, ret, res, doDNSSEC);
+    return getNSEC3Denial(now, zoneEntry, soaSet, soaSignatures, name, type, ret, res, doDNSSEC, validationContext);
   }
 
   ZoneEntry::CacheEntry entry;
index eb9d2bfa01e48c4f6ce9f255c5040d85654e0d63..c19b761ab4f94ff04fa6545c1e8665490adb3fde 100644 (file)
@@ -35,17 +35,20 @@ using namespace ::boost::multi_index;
 #include "dnsrecords.hh"
 #include "lock.hh"
 #include "stat_t.hh"
+#include "validate.hh"
 
 class AggressiveNSECCache
 {
 public:
+  static uint64_t s_nsec3DenialProofMaxCost;
+
   AggressiveNSECCache(uint64_t entries) :
     d_maxEntries(entries)
   {
   }
 
   void insertNSEC(const DNSName& zone, const DNSName& owner, const DNSRecord& record, const std::vector<std::shared_ptr<RRSIGRecordContent>>& signatures, bool nsec3);
-  bool getDenial(time_t, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, const ComboAddress& who, const boost::optional<std::string>& routingTag, bool doDNSSEC);
+  bool getDenial(time_t, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, const ComboAddress& who, const boost::optional<std::string>& routingTag, bool doDNSSEC, pdns::validation::ValidationContext& validationContext);
 
   void removeZoneInfo(const DNSName& zone, bool subzones);
 
@@ -132,7 +135,7 @@ private:
   std::shared_ptr<LockGuarded<ZoneEntry>> getBestZone(const DNSName& zone);
   bool getNSECBefore(time_t now, std::shared_ptr<LockGuarded<ZoneEntry>>& zoneEntry, const DNSName& name, ZoneEntry::CacheEntry& entry);
   bool getNSEC3(time_t now, std::shared_ptr<LockGuarded<ZoneEntry>>& zoneEntry, const DNSName& name, ZoneEntry::CacheEntry& entry);
-  bool getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded<ZoneEntry>>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC);
+  bool getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded<ZoneEntry>>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, pdns::validation::ValidationContext& validationContext);
   bool synthesizeFromNSEC3Wildcard(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, ZoneEntry::CacheEntry& nextCloser, const DNSName& wildcardName);
   bool synthesizeFromNSECWildcard(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, ZoneEntry::CacheEntry& nsec, const DNSName& wildcardName);
 
index bb6b742b890de023cd318e021bfe9801249665aa..2fa84153e4f937a671ac9017f90412b56f75ba5b 100644 (file)
@@ -1436,6 +1436,10 @@ static int serviceMain(int argc, char* argv[], Logr::log_t log)
 
   g_dnssecLogBogus = ::arg().mustDo("dnssec-log-bogus");
   g_maxNSEC3Iterations = ::arg().asNum("nsec3-max-iterations");
+  g_maxRRSIGsPerRecordToConsider = ::arg().asNum("max-rrsigs-per-record");
+  g_maxNSEC3sPerRecordToConsider = ::arg().asNum("max-nsec3s-per-record");
+  g_maxDNSKEYsToConsider = ::arg().asNum("max-dnskeys");
+  g_maxDSsToConsider = ::arg().asNum("max-ds-per-zone");
 
   g_maxCacheEntries = ::arg().asNum("max-cache-entries");
   g_maxPacketCacheEntries = ::arg().asNum("max-packetcache-entries");
@@ -1522,6 +1526,8 @@ static int serviceMain(int argc, char* argv[], Logr::log_t log)
   SyncRes::s_maxnsaddressqperq = ::arg().asNum("max-ns-address-qperq");
   SyncRes::s_maxtotusec = 1000 * ::arg().asNum("max-total-msec");
   SyncRes::s_maxdepth = ::arg().asNum("max-recursion-depth");
+  SyncRes::s_maxvalidationsperq = ::arg().asNum("max-signature-validations-per-query");
+  SyncRes::s_maxnsec3iterationsperq = ::arg().asNum("max-nsec3-hash-computations-per-query");
   SyncRes::s_rootNXTrust = ::arg().mustDo("root-nx-trust");
   SyncRes::s_refresh_ttlperc = ::arg().asNum("refresh-on-ttl-perc");
   SyncRes::s_locked_ttlperc = ::arg().asNum("record-cache-locked-ttl-perc");
@@ -1701,6 +1707,7 @@ static int serviceMain(int argc, char* argv[], Logr::log_t log)
 
   g_addExtendedResolutionDNSErrors = ::arg().mustDo("extended-resolution-errors");
 
+  AggressiveNSECCache::s_nsec3DenialProofMaxCost = ::arg().asNum("aggressive-cache-max-nsec3-hash-cost");
   if (::arg().asNum("aggressive-nsec-cache-size") > 0) {
     if (g_dnssecmode == DNSSECMode::ValidateAll || g_dnssecmode == DNSSECMode::ValidateForLog || g_dnssecmode == DNSSECMode::Process) {
       g_aggressiveNSECCache = make_unique<AggressiveNSECCache>(::arg().asNum("aggressive-nsec-cache-size"));
@@ -2773,6 +2780,14 @@ int main(int argc, char** argv)
     ::arg().set("tcp-fast-open-connect", "Enable TCP Fast Open support on outgoing sockets") = "no";
     ::arg().set("nsec3-max-iterations", "Maximum number of iterations allowed for an NSEC3 record") = "150";
 
+    ::arg().set("max-rrsigs-per-record", "Maximum number of RRSIGs to consider when validating a given record") = "2";
+    ::arg().set("max-nsec3s-per-record", "Maximum number of NSEC3s to consider when validating a given denial of existence") = "10";
+    ::arg().set("max-signature-validations-per-query", "Maximum number of RRSIG signatures we are willing to validate per incoming query") = "30";
+    ::arg().set("max-nsec3-hash-computations-per-query", "Maximum number of NSEC3 hashes that we are willing to compute during DNSSEC validation, per incoming query") = "600";
+    ::arg().set("aggressive-cache-max-nsec3-hash-cost", "Maximum estimated NSEC3 cost for a given query to consider aggressive use of the NSEC3 cache") = "150";
+    ::arg().set("max-dnskeys", "Maximum number of DNSKEYs with the same algorithm and tag to consider when validating a given record") = "2";
+    ::arg().set("max-ds-per-zone", "Maximum number of DS records to consider per zone") = "8";
+
     ::arg().set("cpu-map", "Thread to CPU mapping, space separated thread-id=cpu1,cpu2..cpuN pairs") = "";
 
     ::arg().setSwitch("log-rpz-changes", "Log additions and removals to RPZ zones at Info level") = "no";
index a83dc43da87a2c85b7a5eba611c21f86d617f5b3..dfe61c915128e77aee27aa2151cf547dee047c44 100644 (file)
@@ -243,6 +243,8 @@ pdns::ZoneMD::Result ZoneData::processLines(const vector<string>& lines, const R
 
 vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const
 {
+  pdns::validation::ValidationContext validationContext;
+  validationContext.d_nsec3IterationsRemainingQuota = std::numeric_limits<decltype(validationContext.d_nsec3IterationsRemainingQuota)>::max();
   zonemdCount = 0;
 
   SyncRes sr({d_now, 0});
@@ -266,7 +268,7 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const
   }
 
   skeyset_t validKeys;
-  vState dnsKeyState = validateDNSKeysAgainstDS(d_now, d_zone, dsmap, dnsKeys, records, zonemd.getRRSIGs(), validKeys);
+  vState dnsKeyState = validateDNSKeysAgainstDS(d_now, d_zone, dsmap, dnsKeys, records, zonemd.getRRSIGs(), validKeys, validationContext);
   if (dnsKeyState != vState::Secure) {
     return dnsKeyState;
   }
@@ -288,7 +290,7 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const
 
     if (nsecs.records.size() > 0 && nsecs.signatures.size() > 0) {
       // Valdidate the NSEC
-      nsecValidationStatus = validateWithKeySet(d_now, d_zone, nsecs.records, nsecs.signatures, validKeys);
+      nsecValidationStatus = validateWithKeySet(d_now, d_zone, nsecs.records, nsecs.signatures, validKeys, validationContext);
       csp.emplace(std::make_pair(d_zone, QType::NSEC), nsecs);
     }
     else if (nsec3s.records.size() > 0 && nsec3s.signatures.size() > 0) {
@@ -297,13 +299,13 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const
       for (const auto& rec : zonemd.getNSEC3Params()) {
         records.emplace(rec);
       }
-      nsecValidationStatus = validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys);
+      nsecValidationStatus = validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys, validationContext);
       if (nsecValidationStatus != vState::Secure) {
         d_log->info("NSEC3PARAMS records did not validate");
         return nsecValidationStatus;
       }
       // Valdidate the NSEC3
-      nsecValidationStatus = validateWithKeySet(d_now, zonemd.getNSEC3Label(), nsec3s.records, nsec3s.signatures, validKeys);
+      nsecValidationStatus = validateWithKeySet(d_now, zonemd.getNSEC3Label(), nsec3s.records, nsec3s.signatures, validKeys, validationContext);
       csp.emplace(std::make_pair(zonemd.getNSEC3Label(), QType::NSEC3), nsec3s);
     }
     else {
@@ -316,7 +318,7 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const
       return nsecValidationStatus;
     }
 
-    auto denial = getDenial(csp, d_zone, QType::ZONEMD, false, false, true);
+    auto denial = getDenial(csp, d_zone, QType::ZONEMD, false, false, validationContext, true);
     if (denial == dState::NXQTYPE) {
       d_log->info("Validated denial of absence of ZONEMD record");
       return vState::Secure;
@@ -330,7 +332,7 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const
   for (const auto& rec : zonemdRecords) {
     records.emplace(rec);
   }
-  return validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys);
+  return validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys, validationContext);
 }
 
 void ZoneData::ZoneToCache(const RecZoneToCache::Config& config)
index 51693050c66c48c279e7b7a55fb8837f10312f72..75408cd125d2545433c058dc67f57c6db156aca3 100644 (file)
@@ -1218,6 +1218,22 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec_dump)
   free(line);
 }
 
+static bool getDenialWrapper(std::unique_ptr<AggressiveNSECCache>& cache, time_t now, const DNSName& name, const QType& qtype, const std::optional<int> expectedResult = std::nullopt, const std::optional<size_t> expectedRecordsCount = std::nullopt)
+{
+  int res;
+  std::vector<DNSRecord> results;
+  pdns::validation::ValidationContext validationContext;
+  validationContext.d_nsec3IterationsRemainingQuota = std::numeric_limits<decltype(validationContext.d_nsec3IterationsRemainingQuota)>::max();
+  bool found = cache->getDenial(now, name, qtype, results, res, ComboAddress("192.0.2.1"), boost::none, true, validationContext);
+  if (expectedResult) {
+    BOOST_CHECK_EQUAL(res, *expectedResult);
+  }
+  if (expectedRecordsCount) {
+    BOOST_CHECK_EQUAL(results.size(), *expectedRecordsCount);
+  }
+  return found;
+}
+
 BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_rollover)
 {
   /* test that we don't compare a hash using the wrong (former) salt or iterations count in case of a rollover,
@@ -1272,12 +1288,9 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_rollover)
 
   BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U);
 
-  int res;
-  std::vector<DNSRecord> results;
-
   /* we can use the NSEC3s we have */
   /* direct match */
-  BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
+  BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA), true);
 
   DNSName other("other.powerdns.com");
   /* now we insert a new NSEC3, with a different salt, changing that value for the zone */
@@ -1304,10 +1317,10 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_rollover)
 
   /* we should be able to find a direct match for that name */
   /* direct match */
-  BOOST_CHECK_EQUAL(cache->getDenial(now, other, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
+  BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, other, QType::AAAA), true);
 
   /* but we should not be able to use the other NSEC3s */
-  BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false);
+  BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA), false);
 
   /* and the same thing but this time updating the iterations count instead of the salt */
   DNSName other2("other2.powerdns.com");
@@ -1334,10 +1347,10 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_rollover)
 
   /* we should be able to find a direct match for that name */
   /* direct match */
-  BOOST_CHECK_EQUAL(cache->getDenial(now, other2, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
+  BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, other2, QType::AAAA), true);
 
   /* but we should not be able to use the other NSEC3s */
-  BOOST_CHECK_EQUAL(cache->getDenial(now, other, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false);
+  BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, other, QType::AAAA), false);
 }
 
 BOOST_AUTO_TEST_CASE(test_aggressive_nsec_ancestor_cases)
@@ -1385,15 +1398,10 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec_ancestor_cases)
     BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U);
 
     /* the cache should now be able to deny other types (except the DS) */
-    int res;
-    std::vector<DNSRecord> results;
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
-    BOOST_CHECK_EQUAL(res, RCode::NoError);
-    BOOST_CHECK_EQUAL(results.size(), 3U);
+     /* the cache should now be able to deny other types (except the DS) */
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NoError, 3U), true);
     /* but not the DS that lives in the parent zone */
-    results.clear();
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), false);
-    BOOST_CHECK_EQUAL(results.size(), 0U);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, std::nullopt, 0U), false);
   }
 
   {
@@ -1418,14 +1426,9 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec_ancestor_cases)
     BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U);
 
     /* the cache should now be able to deny the DS */
-    int res;
-    std::vector<DNSRecord> results;
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
-    BOOST_CHECK_EQUAL(res, RCode::NoError);
-    BOOST_CHECK_EQUAL(results.size(), 3U);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NoError, 3U), true);
     /* but not any type that lives in the child zone */
-    results.clear();
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA), false);
   }
 
   {
@@ -1450,16 +1453,9 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec_ancestor_cases)
     BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U);
 
     /* the cache should now be able to deny other types */
-    int res;
-    std::vector<DNSRecord> results;
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
-    BOOST_CHECK_EQUAL(res, RCode::NoError);
-    BOOST_CHECK_EQUAL(results.size(), 3U);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NoError, 3U), true);
     /* including the DS */
-    results.clear();
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
-    BOOST_CHECK_EQUAL(res, RCode::NoError);
-    BOOST_CHECK_EQUAL(results.size(), 3U);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NoError, 3U), true);
   }
 
   {
@@ -1508,17 +1504,10 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec_ancestor_cases)
     }
 
     /* the cache should now be able to deny any type for the name  */
-    int res;
-    std::vector<DNSRecord> results;
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
-    BOOST_CHECK_EQUAL(res, RCode::NXDomain);
-    BOOST_CHECK_EQUAL(results.size(), 5U);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NXDomain, 5U), true);
 
     /* including the DS, since we are not at the apex */
-    results.clear();
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
-    BOOST_CHECK_EQUAL(res, RCode::NXDomain);
-    BOOST_CHECK_EQUAL(results.size(), 5U);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NXDomain, 5U), true);
   }
 }
 
@@ -1576,15 +1565,9 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_ancestor_cases)
     BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U);
 
     /* the cache should now be able to deny other types (except the DS) */
-    int res;
-    std::vector<DNSRecord> results;
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
-    BOOST_CHECK_EQUAL(res, RCode::NoError);
-    BOOST_CHECK_EQUAL(results.size(), 3U);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NoError, 3U), true);
     /* but not the DS that lives in the parent zone */
-    results.clear();
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), false);
-    BOOST_CHECK_EQUAL(results.size(), 0U);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, std::nullopt, 0U), false);
   }
 
   {
@@ -1615,14 +1598,9 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_ancestor_cases)
     BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U);
 
     /* the cache should now be able to deny the DS */
-    int res;
-    std::vector<DNSRecord> results;
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
-    BOOST_CHECK_EQUAL(res, RCode::NoError);
-    BOOST_CHECK_EQUAL(results.size(), 3U);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NoError, 3U), true);
     /* but not any type that lives in the child zone */
-    results.clear();
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA), false);
   }
 
   {
@@ -1653,16 +1631,9 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_ancestor_cases)
     BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U);
 
     /* the cache should now be able to deny other types */
-    int res;
-    std::vector<DNSRecord> results;
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
-    BOOST_CHECK_EQUAL(res, RCode::NoError);
-    BOOST_CHECK_EQUAL(results.size(), 3U);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NoError, 3U), true);
     /* including the DS */
-    results.clear();
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
-    BOOST_CHECK_EQUAL(res, RCode::NoError);
-    BOOST_CHECK_EQUAL(results.size(), 3U);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NoError, 3U), true);
   }
 
   {
@@ -1757,17 +1728,9 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_ancestor_cases)
     }
 
     /* the cache should now be able to deny any type for the name  */
-    int res;
-    std::vector<DNSRecord> results;
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
-    BOOST_CHECK_EQUAL(res, RCode::NXDomain);
-    BOOST_CHECK_EQUAL(results.size(), 7U);
-
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NXDomain, 7U), true);
     /* including the DS, since we are not at the apex */
-    results.clear();
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
-    BOOST_CHECK_EQUAL(res, RCode::NXDomain);
-    BOOST_CHECK_EQUAL(results.size(), 7U);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NXDomain, 7U), true);
   }
   {
     /* we insert NSEC3s coming from the parent zone that could look like a valid denial but are not */
@@ -1862,15 +1825,166 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_ancestor_cases)
     }
 
     /* the cache should NOT be able to deny the name  */
-    int res;
-    std::vector<DNSRecord> results;
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false);
-    BOOST_CHECK_EQUAL(results.size(), 0U);
-
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, std::nullopt, 0U), false);
     /* and the same for the DS */
-    results.clear();
-    BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), false);
-    BOOST_CHECK_EQUAL(results.size(), 0U);
+    BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, std::nullopt, 0U), false);
+  }
+}
+
+BOOST_AUTO_TEST_CASE(test_aggressive_max_nsec3_hash_cost)
+{
+  g_recCache = std::make_unique<MemRecursorCache>();
+
+  const DNSName zone("powerdns.com");
+  time_t now = time(nullptr);
+
+  /* first we need a SOA */
+  std::vector<DNSRecord> records;
+  time_t ttd = now + 30;
+  DNSRecord drSOA;
+  drSOA.d_name = zone;
+  drSOA.d_type = QType::SOA;
+  drSOA.d_class = QClass::IN;
+  drSOA.d_content = std::make_shared<SOARecordContent>("pdns-public-ns1.powerdns.com. pieter\\.lexis.powerdns.com. 2017032301 10800 3600 604800 3600");
+  drSOA.d_ttl = static_cast<uint32_t>(ttd); // XXX truncation
+  drSOA.d_place = DNSResourceRecord::ANSWER;
+  records.push_back(drSOA);
+
+  g_recCache->replace(now, zone, QType(QType::SOA), records, {}, {}, true, zone, boost::none, boost::none, vState::Secure);
+  BOOST_CHECK_EQUAL(g_recCache->size(), 1U);
+
+  auto insertNSEC3s = [zone, now](std::unique_ptr<AggressiveNSECCache>& cache, const std::string& salt, unsigned int iterationsCount) -> void {
+    {
+      /* insert a NSEC3 matching the apex (will be the closest encloser) */
+      DNSName name("powerdns.com");
+      std::string hashed = hashQNameWithSalt(salt, iterationsCount, name);
+      DNSRecord rec;
+      rec.d_name = DNSName(toBase32Hex(hashed)) + zone;
+      rec.d_type = QType::NSEC3;
+      rec.d_ttl = now + 10;
+
+      NSEC3RecordContent nrc;
+      nrc.d_algorithm = 1;
+      nrc.d_flags = 0;
+      nrc.d_iterations = iterationsCount;
+      nrc.d_salt = salt;
+      nrc.d_nexthash = hashed;
+      incrementHash(nrc.d_nexthash);
+      for (const auto& type : {QType::A}) {
+        nrc.set(type);
+      }
+
+      rec.d_content = std::make_shared<NSEC3RecordContent>(nrc);
+      auto rrsig = std::make_shared<RRSIGRecordContent>("NSEC3 5 3 10 20370101000000 20370101000000 24567 powerdns.com. data");
+      cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true);
+    }
+    {
+      /* insert a NSEC3 matching *.powerdns.com (wildcard) */
+      DNSName name("*.powerdns.com");
+      std::string hashed = hashQNameWithSalt(salt, iterationsCount, name);
+      auto before = hashed;
+      decrementHash(before);
+      DNSRecord rec;
+      rec.d_name = DNSName(toBase32Hex(before)) + zone;
+      rec.d_type = QType::NSEC3;
+      rec.d_ttl = now + 10;
+
+      NSEC3RecordContent nrc;
+      nrc.d_algorithm = 1;
+      nrc.d_flags = 0;
+      nrc.d_iterations = iterationsCount;
+      nrc.d_salt = salt;
+      nrc.d_nexthash = hashed;
+      incrementHash(nrc.d_nexthash);
+      for (const auto& type : {QType::A}) {
+        nrc.set(type);
+      }
+
+      rec.d_content = std::make_shared<NSEC3RecordContent>(nrc);
+      auto rrsig = std::make_shared<RRSIGRecordContent>("NSEC3 5 3 10 20370101000000 20370101000000 24567 powerdns.com. data");
+      cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true);
+    }
+    {
+      /* insert a NSEC3 matching sub.powerdns.com (next closer) */
+      DNSName name("sub.powerdns.com");
+      std::string hashed = hashQNameWithSalt(salt, iterationsCount, name);
+      auto before = hashed;
+      decrementHash(before);
+      DNSRecord rec;
+      rec.d_name = DNSName(toBase32Hex(before)) + zone;
+      rec.d_type = QType::NSEC3;
+      rec.d_ttl = now + 10;
+
+      NSEC3RecordContent nrc;
+      nrc.d_algorithm = 1;
+      nrc.d_flags = 0;
+      nrc.d_iterations = iterationsCount;
+      nrc.d_salt = salt;
+      nrc.d_nexthash = hashed;
+      incrementHash(nrc.d_nexthash);
+      for (const auto& type : {QType::A}) {
+        nrc.set(type);
+      }
+
+      rec.d_content = std::make_shared<NSEC3RecordContent>(nrc);
+      auto rrsig = std::make_shared<RRSIGRecordContent>("NSEC3 5 3 10 20370101000000 20370101000000 24567 powerdns.com. data");
+      cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true);
+    }
+    BOOST_CHECK_EQUAL(cache->getEntriesCount(), 3U);
+  };
+
+  {
+    /* zone with cheap parameters */
+    const std::string salt;
+    const unsigned int iterationsCount = 0;
+    AggressiveNSECCache::s_nsec3DenialProofMaxCost = 10;
+
+    auto cache = make_unique<AggressiveNSECCache>(10000);
+    insertNSEC3s(cache, salt, iterationsCount);
+
+    /* the cache should now be able to deny everything below sub.powerdns.com,
+       IF IT DOES NOT EXCEED THE COST */
+    {
+      /* short name: 10 labels below the zone apex */
+      DNSName lookupName("a.b.c.d.e.f.g.h.i.sub.powerdns.com.");
+      BOOST_CHECK_EQUAL(lookupName.countLabels() - zone.countLabels(), 10U);
+      BOOST_CHECK_LE(getNSEC3DenialProofWorstCaseIterationsCount(lookupName.countLabels() - zone.countLabels(), iterationsCount, salt.size()), AggressiveNSECCache::s_nsec3DenialProofMaxCost);
+      BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, lookupName, QType::AAAA, RCode::NXDomain, 7U), true);
+    }
+    {
+      /* longer name: 11 labels below the zone apex */
+      DNSName lookupName("a.b.c.d.e.f.g.h.i.j.sub.powerdns.com.");
+      BOOST_CHECK_EQUAL(lookupName.countLabels() - zone.countLabels(), 11U);
+      BOOST_CHECK_GT(getNSEC3DenialProofWorstCaseIterationsCount(lookupName.countLabels() - zone.countLabels(), iterationsCount, salt.size()), AggressiveNSECCache::s_nsec3DenialProofMaxCost);
+      BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, lookupName, QType::AAAA), false);
+    }
+  }
+
+  {
+    /* zone with expensive parameters */
+    const std::string salt("deadbeef");
+    const unsigned int iterationsCount = 50;
+    AggressiveNSECCache::s_nsec3DenialProofMaxCost = 100;
+
+    auto cache = make_unique<AggressiveNSECCache>(10000);
+    insertNSEC3s(cache, salt, iterationsCount);
+
+    /* the cache should now be able to deny everything below sub.powerdns.com,
+       IF IT DOES NOT EXCEED THE COST */
+    {
+      /* short name: 1 label below the zone apex */
+      DNSName lookupName("sub.powerdns.com.");
+      BOOST_CHECK_EQUAL(lookupName.countLabels() - zone.countLabels(), 1U);
+      BOOST_CHECK_LE(getNSEC3DenialProofWorstCaseIterationsCount(lookupName.countLabels() - zone.countLabels(), iterationsCount, salt.size()), AggressiveNSECCache::s_nsec3DenialProofMaxCost);
+      BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, lookupName, QType::AAAA, RCode::NXDomain, 7U), true);
+    }
+    {
+      /* longer name: 2 labels below the zone apex */
+      DNSName lookupName("a.sub.powerdns.com.");
+      BOOST_CHECK_EQUAL(lookupName.countLabels() - zone.countLabels(), 2U);
+      BOOST_CHECK_GT(getNSEC3DenialProofWorstCaseIterationsCount(lookupName.countLabels() - zone.countLabels(), iterationsCount, salt.size()), AggressiveNSECCache::s_nsec3DenialProofMaxCost);
+      BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, lookupName, QType::AAAA), false);
+    }
   }
 }
 
index ae2c062132927231100d4783bdd5deff25e291fc..5b7f26deb261591a34b87e20b93bbea2d959378c 100644 (file)
@@ -329,9 +329,27 @@ bool addRRSIG(const testkeysset_t& keys, std::vector<DNSRecord>& records, const
     throw std::runtime_error("No DNSKEY found for " + signer.toLogString() + ", unable to compute the requested RRSIG");
   }
 
-  size_t recordsCount = records.size();
-  const DNSName& name = records[recordsCount - 1].d_name;
-  const uint16_t type = records[recordsCount - 1].d_type;
+  DNSName name;
+  uint16_t type{QType::ENT};
+  DNSResourceRecord::Place place{DNSResourceRecord::ANSWER};
+  uint32_t ttl{0};
+  bool found = false;
+
+  /* locate the last non-RRSIG record */
+  for (auto recordIterator = records.rbegin(); recordIterator != records.rend(); ++recordIterator) {
+    if (recordIterator->d_type != QType::RRSIG) {
+      name = recordIterator->d_name;
+      type = recordIterator->d_type;
+      place = recordIterator->d_place;
+      ttl = recordIterator->d_ttl;
+      found = true;
+      break;
+    }
+  }
+
+  if (!found) {
+    throw std::runtime_error("Unable to locate the record that the RRSIG should cover");
+  }
 
   sortedRecords_t recordcontents;
   for (const auto& record : records) {
@@ -341,16 +359,16 @@ bool addRRSIG(const testkeysset_t& keys, std::vector<DNSRecord>& records, const
   }
 
   RRSIGRecordContent rrc;
-  computeRRSIG(it->second.first, signer, wildcard ? *wildcard : records[recordsCount - 1].d_name, records[recordsCount - 1].d_type, records[recordsCount - 1].d_ttl, sigValidity, rrc, recordcontents, algo, boost::none, now);
+  computeRRSIG(it->second.first, signer, wildcard ? *wildcard : name, type, ttl, sigValidity, rrc, recordcontents, algo, boost::none, now);
   if (broken) {
     rrc.d_signature[0] ^= 42;
   }
 
   DNSRecord rec;
   rec.d_type = QType::RRSIG;
-  rec.d_place = records[recordsCount - 1].d_place;
-  rec.d_name = records[recordsCount - 1].d_name;
-  rec.d_ttl = records[recordsCount - 1].d_ttl;
+  rec.d_place = place;
+  rec.d_name = name;
+  rec.d_ttl = ttl;
 
   rec.d_content = std::make_shared<RRSIGRecordContent>(rrc);
   records.push_back(rec);
index 23358d9bc8e7a774474d9957a8eea33f2339bc64..c1f65db0239bf369d98174800d853a359fdc373c 100644 (file)
@@ -439,7 +439,6 @@ BOOST_AUTO_TEST_CASE(test_dnssec_rrsig)
 
   auto dcke = DNSCryptoKeyEngine::make(DNSSECKeeper::ECDSA256);
   dcke->create(dcke->getBits());
-  // cerr<<dcke->convertToISC()<<endl;
   DNSSECPrivateKey dpk;
   dpk.d_flags = 256;
   dpk.setKey(std::move(dcke));
@@ -460,8 +459,9 @@ BOOST_AUTO_TEST_CASE(test_dnssec_rrsig)
   std::vector<std::shared_ptr<RRSIGRecordContent>> sigs;
   sigs.push_back(std::make_shared<RRSIGRecordContent>(rrc));
 
-  BOOST_CHECK(validateWithKeySet(now, qname, recordcontents, sigs, keyset) == vState::Secure);
-}
+  pdns::validation::ValidationContext validationContext;
+  BOOST_CHECK(validateWithKeySet(now, qname, recordcontents, sigs, keyset, validationContext) == vState::Secure);
+  BOOST_CHECK_EQUAL(validationContext.d_validationsCounter, 1U);}
 
 BOOST_AUTO_TEST_CASE(test_dnssec_root_validation_csk)
 {
@@ -837,6 +837,7 @@ BOOST_AUTO_TEST_CASE(test_dnssec_bogus_dnskey_revoked)
   BOOST_REQUIRE_EQUAL(ret.size(), 14U);
   BOOST_CHECK_EQUAL(queriesCount, 2U);
 }
+
 BOOST_AUTO_TEST_CASE(test_dnssec_bogus_dnskey_doesnt_match_ds)
 {
   std::unique_ptr<SyncRes> sr;
@@ -959,6 +960,287 @@ BOOST_AUTO_TEST_CASE(test_dnssec_bogus_dnskey_doesnt_match_ds)
   BOOST_CHECK_EQUAL(queriesCount, 4U);
 }
 
+BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_dss)
+{
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+
+  setDNSSECValidation(sr, DNSSECMode::Process);
+
+  primeHints();
+  const DNSName target(".");
+  testkeysset_t keys;
+
+  g_maxDSsToConsider = 1;
+
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  /* generate more DSs for the zone than we are willing to consider: only the last one will be used to generate DNSKEY records */
+  for (size_t idx = 0; idx < (g_maxDSsToConsider + 10U); idx++) {
+    generateKeyMaterial(g_rootdnsname, DNSSECKeeper::RSASHA512, DNSSECKeeper::DIGEST_SHA384, keys, luaconfsCopy.dsAnchors);
+  }
+  g_luaconfs.setState(luaconfsCopy);
+
+  size_t queriesCount = 0;
+
+  sr->setAsyncCallback([target, &queriesCount, keys](const ComboAddress& /* ip */, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+    queriesCount++;
+
+    if (domain == target && type == QType::NS) {
+
+      setLWResult(res, 0, true, false, true);
+      char addr[] = "a.root-servers.net.";
+      for (char idx = 'a'; idx <= 'm'; idx++) {
+        addr[0] = idx;
+        addRecordToLW(res, domain, QType::NS, std::string(addr), DNSResourceRecord::ANSWER, 3600);
+      }
+
+      addRRSIG(keys, res->d_records, domain, 300);
+
+      addRecordToLW(res, "a.root-servers.net.", QType::A, "198.41.0.4", DNSResourceRecord::ADDITIONAL, 3600);
+      addRecordToLW(res, "a.root-servers.net.", QType::AAAA, "2001:503:ba3e::2:30", DNSResourceRecord::ADDITIONAL, 3600);
+
+      return LWResult::Result::Success;
+    }
+    else if (domain == target && type == QType::DNSKEY) {
+
+      setLWResult(res, 0, true, false, true);
+
+      addDNSKEY(keys, domain, 300, res->d_records);
+      addRRSIG(keys, res->d_records, domain, 300);
+
+      return LWResult::Result::Success;
+    }
+
+    return LWResult::Result::Timeout;
+  });
+
+  /* === with validation enabled === */
+  sr->setDNSSECValidationRequested(true);
+  vector<DNSRecord> ret;
+  int res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidDNSKEY);
+  /* 13 NS + 1 RRSIG */
+  BOOST_REQUIRE_EQUAL(ret.size(), 14U);
+  BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+  /* again, to test the cache */
+  ret.clear();
+  res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidDNSKEY);
+  BOOST_REQUIRE_EQUAL(ret.size(), 14U);
+  BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+  g_maxDNSKEYsToConsider = 0;
+}
+
+BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_dnskeys)
+{
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+
+  setDNSSECValidation(sr, DNSSECMode::Process);
+
+  primeHints();
+  const DNSName target(".");
+  testkeysset_t dskeys;
+  testkeysset_t keys;
+
+  DNSKEYRecordContent dnskeyRecordContent;
+  dnskeyRecordContent.d_algorithm = 13;
+  /* Generate key material for "." */
+  auto dckeDS = DNSCryptoKeyEngine::makeFromISCString(dnskeyRecordContent, R"PKEY(Private-key-format: v1.2
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: Ovj4pzrSh0U6aEVoKaPFhK1D4NMG0xrymj9+6TpwC8o=)PKEY");
+  DNSSECPrivateKey dskey;
+  dskey.setKey(std::move(dckeDS));
+  dskey.d_flags = 257;
+  assert(dskey.getTag() == 31337);
+  DSRecordContent drc = makeDSFromDNSKey(target, dskey.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
+  dskeys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dskey, drc);
+
+  /* Different key, same tag */
+  auto dcke = DNSCryptoKeyEngine::makeFromISCString(dnskeyRecordContent, R"PKEY(Private-key-format: v1.2
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: n7SRA4n6NejhZBWQOhjTaICYSpkTl6plJn1ATFG23FI=)PKEY");
+  DNSSECPrivateKey dpk;
+  dpk.d_flags = 256;
+  dpk.setKey(std::move(dcke));
+  assert(dpk.getTag() == dskey.getTag());
+  DSRecordContent uselessdrc = makeDSFromDNSKey(target, dpk.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
+  keys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dpk, uselessdrc);
+
+  /* Set the root DS (one of them!) */
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  luaconfsCopy.dsAnchors[g_rootdnsname].insert(drc);
+  g_luaconfs.setState(luaconfsCopy);
+
+  size_t queriesCount = 0;
+
+  sr->setAsyncCallback([target, &queriesCount, keys, dskeys](const ComboAddress& /* ip */, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+    queriesCount++;
+
+    if (domain == target && type == QType::NS) {
+
+      setLWResult(res, 0, true, false, true);
+      char addr[] = "a.root-servers.net.";
+      for (char idx = 'a'; idx <= 'm'; idx++) {
+        addr[0] = idx;
+        addRecordToLW(res, domain, QType::NS, std::string(addr), DNSResourceRecord::ANSWER, 3600);
+      }
+
+      addRRSIG(dskeys, res->d_records, domain, 300);
+
+      addRecordToLW(res, "a.root-servers.net.", QType::A, "198.41.0.4", DNSResourceRecord::ADDITIONAL, 3600);
+      addRecordToLW(res, "a.root-servers.net.", QType::AAAA, "2001:503:ba3e::2:30", DNSResourceRecord::ADDITIONAL, 3600);
+
+      return LWResult::Result::Success;
+    }
+    else if (domain == target && type == QType::DNSKEY) {
+
+      setLWResult(res, 0, true, false, true);
+
+      addDNSKEY(keys, domain, 300, res->d_records);
+      addDNSKEY(dskeys, domain, 300, res->d_records);
+      addRRSIG(keys, res->d_records, domain, 300);
+      addRRSIG(dskeys, res->d_records, domain, 300);
+
+      return LWResult::Result::Success;
+    }
+
+    return LWResult::Result::Timeout;
+  });
+
+  g_maxDNSKEYsToConsider = 1;
+
+  /* === with validation enabled === */
+  sr->setDNSSECValidationRequested(true);
+  vector<DNSRecord> ret;
+  int res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidDNSKEY);
+  /* 13 NS + 1 RRSIG */
+  BOOST_REQUIRE_EQUAL(ret.size(), 14U);
+  BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+  /* again, to test the cache */
+  ret.clear();
+  res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidDNSKEY);
+  BOOST_REQUIRE_EQUAL(ret.size(), 14U);
+  BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+  g_maxDNSKEYsToConsider = 0;
+}
+
+BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_dnskeys_while_checking_signature)
+{
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+
+  setDNSSECValidation(sr, DNSSECMode::Process);
+
+  primeHints();
+  const DNSName target(".");
+  testkeysset_t dskeys;
+  testkeysset_t keys;
+  testkeysset_t otherkeys;
+
+  DNSKEYRecordContent dnskeyRecordContent;
+  dnskeyRecordContent.d_algorithm = 13;
+  /* Generate key material for "." */
+  auto dckeDS = DNSCryptoKeyEngine::makeFromISCString(dnskeyRecordContent, R"PKEY(Private-key-format: v1.2
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: Ovj4pzrSh0U6aEVoKaPFhK1D4NMG0xrymj9+6TpwC8o=)PKEY");
+  DNSSECPrivateKey dskey;
+  dskey.setKey(std::move(dckeDS));
+  dskey.d_flags = 257;
+  assert(dskey.getTag() == 31337);
+  DSRecordContent drc = makeDSFromDNSKey(target, dskey.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
+  dskeys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dskey, drc);
+
+  /* Different key, same tag */
+  auto dcke = DNSCryptoKeyEngine::makeFromISCString(dnskeyRecordContent, R"PKEY(Private-key-format: v1.2
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: pTaMJcvNrPIIiQiHGvCLZZASroyQpUwew5FvCgjHNsk=)PKEY");
+  DNSSECPrivateKey dpk;
+  // why 258, you may ask? We need this record to be sorted AFTER the other one in a sortedRecords_t
+  // so that the validation of the DNSKEY rrset succeeds
+  dpk.setKey(std::move(dcke));
+  dpk.d_flags = 258;
+  assert(dpk.getTag() == dskey.getTag());
+  DSRecordContent uselessdrc = makeDSFromDNSKey(target, dpk.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
+  keys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dpk, uselessdrc);
+
+  /* Set the root DSs (only one of them) */
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  luaconfsCopy.dsAnchors[g_rootdnsname].insert(drc);
+  g_luaconfs.setState(luaconfsCopy);
+
+  size_t queriesCount = 0;
+
+  sr->setAsyncCallback([target, &queriesCount, keys, dskeys](const ComboAddress& /* ip */, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+    queriesCount++;
+
+    if (domain == target && type == QType::NS) {
+
+      setLWResult(res, 0, true, false, true);
+      char addr[] = "a.root-servers.net.";
+      for (char idx = 'a'; idx <= 'm'; idx++) {
+        addr[0] = idx;
+        addRecordToLW(res, domain, QType::NS, std::string(addr), DNSResourceRecord::ANSWER, 3600);
+      }
+
+      addRRSIG(keys, res->d_records, domain, 300);
+      addRRSIG(dskeys, res->d_records, domain, 300);
+
+      addRecordToLW(res, "a.root-servers.net.", QType::A, "198.41.0.4", DNSResourceRecord::ADDITIONAL, 3600);
+      addRecordToLW(res, "a.root-servers.net.", QType::AAAA, "2001:503:ba3e::2:30", DNSResourceRecord::ADDITIONAL, 3600);
+
+      return LWResult::Result::Success;
+    }
+    else if (domain == target && type == QType::DNSKEY) {
+
+      setLWResult(res, 0, true, false, true);
+
+      addDNSKEY(dskeys, domain, 300, res->d_records);
+      addDNSKEY(keys, domain, 300, res->d_records);
+      addRRSIG(dskeys, res->d_records, domain, 300);
+
+      return LWResult::Result::Success;
+    }
+
+    return LWResult::Result::Timeout;
+  });
+
+  g_maxDNSKEYsToConsider = 1;
+
+  /* === with validation enabled === */
+  sr->setDNSSECValidationRequested(true);
+  vector<DNSRecord> ret;
+  int res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidRRSIG);
+  /* 13 NS + 1 RRSIG */
+  BOOST_REQUIRE_EQUAL(ret.size(), 15U);
+  BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+  /* again, to test the cache */
+  ret.clear();
+  res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidRRSIG);
+  BOOST_REQUIRE_EQUAL(ret.size(), 15U);
+  BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+  g_maxDNSKEYsToConsider = 0;
+}
+
 BOOST_AUTO_TEST_CASE(test_dnssec_bogus_rrsig_signed_with_unknown_dnskey)
 {
   std::unique_ptr<SyncRes> sr;
@@ -1423,6 +1705,148 @@ BOOST_AUTO_TEST_CASE(test_dnssec_bogus_bad_sig)
   BOOST_CHECK_EQUAL(queriesCount, 2U);
 }
 
+BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_sigs)
+{
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+
+  setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+  primeHints();
+  const DNSName target(".");
+  testkeysset_t keys;
+
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  generateKeyMaterial(g_rootdnsname, DNSSECKeeper::RSASHA512, DNSSECKeeper::DIGEST_SHA384, keys, luaconfsCopy.dsAnchors);
+
+  g_luaconfs.setState(luaconfsCopy);
+  /* make sure that the signature inception and validity times are computed
+     based on the SyncRes time, not the current one, in case the function
+     takes too long. */
+  const time_t fixedNow = sr->getNow().tv_sec;
+
+  size_t queriesCount = 0;
+
+  sr->setAsyncCallback([target, &queriesCount, keys, fixedNow](const ComboAddress& /* ip */, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+    queriesCount++;
+
+    if (domain == target && type == QType::NS) {
+
+      setLWResult(res, 0, true, false, true);
+      char addr[] = "a.root-servers.net.";
+      for (char idx = 'a'; idx <= 'm'; idx++) {
+        addr[0] = idx;
+        addRecordToLW(res, domain, QType::NS, std::string(addr), DNSResourceRecord::ANSWER, 3600);
+      }
+
+      addRRSIG(keys, res->d_records, domain, 300, true, boost::none, boost::none, fixedNow);
+      addRRSIG(keys, res->d_records, domain, 300, true, boost::none, boost::none, fixedNow);
+      addRRSIG(keys, res->d_records, domain, 300, false, boost::none, boost::none, fixedNow);
+
+      addRecordToLW(res, "a.root-servers.net.", QType::A, "198.41.0.4", DNSResourceRecord::ADDITIONAL, 3600);
+      addRecordToLW(res, "a.root-servers.net.", QType::AAAA, "2001:503:ba3e::2:30", DNSResourceRecord::ADDITIONAL, 3600);
+
+      return LWResult::Result::Success;
+    }
+    else if (domain == target && type == QType::DNSKEY) {
+
+      setLWResult(res, 0, true, false, true);
+
+      addDNSKEY(keys, domain, 300, res->d_records);
+      addRRSIG(keys, res->d_records, domain, 300, false, boost::none, boost::none, fixedNow);
+
+      return LWResult::Result::Success;
+    }
+
+    return LWResult::Result::Timeout;
+  });
+
+  g_maxRRSIGsPerRecordToConsider = 2;
+
+  vector<DNSRecord> ret;
+  int res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidRRSIG);
+  /* 13 NS + 1 RRSIG */
+  BOOST_REQUIRE_EQUAL(ret.size(), 16U);
+  BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+  /* again, to test the cache */
+  ret.clear();
+  res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidRRSIG);
+  BOOST_REQUIRE_EQUAL(ret.size(), 16U);
+  BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+  g_maxRRSIGsPerRecordToConsider = 0;
+}
+
+BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_sig_validations)
+{
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+
+  setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+  primeHints();
+  const DNSName target(".");
+  testkeysset_t keys;
+
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  generateKeyMaterial(g_rootdnsname, DNSSECKeeper::RSASHA512, DNSSECKeeper::DIGEST_SHA384, keys, luaconfsCopy.dsAnchors);
+
+  g_luaconfs.setState(luaconfsCopy);
+  /* make sure that the signature inception and validity times are computed
+     based on the SyncRes time, not the current one, in case the function
+     takes too long. */
+  const time_t fixedNow = sr->getNow().tv_sec;
+
+  size_t queriesCount = 0;
+
+  sr->setAsyncCallback([target, &queriesCount, keys, fixedNow](const ComboAddress& /* ip */, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+    queriesCount++;
+
+    if (domain == target && type == QType::NS) {
+
+      setLWResult(res, 0, true, false, true);
+      char addr[] = "a.root-servers.net.";
+      for (char idx = 'a'; idx <= 'm'; idx++) {
+        addr[0] = idx;
+        addRecordToLW(res, domain, QType::NS, std::string(addr), DNSResourceRecord::ANSWER, 3600);
+      }
+
+      addRRSIG(keys, res->d_records, domain, 300, true, boost::none, boost::none, fixedNow);
+      addRRSIG(keys, res->d_records, domain, 300, false, boost::none, boost::none, fixedNow);
+
+      addRecordToLW(res, "a.root-servers.net.", QType::A, "198.41.0.4", DNSResourceRecord::ADDITIONAL, 3600);
+      addRecordToLW(res, "a.root-servers.net.", QType::AAAA, "2001:503:ba3e::2:30", DNSResourceRecord::ADDITIONAL, 3600);
+
+      return LWResult::Result::Success;
+    }
+    else if (domain == target && type == QType::DNSKEY) {
+
+      setLWResult(res, 0, true, false, true);
+
+      addDNSKEY(keys, domain, 300, res->d_records);
+      addRRSIG(keys, res->d_records, domain, 300, false, boost::none, boost::none, fixedNow);
+
+      return LWResult::Result::Success;
+    }
+
+    return LWResult::Result::Timeout;
+  });
+
+  SyncRes::s_maxvalidationsperq = 1U;
+
+  vector<DNSRecord> ret;
+  BOOST_REQUIRE_THROW(sr->beginResolve(target, QType(QType::NS), QClass::IN, ret), ImmediateServFailException);
+
+  SyncRes::s_maxvalidationsperq = 0U;
+}
+
 BOOST_AUTO_TEST_CASE(test_dnssec_bogus_bad_algo)
 {
   std::unique_ptr<SyncRes> sr;
index 1af1d8719665d6a79168973a13f7d4fa586d7199..306f4ac74991193cf355fb85c5bf00f4f3df495c 100644 (file)
@@ -1240,6 +1240,177 @@ BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec3_nodata_nowildcard)
   BOOST_CHECK_EQUAL(queriesCount, 4U);
 }
 
+BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec3_too_many_nsec3s)
+{
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+
+  setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+  primeHints();
+  const DNSName target("www.com.");
+  testkeysset_t keys;
+
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys, luaconfsCopy.dsAnchors);
+  generateKeyMaterial(DNSName("com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys);
+
+  g_luaconfs.setState(luaconfsCopy);
+
+  size_t queriesCount = 0;
+
+  sr->setAsyncCallback([target, &queriesCount, keys](const ComboAddress& ip, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+    queriesCount++;
+
+    if (type == QType::DS || type == QType::DNSKEY) {
+      if (type == QType::DS && domain == target) {
+        DNSName auth("com.");
+        setLWResult(res, 0, true, false, true);
+
+        addRecordToLW(res, auth, QType::SOA, "foo. bar. 2017032800 1800 900 604800 86400", DNSResourceRecord::AUTHORITY, 86400);
+        addRRSIG(keys, res->d_records, auth, 300);
+        /* add a NSEC3 denying the DS AND the existence of a cut (no NS) */
+        /* first the closest encloser */
+        addNSEC3UnhashedRecordToLW(DNSName("com."), auth, "whatever", {QType::A, QType::TXT, QType::RRSIG, QType::NSEC}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, auth, 300);
+        /* then the next closer */
+        addNSEC3NarrowRecordToLW(domain, DNSName("com."), {QType::RRSIG, QType::NSEC}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, auth, 300);
+        /* a wildcard matches but has no record for this type */
+        addNSEC3UnhashedRecordToLW(DNSName("*.com."), DNSName("com."), "whatever", {QType::AAAA, QType::NSEC, QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("com"), 300, false, boost::none, DNSName("*.com"));
+        return LWResult::Result::Success;
+      }
+      return genericDSAndDNSKEYHandler(res, domain, domain, type, keys);
+    }
+    else {
+      if (isRootServer(ip)) {
+        setLWResult(res, 0, false, false, true);
+        addRecordToLW(res, "com.", QType::NS, "a.gtld-servers.com.", DNSResourceRecord::AUTHORITY, 3600);
+        addDS(DNSName("com."), 300, res->d_records, keys);
+        addRRSIG(keys, res->d_records, DNSName("."), 300);
+        addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600);
+        return LWResult::Result::Success;
+      }
+      else if (ip == ComboAddress("192.0.2.1:53")) {
+        setLWResult(res, 0, true, false, true);
+        /* no data */
+        addRecordToLW(res, DNSName("com."), QType::SOA, "com. com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+        addRRSIG(keys, res->d_records, DNSName("com."), 300);
+        /* no record for this name */
+        /* first the closest encloser */
+        addNSEC3UnhashedRecordToLW(DNSName("com."), DNSName("com."), "whatever", {QType::A, QType::TXT, QType::RRSIG, QType::NSEC}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("com."), 300);
+        /* then the next closer */
+        addNSEC3NarrowRecordToLW(domain, DNSName("com."), {QType::RRSIG, QType::NSEC}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("com."), 300);
+        /* a wildcard matches but has no record for this type */
+        addNSEC3UnhashedRecordToLW(DNSName("*.com."), DNSName("com."), "whatever", {QType::AAAA, QType::NSEC, QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("com"), 300, false, boost::none, DNSName("*.com"));
+        return LWResult::Result::Success;
+      }
+    }
+
+    return LWResult::Result::Timeout;
+  });
+
+  /* we allow at most 2 NSEC3s, but we need at least 3 of them to
+     get a valid denial so we will go Bogus */
+  g_maxNSEC3sPerRecordToConsider = 2;
+
+  vector<DNSRecord> ret;
+  int res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusInvalidDenial);
+  BOOST_REQUIRE_EQUAL(ret.size(), 8U);
+  BOOST_CHECK_EQUAL(queriesCount, 5U);
+
+  g_maxNSEC3sPerRecordToConsider = 0;
+}
+
+BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec3_too_many_nsec3s_per_query)
+{
+  SyncRes::s_maxnsec3iterationsperq = 20;
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+
+  setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+  primeHints();
+  const DNSName target("www.com.");
+  testkeysset_t keys;
+
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys, luaconfsCopy.dsAnchors);
+  generateKeyMaterial(DNSName("com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys);
+
+  g_luaconfs.setState(luaconfsCopy);
+
+  size_t queriesCount = 0;
+
+  sr->setAsyncCallback([target, &queriesCount, keys](const ComboAddress& ip, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+    queriesCount++;
+
+    if (type == QType::DS || type == QType::DNSKEY) {
+      if (type == QType::DS && domain == target) {
+        DNSName auth("com.");
+        setLWResult(res, 0, true, false, true);
+
+        addRecordToLW(res, auth, QType::SOA, "foo. bar. 2017032800 1800 900 604800 86400", DNSResourceRecord::AUTHORITY, 86400);
+        addRRSIG(keys, res->d_records, auth, 300);
+        /* add a NSEC3 denying the DS AND the existence of a cut (no NS) */
+        /* first the closest encloser */
+        addNSEC3UnhashedRecordToLW(DNSName("com."), auth, "whatever", {QType::A, QType::TXT, QType::RRSIG, QType::NSEC}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, auth, 300);
+        /* then the next closer */
+        addNSEC3NarrowRecordToLW(domain, DNSName("com."), {QType::RRSIG, QType::NSEC}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, auth, 300);
+        /* a wildcard matches but has no record for this type */
+        addNSEC3UnhashedRecordToLW(DNSName("*.com."), DNSName("com."), "whatever", {QType::AAAA, QType::NSEC, QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("com"), 300, false, boost::none, DNSName("*.com"));
+        return LWResult::Result::Success;
+      }
+      return genericDSAndDNSKEYHandler(res, domain, domain, type, keys);
+    }
+    else {
+      if (isRootServer(ip)) {
+        setLWResult(res, 0, false, false, true);
+        addRecordToLW(res, "com.", QType::NS, "a.gtld-servers.com.", DNSResourceRecord::AUTHORITY, 3600);
+        addDS(DNSName("com."), 300, res->d_records, keys);
+        addRRSIG(keys, res->d_records, DNSName("."), 300);
+        addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600);
+        return LWResult::Result::Success;
+      }
+      else if (ip == ComboAddress("192.0.2.1:53")) {
+        setLWResult(res, 0, true, false, true);
+        /* no data */
+        addRecordToLW(res, DNSName("com."), QType::SOA, "com. com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+        addRRSIG(keys, res->d_records, DNSName("com."), 300);
+        /* no record for this name */
+        /* first the closest encloser */
+        addNSEC3UnhashedRecordToLW(DNSName("com."), DNSName("com."), "whatever", {QType::A, QType::TXT, QType::RRSIG, QType::NSEC}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("com."), 300);
+        /* then the next closer */
+        addNSEC3NarrowRecordToLW(domain, DNSName("com."), {QType::RRSIG, QType::NSEC}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("com."), 300);
+        /* a wildcard matches but has no record for this type */
+        addNSEC3UnhashedRecordToLW(DNSName("*.com."), DNSName("com."), "whatever", {QType::AAAA, QType::NSEC, QType::RRSIG}, 600, res->d_records);
+        addRRSIG(keys, res->d_records, DNSName("com"), 300, false, boost::none, DNSName("*.com"));
+        return LWResult::Result::Success;
+      }
+    }
+
+    return LWResult::Result::Timeout;
+  });
+
+  vector<DNSRecord> ret;
+  BOOST_CHECK_THROW(sr->beginResolve(target, QType(QType::A), QClass::IN, ret), pdns::validation::TooManySEC3IterationsException);
+
+  SyncRes::s_maxnsec3iterationsperq = 0;
+}
+
 BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec3_nodata_nowildcard_duplicated_nsec3)
 {
   std::unique_ptr<SyncRes> sr;
index a9bdd1a9396d05df02665b857af497ba4ee85a51..41df583cc653975bd79b6a351c43ac7436b2316f 100644 (file)
@@ -5,6 +5,13 @@
 
 BOOST_AUTO_TEST_SUITE(syncres_cc8)
 
+static dState getDenial(const cspmap_t& validrrsets, const DNSName& qname, uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, bool needWildcardProof = true, unsigned int wildcardLabelsCount = 0)
+{
+  pdns::validation::ValidationContext context;
+  context.d_nsec3IterationsRemainingQuota = std::numeric_limits<decltype(context.d_nsec3IterationsRemainingQuota)>::max();
+  return getDenial(validrrsets, qname, qtype, referralToUnsigned, wantsNoDataProof, context, needWildcardProof, wildcardLabelsCount);
+}
+
 BOOST_AUTO_TEST_CASE(test_nsec_denial_nowrap)
 {
   initSR();
@@ -413,7 +420,8 @@ BOOST_AUTO_TEST_CASE(test_nsec3_nxqtype_ds)
   sortedRecords_t recordContents;
   vector<shared_ptr<RRSIGRecordContent>> signatureContents;
 
-  addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::A}, 600, records);
+  const unsigned int nbIterations = 10;
+  addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::A}, 600, records, nbIterations);
   recordContents.insert(records.at(0).d_content);
   addRRSIG(keys, records, DNSName("powerdns.com."), 300);
   signatureContents.push_back(getRR<RRSIGRecordContent>(records.at(1)));
@@ -425,10 +433,15 @@ BOOST_AUTO_TEST_CASE(test_nsec3_nxqtype_ds)
   denialMap[std::pair(records.at(0).d_name, records.at(0).d_type)] = pair;
   records.clear();
 
+  pdns::validation::ValidationContext validationContext;
+  validationContext.d_nsec3IterationsRemainingQuota = 100U;
   /* this NSEC3 is not valid to deny the DS since it is from the child zone */
-  BOOST_CHECK_EQUAL(getDenial(denialMap, DNSName("powerdns.com."), QType::DS, false, true), dState::NODENIAL);
+  BOOST_CHECK_EQUAL(getDenial(denialMap, DNSName("powerdns.com."), QType::DS, false, true, validationContext), dState::NODENIAL);
+  /* the NSEC3 hash is not computed since we it is from the child zone */
+  BOOST_CHECK_EQUAL(validationContext.d_nsec3IterationsRemainingQuota, 100U);
   /* AAAA should be fine, though */
-  BOOST_CHECK_EQUAL(getDenial(denialMap, DNSName("powerdns.com."), QType::AAAA, false, true), dState::NXQTYPE);
+  BOOST_CHECK_EQUAL(getDenial(denialMap, DNSName("powerdns.com."), QType::AAAA, false, true, validationContext), dState::NXQTYPE);
+  BOOST_CHECK_EQUAL(validationContext.d_nsec3IterationsRemainingQuota, (100U - nbIterations));
 }
 
 BOOST_AUTO_TEST_CASE(test_nsec3_nxqtype_cname)
index 3ab05d9f6957ab310c67e72d0ea451bb4b702b98..bbdba357c329cbb63d2b13e72bf77dcda79fffc2 100644 (file)
@@ -427,6 +427,8 @@ unsigned int SyncRes::s_serverdownthrottletime;
 unsigned int SyncRes::s_nonresolvingnsmaxfails;
 unsigned int SyncRes::s_nonresolvingnsthrottletime;
 unsigned int SyncRes::s_ecscachelimitttl;
+unsigned int SyncRes::s_maxvalidationsperq;
+unsigned int SyncRes::s_maxnsec3iterationsperq;
 pdns::stat_t SyncRes::s_authzonequeries;
 pdns::stat_t SyncRes::s_queries;
 pdns::stat_t SyncRes::s_outgoingtimeouts;
@@ -498,8 +500,8 @@ static inline void accountAuthLatency(uint64_t usec, int family)
 SyncRes::SyncRes(const struct timeval& now) :  d_authzonequeries(0), d_outqueries(0), d_tcpoutqueries(0), d_dotoutqueries(0), d_throttledqueries(0), d_timeouts(0), d_unreachables(0),
                                               d_totUsec(0), d_now(now),
                                               d_cacheonly(false), d_doDNSSEC(false), d_doEDNS0(false), d_qNameMinimization(s_qnameminimization), d_lm(s_lm)
-
 {
+  d_validationContext.d_nsec3IterationsRemainingQuota = s_maxnsec3iterationsperq > 0 ? s_maxnsec3iterationsperq : std::numeric_limits<decltype(d_validationContext.d_nsec3IterationsRemainingQuota)>::max();
 }
 
 static void allowAdditionalEntry(std::unordered_set<DNSName>& allowedAdditionals, const DNSRecord& rec);
@@ -2973,7 +2975,7 @@ bool SyncRes::doCacheCheck(const DNSName &qname, const DNSName& authname, bool w
 
   /* let's check if we have a NSEC covering that record */
   if (g_aggressiveNSECCache && !wasForwardedOrAuthZone) {
-    if (g_aggressiveNSECCache->getDenial(d_now.tv_sec, qname, qtype, ret, res, d_cacheRemote, d_routingTag, d_doDNSSEC)) {
+    if (g_aggressiveNSECCache->getDenial(d_now.tv_sec, qname, qtype, ret, res, d_cacheRemote, d_routingTag, d_doDNSSEC, d_validationContext)) {
       state = vState::Secure;
       return true;
     }
@@ -3635,7 +3637,7 @@ vState SyncRes::getDSRecords(const DNSName& zone, dsmap_t& ds, bool taOnly, unsi
            - a delegation to a non-DNSSEC signed zone
            - no delegation, we stay in the same zone
         */
-        if (gotCNAME || denialProvesNoDelegation(zone, dsrecords)) {
+        if (gotCNAME || denialProvesNoDelegation(zone, dsrecords, d_validationContext)) {
           /* we are still inside the same zone */
 
           if (foundCut) {
@@ -3807,7 +3809,10 @@ vState SyncRes::validateDNSKeys(const DNSName& zone, const std::vector<DNSRecord
 
   LOG(d_prefix<<": trying to validate "<<std::to_string(tentativeKeys.size())<<" DNSKEYs with "<<std::to_string(ds.size())<<" DS"<<endl);
   skeyset_t validatedKeys;
-  auto state = validateDNSKeysAgainstDS(d_now.tv_sec, zone, ds, tentativeKeys, toSign, signatures, validatedKeys);
+  auto state = validateDNSKeysAgainstDS(d_now.tv_sec, zone, ds, tentativeKeys, toSign, signatures, validatedKeys, d_validationContext);
+  if (s_maxvalidationsperq != 0 && d_validationContext.d_validationsCounter > s_maxvalidationsperq) {
+   throw ImmediateServFailException("Server Failure while validating DNSKEYs, too many signature validations for this query");
+ }
 
   LOG(d_prefix<<": we now have "<<std::to_string(validatedKeys.size())<<" DNSKEYs"<<endl);
 
@@ -3972,7 +3977,11 @@ vState SyncRes::validateRecordsWithSigs(unsigned int depth, const DNSName& qname
   }
 
   LOG(d_prefix<<"Going to validate "<<recordcontents.size()<< " record contents with "<<signatures.size()<<" sigs and "<<keys.size()<<" keys for "<<name<<"|"<<type.toString()<<endl);
-  vState state = validateWithKeySet(d_now.tv_sec, name, recordcontents, signatures, keys, false);
+  vState state = validateWithKeySet(d_now.tv_sec, name, recordcontents, signatures, keys, d_validationContext, false);
+  if (s_maxvalidationsperq != 0 && d_validationContext.d_validationsCounter > s_maxvalidationsperq) {
+    throw ImmediateServFailException("Server Failure while validating records, too many signature validations for this query");
+  }
+
   if (state == vState::Secure) {
     LOG(d_prefix<<"Secure!"<<endl);
     return vState::Secure;
@@ -4683,7 +4692,7 @@ void SyncRes::updateDenialValidationState(vState& neValidationState, const DNSNa
 dState SyncRes::getDenialValidationState(const NegCache::NegCacheEntry& ne, const dState expectedState, bool referralToUnsigned)
 {
   cspmap_t csp = harvestCSPFromNE(ne);
-  return getDenial(csp, ne.d_name, ne.d_qtype.getCode(), referralToUnsigned, expectedState == dState::NXQTYPE);
+  return getDenial(csp, ne.d_name, ne.d_qtype.getCode(), referralToUnsigned, expectedState == dState::NXQTYPE, d_validationContext);
 }
 
 bool SyncRes::processRecords(const std::string& prefix, const DNSName& qname, const QType qtype, const DNSName& auth, LWResult& lwr, const bool sendRDQuery, vector<DNSRecord>& ret, set<DNSName>& nsset, DNSName& newtarget, DNSName& newauth, bool& realreferral, bool& negindic, vState& state, const bool needWildcardProof, const bool gatherWildcardProof, const unsigned int wildcardLabelsCount, int& rcode, bool& negIndicHasSignatures, unsigned int depth)
@@ -4854,7 +4863,7 @@ bool SyncRes::processRecords(const std::string& prefix, const DNSName& qname, co
                as described in section 5.3.4 of RFC 4035 and 5.3 of RFC 7129.
             */
             cspmap_t csp = harvestCSPFromNE(ne);
-            dState res = getDenial(csp, qname, ne.d_qtype.getCode(), false, false, false, wildcardLabelsCount);
+            dState res = getDenial(csp, qname, ne.d_qtype.getCode(), false, false, d_validationContext, false, wildcardLabelsCount);
             if (res != dState::NXDOMAIN) {
               vState st = vState::BogusInvalidDenial;
               if (res == dState::INSECURE || res == dState::OPTOUT) {
index 1bdf1508b77b4a4495e7b569a6f332e61fd92544..0006c6c67c73a06d5b0a1fcd2b760c92a53f211b 100644 (file)
@@ -504,8 +504,9 @@ public:
   static unsigned int s_serverdownthrottletime;
   static unsigned int s_nonresolvingnsmaxfails;
   static unsigned int s_nonresolvingnsthrottletime;
-
   static unsigned int s_ecscachelimitttl;
+  static unsigned int s_maxvalidationsperq;
+  static unsigned int s_maxnsec3iterationsperq;
   static uint8_t s_ecsipv4limit;
   static uint8_t s_ecsipv6limit;
   static uint8_t s_ecsipv4cachelimit;
@@ -655,6 +656,7 @@ private:
   std::shared_ptr<std::vector<std::unique_ptr<RemoteLogger>>> d_outgoingProtobufServers;
   std::shared_ptr<std::vector<std::unique_ptr<FrameStreamLogger>>> d_frameStreamServers;
   boost::optional<const boost::uuids::uuid&> d_initialRequestId;
+  pdns::validation::ValidationContext d_validationContext;
   asyncresolve_t d_asyncResolve{nullptr};
   struct timeval d_now;
   /* if the client is asking for a DS that does not exist, we need to provide the SOA along with the NSEC(3) proof
index 2dfd0ae94cb627647003a41c25bc98a98c1d29b9..12a2034efca851fe94e78d18ece169f7e32223aa 100644 (file)
@@ -1,3 +1,25 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
 #include "validate.hh"
 #include "misc.hh"
 #include "dnssecinfra.hh"
@@ -11,6 +33,11 @@ uint16_t g_maxNSEC3Iterations{0};
 
 #define LOG(x) if(g_dnssecLOG) { g_log <<Logger::Warning << x; }
 
+uint16_t g_maxRRSIGsPerRecordToConsider{0};
+uint16_t g_maxNSEC3sPerRecordToConsider{0};
+uint16_t g_maxDNSKEYsToConsider{0};
+uint16_t g_maxDSsToConsider{0};
+
 static bool isAZoneKey(const DNSKEYRecordContent& key)
 {
   /* rfc4034 Section 2.1.1:
@@ -55,20 +82,20 @@ static vector<shared_ptr<DNSKEYRecordContent > > getByTag(const skeyset_t& keys,
   return ret;
 }
 
-bool isCoveredByNSEC3Hash(const std::string& h, const std::string& beginHash, const std::string& nextHash)
+bool isCoveredByNSEC3Hash(const std::string& hash, const std::string& beginHash, const std::string& nextHash)
 {
-  return ((beginHash < h && h < nextHash) ||          // no wrap          BEGINNING --- HASH -- END
-          (nextHash > h  && beginHash > nextHash) ||  // wrap             HASH --- END --- BEGINNING
-          (nextHash < beginHash  && beginHash < h) || // wrap other case  END --- BEGINNING --- HASH
-          (beginHash == nextHash && h != beginHash));   // "we have only 1 NSEC3 record, LOL!"
+  return ((beginHash < hash && hash < nextHash) ||          // no wrap          BEGINNING --- HASH -- END
+          (nextHash > hash  && beginHash > nextHash) ||  // wrap             HASH --- END --- BEGINNING
+          (nextHash < beginHash  && beginHash < hash) || // wrap other case  END --- BEGINNING --- HASH
+          (beginHash == nextHash && hash != beginHash));   // "we have only 1 NSEC3 record, LOL!"
 }
 
-bool isCoveredByNSEC3Hash(const DNSName& h, const DNSName& beginHash, const DNSName& nextHash)
+bool isCoveredByNSEC3Hash(const DNSName& name, const DNSName& beginHash, const DNSName& nextHash)
 {
-  return ((beginHash.canonCompare(h) && h.canonCompare(nextHash)) ||          // no wrap          BEGINNING --- HASH -- END
-          (h.canonCompare(nextHash) && nextHash.canonCompare(beginHash)) ||  // wrap             HASH --- END --- BEGINNING
-          (nextHash.canonCompare(beginHash) && beginHash.canonCompare(h)) || // wrap other case  END --- BEGINNING --- HASH
-          (beginHash == nextHash && h != beginHash));   // "we have only 1 NSEC3 record, LOL!"
+  return ((beginHash.canonCompare(name) && name.canonCompare(nextHash)) ||          // no wrap          BEGINNING --- HASH -- END
+          (name.canonCompare(nextHash) && nextHash.canonCompare(beginHash)) ||  // wrap             HASH --- END --- BEGINNING
+          (nextHash.canonCompare(beginHash) && beginHash.canonCompare(name)) || // wrap other case  END --- BEGINNING --- HASH
+          (beginHash == nextHash && name != beginHash));   // "we have only 1 NSEC3 record, LOL!"
 }
 
 bool isCoveredByNSEC(const DNSName& name, const DNSName& begin, const DNSName& next)
@@ -88,38 +115,49 @@ static bool nsecProvesENT(const DNSName& name, const DNSName& begin, const DNSNa
   return begin.canonCompare(name) && next != name && next.isPartOf(name);
 }
 
-using nsec3HashesCache = std::map<std::tuple<DNSName, std::string, uint16_t>, std::string>;
-
-static std::string getHashFromNSEC3(const DNSName& qname, const std::shared_ptr<NSEC3RecordContent>& nsec3, nsec3HashesCache& cache)
+[[nodiscard]] std::string getHashFromNSEC3(const DNSName& qname, uint16_t iterations, const std::string& salt, pdns::validation::ValidationContext& context)
 {
   std::string result;
 
-  if (g_maxNSEC3Iterations && nsec3->d_iterations > g_maxNSEC3Iterations) {
+  if (g_maxNSEC3Iterations != 0 && iterations > g_maxNSEC3Iterations) {
     return result;
   }
 
-  auto key = std::make_tuple(qname, nsec3->d_salt, nsec3->d_iterations);
-  auto it = cache.find(key);
-  if (it != cache.end())
+  auto key = std::tuple(qname, salt, iterations);
+  auto iter = context.d_nsec3Cache.find(key);
+  if (iter != context.d_nsec3Cache.end())
   {
-    return it->second;
+    return iter->second;
   }
 
-  result = hashQNameWithSalt(nsec3->d_salt, nsec3->d_iterations, qname);
-  cache[key] = result;
+  if (context.d_nsec3IterationsRemainingQuota < iterations) {
+    // we throw here because we cannot take the risk that the result
+    // be cached, since a different query can try to validate the
+    // same result with a bigger NSEC3 iterations quota
+    throw pdns::validation::TooManySEC3IterationsException();
+  }
+
+  result = hashQNameWithSalt(salt, iterations, qname);
+  context.d_nsec3IterationsRemainingQuota -= iterations;
+  context.d_nsec3Cache[key] = result;
   return result;
 }
 
+[[nodiscard]] static std::string getHashFromNSEC3(const DNSName& qname, const NSEC3RecordContent& nsec3, pdns::validation::ValidationContext& context)
+{
+  return getHashFromNSEC3(qname, nsec3.d_iterations, nsec3.d_salt, context);
+}
+
 /* There is no delegation at this exact point if:
    - the name exists but the NS type is not set
    - the name does not exist
    One exception, if the name is covered by an opt-out NSEC3
    it doesn't prove that an insecure delegation doesn't exist.
 */
-bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>& dsrecords)
+bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>& dsrecords, pdns::validation::ValidationContext& context)
 {
-  nsec3HashesCache cache;
-
+  uint16_t nsec3sConsidered = 0;
   for (const auto& record : dsrecords) {
     if (record.d_type == QType::NSEC) {
       const auto nsec = getRR<NSECRecordContent>(record);
@@ -141,17 +179,22 @@ bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>&
         continue;
       }
 
-      const string h = getHashFromNSEC3(zone, nsec3, cache);
-      if (h.empty()) {
+      if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+        return false;
+      }
+      nsec3sConsidered++;
+
+      const string hash = getHashFromNSEC3(zone, *nsec3, context);
+      if (hash.empty()) {
         return false;
       }
 
       const string beginHash = fromBase32Hex(record.d_name.getRawLabels()[0]);
-      if (beginHash == h) {
+      if (beginHash == hash) {
         return !nsec3->isSet(QType::NS);
       }
 
-      if (isCoveredByNSEC3Hash(h, beginHash, nsec3->d_nexthash)) {
+      if (isCoveredByNSEC3Hash(hash, beginHash, nsec3->d_nexthash)) {
         return !(nsec3->isOptOut());
       }
     }
@@ -167,11 +210,7 @@ bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>&
 */
 bool isWildcardExpanded(unsigned int labelCount, const std::shared_ptr<RRSIGRecordContent>& sign)
 {
-  if (sign && sign->d_labels < labelCount) {
-    return true;
-  }
-
-  return false;
+  return sign->d_labels < labelCount;
 }
 
 static bool isWildcardExpanded(const DNSName& owner, const std::vector<std::shared_ptr<RRSIGRecordContent> >& signatures)
@@ -187,11 +226,8 @@ static bool isWildcardExpanded(const DNSName& owner, const std::vector<std::shar
 
 bool isWildcardExpandedOntoItself(const DNSName& owner, unsigned int labelCount, const std::shared_ptr<RRSIGRecordContent>& sign)
 {
-  if (owner.isWildcard() && (labelCount - 1) == sign->d_labels) {
-    /* this is a wildcard alright, but it has not been expanded */
-    return true;
-  }
-  return false;
+  /* this is a wildcard alright, but it has not been expanded */
+  return owner.isWildcard() && (labelCount - 1) == sign->d_labels;
 }
 
 static bool isWildcardExpandedOntoItself(const DNSName& owner, const std::vector<std::shared_ptr<RRSIGRecordContent> >& signatures)
@@ -230,10 +266,10 @@ DNSName getNSECOwnerName(const DNSName& initialOwner, const std::vector<std::sha
   return result;
 }
 
-static bool isNSECAncestorDelegation(const DNSName& signer, const DNSName& owner, const std::shared_ptr<NSECRecordContent>& nsec)
+static bool isNSECAncestorDelegation(const DNSName& signer, const DNSName& owner, const NSECRecordContent& nsec)
 {
-  return nsec->isSet(QType::NS) &&
-    !nsec->isSet(QType::SOA) &&
+  return nsec.isSet(QType::NS) &&
+    !nsec.isSet(QType::SOA) &&
     signer.countLabels() < owner.countLabels();
 }
 
@@ -248,17 +284,17 @@ static bool provesNoDataWildCard(const DNSName& qname, const uint16_t qtype, con
 {
   const DNSName wildcard = g_wildcarddnsname + closestEncloser;
   LOG("Trying to prove that there is no data in wildcard for "<<qname<<"/"<<QType(qtype)<<endl);
-  for (const auto& v : validrrsets) {
-    LOG("Do have: "<<v.first.first<<"/"<<DNSRecordContent::NumberToType(v.first.second)<<endl);
-    if (v.first.second == QType::NSEC) {
-      for (const auto& r : v.second.records) {
-        LOG("\t"<<r->getZoneRepresentation()<<endl);
-        auto nsec = std::dynamic_pointer_cast<NSECRecordContent>(r);
+  for (const auto& validset : validrrsets) {
+    LOG("Do have: "<<validset.first.first<<"/"<<DNSRecordContent::NumberToType(validset.first.second)<<endl);
+    if (validset.first.second == QType::NSEC) {
+      for (const auto& record : validset.second.records) {
+        LOG("\t"<<record->getZoneRepresentation()<<endl);
+        auto nsec = std::dynamic_pointer_cast<NSECRecordContent>(record);
         if (!nsec) {
           continue;
         }
 
-        DNSName owner = getNSECOwnerName(v.first.first, v.second.signatures);
+        DNSName owner = getNSECOwnerName(validset.first.first, validset.second.signatures);
         if (owner != wildcard) {
           continue;
         }
@@ -295,17 +331,17 @@ static bool provesNoWildCard(const DNSName& qname, const uint16_t qtype, const D
 {
   LOG("Trying to prove that there is no wildcard for "<<qname<<"/"<<QType(qtype)<<endl);
   const DNSName wildcard = g_wildcarddnsname + closestEncloser;
-  for (const auto& v : validrrsets) {
-    LOG("Do have: "<<v.first.first<<"/"<<DNSRecordContent::NumberToType(v.first.second)<<endl);
-    if (v.first.second == QType::NSEC) {
-      for (const auto& r : v.second.records) {
-        LOG("\t"<<r->getZoneRepresentation()<<endl);
-        auto nsec = std::dynamic_pointer_cast<NSECRecordContent>(r);
+  for (const auto& validset : validrrsets) {
+    LOG("Do have: "<<validset.first.first<<"/"<<DNSRecordContent::NumberToType(validset.first.second)<<endl);
+    if (validset.first.second == QType::NSEC) {
+      for (const auto& records : validset.second.records) {
+        LOG("\t"<<records->getZoneRepresentation()<<endl);
+        auto nsec = std::dynamic_pointer_cast<const NSECRecordContent>(records);
         if (!nsec) {
           continue;
         }
 
-        const DNSName owner = getNSECOwnerName(v.first.first, v.second.signatures);
+        const DNSName owner = getNSECOwnerName(validset.first.first, validset.second.signatures);
         LOG("Comparing owner: "<<owner<<" with target: "<<wildcard<<endl);
 
         if (qname != owner && qname.isPartOf(owner) && nsec->isSet(QType::DNAME)) {
@@ -339,37 +375,38 @@ static bool provesNoWildCard(const DNSName& qname, const uint16_t qtype, const D
   If `wildcardExists` is not NULL, if will be set to true if a wildcard exists
   for this qname but doesn't have this qtype.
 */
-static bool provesNSEC3NoWildCard(const DNSName& closestEncloser, uint16_t const qtype, const cspmap_t& validrrsets, bool* wildcardExists, nsec3HashesCache& cache)
+static bool provesNSEC3NoWildCard(const DNSName& closestEncloser, uint16_t const qtype, const cspmap_t& validrrsets, bool* wildcardExists, pdns::validation::ValidationContext& context)
 {
   auto wildcard = g_wildcarddnsname + closestEncloser;
   LOG("Trying to prove that there is no wildcard for "<<wildcard<<"/"<<QType(qtype)<<endl);
 
-  for (const auto& v : validrrsets) {
-    LOG("Do have: "<<v.first.first<<"/"<<DNSRecordContent::NumberToType(v.first.second)<<endl);
-    if (v.first.second == QType::NSEC3) {
-      for (const auto& r : v.second.records) {
-        LOG("\t"<<r->getZoneRepresentation()<<endl);
-        auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(r);
+  for (const auto& validset : validrrsets) {
+    LOG("Do have: "<<validset.first.first<<"/"<<DNSRecordContent::NumberToType(validset.first.second)<<endl);
+    if (validset.first.second == QType::NSEC3) {
+      for (const auto& records : validset.second.records) {
+        LOG("\t"<<records->getZoneRepresentation()<<endl);
+        auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(records);
         if (!nsec3) {
           continue;
         }
 
-        const DNSName signer = getSigner(v.second.signatures);
-        if (!v.first.first.isPartOf(signer)) {
+        const DNSName signer = getSigner(validset.second.signatures);
+        if (!validset.first.first.isPartOf(signer)) {
           continue;
         }
 
-        string h = getHashFromNSEC3(wildcard, nsec3, cache);
-        if (h.empty()) {
+        string hash = getHashFromNSEC3(wildcard, *nsec3, context);
+        if (hash.empty()) {
+          LOG("Unsupported hash, ignoring"<<endl);
           return false;
         }
-        LOG("\tWildcard hash: "<<toBase32Hex(h)<<endl);
-        string beginHash=fromBase32Hex(v.first.first.getRawLabels()[0]);
-        LOG("\tNSEC3 hash: "<<toBase32Hex(beginHash)<<" -> "<<toBase32Hex(nsec3->d_nexthash)<<endl);
+        LOG("\tWildcard hash: "<<toBase32Hex(hash)<<endl);
+        string beginHash=fromBase32Hex(validset.first.first.getRawLabels()[0]);
+        LOG(":\tNSEC3 hash: "<<toBase32Hex(beginHash)<<" -> "<<toBase32Hex(nsec3->d_nexthash)<<endl);
 
-        if (beginHash == h) {
+        if (beginHash == hash) {
           LOG("\tWildcard hash matches");
-          if (wildcardExists) {
+          if (wildcardExists != nullptr) {
             *wildcardExists = true;
           }
 
@@ -379,7 +416,7 @@ static bool provesNSEC3NoWildCard(const DNSName& closestEncloser, uint16_t const
              that (original) owner name other than DS RRs, and all RRs below that
              owner name regardless of type.
           */
-          if (qtype != QType::DS && isNSEC3AncestorDelegation(signer, v.first.first, nsec3)) {
+          if (qtype != QType::DS && isNSEC3AncestorDelegation(signer, validset.first.first, nsec3)) {
             /* this is an "ancestor delegation" NSEC3 RR */
             LOG(" BUT an ancestor delegation NSEC3 RR can only deny the existence of a DS"<<endl);
             return false;
@@ -393,7 +430,7 @@ static bool provesNSEC3NoWildCard(const DNSName& closestEncloser, uint16_t const
           return false;
         }
 
-        if (isCoveredByNSEC3Hash(h, beginHash, nsec3->d_nexthash)) {
+        if (isCoveredByNSEC3Hash(hash, beginHash, nsec3->d_nexthash)) {
           LOG("\tWildcard hash is covered"<<endl);
           return true;
         }
@@ -418,9 +455,9 @@ dState matchesNSEC(const DNSName& name, uint16_t qtype, const DNSName& nsecOwner
      that (original) owner name other than DS RRs, and all RRs below that
      owner name regardless of type.
   */
-  if (name.isPartOf(owner) && isNSECAncestorDelegation(signer, owner, nsec)) {
+   if (name.isPartOf(owner) && isNSECAncestorDelegation(signer, owner, *nsec)) {
     /* this is an "ancestor delegation" NSEC RR */
-    if (!(qtype == QType::DS && name == owner)) {
+    if (qtype != QType::DS || name != owner) {
       LOG("An ancestor delegation NSEC RR can only deny the existence of a DS"<<endl);
       return dState::NODENIAL;
     }
@@ -429,12 +466,12 @@ dState matchesNSEC(const DNSName& name, uint16_t qtype, const DNSName& nsecOwner
   /* check if the type is denied */
   if (name == owner) {
     if (!isTypeDenied(nsec, QType(qtype))) {
-      LOG("Does _not_ deny existence of type "<<QType(qtype)<<endl);
+      LOG("does _not_ deny existence of type "<<QType(qtype)<<endl);
       return dState::NODENIAL;
     }
 
     if (qtype == QType::DS && signer == name) {
-      LOG("The NSEC comes from the child zone and cannot be used to deny a DS");
+      LOG("The NSEC comes from the child zone and cannot be used to deny a DS"<<endl);
       return dState::NODENIAL;
     }
 
@@ -457,7 +494,7 @@ dState matchesNSEC(const DNSName& name, uint16_t qtype, const DNSName& nsecOwner
   }
 
   if (isCoveredByNSEC(name, owner, nsec->d_next)) {
-    LOG(name<<" is covered by ("<<owner<<" to "<<nsec->d_next<<") ");
+    LOG(name<<" is covered by ("<<owner<<" to "<<nsec->d_next<<")");
 
     if (nsecProvesENT(name, owner, nsec->d_next)) {
       LOG("Denies existence of type "<<name<<"/"<<QType(qtype)<<" by proving that "<<name<<" is an ENT"<<endl);
@@ -470,6 +507,11 @@ dState matchesNSEC(const DNSName& name, uint16_t qtype, const DNSName& nsecOwner
   return dState::INCONCLUSIVE;
 }
 
+[[nodiscard]] uint64_t getNSEC3DenialProofWorstCaseIterationsCount(uint8_t maxLabels, uint16_t iterations, size_t saltLength)
+{
+  return (iterations + 1 + (saltLength > 0 ? 1 : 0)) * maxLabels;
+}
+
 /*
   This function checks whether the existence of qname|qtype is denied by the NSEC and NSEC3
   in validrrsets.
@@ -481,34 +523,34 @@ dState matchesNSEC(const DNSName& name, uint16_t qtype, const DNSName& nsecOwner
   useful when we have a positive answer synthesized from a wildcard and we only need to prove that the exact
   name does not exist.
 */
-
-dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, bool needWildcardProof, unsigned int wildcardLabelsCount)
+dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, pdns::validation::ValidationContext& context, bool needWildcardProof, unsigned int wildcardLabelsCount) // NOLINT(readability-function-cognitive-complexity): https://github.com/PowerDNS/pdns/issues/12791
 {
-  nsec3HashesCache cache;
   bool nsec3Seen = false;
   if (!needWildcardProof && wildcardLabelsCount == 0) {
     throw PDNSException("Invalid wildcard labels count for the validation of a positive answer synthesized from a wildcard");
   }
 
-  for (const auto& v : validrrsets) {
-    LOG("Do have: "<<v.first.first<<"/"<<DNSRecordContent::NumberToType(v.first.second)<<endl);
+  uint8_t numberOfLabelsOfParentZone{std::numeric_limits<uint8_t>::max()};
+  uint16_t nsec3sConsidered = 0;
+  for (const auto& validset : validrrsets) {
+    LOG("Do have: "<<validset.first.first<<"/"<<DNSRecordContent::NumberToType(validset.first.second)<<endl);
 
-    if (v.first.second==QType::NSEC) {
-      for (const auto& r : v.second.records) {
-        LOG("\t"<<r->getZoneRepresentation()<<endl);
+    if (validset.first.second==QType::NSEC) {
+      for (const auto& record : validset.second.records) {
+        LOG("\t"<<record->getZoneRepresentation()<<endl);
 
-        if (v.second.signatures.empty()) {
+        if (validset.second.signatures.empty()) {
           continue;
         }
 
-        auto nsec = std::dynamic_pointer_cast<NSECRecordContent>(r);
+        auto nsec = std::dynamic_pointer_cast<const NSECRecordContent>(record);
         if (!nsec) {
           continue;
         }
 
-        const DNSName owner = getNSECOwnerName(v.first.first, v.second.signatures);
-        const DNSName signer = getSigner(v.second.signatures);
-        if (!v.first.first.isPartOf(signer) || !owner.isPartOf(signer) ) {
+        const DNSName owner = getNSECOwnerName(validset.first.first, validset.second.signatures);
+        const DNSName signer = getSigner(validset.second.signatures);
+        if (!validset.first.first.isPartOf(signer) || !owner.isPartOf(signer) ) {
            continue;
         }
 
@@ -528,9 +570,9 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
            that (original) owner name other than DS RRs, and all RRs below that
            owner name regardless of type.
         */
-        if (qname.isPartOf(owner) && isNSECAncestorDelegation(signer, owner, nsec)) {
+        if (qname.isPartOf(owner) && isNSECAncestorDelegation(signer, owner, *nsec)) {
           /* this is an "ancestor delegation" NSEC RR */
-          if (!(qtype == QType::DS && qname == owner)) {
+          if (qtype != QType::DS || qname != owner) {
             LOG("An ancestor delegation NSEC RR can only deny the existence of a DS"<<endl);
             return dState::NODENIAL;
           }
@@ -544,7 +586,7 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
         /* check if the type is denied */
         if (qname == owner) {
           if (!isTypeDenied(nsec, QType(qtype))) {
-            LOG("Does _not_ deny existence of type "<<QType(qtype)<<endl);
+            LOG("does _not_ deny existence of type "<<QType(qtype)<<endl);
             return dState::NODENIAL;
           }
 
@@ -566,7 +608,7 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
           /* we know that the name exists (but this qtype doesn't) so except
              if the answer was generated by a wildcard expansion, no wildcard
              could have matched (rfc4035 section 5.4 bullet 1) */
-          if (needWildcardProof && (!isWildcardExpanded(owner, v.second.signatures) || isWildcardExpandedOntoItself(owner, v.second.signatures))) {
+          if (needWildcardProof && (!isWildcardExpanded(owner, validset.second.signatures) || isWildcardExpandedOntoItself(owner, validset.second.signatures))) {
             needWildcardProof = false;
           }
 
@@ -593,13 +635,13 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
              asserted, then DNAME substitution should have been done, but the
              substitution has not been done as specified.
           */
-          LOG("The DNAME bit is set and the query name is a subdomain of that NSEC");
+          LOG("The DNAME bit is set and the query name is a subdomain of that NSEC"<< endl);
           return dState::NODENIAL;
         }
 
         /* check if the whole NAME is denied existing */
         if (isCoveredByNSEC(qname, owner, nsec->d_next)) {
-          LOG(qname<<" is covered by ("<<owner<<" to "<<nsec->d_next<<") ");
+          LOG(qname<<" is covered by ("<<owner<<" to "<<nsec->d_next<<")");
 
           if (nsecProvesENT(qname, owner, nsec->d_next)) {
             if (wantsNoDataProof) {
@@ -608,11 +650,9 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
               LOG("Denies existence of type "<<qname<<"/"<<QType(qtype)<<" by proving that "<<qname<<" is an ENT"<<endl);
               return dState::NXQTYPE;
             }
-            else {
-              /* but for a NXDOMAIN proof, this doesn't make sense! */
-              LOG("but it tries to deny the existence of "<<qname<<" by proving that "<<qname<<" is an ENT, this does not make sense!"<<endl);
-              return dState::NODENIAL;
-            }
+            /* but for a NXDOMAIN proof, this doesn't make sense! */
+            LOG("but it tries to deny the existence of "<<qname<<" by proving that "<<qname<<" is an ENT, this does not make sense!"<<endl);
+            return dState::NODENIAL;
           }
 
           if (!needWildcardProof) {
@@ -639,45 +679,52 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
           return dState::NODENIAL;
         }
 
-        LOG("Did not deny existence of "<<QType(qtype)<<", "<<v.first.first<<"?="<<qname<<", "<<nsec->isSet(qtype)<<", next: "<<nsec->d_next<<endl);
+        LOG("Did not deny existence of "<<QType(qtype)<<", "<<validset.first.first<<"?="<<qname<<", "<<nsec->isSet(qtype)<<", next: "<<nsec->d_next<<endl);
       }
-    } else if(v.first.second==QType::NSEC3) {
-      for (const auto& r : v.second.records) {
-        LOG("\t"<<r->getZoneRepresentation()<<endl);
-        auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(r);
+    } else if(validset.first.second==QType::NSEC3) {
+      for (const auto& record : validset.second.records) {
+        LOG("\t"<<record->getZoneRepresentation()<<endl);
+        auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(record);
         if (!nsec3) {
           continue;
         }
 
-        if (v.second.signatures.empty()) {
+        if (validset.second.signatures.empty()) {
           continue;
         }
 
-        const DNSName& hashedOwner = v.first.first;
-        const DNSName signer = getSigner(v.second.signatures);
+        const DNSName& hashedOwner = validset.first.first;
+        const DNSName signer = getSigner(validset.second.signatures);
         if (!hashedOwner.isPartOf(signer)) {
           LOG("Owner "<<hashedOwner<<" is not part of the signer "<<signer<<", ignoring"<<endl);
           continue;
         }
+        numberOfLabelsOfParentZone = std::min(numberOfLabelsOfParentZone, static_cast<uint8_t>(signer.countLabels()));
 
         if (qtype == QType::DS && !qname.isRoot() && signer == qname) {
           LOG("A NSEC3 RR from the child zone cannot deny the existence of a DS"<<endl);
           continue;
         }
 
-        string h = getHashFromNSEC3(qname, nsec3, cache);
-        if (h.empty()) {
+        if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+          LOG("Too many NSEC3s for this record"<<endl);
+          return dState::NODENIAL;
+        }
+        nsec3sConsidered++;
+
+        string hash = getHashFromNSEC3(qname, *nsec3, context);
+        if (hash.empty()) {
           LOG("Unsupported hash, ignoring"<<endl);
           return dState::INSECURE;
         }
 
         nsec3Seen = true;
 
-        LOG("\tquery hash: "<<toBase32Hex(h)<<endl);
+        LOG("\tquery hash: "<<toBase32Hex(hash)<<endl);
         string beginHash = fromBase32Hex(hashedOwner.getRawLabels()[0]);
 
         // If the name exists, check if the qtype is denied
-        if (beginHash == h) {
+        if (beginHash == hash) {
 
           /* The NSEC3 is either a delegation one, from the parent zone, and
            * must have the NS bit set but not the SOA one, or a regular NSEC3
@@ -735,42 +782,49 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
 
   DNSName closestEncloser(qname);
   bool found = false;
-
   if (needWildcardProof) {
+    nsec3sConsidered = 0;
     /* We now need to look for a NSEC3 covering the closest (provable) encloser
        RFC 5155 section-7.2.1
        RFC 7129 section-5.5
     */
     LOG("Now looking for the closest encloser for "<<qname<<endl);
 
-    while (found == false && closestEncloser.chopOff()) {
+    while (!found && closestEncloser.chopOff() && closestEncloser.countLabels() >= numberOfLabelsOfParentZone) {
 
-      for(const auto& v : validrrsets) {
-        if(v.first.second==QType::NSEC3) {
-          for(const auto& r : v.second.records) {
-            LOG("\t"<<r->getZoneRepresentation()<<endl);
-            auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(r);
+      for(const auto& validset : validrrsets) {
+        if(validset.first.second==QType::NSEC3) {
+          for(const auto& record : validset.second.records) {
+            LOG("\t"<<record->getZoneRepresentation()<<endl);
+            auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(record);
             if (!nsec3) {
               continue;
             }
 
-            const DNSName signer = getSigner(v.second.signatures);
-            if (!v.first.first.isPartOf(signer)) {
-              LOG("Owner "<<v.first.first<<" is not part of the signer "<<signer<<", ignoring"<<endl);
+            const DNSName signer = getSigner(validset.second.signatures);
+            if (!validset.first.first.isPartOf(signer)) {
+              LOG("Owner "<<validset.first.first<<" is not part of the signer "<<signer<<", ignoring"<<endl);
               continue;
             }
 
-            string h = getHashFromNSEC3(closestEncloser, nsec3, cache);
-            if (h.empty()) {
+            if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+              LOG("Too many NSEC3s for this record"<<endl);
+              return dState::NODENIAL;
+            }
+            nsec3sConsidered++;
+
+            string hash = getHashFromNSEC3(closestEncloser, *nsec3, context);
+            if (hash.empty()) {
+              LOG(": Unsupported hash, ignoring"<<endl);
               return dState::INSECURE;
             }
 
-            string beginHash=fromBase32Hex(v.first.first.getRawLabels()[0]);
+            string beginHash=fromBase32Hex(validset.first.first.getRawLabels()[0]);
 
-            LOG("Comparing "<<toBase32Hex(h)<<" ("<<closestEncloser<<") against "<<toBase32Hex(beginHash)<<endl);
-            if (beginHash == h) {
+            LOG("Comparing "<<toBase32Hex(hash)<<" ("<<closestEncloser<<") against "<<toBase32Hex(beginHash)<<endl);
+            if (beginHash == hash) {
               /* If the closest encloser is a delegation NS we know nothing about the names in the child zone. */
-              if (isNSEC3AncestorDelegation(signer, v.first.first, nsec3)) {
+              if (isNSEC3AncestorDelegation(signer, validset.first.first, nsec3)) {
                 LOG("An ancestor delegation NSEC3 RR can only deny the existence of a DS"<<endl);
                 continue;
               }
@@ -796,7 +850,7 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
             }
           }
         }
-        if (found == true) {
+        if (found) {
           break;
         }
       }
@@ -819,7 +873,7 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
   bool nextCloserFound = false;
   bool isOptOut = false;
 
-  if (found == true) {
+  if (found) {
     /* now that we have found the closest (provable) encloser,
        we can construct the next closer (RFC7129 section-5.5) name
        and look for a NSEC3 RR covering it */
@@ -827,31 +881,40 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
     if (labelIdx >= 1) {
       DNSName nextCloser(closestEncloser);
       nextCloser.prependRawLabel(qname.getRawLabel(labelIdx - 1));
+      nsec3sConsidered = 0;
       LOG("Looking for a NSEC3 covering the next closer name "<<nextCloser<<endl);
 
-      for(const auto& v : validrrsets) {
-        if(v.first.second==QType::NSEC3) {
-          for(const auto& r : v.second.records) {
-            LOG("\t"<<r->getZoneRepresentation()<<endl);
-            auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(r);
-            if(!nsec3)
+      for (const auto& validset : validrrsets) {
+        if (validset.first.second == QType::NSEC3) {
+          for (const auto& record : validset.second.records) {
+            LOG("\t"<<record->getZoneRepresentation()<<endl);
+            auto nsec3 = std::dynamic_pointer_cast<const NSEC3RecordContent>(record);
+            if (!nsec3) {
               continue;
+            }
 
-            string h = getHashFromNSEC3(nextCloser, nsec3, cache);
-            if (h.empty()) {
+            if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+              LOG("Too many NSEC3s for this record"<<endl);
+              return dState::NODENIAL;
+            }
+            nsec3sConsidered++;
+
+            string hash = getHashFromNSEC3(nextCloser, *nsec3, context);
+            if (hash.empty()) {
+              LOG("Unsupported hash, ignoring"<<endl);
               return dState::INSECURE;
             }
 
-            const DNSName signer = getSigner(v.second.signatures);
-            if (!v.first.first.isPartOf(signer)) {
-              LOG("Owner "<<v.first.first<<" is not part of the signer "<<signer<<", ignoring"<<endl);
+            const DNSName signer = getSigner(validset.second.signatures);
+            if (!validset.first.first.isPartOf(signer)) {
+              LOG("Owner "<<validset.first.first<<" is not part of the signer "<<signer<<", ignoring"<<endl);
               continue;
             }
 
-            string beginHash=fromBase32Hex(v.first.first.getRawLabels()[0]);
+            string beginHash=fromBase32Hex(validset.first.first.getRawLabels()[0]);
 
-            LOG("Comparing "<<toBase32Hex(h)<<" against "<<toBase32Hex(beginHash)<<" -> "<<toBase32Hex(nsec3->d_nexthash)<<endl);
-            if (isCoveredByNSEC3Hash(h, beginHash, nsec3->d_nexthash)) {
+            LOG("Comparing "<<toBase32Hex(hash)<<" against "<<toBase32Hex(beginHash)<<" -> "<<toBase32Hex(nsec3->d_nexthash)<<endl);
+            if (isCoveredByNSEC3Hash(hash, beginHash, nsec3->d_nexthash)) {
               LOG("Denies existence of name "<<qname<<"/"<<QType(qtype));
               nextCloserFound = true;
 
@@ -863,7 +926,7 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
               LOG(endl);
               break;
             }
-            LOG("Did not cover us ("<<qname<<"), start="<<v.first.first<<", us="<<toBase32Hex(h)<<", end="<<toBase32Hex(nsec3->d_nexthash)<<endl);
+            LOG("Did not cover us ("<<qname<<"), start="<<validset.first.first<<", us="<<toBase32Hex(hash)<<", end="<<toBase32Hex(nsec3->d_nexthash)<<endl);
           }
         }
         if (nextCloserFound) {
@@ -876,7 +939,7 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
   if (nextCloserFound) {
     bool wildcardExists = false;
     /* RFC 7129 section-5.6 */
-    if (needWildcardProof && !provesNSEC3NoWildCard(closestEncloser, qtype, validrrsets, &wildcardExists, cache)) {
+    if (needWildcardProof && !provesNSEC3NoWildCard(closestEncloser, qtype, validrrsets, &wildcardExists, context)) {
       if (!isOptOut) {
         LOG("But the existence of a wildcard is not denied for "<<qname<<"/"<<QType(qtype)<<endl);
         return dState::NODENIAL;
@@ -886,85 +949,53 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
     if (isOptOut) {
       return dState::OPTOUT;
     }
-    else {
-      if (wildcardExists) {
-        return dState::NXQTYPE;
-      }
-      return dState::NXDOMAIN;
+    if (wildcardExists) {
+      return dState::NXQTYPE;
     }
+    return dState::NXDOMAIN;
   }
 
   // There were no valid NSEC(3) records
   return dState::NODENIAL;
 }
 
-/*
- * Finds all the zone-cuts between begin (longest name) and end (shortest name),
- * returns them all zone cuts, including end, but (possibly) not begin
- */
-static const vector<DNSName> getZoneCuts(const DNSName& begin, const DNSName& end, DNSRecordOracle& dro)
-{
-  vector<DNSName> ret;
-  if(!begin.isPartOf(end))
-    throw PDNSException(end.toLogString() + "is not part of " + begin.toLogString());
-
-  DNSName qname(end);
-  vector<string> labelsToAdd = begin.makeRelative(end).getRawLabels();
-
-  // The shortest name is assumed to a zone cut
-  ret.push_back(qname);
-  while(qname != begin) {
-    bool foundCut = false;
-    if (labelsToAdd.empty())
-      break;
-
-    qname.prependRawLabel(labelsToAdd.back());
-    labelsToAdd.pop_back();
-    auto records = dro.get(qname, (uint16_t)QType::NS);
-    for (const auto& record : records) {
-      if(record.d_type != QType::NS || record.d_name != qname)
-        continue;
-      foundCut = true;
-      break;
-    }
-    if (foundCut)
-      ret.push_back(qname);
-  }
-  return ret;
-}
-
-bool isRRSIGNotExpired(const time_t now, const shared_ptr<RRSIGRecordContent>& sig)
+bool isRRSIGNotExpired(const time_t now, const std::shared_ptr<RRSIGRecordContent>& sig)
 {
   // Should use https://www.rfc-editor.org/rfc/rfc4034.txt section 3.1.5
   return sig->d_sigexpire >= now;
 }
 
-bool isRRSIGIncepted(const time_t now, const shared_ptr<RRSIGRecordContent>& sig)
+bool isRRSIGIncepted(const time_t now, const std::shared_ptr<RRSIGRecordContent>& sig)
 {
   // Should use https://www.rfc-editor.org/rfc/rfc4034.txt section 3.1.5
   return sig->d_siginception - g_signatureInceptionSkew <= now;
 }
 
-static bool checkSignatureWithKey(time_t now, const shared_ptr<RRSIGRecordContent> sig, const shared_ptr<DNSKEYRecordContent> key, const std::string& msg, vState& ede)
+namespace {
+  [[nodiscard]] bool checkSignatureInceptionAndExpiry(const DNSName& qname, time_t now, const std::shared_ptr<RRSIGRecordContent>& sig, vState& ede)
+{
+  /* rfc4035:
+     - The validator's notion of the current time MUST be less than or equal to the time listed in the RRSIG RR's Expiration field.
+     - The validator's notion of the current time MUST be greater than or equal to the time listed in the RRSIG RR's Inception field.
+  */
+  if (isRRSIGIncepted(now, sig) && isRRSIGNotExpired(now, sig)) {
+    return true;
+  }
+  ede = ((sig->d_siginception - g_signatureInceptionSkew) > now) ? vState::BogusSignatureNotYetValid : vState::BogusSignatureExpired;
+  LOG("Signature is "<<(ede == vState::BogusSignatureNotYetValid ? "not yet valid" : "expired")<<" (inception: "<<sig->d_siginception<<", inception skew: "<<g_signatureInceptionSkew<<", expiration: "<<sig->d_sigexpire<<", now: "<<now<<")"<<endl);
+  return false;
+}
+
+  [[nodiscard]] bool checkSignatureWithKey(const DNSName& qname, const std::shared_ptr<RRSIGRecordContent>& sig, const std::shared_ptr<DNSKEYRecordContent>& key, const std::string& msg, vState& ede)
 {
   bool result = false;
   try {
-    /* rfc4035:
-       - The validator's notion of the current time MUST be less than or equal to the time listed in the RRSIG RR's Expiration field.
-       - The validator's notion of the current time MUST be greater than or equal to the time listed in the RRSIG RR's Inception field.
-    */
-    if (isRRSIGIncepted(now, sig) && isRRSIGNotExpired(now, sig)) {
-      auto dke = DNSCryptoKeyEngine::makeFromPublicKeyString(key->d_algorithm, key->d_key);
-      result = dke->verify(msg, sig->d_signature);
-      LOG("signature by key with tag "<<sig->d_tag<<" and algorithm "<<DNSSECKeeper::algorithm2name(sig->d_algorithm)<<" was " << (result ? "" : "NOT ")<<"valid"<<endl);
-      if (!result) {
-        ede = vState::BogusNoValidRRSIG;
-      }
+    auto dke = DNSCryptoKeyEngine::makeFromPublicKeyString(key->d_algorithm, key->d_key);
+    result = dke->verify(msg, sig->d_signature);
+    LOG("Signature by key with tag "<<sig->d_tag<<" and algorithm "<<DNSSECKeeper::algorithm2name(sig->d_algorithm)<<" was " << (result ? "" : "NOT ")<<"valid"<<endl);
+    if (!result) {
+      ede = vState::BogusNoValidRRSIG;
     }
-    else {
-      ede = ((sig->d_siginception - g_signatureInceptionSkew) > now) ? vState::BogusSignatureNotYetValid : vState::BogusSignatureExpired;
-      LOG("Signature is "<<(ede == vState::BogusSignatureNotYetValid ? "not yet valid" : "expired")<<" (inception: "<<sig->d_siginception<<", inception skew: "<<g_signatureInceptionSkew<<", expiration: "<<sig->d_sigexpire<<", now: "<<now<<")"<<endl);
-     }
   }
   catch (const std::exception& e) {
     LOG("Could not make a validator for signature: "<<e.what()<<endl);
@@ -973,32 +1004,60 @@ static bool checkSignatureWithKey(time_t now, const shared_ptr<RRSIGRecordConten
   return result;
 }
 
-vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& toSign, const vector<shared_ptr<RRSIGRecordContent> >& signatures, const skeyset_t& keys, bool validateAllSigs)
+}
+
+vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& toSign, const vector<shared_ptr<RRSIGRecordContent> >& signatures, const skeyset_t& keys, pdns::validation::ValidationContext& context, bool validateAllSigs)
 {
-  bool foundKey = false;
+  bool missingKey = false;
   bool isValid = false;
   bool allExpired = true;
   bool noneIncepted = true;
+  uint16_t signaturesConsidered = 0;
 
-  for(const auto& signature : signatures) {
+  for (const auto& signature : signatures) {
     unsigned int labelCount = name.countLabels();
     if (signature->d_labels > labelCount) {
       LOG(name<<": Discarding invalid RRSIG whose label count is "<<signature->d_labels<<" while the RRset owner name has only "<<labelCount<<endl);
       continue;
     }
 
+    vState ede = vState::Indeterminate;
+    if (!checkSignatureInceptionAndExpiry(name, now, signature, ede)) {
+      if (isRRSIGIncepted(now, signature)) {
+        noneIncepted = false;
+      }
+      if (isRRSIGNotExpired(now, signature)) {
+        allExpired = false;
+      }
+      continue;
+    }
+
+    if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) {
+      LOG("We have already considered "<<std::to_string(signaturesConsidered)<<" RRSIG"<<addS(signaturesConsidered)<<" for this record, stopping now"<<endl;);
+      // possibly going Bogus, the RRSIGs have not been validated so Insecure would be wrong
+      break;
+    }
+    signaturesConsidered++;
+    context.d_validationsCounter++;
+
     auto keysMatchingTag = getByTag(keys, signature->d_tag, signature->d_algorithm);
 
     if (keysMatchingTag.empty()) {
       LOG("No key provided for "<<signature->d_tag<<" and algorithm "<<std::to_string(signature->d_algorithm)<<endl;);
+      missingKey = true;
       continue;
     }
 
     string msg = getMessageForRRSET(name, *signature, toSign, true);
+    uint16_t dnskeysConsidered = 0;
     for (const auto& key : keysMatchingTag) {
-      vState ede;
-      bool signIsValid = checkSignatureWithKey(now, signature, key, msg, ede);
-      foundKey = true;
+      if (g_maxDNSKEYsToConsider > 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) {
+        LOG("We have already considered "<<std::to_string(dnskeysConsidered)<<" DNSKEY"<<addS(dnskeysConsidered)<<" for tag "<<std::to_string(signature->d_tag)<<" and algorithm "<<std::to_string(signature->d_algorithm)<<", not considering the remaining ones for this signature"<<endl;);
+        return isValid ? vState::Secure : vState::BogusNoValidRRSIG;
+      }
+      dnskeysConsidered++;
+
+      bool signIsValid = checkSignatureWithKey(name, signature, key, msg, ede);
 
       if (signIsValid) {
         isValid = true;
@@ -1025,7 +1084,7 @@ vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t
   if (isValid) {
     return vState::Secure;
   }
-  if (!foundKey) {
+  if (missingKey) {
     return vState::BogusNoValidRRSIG;
   }
   if (noneIncepted) {
@@ -1040,23 +1099,6 @@ vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t
   return vState::BogusNoValidRRSIG;
 }
 
-void validateWithKeySet(const cspmap_t& rrsets, cspmap_t& validated, const skeyset_t& keys)
-{
-  validated.clear();
-  /*  cerr<<"Validating an rrset with following keys: "<<endl;
-  for(auto& key : keys) {
-    cerr<<"\tTag: "<<key->getTag()<<" -> "<<key->getZoneRepresentation()<<endl;
-  }
-  */
-  time_t now = time(nullptr);
-  for(auto i=rrsets.cbegin(); i!=rrsets.cend(); i++) {
-    LOG("validating "<<(i->first.first)<<"/"<<DNSRecordContent::NumberToType(i->first.second)<<" with "<<i->second.signatures.size()<<" sigs"<<endl);
-    if (validateWithKeySet(now, i->first.first, i->second.records, i->second.signatures, keys, true) == vState::Secure) {
-      validated[i->first] = i->second;
-    }
-  }
-}
-
 // returns vState
 // should return vState, zone cut and validated keyset
 // i.e. www.7bits.nl -> insecure/7bits.nl/[]
@@ -1068,7 +1110,9 @@ cspmap_t harvestCSPFromRecs(const vector<DNSRecord>& recs)
   cspmap_t cspmap;
   for(const auto& rec : recs) {
     //        cerr<<"res "<<rec.d_name<<"/"<<rec.d_type<<endl;
-    if(rec.d_type == QType::OPT) continue;
+    if (rec.d_type == QType::OPT) {
+      continue;
+    }
 
     if(rec.d_type == QType::RRSIG) {
       auto rrc = getRR<RRSIGRecordContent>(rec);
@@ -1085,44 +1129,60 @@ cspmap_t harvestCSPFromRecs(const vector<DNSRecord>& recs)
 
 bool getTrustAnchor(const map<DNSName,dsmap_t>& anchors, const DNSName& zone, dsmap_t &res)
 {
-  const auto& it = anchors.find(zone);
+  const auto& iter = anchors.find(zone);
 
-  if (it == anchors.cend()) {
+  if (iter == anchors.cend()) {
     return false;
   }
 
-  res = it->second;
+  res = iter->second;
   return true;
 }
 
 bool haveNegativeTrustAnchor(const map<DNSName,std::string>& negAnchors, const DNSName& zone, std::string& reason)
 {
-  const auto& it = negAnchors.find(zone);
+  const auto& iter = negAnchors.find(zone);
 
-  if (it == negAnchors.cend()) {
+  if (iter == negAnchors.cend()) {
     return false;
   }
 
-  reason = it->second;
+  reason = iter->second;
   return true;
 }
 
-vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector<shared_ptr<RRSIGRecordContent> >& sigs, skeyset_t& validkeys)
+vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector<shared_ptr<RRSIGRecordContent> >& sigs, skeyset_t& validkeys, pdns::validation::ValidationContext& context)
 {
   /*
    * Check all DNSKEY records against all DS records and place all DNSKEY records
    * that have DS records (that we support the algo for) in the tentative key storage
    */
-  for (const auto& dsrc : dsmap)
-  {
-    auto r = getByTag(tkeys, dsrc.d_tag, dsrc.d_algorithm);
+  uint16_t dssConsidered = 0;
+  for (const auto& dsrc : dsmap) {
+    if (g_maxDSsToConsider > 0 && dssConsidered > g_maxDSsToConsider) {
+      LOG("We have already considered "<<std::to_string(dssConsidered)<<" DS"<<addS(dssConsidered)<<", not considering the remaining ones"<<endl;);
+      return vState::BogusNoValidDNSKEY;
+    }
+    ++dssConsidered;
+
+    uint16_t dnskeysConsidered = 0;
+    auto record = getByTag(tkeys, dsrc.d_tag, dsrc.d_algorithm);
     // cerr<<"looking at DS with tag "<<dsrc.d_tag<<", algo "<<DNSSECKeeper::algorithm2name(dsrc.d_algorithm)<<", digest "<<std::to_string(dsrc.d_digesttype)<<" for "<<zone<<", got "<<r.size()<<" DNSKEYs for tag"<<endl;
 
-    for (const auto& drc : r)
-    {
+    for (const auto& drc : record) {
       bool isValid = false;
       bool dsCreated = false;
       DSRecordContent dsrc2;
+
+      if (g_maxDNSKEYsToConsider > 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) {
+        LOG("We have already considered "<<std::to_string(dnskeysConsidered)<<" DNSKEY"<<addS(dnskeysConsidered)<<" for tag "<<std::to_string(dsrc.d_tag)<<" and algorithm "<<std::to_string(dsrc.d_algorithm)<<", not considering the remaining ones for this DS"<<endl;);
+        // we need to break because we can have a partially validated set
+        // where the KSK signs the ZSK(s), and even if we don't
+        // we are going to try to get the correct EDE status (revoked, expired, ...)
+        break;
+      }
+      dnskeysConsidered++;
+
       try {
         dsrc2 = makeDSFromDNSKey(zone, *drc, dsrc.d_digesttype);
         dsCreated = true;
@@ -1150,14 +1210,17 @@ vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t&
   //    cerr<<"got "<<validkeys.size()<<"/"<<tkeys.size()<<" valid/tentative keys"<<endl;
   // these counts could be off if we somehow ended up with
   // duplicate keys. Should switch to a type that prevents that.
-  if (validkeys.size() < tkeys.size())
-  {
+  if (!tkeys.empty() && validkeys.size() < tkeys.size()) {
     // this should mean that we have one or more DS-validated DNSKEYs
     // but not a fully validated DNSKEY set, yet
     // one of these valid DNSKEYs should be able to validate the
     // whole set
-    for (const auto& sig : sigs)
-    {
+    uint16_t signaturesConsidered = 0;
+    for (const auto& sig : sigs) {
+      if (!checkSignatureInceptionAndExpiry(zone, now, sig, ede)) {
+        continue;
+      }
+
       //        cerr<<"got sig for keytag "<<i->d_tag<<" matching "<<getByTag(tkeys, i->d_tag).size()<<" keys of which "<<getByTag(validkeys, i->d_tag).size()<<" valid"<<endl;
       auto bytag = getByTag(validkeys, sig->d_tag, sig->d_algorithm);
 
@@ -1165,20 +1228,42 @@ vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t&
         continue;
       }
 
+      if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) {
+        LOG("We have already considered "<<std::to_string(signaturesConsidered)<<" RRSIG"<<addS(signaturesConsidered)<<" for this record, stopping now"<<endl;);
+        // possibly going Bogus, the RRSIGs have not been validated so Insecure would be wrong
+        return vState::BogusNoValidDNSKEY;
+      }
+
       string msg = getMessageForRRSET(zone, *sig, toSign);
+      uint16_t dnskeysConsidered = 0;
       for (const auto& key : bytag) {
+        if (g_maxDNSKEYsToConsider > 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) {
+          LOG("We have already considered "<<std::to_string(dnskeysConsidered)<<" DNSKEY"<<addS(dnskeysConsidered)<<" for tag "<<std::to_string(sig->d_tag)<<" and algorithm "<<std::to_string(sig->d_algorithm)<<", not considering the remaining ones for this signature"<<endl;);
+          return vState::BogusNoValidDNSKEY;
+        }
+        dnskeysConsidered++;
+
+        if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) {
+          LOG("We have already considered "<<std::to_string(signaturesConsidered)<<" RRSIG"<<addS(signaturesConsidered)<<" for this record, stopping now"<<endl;);
+          // possibly going Bogus, the RRSIGs have not been validated so Insecure would be wrong
+          return vState::BogusNoValidDNSKEY;
+        }
         //          cerr<<"validating : ";
-        bool signIsValid = checkSignatureWithKey(now, sig, key, msg, ede);
+        bool signIsValid = checkSignatureWithKey(zone, sig, key, msg, ede);
+        signaturesConsidered++;
+        context.d_validationsCounter++;
 
-        if (signIsValid)
-        {
+        if (signIsValid) {
           LOG("validation succeeded - whole DNSKEY set is valid"<<endl);
           validkeys = tkeys;
           break;
         }
-        else {
-          LOG("Validation did not succeed!"<<endl);
-        }
+        LOG("Validation did not succeed!"<<endl);
+      }
+
+      if (validkeys.size() == tkeys.size()) {
+        // we validated the whole DNSKEY set already */
+        break;
       }
       //        if(validkeys.empty()) cerr<<"did not manage to validate DNSKEY set based on DS-validated KSK, only passing KSK on"<<endl;
     }
@@ -1243,169 +1328,15 @@ vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t&
   return vState::Secure;
 }
 
-vState getKeysFor(DNSRecordOracle& dro, const DNSName& zone, skeyset_t& keyset)
+bool isSupportedDS(const DSRecordContent& dsrec)
 {
-  auto luaLocal = g_luaconfs.getLocal();
-  const auto anchors = luaLocal->dsAnchors;
-  if (anchors.empty()) // Nothing to do here
-    return vState::Insecure;
-
-  // Determine the lowest (i.e. with the most labels) Trust Anchor for zone
-  DNSName lowestTA(".");
-  for (auto const &anchor : anchors)
-    if (zone.isPartOf(anchor.first) && lowestTA.countLabels() < anchor.first.countLabels())
-      lowestTA = anchor.first;
-
-  // Before searching for the keys, see if we have a Negative Trust Anchor. If
-  // so, test if the NTA is valid and return an NTA state
-  const auto negAnchors = luaLocal->negAnchors;
-
-  if (!negAnchors.empty()) {
-    DNSName lowestNTA;
-
-    for (auto const &negAnchor : negAnchors)
-      if (zone.isPartOf(negAnchor.first) && lowestNTA.countLabels() <= negAnchor.first.countLabels())
-        lowestNTA = negAnchor.first;
-
-    if(!lowestNTA.empty()) {
-      LOG("Found a Negative Trust Anchor for "<<lowestNTA<<", which was added with reason '"<<negAnchors.at(lowestNTA)<<"', ");
-
-      /* RFC 7646 section 2.1 tells us that we SHOULD still validate if there
-       * is a Trust Anchor below the Negative Trust Anchor for the name we
-       * attempt validation for. However, section 3 tells us this positive
-       * Trust Anchor MUST be *below* the name and not the name itself
-       */
-      if(lowestTA.countLabels() <= lowestNTA.countLabels()) {
-        LOG("marking answer Insecure"<<endl);
-        return vState::NTA; // Not Insecure, this way validateRecords() can shortcut
-      }
-      LOG("but a Trust Anchor for "<<lowestTA<<" is configured, continuing validation."<<endl);
-    }
-  }
-
-  skeyset_t validkeys;
-  dsmap_t dsmap;
-
-  dsmap_t* tmp = (dsmap_t*) rplookup(anchors, lowestTA);
-  if (tmp)
-    dsmap = *tmp;
-
-  auto zoneCuts = getZoneCuts(zone, lowestTA, dro);
-
-  LOG("Found the following zonecuts:")
-  for(const auto& zonecut : zoneCuts)
-    LOG(" => "<<zonecut);
-  LOG(endl);
-
-  for(auto zoneCutIter = zoneCuts.cbegin(); zoneCutIter != zoneCuts.cend(); ++zoneCutIter)
-  {
-    vector<shared_ptr<RRSIGRecordContent> > sigs;
-    sortedRecords_t toSign;
-
-    skeyset_t tkeys; // tentative keys
-    validkeys.clear();
-
-    //    cerr<<"got DS for ["<<qname<<"], grabbing DNSKEYs"<<endl;
-    auto records=dro.get(*zoneCutIter, (uint16_t)QType::DNSKEY);
-    // this should use harvest perhaps
-    for(const auto& rec : records) {
-      if(rec.d_name != *zoneCutIter)
-        continue;
-
-      if(rec.d_type == QType::RRSIG)
-      {
-        auto rrc=getRR<RRSIGRecordContent> (rec);
-        if(rrc) {
-          LOG("Got signature: "<<rrc->getZoneRepresentation()<<" with tag "<<rrc->d_tag<<", for type "<<DNSRecordContent::NumberToType(rrc->d_type)<<endl);
-          if(rrc->d_type != QType::DNSKEY)
-            continue;
-          sigs.push_back(rrc);
-        }
-      }
-      else if(rec.d_type == QType::DNSKEY)
-      {
-        auto drc=getRR<DNSKEYRecordContent> (rec);
-        if(drc) {
-          tkeys.insert(drc);
-          LOG("Inserting key with tag "<<drc->getTag()<<" and algorithm "<<DNSSECKeeper::algorithm2name(drc->d_algorithm)<<": "<<drc->getZoneRepresentation()<<endl);
-
-          toSign.insert(rec.d_content);
-        }
-      }
-    }
-    LOG("got "<<tkeys.size()<<" keys and "<<sigs.size()<<" sigs from server"<<endl);
-
-    /*
-     * Check all DNSKEY records against all DS records and place all DNSKEY records
-     * that have DS records (that we support the algo for) in the tentative key storage
-     */
-    auto state = validateDNSKeysAgainstDS(time(nullptr), *zoneCutIter, dsmap, tkeys, toSign, sigs, validkeys);
-
-    if (validkeys.empty())
-    {
-      LOG("ended up with zero valid DNSKEYs, going Bogus"<<endl);
-      return state;
-    }
-    LOG("situation: we have one or more valid DNSKEYs for ["<<*zoneCutIter<<"] (want ["<<zone<<"])"<<endl);
-
-    if (zoneCutIter == zoneCuts.cend()-1) {
-      LOG("requested keyset found! returning Secure for the keyset"<<endl);
-      keyset.insert(validkeys.cbegin(), validkeys.cend());
-      return state;
-    }
-
-    // We now have the DNSKEYs, use them to validate the DS records at the next zonecut
-    LOG("next name ["<<*(zoneCutIter+1)<<"], trying to get DS"<<endl);
-
-    dsmap_t tdsmap; // tentative DSes
-    dsmap.clear();
-    toSign.clear();
-
-    auto recs=dro.get(*(zoneCutIter+1), QType::DS);
-
-    cspmap_t cspmap=harvestCSPFromRecs(recs);
-
-    cspmap_t validrrsets;
-    validateWithKeySet(cspmap, validrrsets, validkeys);
-
-    LOG("got "<<cspmap.count(pair(*(zoneCutIter+1),QType::DS))<<" records for DS query of which "<<validrrsets.count(pair(*(zoneCutIter+1),QType::DS))<<" valid "<<endl);
-
-    auto r = validrrsets.equal_range(pair(*(zoneCutIter+1), QType::DS));
-    if(r.first == r.second) {
-      LOG("No DS for "<<*(zoneCutIter+1)<<", now look for a secure denial"<<endl);
-      dState res = getDenial(validrrsets, *(zoneCutIter+1), QType::DS, true, true);
-      if (res == dState::INSECURE || res == dState::NXDOMAIN)
-        return vState::BogusInvalidDenial;
-      if (res == dState::NXQTYPE || res == dState::OPTOUT)
-        return vState::Insecure;
-    }
-
-    /*
-     * Collect all DS records and add them to the dsmap for the next iteration
-     */
-    for(auto cspiter =r.first;  cspiter!=r.second; cspiter++) {
-      for(auto j=cspiter->second.records.cbegin(); j!=cspiter->second.records.cend(); j++)
-      {
-        const auto dsrc=std::dynamic_pointer_cast<DSRecordContent>(*j);
-        if(dsrc) {
-          dsmap.insert(*dsrc);
-        }
-      }
-    }
-  }
-  // There were no zone cuts (aka, we should never get here)
-  return vState::BogusUnableToGetDNSKEYs;
-}
-
-bool isSupportedDS(const DSRecordContent& ds)
-{
-  if (!DNSCryptoKeyEngine::isAlgorithmSupported(ds.d_algorithm)) {
-    LOG("Discarding DS "<<ds.d_tag<<" because we don't support algorithm number "<<std::to_string(ds.d_algorithm)<<endl);
+  if (!DNSCryptoKeyEngine::isAlgorithmSupported(dsrec.d_algorithm)) {
+    LOG("Discarding DS "<<dsrec.d_tag<<" because we don't support algorithm number "<<std::to_string(dsrec.d_algorithm)<<endl);
     return false;
   }
 
-  if (!DNSCryptoKeyEngine::isDigestSupported(ds.d_digesttype)) {
-    LOG("Discarding DS "<<ds.d_tag<<" because we don't support digest number "<<std::to_string(ds.d_digesttype)<<endl);
+  if (!DNSCryptoKeyEngine::isDigestSupported(dsrec.d_digesttype)) {
+    LOG("Discarding DS "<<dsrec.d_tag<<" because we don't support digest number "<<std::to_string(dsrec.d_digesttype)<<endl);
     return false;
   }
 
@@ -1420,7 +1351,7 @@ DNSName getSigner(const std::vector<std::shared_ptr<RRSIGRecordContent> >& signa
     }
   }
 
-  return DNSName();
+  return {};
 }
 
 const std::string& vStateToString(vState state)
@@ -1429,17 +1360,17 @@ const std::string& vStateToString(vState state)
   return vStates.at(static_cast<size_t>(state));
 }
 
-std::ostream& operator<<(std::ostream &os, const vState d)
+std::ostream& operator<<(std::ostream &ostr, const vState dstate)
 {
-  os<<vStateToString(d);
-  return os;
+  ostr<<vStateToString(dstate);
+  return ostr;
 }
 
-std::ostream& operator<<(std::ostream &os, const dState d)
+std::ostream& operator<<(std::ostream &ostr, const dState dstate)
 {
   static const std::vector<std::string> dStates = {"no denial", "inconclusive", "nxdomain", "nxqtype", "empty non-terminal", "insecure", "opt-out"};
-  os<<dStates.at(static_cast<size_t>(d));
-  return os;
+  ostr<<dStates.at(static_cast<size_t>(dstate));
+  return ostr;
 }
 
 void updateDNSSECValidationState(vState& state, const vState stateUpdate)
@@ -1450,10 +1381,7 @@ void updateDNSSECValidationState(vState& state, const vState stateUpdate)
   else if (stateUpdate == vState::NTA) {
     state = vState::Insecure;
   }
-  else if (vStateIsBogus(stateUpdate)) {
-    state = stateUpdate;
-  }
-  else if (state == vState::Indeterminate) {
+  else if (vStateIsBogus(stateUpdate) || state == vState::Indeterminate) {
     state = stateUpdate;
   }
   else if (stateUpdate == vState::Insecure) {
index ec11f0b27b36b4a66f76bebab2dd27ea1c103708..0f80b7dffc0f25ed4f4c966169245275e4774caf 100644 (file)
  */
 #pragma once
 
+#include <tuple>
+
 #include "dnsparser.hh"
 #include "dnsname.hh"
 #include <vector>
 #include "namespaces.hh"
 #include "dnsrecords.hh"
 #include "dnssecinfra.hh"
+
 extern bool g_dnssecLOG;
 extern time_t g_signatureInceptionSkew;
 extern uint16_t g_maxNSEC3Iterations;
+extern uint16_t g_maxRRSIGsPerRecordToConsider;
+extern uint16_t g_maxNSEC3sPerRecordToConsider;
+extern uint16_t g_maxDNSKEYsToConsider;
+extern uint16_t g_maxDSsToConsider;
 
 // 4033 5
 enum class vState : uint8_t { Indeterminate, Insecure, Secure, NTA, TA, BogusNoValidDNSKEY, BogusInvalidDenial, BogusUnableToGetDSs, BogusUnableToGetDNSKEYs, BogusSelfSignedDS, BogusNoRRSIG, BogusNoValidRRSIG, BogusMissingNegativeIndication, BogusSignatureNotYetValid, BogusSignatureExpired, BogusUnsupportedDNSKEYAlgo, BogusUnsupportedDSDigestType, BogusNoZoneKeyBitSet, BogusRevokedDNSKEY, BogusInvalidDNSKEYProtocol };
@@ -72,8 +78,30 @@ struct sharedDNSKeyRecordContentCompare
 
 typedef set<shared_ptr<DNSKEYRecordContent>, sharedDNSKeyRecordContentCompare > skeyset_t;
 
+namespace pdns
+{
+namespace validation
+{
+using Nsec3HashesCache = std::map<std::tuple<DNSName, std::string, uint16_t>, std::string>;
+
+struct ValidationContext
+{
+  Nsec3HashesCache d_nsec3Cache;
+  unsigned int d_validationsCounter{0};
+  unsigned int d_nsec3IterationsRemainingQuota{0};
+};
 
-vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& records, const vector<shared_ptr<RRSIGRecordContent> >& signatures, const skeyset_t& keys, bool validateAllSigs=true);
+class TooManySEC3IterationsException : public std::runtime_error
+{
+public:
+  TooManySEC3IterationsException(): std::runtime_error("Too many NSEC3 hash computations per query")
+  {
+  }
+};
+
+}
+}
+vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& records, const vector<shared_ptr<RRSIGRecordContent> >& signatures, const skeyset_t& keys, pdns::validation::ValidationContext& context, bool validateAllSigs=true);
 bool isCoveredByNSEC(const DNSName& name, const DNSName& begin, const DNSName& next);
 bool isCoveredByNSEC3Hash(const std::string& h, const std::string& beginHash, const std::string& nextHash);
 bool isCoveredByNSEC3Hash(const DNSName& h, const DNSName& beginHash, const DNSName& nextHash);
@@ -82,11 +110,11 @@ cspmap_t harvestCSPFromRecs(const vector<DNSRecord>& recs);
 vState getKeysFor(DNSRecordOracle& dro, const DNSName& zone, skeyset_t& keyset);
 bool getTrustAnchor(const map<DNSName,dsmap_t>& anchors, const DNSName& zone, dsmap_t &res);
 bool haveNegativeTrustAnchor(const map<DNSName,std::string>& negAnchors, const DNSName& zone, std::string& reason);
-vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector<shared_ptr<RRSIGRecordContent> >& sigs, skeyset_t& validkeys);
-dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, bool needsWildcardProof=true, unsigned int wildcardLabelsCount=0);
+vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector<shared_ptr<RRSIGRecordContent> >& sigs, skeyset_t& validkeys, pdns::validation::ValidationContext& context);
+dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, pdns::validation::ValidationContext& context, bool needsWildcardProof=true, unsigned int wildcardLabelsCount=0);
 bool isSupportedDS(const DSRecordContent& ds);
 DNSName getSigner(const std::vector<std::shared_ptr<RRSIGRecordContent> >& signatures);
-bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>& dsrecords);
+bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>& dsrecords, pdns::validation::ValidationContext& context);
 bool isRRSIGNotExpired(const time_t now, const std::shared_ptr<RRSIGRecordContent>& sig);
 bool isRRSIGIncepted(const time_t now, const shared_ptr<RRSIGRecordContent>& sig);
 bool isWildcardExpanded(unsigned int labelCount, const std::shared_ptr<RRSIGRecordContent>& sign);
@@ -98,6 +126,8 @@ dState matchesNSEC(const DNSName& name, uint16_t qtype, const DNSName& nsecOwner
 bool isNSEC3AncestorDelegation(const DNSName& signer, const DNSName& owner, const std::shared_ptr<NSEC3RecordContent>& nsec3);
 DNSName getNSECOwnerName(const DNSName& initialOwner, const std::vector<std::shared_ptr<RRSIGRecordContent> >& signatures);
 DNSName getClosestEncloserFromNSEC(const DNSName& name, const DNSName& owner, const DNSName& next);
+[[nodiscard]] uint64_t getNSEC3DenialProofWorstCaseIterationsCount(uint8_t maxLabels, uint16_t iterations, size_t saltLength);
+[[nodiscard]] std::string getHashFromNSEC3(const DNSName& qname, uint16_t iterations, const std::string& salt, pdns::validation::ValidationContext& context);
 
 template <typename NSEC> bool isTypeDenied(const NSEC& nsec, const QType& type)
 {
index a4640176c69df64241f9d6321345128d79dfed6c..1e8e47cde795e76cb74c934436ee73fbf39a9289 100644 (file)
@@ -14,6 +14,8 @@ class AggressiveNSECCacheBase(RecursorTest):
     _config_template = """
     dnssec=validate
     aggressive-nsec-cache-size=10000
+    aggressive-cache-max-nsec3-hash-cost=204
+    nsec3-max-iterations=150
     webserver=yes
     webserver-port=%d
     webserver-address=127.0.0.1