From: Otto Moerbeek Date: Tue, 2 Jan 2024 12:11:43 +0000 (+0100) Subject: Backport of keytrap to 4.9.x up to 5f6726ca4c759cb6c8fb5f131334dab64a4980d5 X-Git-Tag: rec-4.9.3~4 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3a4cf273247e384c98b45857a87ddc234a1941ce;p=thirdparty%2Fpdns.git Backport of keytrap to 4.9.x up to 5f6726ca4c759cb6c8fb5f131334dab64a4980d5 --- diff --git a/pdns/recursordist/aggressive_nsec.cc b/pdns/recursordist/aggressive_nsec.cc index b419f5ef25..16ea10e011 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 */ @@ -535,7 +536,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; @@ -551,7 +552,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)) { @@ -598,8 +609,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); @@ -647,7 +660,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) { @@ -800,7 +813,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 05a3eeb3a5..07f2fd6465 100644 --- a/pdns/recursordist/aggressive_nsec.hh +++ b/pdns/recursordist/aggressive_nsec.hh @@ -36,11 +36,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) : @@ -54,7 +56,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); @@ -144,7 +146,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 6f841c6147..3668747c07 100644 --- a/pdns/recursordist/pdns_recursor.cc +++ b/pdns/recursordist/pdns_recursor.cc @@ -562,6 +562,15 @@ static PolicyResult handlePolicyHit(const DNSFilterEngine::Policy& appliedPolicy res = RCode::ServFail; break; } + catch (const pdns::validation::TooManySEC3IterationsException& e) { + if (g_logCommonErrors) { + SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << dc->getRemote() << " during resolve of the custom filter policy '" << appliedPolicy.getName() << "' while resolving '" << dc->d_mdp.d_qname << "' because: " << e.what() << endl, + sr.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during resolve of the custom filter policy", + "policyName", Logging::Loggable(appliedPolicy.getName()), "exception", Logging::Loggable("TooManySEC3IterationsException"))); + } + res = RCode::ServFail; + break; + } catch (const PolicyHitException& e) { if (g_logCommonErrors) { SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << dc->getRemote() << " during resolve of the custom filter policy '" << appliedPolicy.getName() << "' while resolving '" << dc->d_mdp.d_qname << "' because another RPZ policy was hit" << endl, @@ -1245,6 +1254,13 @@ void startDoResolve(void* p) // NOLINT(readability-function-cognitive-complexity } res = RCode::ServFail; } + catch (const pdns::validation::TooManySEC3IterationsException& e) { + if (g_logCommonErrors) { + SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << dc->getRemote() << " during resolve of '" << dc->d_mdp.d_qname << "' because: " << e.what() << endl, + sr.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during resolve")); + } + res = RCode::ServFail; + } catch (const SendTruncatedAnswerException& e) { ret.clear(); sr.d_appliedPolicy.addSOAtoRPZResult(ret); @@ -1420,6 +1436,13 @@ void startDoResolve(void* p) // NOLINT(readability-function-cognitive-complexity sr.d_slog->error(Logr::Notice, e.reason, "Sending SERVFAIL during validation", "exception", Logging::Loggable("ImmediateServFailException"))); goto sendit; } + catch (const pdns::validation::TooManySEC3IterationsException& e) { + if (g_logCommonErrors) { + SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << dc->getRemote() << " during validation of '" << dc->d_mdp.d_qname << "|" << QType(dc->d_mdp.d_qtype) << "' because: " << e.what() << endl, + sr.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during validation", "exception", Logging::Loggable("TooManySEC3IterationsException"))); + } + goto sendit; // NOLINT(cppcoreguidelines-avoid-goto) + } } if (ret.size()) { diff --git a/pdns/recursordist/rec-main.cc b/pdns/recursordist/rec-main.cc index ddb5ba5c3c..cceb251bd8 100644 --- a/pdns/recursordist/rec-main.cc +++ b/pdns/recursordist/rec-main.cc @@ -1492,6 +1492,9 @@ 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"); vector nums; bool automatic = true; @@ -1579,6 +1582,8 @@ static int initSyncRes(Logr::log_t log, const std::optional& myHost 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"); @@ -2109,6 +2114,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))); @@ -2996,6 +3002,12 @@ static void initArgs() ::arg().set("tcp-fast-open", "Enable TCP Fast Open support on the listening sockets, using the supplied numerical value as the queue size") = "0"; ::arg().set("tcp-fast-open-connect", "Enable TCP Fast Open support on outgoing sockets") = "no"; ::arg().set("nsec3-max-iterations", "Maximum number of iterations allowed for an NSEC3 record") = "150"; + ::arg().set("max-rrsigs-per-record", "Maximum number of RRSIGs to consider when validating a given record") = "0"; + ::arg().set("max-nsec3s-per-record", "Maximum number of NSEC3s to consider when validating a given denial of existence") = "0"; + ::arg().set("max-signature-validations-per-query", "Maximum number of RRSIG signatures we are willing to validate per incoming query") = "0"; + ::arg().set("max-nsec3-hash-computations-per-query", "Maximum number of NSEC3 hashes that we are willing to compute during DNSSEC validation, per incoming query") = "0"; + ::arg().set("aggressive-cache-max-nsec3-hash-cost", "Maximum estimated NSEC3 cost for a given query to consider aggressive use of the NSEC3 cache") = "0"; + ::arg().set("max-dnskeys", "Maximum number of DNSKEYs with the same algorithm and tag to consider when validating a given record") = "0"; ::arg().set("cpu-map", "Thread to CPU mapping, space separated thread-id=cpu1,cpu2..cpuN pairs") = ""; diff --git a/pdns/recursordist/rec-zonetocache.cc b/pdns/recursordist/rec-zonetocache.cc index 2f2cbfabec..d758989568 100644 --- a/pdns/recursordist/rec-zonetocache.cc +++ b/pdns/recursordist/rec-zonetocache.cc @@ -243,6 +243,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 sr({d_now, 0}); @@ -266,7 +268,7 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const } skeyset_t validKeys; - vState dnsKeyState = validateDNSKeysAgainstDS(d_now, d_zone, dsmap, dnsKeys, records, zonemd.getRRSIGs(), validKeys, std::nullopt); + vState dnsKeyState = validateDNSKeysAgainstDS(d_now, d_zone, dsmap, dnsKeys, records, zonemd.getRRSIGs(), validKeys, std::nullopt, validationContext); if (dnsKeyState != vState::Secure) { return dnsKeyState; } @@ -288,7 +290,7 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const if (nsecs.records.size() > 0 && nsecs.signatures.size() > 0) { // Valdidate the NSEC - nsecValidationStatus = validateWithKeySet(d_now, d_zone, nsecs.records, nsecs.signatures, validKeys, std::nullopt); + nsecValidationStatus = validateWithKeySet(d_now, d_zone, nsecs.records, nsecs.signatures, validKeys, std::nullopt, validationContext); csp.emplace(std::make_pair(d_zone, QType::NSEC), nsecs); } else if (nsec3s.records.size() > 0 && nsec3s.signatures.size() > 0) { @@ -297,13 +299,13 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const for (const auto& rec : zonemd.getNSEC3Params()) { records.emplace(rec); } - nsecValidationStatus = validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys, std::nullopt); + nsecValidationStatus = validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys, std::nullopt, validationContext); if (nsecValidationStatus != vState::Secure) { d_log->info("NSEC3PARAMS records did not validate"); return nsecValidationStatus; } // Valdidate the NSEC3 - nsecValidationStatus = validateWithKeySet(d_now, zonemd.getNSEC3Label(), nsec3s.records, nsec3s.signatures, validKeys, std::nullopt); + nsecValidationStatus = validateWithKeySet(d_now, zonemd.getNSEC3Label(), nsec3s.records, nsec3s.signatures, validKeys, std::nullopt, validationContext); csp.emplace(std::make_pair(zonemd.getNSEC3Label(), QType::NSEC3), nsec3s); } else { @@ -316,7 +318,7 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const return nsecValidationStatus; } - auto denial = getDenial(csp, d_zone, QType::ZONEMD, false, false, std::nullopt, true); + auto denial = getDenial(csp, d_zone, QType::ZONEMD, false, false, validationContext, std::nullopt, true); if (denial == dState::NXQTYPE) { d_log->info("Validated denial of absence of ZONEMD record"); return vState::Secure; @@ -330,7 +332,7 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const for (const auto& rec : zonemdRecords) { records.emplace(rec); } - return validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys, 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/syncres.cc b/pdns/recursordist/syncres.cc index d237f6911d..803d9f8088 100644 --- a/pdns/recursordist/syncres.cc +++ b/pdns/recursordist/syncres.cc @@ -442,6 +442,8 @@ unsigned int SyncRes::s_serverdownthrottletime; unsigned int SyncRes::s_nonresolvingnsmaxfails; unsigned int SyncRes::s_nonresolvingnsthrottletime; unsigned int SyncRes::s_ecscachelimitttl; +unsigned int SyncRes::s_maxvalidationsperq; +unsigned int SyncRes::s_maxnsec3iterationsperq; pdns::stat_t SyncRes::s_ecsqueries; pdns::stat_t SyncRes::s_ecsresponses; std::map SyncRes::s_ecsResponsesBySubnetSize4; @@ -521,8 +523,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); @@ -2997,7 +2999,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)"}; @@ -3668,7 +3670,7 @@ vState SyncRes::getDSRecords(const DNSName& zone, dsmap_t& ds, bool taOnly, unsi - a delegation to a non-DNSSEC signed zone - no delegation, we stay in the same zone */ - if (gotCNAME || denialProvesNoDelegation(zone, dsrecords)) { + if (gotCNAME || denialProvesNoDelegation(zone, dsrecords, d_validationContext)) { /* we are still inside the same zone */ if (foundCut) { @@ -3841,7 +3843,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); @@ -4006,7 +4012,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; @@ -4710,7 +4720,7 @@ void SyncRes::updateDenialValidationState(const DNSName& qname, vState& neValida dState SyncRes::getDenialValidationState(const NegCache::NegCacheEntry& ne, const dState expectedState, bool referralToUnsigned, const string& prefix) { cspmap_t csp = harvestCSPFromNE(ne); - return getDenial(csp, ne.d_name, ne.d_qtype.getCode(), referralToUnsigned, expectedState == dState::NXQTYPE, LogObject(prefix)); + return getDenial(csp, ne.d_name, ne.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) @@ -4874,7 +4884,7 @@ bool SyncRes::processRecords(const std::string& prefix, const DNSName& qname, co as described in section 5.3.4 of RFC 4035 and 5.3 of RFC 7129. */ cspmap_t csp = harvestCSPFromNE(ne); - dState res = getDenial(csp, qname, ne.d_qtype.getCode(), false, false, LogObject(prefix), false, wildcardLabelsCount); + dState res = getDenial(csp, qname, ne.d_qtype.getCode(), false, false, d_validationContext, LogObject(prefix), false, wildcardLabelsCount); if (res != dState::NXDOMAIN) { vState st = vState::BogusInvalidDenial; if (res == dState::INSECURE || res == dState::OPTOUT) { diff --git a/pdns/recursordist/syncres.hh b/pdns/recursordist/syncres.hh index c12edd8074..d514f5c3fc 100644 --- a/pdns/recursordist/syncres.hh +++ b/pdns/recursordist/syncres.hh @@ -525,8 +525,9 @@ public: static unsigned int s_serverdownthrottletime; static unsigned int s_nonresolvingnsmaxfails; static unsigned int s_nonresolvingnsthrottletime; - static unsigned int s_ecscachelimitttl; + static unsigned int s_maxvalidationsperq; + static unsigned int s_maxnsec3iterationsperq; static uint8_t s_ecsipv4limit; static uint8_t s_ecsipv6limit; static uint8_t s_ecsipv4cachelimit; @@ -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 249e9b561f..4d02016f52 100644 --- a/pdns/recursordist/test-aggressive_nsec_cc.cc +++ b/pdns/recursordist/test-aggressive_nsec_cc.cc @@ -1314,6 +1314,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, @@ -1369,12 +1385,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 */ @@ -1401,10 +1414,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"); @@ -1431,10 +1444,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) @@ -1482,15 +1495,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); } { @@ -1515,14 +1522,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); } { @@ -1547,16 +1549,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); } { @@ -1605,17 +1600,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); } } @@ -1674,15 +1662,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); } { @@ -1713,14 +1695,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); } { @@ -1751,16 +1728,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); } { @@ -1855,17 +1825,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 */ @@ -1960,15 +1922,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 179d6b0269..0b0751ae08 100644 --- a/pdns/recursordist/test-syncres_cc.cc +++ b/pdns/recursordist/test-syncres_cc.cc @@ -326,9 +326,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) { @@ -338,16 +356,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 3e02e501bc..e6545b73aa 100644 --- a/pdns/recursordist/test-syncres_cc4.cc +++ b/pdns/recursordist/test-syncres_cc4.cc @@ -459,7 +459,9 @@ BOOST_AUTO_TEST_CASE(test_dnssec_rrsig) std::vector> 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) @@ -832,6 +834,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; @@ -855,10 +858,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(); @@ -952,6 +955,106 @@ 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_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 DSs (two of them!) */ + auto luaconfsCopy = g_luaconfs.getCopy(); + luaconfsCopy.dsAnchors.clear(); + luaconfsCopy.dsAnchors[g_rootdnsname].insert(drc); + luaconfsCopy.dsAnchors[g_rootdnsname].insert(uselessdrc); + 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); + addRRSIG(keys, res->d_records, domain, 300); + addDNSKEY(dskeys, 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::Insecure); + /* 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::Insecure); + BOOST_REQUIRE_EQUAL(ret.size(), 14U); + BOOST_CHECK_EQUAL(queriesCount, 2U); + + g_maxDNSKEYsToConsider = 0; +} + BOOST_AUTO_TEST_CASE(test_dnssec_bogus_rrsig_signed_with_unknown_dnskey) { std::unique_ptr sr; @@ -1412,6 +1515,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 bb893939f1..b85aa31527 100644 --- a/pdns/recursordist/test-syncres_cc5.cc +++ b/pdns/recursordist/test-syncres_cc5.cc @@ -1240,6 +1240,177 @@ BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec3_nodata_nowildcard) BOOST_CHECK_EQUAL(queriesCount, 4U); } +BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec3_too_many_nsec3s) +{ + std::unique_ptr 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 2026f607fb..af86abe6e3 100644 --- a/pdns/recursordist/test-syncres_cc8.cc +++ b/pdns/recursordist/test-syncres_cc8.cc @@ -5,6 +5,13 @@ BOOST_AUTO_TEST_SUITE(syncres_cc8) +static dState getDenial(const cspmap_t& validrrsets, const DNSName& qname, uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, 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(); @@ -412,8 +419,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))); @@ -425,10 +432,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 eb96ca6bd4..4bfb9f1cc0 100644 --- a/pdns/validate.cc +++ b/pdns/validate.cc @@ -30,6 +30,9 @@ 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}; static bool isAZoneKey(const DNSKEYRecordContent& key) { @@ -108,38 +111,49 @@ static bool nsecProvesENT(const DNSName& name, const DNSName& begin, const DNSNa return begin.canonCompare(name) && next != name && next.isPartOf(name); } -using nsec3HashesCache = std::map, std::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::make_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) { const auto nsec = getRR(record); @@ -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) - { + for (const auto& dsrc : dsmap) { auto record = getByTag(tkeys, dsrc.d_tag, dsrc.d_algorithm, log); // cerr<<"looking at DS with tag "< 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) { + 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); @@ -1126,9 +1216,16 @@ vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& } 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 "<d_tag)<<" and algorithm "<d_algorithm)<<", not considering the remaining ones for this signature"<, 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 +124,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 2ff833efcd..88be185500 100644 --- a/regression-tests.recursor-dnssec/test_AggressiveNSECCache.py +++ b/regression-tests.recursor-dnssec/test_AggressiveNSECCache.py @@ -16,6 +16,8 @@ class AggressiveNSECCacheBase(RecursorTest): _config_template = """ dnssec=validate aggressive-nsec-cache-size=10000 + aggressive-cache-max-nsec3-hash-cost=204 + nsec3-max-iterations=150 webserver=yes webserver-port=%d webserver-address=127.0.0.1