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,
}
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;
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()) {
#include "validate.hh"
std::unique_ptr<AggressiveNSECCache> g_aggressiveNSECCache{nullptr};
+uint64_t AggressiveNSECCache::s_nsec3DenialProofMaxCost{0};
/* this is defined in syncres.hh and we are not importing that here */
extern std::unique_ptr<MemRecursorCache> g_recCache;
return true;
}
-bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded<AggressiveNSECCache::ZoneEntry>>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC)
+bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded<AggressiveNSECCache::ZoneEntry>>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, pdns::validation::ValidationContext& validationContext)
{
DNSName zone;
std::string salt;
iterations = entry->d_iterations;
}
- auto nameHash = DNSName(toBase32Hex(hashQNameWithSalt(salt, iterations, name))) + zone;
+ const auto zoneLabelsCount = zone.countLabels();
+ if (s_nsec3DenialProofMaxCost != 0) {
+ const auto worstCaseIterations = getNSEC3DenialProofWorstCaseIterationsCount(name.countLabels() - zoneLabelsCount, iterations, salt.length());
+ if (worstCaseIterations > s_nsec3DenialProofMaxCost) {
+ // skip NSEC3 aggressive cache for expensive NSEC3 parameters: "if you want us to take the pain of PRSD away from you, you need to make it cheap for us to do so"
+ LOG(name << ": Skipping aggressive use of the NSEC3 cache since the zone parameters are too expensive" << endl);
+ return false;
+ }
+ }
+
+ auto nameHash = DNSName(toBase32Hex(getHashFromNSEC3(name, iterations, salt, validationContext))) + zone;
ZoneEntry::CacheEntry exactNSEC3;
if (getNSEC3(now, zoneEntry, nameHash, exactNSEC3)) {
DNSName closestEncloser(name);
bool found = false;
ZoneEntry::CacheEntry closestNSEC3;
- while (!found && closestEncloser.chopOff()) {
- auto closestHash = DNSName(toBase32Hex(hashQNameWithSalt(salt, iterations, closestEncloser))) + zone;
+ auto remainingLabels = closestEncloser.countLabels() - 1;
+ while (!found && closestEncloser.chopOff() && remainingLabels >= zoneLabelsCount) {
+ auto closestHash = DNSName(toBase32Hex(getHashFromNSEC3(closestEncloser, iterations, salt, validationContext))) + zone;
+ remainingLabels--;
if (getNSEC3(now, zoneEntry, closestHash, closestNSEC3)) {
LOG("Found closest encloser at " << closestEncloser << " (" << closestHash << ")" << endl);
return true;
}
-bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, const ComboAddress& who, const boost::optional<std::string>& routingTag, bool doDNSSEC)
+bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, const ComboAddress& who, const boost::optional<std::string>& routingTag, bool doDNSSEC, pdns::validation::ValidationContext& validationContext)
{
std::shared_ptr<LockGuarded<ZoneEntry>> zoneEntry;
if (type == QType::DS) {
}
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;
#include "dnsrecords.hh"
#include "lock.hh"
#include "stat_t.hh"
+#include "validate.hh"
class AggressiveNSECCache
{
public:
+ static uint64_t s_nsec3DenialProofMaxCost;
+
AggressiveNSECCache(uint64_t entries) :
d_maxEntries(entries)
{
}
void insertNSEC(const DNSName& zone, const DNSName& owner, const DNSRecord& record, const std::vector<std::shared_ptr<RRSIGRecordContent>>& signatures, bool nsec3);
- bool getDenial(time_t, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, const ComboAddress& who, const boost::optional<std::string>& routingTag, bool doDNSSEC);
+ bool getDenial(time_t, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, const ComboAddress& who, const boost::optional<std::string>& routingTag, bool doDNSSEC, pdns::validation::ValidationContext& validationContext);
void removeZoneInfo(const DNSName& zone, bool subzones);
std::shared_ptr<LockGuarded<ZoneEntry>> getBestZone(const DNSName& zone);
bool getNSECBefore(time_t now, std::shared_ptr<LockGuarded<ZoneEntry>>& zoneEntry, const DNSName& name, ZoneEntry::CacheEntry& entry);
bool getNSEC3(time_t now, std::shared_ptr<LockGuarded<ZoneEntry>>& zoneEntry, const DNSName& name, ZoneEntry::CacheEntry& entry);
- bool getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded<ZoneEntry>>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC);
+ bool getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded<ZoneEntry>>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, pdns::validation::ValidationContext& validationContext);
bool synthesizeFromNSEC3Wildcard(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, ZoneEntry::CacheEntry& nextCloser, const DNSName& wildcardName);
bool synthesizeFromNSECWildcard(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, ZoneEntry::CacheEntry& nsec, const DNSName& wildcardName);
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");
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");
g_addExtendedResolutionDNSErrors = ::arg().mustDo("extended-resolution-errors");
+ AggressiveNSECCache::s_nsec3DenialProofMaxCost = ::arg().asNum("aggressive-cache-max-nsec3-hash-cost");
if (::arg().asNum("aggressive-nsec-cache-size") > 0) {
if (g_dnssecmode == DNSSECMode::ValidateAll || g_dnssecmode == DNSSECMode::ValidateForLog || g_dnssecmode == DNSSECMode::Process) {
g_aggressiveNSECCache = make_unique<AggressiveNSECCache>(::arg().asNum("aggressive-nsec-cache-size"));
::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";
vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const
{
+ pdns::validation::ValidationContext validationContext;
+ validationContext.d_nsec3IterationsRemainingQuota = std::numeric_limits<decltype(validationContext.d_nsec3IterationsRemainingQuota)>::max();
zonemdCount = 0;
SyncRes sr({d_now, 0});
}
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;
}
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) {
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 {
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;
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)
free(line);
}
+static bool getDenialWrapper(std::unique_ptr<AggressiveNSECCache>& cache, time_t now, const DNSName& name, const QType& qtype, const std::optional<int> expectedResult = std::nullopt, const std::optional<size_t> expectedRecordsCount = std::nullopt)
+{
+ int res;
+ std::vector<DNSRecord> results;
+ pdns::validation::ValidationContext validationContext;
+ validationContext.d_nsec3IterationsRemainingQuota = std::numeric_limits<decltype(validationContext.d_nsec3IterationsRemainingQuota)>::max();
+ bool found = cache->getDenial(now, name, qtype, results, res, ComboAddress("192.0.2.1"), boost::none, true, validationContext);
+ if (expectedResult) {
+ BOOST_CHECK_EQUAL(res, *expectedResult);
+ }
+ if (expectedRecordsCount) {
+ BOOST_CHECK_EQUAL(results.size(), *expectedRecordsCount);
+ }
+ return found;
+}
+
BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_rollover)
{
/* test that we don't compare a hash using the wrong (former) salt or iterations count in case of a rollover,
BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U);
- int res;
- std::vector<DNSRecord> results;
-
/* we can use the NSEC3s we have */
/* direct match */
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA), true);
DNSName other("other.powerdns.com");
/* now we insert a new NSEC3, with a different salt, changing that value for the zone */
/* 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");
/* 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)
BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U);
/* the cache should now be able to deny other types (except the DS) */
- int res;
- std::vector<DNSRecord> results;
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
- BOOST_CHECK_EQUAL(res, RCode::NoError);
- BOOST_CHECK_EQUAL(results.size(), 3U);
+ /* the cache should now be able to deny other types (except the DS) */
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NoError, 3U), true);
/* but not the DS that lives in the parent zone */
- results.clear();
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), false);
- BOOST_CHECK_EQUAL(results.size(), 0U);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, std::nullopt, 0U), false);
}
{
BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U);
/* the cache should now be able to deny the DS */
- int res;
- std::vector<DNSRecord> results;
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
- BOOST_CHECK_EQUAL(res, RCode::NoError);
- BOOST_CHECK_EQUAL(results.size(), 3U);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NoError, 3U), true);
/* but not any type that lives in the child zone */
- results.clear();
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA), false);
}
{
BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U);
/* the cache should now be able to deny other types */
- int res;
- std::vector<DNSRecord> results;
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
- BOOST_CHECK_EQUAL(res, RCode::NoError);
- BOOST_CHECK_EQUAL(results.size(), 3U);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NoError, 3U), true);
/* including the DS */
- results.clear();
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
- BOOST_CHECK_EQUAL(res, RCode::NoError);
- BOOST_CHECK_EQUAL(results.size(), 3U);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NoError, 3U), true);
}
{
}
/* the cache should now be able to deny any type for the name */
- int res;
- std::vector<DNSRecord> results;
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
- BOOST_CHECK_EQUAL(res, RCode::NXDomain);
- BOOST_CHECK_EQUAL(results.size(), 5U);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NXDomain, 5U), true);
/* including the DS, since we are not at the apex */
- results.clear();
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
- BOOST_CHECK_EQUAL(res, RCode::NXDomain);
- BOOST_CHECK_EQUAL(results.size(), 5U);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NXDomain, 5U), true);
}
}
BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U);
/* the cache should now be able to deny other types (except the DS) */
- int res;
- std::vector<DNSRecord> results;
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
- BOOST_CHECK_EQUAL(res, RCode::NoError);
- BOOST_CHECK_EQUAL(results.size(), 3U);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NoError, 3U), true);
/* but not the DS that lives in the parent zone */
- results.clear();
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), false);
- BOOST_CHECK_EQUAL(results.size(), 0U);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, std::nullopt, 0U), false);
}
{
BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U);
/* the cache should now be able to deny the DS */
- int res;
- std::vector<DNSRecord> results;
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
- BOOST_CHECK_EQUAL(res, RCode::NoError);
- BOOST_CHECK_EQUAL(results.size(), 3U);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NoError, 3U), true);
/* but not any type that lives in the child zone */
- results.clear();
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA), false);
}
{
BOOST_CHECK_EQUAL(cache->getEntriesCount(), 1U);
/* the cache should now be able to deny other types */
- int res;
- std::vector<DNSRecord> results;
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
- BOOST_CHECK_EQUAL(res, RCode::NoError);
- BOOST_CHECK_EQUAL(results.size(), 3U);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NoError, 3U), true);
/* including the DS */
- results.clear();
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
- BOOST_CHECK_EQUAL(res, RCode::NoError);
- BOOST_CHECK_EQUAL(results.size(), 3U);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NoError, 3U), true);
}
{
}
/* the cache should now be able to deny any type for the name */
- int res;
- std::vector<DNSRecord> results;
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
- BOOST_CHECK_EQUAL(res, RCode::NXDomain);
- BOOST_CHECK_EQUAL(results.size(), 7U);
-
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, RCode::NXDomain, 7U), true);
/* including the DS, since we are not at the apex */
- results.clear();
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), true);
- BOOST_CHECK_EQUAL(res, RCode::NXDomain);
- BOOST_CHECK_EQUAL(results.size(), 7U);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, RCode::NXDomain, 7U), true);
}
{
/* we insert NSEC3s coming from the parent zone that could look like a valid denial but are not */
}
/* the cache should NOT be able to deny the name */
- int res;
- std::vector<DNSRecord> results;
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::AAAA, results, res, ComboAddress("192.0.2.1"), boost::none, true), false);
- BOOST_CHECK_EQUAL(results.size(), 0U);
-
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::AAAA, std::nullopt, 0U), false);
/* and the same for the DS */
- results.clear();
- BOOST_CHECK_EQUAL(cache->getDenial(now, name, QType::DS, results, res, ComboAddress("192.0.2.1"), boost::none, true), false);
- BOOST_CHECK_EQUAL(results.size(), 0U);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, name, QType::DS, std::nullopt, 0U), false);
+ }
+}
+
+BOOST_AUTO_TEST_CASE(test_aggressive_max_nsec3_hash_cost)
+{
+ g_recCache = std::make_unique<MemRecursorCache>();
+
+ const DNSName zone("powerdns.com");
+ time_t now = time(nullptr);
+
+ /* first we need a SOA */
+ std::vector<DNSRecord> records;
+ time_t ttd = now + 30;
+ DNSRecord drSOA;
+ drSOA.d_name = zone;
+ drSOA.d_type = QType::SOA;
+ drSOA.d_class = QClass::IN;
+ drSOA.d_content = std::make_shared<SOARecordContent>("pdns-public-ns1.powerdns.com. pieter\\.lexis.powerdns.com. 2017032301 10800 3600 604800 3600");
+ drSOA.d_ttl = static_cast<uint32_t>(ttd); // XXX truncation
+ drSOA.d_place = DNSResourceRecord::ANSWER;
+ records.push_back(drSOA);
+
+ g_recCache->replace(now, zone, QType(QType::SOA), records, {}, {}, true, zone, boost::none, boost::none, vState::Secure);
+ BOOST_CHECK_EQUAL(g_recCache->size(), 1U);
+
+ auto insertNSEC3s = [zone, now](std::unique_ptr<AggressiveNSECCache>& cache, const std::string& salt, unsigned int iterationsCount) -> void {
+ {
+ /* insert a NSEC3 matching the apex (will be the closest encloser) */
+ DNSName name("powerdns.com");
+ std::string hashed = hashQNameWithSalt(salt, iterationsCount, name);
+ DNSRecord rec;
+ rec.d_name = DNSName(toBase32Hex(hashed)) + zone;
+ rec.d_type = QType::NSEC3;
+ rec.d_ttl = now + 10;
+
+ NSEC3RecordContent nrc;
+ nrc.d_algorithm = 1;
+ nrc.d_flags = 0;
+ nrc.d_iterations = iterationsCount;
+ nrc.d_salt = salt;
+ nrc.d_nexthash = hashed;
+ incrementHash(nrc.d_nexthash);
+ for (const auto& type : {QType::A}) {
+ nrc.set(type);
+ }
+
+ rec.d_content = std::make_shared<NSEC3RecordContent>(nrc);
+ auto rrsig = std::make_shared<RRSIGRecordContent>("NSEC3 5 3 10 20370101000000 20370101000000 24567 powerdns.com. data");
+ cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true);
+ }
+ {
+ /* insert a NSEC3 matching *.powerdns.com (wildcard) */
+ DNSName name("*.powerdns.com");
+ std::string hashed = hashQNameWithSalt(salt, iterationsCount, name);
+ auto before = hashed;
+ decrementHash(before);
+ DNSRecord rec;
+ rec.d_name = DNSName(toBase32Hex(before)) + zone;
+ rec.d_type = QType::NSEC3;
+ rec.d_ttl = now + 10;
+
+ NSEC3RecordContent nrc;
+ nrc.d_algorithm = 1;
+ nrc.d_flags = 0;
+ nrc.d_iterations = iterationsCount;
+ nrc.d_salt = salt;
+ nrc.d_nexthash = hashed;
+ incrementHash(nrc.d_nexthash);
+ for (const auto& type : {QType::A}) {
+ nrc.set(type);
+ }
+
+ rec.d_content = std::make_shared<NSEC3RecordContent>(nrc);
+ auto rrsig = std::make_shared<RRSIGRecordContent>("NSEC3 5 3 10 20370101000000 20370101000000 24567 powerdns.com. data");
+ cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true);
+ }
+ {
+ /* insert a NSEC3 matching sub.powerdns.com (next closer) */
+ DNSName name("sub.powerdns.com");
+ std::string hashed = hashQNameWithSalt(salt, iterationsCount, name);
+ auto before = hashed;
+ decrementHash(before);
+ DNSRecord rec;
+ rec.d_name = DNSName(toBase32Hex(before)) + zone;
+ rec.d_type = QType::NSEC3;
+ rec.d_ttl = now + 10;
+
+ NSEC3RecordContent nrc;
+ nrc.d_algorithm = 1;
+ nrc.d_flags = 0;
+ nrc.d_iterations = iterationsCount;
+ nrc.d_salt = salt;
+ nrc.d_nexthash = hashed;
+ incrementHash(nrc.d_nexthash);
+ for (const auto& type : {QType::A}) {
+ nrc.set(type);
+ }
+
+ rec.d_content = std::make_shared<NSEC3RecordContent>(nrc);
+ auto rrsig = std::make_shared<RRSIGRecordContent>("NSEC3 5 3 10 20370101000000 20370101000000 24567 powerdns.com. data");
+ cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true);
+ }
+ BOOST_CHECK_EQUAL(cache->getEntriesCount(), 3U);
+ };
+
+ {
+ /* zone with cheap parameters */
+ const std::string salt;
+ const unsigned int iterationsCount = 0;
+ AggressiveNSECCache::s_nsec3DenialProofMaxCost = 10;
+
+ auto cache = make_unique<AggressiveNSECCache>(10000);
+ insertNSEC3s(cache, salt, iterationsCount);
+
+ /* the cache should now be able to deny everything below sub.powerdns.com,
+ IF IT DOES NOT EXCEED THE COST */
+ {
+ /* short name: 10 labels below the zone apex */
+ DNSName lookupName("a.b.c.d.e.f.g.h.i.sub.powerdns.com.");
+ BOOST_CHECK_EQUAL(lookupName.countLabels() - zone.countLabels(), 10U);
+ BOOST_CHECK_LE(getNSEC3DenialProofWorstCaseIterationsCount(lookupName.countLabels() - zone.countLabels(), iterationsCount, salt.size()), AggressiveNSECCache::s_nsec3DenialProofMaxCost);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, lookupName, QType::AAAA, RCode::NXDomain, 7U), true);
+ }
+ {
+ /* longer name: 11 labels below the zone apex */
+ DNSName lookupName("a.b.c.d.e.f.g.h.i.j.sub.powerdns.com.");
+ BOOST_CHECK_EQUAL(lookupName.countLabels() - zone.countLabels(), 11U);
+ BOOST_CHECK_GT(getNSEC3DenialProofWorstCaseIterationsCount(lookupName.countLabels() - zone.countLabels(), iterationsCount, salt.size()), AggressiveNSECCache::s_nsec3DenialProofMaxCost);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, lookupName, QType::AAAA), false);
+ }
+ }
+
+ {
+ /* zone with expensive parameters */
+ const std::string salt("deadbeef");
+ const unsigned int iterationsCount = 50;
+ AggressiveNSECCache::s_nsec3DenialProofMaxCost = 100;
+
+ auto cache = make_unique<AggressiveNSECCache>(10000);
+ insertNSEC3s(cache, salt, iterationsCount);
+
+ /* the cache should now be able to deny everything below sub.powerdns.com,
+ IF IT DOES NOT EXCEED THE COST */
+ {
+ /* short name: 1 label below the zone apex */
+ DNSName lookupName("sub.powerdns.com.");
+ BOOST_CHECK_EQUAL(lookupName.countLabels() - zone.countLabels(), 1U);
+ BOOST_CHECK_LE(getNSEC3DenialProofWorstCaseIterationsCount(lookupName.countLabels() - zone.countLabels(), iterationsCount, salt.size()), AggressiveNSECCache::s_nsec3DenialProofMaxCost);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, lookupName, QType::AAAA, RCode::NXDomain, 7U), true);
+ }
+ {
+ /* longer name: 2 labels below the zone apex */
+ DNSName lookupName("a.sub.powerdns.com.");
+ BOOST_CHECK_EQUAL(lookupName.countLabels() - zone.countLabels(), 2U);
+ BOOST_CHECK_GT(getNSEC3DenialProofWorstCaseIterationsCount(lookupName.countLabels() - zone.countLabels(), iterationsCount, salt.size()), AggressiveNSECCache::s_nsec3DenialProofMaxCost);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, lookupName, QType::AAAA), false);
+ }
}
}
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) {
}
RRSIGRecordContent rrc;
- computeRRSIG(it->second.first, signer, wildcard ? *wildcard : records[recordsCount - 1].d_name, records[recordsCount - 1].d_type, records[recordsCount - 1].d_ttl, sigValidity, rrc, recordcontents, algo, boost::none, now);
+ computeRRSIG(it->second.first, signer, wildcard ? *wildcard : name, type, ttl, sigValidity, rrc, recordcontents, algo, boost::none, now);
if (broken) {
rrc.d_signature[0] ^= 42;
}
DNSRecord rec;
rec.d_type = QType::RRSIG;
- rec.d_place = records[recordsCount - 1].d_place;
- rec.d_name = records[recordsCount - 1].d_name;
- rec.d_ttl = records[recordsCount - 1].d_ttl;
+ rec.d_place = place;
+ rec.d_name = name;
+ rec.d_ttl = ttl;
rec.d_content = std::make_shared<RRSIGRecordContent>(rrc);
records.push_back(rec);
auto dcke = DNSCryptoKeyEngine::make(DNSSECKeeper::ECDSA256);
dcke->create(dcke->getBits());
- // cerr<<dcke->convertToISC()<<endl;
DNSSECPrivateKey dpk;
dpk.d_flags = 256;
dpk.setKey(std::move(dcke));
std::vector<std::shared_ptr<RRSIGRecordContent>> sigs;
sigs.push_back(std::make_shared<RRSIGRecordContent>(rrc));
- BOOST_CHECK(validateWithKeySet(now, qname, recordcontents, sigs, keyset) == vState::Secure);
-}
+ pdns::validation::ValidationContext validationContext;
+ BOOST_CHECK(validateWithKeySet(now, qname, recordcontents, sigs, keyset, validationContext) == vState::Secure);
+ BOOST_CHECK_EQUAL(validationContext.d_validationsCounter, 1U);}
BOOST_AUTO_TEST_CASE(test_dnssec_root_validation_csk)
{
BOOST_REQUIRE_EQUAL(ret.size(), 14U);
BOOST_CHECK_EQUAL(queriesCount, 2U);
}
+
BOOST_AUTO_TEST_CASE(test_dnssec_bogus_dnskey_doesnt_match_ds)
{
std::unique_ptr<SyncRes> sr;
BOOST_CHECK_EQUAL(queriesCount, 4U);
}
+BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_dss)
+{
+ std::unique_ptr<SyncRes> sr;
+ initSR(sr, true);
+
+ setDNSSECValidation(sr, DNSSECMode::Process);
+
+ primeHints();
+ const DNSName target(".");
+ testkeysset_t keys;
+
+ g_maxDSsToConsider = 1;
+
+ auto luaconfsCopy = g_luaconfs.getCopy();
+ luaconfsCopy.dsAnchors.clear();
+ /* generate more DSs for the zone than we are willing to consider: only the last one will be used to generate DNSKEY records */
+ for (size_t idx = 0; idx < (g_maxDSsToConsider + 10U); idx++) {
+ generateKeyMaterial(g_rootdnsname, DNSSECKeeper::RSASHA512, DNSSECKeeper::DIGEST_SHA384, keys, luaconfsCopy.dsAnchors);
+ }
+ g_luaconfs.setState(luaconfsCopy);
+
+ size_t queriesCount = 0;
+
+ sr->setAsyncCallback([target, &queriesCount, keys](const ComboAddress& /* ip */, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+ queriesCount++;
+
+ if (domain == target && type == QType::NS) {
+
+ setLWResult(res, 0, true, false, true);
+ char addr[] = "a.root-servers.net.";
+ for (char idx = 'a'; idx <= 'm'; idx++) {
+ addr[0] = idx;
+ addRecordToLW(res, domain, QType::NS, std::string(addr), DNSResourceRecord::ANSWER, 3600);
+ }
+
+ addRRSIG(keys, res->d_records, domain, 300);
+
+ addRecordToLW(res, "a.root-servers.net.", QType::A, "198.41.0.4", DNSResourceRecord::ADDITIONAL, 3600);
+ addRecordToLW(res, "a.root-servers.net.", QType::AAAA, "2001:503:ba3e::2:30", DNSResourceRecord::ADDITIONAL, 3600);
+
+ return LWResult::Result::Success;
+ }
+ else if (domain == target && type == QType::DNSKEY) {
+
+ setLWResult(res, 0, true, false, true);
+
+ addDNSKEY(keys, domain, 300, res->d_records);
+ addRRSIG(keys, res->d_records, domain, 300);
+
+ return LWResult::Result::Success;
+ }
+
+ return LWResult::Result::Timeout;
+ });
+
+ /* === with validation enabled === */
+ sr->setDNSSECValidationRequested(true);
+ vector<DNSRecord> ret;
+ int res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidDNSKEY);
+ /* 13 NS + 1 RRSIG */
+ BOOST_REQUIRE_EQUAL(ret.size(), 14U);
+ BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+ /* again, to test the cache */
+ ret.clear();
+ res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidDNSKEY);
+ BOOST_REQUIRE_EQUAL(ret.size(), 14U);
+ BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+ g_maxDNSKEYsToConsider = 0;
+}
+
+BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_dnskeys)
+{
+ std::unique_ptr<SyncRes> sr;
+ initSR(sr, true);
+
+ setDNSSECValidation(sr, DNSSECMode::Process);
+
+ primeHints();
+ const DNSName target(".");
+ testkeysset_t dskeys;
+ testkeysset_t keys;
+
+ DNSKEYRecordContent dnskeyRecordContent;
+ dnskeyRecordContent.d_algorithm = 13;
+ /* Generate key material for "." */
+ auto dckeDS = DNSCryptoKeyEngine::makeFromISCString(dnskeyRecordContent, R"PKEY(Private-key-format: v1.2
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: Ovj4pzrSh0U6aEVoKaPFhK1D4NMG0xrymj9+6TpwC8o=)PKEY");
+ DNSSECPrivateKey dskey;
+ dskey.setKey(std::move(dckeDS));
+ dskey.d_flags = 257;
+ assert(dskey.getTag() == 31337);
+ DSRecordContent drc = makeDSFromDNSKey(target, dskey.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
+ dskeys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dskey, drc);
+
+ /* Different key, same tag */
+ auto dcke = DNSCryptoKeyEngine::makeFromISCString(dnskeyRecordContent, R"PKEY(Private-key-format: v1.2
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: n7SRA4n6NejhZBWQOhjTaICYSpkTl6plJn1ATFG23FI=)PKEY");
+ DNSSECPrivateKey dpk;
+ dpk.d_flags = 256;
+ dpk.setKey(std::move(dcke));
+ assert(dpk.getTag() == dskey.getTag());
+ DSRecordContent uselessdrc = makeDSFromDNSKey(target, dpk.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
+ keys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dpk, uselessdrc);
+
+ /* Set the root DS (one of them!) */
+ auto luaconfsCopy = g_luaconfs.getCopy();
+ luaconfsCopy.dsAnchors.clear();
+ luaconfsCopy.dsAnchors[g_rootdnsname].insert(drc);
+ g_luaconfs.setState(luaconfsCopy);
+
+ size_t queriesCount = 0;
+
+ sr->setAsyncCallback([target, &queriesCount, keys, dskeys](const ComboAddress& /* ip */, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+ queriesCount++;
+
+ if (domain == target && type == QType::NS) {
+
+ setLWResult(res, 0, true, false, true);
+ char addr[] = "a.root-servers.net.";
+ for (char idx = 'a'; idx <= 'm'; idx++) {
+ addr[0] = idx;
+ addRecordToLW(res, domain, QType::NS, std::string(addr), DNSResourceRecord::ANSWER, 3600);
+ }
+
+ addRRSIG(dskeys, res->d_records, domain, 300);
+
+ addRecordToLW(res, "a.root-servers.net.", QType::A, "198.41.0.4", DNSResourceRecord::ADDITIONAL, 3600);
+ addRecordToLW(res, "a.root-servers.net.", QType::AAAA, "2001:503:ba3e::2:30", DNSResourceRecord::ADDITIONAL, 3600);
+
+ return LWResult::Result::Success;
+ }
+ else if (domain == target && type == QType::DNSKEY) {
+
+ setLWResult(res, 0, true, false, true);
+
+ addDNSKEY(keys, domain, 300, res->d_records);
+ addDNSKEY(dskeys, domain, 300, res->d_records);
+ addRRSIG(keys, res->d_records, domain, 300);
+ addRRSIG(dskeys, res->d_records, domain, 300);
+
+ return LWResult::Result::Success;
+ }
+
+ return LWResult::Result::Timeout;
+ });
+
+ g_maxDNSKEYsToConsider = 1;
+
+ /* === with validation enabled === */
+ sr->setDNSSECValidationRequested(true);
+ vector<DNSRecord> ret;
+ int res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidDNSKEY);
+ /* 13 NS + 1 RRSIG */
+ BOOST_REQUIRE_EQUAL(ret.size(), 14U);
+ BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+ /* again, to test the cache */
+ ret.clear();
+ res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidDNSKEY);
+ BOOST_REQUIRE_EQUAL(ret.size(), 14U);
+ BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+ g_maxDNSKEYsToConsider = 0;
+}
+
+BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_dnskeys_while_checking_signature)
+{
+ std::unique_ptr<SyncRes> sr;
+ initSR(sr, true);
+
+ setDNSSECValidation(sr, DNSSECMode::Process);
+
+ primeHints();
+ const DNSName target(".");
+ testkeysset_t dskeys;
+ testkeysset_t keys;
+ testkeysset_t otherkeys;
+
+ DNSKEYRecordContent dnskeyRecordContent;
+ dnskeyRecordContent.d_algorithm = 13;
+ /* Generate key material for "." */
+ auto dckeDS = DNSCryptoKeyEngine::makeFromISCString(dnskeyRecordContent, R"PKEY(Private-key-format: v1.2
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: Ovj4pzrSh0U6aEVoKaPFhK1D4NMG0xrymj9+6TpwC8o=)PKEY");
+ DNSSECPrivateKey dskey;
+ dskey.setKey(std::move(dckeDS));
+ dskey.d_flags = 257;
+ assert(dskey.getTag() == 31337);
+ DSRecordContent drc = makeDSFromDNSKey(target, dskey.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
+ dskeys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dskey, drc);
+
+ /* Different key, same tag */
+ auto dcke = DNSCryptoKeyEngine::makeFromISCString(dnskeyRecordContent, R"PKEY(Private-key-format: v1.2
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: pTaMJcvNrPIIiQiHGvCLZZASroyQpUwew5FvCgjHNsk=)PKEY");
+ DNSSECPrivateKey dpk;
+ // why 258, you may ask? We need this record to be sorted AFTER the other one in a sortedRecords_t
+ // so that the validation of the DNSKEY rrset succeeds
+ dpk.setKey(std::move(dcke));
+ dpk.d_flags = 258;
+ assert(dpk.getTag() == dskey.getTag());
+ DSRecordContent uselessdrc = makeDSFromDNSKey(target, dpk.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
+ keys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dpk, uselessdrc);
+
+ /* Set the root DSs (only one of them) */
+ auto luaconfsCopy = g_luaconfs.getCopy();
+ luaconfsCopy.dsAnchors.clear();
+ luaconfsCopy.dsAnchors[g_rootdnsname].insert(drc);
+ g_luaconfs.setState(luaconfsCopy);
+
+ size_t queriesCount = 0;
+
+ sr->setAsyncCallback([target, &queriesCount, keys, dskeys](const ComboAddress& /* ip */, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+ queriesCount++;
+
+ if (domain == target && type == QType::NS) {
+
+ setLWResult(res, 0, true, false, true);
+ char addr[] = "a.root-servers.net.";
+ for (char idx = 'a'; idx <= 'm'; idx++) {
+ addr[0] = idx;
+ addRecordToLW(res, domain, QType::NS, std::string(addr), DNSResourceRecord::ANSWER, 3600);
+ }
+
+ addRRSIG(keys, res->d_records, domain, 300);
+ addRRSIG(dskeys, res->d_records, domain, 300);
+
+ addRecordToLW(res, "a.root-servers.net.", QType::A, "198.41.0.4", DNSResourceRecord::ADDITIONAL, 3600);
+ addRecordToLW(res, "a.root-servers.net.", QType::AAAA, "2001:503:ba3e::2:30", DNSResourceRecord::ADDITIONAL, 3600);
+
+ return LWResult::Result::Success;
+ }
+ else if (domain == target && type == QType::DNSKEY) {
+
+ setLWResult(res, 0, true, false, true);
+
+ addDNSKEY(dskeys, domain, 300, res->d_records);
+ addDNSKEY(keys, domain, 300, res->d_records);
+ addRRSIG(dskeys, res->d_records, domain, 300);
+
+ return LWResult::Result::Success;
+ }
+
+ return LWResult::Result::Timeout;
+ });
+
+ g_maxDNSKEYsToConsider = 1;
+
+ /* === with validation enabled === */
+ sr->setDNSSECValidationRequested(true);
+ vector<DNSRecord> ret;
+ int res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidRRSIG);
+ /* 13 NS + 1 RRSIG */
+ BOOST_REQUIRE_EQUAL(ret.size(), 15U);
+ BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+ /* again, to test the cache */
+ ret.clear();
+ res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidRRSIG);
+ BOOST_REQUIRE_EQUAL(ret.size(), 15U);
+ BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+ g_maxDNSKEYsToConsider = 0;
+}
+
BOOST_AUTO_TEST_CASE(test_dnssec_bogus_rrsig_signed_with_unknown_dnskey)
{
std::unique_ptr<SyncRes> sr;
BOOST_CHECK_EQUAL(queriesCount, 2U);
}
+BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_sigs)
+{
+ std::unique_ptr<SyncRes> sr;
+ initSR(sr, true);
+
+ setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+ primeHints();
+ const DNSName target(".");
+ testkeysset_t keys;
+
+ auto luaconfsCopy = g_luaconfs.getCopy();
+ luaconfsCopy.dsAnchors.clear();
+ generateKeyMaterial(g_rootdnsname, DNSSECKeeper::RSASHA512, DNSSECKeeper::DIGEST_SHA384, keys, luaconfsCopy.dsAnchors);
+
+ g_luaconfs.setState(luaconfsCopy);
+ /* make sure that the signature inception and validity times are computed
+ based on the SyncRes time, not the current one, in case the function
+ takes too long. */
+ const time_t fixedNow = sr->getNow().tv_sec;
+
+ size_t queriesCount = 0;
+
+ sr->setAsyncCallback([target, &queriesCount, keys, fixedNow](const ComboAddress& /* ip */, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+ queriesCount++;
+
+ if (domain == target && type == QType::NS) {
+
+ setLWResult(res, 0, true, false, true);
+ char addr[] = "a.root-servers.net.";
+ for (char idx = 'a'; idx <= 'm'; idx++) {
+ addr[0] = idx;
+ addRecordToLW(res, domain, QType::NS, std::string(addr), DNSResourceRecord::ANSWER, 3600);
+ }
+
+ addRRSIG(keys, res->d_records, domain, 300, true, boost::none, boost::none, fixedNow);
+ addRRSIG(keys, res->d_records, domain, 300, true, boost::none, boost::none, fixedNow);
+ addRRSIG(keys, res->d_records, domain, 300, false, boost::none, boost::none, fixedNow);
+
+ addRecordToLW(res, "a.root-servers.net.", QType::A, "198.41.0.4", DNSResourceRecord::ADDITIONAL, 3600);
+ addRecordToLW(res, "a.root-servers.net.", QType::AAAA, "2001:503:ba3e::2:30", DNSResourceRecord::ADDITIONAL, 3600);
+
+ return LWResult::Result::Success;
+ }
+ else if (domain == target && type == QType::DNSKEY) {
+
+ setLWResult(res, 0, true, false, true);
+
+ addDNSKEY(keys, domain, 300, res->d_records);
+ addRRSIG(keys, res->d_records, domain, 300, false, boost::none, boost::none, fixedNow);
+
+ return LWResult::Result::Success;
+ }
+
+ return LWResult::Result::Timeout;
+ });
+
+ g_maxRRSIGsPerRecordToConsider = 2;
+
+ vector<DNSRecord> ret;
+ int res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidRRSIG);
+ /* 13 NS + 1 RRSIG */
+ BOOST_REQUIRE_EQUAL(ret.size(), 16U);
+ BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+ /* again, to test the cache */
+ ret.clear();
+ res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidRRSIG);
+ BOOST_REQUIRE_EQUAL(ret.size(), 16U);
+ BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+ g_maxRRSIGsPerRecordToConsider = 0;
+}
+
+BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_sig_validations)
+{
+ std::unique_ptr<SyncRes> sr;
+ initSR(sr, true);
+
+ setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+ primeHints();
+ const DNSName target(".");
+ testkeysset_t keys;
+
+ auto luaconfsCopy = g_luaconfs.getCopy();
+ luaconfsCopy.dsAnchors.clear();
+ generateKeyMaterial(g_rootdnsname, DNSSECKeeper::RSASHA512, DNSSECKeeper::DIGEST_SHA384, keys, luaconfsCopy.dsAnchors);
+
+ g_luaconfs.setState(luaconfsCopy);
+ /* make sure that the signature inception and validity times are computed
+ based on the SyncRes time, not the current one, in case the function
+ takes too long. */
+ const time_t fixedNow = sr->getNow().tv_sec;
+
+ size_t queriesCount = 0;
+
+ sr->setAsyncCallback([target, &queriesCount, keys, fixedNow](const ComboAddress& /* ip */, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+ queriesCount++;
+
+ if (domain == target && type == QType::NS) {
+
+ setLWResult(res, 0, true, false, true);
+ char addr[] = "a.root-servers.net.";
+ for (char idx = 'a'; idx <= 'm'; idx++) {
+ addr[0] = idx;
+ addRecordToLW(res, domain, QType::NS, std::string(addr), DNSResourceRecord::ANSWER, 3600);
+ }
+
+ addRRSIG(keys, res->d_records, domain, 300, true, boost::none, boost::none, fixedNow);
+ addRRSIG(keys, res->d_records, domain, 300, false, boost::none, boost::none, fixedNow);
+
+ addRecordToLW(res, "a.root-servers.net.", QType::A, "198.41.0.4", DNSResourceRecord::ADDITIONAL, 3600);
+ addRecordToLW(res, "a.root-servers.net.", QType::AAAA, "2001:503:ba3e::2:30", DNSResourceRecord::ADDITIONAL, 3600);
+
+ return LWResult::Result::Success;
+ }
+ else if (domain == target && type == QType::DNSKEY) {
+
+ setLWResult(res, 0, true, false, true);
+
+ addDNSKEY(keys, domain, 300, res->d_records);
+ addRRSIG(keys, res->d_records, domain, 300, false, boost::none, boost::none, fixedNow);
+
+ return LWResult::Result::Success;
+ }
+
+ return LWResult::Result::Timeout;
+ });
+
+ SyncRes::s_maxvalidationsperq = 1U;
+
+ vector<DNSRecord> ret;
+ BOOST_REQUIRE_THROW(sr->beginResolve(target, QType(QType::NS), QClass::IN, ret), ImmediateServFailException);
+
+ SyncRes::s_maxvalidationsperq = 0U;
+}
+
BOOST_AUTO_TEST_CASE(test_dnssec_bogus_bad_algo)
{
std::unique_ptr<SyncRes> sr;
BOOST_CHECK_EQUAL(queriesCount, 4U);
}
+BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec3_too_many_nsec3s)
+{
+ std::unique_ptr<SyncRes> sr;
+ initSR(sr, true);
+
+ setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+ primeHints();
+ const DNSName target("www.com.");
+ testkeysset_t keys;
+
+ auto luaconfsCopy = g_luaconfs.getCopy();
+ luaconfsCopy.dsAnchors.clear();
+ generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys, luaconfsCopy.dsAnchors);
+ generateKeyMaterial(DNSName("com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys);
+
+ g_luaconfs.setState(luaconfsCopy);
+
+ size_t queriesCount = 0;
+
+ sr->setAsyncCallback([target, &queriesCount, keys](const ComboAddress& ip, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+ queriesCount++;
+
+ if (type == QType::DS || type == QType::DNSKEY) {
+ if (type == QType::DS && domain == target) {
+ DNSName auth("com.");
+ setLWResult(res, 0, true, false, true);
+
+ addRecordToLW(res, auth, QType::SOA, "foo. bar. 2017032800 1800 900 604800 86400", DNSResourceRecord::AUTHORITY, 86400);
+ addRRSIG(keys, res->d_records, auth, 300);
+ /* add a NSEC3 denying the DS AND the existence of a cut (no NS) */
+ /* first the closest encloser */
+ addNSEC3UnhashedRecordToLW(DNSName("com."), auth, "whatever", {QType::A, QType::TXT, QType::RRSIG, QType::NSEC}, 600, res->d_records);
+ addRRSIG(keys, res->d_records, auth, 300);
+ /* then the next closer */
+ addNSEC3NarrowRecordToLW(domain, DNSName("com."), {QType::RRSIG, QType::NSEC}, 600, res->d_records);
+ addRRSIG(keys, res->d_records, auth, 300);
+ /* a wildcard matches but has no record for this type */
+ addNSEC3UnhashedRecordToLW(DNSName("*.com."), DNSName("com."), "whatever", {QType::AAAA, QType::NSEC, QType::RRSIG}, 600, res->d_records);
+ addRRSIG(keys, res->d_records, DNSName("com"), 300, false, boost::none, DNSName("*.com"));
+ return LWResult::Result::Success;
+ }
+ return genericDSAndDNSKEYHandler(res, domain, domain, type, keys);
+ }
+ else {
+ if (isRootServer(ip)) {
+ setLWResult(res, 0, false, false, true);
+ addRecordToLW(res, "com.", QType::NS, "a.gtld-servers.com.", DNSResourceRecord::AUTHORITY, 3600);
+ addDS(DNSName("com."), 300, res->d_records, keys);
+ addRRSIG(keys, res->d_records, DNSName("."), 300);
+ addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600);
+ return LWResult::Result::Success;
+ }
+ else if (ip == ComboAddress("192.0.2.1:53")) {
+ setLWResult(res, 0, true, false, true);
+ /* no data */
+ addRecordToLW(res, DNSName("com."), QType::SOA, "com. com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+ addRRSIG(keys, res->d_records, DNSName("com."), 300);
+ /* no record for this name */
+ /* first the closest encloser */
+ addNSEC3UnhashedRecordToLW(DNSName("com."), DNSName("com."), "whatever", {QType::A, QType::TXT, QType::RRSIG, QType::NSEC}, 600, res->d_records);
+ addRRSIG(keys, res->d_records, DNSName("com."), 300);
+ /* then the next closer */
+ addNSEC3NarrowRecordToLW(domain, DNSName("com."), {QType::RRSIG, QType::NSEC}, 600, res->d_records);
+ addRRSIG(keys, res->d_records, DNSName("com."), 300);
+ /* a wildcard matches but has no record for this type */
+ addNSEC3UnhashedRecordToLW(DNSName("*.com."), DNSName("com."), "whatever", {QType::AAAA, QType::NSEC, QType::RRSIG}, 600, res->d_records);
+ addRRSIG(keys, res->d_records, DNSName("com"), 300, false, boost::none, DNSName("*.com"));
+ return LWResult::Result::Success;
+ }
+ }
+
+ return LWResult::Result::Timeout;
+ });
+
+ /* we allow at most 2 NSEC3s, but we need at least 3 of them to
+ get a valid denial so we will go Bogus */
+ g_maxNSEC3sPerRecordToConsider = 2;
+
+ vector<DNSRecord> ret;
+ int res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusInvalidDenial);
+ BOOST_REQUIRE_EQUAL(ret.size(), 8U);
+ BOOST_CHECK_EQUAL(queriesCount, 5U);
+
+ g_maxNSEC3sPerRecordToConsider = 0;
+}
+
+BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec3_too_many_nsec3s_per_query)
+{
+ SyncRes::s_maxnsec3iterationsperq = 20;
+ std::unique_ptr<SyncRes> sr;
+ initSR(sr, true);
+
+ setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+ primeHints();
+ const DNSName target("www.com.");
+ testkeysset_t keys;
+
+ auto luaconfsCopy = g_luaconfs.getCopy();
+ luaconfsCopy.dsAnchors.clear();
+ generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys, luaconfsCopy.dsAnchors);
+ generateKeyMaterial(DNSName("com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys);
+
+ g_luaconfs.setState(luaconfsCopy);
+
+ size_t queriesCount = 0;
+
+ sr->setAsyncCallback([target, &queriesCount, keys](const ComboAddress& ip, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+ queriesCount++;
+
+ if (type == QType::DS || type == QType::DNSKEY) {
+ if (type == QType::DS && domain == target) {
+ DNSName auth("com.");
+ setLWResult(res, 0, true, false, true);
+
+ addRecordToLW(res, auth, QType::SOA, "foo. bar. 2017032800 1800 900 604800 86400", DNSResourceRecord::AUTHORITY, 86400);
+ addRRSIG(keys, res->d_records, auth, 300);
+ /* add a NSEC3 denying the DS AND the existence of a cut (no NS) */
+ /* first the closest encloser */
+ addNSEC3UnhashedRecordToLW(DNSName("com."), auth, "whatever", {QType::A, QType::TXT, QType::RRSIG, QType::NSEC}, 600, res->d_records);
+ addRRSIG(keys, res->d_records, auth, 300);
+ /* then the next closer */
+ addNSEC3NarrowRecordToLW(domain, DNSName("com."), {QType::RRSIG, QType::NSEC}, 600, res->d_records);
+ addRRSIG(keys, res->d_records, auth, 300);
+ /* a wildcard matches but has no record for this type */
+ addNSEC3UnhashedRecordToLW(DNSName("*.com."), DNSName("com."), "whatever", {QType::AAAA, QType::NSEC, QType::RRSIG}, 600, res->d_records);
+ addRRSIG(keys, res->d_records, DNSName("com"), 300, false, boost::none, DNSName("*.com"));
+ return LWResult::Result::Success;
+ }
+ return genericDSAndDNSKEYHandler(res, domain, domain, type, keys);
+ }
+ else {
+ if (isRootServer(ip)) {
+ setLWResult(res, 0, false, false, true);
+ addRecordToLW(res, "com.", QType::NS, "a.gtld-servers.com.", DNSResourceRecord::AUTHORITY, 3600);
+ addDS(DNSName("com."), 300, res->d_records, keys);
+ addRRSIG(keys, res->d_records, DNSName("."), 300);
+ addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600);
+ return LWResult::Result::Success;
+ }
+ else if (ip == ComboAddress("192.0.2.1:53")) {
+ setLWResult(res, 0, true, false, true);
+ /* no data */
+ addRecordToLW(res, DNSName("com."), QType::SOA, "com. com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+ addRRSIG(keys, res->d_records, DNSName("com."), 300);
+ /* no record for this name */
+ /* first the closest encloser */
+ addNSEC3UnhashedRecordToLW(DNSName("com."), DNSName("com."), "whatever", {QType::A, QType::TXT, QType::RRSIG, QType::NSEC}, 600, res->d_records);
+ addRRSIG(keys, res->d_records, DNSName("com."), 300);
+ /* then the next closer */
+ addNSEC3NarrowRecordToLW(domain, DNSName("com."), {QType::RRSIG, QType::NSEC}, 600, res->d_records);
+ addRRSIG(keys, res->d_records, DNSName("com."), 300);
+ /* a wildcard matches but has no record for this type */
+ addNSEC3UnhashedRecordToLW(DNSName("*.com."), DNSName("com."), "whatever", {QType::AAAA, QType::NSEC, QType::RRSIG}, 600, res->d_records);
+ addRRSIG(keys, res->d_records, DNSName("com"), 300, false, boost::none, DNSName("*.com"));
+ return LWResult::Result::Success;
+ }
+ }
+
+ return LWResult::Result::Timeout;
+ });
+
+ vector<DNSRecord> ret;
+ BOOST_CHECK_THROW(sr->beginResolve(target, QType(QType::A), QClass::IN, ret), pdns::validation::TooManySEC3IterationsException);
+
+ SyncRes::s_maxnsec3iterationsperq = 0;
+}
+
BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec3_nodata_nowildcard_duplicated_nsec3)
{
std::unique_ptr<SyncRes> sr;
BOOST_AUTO_TEST_SUITE(syncres_cc8)
+static dState getDenial(const cspmap_t& validrrsets, const DNSName& qname, uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, bool needWildcardProof = true, unsigned int wildcardLabelsCount = 0)
+{
+ pdns::validation::ValidationContext context;
+ context.d_nsec3IterationsRemainingQuota = std::numeric_limits<decltype(context.d_nsec3IterationsRemainingQuota)>::max();
+ return getDenial(validrrsets, qname, qtype, referralToUnsigned, wantsNoDataProof, context, needWildcardProof, wildcardLabelsCount);
+}
+
BOOST_AUTO_TEST_CASE(test_nsec_denial_nowrap)
{
initSR();
sortedRecords_t recordContents;
vector<shared_ptr<RRSIGRecordContent>> signatureContents;
- addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::A}, 600, records);
+ const unsigned int nbIterations = 10;
+ addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::A}, 600, records, nbIterations);
recordContents.insert(records.at(0).d_content);
addRRSIG(keys, records, DNSName("powerdns.com."), 300);
signatureContents.push_back(getRR<RRSIGRecordContent>(records.at(1)));
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)
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;
SyncRes::SyncRes(const struct timeval& now) : d_authzonequeries(0), d_outqueries(0), d_tcpoutqueries(0), d_dotoutqueries(0), d_throttledqueries(0), d_timeouts(0), d_unreachables(0),
d_totUsec(0), d_now(now),
d_cacheonly(false), d_doDNSSEC(false), d_doEDNS0(false), d_qNameMinimization(s_qnameminimization), d_lm(s_lm)
-
{
+ d_validationContext.d_nsec3IterationsRemainingQuota = s_maxnsec3iterationsperq > 0 ? s_maxnsec3iterationsperq : std::numeric_limits<decltype(d_validationContext.d_nsec3IterationsRemainingQuota)>::max();
}
static void allowAdditionalEntry(std::unordered_set<DNSName>& allowedAdditionals, const DNSRecord& rec);
/* 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;
}
- 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) {
LOG(d_prefix<<": trying to validate "<<std::to_string(tentativeKeys.size())<<" DNSKEYs with "<<std::to_string(ds.size())<<" DS"<<endl);
skeyset_t validatedKeys;
- auto state = validateDNSKeysAgainstDS(d_now.tv_sec, zone, ds, tentativeKeys, toSign, signatures, validatedKeys);
+ auto state = validateDNSKeysAgainstDS(d_now.tv_sec, zone, ds, tentativeKeys, toSign, signatures, validatedKeys, d_validationContext);
+ if (s_maxvalidationsperq != 0 && d_validationContext.d_validationsCounter > s_maxvalidationsperq) {
+ throw ImmediateServFailException("Server Failure while validating DNSKEYs, too many signature validations for this query");
+ }
LOG(d_prefix<<": we now have "<<std::to_string(validatedKeys.size())<<" DNSKEYs"<<endl);
}
LOG(d_prefix<<"Going to validate "<<recordcontents.size()<< " record contents with "<<signatures.size()<<" sigs and "<<keys.size()<<" keys for "<<name<<"|"<<type.toString()<<endl);
- vState state = validateWithKeySet(d_now.tv_sec, name, recordcontents, signatures, keys, false);
+ vState state = validateWithKeySet(d_now.tv_sec, name, recordcontents, signatures, keys, d_validationContext, false);
+ if (s_maxvalidationsperq != 0 && d_validationContext.d_validationsCounter > s_maxvalidationsperq) {
+ throw ImmediateServFailException("Server Failure while validating records, too many signature validations for this query");
+ }
+
if (state == vState::Secure) {
LOG(d_prefix<<"Secure!"<<endl);
return vState::Secure;
dState SyncRes::getDenialValidationState(const NegCache::NegCacheEntry& ne, const dState expectedState, bool referralToUnsigned)
{
cspmap_t csp = harvestCSPFromNE(ne);
- return getDenial(csp, ne.d_name, ne.d_qtype.getCode(), referralToUnsigned, expectedState == dState::NXQTYPE);
+ return getDenial(csp, ne.d_name, ne.d_qtype.getCode(), referralToUnsigned, expectedState == dState::NXQTYPE, d_validationContext);
}
bool SyncRes::processRecords(const std::string& prefix, const DNSName& qname, const QType qtype, const DNSName& auth, LWResult& lwr, const bool sendRDQuery, vector<DNSRecord>& ret, set<DNSName>& nsset, DNSName& newtarget, DNSName& newauth, bool& realreferral, bool& negindic, vState& state, const bool needWildcardProof, const bool gatherWildcardProof, const unsigned int wildcardLabelsCount, int& rcode, bool& negIndicHasSignatures, unsigned int depth)
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) {
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;
std::shared_ptr<std::vector<std::unique_ptr<RemoteLogger>>> d_outgoingProtobufServers;
std::shared_ptr<std::vector<std::unique_ptr<FrameStreamLogger>>> d_frameStreamServers;
boost::optional<const boost::uuids::uuid&> d_initialRequestId;
+ pdns::validation::ValidationContext d_validationContext;
asyncresolve_t d_asyncResolve{nullptr};
struct timeval d_now;
/* if the client is asking for a DS that does not exist, we need to provide the SOA along with the NSEC(3) proof
+/*
+ * 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"
#define LOG(x) if(g_dnssecLOG) { g_log <<Logger::Warning << x; }
+uint16_t g_maxRRSIGsPerRecordToConsider{0};
+uint16_t g_maxNSEC3sPerRecordToConsider{0};
+uint16_t g_maxDNSKEYsToConsider{0};
+uint16_t g_maxDSsToConsider{0};
+
static bool isAZoneKey(const DNSKEYRecordContent& key)
{
/* rfc4034 Section 2.1.1:
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)
return begin.canonCompare(name) && next != name && next.isPartOf(name);
}
-using nsec3HashesCache = std::map<std::tuple<DNSName, std::string, uint16_t>, std::string>;
-
-static std::string getHashFromNSEC3(const DNSName& qname, const std::shared_ptr<NSEC3RecordContent>& nsec3, nsec3HashesCache& cache)
+[[nodiscard]] std::string getHashFromNSEC3(const DNSName& qname, uint16_t iterations, const std::string& salt, pdns::validation::ValidationContext& context)
{
std::string result;
- if (g_maxNSEC3Iterations && nsec3->d_iterations > g_maxNSEC3Iterations) {
+ if (g_maxNSEC3Iterations != 0 && iterations > g_maxNSEC3Iterations) {
return result;
}
- auto key = std::make_tuple(qname, nsec3->d_salt, nsec3->d_iterations);
- auto it = cache.find(key);
- if (it != cache.end())
+ auto key = std::tuple(qname, salt, iterations);
+ auto iter = context.d_nsec3Cache.find(key);
+ if (iter != context.d_nsec3Cache.end())
{
- return it->second;
+ return iter->second;
}
- result = hashQNameWithSalt(nsec3->d_salt, nsec3->d_iterations, qname);
- cache[key] = result;
+ if (context.d_nsec3IterationsRemainingQuota < iterations) {
+ // we throw here because we cannot take the risk that the result
+ // be cached, since a different query can try to validate the
+ // same result with a bigger NSEC3 iterations quota
+ throw pdns::validation::TooManySEC3IterationsException();
+ }
+
+ result = hashQNameWithSalt(salt, iterations, qname);
+ context.d_nsec3IterationsRemainingQuota -= iterations;
+ context.d_nsec3Cache[key] = result;
return result;
}
+[[nodiscard]] static std::string getHashFromNSEC3(const DNSName& qname, const NSEC3RecordContent& nsec3, pdns::validation::ValidationContext& context)
+{
+ return getHashFromNSEC3(qname, nsec3.d_iterations, nsec3.d_salt, context);
+}
+
/* There is no delegation at this exact point if:
- the name exists but the NS type is not set
- the name does not exist
One exception, if the name is covered by an opt-out NSEC3
it doesn't prove that an insecure delegation doesn't exist.
*/
-bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>& dsrecords)
+bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>& dsrecords, pdns::validation::ValidationContext& context)
{
- nsec3HashesCache cache;
-
+ uint16_t nsec3sConsidered = 0;
+
for (const auto& record : dsrecords) {
if (record.d_type == QType::NSEC) {
const auto nsec = getRR<NSECRecordContent>(record);
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());
}
}
*/
bool isWildcardExpanded(unsigned int labelCount, const std::shared_ptr<RRSIGRecordContent>& sign)
{
- if (sign && sign->d_labels < labelCount) {
- return true;
- }
-
- return false;
+ return sign->d_labels < labelCount;
}
static bool isWildcardExpanded(const DNSName& owner, const std::vector<std::shared_ptr<RRSIGRecordContent> >& signatures)
bool isWildcardExpandedOntoItself(const DNSName& owner, unsigned int labelCount, const std::shared_ptr<RRSIGRecordContent>& sign)
{
- if (owner.isWildcard() && (labelCount - 1) == sign->d_labels) {
- /* this is a wildcard alright, but it has not been expanded */
- return true;
- }
- return false;
+ /* this is a wildcard alright, but it has not been expanded */
+ return owner.isWildcard() && (labelCount - 1) == sign->d_labels;
}
static bool isWildcardExpandedOntoItself(const DNSName& owner, const std::vector<std::shared_ptr<RRSIGRecordContent> >& signatures)
return result;
}
-static bool isNSECAncestorDelegation(const DNSName& signer, const DNSName& owner, const std::shared_ptr<NSECRecordContent>& nsec)
+static bool isNSECAncestorDelegation(const DNSName& signer, const DNSName& owner, const NSECRecordContent& nsec)
{
- return nsec->isSet(QType::NS) &&
- !nsec->isSet(QType::SOA) &&
+ return nsec.isSet(QType::NS) &&
+ !nsec.isSet(QType::SOA) &&
signer.countLabels() < owner.countLabels();
}
{
const DNSName wildcard = g_wildcarddnsname + closestEncloser;
LOG("Trying to prove that there is no data in wildcard for "<<qname<<"/"<<QType(qtype)<<endl);
- for (const auto& v : validrrsets) {
- LOG("Do have: "<<v.first.first<<"/"<<DNSRecordContent::NumberToType(v.first.second)<<endl);
- if (v.first.second == QType::NSEC) {
- for (const auto& r : v.second.records) {
- LOG("\t"<<r->getZoneRepresentation()<<endl);
- auto nsec = std::dynamic_pointer_cast<NSECRecordContent>(r);
+ for (const auto& validset : validrrsets) {
+ LOG("Do have: "<<validset.first.first<<"/"<<DNSRecordContent::NumberToType(validset.first.second)<<endl);
+ if (validset.first.second == QType::NSEC) {
+ for (const auto& record : validset.second.records) {
+ LOG("\t"<<record->getZoneRepresentation()<<endl);
+ auto nsec = std::dynamic_pointer_cast<NSECRecordContent>(record);
if (!nsec) {
continue;
}
- DNSName owner = getNSECOwnerName(v.first.first, v.second.signatures);
+ DNSName owner = getNSECOwnerName(validset.first.first, validset.second.signatures);
if (owner != wildcard) {
continue;
}
{
LOG("Trying to prove that there is no wildcard for "<<qname<<"/"<<QType(qtype)<<endl);
const DNSName wildcard = g_wildcarddnsname + closestEncloser;
- for (const auto& v : validrrsets) {
- LOG("Do have: "<<v.first.first<<"/"<<DNSRecordContent::NumberToType(v.first.second)<<endl);
- if (v.first.second == QType::NSEC) {
- for (const auto& r : v.second.records) {
- LOG("\t"<<r->getZoneRepresentation()<<endl);
- auto nsec = std::dynamic_pointer_cast<NSECRecordContent>(r);
+ for (const auto& validset : validrrsets) {
+ LOG("Do have: "<<validset.first.first<<"/"<<DNSRecordContent::NumberToType(validset.first.second)<<endl);
+ if (validset.first.second == QType::NSEC) {
+ for (const auto& records : validset.second.records) {
+ LOG("\t"<<records->getZoneRepresentation()<<endl);
+ auto nsec = std::dynamic_pointer_cast<const NSECRecordContent>(records);
if (!nsec) {
continue;
}
- const DNSName owner = getNSECOwnerName(v.first.first, v.second.signatures);
+ const DNSName owner = getNSECOwnerName(validset.first.first, validset.second.signatures);
LOG("Comparing owner: "<<owner<<" with target: "<<wildcard<<endl);
if (qname != owner && qname.isPartOf(owner) && nsec->isSet(QType::DNAME)) {
If `wildcardExists` is not NULL, if will be set to true if a wildcard exists
for this qname but doesn't have this qtype.
*/
-static bool provesNSEC3NoWildCard(const DNSName& closestEncloser, uint16_t const qtype, const cspmap_t& validrrsets, bool* wildcardExists, nsec3HashesCache& cache)
+static bool provesNSEC3NoWildCard(const DNSName& closestEncloser, uint16_t const qtype, const cspmap_t& validrrsets, bool* wildcardExists, pdns::validation::ValidationContext& context)
{
auto wildcard = g_wildcarddnsname + closestEncloser;
LOG("Trying to prove that there is no wildcard for "<<wildcard<<"/"<<QType(qtype)<<endl);
- for (const auto& v : validrrsets) {
- LOG("Do have: "<<v.first.first<<"/"<<DNSRecordContent::NumberToType(v.first.second)<<endl);
- if (v.first.second == QType::NSEC3) {
- for (const auto& r : v.second.records) {
- LOG("\t"<<r->getZoneRepresentation()<<endl);
- auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(r);
+ for (const auto& validset : validrrsets) {
+ LOG("Do have: "<<validset.first.first<<"/"<<DNSRecordContent::NumberToType(validset.first.second)<<endl);
+ if (validset.first.second == QType::NSEC3) {
+ for (const auto& records : validset.second.records) {
+ LOG("\t"<<records->getZoneRepresentation()<<endl);
+ auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(records);
if (!nsec3) {
continue;
}
- const DNSName signer = getSigner(v.second.signatures);
- if (!v.first.first.isPartOf(signer)) {
+ const DNSName signer = getSigner(validset.second.signatures);
+ if (!validset.first.first.isPartOf(signer)) {
continue;
}
- string h = getHashFromNSEC3(wildcard, nsec3, cache);
- if (h.empty()) {
+ string hash = getHashFromNSEC3(wildcard, *nsec3, context);
+ if (hash.empty()) {
+ LOG("Unsupported hash, ignoring"<<endl);
return false;
}
- LOG("\tWildcard hash: "<<toBase32Hex(h)<<endl);
- string beginHash=fromBase32Hex(v.first.first.getRawLabels()[0]);
- LOG("\tNSEC3 hash: "<<toBase32Hex(beginHash)<<" -> "<<toBase32Hex(nsec3->d_nexthash)<<endl);
+ LOG("\tWildcard hash: "<<toBase32Hex(hash)<<endl);
+ string beginHash=fromBase32Hex(validset.first.first.getRawLabels()[0]);
+ LOG(":\tNSEC3 hash: "<<toBase32Hex(beginHash)<<" -> "<<toBase32Hex(nsec3->d_nexthash)<<endl);
- if (beginHash == h) {
+ if (beginHash == hash) {
LOG("\tWildcard hash matches");
- if (wildcardExists) {
+ if (wildcardExists != nullptr) {
*wildcardExists = true;
}
that (original) owner name other than DS RRs, and all RRs below that
owner name regardless of type.
*/
- if (qtype != QType::DS && isNSEC3AncestorDelegation(signer, v.first.first, nsec3)) {
+ if (qtype != QType::DS && isNSEC3AncestorDelegation(signer, validset.first.first, nsec3)) {
/* this is an "ancestor delegation" NSEC3 RR */
LOG(" BUT an ancestor delegation NSEC3 RR can only deny the existence of a DS"<<endl);
return false;
return false;
}
- if (isCoveredByNSEC3Hash(h, beginHash, nsec3->d_nexthash)) {
+ if (isCoveredByNSEC3Hash(hash, beginHash, nsec3->d_nexthash)) {
LOG("\tWildcard hash is covered"<<endl);
return true;
}
that (original) owner name other than DS RRs, and all RRs below that
owner name regardless of type.
*/
- if (name.isPartOf(owner) && isNSECAncestorDelegation(signer, owner, nsec)) {
+ if (name.isPartOf(owner) && isNSECAncestorDelegation(signer, owner, *nsec)) {
/* this is an "ancestor delegation" NSEC RR */
- if (!(qtype == QType::DS && name == owner)) {
+ if (qtype != QType::DS || name != owner) {
LOG("An ancestor delegation NSEC RR can only deny the existence of a DS"<<endl);
return dState::NODENIAL;
}
/* check if the type is denied */
if (name == owner) {
if (!isTypeDenied(nsec, QType(qtype))) {
- LOG("Does _not_ deny existence of type "<<QType(qtype)<<endl);
+ LOG("does _not_ deny existence of type "<<QType(qtype)<<endl);
return dState::NODENIAL;
}
if (qtype == QType::DS && signer == name) {
- LOG("The NSEC comes from the child zone and cannot be used to deny a DS");
+ LOG("The NSEC comes from the child zone and cannot be used to deny a DS"<<endl);
return dState::NODENIAL;
}
}
if (isCoveredByNSEC(name, owner, nsec->d_next)) {
- LOG(name<<" is covered by ("<<owner<<" to "<<nsec->d_next<<") ");
+ LOG(name<<" is covered by ("<<owner<<" to "<<nsec->d_next<<")");
if (nsecProvesENT(name, owner, nsec->d_next)) {
LOG("Denies existence of type "<<name<<"/"<<QType(qtype)<<" by proving that "<<name<<" is an ENT"<<endl);
return dState::INCONCLUSIVE;
}
+[[nodiscard]] uint64_t getNSEC3DenialProofWorstCaseIterationsCount(uint8_t maxLabels, uint16_t iterations, size_t saltLength)
+{
+ return (iterations + 1 + (saltLength > 0 ? 1 : 0)) * maxLabels;
+}
+
/*
This function checks whether the existence of qname|qtype is denied by the NSEC and NSEC3
in validrrsets.
useful when we have a positive answer synthesized from a wildcard and we only need to prove that the exact
name does not exist.
*/
-
-dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, bool needWildcardProof, unsigned int wildcardLabelsCount)
+dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, pdns::validation::ValidationContext& context, bool needWildcardProof, unsigned int wildcardLabelsCount) // NOLINT(readability-function-cognitive-complexity): https://github.com/PowerDNS/pdns/issues/12791
{
- nsec3HashesCache cache;
bool nsec3Seen = false;
if (!needWildcardProof && wildcardLabelsCount == 0) {
throw PDNSException("Invalid wildcard labels count for the validation of a positive answer synthesized from a wildcard");
}
- for (const auto& v : validrrsets) {
- LOG("Do have: "<<v.first.first<<"/"<<DNSRecordContent::NumberToType(v.first.second)<<endl);
+ uint8_t numberOfLabelsOfParentZone{std::numeric_limits<uint8_t>::max()};
+ uint16_t nsec3sConsidered = 0;
+ for (const auto& validset : validrrsets) {
+ LOG("Do have: "<<validset.first.first<<"/"<<DNSRecordContent::NumberToType(validset.first.second)<<endl);
- if (v.first.second==QType::NSEC) {
- for (const auto& r : v.second.records) {
- LOG("\t"<<r->getZoneRepresentation()<<endl);
+ if (validset.first.second==QType::NSEC) {
+ for (const auto& record : validset.second.records) {
+ LOG("\t"<<record->getZoneRepresentation()<<endl);
- if (v.second.signatures.empty()) {
+ if (validset.second.signatures.empty()) {
continue;
}
- auto nsec = std::dynamic_pointer_cast<NSECRecordContent>(r);
+ auto nsec = std::dynamic_pointer_cast<const NSECRecordContent>(record);
if (!nsec) {
continue;
}
- const DNSName owner = getNSECOwnerName(v.first.first, v.second.signatures);
- const DNSName signer = getSigner(v.second.signatures);
- if (!v.first.first.isPartOf(signer) || !owner.isPartOf(signer) ) {
+ const DNSName owner = getNSECOwnerName(validset.first.first, validset.second.signatures);
+ const DNSName signer = getSigner(validset.second.signatures);
+ if (!validset.first.first.isPartOf(signer) || !owner.isPartOf(signer) ) {
continue;
}
that (original) owner name other than DS RRs, and all RRs below that
owner name regardless of type.
*/
- if (qname.isPartOf(owner) && isNSECAncestorDelegation(signer, owner, nsec)) {
+ if (qname.isPartOf(owner) && isNSECAncestorDelegation(signer, owner, *nsec)) {
/* this is an "ancestor delegation" NSEC RR */
- if (!(qtype == QType::DS && qname == owner)) {
+ if (qtype != QType::DS || qname != owner) {
LOG("An ancestor delegation NSEC RR can only deny the existence of a DS"<<endl);
return dState::NODENIAL;
}
/* check if the type is denied */
if (qname == owner) {
if (!isTypeDenied(nsec, QType(qtype))) {
- LOG("Does _not_ deny existence of type "<<QType(qtype)<<endl);
+ LOG("does _not_ deny existence of type "<<QType(qtype)<<endl);
return dState::NODENIAL;
}
/* we know that the name exists (but this qtype doesn't) so except
if the answer was generated by a wildcard expansion, no wildcard
could have matched (rfc4035 section 5.4 bullet 1) */
- if (needWildcardProof && (!isWildcardExpanded(owner, v.second.signatures) || isWildcardExpandedOntoItself(owner, v.second.signatures))) {
+ if (needWildcardProof && (!isWildcardExpanded(owner, validset.second.signatures) || isWildcardExpandedOntoItself(owner, validset.second.signatures))) {
needWildcardProof = false;
}
asserted, then DNAME substitution should have been done, but the
substitution has not been done as specified.
*/
- LOG("The DNAME bit is set and the query name is a subdomain of that NSEC");
+ LOG("The DNAME bit is set and the query name is a subdomain of that NSEC"<< endl);
return dState::NODENIAL;
}
/* check if the whole NAME is denied existing */
if (isCoveredByNSEC(qname, owner, nsec->d_next)) {
- LOG(qname<<" is covered by ("<<owner<<" to "<<nsec->d_next<<") ");
+ LOG(qname<<" is covered by ("<<owner<<" to "<<nsec->d_next<<")");
if (nsecProvesENT(qname, owner, nsec->d_next)) {
if (wantsNoDataProof) {
LOG("Denies existence of type "<<qname<<"/"<<QType(qtype)<<" by proving that "<<qname<<" is an ENT"<<endl);
return dState::NXQTYPE;
}
- else {
- /* but for a NXDOMAIN proof, this doesn't make sense! */
- LOG("but it tries to deny the existence of "<<qname<<" by proving that "<<qname<<" is an ENT, this does not make sense!"<<endl);
- return dState::NODENIAL;
- }
+ /* but for a NXDOMAIN proof, this doesn't make sense! */
+ LOG("but it tries to deny the existence of "<<qname<<" by proving that "<<qname<<" is an ENT, this does not make sense!"<<endl);
+ return dState::NODENIAL;
}
if (!needWildcardProof) {
return dState::NODENIAL;
}
- LOG("Did not deny existence of "<<QType(qtype)<<", "<<v.first.first<<"?="<<qname<<", "<<nsec->isSet(qtype)<<", next: "<<nsec->d_next<<endl);
+ LOG("Did not deny existence of "<<QType(qtype)<<", "<<validset.first.first<<"?="<<qname<<", "<<nsec->isSet(qtype)<<", next: "<<nsec->d_next<<endl);
}
- } else if(v.first.second==QType::NSEC3) {
- for (const auto& r : v.second.records) {
- LOG("\t"<<r->getZoneRepresentation()<<endl);
- auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(r);
+ } else if(validset.first.second==QType::NSEC3) {
+ for (const auto& record : validset.second.records) {
+ LOG("\t"<<record->getZoneRepresentation()<<endl);
+ auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(record);
if (!nsec3) {
continue;
}
- if (v.second.signatures.empty()) {
+ if (validset.second.signatures.empty()) {
continue;
}
- const DNSName& hashedOwner = v.first.first;
- const DNSName signer = getSigner(v.second.signatures);
+ const DNSName& hashedOwner = validset.first.first;
+ const DNSName signer = getSigner(validset.second.signatures);
if (!hashedOwner.isPartOf(signer)) {
LOG("Owner "<<hashedOwner<<" is not part of the signer "<<signer<<", ignoring"<<endl);
continue;
}
+ numberOfLabelsOfParentZone = std::min(numberOfLabelsOfParentZone, static_cast<uint8_t>(signer.countLabels()));
if (qtype == QType::DS && !qname.isRoot() && signer == qname) {
LOG("A NSEC3 RR from the child zone cannot deny the existence of a DS"<<endl);
continue;
}
- string h = getHashFromNSEC3(qname, nsec3, cache);
- if (h.empty()) {
+ if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+ LOG("Too many NSEC3s for this record"<<endl);
+ return dState::NODENIAL;
+ }
+ nsec3sConsidered++;
+
+ string hash = getHashFromNSEC3(qname, *nsec3, context);
+ if (hash.empty()) {
LOG("Unsupported hash, ignoring"<<endl);
return dState::INSECURE;
}
nsec3Seen = true;
- LOG("\tquery hash: "<<toBase32Hex(h)<<endl);
+ LOG("\tquery hash: "<<toBase32Hex(hash)<<endl);
string beginHash = fromBase32Hex(hashedOwner.getRawLabels()[0]);
// If the name exists, check if the qtype is denied
- if (beginHash == h) {
+ if (beginHash == hash) {
/* The NSEC3 is either a delegation one, from the parent zone, and
* must have the NS bit set but not the SOA one, or a regular NSEC3
DNSName closestEncloser(qname);
bool found = false;
-
if (needWildcardProof) {
+ nsec3sConsidered = 0;
/* We now need to look for a NSEC3 covering the closest (provable) encloser
RFC 5155 section-7.2.1
RFC 7129 section-5.5
*/
LOG("Now looking for the closest encloser for "<<qname<<endl);
- while (found == false && closestEncloser.chopOff()) {
+ while (!found && closestEncloser.chopOff() && closestEncloser.countLabels() >= numberOfLabelsOfParentZone) {
- for(const auto& v : validrrsets) {
- if(v.first.second==QType::NSEC3) {
- for(const auto& r : v.second.records) {
- LOG("\t"<<r->getZoneRepresentation()<<endl);
- auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(r);
+ for(const auto& validset : validrrsets) {
+ if(validset.first.second==QType::NSEC3) {
+ for(const auto& record : validset.second.records) {
+ LOG("\t"<<record->getZoneRepresentation()<<endl);
+ auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(record);
if (!nsec3) {
continue;
}
- const DNSName signer = getSigner(v.second.signatures);
- if (!v.first.first.isPartOf(signer)) {
- LOG("Owner "<<v.first.first<<" is not part of the signer "<<signer<<", ignoring"<<endl);
+ const DNSName signer = getSigner(validset.second.signatures);
+ if (!validset.first.first.isPartOf(signer)) {
+ LOG("Owner "<<validset.first.first<<" is not part of the signer "<<signer<<", ignoring"<<endl);
continue;
}
- string h = getHashFromNSEC3(closestEncloser, nsec3, cache);
- if (h.empty()) {
+ if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+ LOG("Too many NSEC3s for this record"<<endl);
+ return dState::NODENIAL;
+ }
+ nsec3sConsidered++;
+
+ string hash = getHashFromNSEC3(closestEncloser, *nsec3, context);
+ if (hash.empty()) {
+ LOG(": Unsupported hash, ignoring"<<endl);
return dState::INSECURE;
}
- string beginHash=fromBase32Hex(v.first.first.getRawLabels()[0]);
+ string beginHash=fromBase32Hex(validset.first.first.getRawLabels()[0]);
- LOG("Comparing "<<toBase32Hex(h)<<" ("<<closestEncloser<<") against "<<toBase32Hex(beginHash)<<endl);
- if (beginHash == h) {
+ LOG("Comparing "<<toBase32Hex(hash)<<" ("<<closestEncloser<<") against "<<toBase32Hex(beginHash)<<endl);
+ if (beginHash == hash) {
/* If the closest encloser is a delegation NS we know nothing about the names in the child zone. */
- if (isNSEC3AncestorDelegation(signer, v.first.first, nsec3)) {
+ if (isNSEC3AncestorDelegation(signer, validset.first.first, nsec3)) {
LOG("An ancestor delegation NSEC3 RR can only deny the existence of a DS"<<endl);
continue;
}
}
}
}
- if (found == true) {
+ if (found) {
break;
}
}
bool nextCloserFound = false;
bool isOptOut = false;
- if (found == true) {
+ if (found) {
/* now that we have found the closest (provable) encloser,
we can construct the next closer (RFC7129 section-5.5) name
and look for a NSEC3 RR covering it */
if (labelIdx >= 1) {
DNSName nextCloser(closestEncloser);
nextCloser.prependRawLabel(qname.getRawLabel(labelIdx - 1));
+ nsec3sConsidered = 0;
LOG("Looking for a NSEC3 covering the next closer name "<<nextCloser<<endl);
- for(const auto& v : validrrsets) {
- if(v.first.second==QType::NSEC3) {
- for(const auto& r : v.second.records) {
- LOG("\t"<<r->getZoneRepresentation()<<endl);
- auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(r);
- if(!nsec3)
+ for (const auto& validset : validrrsets) {
+ if (validset.first.second == QType::NSEC3) {
+ for (const auto& record : validset.second.records) {
+ LOG("\t"<<record->getZoneRepresentation()<<endl);
+ auto nsec3 = std::dynamic_pointer_cast<const NSEC3RecordContent>(record);
+ if (!nsec3) {
continue;
+ }
- string h = getHashFromNSEC3(nextCloser, nsec3, cache);
- if (h.empty()) {
+ if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+ LOG("Too many NSEC3s for this record"<<endl);
+ return dState::NODENIAL;
+ }
+ nsec3sConsidered++;
+
+ string hash = getHashFromNSEC3(nextCloser, *nsec3, context);
+ if (hash.empty()) {
+ LOG("Unsupported hash, ignoring"<<endl);
return dState::INSECURE;
}
- const DNSName signer = getSigner(v.second.signatures);
- if (!v.first.first.isPartOf(signer)) {
- LOG("Owner "<<v.first.first<<" is not part of the signer "<<signer<<", ignoring"<<endl);
+ const DNSName signer = getSigner(validset.second.signatures);
+ if (!validset.first.first.isPartOf(signer)) {
+ LOG("Owner "<<validset.first.first<<" is not part of the signer "<<signer<<", ignoring"<<endl);
continue;
}
- string beginHash=fromBase32Hex(v.first.first.getRawLabels()[0]);
+ string beginHash=fromBase32Hex(validset.first.first.getRawLabels()[0]);
- LOG("Comparing "<<toBase32Hex(h)<<" against "<<toBase32Hex(beginHash)<<" -> "<<toBase32Hex(nsec3->d_nexthash)<<endl);
- if (isCoveredByNSEC3Hash(h, beginHash, nsec3->d_nexthash)) {
+ LOG("Comparing "<<toBase32Hex(hash)<<" against "<<toBase32Hex(beginHash)<<" -> "<<toBase32Hex(nsec3->d_nexthash)<<endl);
+ if (isCoveredByNSEC3Hash(hash, beginHash, nsec3->d_nexthash)) {
LOG("Denies existence of name "<<qname<<"/"<<QType(qtype));
nextCloserFound = true;
LOG(endl);
break;
}
- LOG("Did not cover us ("<<qname<<"), start="<<v.first.first<<", us="<<toBase32Hex(h)<<", end="<<toBase32Hex(nsec3->d_nexthash)<<endl);
+ LOG("Did not cover us ("<<qname<<"), start="<<validset.first.first<<", us="<<toBase32Hex(hash)<<", end="<<toBase32Hex(nsec3->d_nexthash)<<endl);
}
}
if (nextCloserFound) {
if (nextCloserFound) {
bool wildcardExists = false;
/* RFC 7129 section-5.6 */
- if (needWildcardProof && !provesNSEC3NoWildCard(closestEncloser, qtype, validrrsets, &wildcardExists, cache)) {
+ if (needWildcardProof && !provesNSEC3NoWildCard(closestEncloser, qtype, validrrsets, &wildcardExists, context)) {
if (!isOptOut) {
LOG("But the existence of a wildcard is not denied for "<<qname<<"/"<<QType(qtype)<<endl);
return dState::NODENIAL;
if (isOptOut) {
return dState::OPTOUT;
}
- else {
- if (wildcardExists) {
- return dState::NXQTYPE;
- }
- return dState::NXDOMAIN;
+ if (wildcardExists) {
+ return dState::NXQTYPE;
}
+ return dState::NXDOMAIN;
}
// There were no valid NSEC(3) records
return dState::NODENIAL;
}
-/*
- * Finds all the zone-cuts between begin (longest name) and end (shortest name),
- * returns them all zone cuts, including end, but (possibly) not begin
- */
-static const vector<DNSName> getZoneCuts(const DNSName& begin, const DNSName& end, DNSRecordOracle& dro)
-{
- vector<DNSName> ret;
- if(!begin.isPartOf(end))
- throw PDNSException(end.toLogString() + "is not part of " + begin.toLogString());
-
- DNSName qname(end);
- vector<string> labelsToAdd = begin.makeRelative(end).getRawLabels();
-
- // The shortest name is assumed to a zone cut
- ret.push_back(qname);
- while(qname != begin) {
- bool foundCut = false;
- if (labelsToAdd.empty())
- break;
-
- qname.prependRawLabel(labelsToAdd.back());
- labelsToAdd.pop_back();
- auto records = dro.get(qname, (uint16_t)QType::NS);
- for (const auto& record : records) {
- if(record.d_type != QType::NS || record.d_name != qname)
- continue;
- foundCut = true;
- break;
- }
- if (foundCut)
- ret.push_back(qname);
- }
- return ret;
-}
-
-bool isRRSIGNotExpired(const time_t now, const shared_ptr<RRSIGRecordContent>& sig)
+bool isRRSIGNotExpired(const time_t now, const std::shared_ptr<RRSIGRecordContent>& sig)
{
// Should use https://www.rfc-editor.org/rfc/rfc4034.txt section 3.1.5
return sig->d_sigexpire >= now;
}
-bool isRRSIGIncepted(const time_t now, const shared_ptr<RRSIGRecordContent>& sig)
+bool isRRSIGIncepted(const time_t now, const std::shared_ptr<RRSIGRecordContent>& sig)
{
// Should use https://www.rfc-editor.org/rfc/rfc4034.txt section 3.1.5
return sig->d_siginception - g_signatureInceptionSkew <= now;
}
-static bool checkSignatureWithKey(time_t now, const shared_ptr<RRSIGRecordContent> sig, const shared_ptr<DNSKEYRecordContent> key, const std::string& msg, vState& ede)
+namespace {
+ [[nodiscard]] bool checkSignatureInceptionAndExpiry(const DNSName& qname, time_t now, const std::shared_ptr<RRSIGRecordContent>& sig, vState& ede)
+{
+ /* rfc4035:
+ - The validator's notion of the current time MUST be less than or equal to the time listed in the RRSIG RR's Expiration field.
+ - The validator's notion of the current time MUST be greater than or equal to the time listed in the RRSIG RR's Inception field.
+ */
+ if (isRRSIGIncepted(now, sig) && isRRSIGNotExpired(now, sig)) {
+ return true;
+ }
+ ede = ((sig->d_siginception - g_signatureInceptionSkew) > now) ? vState::BogusSignatureNotYetValid : vState::BogusSignatureExpired;
+ LOG("Signature is "<<(ede == vState::BogusSignatureNotYetValid ? "not yet valid" : "expired")<<" (inception: "<<sig->d_siginception<<", inception skew: "<<g_signatureInceptionSkew<<", expiration: "<<sig->d_sigexpire<<", now: "<<now<<")"<<endl);
+ return false;
+}
+
+ [[nodiscard]] bool checkSignatureWithKey(const DNSName& qname, const std::shared_ptr<RRSIGRecordContent>& sig, const std::shared_ptr<DNSKEYRecordContent>& key, const std::string& msg, vState& ede)
{
bool result = false;
try {
- /* rfc4035:
- - The validator's notion of the current time MUST be less than or equal to the time listed in the RRSIG RR's Expiration field.
- - The validator's notion of the current time MUST be greater than or equal to the time listed in the RRSIG RR's Inception field.
- */
- if (isRRSIGIncepted(now, sig) && isRRSIGNotExpired(now, sig)) {
- auto dke = DNSCryptoKeyEngine::makeFromPublicKeyString(key->d_algorithm, key->d_key);
- result = dke->verify(msg, sig->d_signature);
- LOG("signature by key with tag "<<sig->d_tag<<" and algorithm "<<DNSSECKeeper::algorithm2name(sig->d_algorithm)<<" was " << (result ? "" : "NOT ")<<"valid"<<endl);
- if (!result) {
- ede = vState::BogusNoValidRRSIG;
- }
+ auto dke = DNSCryptoKeyEngine::makeFromPublicKeyString(key->d_algorithm, key->d_key);
+ result = dke->verify(msg, sig->d_signature);
+ LOG("Signature by key with tag "<<sig->d_tag<<" and algorithm "<<DNSSECKeeper::algorithm2name(sig->d_algorithm)<<" was " << (result ? "" : "NOT ")<<"valid"<<endl);
+ if (!result) {
+ ede = vState::BogusNoValidRRSIG;
}
- else {
- ede = ((sig->d_siginception - g_signatureInceptionSkew) > now) ? vState::BogusSignatureNotYetValid : vState::BogusSignatureExpired;
- LOG("Signature is "<<(ede == vState::BogusSignatureNotYetValid ? "not yet valid" : "expired")<<" (inception: "<<sig->d_siginception<<", inception skew: "<<g_signatureInceptionSkew<<", expiration: "<<sig->d_sigexpire<<", now: "<<now<<")"<<endl);
- }
}
catch (const std::exception& e) {
LOG("Could not make a validator for signature: "<<e.what()<<endl);
return result;
}
-vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& toSign, const vector<shared_ptr<RRSIGRecordContent> >& signatures, const skeyset_t& keys, bool validateAllSigs)
+}
+
+vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& toSign, const vector<shared_ptr<RRSIGRecordContent> >& signatures, const skeyset_t& keys, pdns::validation::ValidationContext& context, bool validateAllSigs)
{
- bool foundKey = false;
+ bool missingKey = false;
bool isValid = false;
bool allExpired = true;
bool noneIncepted = true;
+ uint16_t signaturesConsidered = 0;
- for(const auto& signature : signatures) {
+ for (const auto& signature : signatures) {
unsigned int labelCount = name.countLabels();
if (signature->d_labels > labelCount) {
LOG(name<<": Discarding invalid RRSIG whose label count is "<<signature->d_labels<<" while the RRset owner name has only "<<labelCount<<endl);
continue;
}
+ vState ede = vState::Indeterminate;
+ if (!checkSignatureInceptionAndExpiry(name, now, signature, ede)) {
+ if (isRRSIGIncepted(now, signature)) {
+ noneIncepted = false;
+ }
+ if (isRRSIGNotExpired(now, signature)) {
+ allExpired = false;
+ }
+ continue;
+ }
+
+ if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) {
+ LOG("We have already considered "<<std::to_string(signaturesConsidered)<<" RRSIG"<<addS(signaturesConsidered)<<" for this record, stopping now"<<endl;);
+ // possibly going Bogus, the RRSIGs have not been validated so Insecure would be wrong
+ break;
+ }
+ signaturesConsidered++;
+ context.d_validationsCounter++;
+
auto keysMatchingTag = getByTag(keys, signature->d_tag, signature->d_algorithm);
if (keysMatchingTag.empty()) {
LOG("No key provided for "<<signature->d_tag<<" and algorithm "<<std::to_string(signature->d_algorithm)<<endl;);
+ missingKey = true;
continue;
}
string msg = getMessageForRRSET(name, *signature, toSign, true);
+ uint16_t dnskeysConsidered = 0;
for (const auto& key : keysMatchingTag) {
- vState ede;
- bool signIsValid = checkSignatureWithKey(now, signature, key, msg, ede);
- foundKey = true;
+ if (g_maxDNSKEYsToConsider > 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) {
+ LOG("We have already considered "<<std::to_string(dnskeysConsidered)<<" DNSKEY"<<addS(dnskeysConsidered)<<" for tag "<<std::to_string(signature->d_tag)<<" and algorithm "<<std::to_string(signature->d_algorithm)<<", not considering the remaining ones for this signature"<<endl;);
+ return isValid ? vState::Secure : vState::BogusNoValidRRSIG;
+ }
+ dnskeysConsidered++;
+
+ bool signIsValid = checkSignatureWithKey(name, signature, key, msg, ede);
if (signIsValid) {
isValid = true;
if (isValid) {
return vState::Secure;
}
- if (!foundKey) {
+ if (missingKey) {
return vState::BogusNoValidRRSIG;
}
if (noneIncepted) {
return vState::BogusNoValidRRSIG;
}
-void validateWithKeySet(const cspmap_t& rrsets, cspmap_t& validated, const skeyset_t& keys)
-{
- validated.clear();
- /* cerr<<"Validating an rrset with following keys: "<<endl;
- for(auto& key : keys) {
- cerr<<"\tTag: "<<key->getTag()<<" -> "<<key->getZoneRepresentation()<<endl;
- }
- */
- time_t now = time(nullptr);
- for(auto i=rrsets.cbegin(); i!=rrsets.cend(); i++) {
- LOG("validating "<<(i->first.first)<<"/"<<DNSRecordContent::NumberToType(i->first.second)<<" with "<<i->second.signatures.size()<<" sigs"<<endl);
- if (validateWithKeySet(now, i->first.first, i->second.records, i->second.signatures, keys, true) == vState::Secure) {
- validated[i->first] = i->second;
- }
- }
-}
-
// returns vState
// should return vState, zone cut and validated keyset
// i.e. www.7bits.nl -> insecure/7bits.nl/[]
cspmap_t cspmap;
for(const auto& rec : recs) {
// cerr<<"res "<<rec.d_name<<"/"<<rec.d_type<<endl;
- if(rec.d_type == QType::OPT) continue;
+ if (rec.d_type == QType::OPT) {
+ continue;
+ }
if(rec.d_type == QType::RRSIG) {
auto rrc = getRR<RRSIGRecordContent>(rec);
bool getTrustAnchor(const map<DNSName,dsmap_t>& anchors, const DNSName& zone, dsmap_t &res)
{
- const auto& it = anchors.find(zone);
+ const auto& iter = anchors.find(zone);
- if (it == anchors.cend()) {
+ if (iter == anchors.cend()) {
return false;
}
- res = it->second;
+ res = iter->second;
return true;
}
bool haveNegativeTrustAnchor(const map<DNSName,std::string>& negAnchors, const DNSName& zone, std::string& reason)
{
- const auto& it = negAnchors.find(zone);
+ const auto& iter = negAnchors.find(zone);
- if (it == negAnchors.cend()) {
+ if (iter == negAnchors.cend()) {
return false;
}
- reason = it->second;
+ reason = iter->second;
return true;
}
-vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector<shared_ptr<RRSIGRecordContent> >& sigs, skeyset_t& validkeys)
+vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector<shared_ptr<RRSIGRecordContent> >& sigs, skeyset_t& validkeys, pdns::validation::ValidationContext& context)
{
/*
* Check all DNSKEY records against all DS records and place all DNSKEY records
* that have DS records (that we support the algo for) in the tentative key storage
*/
- for (const auto& dsrc : dsmap)
- {
- auto r = getByTag(tkeys, dsrc.d_tag, dsrc.d_algorithm);
+ uint16_t dssConsidered = 0;
+ for (const auto& dsrc : dsmap) {
+ if (g_maxDSsToConsider > 0 && dssConsidered > g_maxDSsToConsider) {
+ LOG("We have already considered "<<std::to_string(dssConsidered)<<" DS"<<addS(dssConsidered)<<", not considering the remaining ones"<<endl;);
+ return vState::BogusNoValidDNSKEY;
+ }
+ ++dssConsidered;
+
+ uint16_t dnskeysConsidered = 0;
+ auto record = getByTag(tkeys, dsrc.d_tag, dsrc.d_algorithm);
// cerr<<"looking at DS with tag "<<dsrc.d_tag<<", algo "<<DNSSECKeeper::algorithm2name(dsrc.d_algorithm)<<", digest "<<std::to_string(dsrc.d_digesttype)<<" for "<<zone<<", got "<<r.size()<<" DNSKEYs for tag"<<endl;
- for (const auto& drc : r)
- {
+ for (const auto& drc : record) {
bool isValid = false;
bool dsCreated = false;
DSRecordContent dsrc2;
+
+ if (g_maxDNSKEYsToConsider > 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) {
+ LOG("We have already considered "<<std::to_string(dnskeysConsidered)<<" DNSKEY"<<addS(dnskeysConsidered)<<" for tag "<<std::to_string(dsrc.d_tag)<<" and algorithm "<<std::to_string(dsrc.d_algorithm)<<", not considering the remaining ones for this DS"<<endl;);
+ // we need to break because we can have a partially validated set
+ // where the KSK signs the ZSK(s), and even if we don't
+ // we are going to try to get the correct EDE status (revoked, expired, ...)
+ break;
+ }
+ dnskeysConsidered++;
+
try {
dsrc2 = makeDSFromDNSKey(zone, *drc, dsrc.d_digesttype);
dsCreated = true;
// cerr<<"got "<<validkeys.size()<<"/"<<tkeys.size()<<" valid/tentative keys"<<endl;
// these counts could be off if we somehow ended up with
// duplicate keys. Should switch to a type that prevents that.
- if (validkeys.size() < tkeys.size())
- {
+ if (!tkeys.empty() && validkeys.size() < tkeys.size()) {
// this should mean that we have one or more DS-validated DNSKEYs
// but not a fully validated DNSKEY set, yet
// one of these valid DNSKEYs should be able to validate the
// whole set
- for (const auto& sig : sigs)
- {
+ uint16_t signaturesConsidered = 0;
+ for (const auto& sig : sigs) {
+ if (!checkSignatureInceptionAndExpiry(zone, now, sig, ede)) {
+ continue;
+ }
+
// cerr<<"got sig for keytag "<<i->d_tag<<" matching "<<getByTag(tkeys, i->d_tag).size()<<" keys of which "<<getByTag(validkeys, i->d_tag).size()<<" valid"<<endl;
auto bytag = getByTag(validkeys, sig->d_tag, sig->d_algorithm);
continue;
}
+ if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) {
+ LOG("We have already considered "<<std::to_string(signaturesConsidered)<<" RRSIG"<<addS(signaturesConsidered)<<" for this record, stopping now"<<endl;);
+ // possibly going Bogus, the RRSIGs have not been validated so Insecure would be wrong
+ return vState::BogusNoValidDNSKEY;
+ }
+
string msg = getMessageForRRSET(zone, *sig, toSign);
+ uint16_t dnskeysConsidered = 0;
for (const auto& key : bytag) {
+ if (g_maxDNSKEYsToConsider > 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) {
+ LOG("We have already considered "<<std::to_string(dnskeysConsidered)<<" DNSKEY"<<addS(dnskeysConsidered)<<" for tag "<<std::to_string(sig->d_tag)<<" and algorithm "<<std::to_string(sig->d_algorithm)<<", not considering the remaining ones for this signature"<<endl;);
+ return vState::BogusNoValidDNSKEY;
+ }
+ dnskeysConsidered++;
+
+ if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) {
+ LOG("We have already considered "<<std::to_string(signaturesConsidered)<<" RRSIG"<<addS(signaturesConsidered)<<" for this record, stopping now"<<endl;);
+ // possibly going Bogus, the RRSIGs have not been validated so Insecure would be wrong
+ return vState::BogusNoValidDNSKEY;
+ }
// cerr<<"validating : ";
- bool signIsValid = checkSignatureWithKey(now, sig, key, msg, ede);
+ bool signIsValid = checkSignatureWithKey(zone, sig, key, msg, ede);
+ signaturesConsidered++;
+ context.d_validationsCounter++;
- if (signIsValid)
- {
+ if (signIsValid) {
LOG("validation succeeded - whole DNSKEY set is valid"<<endl);
validkeys = tkeys;
break;
}
- else {
- LOG("Validation did not succeed!"<<endl);
- }
+ LOG("Validation did not succeed!"<<endl);
+ }
+
+ if (validkeys.size() == tkeys.size()) {
+ // we validated the whole DNSKEY set already */
+ break;
}
// if(validkeys.empty()) cerr<<"did not manage to validate DNSKEY set based on DS-validated KSK, only passing KSK on"<<endl;
}
return vState::Secure;
}
-vState getKeysFor(DNSRecordOracle& dro, const DNSName& zone, skeyset_t& keyset)
+bool isSupportedDS(const DSRecordContent& dsrec)
{
- auto luaLocal = g_luaconfs.getLocal();
- const auto anchors = luaLocal->dsAnchors;
- if (anchors.empty()) // Nothing to do here
- return vState::Insecure;
-
- // Determine the lowest (i.e. with the most labels) Trust Anchor for zone
- DNSName lowestTA(".");
- for (auto const &anchor : anchors)
- if (zone.isPartOf(anchor.first) && lowestTA.countLabels() < anchor.first.countLabels())
- lowestTA = anchor.first;
-
- // Before searching for the keys, see if we have a Negative Trust Anchor. If
- // so, test if the NTA is valid and return an NTA state
- const auto negAnchors = luaLocal->negAnchors;
-
- if (!negAnchors.empty()) {
- DNSName lowestNTA;
-
- for (auto const &negAnchor : negAnchors)
- if (zone.isPartOf(negAnchor.first) && lowestNTA.countLabels() <= negAnchor.first.countLabels())
- lowestNTA = negAnchor.first;
-
- if(!lowestNTA.empty()) {
- LOG("Found a Negative Trust Anchor for "<<lowestNTA<<", which was added with reason '"<<negAnchors.at(lowestNTA)<<"', ");
-
- /* RFC 7646 section 2.1 tells us that we SHOULD still validate if there
- * is a Trust Anchor below the Negative Trust Anchor for the name we
- * attempt validation for. However, section 3 tells us this positive
- * Trust Anchor MUST be *below* the name and not the name itself
- */
- if(lowestTA.countLabels() <= lowestNTA.countLabels()) {
- LOG("marking answer Insecure"<<endl);
- return vState::NTA; // Not Insecure, this way validateRecords() can shortcut
- }
- LOG("but a Trust Anchor for "<<lowestTA<<" is configured, continuing validation."<<endl);
- }
- }
-
- skeyset_t validkeys;
- dsmap_t dsmap;
-
- dsmap_t* tmp = (dsmap_t*) rplookup(anchors, lowestTA);
- if (tmp)
- dsmap = *tmp;
-
- auto zoneCuts = getZoneCuts(zone, lowestTA, dro);
-
- LOG("Found the following zonecuts:")
- for(const auto& zonecut : zoneCuts)
- LOG(" => "<<zonecut);
- LOG(endl);
-
- for(auto zoneCutIter = zoneCuts.cbegin(); zoneCutIter != zoneCuts.cend(); ++zoneCutIter)
- {
- vector<shared_ptr<RRSIGRecordContent> > sigs;
- sortedRecords_t toSign;
-
- skeyset_t tkeys; // tentative keys
- validkeys.clear();
-
- // cerr<<"got DS for ["<<qname<<"], grabbing DNSKEYs"<<endl;
- auto records=dro.get(*zoneCutIter, (uint16_t)QType::DNSKEY);
- // this should use harvest perhaps
- for(const auto& rec : records) {
- if(rec.d_name != *zoneCutIter)
- continue;
-
- if(rec.d_type == QType::RRSIG)
- {
- auto rrc=getRR<RRSIGRecordContent> (rec);
- if(rrc) {
- LOG("Got signature: "<<rrc->getZoneRepresentation()<<" with tag "<<rrc->d_tag<<", for type "<<DNSRecordContent::NumberToType(rrc->d_type)<<endl);
- if(rrc->d_type != QType::DNSKEY)
- continue;
- sigs.push_back(rrc);
- }
- }
- else if(rec.d_type == QType::DNSKEY)
- {
- auto drc=getRR<DNSKEYRecordContent> (rec);
- if(drc) {
- tkeys.insert(drc);
- LOG("Inserting key with tag "<<drc->getTag()<<" and algorithm "<<DNSSECKeeper::algorithm2name(drc->d_algorithm)<<": "<<drc->getZoneRepresentation()<<endl);
-
- toSign.insert(rec.d_content);
- }
- }
- }
- LOG("got "<<tkeys.size()<<" keys and "<<sigs.size()<<" sigs from server"<<endl);
-
- /*
- * Check all DNSKEY records against all DS records and place all DNSKEY records
- * that have DS records (that we support the algo for) in the tentative key storage
- */
- auto state = validateDNSKeysAgainstDS(time(nullptr), *zoneCutIter, dsmap, tkeys, toSign, sigs, validkeys);
-
- if (validkeys.empty())
- {
- LOG("ended up with zero valid DNSKEYs, going Bogus"<<endl);
- return state;
- }
- LOG("situation: we have one or more valid DNSKEYs for ["<<*zoneCutIter<<"] (want ["<<zone<<"])"<<endl);
-
- if (zoneCutIter == zoneCuts.cend()-1) {
- LOG("requested keyset found! returning Secure for the keyset"<<endl);
- keyset.insert(validkeys.cbegin(), validkeys.cend());
- return state;
- }
-
- // We now have the DNSKEYs, use them to validate the DS records at the next zonecut
- LOG("next name ["<<*(zoneCutIter+1)<<"], trying to get DS"<<endl);
-
- dsmap_t tdsmap; // tentative DSes
- dsmap.clear();
- toSign.clear();
-
- auto recs=dro.get(*(zoneCutIter+1), QType::DS);
-
- cspmap_t cspmap=harvestCSPFromRecs(recs);
-
- cspmap_t validrrsets;
- validateWithKeySet(cspmap, validrrsets, validkeys);
-
- LOG("got "<<cspmap.count(pair(*(zoneCutIter+1),QType::DS))<<" records for DS query of which "<<validrrsets.count(pair(*(zoneCutIter+1),QType::DS))<<" valid "<<endl);
-
- auto r = validrrsets.equal_range(pair(*(zoneCutIter+1), QType::DS));
- if(r.first == r.second) {
- LOG("No DS for "<<*(zoneCutIter+1)<<", now look for a secure denial"<<endl);
- dState res = getDenial(validrrsets, *(zoneCutIter+1), QType::DS, true, true);
- if (res == dState::INSECURE || res == dState::NXDOMAIN)
- return vState::BogusInvalidDenial;
- if (res == dState::NXQTYPE || res == dState::OPTOUT)
- return vState::Insecure;
- }
-
- /*
- * Collect all DS records and add them to the dsmap for the next iteration
- */
- for(auto cspiter =r.first; cspiter!=r.second; cspiter++) {
- for(auto j=cspiter->second.records.cbegin(); j!=cspiter->second.records.cend(); j++)
- {
- const auto dsrc=std::dynamic_pointer_cast<DSRecordContent>(*j);
- if(dsrc) {
- dsmap.insert(*dsrc);
- }
- }
- }
- }
- // There were no zone cuts (aka, we should never get here)
- return vState::BogusUnableToGetDNSKEYs;
-}
-
-bool isSupportedDS(const DSRecordContent& ds)
-{
- if (!DNSCryptoKeyEngine::isAlgorithmSupported(ds.d_algorithm)) {
- LOG("Discarding DS "<<ds.d_tag<<" because we don't support algorithm number "<<std::to_string(ds.d_algorithm)<<endl);
+ if (!DNSCryptoKeyEngine::isAlgorithmSupported(dsrec.d_algorithm)) {
+ LOG("Discarding DS "<<dsrec.d_tag<<" because we don't support algorithm number "<<std::to_string(dsrec.d_algorithm)<<endl);
return false;
}
- if (!DNSCryptoKeyEngine::isDigestSupported(ds.d_digesttype)) {
- LOG("Discarding DS "<<ds.d_tag<<" because we don't support digest number "<<std::to_string(ds.d_digesttype)<<endl);
+ if (!DNSCryptoKeyEngine::isDigestSupported(dsrec.d_digesttype)) {
+ LOG("Discarding DS "<<dsrec.d_tag<<" because we don't support digest number "<<std::to_string(dsrec.d_digesttype)<<endl);
return false;
}
}
}
- return DNSName();
+ return {};
}
const std::string& vStateToString(vState state)
return vStates.at(static_cast<size_t>(state));
}
-std::ostream& operator<<(std::ostream &os, const vState d)
+std::ostream& operator<<(std::ostream &ostr, const vState dstate)
{
- os<<vStateToString(d);
- return os;
+ ostr<<vStateToString(dstate);
+ return ostr;
}
-std::ostream& operator<<(std::ostream &os, const dState d)
+std::ostream& operator<<(std::ostream &ostr, const dState dstate)
{
static const std::vector<std::string> dStates = {"no denial", "inconclusive", "nxdomain", "nxqtype", "empty non-terminal", "insecure", "opt-out"};
- os<<dStates.at(static_cast<size_t>(d));
- return os;
+ ostr<<dStates.at(static_cast<size_t>(dstate));
+ return ostr;
}
void updateDNSSECValidationState(vState& state, const vState stateUpdate)
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) {
*/
#pragma once
+#include <tuple>
+
#include "dnsparser.hh"
#include "dnsname.hh"
#include <vector>
#include "namespaces.hh"
#include "dnsrecords.hh"
#include "dnssecinfra.hh"
-
+
extern bool g_dnssecLOG;
extern time_t g_signatureInceptionSkew;
extern uint16_t g_maxNSEC3Iterations;
+extern uint16_t g_maxRRSIGsPerRecordToConsider;
+extern uint16_t g_maxNSEC3sPerRecordToConsider;
+extern uint16_t g_maxDNSKEYsToConsider;
+extern uint16_t g_maxDSsToConsider;
// 4033 5
enum class vState : uint8_t { Indeterminate, Insecure, Secure, NTA, TA, BogusNoValidDNSKEY, BogusInvalidDenial, BogusUnableToGetDSs, BogusUnableToGetDNSKEYs, BogusSelfSignedDS, BogusNoRRSIG, BogusNoValidRRSIG, BogusMissingNegativeIndication, BogusSignatureNotYetValid, BogusSignatureExpired, BogusUnsupportedDNSKEYAlgo, BogusUnsupportedDSDigestType, BogusNoZoneKeyBitSet, BogusRevokedDNSKEY, BogusInvalidDNSKEYProtocol };
typedef set<shared_ptr<DNSKEYRecordContent>, sharedDNSKeyRecordContentCompare > skeyset_t;
+namespace pdns
+{
+namespace validation
+{
+using Nsec3HashesCache = std::map<std::tuple<DNSName, std::string, uint16_t>, std::string>;
+
+struct ValidationContext
+{
+ Nsec3HashesCache d_nsec3Cache;
+ unsigned int d_validationsCounter{0};
+ unsigned int d_nsec3IterationsRemainingQuota{0};
+};
-vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& records, const vector<shared_ptr<RRSIGRecordContent> >& signatures, const skeyset_t& keys, bool validateAllSigs=true);
+class TooManySEC3IterationsException : public std::runtime_error
+{
+public:
+ TooManySEC3IterationsException(): std::runtime_error("Too many NSEC3 hash computations per query")
+ {
+ }
+};
+
+}
+}
+vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& records, const vector<shared_ptr<RRSIGRecordContent> >& signatures, const skeyset_t& keys, pdns::validation::ValidationContext& context, bool validateAllSigs=true);
bool isCoveredByNSEC(const DNSName& name, const DNSName& begin, const DNSName& next);
bool isCoveredByNSEC3Hash(const std::string& h, const std::string& beginHash, const std::string& nextHash);
bool isCoveredByNSEC3Hash(const DNSName& h, const DNSName& beginHash, const DNSName& nextHash);
vState getKeysFor(DNSRecordOracle& dro, const DNSName& zone, skeyset_t& keyset);
bool getTrustAnchor(const map<DNSName,dsmap_t>& anchors, const DNSName& zone, dsmap_t &res);
bool haveNegativeTrustAnchor(const map<DNSName,std::string>& negAnchors, const DNSName& zone, std::string& reason);
-vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector<shared_ptr<RRSIGRecordContent> >& sigs, skeyset_t& validkeys);
-dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, bool needsWildcardProof=true, unsigned int wildcardLabelsCount=0);
+vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector<shared_ptr<RRSIGRecordContent> >& sigs, skeyset_t& validkeys, pdns::validation::ValidationContext& context);
+dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, pdns::validation::ValidationContext& context, bool needsWildcardProof=true, unsigned int wildcardLabelsCount=0);
bool isSupportedDS(const DSRecordContent& ds);
DNSName getSigner(const std::vector<std::shared_ptr<RRSIGRecordContent> >& signatures);
-bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>& dsrecords);
+bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>& dsrecords, pdns::validation::ValidationContext& context);
bool isRRSIGNotExpired(const time_t now, const std::shared_ptr<RRSIGRecordContent>& sig);
bool isRRSIGIncepted(const time_t now, const shared_ptr<RRSIGRecordContent>& sig);
bool isWildcardExpanded(unsigned int labelCount, const std::shared_ptr<RRSIGRecordContent>& sign);
bool isNSEC3AncestorDelegation(const DNSName& signer, const DNSName& owner, const std::shared_ptr<NSEC3RecordContent>& nsec3);
DNSName getNSECOwnerName(const DNSName& initialOwner, const std::vector<std::shared_ptr<RRSIGRecordContent> >& signatures);
DNSName getClosestEncloserFromNSEC(const DNSName& name, const DNSName& owner, const DNSName& next);
+[[nodiscard]] uint64_t getNSEC3DenialProofWorstCaseIterationsCount(uint8_t maxLabels, uint16_t iterations, size_t saltLength);
+[[nodiscard]] std::string getHashFromNSEC3(const DNSName& qname, uint16_t iterations, const std::string& salt, pdns::validation::ValidationContext& context);
template <typename NSEC> bool isTypeDenied(const NSEC& nsec, const QType& type)
{
_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