From 15e973d6d882ed51fbad70dd082b11c7ad299466 Mon Sep 17 00:00:00 2001 From: Otto Moerbeek Date: Fri, 9 Feb 2024 13:28:00 +0100 Subject: [PATCH] rec: CVE-2023-50387 and CVE-2023-50868 --- pdns/recursordist/aggressive_nsec.cc | 29 +- pdns/recursordist/aggressive_nsec.hh | 8 +- pdns/recursordist/pdns_recursor.cc | 23 + pdns/recursordist/rec-main.cc | 7 + pdns/recursordist/rec-zonetocache.cc | 14 +- pdns/recursordist/settings/table.py | 81 +++- pdns/recursordist/syncres.cc | 24 +- pdns/recursordist/syncres.hh | 4 +- pdns/recursordist/test-aggressive_nsec_cc.cc | 280 ++++++++---- pdns/recursordist/test-syncres_cc.cc | 32 +- pdns/recursordist/test-syncres_cc4.cc | 429 +++++++++++++++++- pdns/recursordist/test-syncres_cc5.cc | 171 +++++++ pdns/recursordist/test-syncres_cc8.cc | 20 +- pdns/validate.cc | 246 +++++++--- pdns/validate.hh | 34 +- .../test_AggressiveNSECCache.py | 1 + 16 files changed, 1213 insertions(+), 190 deletions(-) diff --git a/pdns/recursordist/aggressive_nsec.cc b/pdns/recursordist/aggressive_nsec.cc index fecebc0d68..9cd729a0b8 100644 --- a/pdns/recursordist/aggressive_nsec.cc +++ b/pdns/recursordist/aggressive_nsec.cc @@ -29,6 +29,7 @@ #include "validate.hh" std::unique_ptr 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>& zoneEntry, std::vector& soaSet, std::vector>& soaSignatures, const DNSName& name, const QType& type, std::vector& ret, int& res, bool doDNSSEC, const OptLog& log) +bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr>& zoneEntry, std::vector& soaSet, std::vector>& soaSignatures, const DNSName& name, const QType& type, std::vector& 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_ptrd_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= 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& ret, int& res, const ComboAddress& who, const boost::optional& routingTag, bool doDNSSEC, const OptLog& log) +bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType& type, std::vector& ret, int& res, const ComboAddress& who, const boost::optional& routingTag, bool doDNSSEC, pdns::validation::ValidationContext& validationContext, const OptLog& log) { std::shared_ptr> 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; diff --git a/pdns/recursordist/aggressive_nsec.hh b/pdns/recursordist/aggressive_nsec.hh index 57ed2a2202..6c7dfbb709 100644 --- a/pdns/recursordist/aggressive_nsec.hh +++ b/pdns/recursordist/aggressive_nsec.hh @@ -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>& signatures, bool nsec3); - bool getDenial(time_t, const DNSName& name, const QType& type, std::vector& ret, int& res, const ComboAddress& who, const boost::optional& routingTag, bool doDNSSEC, const OptLog& log = std::nullopt); + bool getDenial(time_t, const DNSName& name, const QType& type, std::vector& ret, int& res, const ComboAddress& who, const boost::optional& 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> getBestZone(const DNSName& zone); bool getNSECBefore(time_t now, std::shared_ptr>& zoneEntry, const DNSName& name, ZoneEntry::CacheEntry& entry); bool getNSEC3(time_t now, std::shared_ptr>& zoneEntry, const DNSName& name, ZoneEntry::CacheEntry& entry); - bool getNSEC3Denial(time_t now, std::shared_ptr>& zoneEntry, std::vector& soaSet, std::vector>& soaSignatures, const DNSName& name, const QType& type, std::vector& ret, int& res, bool doDNSSEC, const OptLog&); + bool getNSEC3Denial(time_t now, std::shared_ptr>& zoneEntry, std::vector& soaSet, std::vector>& soaSignatures, const DNSName& name, const QType& type, std::vector& ret, int& res, bool doDNSSEC, const OptLog&, pdns::validation::ValidationContext& validationContext); bool synthesizeFromNSEC3Wildcard(time_t now, const DNSName& name, const QType& type, std::vector& 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& ret, int& res, bool doDNSSEC, ZoneEntry::CacheEntry& nsec, const DNSName& wildcardName, const OptLog&); diff --git a/pdns/recursordist/pdns_recursor.cc b/pdns/recursordist/pdns_recursor.cc index 9f9c8eeacf..5071a38787 100644 --- a/pdns/recursordist/pdns_recursor.cc +++ b/pdns/recursordist/pdns_recursor.cc @@ -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()) { diff --git a/pdns/recursordist/rec-main.cc b/pdns/recursordist/rec-main.cc index a435f3a0d3..54d117bd73 100644 --- a/pdns/recursordist/rec-main.cc +++ b/pdns/recursordist/rec-main.cc @@ -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 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(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))); diff --git a/pdns/recursordist/rec-zonetocache.cc b/pdns/recursordist/rec-zonetocache.cc index a8692a9878..8d4ab260bf 100644 --- a/pdns/recursordist/rec-zonetocache.cc +++ b/pdns/recursordist/rec-zonetocache.cc @@ -250,6 +250,8 @@ pdns::ZoneMD::Result ZoneData::processLines(const vector& lines, const R vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const { + pdns::validation::ValidationContext validationContext; + validationContext.d_nsec3IterationsRemainingQuota = std::numeric_limits::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) diff --git a/pdns/recursordist/settings/table.py b/pdns/recursordist/settings/table.py index a9de993c39..5028b4c5f4 100644 --- a/pdns/recursordist/settings/table.py +++ b/pdns/recursordist/settings/table.py @@ -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 `. +Can be used for better :doc:`security `. ''' }, { diff --git a/pdns/recursordist/syncres.cc b/pdns/recursordist/syncres.cc index acde0be1de..48c4acc2b3 100644 --- a/pdns/recursordist/syncres.cc +++ b/pdns/recursordist/syncres.cc @@ -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 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::max(); } static void allowAdditionalEntry(std::unordered_set& 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(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 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& ret, set& 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) { diff --git a/pdns/recursordist/syncres.hh b/pdns/recursordist/syncres.hh index 0c9e5f8dae..ac5cb196ec 100644 --- a/pdns/recursordist/syncres.hh +++ b/pdns/recursordist/syncres.hh @@ -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>> d_outgoingProtobufServers; std::shared_ptr>> d_frameStreamServers; boost::optional 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; diff --git a/pdns/recursordist/test-aggressive_nsec_cc.cc b/pdns/recursordist/test-aggressive_nsec_cc.cc index fcc3a81fda..7290c73194 100644 --- a/pdns/recursordist/test-aggressive_nsec_cc.cc +++ b/pdns/recursordist/test-aggressive_nsec_cc.cc @@ -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& cache, time_t now, const DNSName& name, const QType& qtype, const std::optional expectedResult = std::nullopt, const std::optional expectedRecordsCount = std::nullopt) +{ + int res; + std::vector results; + pdns::validation::ValidationContext validationContext; + validationContext.d_nsec3IterationsRemainingQuota = std::numeric_limits::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 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 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 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 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 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 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 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 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 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 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(); + + const DNSName zone("powerdns.com"); + time_t now = time(nullptr); + + /* first we need a SOA */ + std::vector 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("pdns-public-ns1.powerdns.com. pieter\\.lexis.powerdns.com. 2017032301 10800 3600 604800 3600")); + drSOA.d_ttl = static_cast(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& 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(nrc)); + auto rrsig = std::make_shared("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(nrc)); + auto rrsig = std::make_shared("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(nrc)); + auto rrsig = std::make_shared("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(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(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); + } } } diff --git a/pdns/recursordist/test-syncres_cc.cc b/pdns/recursordist/test-syncres_cc.cc index 84c999ee65..0aa0ceccc2 100644 --- a/pdns/recursordist/test-syncres_cc.cc +++ b/pdns/recursordist/test-syncres_cc.cc @@ -333,9 +333,27 @@ bool addRRSIG(const testkeysset_t& keys, std::vector& 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& 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(rrc)); records.push_back(rec); diff --git a/pdns/recursordist/test-syncres_cc4.cc b/pdns/recursordist/test-syncres_cc4.cc index 2f21dd32b0..7e5db3c57b 100644 --- a/pdns/recursordist/test-syncres_cc4.cc +++ b/pdns/recursordist/test-syncres_cc4.cc @@ -442,7 +442,6 @@ BOOST_AUTO_TEST_CASE(test_dnssec_rrsig) auto dcke = DNSCryptoKeyEngine::make(DNSSECKeeper::ECDSA256); dcke->create(dcke->getBits()); - // cerr<convertToISC()<> sigs; sigs.push_back(std::make_shared(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 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(dskey, drc); - keys[target] = std::pair(dpk, uselessdrc); + keys[target] = std::pair(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 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& /* srcmask */, boost::optional /* 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 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 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(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(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& /* srcmask */, boost::optional /* 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 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 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(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(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& /* srcmask */, boost::optional /* 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 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 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 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& /* srcmask */, boost::optional /* 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 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 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& /* srcmask */, boost::optional /* 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 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 sr; diff --git a/pdns/recursordist/test-syncres_cc5.cc b/pdns/recursordist/test-syncres_cc5.cc index 7e97556b38..85441f79eb 100644 --- a/pdns/recursordist/test-syncres_cc5.cc +++ b/pdns/recursordist/test-syncres_cc5.cc @@ -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 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& /* srcmask */, boost::optional /* 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 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 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& /* srcmask */, boost::optional /* 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 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 sr; diff --git a/pdns/recursordist/test-syncres_cc8.cc b/pdns/recursordist/test-syncres_cc8.cc index 7306fbaba2..901c8b7784 100644 --- a/pdns/recursordist/test-syncres_cc8.cc +++ b/pdns/recursordist/test-syncres_cc8.cc @@ -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::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> 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(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) diff --git a/pdns/validate.cc b/pdns/validate.cc index decc2f3f64..16d144e643 100644 --- a/pdns/validate.cc +++ b/pdns/validate.cc @@ -30,6 +30,10 @@ 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::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& dsrecords) +bool denialProvesNoDelegation(const DNSName& zone, const std::vector& 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& 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 "< 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::max()}; + uint16_t nsec3sConsidered = 0; for (const auto& validset : validrrsets) { VLOG(log, qname << ": Do have: "<(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"< 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) { + VLOG(log, qname << ": Too many NSEC3s for this record"<= 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"<= 1) { DNSName nextCloser(closestEncloser); nextCloser.prependRawLabel(qname.getRawLabel(labelIdx - 1)); + nsec3sConsidered = 0; VLOG(log, qname << ":Looking for a NSEC3 covering the next closer name "<getZoneRepresentation()<(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"< now) ? vState::BogusSignatureNotYetValid : vState::BogusSignatureExpired; + VLOG(log, qname << ": Signature is "<<(ede == vState::BogusSignatureNotYetValid ? "not yet valid" : "expired")<<" (inception: "<verify(msg, sig.d_signature); - VLOG(log, qname << ": Signature by key with tag "<verify(msg, sig.d_signature); + VLOG(log, qname << ": Signature by key with tag "< now) ? vState::BogusSignatureNotYetValid : vState::BogusSignatureExpired; - VLOG(log, qname << ": Signature is "<<(ede == vState::BogusSignatureNotYetValid ? "not yet valid" : "expired")<<" (inception: "< >& signatures, const skeyset_t& keys, const OptLog& log, bool validateAllSigs) +} + +vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& toSign, const vector >& 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 "<d_labels<<" while the RRset owner name has only "< 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) { + VLOG(log, name<<": We have already considered "<d_tag, signature->d_algorithm, log); if (keysMatchingTag.empty()) { - VLOG(log, name<<": No key provided for "<d_tag<<" and algorithm "<d_algorithm)<d_tag<<" and algorithm "<d_algorithm)< 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) { + VLOG(log, name << ": We have already considered "<d_tag)<<" and algorithm "<d_algorithm)<<", not considering the remaining ones for this signature"<& 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 >& 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 >& 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 "< 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) { + VLOG(log, zone << ": We have already considered "<d_tag<<" matching "<d_tag).size()<<" keys of which "<d_tag).size()<<" valid"<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 "< 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) { + VLOG(log, zone << ": We have already considered "<d_tag)<<" and algorithm "<d_algorithm)<<", not considering the remaining ones for this signature"< 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) { + VLOG(log, zone << ": We have already considered "<, sharedDNSKeyRecordContentCompare>; +namespace pdns::validation +{ +using Nsec3HashesCache = std::map, 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 >& 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 >& 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& recs); bool getTrustAnchor(const map& anchors, const DNSName& zone, dsmap_t &res); bool haveNegativeTrustAnchor(const map& 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 >& 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 >& 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 >& signatures); -bool denialProvesNoDelegation(const DNSName& zone, const std::vector& dsrecords); +bool denialProvesNoDelegation(const DNSName& zone, const std::vector& 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 >& 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 bool isTypeDenied(const NSEC& nsec, const QType& type) { diff --git a/regression-tests.recursor-dnssec/test_AggressiveNSECCache.py b/regression-tests.recursor-dnssec/test_AggressiveNSECCache.py index 1e149af6ac..88be185500 100644 --- a/regression-tests.recursor-dnssec/test_AggressiveNSECCache.py +++ b/regression-tests.recursor-dnssec/test_AggressiveNSECCache.py @@ -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 -- 2.39.5