]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
rec: CVE-2023-50387 and CVE-2023-50868 13781/head
authorOtto Moerbeek <otto.moerbeek@open-xchange.com>
Fri, 9 Feb 2024 12:28:00 +0000 (13:28 +0100)
committerOtto Moerbeek <otto.moerbeek@open-xchange.com>
Fri, 9 Feb 2024 12:28:00 +0000 (13:28 +0100)
16 files changed:
pdns/recursordist/aggressive_nsec.cc
pdns/recursordist/aggressive_nsec.hh
pdns/recursordist/pdns_recursor.cc
pdns/recursordist/rec-main.cc
pdns/recursordist/rec-zonetocache.cc
pdns/recursordist/settings/table.py
pdns/recursordist/syncres.cc
pdns/recursordist/syncres.hh
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/validate.cc
pdns/validate.hh
regression-tests.recursor-dnssec/test_AggressiveNSECCache.py

index fecebc0d689a788de8d426873984a0b8c50edd62..9cd729a0b807673773249247ff20fdd81775902d 100644 (file)
@@ -29,6 +29,7 @@
 #include "validate.hh"
 
 std::unique_ptr<AggressiveNSECCache> g_aggressiveNSECCache{nullptr};
+uint64_t AggressiveNSECCache::s_nsec3DenialProofMaxCost{0};
 uint8_t AggressiveNSECCache::s_maxNSEC3CommonPrefix = AggressiveNSECCache::s_default_maxNSEC3CommonPrefix;
 
 /* this is defined in syncres.hh and we are not importing that here */
@@ -541,7 +542,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<const RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, const OptLog& log)
+bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded<AggressiveNSECCache::ZoneEntry>>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<const RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, const OptLog& log, pdns::validation::ValidationContext& validationContext)
 {
   DNSName zone;
   std::string salt;
@@ -557,7 +558,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"
+      VLOG(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)) {
@@ -604,8 +615,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)) {
       VLOG(log, name << ": Found closest encloser at " << closestEncloser << " (" << closestHash << ")" << endl);
@@ -653,7 +666,7 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded
   DNSName nsecFound;
   DNSName nextCloser(closestEncloser);
   nextCloser.prependRawLabel(name.getRawLabel(labelIdx - 1));
-  auto nextCloserHash = toBase32Hex(hashQNameWithSalt(salt, iterations, nextCloser));
+  auto nextCloserHash = toBase32Hex(getHashFromNSEC3(nextCloser, iterations, salt, validationContext));
   VLOG(log, name << ": Looking for a NSEC3 covering the next closer " << nextCloser << " (" << nextCloserHash << ")" << endl);
 
   ZoneEntry::CacheEntry nextCloserEntry;
@@ -682,7 +695,7 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded
   /* An ancestor NSEC3 would be fine here, since it does prove that there is no delegation at the next closer
      name (we don't insert opt-out NSEC3s into the cache). */
   DNSName wildcard(g_wildcarddnsname + closestEncloser);
-  auto wcHash = toBase32Hex(hashQNameWithSalt(salt, iterations, wildcard));
+  auto wcHash = toBase32Hex(getHashFromNSEC3(wildcard, iterations, salt, validationContext));
   VLOG(log, name << ": Looking for a NSEC3 covering the wildcard " << wildcard << " (" << wcHash << ")" << endl);
 
   ZoneEntry::CacheEntry wcEntry;
@@ -767,7 +780,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, const OptLog& log)
+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, const OptLog& log)
 {
   std::shared_ptr<LockGuarded<ZoneEntry>> zoneEntry;
   if (type == QType::DS) {
@@ -807,7 +820,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, log);
+    return getNSEC3Denial(now, zoneEntry, soaSet, soaSignatures, name, type, ret, res, doDNSSEC, log, validationContext);
   }
 
   ZoneEntry::CacheEntry entry;
index 57ed2a2202bfdaec8e24d7b1e3f96e68baa74ceb..6c7dfbb709023383208f1e092f0115505ff3f99d 100644 (file)
@@ -37,11 +37,13 @@ using namespace ::boost::multi_index;
 #include "lock.hh"
 #include "stat_t.hh"
 #include "logger.hh"
+#include "validate.hh"
 
 class AggressiveNSECCache
 {
 public:
-  static const uint8_t s_default_maxNSEC3CommonPrefix = 10;
+  static constexpr uint8_t s_default_maxNSEC3CommonPrefix = 10;
+  static uint64_t s_nsec3DenialProofMaxCost;
   static uint8_t s_maxNSEC3CommonPrefix;
 
   AggressiveNSECCache(uint64_t entries) :
@@ -60,7 +62,7 @@ public:
   }
 
   void insertNSEC(const DNSName& zone, const DNSName& owner, const DNSRecord& record, const std::vector<std::shared_ptr<const 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, const OptLog& log = std::nullopt);
+  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, const OptLog& log = std::nullopt);
 
   void removeZoneInfo(const DNSName& zone, bool subzones);
 
@@ -150,7 +152,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<const RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, const OptLog&);
+  bool getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded<ZoneEntry>>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<const RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, const OptLog&, 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, const OptLog&);
   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, const OptLog&);
 
index 9f9c8eeacf1d847f89bf794bd4a641210c1a4756..5071a38787c5ee4b1351d2f0430dd2d5354292e2 100644 (file)
@@ -568,6 +568,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 " << comboWriter->getRemote() << " during resolve of the custom filter policy '" << appliedPolicy.getName() << "' while resolving '" << comboWriter->d_mdp.d_qname << "' because: " << e.what() << endl,
+                 resolver.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 " << comboWriter->getRemote() << " during resolve of the custom filter policy '" << appliedPolicy.getName() << "' while resolving '" << comboWriter->d_mdp.d_qname << "' because another RPZ policy was hit" << endl,
@@ -1270,6 +1279,13 @@ void startDoResolve(void* arg) // NOLINT(readability-function-cognitive-complexi
         }
         res = RCode::ServFail;
       }
+      catch (const pdns::validation::TooManySEC3IterationsException& e) {
+        if (g_logCommonErrors) {
+          SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << comboWriter->getRemote() << " during resolve of '" << comboWriter->d_mdp.d_qname << "' because: " << e.what() << endl,
+               resolver.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during resolve"));
+        }
+        res = RCode::ServFail;
+      }
       catch (const SendTruncatedAnswerException& e) {
         ret.clear();
         resolver.d_appliedPolicy.addSOAtoRPZResult(ret);
@@ -1449,6 +1465,13 @@ void startDoResolve(void* arg) // NOLINT(readability-function-cognitive-complexi
           }
           goto sendit; // NOLINT(cppcoreguidelines-avoid-goto)
         }
+        catch (const pdns::validation::TooManySEC3IterationsException& e) {
+          if (g_logCommonErrors) {
+            SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << comboWriter->getRemote() << " during validation of '" << comboWriter->d_mdp.d_qname << "|" << QType(comboWriter->d_mdp.d_qtype) << "' because: " << e.what() << endl,
+                 resolver.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during validation", "exception", Logging::Loggable("TooManySEC3IterationsException")));
+          }
+          goto sendit; // NOLINT(cppcoreguidelines-avoid-goto)
+        }
       }
 
       if (!ret.empty()) {
index a435f3a0d37a8f61e30382c8283be9ef26397f84..54d117bd7315869e5a976fdbe5714bf68c2792b6 100644 (file)
@@ -1565,6 +1565,10 @@ static int initDNSSEC(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");
 
   vector<string> nums;
   bool automatic = true;
@@ -1654,6 +1658,8 @@ static int initSyncRes(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");
@@ -2170,6 +2176,7 @@ static int serviceMain(Logr::log_t log)
     }
   }
 
+  AggressiveNSECCache::s_nsec3DenialProofMaxCost = ::arg().asNum("aggressive-cache-max-nsec3-hash-cost");
   AggressiveNSECCache::s_maxNSEC3CommonPrefix = static_cast<uint8_t>(std::round(std::log2(::arg().asNum("aggressive-cache-min-nsec3-hit-ratio"))));
   SLOG(g_log << Logger::Debug << "NSEC3 aggressive cache tuning: aggressive-cache-min-nsec3-hit-ratio: " << ::arg().asNum("aggressive-cache-min-nsec3-hit-ratio") << " max common prefix bits: " << std::to_string(AggressiveNSECCache::s_maxNSEC3CommonPrefix) << endl,
        log->info(Logr::Debug, "NSEC3 aggressive cache tuning", "aggressive-cache-min-nsec3-hit-ratio", Logging::Loggable(::arg().asNum("aggressive-cache-min-nsec3-hit-ratio")), "maxCommonPrefixBits", Logging::Loggable(AggressiveNSECCache::s_maxNSEC3CommonPrefix)));
index a8692a987833eee213108907bc9c29035e0276b4..8d4ab260bf47499c6b59bcb0033cb60d1b410559 100644 (file)
@@ -250,6 +250,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 resolver({d_now, 0});
@@ -273,7 +275,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, std::nullopt);
+  vState dnsKeyState = validateDNSKeysAgainstDS(d_now, d_zone, dsmap, dnsKeys, records, zonemd.getRRSIGs(), validKeys, std::nullopt, validationContext);
   if (dnsKeyState != vState::Secure) {
     return dnsKeyState;
   }
@@ -295,7 +297,7 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const
 
     if (!nsecs.records.empty() && !nsecs.signatures.empty()) {
       // Valdidate the NSEC
-      nsecValidationStatus = validateWithKeySet(d_now, d_zone, nsecs.records, nsecs.signatures, validKeys, std::nullopt);
+      nsecValidationStatus = validateWithKeySet(d_now, d_zone, nsecs.records, nsecs.signatures, validKeys, std::nullopt, validationContext);
       csp.emplace(std::pair(d_zone, QType::NSEC), nsecs);
     }
     else if (!nsec3s.records.empty() && !nsec3s.signatures.empty()) {
@@ -304,13 +306,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, std::nullopt);
+      nsecValidationStatus = validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys, std::nullopt, validationContext);
       if (nsecValidationStatus != vState::Secure) {
         d_log->info(Logr::Warning, "NSEC3PARAMS records did not validate");
         return nsecValidationStatus;
       }
       // Valdidate the NSEC3
-      nsecValidationStatus = validateWithKeySet(d_now, zonemd.getNSEC3Label(), nsec3s.records, nsec3s.signatures, validKeys, std::nullopt);
+      nsecValidationStatus = validateWithKeySet(d_now, zonemd.getNSEC3Label(), nsec3s.records, nsec3s.signatures, validKeys, std::nullopt, validationContext);
       csp.emplace(std::pair(zonemd.getNSEC3Label(), QType::NSEC3), nsec3s);
     }
     else {
@@ -323,7 +325,7 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const
       return nsecValidationStatus;
     }
 
-    auto denial = getDenial(csp, d_zone, QType::ZONEMD, false, false, std::nullopt, true);
+    auto denial = getDenial(csp, d_zone, QType::ZONEMD, false, false, validationContext, std::nullopt, true);
     if (denial == dState::NXQTYPE) {
       d_log->info(Logr::Info, "Validated denial of existence of ZONEMD record");
       return vState::Secure;
@@ -337,7 +339,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, std::nullopt);
+  return validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys, std::nullopt, validationContext);
 }
 
 void ZoneData::ZoneToCache(const RecZoneToCache::Config& config)
index a9de993c39768acc921507f0e553335babed4b16..5028b4c5f482f9754a851c51b5bda3a6f0d580a3 100644 (file)
@@ -1395,7 +1395,7 @@ In that case no probe will be scheduled.
 
 .. note::
   DoT probing is an experimental feature.
-  Please test thoroughly to determine if it is suitable in your specific production environment before enabling. 
+  Please test thoroughly to determine if it is suitable in your specific production environment before enabling.
  ''',
     'versionadded': '4.7.0'
     },
@@ -1865,6 +1865,83 @@ If an answer containing an NSEC3 record with more iterations is received, its DN
         'versionchanged': [('4.5.2', 'Default is now 150, was 2500 before.'),
                            ('5.0.0', 'Default is now 50, was 150 before.')]
     },
+    {
+        'name' : 'max_rrsigs_per_record',
+        'section' : 'dnssec',
+        'type' : LType.Uint64,
+        'default' : '2',
+        'help' : 'Maximum number of RRSIGs to consider when validating a given record',
+        'doc' : '''
+Maximum number of RRSIGs we are willing to cryptographically check when validating a given record. Expired or not yet incepted RRSIGs do not count toward to this limit.
+ ''',
+        'versionadded': ['5.0.2', '4.9.3', '4.8.6'],
+    },
+    {
+        'name' : 'max_nsec3s_per_record',
+        'section' : 'dnssec',
+        'type' : LType.Uint64,
+        'default' : '10',
+        'help' : 'Maximum number of NSEC3s to consider when validating a given denial of existence',
+        'doc' : '''
+Maximum number of NSEC3s to consider when validating a given denial of existence.
+ ''',
+        'versionadded': ['5.0.2', '4.9.3', '4.8.6'],
+    },
+    {
+        'name' : 'max_signature_validations_per_query',
+        'section' : 'dnssec',
+        'type' : LType.Uint64,
+        'default' : '30',
+        'help' : 'Maximum number of RRSIG signatures we are willing to validate per incoming query',
+        'doc' : '''
+Maximum number of RRSIG signatures we are willing to validate per incoming query.
+ ''',
+        'versionadded': ['5.0.2', '4.9.3', '4.8.6'],
+    },
+    {
+        'name' : 'max_nsec3_hash_computations_per_query',
+        'section' : 'dnssec',
+        'type' : LType.Uint64,
+        'default' : '600',
+        'help' : 'Maximum number of NSEC3 hashes that we are willing to compute during DNSSEC validation, per incoming query',
+        'doc' : '''
+Maximum number of NSEC3 hashes that we are willing to compute during DNSSEC validation, per incoming query.
+ ''',
+        'versionadded': ['5.0.2', '4.9.3', '4.8.6'],
+    },
+    {
+        'name' : 'aggressive_cache_max_nsec3_hash_cost',
+        'section' : 'dnssec',
+        'type' : LType.Uint64,
+        'default' : '150',
+        'help' : 'Maximum estimated NSEC3 cost for a given query to consider aggressive use of the NSEC3 cache',
+        'doc' : '''
+Maximum estimated NSEC3 cost for a given query to consider aggressive use of the NSEC3 cache. The cost is estimated based on a heuristic taking the zone's NSEC3 salt and iterations parameters into account, as well at the number of labels of the requested name. For example a query for a name like a.b.c.d.e.f.example.com. in an example.com zone. secured with NSEC3 and 10 iterations (NSEC3 iterations count of 9) and an empty salt will have an estimated worst-case cost of 10 (iterations) * 6 (number of labels) = 60. The aggressive NSEC cache is an optimization to reduce the number of queries to authoritative servers, which is especially useful when a zone is under pseudo-random subdomain attack, and we want to skip it the zone parameters make it expensive.
+''',
+        'versionadded': ['5.0.2', '4.9.3', '4.8.6'],
+    },
+    {
+        'name' : 'max_ds_per_zone',
+        'section' : 'dnssec',
+        'type' : LType.Uint64,
+        'default' : '8',
+        'help' : 'Maximum number of DS records to consider per zone',
+        'doc' : '''
+Maximum number of DS records to consider when validating records inside a zone..
+ ''',
+        'versionadded': ['5.0.2', '4.9.3', '4.8.6'],
+    },
+    {
+        'name' : 'max_dnskeys',
+        'section' : 'dnssec',
+        'type' : LType.Uint64,
+        'default' : '2',
+        'help' : 'Maximum number of DNSKEYs with the same algorithm and tag to consider when validating a given record',
+        'doc' : '''
+Maximum number of DNSKEYs with the same algorithm and tag to consider when validating a given record. Setting this value to 1 effectively denies DNSKEY tag collisions in a zone.
+ ''',
+        'versionadded': ['5.0.2', '4.9.3', '4.8.6'],
+    },
     {
         'name' : 'ttl',
         'section' : 'packetcache',
@@ -2270,7 +2347,7 @@ Query example (where 192.0.2.14 is your server):
         'help' : 'If set, change group id to this gid for more security',
         'doc' : '''
 PowerDNS can change its user and group id after binding to its socket.
-Can be used for better :doc:`security <security>`. 
+Can be used for better :doc:`security <security>`.
  '''
     },
     {
index acde0be1de4b134ffd1fa3cba9ab708f0f1068ca..48c4acc2b3cdef18d98ed176ff8dfb3a62c34206 100644 (file)
@@ -446,6 +446,8 @@ unsigned int SyncRes::s_unthrottle_n;
 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_ecsqueries;
 pdns::stat_t SyncRes::s_ecsresponses;
 std::map<uint8_t, pdns::stat_t> SyncRes::s_ecsResponsesBySubnetSize4;
@@ -539,8 +541,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_fixednow(now), 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);
@@ -3044,7 +3046,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, LogObject(prefix))) {
+    if (g_aggressiveNSECCache->getDenial(d_now.tv_sec, qname, qtype, ret, res, d_cacheRemote, d_routingTag, d_doDNSSEC, d_validationContext, LogObject(prefix))) {
       context.state = vState::Secure;
       if (s_addExtendedResolutionDNSErrors) {
         context.extendedError = EDNSExtendedError{static_cast<uint16_t>(EDNSExtendedError::code::Synthesized), "Result synthesized from aggressive NSEC cache (RFC8198)"};
@@ -3717,7 +3719,7 @@ vState SyncRes::getDSRecords(const DNSName& zone, dsmap_t& dsMap, bool onlyTA, u
          - 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 != nullptr) {
@@ -3882,7 +3884,11 @@ vState SyncRes::validateDNSKeys(const DNSName& zone, const std::vector<DNSRecord
 
   LOG(prefix << zone << ": Trying to validate " << std::to_string(tentativeKeys.size()) << " DNSKEYs with " << std::to_string(dsMap.size()) << " DS" << endl);
   skeyset_t validatedKeys;
-  auto state = validateDNSKeysAgainstDS(d_now.tv_sec, zone, dsMap, tentativeKeys, toSign, signatures, validatedKeys, LogObject(prefix));
+  auto state = validateDNSKeysAgainstDS(d_now.tv_sec, zone, dsMap, tentativeKeys, toSign, signatures, validatedKeys, LogObject(prefix), 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(prefix << zone << ": We now have " << std::to_string(validatedKeys.size()) << " DNSKEYs" << endl);
 
@@ -4043,7 +4049,11 @@ vState SyncRes::validateRecordsWithSigs(unsigned int depth, const string& prefix
   }
 
   LOG(prefix << name << ": 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, LogObject(prefix), false);
+  vState state = validateWithKeySet(d_now.tv_sec, name, recordcontents, signatures, keys, LogObject(prefix), 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(prefix << name << ": Secure!" << endl);
     return vState::Secure;
@@ -4745,7 +4755,7 @@ void SyncRes::updateDenialValidationState(const DNSName& qname, vState& neValida
 dState SyncRes::getDenialValidationState(const NegCache::NegCacheEntry& negEntry, const dState expectedState, bool referralToUnsigned, const string& prefix)
 {
   cspmap_t csp = harvestCSPFromNE(negEntry);
-  return getDenial(csp, negEntry.d_name, negEntry.d_qtype.getCode(), referralToUnsigned, expectedState == dState::NXQTYPE, LogObject(prefix));
+  return getDenial(csp, negEntry.d_name, negEntry.d_qtype.getCode(), referralToUnsigned, expectedState == dState::NXQTYPE, d_validationContext, LogObject(prefix));
 }
 
 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) // // NOLINT(readability-function-cognitive-complexity)
@@ -4910,7 +4920,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(negEntry);
-            dState res = getDenial(csp, qname, negEntry.d_qtype.getCode(), false, false, LogObject(prefix), false, wildcardLabelsCount);
+            dState res = getDenial(csp, qname, negEntry.d_qtype.getCode(), false, false, d_validationContext, LogObject(prefix), false, wildcardLabelsCount);
             if (res != dState::NXDOMAIN) {
               vState tmpState = vState::BogusInvalidDenial;
               if (res == dState::INSECURE || res == dState::OPTOUT) {
index 0c9e5f8daee95ebe68cbd1eb96bb853d2bd81df3..ac5cb196ec21b60b82384afa8f36da01da0d1429 100644 (file)
@@ -520,8 +520,9 @@ public:
   static unsigned int s_nonresolvingnsmaxfails;
   static unsigned int s_nonresolvingnsthrottletime;
   static unsigned int s_unthrottle_n;
-
   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;
@@ -698,6 +699,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};
   // d_now is initialized in the constructor and updates after outgoing requests in lwres.cc:asyncresolve
   struct timeval d_now;
index fcc3a81fdadb09457bb944aae417c9f6ee589bf2..7290c7319424971f21208802ea73e2b47fd2a006 100644 (file)
@@ -1320,6 +1320,22 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec_dump)
   free(line); // NOLINT: it's the API.
 }
 
+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,
@@ -1375,12 +1391,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 */
@@ -1407,10 +1420,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");
@@ -1437,10 +1450,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)
@@ -1488,15 +1501,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 (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);
   }
 
   {
@@ -1521,14 +1528,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);
   }
 
   {
@@ -1553,16 +1555,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);
   }
 
   {
@@ -1611,17 +1606,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);
   }
 }
 
@@ -1680,15 +1668,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);
   }
 
   {
@@ -1719,14 +1701,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);
   }
 
   {
@@ -1757,16 +1734,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);
   }
 
   {
@@ -1861,17 +1831,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 */
@@ -1966,15 +1928,167 @@ 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)
+{
+  AggressiveNSECCache::s_maxNSEC3CommonPrefix = 159;
+  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.setContent(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.setContent(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.setContent(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.setContent(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 84c999ee653bfcb15c92c8593c2d44e59e8512e9..0aa0ceccc2389d6f5af8c47cdbb2cda21fadcbc3 100644 (file)
@@ -333,9 +333,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) {
@@ -345,16 +363,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.setContent(std::make_shared<RRSIGRecordContent>(rrc));
   records.push_back(rec);
index 2f21dd32b0103f121ce95d8c82478dbef651a731..7e5db3c57b3d1a6fb998593c45ffc8867dfee272 100644 (file)
@@ -442,7 +442,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.setKey(std::move(dcke), 256);
 
@@ -462,7 +461,9 @@ BOOST_AUTO_TEST_CASE(test_dnssec_rrsig)
   std::vector<std::shared_ptr<const RRSIGRecordContent>> sigs;
   sigs.push_back(std::make_shared<RRSIGRecordContent>(rrc));
 
-  BOOST_CHECK(validateWithKeySet(now, qname, recordcontents, sigs, keyset, std::nullopt) == vState::Secure);
+  pdns::validation::ValidationContext validationContext;
+  BOOST_CHECK(validateWithKeySet(now, qname, recordcontents, sigs, keyset, std::nullopt, validationContext) == vState::Secure);
+  BOOST_CHECK_EQUAL(validationContext.d_validationsCounter, 1U);
 }
 
 BOOST_AUTO_TEST_CASE(test_dnssec_root_validation_csk)
@@ -835,6 +836,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;
@@ -858,10 +860,10 @@ BOOST_AUTO_TEST_CASE(test_dnssec_bogus_dnskey_doesnt_match_ds)
   dcke->create(dcke->getBits());
   DNSSECPrivateKey dpk;
   dpk.setKey(std::move(dcke), 256);
-  DSRecordContent uselessdrc = makeDSFromDNSKey(target, dpk.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
+  DSRecordContent seconddrc = makeDSFromDNSKey(target, dpk.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
 
   dskeys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dskey, drc);
-  keys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dpk, uselessdrc);
+  keys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dpk, seconddrc);
 
   /* Set the root DS */
   auto luaconfsCopy = g_luaconfs.getCopy();
@@ -955,6 +957,283 @@ 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), 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.setKey(std::move(dcke), 256);
+  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), 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), 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;
@@ -1415,6 +1694,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 7e97556b38a6f7fa8231cc332e17c17defb7d8d3..85441f79ebf117eb3d2a6d9e07ce911368cb4fc1 100644 (file)
@@ -1237,6 +1237,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 7306fbaba22b4b35beb2978be9e6aa777d083ab3..901c8b7784cc2071f95af85f54b7c2b64eca2d0e 100644 (file)
@@ -8,6 +8,13 @@
 
 BOOST_AUTO_TEST_SUITE(syncres_cc8)
 
+static dState getDenial(const cspmap_t& validrrsets, const DNSName& qname, uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, const OptLog& log = std::nullopt, 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, log, needWildcardProof, wildcardLabelsCount);
+}
+
 BOOST_AUTO_TEST_CASE(test_nsec_denial_nowrap)
 {
   initSR();
@@ -415,8 +422,8 @@ BOOST_AUTO_TEST_CASE(test_nsec3_nxqtype_ds)
 
   sortedRecords_t recordContents;
   vector<shared_ptr<const 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).getContent());
   addRRSIG(keys, records, DNSName("powerdns.com."), 300);
   signatureContents.push_back(getRR<RRSIGRecordContent>(records.at(1)));
@@ -428,10 +435,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 decc2f3f64f8c7cc4626294447f1eeafc7ffc66a..16d144e643b0a4d9afdcf937f51464fda4bd94ab 100644 (file)
 
 time_t g_signatureInceptionSkew{0};
 uint16_t g_maxNSEC3Iterations{0};
+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)
 {
@@ -108,37 +112,47 @@ 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 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 != 0 && nsec3.d_iterations > g_maxNSEC3Iterations) {
+  if (g_maxNSEC3Iterations != 0 && iterations > g_maxNSEC3Iterations) {
     return result;
   }
 
-  auto key = std::tuple(qname, nsec3.d_salt, nsec3.d_iterations);
-  auto iter = cache.find(key);
-  if (iter != cache.end())
-  {
+  auto key = std::tuple(qname, salt, iterations);
+  auto iter = context.d_nsec3Cache.find(key);
+  if (iter != context.d_nsec3Cache.end()) {
     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) {
@@ -161,7 +175,12 @@ bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>&
         continue;
       }
 
-      const string hash = getHashFromNSEC3(zone, *nsec3, cache);
+      if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+        return false;
+      }
+      nsec3sConsidered++;
+
+      const string hash = getHashFromNSEC3(zone, *nsec3, context);
       if (hash.empty()) {
         return false;
       }
@@ -352,7 +371,7 @@ 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, const OptLog& log)
+static bool provesNSEC3NoWildCard(const DNSName& closestEncloser, uint16_t const qtype, const cspmap_t& validrrsets, bool* wildcardExists, const OptLog& log, pdns::validation::ValidationContext& context)
 {
   auto wildcard = g_wildcarddnsname + closestEncloser;
   VLOG(log, closestEncloser << ": Trying to prove that there is no wildcard for "<<wildcard<<"/"<<QType(qtype)<<endl);
@@ -372,8 +391,9 @@ static bool provesNSEC3NoWildCard(const DNSName& closestEncloser, uint16_t const
           continue;
         }
 
-        string hash = getHashFromNSEC3(wildcard, *nsec3, cache);
+        string hash = getHashFromNSEC3(wildcard, *nsec3, context);
         if (hash.empty()) {
+          VLOG(log, closestEncloser << ": Unsupported hash, ignoring"<<endl);
           return false;
         }
         VLOG(log, closestEncloser << ":\tWildcard hash: "<<toBase32Hex(hash)<<endl);
@@ -483,6 +503,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.
@@ -494,15 +519,15 @@ 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, const OptLog& log, bool needWildcardProof, unsigned int wildcardLabelsCount) // NOLINT(readability-function-cognitive-complexity): https://github.com/PowerDNS/pdns/issues/12791
+dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, pdns::validation::ValidationContext& context, const OptLog& log, 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");
   }
 
+  uint8_t numberOfLabelsOfParentZone{std::numeric_limits<uint8_t>::max()};
+  uint16_t nsec3sConsidered = 0;
   for (const auto& validset : validrrsets) {
     VLOG(log, qname << ": Do have: "<<validset.first.first<<"/"<<DNSRecordContent::NumberToType(validset.first.second)<<endl);
 
@@ -670,13 +695,20 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
           VLOG(log, qname << ": 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) {
           VLOG(log, qname << ": A NSEC3 RR from the child zone cannot deny the existence of a DS"<<endl);
           continue;
         }
 
-        string hash = getHashFromNSEC3(qname, *nsec3, cache);
+        if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+          VLOG(log, qname << ": Too many NSEC3s for this record"<<endl);
+          return dState::NODENIAL;
+        }
+        nsec3sConsidered++;
+
+        string hash = getHashFromNSEC3(qname, *nsec3, context);
         if (hash.empty()) {
           VLOG(log, qname << ": Unsupported hash, ignoring"<<endl);
           return dState::INSECURE;
@@ -746,15 +778,15 @@ 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
     */
     VLOG(log, qname << ": Now looking for the closest encloser for "<<qname<<endl);
 
-    while (!found && closestEncloser.chopOff()) {
+    while (!found && closestEncloser.chopOff() && closestEncloser.countLabels() >= numberOfLabelsOfParentZone) {
 
       for(const auto& validset : validrrsets) {
         if(validset.first.second==QType::NSEC3) {
@@ -771,8 +803,15 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
               continue;
             }
 
-            string hash = getHashFromNSEC3(closestEncloser, *nsec3, cache);
+            if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+              VLOG(log, qname << ": Too many NSEC3s for this record"<<endl);
+              return dState::NODENIAL;
+            }
+            nsec3sConsidered++;
+
+            string hash = getHashFromNSEC3(closestEncloser, *nsec3, context);
             if (hash.empty()) {
+              VLOG(log, qname << ": Unsupported hash, ignoring"<<endl);
               return dState::INSECURE;
             }
 
@@ -838,19 +877,27 @@ 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;
       VLOG(log, qname << ":Looking for a NSEC3 covering the next closer name "<<nextCloser<<endl);
 
-      for(const auto& validset : validrrsets) {
-        if(validset.first.second==QType::NSEC3) {
-          for(const auto& record : validset.second.records) {
+      for (const auto& validset : validrrsets) {
+        if (validset.first.second == QType::NSEC3) {
+          for (const auto& record : validset.second.records) {
             VLOG(log, qname << ":\t"<<record->getZoneRepresentation()<<endl);
             auto nsec3 = std::dynamic_pointer_cast<const NSEC3RecordContent>(record);
             if (!nsec3) {
               continue;
             }
 
-            string hash = getHashFromNSEC3(nextCloser, *nsec3, cache);
+            if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+              VLOG(log, qname << ": Too many NSEC3s for this record"<<endl);
+              return dState::NODENIAL;
+            }
+            nsec3sConsidered++;
+
+            string hash = getHashFromNSEC3(nextCloser, *nsec3, context);
             if (hash.empty()) {
+              VLOG(log, qname << ": Unsupported hash, ignoring"<<endl);
               return dState::INSECURE;
             }
 
@@ -888,7 +935,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, log)) {
+    if (needWildcardProof && !provesNSEC3NoWildCard(closestEncloser, qtype, validrrsets, &wildcardExists, log, context)) {
       if (!isOptOut) {
         VLOG(log, qname << ": But the existence of a wildcard is not denied for "<<qname<<"/"<<QType(qtype)<<endl);
         return dState::NODENIAL;
@@ -920,26 +967,31 @@ bool isRRSIGIncepted(const time_t now, const RRSIGRecordContent& sig)
   return sig.d_siginception - g_signatureInceptionSkew <= now;
 }
 
-static bool checkSignatureWithKey(const DNSName& qname, time_t now, const RRSIGRecordContent& sig, const DNSKEYRecordContent& key, const std::string& msg, vState& ede, const OptLog& log)
+namespace {
+[[nodiscard]] bool checkSignatureInceptionAndExpiry(const DNSName& qname, time_t now, const RRSIGRecordContent& sig, vState& ede, const OptLog& log)
+{
+  /* 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;
+  VLOG(log, qname << ": 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 RRSIGRecordContent& sig, const DNSKEYRecordContent& key, const std::string& msg, vState& ede, const OptLog& log)
 {
   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);
-      VLOG(log, qname << ": 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);
+    VLOG(log, qname << ": 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;
-      VLOG(log, qname << ": 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) {
     VLOG(log, qname << ": Could not make a validator for signature: "<<e.what()<<endl);
@@ -948,32 +1000,60 @@ static bool checkSignatureWithKey(const DNSName& qname, time_t now, const RRSIGR
   return result;
 }
 
-vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& toSign, const vector<shared_ptr<const RRSIGRecordContent> >& signatures, const skeyset_t& keys, const OptLog& log, bool validateAllSigs)
+}
+
+vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& toSign, const vector<shared_ptr<const RRSIGRecordContent> >& signatures, const skeyset_t& keys, const OptLog& log, 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) {
       VLOG(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, log)) {
+      if (isRRSIGIncepted(now, *signature)) {
+        noneIncepted = false;
+      }
+      if (isRRSIGNotExpired(now, *signature)) {
+        allExpired = false;
+      }
+      continue;
+    }
+
+    if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) {
+      VLOG(log, name<<": 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, log);
 
     if (keysMatchingTag.empty()) {
-      VLOG(log, name<<": No key provided for "<<signature->d_tag<<" and algorithm "<<std::to_string(signature->d_algorithm)<<endl;);
+      VLOG(log, name << ": 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 = vState::Indeterminate;
-      bool signIsValid = checkSignatureWithKey(name, now, *signature, *key, msg, ede, log);
-      foundKey = true;
+      if (g_maxDNSKEYsToConsider > 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) {
+        VLOG(log, name << ": 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, log);
 
       if (signIsValid) {
         isValid = true;
@@ -1000,7 +1080,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) {
@@ -1067,22 +1147,38 @@ bool haveNegativeTrustAnchor(const map<DNSName,std::string>& negAnchors, const D
   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<const RRSIGRecordContent> >& sigs, skeyset_t& validkeys, const OptLog& log)
+vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector<shared_ptr<const RRSIGRecordContent> >& sigs, skeyset_t& validkeys, const OptLog& log, 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)
-  {
+  uint16_t dssConsidered = 0;
+  for (const auto& dsrc : dsmap) {
+    if (g_maxDSsToConsider > 0 && dssConsidered > g_maxDSsToConsider) {
+      VLOG(log, zone << ": 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, log);
     // 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 : record)
-    {
+    for (const auto& drc : record) {
       bool isValid = false;
       bool dsCreated = false;
       DSRecordContent dsrc2;
+
+      if (g_maxDNSKEYsToConsider > 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) {
+        VLOG(log, zone << ": 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;
@@ -1110,14 +1206,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, log)) {
+        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, log);
 
@@ -1125,10 +1224,30 @@ vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t&
         continue;
       }
 
+      if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) {
+        VLOG(log, zone << ": 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) {
+          VLOG(log, zone << ": 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) {
+          VLOG(log, zone << ": 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(zone, now, *sig, *key, msg, ede, log);
+        bool signIsValid = checkSignatureWithKey(zone, *sig, *key, msg, ede, log);
+        signaturesConsidered++;
+        context.d_validationsCounter++;
 
         if (signIsValid) {
           VLOG(log, zone << ": Validation succeeded - whole DNSKEY set is valid"<<endl);
@@ -1137,6 +1256,11 @@ vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t&
         }
         VLOG(log, zone << ": 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;
     }
   }
index 9ca589572df5ed158273f7e678cf27c3c52bfbc1..7d844bf11c6d07ce66fe5137139a85512aa998cb 100644 (file)
 
 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 };
@@ -77,19 +81,39 @@ struct sharedDNSKeyRecordContentCompare
 
 using skeyset_t = set<shared_ptr<const DNSKEYRecordContent>, sharedDNSKeyRecordContentCompare>;
 
+namespace pdns::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};
+};
+
+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& toSign, const vector<shared_ptr<const RRSIGRecordContent> >& signatures, const skeyset_t& keys, const OptLog& log, bool validateAllSigs=true);
+vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& toSign, const vector<shared_ptr<const RRSIGRecordContent> >& signatures, const skeyset_t& keys, const OptLog& log, pdns::validation::ValidationContext& context, bool validateAllSigs=true);
 bool isCoveredByNSEC(const DNSName& name, const DNSName& begin, const DNSName& next);
 bool isCoveredByNSEC3Hash(const std::string& hash, const std::string& beginHash, const std::string& nextHash);
 bool isCoveredByNSEC3Hash(const DNSName& name, const DNSName& beginHash, const DNSName& nextHash);
 cspmap_t harvestCSPFromRecs(const vector<DNSRecord>& recs);
 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<const RRSIGRecordContent> >& sigs, skeyset_t& validkeys, const OptLog&);
-dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, const OptLog& log = std::nullopt, bool needWildcardProof=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<const RRSIGRecordContent> >& sigs, skeyset_t& validkeys, const OptLog&, pdns::validation::ValidationContext& context);
+dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, pdns::validation::ValidationContext& context, const OptLog& log = std::nullopt, bool needWildcardProof=true, unsigned int wildcardLabelsCount=0);
 bool isSupportedDS(const DSRecordContent& dsRecordContent, const OptLog&);
 DNSName getSigner(const std::vector<std::shared_ptr<const 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(time_t now, const RRSIGRecordContent& sig);
 bool isRRSIGIncepted(time_t now, const RRSIGRecordContent& sig);
 bool isWildcardExpanded(unsigned int labelCount, const RRSIGRecordContent& sign);
@@ -101,6 +125,8 @@ dState matchesNSEC(const DNSName& name, uint16_t qtype, const DNSName& nsecOwner
 bool isNSEC3AncestorDelegation(const DNSName& signer, const DNSName& owner, const NSEC3RecordContent& nsec3);
 DNSName getNSECOwnerName(const DNSName& initialOwner, const std::vector<std::shared_ptr<const 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 1e149af6acdd404f64e4f805c7046e4dd67a5831..88be1855002ef54d3e294fc7cf692df3a3c45847 100644 (file)
@@ -16,6 +16,7 @@ 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