From a4bc1426238b28b07c0c07cd5e55478a42ed0a18 Mon Sep 17 00:00:00 2001 From: Otto Moerbeek Date: Mon, 22 Jan 2024 12:19:16 +0100 Subject: [PATCH] Backport of Keytrap to rec-4.8.x --- pdns/pdns_recursor.cc | 23 + pdns/recursordist/aggressive_nsec.cc | 25 +- pdns/recursordist/aggressive_nsec.hh | 7 +- pdns/recursordist/rec-main.cc | 15 + pdns/recursordist/rec-zonetocache.cc | 14 +- pdns/recursordist/test-aggressive_nsec_cc.cc | 280 +++++-- pdns/recursordist/test-syncres_cc.cc | 32 +- pdns/recursordist/test-syncres_cc4.cc | 430 +++++++++- pdns/recursordist/test-syncres_cc5.cc | 171 ++++ pdns/recursordist/test-syncres_cc8.cc | 19 +- pdns/syncres.cc | 23 +- pdns/syncres.hh | 4 +- pdns/validate.cc | 772 ++++++++---------- pdns/validate.hh | 40 +- .../test_AggressiveNSECCache.py | 2 + 15 files changed, 1312 insertions(+), 545 deletions(-) diff --git a/pdns/pdns_recursor.cc b/pdns/pdns_recursor.cc index 858dc28b9d..0e70d14a85 100644 --- a/pdns/pdns_recursor.cc +++ b/pdns/pdns_recursor.cc @@ -551,6 +551,15 @@ static PolicyResult handlePolicyHit(const DNSFilterEngine::Policy& appliedPolicy res = RCode::ServFail; break; } + catch (const pdns::validation::TooManySEC3IterationsException& e) { + if (g_logCommonErrors) { + SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << dc->getRemote() << " during resolve of the custom filter policy '" << appliedPolicy.getName() << "' while resolving '" << dc->d_mdp.d_qname << "' because: " << e.what() << endl, + sr.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during resolve of the custom filter policy", + "policyName", Logging::Loggable(appliedPolicy.getName()), "exception", Logging::Loggable("TooManySEC3IterationsException"))); + } + res = RCode::ServFail; + break; + } catch (const PolicyHitException& e) { if (g_logCommonErrors) { SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << dc->getRemote() << " during resolve of the custom filter policy '" << appliedPolicy.getName() << "' while resolving '" << dc->d_mdp.d_qname << "' because another RPZ policy was hit" << endl, @@ -1197,6 +1206,13 @@ void startDoResolve(void* p) } res = RCode::ServFail; } + catch (const pdns::validation::TooManySEC3IterationsException& e) { + if (g_logCommonErrors) { + SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << dc->getRemote() << " during resolve of '" << dc->d_mdp.d_qname << "' because: " << e.what() << endl, + sr.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during resolve")); + } + res = RCode::ServFail; + } catch (const SendTruncatedAnswerException& e) { ret.clear(); res = RCode::NoError; @@ -1379,6 +1395,13 @@ void startDoResolve(void* p) sr.d_slog->error(Logr::Notice, e.reason, "Sending SERVFAIL during validation", "exception", Logging::Loggable("ImmediateServFailException"))); goto sendit; } + catch (const pdns::validation::TooManySEC3IterationsException& e) { + if (g_logCommonErrors) { + SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << dc->getRemote() << " during validation of '" << dc->d_mdp.d_qname << "|" << QType(dc->d_mdp.d_qtype) << "' because: " << e.what() << endl, + sr.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during validation", "exception", Logging::Loggable("TooManySEC3IterationsException"))); + } + goto sendit; // NOLINT(cppcoreguidelines-avoid-goto) + } } if (ret.size()) { diff --git a/pdns/recursordist/aggressive_nsec.cc b/pdns/recursordist/aggressive_nsec.cc index c0b1d6e182..3b6e344f42 100644 --- a/pdns/recursordist/aggressive_nsec.cc +++ b/pdns/recursordist/aggressive_nsec.cc @@ -28,6 +28,7 @@ #include "validate.hh" std::unique_ptr g_aggressiveNSECCache{nullptr}; +uint64_t AggressiveNSECCache::s_nsec3DenialProofMaxCost{0}; /* this is defined in syncres.hh and we are not importing that here */ extern std::unique_ptr g_recCache; @@ -514,7 +515,7 @@ bool AggressiveNSECCache::synthesizeFromNSECWildcard(time_t now, const DNSName& return true; } -bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr>& zoneEntry, std::vector& soaSet, std::vector>& soaSignatures, const DNSName& name, const QType& type, std::vector& ret, int& res, bool doDNSSEC) +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, pdns::validation::ValidationContext& validationContext) { DNSName zone; std::string salt; @@ -530,7 +531,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" + LOG(name << ": Skipping aggressive use of the NSEC3 cache since the zone parameters are too expensive" << endl); + return false; + } + } + + auto nameHash = DNSName(toBase32Hex(getHashFromNSEC3(name, iterations, salt, validationContext))) + zone; ZoneEntry::CacheEntry exactNSEC3; if (getNSEC3(now, zoneEntry, nameHash, exactNSEC3)) { @@ -577,8 +588,10 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr= zoneLabelsCount) { + auto closestHash = DNSName(toBase32Hex(getHashFromNSEC3(closestEncloser, iterations, salt, validationContext))) + zone; + remainingLabels--; if (getNSEC3(now, zoneEntry, closestHash, closestNSEC3)) { LOG("Found closest encloser at " << closestEncloser << " (" << closestHash << ")" << endl); @@ -739,7 +752,7 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr& ret, int& res, const ComboAddress& who, const boost::optional& routingTag, bool doDNSSEC) +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) { std::shared_ptr> zoneEntry; if (type == QType::DS) { @@ -779,7 +792,7 @@ bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType } if (nsec3) { - return getNSEC3Denial(now, zoneEntry, soaSet, soaSignatures, name, type, ret, res, doDNSSEC); + return getNSEC3Denial(now, zoneEntry, soaSet, soaSignatures, name, type, ret, res, doDNSSEC, validationContext); } ZoneEntry::CacheEntry entry; diff --git a/pdns/recursordist/aggressive_nsec.hh b/pdns/recursordist/aggressive_nsec.hh index eb9d2bfa01..c19b761ab4 100644 --- a/pdns/recursordist/aggressive_nsec.hh +++ b/pdns/recursordist/aggressive_nsec.hh @@ -35,17 +35,20 @@ using namespace ::boost::multi_index; #include "dnsrecords.hh" #include "lock.hh" #include "stat_t.hh" +#include "validate.hh" class AggressiveNSECCache { public: + static uint64_t s_nsec3DenialProofMaxCost; + AggressiveNSECCache(uint64_t entries) : d_maxEntries(entries) { } void insertNSEC(const DNSName& zone, const DNSName& owner, const DNSRecord& record, const std::vector>& 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); + 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); void removeZoneInfo(const DNSName& zone, bool subzones); @@ -132,7 +135,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); + 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, 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); bool synthesizeFromNSECWildcard(time_t now, const DNSName& name, const QType& type, std::vector& ret, int& res, bool doDNSSEC, ZoneEntry::CacheEntry& nsec, const DNSName& wildcardName); diff --git a/pdns/recursordist/rec-main.cc b/pdns/recursordist/rec-main.cc index bb6b742b89..2fa84153e4 100644 --- a/pdns/recursordist/rec-main.cc +++ b/pdns/recursordist/rec-main.cc @@ -1436,6 +1436,10 @@ static int serviceMain(int argc, char* argv[], Logr::log_t log) g_dnssecLogBogus = ::arg().mustDo("dnssec-log-bogus"); g_maxNSEC3Iterations = ::arg().asNum("nsec3-max-iterations"); + g_maxRRSIGsPerRecordToConsider = ::arg().asNum("max-rrsigs-per-record"); + g_maxNSEC3sPerRecordToConsider = ::arg().asNum("max-nsec3s-per-record"); + g_maxDNSKEYsToConsider = ::arg().asNum("max-dnskeys"); + g_maxDSsToConsider = ::arg().asNum("max-ds-per-zone"); g_maxCacheEntries = ::arg().asNum("max-cache-entries"); g_maxPacketCacheEntries = ::arg().asNum("max-packetcache-entries"); @@ -1522,6 +1526,8 @@ static int serviceMain(int argc, char* argv[], Logr::log_t log) SyncRes::s_maxnsaddressqperq = ::arg().asNum("max-ns-address-qperq"); SyncRes::s_maxtotusec = 1000 * ::arg().asNum("max-total-msec"); SyncRes::s_maxdepth = ::arg().asNum("max-recursion-depth"); + SyncRes::s_maxvalidationsperq = ::arg().asNum("max-signature-validations-per-query"); + SyncRes::s_maxnsec3iterationsperq = ::arg().asNum("max-nsec3-hash-computations-per-query"); SyncRes::s_rootNXTrust = ::arg().mustDo("root-nx-trust"); SyncRes::s_refresh_ttlperc = ::arg().asNum("refresh-on-ttl-perc"); SyncRes::s_locked_ttlperc = ::arg().asNum("record-cache-locked-ttl-perc"); @@ -1701,6 +1707,7 @@ static int serviceMain(int argc, char* argv[], Logr::log_t log) g_addExtendedResolutionDNSErrors = ::arg().mustDo("extended-resolution-errors"); + AggressiveNSECCache::s_nsec3DenialProofMaxCost = ::arg().asNum("aggressive-cache-max-nsec3-hash-cost"); if (::arg().asNum("aggressive-nsec-cache-size") > 0) { if (g_dnssecmode == DNSSECMode::ValidateAll || g_dnssecmode == DNSSECMode::ValidateForLog || g_dnssecmode == DNSSECMode::Process) { g_aggressiveNSECCache = make_unique(::arg().asNum("aggressive-nsec-cache-size")); @@ -2773,6 +2780,14 @@ int main(int argc, char** argv) ::arg().set("tcp-fast-open-connect", "Enable TCP Fast Open support on outgoing sockets") = "no"; ::arg().set("nsec3-max-iterations", "Maximum number of iterations allowed for an NSEC3 record") = "150"; + ::arg().set("max-rrsigs-per-record", "Maximum number of RRSIGs to consider when validating a given record") = "2"; + ::arg().set("max-nsec3s-per-record", "Maximum number of NSEC3s to consider when validating a given denial of existence") = "10"; + ::arg().set("max-signature-validations-per-query", "Maximum number of RRSIG signatures we are willing to validate per incoming query") = "30"; + ::arg().set("max-nsec3-hash-computations-per-query", "Maximum number of NSEC3 hashes that we are willing to compute during DNSSEC validation, per incoming query") = "600"; + ::arg().set("aggressive-cache-max-nsec3-hash-cost", "Maximum estimated NSEC3 cost for a given query to consider aggressive use of the NSEC3 cache") = "150"; + ::arg().set("max-dnskeys", "Maximum number of DNSKEYs with the same algorithm and tag to consider when validating a given record") = "2"; + ::arg().set("max-ds-per-zone", "Maximum number of DS records to consider per zone") = "8"; + ::arg().set("cpu-map", "Thread to CPU mapping, space separated thread-id=cpu1,cpu2..cpuN pairs") = ""; ::arg().setSwitch("log-rpz-changes", "Log additions and removals to RPZ zones at Info level") = "no"; diff --git a/pdns/recursordist/rec-zonetocache.cc b/pdns/recursordist/rec-zonetocache.cc index a83dc43da8..dfe61c9151 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); + vState dnsKeyState = validateDNSKeysAgainstDS(d_now, d_zone, dsmap, dnsKeys, records, zonemd.getRRSIGs(), validKeys, validationContext); if (dnsKeyState != vState::Secure) { return dnsKeyState; } @@ -288,7 +290,7 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const if (nsecs.records.size() > 0 && nsecs.signatures.size() > 0) { // Valdidate the NSEC - nsecValidationStatus = validateWithKeySet(d_now, d_zone, nsecs.records, nsecs.signatures, validKeys); + nsecValidationStatus = validateWithKeySet(d_now, d_zone, nsecs.records, nsecs.signatures, validKeys, validationContext); csp.emplace(std::make_pair(d_zone, QType::NSEC), nsecs); } else if (nsec3s.records.size() > 0 && nsec3s.signatures.size() > 0) { @@ -297,13 +299,13 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const for (const auto& rec : zonemd.getNSEC3Params()) { records.emplace(rec); } - nsecValidationStatus = validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys); + nsecValidationStatus = validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys, validationContext); if (nsecValidationStatus != vState::Secure) { d_log->info("NSEC3PARAMS records did not validate"); return nsecValidationStatus; } // Valdidate the NSEC3 - nsecValidationStatus = validateWithKeySet(d_now, zonemd.getNSEC3Label(), nsec3s.records, nsec3s.signatures, validKeys); + nsecValidationStatus = validateWithKeySet(d_now, zonemd.getNSEC3Label(), nsec3s.records, nsec3s.signatures, validKeys, validationContext); csp.emplace(std::make_pair(zonemd.getNSEC3Label(), QType::NSEC3), nsec3s); } else { @@ -316,7 +318,7 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const return nsecValidationStatus; } - auto denial = getDenial(csp, d_zone, QType::ZONEMD, false, false, true); + auto denial = getDenial(csp, d_zone, QType::ZONEMD, false, false, validationContext, true); if (denial == dState::NXQTYPE) { d_log->info("Validated denial of absence of ZONEMD record"); return vState::Secure; @@ -330,7 +332,7 @@ vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const for (const auto& rec : zonemdRecords) { records.emplace(rec); } - return validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys); + return validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys, validationContext); } void ZoneData::ZoneToCache(const RecZoneToCache::Config& config) diff --git a/pdns/recursordist/test-aggressive_nsec_cc.cc b/pdns/recursordist/test-aggressive_nsec_cc.cc index 51693050c6..75408cd125 100644 --- a/pdns/recursordist/test-aggressive_nsec_cc.cc +++ b/pdns/recursordist/test-aggressive_nsec_cc.cc @@ -1218,6 +1218,22 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec_dump) free(line); } +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, @@ -1272,12 +1288,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 */ @@ -1304,10 +1317,10 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_rollover) /* we should be able to find a direct match for that name */ /* direct match */ - BOOST_CHECK_EQUAL(cache->getDenial(now, other, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, other, QType::AAAA), true); /* but we should not be able to use the other NSEC3s */ - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA), false); /* and the same thing but this time updating the iterations count instead of the salt */ DNSName other2("other2.powerdns.com"); @@ -1334,10 +1347,10 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_rollover) /* we should be able to find a direct match for that name */ /* direct match */ - BOOST_CHECK_EQUAL(cache->getDenial(now, other2, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, other2, QType::AAAA), true); /* but we should not be able to use the other NSEC3s */ - BOOST_CHECK_EQUAL(cache->getDenial(now, other, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, other, QType::AAAA), false); } BOOST_AUTO_TEST_CASE(test_aggressive_nsec_ancestor_cases) @@ -1385,15 +1398,10 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec_ancestor_cases) BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U); /* the cache should now be able to deny other types (except the DS) */ - int res; - std::vector results; - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); - BOOST_CHECK_EQUAL(res, RCode::NoError); - BOOST_CHECK_EQUAL(results.size(), 3U); + /* the cache should now be able to deny other types (except the DS) */ + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NoError, 3U), true); /* but not the DS that lives in the parent zone */ - results.clear(); - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), false); - BOOST_CHECK_EQUAL(results.size(), 0U); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, std::nullopt, 0U), false); } { @@ -1418,14 +1426,9 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec_ancestor_cases) BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U); /* the cache should now be able to deny the DS */ - int res; - std::vector results; - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); - BOOST_CHECK_EQUAL(res, RCode::NoError); - BOOST_CHECK_EQUAL(results.size(), 3U); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NoError, 3U), true); /* but not any type that lives in the child zone */ - results.clear(); - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA), false); } { @@ -1450,16 +1453,9 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec_ancestor_cases) BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U); /* the cache should now be able to deny other types */ - int res; - std::vector results; - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); - BOOST_CHECK_EQUAL(res, RCode::NoError); - BOOST_CHECK_EQUAL(results.size(), 3U); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NoError, 3U), true); /* including the DS */ - results.clear(); - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); - BOOST_CHECK_EQUAL(res, RCode::NoError); - BOOST_CHECK_EQUAL(results.size(), 3U); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NoError, 3U), true); } { @@ -1508,17 +1504,10 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec_ancestor_cases) } /* the cache should now be able to deny any type for the name */ - int res; - std::vector results; - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); - BOOST_CHECK_EQUAL(res, RCode::NXDomain); - BOOST_CHECK_EQUAL(results.size(), 5U); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NXDomain, 5U), true); /* including the DS, since we are not at the apex */ - results.clear(); - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); - BOOST_CHECK_EQUAL(res, RCode::NXDomain); - BOOST_CHECK_EQUAL(results.size(), 5U); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NXDomain, 5U), true); } } @@ -1576,15 +1565,9 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_ancestor_cases) BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U); /* the cache should now be able to deny other types (except the DS) */ - int res; - std::vector results; - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); - BOOST_CHECK_EQUAL(res, RCode::NoError); - BOOST_CHECK_EQUAL(results.size(), 3U); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NoError, 3U), true); /* but not the DS that lives in the parent zone */ - results.clear(); - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), false); - BOOST_CHECK_EQUAL(results.size(), 0U); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, std::nullopt, 0U), false); } { @@ -1615,14 +1598,9 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_ancestor_cases) BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U); /* the cache should now be able to deny the DS */ - int res; - std::vector results; - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); - BOOST_CHECK_EQUAL(res, RCode::NoError); - BOOST_CHECK_EQUAL(results.size(), 3U); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NoError, 3U), true); /* but not any type that lives in the child zone */ - results.clear(); - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA), false); } { @@ -1653,16 +1631,9 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_ancestor_cases) BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U); /* the cache should now be able to deny other types */ - int res; - std::vector results; - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); - BOOST_CHECK_EQUAL(res, RCode::NoError); - BOOST_CHECK_EQUAL(results.size(), 3U); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NoError, 3U), true); /* including the DS */ - results.clear(); - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); - BOOST_CHECK_EQUAL(res, RCode::NoError); - BOOST_CHECK_EQUAL(results.size(), 3U); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NoError, 3U), true); } { @@ -1757,17 +1728,9 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_ancestor_cases) } /* the cache should now be able to deny any type for the name */ - int res; - std::vector results; - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); - BOOST_CHECK_EQUAL(res, RCode::NXDomain); - BOOST_CHECK_EQUAL(results.size(), 7U); - + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NXDomain, 7U), true); /* including the DS, since we are not at the apex */ - results.clear(); - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true); - BOOST_CHECK_EQUAL(res, RCode::NXDomain); - BOOST_CHECK_EQUAL(results.size(), 7U); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NXDomain, 7U), true); } { /* we insert NSEC3s coming from the parent zone that could look like a valid denial but are not */ @@ -1862,15 +1825,166 @@ BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_ancestor_cases) } /* the cache should NOT be able to deny the name */ - int res; - std::vector results; - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false); - BOOST_CHECK_EQUAL(results.size(), 0U); - + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, std::nullopt, 0U), false); /* and the same for the DS */ - results.clear(); - BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), false); - BOOST_CHECK_EQUAL(results.size(), 0U); + BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, std::nullopt, 0U), false); + } +} + +BOOST_AUTO_TEST_CASE(test_aggressive_max_nsec3_hash_cost) +{ + g_recCache = std::make_unique(); + + 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.d_content = 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.d_content = 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.d_content = 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.d_content = 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 ae2c062132..5b7f26deb2 100644 --- a/pdns/recursordist/test-syncres_cc.cc +++ b/pdns/recursordist/test-syncres_cc.cc @@ -329,9 +329,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) { @@ -341,16 +359,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.d_content = 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 23358d9bc8..c1f65db023 100644 --- a/pdns/recursordist/test-syncres_cc4.cc +++ b/pdns/recursordist/test-syncres_cc4.cc @@ -439,7 +439,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) == vState::Secure); -} + pdns::validation::ValidationContext validationContext; + BOOST_CHECK(validateWithKeySet(now, qname, recordcontents, sigs, keyset, validationContext) == vState::Secure); + BOOST_CHECK_EQUAL(validationContext.d_validationsCounter, 1U);} BOOST_AUTO_TEST_CASE(test_dnssec_root_validation_csk) { @@ -837,6 +837,7 @@ BOOST_AUTO_TEST_CASE(test_dnssec_bogus_dnskey_revoked) BOOST_REQUIRE_EQUAL(ret.size(), 14U); BOOST_CHECK_EQUAL(queriesCount, 2U); } + BOOST_AUTO_TEST_CASE(test_dnssec_bogus_dnskey_doesnt_match_ds) { std::unique_ptr sr; @@ -959,6 +960,287 @@ BOOST_AUTO_TEST_CASE(test_dnssec_bogus_dnskey_doesnt_match_ds) BOOST_CHECK_EQUAL(queriesCount, 4U); } +BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_dss) +{ + std::unique_ptr 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)); + dskey.d_flags = 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.d_flags = 256; + dpk.setKey(std::move(dcke)); + assert(dpk.getTag() == dskey.getTag()); + DSRecordContent uselessdrc = makeDSFromDNSKey(target, dpk.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256); + keys[target] = std::pair(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)); + dskey.d_flags = 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)); + dpk.d_flags = 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; @@ -1423,6 +1705,148 @@ BOOST_AUTO_TEST_CASE(test_dnssec_bogus_bad_sig) BOOST_CHECK_EQUAL(queriesCount, 2U); } +BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_sigs) +{ + std::unique_ptr 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 1af1d87196..306f4ac749 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 a9bdd1a939..41df583cc6 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, 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, needWildcardProof, wildcardLabelsCount); +} + BOOST_AUTO_TEST_CASE(test_nsec_denial_nowrap) { initSR(); @@ -413,7 +420,8 @@ BOOST_AUTO_TEST_CASE(test_nsec3_nxqtype_ds) sortedRecords_t recordContents; vector> signatureContents; - addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::A}, 600, records); + const unsigned int nbIterations = 10; + addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::A}, 600, records, nbIterations); recordContents.insert(records.at(0).d_content); addRRSIG(keys, records, DNSName("powerdns.com."), 300); signatureContents.push_back(getRR(records.at(1))); @@ -425,10 +433,15 @@ BOOST_AUTO_TEST_CASE(test_nsec3_nxqtype_ds) denialMap[std::pair(records.at(0).d_name, records.at(0).d_type)] = pair; records.clear(); + pdns::validation::ValidationContext validationContext; + validationContext.d_nsec3IterationsRemainingQuota = 100U; /* this NSEC3 is not valid to deny the DS since it is from the child zone */ - BOOST_CHECK_EQUAL(getDenial(denialMap, DNSName("powerdns.com."), QType::DS, false, true), dState::NODENIAL); + BOOST_CHECK_EQUAL(getDenial(denialMap, DNSName("powerdns.com."), QType::DS, false, true, validationContext), dState::NODENIAL); + /* the NSEC3 hash is not computed since we it is from the child zone */ + BOOST_CHECK_EQUAL(validationContext.d_nsec3IterationsRemainingQuota, 100U); /* AAAA should be fine, though */ - BOOST_CHECK_EQUAL(getDenial(denialMap, DNSName("powerdns.com."), QType::AAAA, false, true), dState::NXQTYPE); + BOOST_CHECK_EQUAL(getDenial(denialMap, DNSName("powerdns.com."), QType::AAAA, false, true, validationContext), dState::NXQTYPE); + BOOST_CHECK_EQUAL(validationContext.d_nsec3IterationsRemainingQuota, (100U - nbIterations)); } BOOST_AUTO_TEST_CASE(test_nsec3_nxqtype_cname) diff --git a/pdns/syncres.cc b/pdns/syncres.cc index 3ab05d9f69..bbdba357c3 100644 --- a/pdns/syncres.cc +++ b/pdns/syncres.cc @@ -427,6 +427,8 @@ unsigned int SyncRes::s_serverdownthrottletime; unsigned int SyncRes::s_nonresolvingnsmaxfails; unsigned int SyncRes::s_nonresolvingnsthrottletime; unsigned int SyncRes::s_ecscachelimitttl; +unsigned int SyncRes::s_maxvalidationsperq; +unsigned int SyncRes::s_maxnsec3iterationsperq; pdns::stat_t SyncRes::s_authzonequeries; pdns::stat_t SyncRes::s_queries; pdns::stat_t SyncRes::s_outgoingtimeouts; @@ -498,8 +500,8 @@ static inline void accountAuthLatency(uint64_t usec, int family) SyncRes::SyncRes(const struct timeval& now) : d_authzonequeries(0), d_outqueries(0), d_tcpoutqueries(0), d_dotoutqueries(0), d_throttledqueries(0), d_timeouts(0), d_unreachables(0), d_totUsec(0), d_now(now), d_cacheonly(false), d_doDNSSEC(false), d_doEDNS0(false), d_qNameMinimization(s_qnameminimization), d_lm(s_lm) - { + d_validationContext.d_nsec3IterationsRemainingQuota = s_maxnsec3iterationsperq > 0 ? s_maxnsec3iterationsperq : std::numeric_limits::max(); } static void allowAdditionalEntry(std::unordered_set& allowedAdditionals, const DNSRecord& rec); @@ -2973,7 +2975,7 @@ bool SyncRes::doCacheCheck(const DNSName &qname, const DNSName& authname, bool w /* let's check if we have a NSEC covering that record */ if (g_aggressiveNSECCache && !wasForwardedOrAuthZone) { - if (g_aggressiveNSECCache->getDenial(d_now.tv_sec, qname, qtype, ret, res, d_cacheRemote, d_routingTag, d_doDNSSEC)) { + if (g_aggressiveNSECCache->getDenial(d_now.tv_sec, qname, qtype, ret, res, d_cacheRemote, d_routingTag, d_doDNSSEC, d_validationContext)) { state = vState::Secure; return true; } @@ -3635,7 +3637,7 @@ vState SyncRes::getDSRecords(const DNSName& zone, dsmap_t& ds, bool taOnly, unsi - a delegation to a non-DNSSEC signed zone - no delegation, we stay in the same zone */ - if (gotCNAME || denialProvesNoDelegation(zone, dsrecords)) { + if (gotCNAME || denialProvesNoDelegation(zone, dsrecords, d_validationContext)) { /* we are still inside the same zone */ if (foundCut) { @@ -3807,7 +3809,10 @@ vState SyncRes::validateDNSKeys(const DNSName& zone, const std::vector s_maxvalidationsperq) { + throw ImmediateServFailException("Server Failure while validating DNSKEYs, too many signature validations for this query"); + } LOG(d_prefix<<": we now have "< s_maxvalidationsperq) { + throw ImmediateServFailException("Server Failure while validating records, too many signature validations for this query"); + } + if (state == vState::Secure) { LOG(d_prefix<<"Secure!"<& 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) @@ -4854,7 +4863,7 @@ bool SyncRes::processRecords(const std::string& prefix, const DNSName& qname, co as described in section 5.3.4 of RFC 4035 and 5.3 of RFC 7129. */ cspmap_t csp = harvestCSPFromNE(ne); - dState res = getDenial(csp, qname, ne.d_qtype.getCode(), false, false, false, wildcardLabelsCount); + dState res = getDenial(csp, qname, ne.d_qtype.getCode(), false, false, d_validationContext, false, wildcardLabelsCount); if (res != dState::NXDOMAIN) { vState st = vState::BogusInvalidDenial; if (res == dState::INSECURE || res == dState::OPTOUT) { diff --git a/pdns/syncres.hh b/pdns/syncres.hh index 1bdf1508b7..0006c6c67c 100644 --- a/pdns/syncres.hh +++ b/pdns/syncres.hh @@ -504,8 +504,9 @@ public: static unsigned int s_serverdownthrottletime; static unsigned int s_nonresolvingnsmaxfails; static unsigned int s_nonresolvingnsthrottletime; - static unsigned int s_ecscachelimitttl; + static unsigned int s_maxvalidationsperq; + static unsigned int s_maxnsec3iterationsperq; static uint8_t s_ecsipv4limit; static uint8_t s_ecsipv6limit; static uint8_t s_ecsipv4cachelimit; @@ -655,6 +656,7 @@ private: std::shared_ptr>> d_outgoingProtobufServers; std::shared_ptr>> d_frameStreamServers; boost::optional d_initialRequestId; + pdns::validation::ValidationContext d_validationContext; asyncresolve_t d_asyncResolve{nullptr}; struct timeval d_now; /* if the client is asking for a DS that does not exist, we need to provide the SOA along with the NSEC(3) proof diff --git a/pdns/validate.cc b/pdns/validate.cc index 2dfd0ae94c..12a2034efc 100644 --- a/pdns/validate.cc +++ b/pdns/validate.cc @@ -1,3 +1,25 @@ +/* + * This file is part of PowerDNS or dnsdist. + * Copyright -- PowerDNS.COM B.V. and its contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * In addition, for the avoidance of any doubt, permission is granted to + * link this program with OpenSSL and to (re)distribute the binaries + * produced as the result of such linking. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + #include "validate.hh" #include "misc.hh" #include "dnssecinfra.hh" @@ -11,6 +33,11 @@ uint16_t g_maxNSEC3Iterations{0}; #define LOG(x) if(g_dnssecLOG) { g_log < > getByTag(const skeyset_t& keys, return ret; } -bool isCoveredByNSEC3Hash(const std::string& h, const std::string& beginHash, const std::string& nextHash) +bool isCoveredByNSEC3Hash(const std::string& hash, const std::string& beginHash, const std::string& nextHash) { - return ((beginHash < h && h < nextHash) || // no wrap BEGINNING --- HASH -- END - (nextHash > h && beginHash > nextHash) || // wrap HASH --- END --- BEGINNING - (nextHash < beginHash && beginHash < h) || // wrap other case END --- BEGINNING --- HASH - (beginHash == nextHash && h != beginHash)); // "we have only 1 NSEC3 record, LOL!" + return ((beginHash < hash && hash < nextHash) || // no wrap BEGINNING --- HASH -- END + (nextHash > hash && beginHash > nextHash) || // wrap HASH --- END --- BEGINNING + (nextHash < beginHash && beginHash < hash) || // wrap other case END --- BEGINNING --- HASH + (beginHash == nextHash && hash != beginHash)); // "we have only 1 NSEC3 record, LOL!" } -bool isCoveredByNSEC3Hash(const DNSName& h, const DNSName& beginHash, const DNSName& nextHash) +bool isCoveredByNSEC3Hash(const DNSName& name, const DNSName& beginHash, const DNSName& nextHash) { - return ((beginHash.canonCompare(h) && h.canonCompare(nextHash)) || // no wrap BEGINNING --- HASH -- END - (h.canonCompare(nextHash) && nextHash.canonCompare(beginHash)) || // wrap HASH --- END --- BEGINNING - (nextHash.canonCompare(beginHash) && beginHash.canonCompare(h)) || // wrap other case END --- BEGINNING --- HASH - (beginHash == nextHash && h != beginHash)); // "we have only 1 NSEC3 record, LOL!" + return ((beginHash.canonCompare(name) && name.canonCompare(nextHash)) || // no wrap BEGINNING --- HASH -- END + (name.canonCompare(nextHash) && nextHash.canonCompare(beginHash)) || // wrap HASH --- END --- BEGINNING + (nextHash.canonCompare(beginHash) && beginHash.canonCompare(name)) || // wrap other case END --- BEGINNING --- HASH + (beginHash == nextHash && name != beginHash)); // "we have only 1 NSEC3 record, LOL!" } bool isCoveredByNSEC(const DNSName& name, const DNSName& begin, const DNSName& next) @@ -88,38 +115,49 @@ static bool nsecProvesENT(const DNSName& name, const DNSName& begin, const DNSNa return begin.canonCompare(name) && next != name && next.isPartOf(name); } -using nsec3HashesCache = std::map, std::string>; - -static std::string getHashFromNSEC3(const DNSName& qname, const std::shared_ptr& nsec3, nsec3HashesCache& cache) +[[nodiscard]] std::string getHashFromNSEC3(const DNSName& qname, uint16_t iterations, const std::string& salt, pdns::validation::ValidationContext& context) { std::string result; - if (g_maxNSEC3Iterations && nsec3->d_iterations > g_maxNSEC3Iterations) { + if (g_maxNSEC3Iterations != 0 && iterations > g_maxNSEC3Iterations) { return result; } - auto key = std::make_tuple(qname, nsec3->d_salt, nsec3->d_iterations); - auto it = cache.find(key); - if (it != cache.end()) + auto key = std::tuple(qname, salt, iterations); + auto iter = context.d_nsec3Cache.find(key); + if (iter != context.d_nsec3Cache.end()) { - return it->second; + return iter->second; } - result = hashQNameWithSalt(nsec3->d_salt, nsec3->d_iterations, qname); - cache[key] = result; + if (context.d_nsec3IterationsRemainingQuota < iterations) { + // we throw here because we cannot take the risk that the result + // be cached, since a different query can try to validate the + // same result with a bigger NSEC3 iterations quota + throw pdns::validation::TooManySEC3IterationsException(); + } + + result = hashQNameWithSalt(salt, iterations, qname); + context.d_nsec3IterationsRemainingQuota -= iterations; + context.d_nsec3Cache[key] = result; return result; } +[[nodiscard]] static std::string getHashFromNSEC3(const DNSName& qname, const NSEC3RecordContent& nsec3, pdns::validation::ValidationContext& context) +{ + return getHashFromNSEC3(qname, nsec3.d_iterations, nsec3.d_salt, context); +} + /* There is no delegation at this exact point if: - the name exists but the NS type is not set - the name does not exist One exception, if the name is covered by an opt-out NSEC3 it doesn't prove that an insecure delegation doesn't exist. */ -bool denialProvesNoDelegation(const DNSName& zone, const std::vector& 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); @@ -141,17 +179,22 @@ bool denialProvesNoDelegation(const DNSName& zone, const std::vector& continue; } - const string h = getHashFromNSEC3(zone, nsec3, cache); - if (h.empty()) { + if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) { + return false; + } + nsec3sConsidered++; + + const string hash = getHashFromNSEC3(zone, *nsec3, context); + if (hash.empty()) { return false; } const string beginHash = fromBase32Hex(record.d_name.getRawLabels()[0]); - if (beginHash == h) { + if (beginHash == hash) { return !nsec3->isSet(QType::NS); } - if (isCoveredByNSEC3Hash(h, beginHash, nsec3->d_nexthash)) { + if (isCoveredByNSEC3Hash(hash, beginHash, nsec3->d_nexthash)) { return !(nsec3->isOptOut()); } } @@ -167,11 +210,7 @@ bool denialProvesNoDelegation(const DNSName& zone, const std::vector& */ bool isWildcardExpanded(unsigned int labelCount, const std::shared_ptr& sign) { - if (sign && sign->d_labels < labelCount) { - return true; - } - - return false; + return sign->d_labels < labelCount; } static bool isWildcardExpanded(const DNSName& owner, const std::vector >& signatures) @@ -187,11 +226,8 @@ static bool isWildcardExpanded(const DNSName& owner, const std::vector& sign) { - if (owner.isWildcard() && (labelCount - 1) == sign->d_labels) { - /* this is a wildcard alright, but it has not been expanded */ - return true; - } - return false; + /* this is a wildcard alright, but it has not been expanded */ + return owner.isWildcard() && (labelCount - 1) == sign->d_labels; } static bool isWildcardExpandedOntoItself(const DNSName& owner, const std::vector >& signatures) @@ -230,10 +266,10 @@ DNSName getNSECOwnerName(const DNSName& initialOwner, const std::vector& nsec) +static bool isNSECAncestorDelegation(const DNSName& signer, const DNSName& owner, const NSECRecordContent& nsec) { - return nsec->isSet(QType::NS) && - !nsec->isSet(QType::SOA) && + return nsec.isSet(QType::NS) && + !nsec.isSet(QType::SOA) && signer.countLabels() < owner.countLabels(); } @@ -248,17 +284,17 @@ static bool provesNoDataWildCard(const DNSName& qname, const uint16_t qtype, con { const DNSName wildcard = g_wildcarddnsname + closestEncloser; LOG("Trying to prove that there is no data in wildcard for "<getZoneRepresentation()<(r); + for (const auto& validset : validrrsets) { + LOG("Do have: "<getZoneRepresentation()<(record); if (!nsec) { continue; } - DNSName owner = getNSECOwnerName(v.first.first, v.second.signatures); + DNSName owner = getNSECOwnerName(validset.first.first, validset.second.signatures); if (owner != wildcard) { continue; } @@ -295,17 +331,17 @@ static bool provesNoWildCard(const DNSName& qname, const uint16_t qtype, const D { LOG("Trying to prove that there is no wildcard for "<getZoneRepresentation()<(r); + for (const auto& validset : validrrsets) { + LOG("Do have: "<getZoneRepresentation()<(records); if (!nsec) { continue; } - const DNSName owner = getNSECOwnerName(v.first.first, v.second.signatures); + const DNSName owner = getNSECOwnerName(validset.first.first, validset.second.signatures); LOG("Comparing owner: "<isSet(QType::DNAME)) { @@ -339,37 +375,38 @@ static bool provesNoWildCard(const DNSName& qname, const uint16_t qtype, const D If `wildcardExists` is not NULL, if will be set to true if a wildcard exists for this qname but doesn't have this qtype. */ -static bool provesNSEC3NoWildCard(const DNSName& closestEncloser, uint16_t const qtype, const cspmap_t& validrrsets, bool* wildcardExists, nsec3HashesCache& cache) +static bool provesNSEC3NoWildCard(const DNSName& closestEncloser, uint16_t const qtype, const cspmap_t& validrrsets, bool* wildcardExists, pdns::validation::ValidationContext& context) { auto wildcard = g_wildcarddnsname + closestEncloser; LOG("Trying to prove that there is no wildcard for "<getZoneRepresentation()<(r); + for (const auto& validset : validrrsets) { + LOG("Do have: "<getZoneRepresentation()<(records); if (!nsec3) { continue; } - const DNSName signer = getSigner(v.second.signatures); - if (!v.first.first.isPartOf(signer)) { + const DNSName signer = getSigner(validset.second.signatures); + if (!validset.first.first.isPartOf(signer)) { continue; } - string h = getHashFromNSEC3(wildcard, nsec3, cache); - if (h.empty()) { + string hash = getHashFromNSEC3(wildcard, *nsec3, context); + if (hash.empty()) { + LOG("Unsupported hash, ignoring"< "<d_nexthash)< "<d_nexthash)<d_nexthash)) { + if (isCoveredByNSEC3Hash(hash, beginHash, nsec3->d_nexthash)) { LOG("\tWildcard hash is covered"<d_next)) { - LOG(name<<" is covered by ("<d_next<<") "); + LOG(name<<" is covered by ("<d_next<<")"); if (nsecProvesENT(name, owner, nsec->d_next)) { LOG("Denies existence of type "< 0 ? 1 : 0)) * maxLabels; +} + /* This function checks whether the existence of qname|qtype is denied by the NSEC and NSEC3 in validrrsets. @@ -481,34 +523,34 @@ dState matchesNSEC(const DNSName& name, uint16_t qtype, const DNSName& nsecOwner useful when we have a positive answer synthesized from a wildcard and we only need to prove that the exact name does not exist. */ - -dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, bool needWildcardProof, unsigned int wildcardLabelsCount) +dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, pdns::validation::ValidationContext& context, bool needWildcardProof, unsigned int wildcardLabelsCount) // NOLINT(readability-function-cognitive-complexity): https://github.com/PowerDNS/pdns/issues/12791 { - nsec3HashesCache cache; bool nsec3Seen = false; if (!needWildcardProof && wildcardLabelsCount == 0) { throw PDNSException("Invalid wildcard labels count for the validation of a positive answer synthesized from a wildcard"); } - for (const auto& v : validrrsets) { - LOG("Do have: "<::max()}; + uint16_t nsec3sConsidered = 0; + for (const auto& validset : validrrsets) { + LOG("Do have: "<getZoneRepresentation()<getZoneRepresentation()<(r); + auto nsec = std::dynamic_pointer_cast(record); if (!nsec) { continue; } - const DNSName owner = getNSECOwnerName(v.first.first, v.second.signatures); - const DNSName signer = getSigner(v.second.signatures); - if (!v.first.first.isPartOf(signer) || !owner.isPartOf(signer) ) { + const DNSName owner = getNSECOwnerName(validset.first.first, validset.second.signatures); + const DNSName signer = getSigner(validset.second.signatures); + if (!validset.first.first.isPartOf(signer) || !owner.isPartOf(signer) ) { continue; } @@ -528,9 +570,9 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16 that (original) owner name other than DS RRs, and all RRs below that owner name regardless of type. */ - if (qname.isPartOf(owner) && isNSECAncestorDelegation(signer, owner, nsec)) { + if (qname.isPartOf(owner) && isNSECAncestorDelegation(signer, owner, *nsec)) { /* this is an "ancestor delegation" NSEC RR */ - if (!(qtype == QType::DS && qname == owner)) { + if (qtype != QType::DS || qname != owner) { LOG("An ancestor delegation NSEC RR can only deny the existence of a DS"<d_next)) { - LOG(qname<<" is covered by ("<d_next<<") "); + LOG(qname<<" is covered by ("<d_next<<")"); if (nsecProvesENT(qname, owner, nsec->d_next)) { if (wantsNoDataProof) { @@ -608,11 +650,9 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16 LOG("Denies existence of type "<isSet(qtype)<<", next: "<d_next<isSet(qtype)<<", next: "<d_next<getZoneRepresentation()<(r); + } else if(validset.first.second==QType::NSEC3) { + for (const auto& record : validset.second.records) { + LOG("\t"<getZoneRepresentation()<(record); if (!nsec3) { continue; } - if (v.second.signatures.empty()) { + if (validset.second.signatures.empty()) { continue; } - const DNSName& hashedOwner = v.first.first; - const DNSName signer = getSigner(v.second.signatures); + const DNSName& hashedOwner = validset.first.first; + const DNSName signer = getSigner(validset.second.signatures); if (!hashedOwner.isPartOf(signer)) { LOG("Owner "<(signer.countLabels())); if (qtype == QType::DS && !qname.isRoot() && signer == qname) { LOG("A NSEC3 RR from the child zone cannot deny the existence of a DS"< 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) { + LOG("Too many NSEC3s for this record"<= numberOfLabelsOfParentZone) { - for(const auto& v : validrrsets) { - if(v.first.second==QType::NSEC3) { - for(const auto& r : v.second.records) { - LOG("\t"<getZoneRepresentation()<(r); + for(const auto& validset : validrrsets) { + if(validset.first.second==QType::NSEC3) { + for(const auto& record : validset.second.records) { + LOG("\t"<getZoneRepresentation()<(record); if (!nsec3) { continue; } - const DNSName signer = getSigner(v.second.signatures); - if (!v.first.first.isPartOf(signer)) { - LOG("Owner "< 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) { + LOG("Too many NSEC3s for this record"<= 1) { DNSName nextCloser(closestEncloser); nextCloser.prependRawLabel(qname.getRawLabel(labelIdx - 1)); + nsec3sConsidered = 0; LOG("Looking for a NSEC3 covering the next closer name "<getZoneRepresentation()<(r); - if(!nsec3) + for (const auto& validset : validrrsets) { + if (validset.first.second == QType::NSEC3) { + for (const auto& record : validset.second.records) { + LOG("\t"<getZoneRepresentation()<(record); + if (!nsec3) { continue; + } - string h = getHashFromNSEC3(nextCloser, nsec3, cache); - if (h.empty()) { + if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) { + LOG("Too many NSEC3s for this record"< "<d_nexthash)<d_nexthash)) { + LOG("Comparing "< "<d_nexthash)<d_nexthash)) { LOG("Denies existence of name "< getZoneCuts(const DNSName& begin, const DNSName& end, DNSRecordOracle& dro) -{ - vector ret; - if(!begin.isPartOf(end)) - throw PDNSException(end.toLogString() + "is not part of " + begin.toLogString()); - - DNSName qname(end); - vector labelsToAdd = begin.makeRelative(end).getRawLabels(); - - // The shortest name is assumed to a zone cut - ret.push_back(qname); - while(qname != begin) { - bool foundCut = false; - if (labelsToAdd.empty()) - break; - - qname.prependRawLabel(labelsToAdd.back()); - labelsToAdd.pop_back(); - auto records = dro.get(qname, (uint16_t)QType::NS); - for (const auto& record : records) { - if(record.d_type != QType::NS || record.d_name != qname) - continue; - foundCut = true; - break; - } - if (foundCut) - ret.push_back(qname); - } - return ret; -} - -bool isRRSIGNotExpired(const time_t now, const shared_ptr& sig) +bool isRRSIGNotExpired(const time_t now, const std::shared_ptr& sig) { // Should use https://www.rfc-editor.org/rfc/rfc4034.txt section 3.1.5 return sig->d_sigexpire >= now; } -bool isRRSIGIncepted(const time_t now, const shared_ptr& sig) +bool isRRSIGIncepted(const time_t now, const std::shared_ptr& sig) { // Should use https://www.rfc-editor.org/rfc/rfc4034.txt section 3.1.5 return sig->d_siginception - g_signatureInceptionSkew <= now; } -static bool checkSignatureWithKey(time_t now, const shared_ptr sig, const shared_ptr key, const std::string& msg, vState& ede) +namespace { + [[nodiscard]] bool checkSignatureInceptionAndExpiry(const DNSName& qname, time_t now, const std::shared_ptr& sig, vState& ede) +{ + /* rfc4035: + - The validator's notion of the current time MUST be less than or equal to the time listed in the RRSIG RR's Expiration field. + - The validator's notion of the current time MUST be greater than or equal to the time listed in the RRSIG RR's Inception field. + */ + if (isRRSIGIncepted(now, sig) && isRRSIGNotExpired(now, sig)) { + return true; + } + ede = ((sig->d_siginception - g_signatureInceptionSkew) > now) ? vState::BogusSignatureNotYetValid : vState::BogusSignatureExpired; + LOG("Signature is "<<(ede == vState::BogusSignatureNotYetValid ? "not yet valid" : "expired")<<" (inception: "<d_siginception<<", inception skew: "<d_sigexpire<<", now: "<& sig, const std::shared_ptr& key, const std::string& msg, vState& ede) { bool result = false; try { - /* rfc4035: - - The validator's notion of the current time MUST be less than or equal to the time listed in the RRSIG RR's Expiration field. - - The validator's notion of the current time MUST be greater than or equal to the time listed in the RRSIG RR's Inception field. - */ - if (isRRSIGIncepted(now, sig) && isRRSIGNotExpired(now, sig)) { - auto dke = DNSCryptoKeyEngine::makeFromPublicKeyString(key->d_algorithm, key->d_key); - result = dke->verify(msg, sig->d_signature); - LOG("signature by key with tag "<d_tag<<" and algorithm "<d_algorithm)<<" was " << (result ? "" : "NOT ")<<"valid"<d_algorithm, key->d_key); + result = dke->verify(msg, sig->d_signature); + LOG("Signature by key with tag "<d_tag<<" and algorithm "<d_algorithm)<<" was " << (result ? "" : "NOT ")<<"valid"<d_siginception - g_signatureInceptionSkew) > now) ? vState::BogusSignatureNotYetValid : vState::BogusSignatureExpired; - LOG("Signature is "<<(ede == vState::BogusSignatureNotYetValid ? "not yet valid" : "expired")<<" (inception: "<d_siginception<<", inception skew: "<d_sigexpire<<", now: "< >& signatures, const skeyset_t& keys, bool validateAllSigs) +} + +vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& toSign, const vector >& signatures, const skeyset_t& keys, pdns::validation::ValidationContext& context, bool validateAllSigs) { - bool foundKey = false; + bool missingKey = false; bool isValid = false; bool allExpired = true; bool noneIncepted = true; + uint16_t signaturesConsidered = 0; - for(const auto& signature : signatures) { + for (const auto& signature : signatures) { unsigned int labelCount = name.countLabels(); if (signature->d_labels > labelCount) { LOG(name<<": Discarding invalid RRSIG whose label count is "<d_labels<<" while the RRset owner name has only "< 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) { + LOG("We have already considered "<d_tag, signature->d_algorithm); if (keysMatchingTag.empty()) { LOG("No key provided for "<d_tag<<" and algorithm "<d_algorithm)< 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) { + LOG("We have already considered "<d_tag)<<" and algorithm "<d_algorithm)<<", not considering the remaining ones for this signature"<getTag()<<" -> "<getZoneRepresentation()<first.first)<<"/"<first.second)<<" with "<second.signatures.size()<<" sigs"<first.first, i->second.records, i->second.signatures, keys, true) == vState::Secure) { - validated[i->first] = i->second; - } - } -} - // returns vState // should return vState, zone cut and validated keyset // i.e. www.7bits.nl -> insecure/7bits.nl/[] @@ -1068,7 +1110,9 @@ cspmap_t harvestCSPFromRecs(const vector& recs) cspmap_t cspmap; for(const auto& rec : recs) { // cerr<<"res "<(rec); @@ -1085,44 +1129,60 @@ cspmap_t harvestCSPFromRecs(const vector& recs) bool getTrustAnchor(const map& anchors, const DNSName& zone, dsmap_t &res) { - const auto& it = anchors.find(zone); + const auto& iter = anchors.find(zone); - if (it == anchors.cend()) { + if (iter == anchors.cend()) { return false; } - res = it->second; + res = iter->second; return true; } bool haveNegativeTrustAnchor(const map& negAnchors, const DNSName& zone, std::string& reason) { - const auto& it = negAnchors.find(zone); + const auto& iter = negAnchors.find(zone); - if (it == negAnchors.cend()) { + if (iter == negAnchors.cend()) { return false; } - reason = it->second; + reason = iter->second; return true; } -vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector >& sigs, skeyset_t& validkeys) +vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector >& sigs, skeyset_t& validkeys, pdns::validation::ValidationContext& context) { /* * Check all DNSKEY records against all DS records and place all DNSKEY records * that have DS records (that we support the algo for) in the tentative key storage */ - for (const auto& dsrc : dsmap) - { - auto r = getByTag(tkeys, dsrc.d_tag, dsrc.d_algorithm); + uint16_t dssConsidered = 0; + for (const auto& dsrc : dsmap) { + if (g_maxDSsToConsider > 0 && dssConsidered > g_maxDSsToConsider) { + LOG("We have already considered "< 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) { + LOG("We have already considered "<d_tag<<" matching "<d_tag).size()<<" keys of which "<d_tag).size()<<" valid"<d_tag, sig->d_algorithm); @@ -1165,20 +1228,42 @@ vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& continue; } + if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) { + LOG("We have already considered "< 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) { + LOG("We have already considered "<d_tag)<<" and algorithm "<d_algorithm)<<", not considering the remaining ones for this signature"< 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) { + LOG("We have already considered "<dsAnchors; - if (anchors.empty()) // Nothing to do here - return vState::Insecure; - - // Determine the lowest (i.e. with the most labels) Trust Anchor for zone - DNSName lowestTA("."); - for (auto const &anchor : anchors) - if (zone.isPartOf(anchor.first) && lowestTA.countLabels() < anchor.first.countLabels()) - lowestTA = anchor.first; - - // Before searching for the keys, see if we have a Negative Trust Anchor. If - // so, test if the NTA is valid and return an NTA state - const auto negAnchors = luaLocal->negAnchors; - - if (!negAnchors.empty()) { - DNSName lowestNTA; - - for (auto const &negAnchor : negAnchors) - if (zone.isPartOf(negAnchor.first) && lowestNTA.countLabels() <= negAnchor.first.countLabels()) - lowestNTA = negAnchor.first; - - if(!lowestNTA.empty()) { - LOG("Found a Negative Trust Anchor for "< "< > sigs; - sortedRecords_t toSign; - - skeyset_t tkeys; // tentative keys - validkeys.clear(); - - // cerr<<"got DS for ["< (rec); - if(rrc) { - LOG("Got signature: "<getZoneRepresentation()<<" with tag "<d_tag<<", for type "<d_type)<d_type != QType::DNSKEY) - continue; - sigs.push_back(rrc); - } - } - else if(rec.d_type == QType::DNSKEY) - { - auto drc=getRR (rec); - if(drc) { - tkeys.insert(drc); - LOG("Inserting key with tag "<getTag()<<" and algorithm "<d_algorithm)<<": "<getZoneRepresentation()<second.records.cbegin(); j!=cspiter->second.records.cend(); j++) - { - const auto dsrc=std::dynamic_pointer_cast(*j); - if(dsrc) { - dsmap.insert(*dsrc); - } - } - } - } - // There were no zone cuts (aka, we should never get here) - return vState::BogusUnableToGetDNSKEYs; -} - -bool isSupportedDS(const DSRecordContent& ds) -{ - if (!DNSCryptoKeyEngine::isAlgorithmSupported(ds.d_algorithm)) { - LOG("Discarding DS "< >& signa } } - return DNSName(); + return {}; } const std::string& vStateToString(vState state) @@ -1429,17 +1360,17 @@ const std::string& vStateToString(vState state) return vStates.at(static_cast(state)); } -std::ostream& operator<<(std::ostream &os, const vState d) +std::ostream& operator<<(std::ostream &ostr, const vState dstate) { - os< dStates = {"no denial", "inconclusive", "nxdomain", "nxqtype", "empty non-terminal", "insecure", "opt-out"}; - os<(d)); - return os; + ostr<(dstate)); + return ostr; } void updateDNSSECValidationState(vState& state, const vState stateUpdate) @@ -1450,10 +1381,7 @@ void updateDNSSECValidationState(vState& state, const vState stateUpdate) else if (stateUpdate == vState::NTA) { state = vState::Insecure; } - else if (vStateIsBogus(stateUpdate)) { - state = stateUpdate; - } - else if (state == vState::Indeterminate) { + else if (vStateIsBogus(stateUpdate) || state == vState::Indeterminate) { state = stateUpdate; } else if (stateUpdate == vState::Insecure) { diff --git a/pdns/validate.hh b/pdns/validate.hh index ec11f0b27b..0f80b7dffc 100644 --- a/pdns/validate.hh +++ b/pdns/validate.hh @@ -21,16 +21,22 @@ */ #pragma once +#include + #include "dnsparser.hh" #include "dnsname.hh" #include #include "namespaces.hh" #include "dnsrecords.hh" #include "dnssecinfra.hh" - + extern bool g_dnssecLOG; extern time_t g_signatureInceptionSkew; extern uint16_t g_maxNSEC3Iterations; +extern uint16_t g_maxRRSIGsPerRecordToConsider; +extern uint16_t g_maxNSEC3sPerRecordToConsider; +extern uint16_t g_maxDNSKEYsToConsider; +extern uint16_t g_maxDSsToConsider; // 4033 5 enum class vState : uint8_t { Indeterminate, Insecure, Secure, NTA, TA, BogusNoValidDNSKEY, BogusInvalidDenial, BogusUnableToGetDSs, BogusUnableToGetDNSKEYs, BogusSelfSignedDS, BogusNoRRSIG, BogusNoValidRRSIG, BogusMissingNegativeIndication, BogusSignatureNotYetValid, BogusSignatureExpired, BogusUnsupportedDNSKEYAlgo, BogusUnsupportedDSDigestType, BogusNoZoneKeyBitSet, BogusRevokedDNSKEY, BogusInvalidDNSKEYProtocol }; @@ -72,8 +78,30 @@ struct sharedDNSKeyRecordContentCompare typedef set, sharedDNSKeyRecordContentCompare > skeyset_t; +namespace pdns +{ +namespace validation +{ +using Nsec3HashesCache = std::map, std::string>; + +struct ValidationContext +{ + Nsec3HashesCache d_nsec3Cache; + unsigned int d_validationsCounter{0}; + unsigned int d_nsec3IterationsRemainingQuota{0}; +}; -vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& records, const vector >& signatures, const skeyset_t& keys, bool validateAllSigs=true); +class TooManySEC3IterationsException : public std::runtime_error +{ +public: + TooManySEC3IterationsException(): std::runtime_error("Too many NSEC3 hash computations per query") + { + } +}; + +} +} +vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& records, const vector >& signatures, const skeyset_t& keys, pdns::validation::ValidationContext& context, bool validateAllSigs=true); bool isCoveredByNSEC(const DNSName& name, const DNSName& begin, const DNSName& next); bool isCoveredByNSEC3Hash(const std::string& h, const std::string& beginHash, const std::string& nextHash); bool isCoveredByNSEC3Hash(const DNSName& h, const DNSName& beginHash, const DNSName& nextHash); @@ -82,11 +110,11 @@ cspmap_t harvestCSPFromRecs(const vector& recs); vState getKeysFor(DNSRecordOracle& dro, const DNSName& zone, skeyset_t& keyset); 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); -dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, bool needsWildcardProof=true, unsigned int wildcardLabelsCount=0); +vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector >& sigs, skeyset_t& validkeys, pdns::validation::ValidationContext& context); +dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, pdns::validation::ValidationContext& context, bool needsWildcardProof=true, unsigned int wildcardLabelsCount=0); bool isSupportedDS(const DSRecordContent& ds); DNSName getSigner(const std::vector >& 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(const time_t now, const std::shared_ptr& sig); bool isRRSIGIncepted(const time_t now, const shared_ptr& sig); bool isWildcardExpanded(unsigned int labelCount, const std::shared_ptr& sign); @@ -98,6 +126,8 @@ dState matchesNSEC(const DNSName& name, uint16_t qtype, const DNSName& nsecOwner bool isNSEC3AncestorDelegation(const DNSName& signer, const DNSName& owner, const std::shared_ptr& 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 a4640176c6..1e8e47cde7 100644 --- a/regression-tests.recursor-dnssec/test_AggressiveNSECCache.py +++ b/regression-tests.recursor-dnssec/test_AggressiveNSECCache.py @@ -14,6 +14,8 @@ class AggressiveNSECCacheBase(RecursorTest): _config_template = """ dnssec=validate aggressive-nsec-cache-size=10000 + aggressive-cache-max-nsec3-hash-cost=204 + nsec3-max-iterations=150 webserver=yes webserver-port=%d webserver-address=127.0.0.1 -- 2.47.2