#include "validate.hh"
std::unique_ptr<AggressiveNSECCache> g_aggressiveNSECCache{nullptr};
+uint64_t AggressiveNSECCache::s_nsec3DenialProofMaxCost{0};
uint8_t AggressiveNSECCache::s_maxNSEC3CommonPrefix = AggressiveNSECCache::s_default_maxNSEC3CommonPrefix;
/* this is defined in syncres.hh and we are not importing that here */
return true;
}
-bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded<AggressiveNSECCache::ZoneEntry>>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<const RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, const OptLog& log)
+bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded<AggressiveNSECCache::ZoneEntry>>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<const RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, const OptLog& log, pdns::validation::ValidationContext& validationContext)
{
DNSName zone;
std::string salt;
iterations = entry->d_iterations;
}
- auto nameHash = DNSName(toBase32Hex(hashQNameWithSalt(salt, iterations, name))) + zone;
+ const auto zoneLabelsCount = zone.countLabels();
+ if (s_nsec3DenialProofMaxCost != 0) {
+ const auto worstCaseIterations = getNSEC3DenialProofWorstCaseIterationsCount(name.countLabels() - zoneLabelsCount, iterations, salt.length());
+ if (worstCaseIterations > s_nsec3DenialProofMaxCost) {
+ // skip NSEC3 aggressive cache for expensive NSEC3 parameters: "if you want us to take the pain of PRSD away from you, you need to make it cheap for us to do so"
+ VLOG(log, name << ": Skipping aggressive use of the NSEC3 cache since the zone parameters are too expensive" << endl);
+ return false;
+ }
+ }
+
+ auto nameHash = DNSName(toBase32Hex(getHashFromNSEC3(name, iterations, salt, validationContext))) + zone;
ZoneEntry::CacheEntry exactNSEC3;
if (getNSEC3(now, zoneEntry, nameHash, exactNSEC3)) {
DNSName closestEncloser(name);
bool found = false;
ZoneEntry::CacheEntry closestNSEC3;
- while (!found && closestEncloser.chopOff()) {
- auto closestHash = DNSName(toBase32Hex(hashQNameWithSalt(salt, iterations, closestEncloser))) + zone;
+ auto remainingLabels = closestEncloser.countLabels() - 1;
+ while (!found && closestEncloser.chopOff() && remainingLabels >= zoneLabelsCount) {
+ auto closestHash = DNSName(toBase32Hex(getHashFromNSEC3(closestEncloser, iterations, salt, validationContext))) + zone;
+ remainingLabels--;
if (getNSEC3(now, zoneEntry, closestHash, closestNSEC3)) {
VLOG(log, name << ": Found closest encloser at " << closestEncloser << " (" << closestHash << ")" << endl);
DNSName nsecFound;
DNSName nextCloser(closestEncloser);
nextCloser.prependRawLabel(name.getRawLabel(labelIdx - 1));
- auto nextCloserHash = toBase32Hex(hashQNameWithSalt(salt, iterations, nextCloser));
+ auto nextCloserHash = toBase32Hex(getHashFromNSEC3(nextCloser, iterations, salt, validationContext));
VLOG(log, name << ": Looking for a NSEC3 covering the next closer " << nextCloser << " (" << nextCloserHash << ")" << endl);
ZoneEntry::CacheEntry nextCloserEntry;
/* An ancestor NSEC3 would be fine here, since it does prove that there is no delegation at the next closer
name (we don't insert opt-out NSEC3s into the cache). */
DNSName wildcard(g_wildcarddnsname + closestEncloser);
- auto wcHash = toBase32Hex(hashQNameWithSalt(salt, iterations, wildcard));
+ auto wcHash = toBase32Hex(getHashFromNSEC3(wildcard, iterations, salt, validationContext));
VLOG(log, name << ": Looking for a NSEC3 covering the wildcard " << wildcard << " (" << wcHash << ")" << endl);
ZoneEntry::CacheEntry wcEntry;
return true;
}
-bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, const ComboAddress& who, const boost::optional<std::string>& routingTag, bool doDNSSEC, const OptLog& log)
+bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, const ComboAddress& who, const boost::optional<std::string>& routingTag, bool doDNSSEC, pdns::validation::ValidationContext& validationContext, const OptLog& log)
{
std::shared_ptr<LockGuarded<ZoneEntry>> zoneEntry;
if (type == QType::DS) {
}
if (nsec3) {
- return getNSEC3Denial(now, zoneEntry, soaSet, soaSignatures, name, type, ret, res, doDNSSEC, log);
+ return getNSEC3Denial(now, zoneEntry, soaSet, soaSignatures, name, type, ret, res, doDNSSEC, log, validationContext);
}
ZoneEntry::CacheEntry entry;
#include "lock.hh"
#include "stat_t.hh"
#include "logger.hh"
+#include "validate.hh"
class AggressiveNSECCache
{
public:
- static const uint8_t s_default_maxNSEC3CommonPrefix = 10;
+ static constexpr uint8_t s_default_maxNSEC3CommonPrefix = 10;
+ static uint64_t s_nsec3DenialProofMaxCost;
static uint8_t s_maxNSEC3CommonPrefix;
AggressiveNSECCache(uint64_t entries) :
}
void insertNSEC(const DNSName& zone, const DNSName& owner, const DNSRecord& record, const std::vector<std::shared_ptr<const RRSIGRecordContent>>& signatures, bool nsec3);
- bool getDenial(time_t, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, const ComboAddress& who, const boost::optional<std::string>& routingTag, bool doDNSSEC, const OptLog& log = std::nullopt);
+ bool getDenial(time_t, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, const ComboAddress& who, const boost::optional<std::string>& routingTag, bool doDNSSEC, pdns::validation::ValidationContext& validationContext, const OptLog& log = std::nullopt);
void removeZoneInfo(const DNSName& zone, bool subzones);
std::shared_ptr<LockGuarded<ZoneEntry>> getBestZone(const DNSName& zone);
bool getNSECBefore(time_t now, std::shared_ptr<LockGuarded<ZoneEntry>>& zoneEntry, const DNSName& name, ZoneEntry::CacheEntry& entry);
bool getNSEC3(time_t now, std::shared_ptr<LockGuarded<ZoneEntry>>& zoneEntry, const DNSName& name, ZoneEntry::CacheEntry& entry);
- bool getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded<ZoneEntry>>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<const RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, const OptLog&);
+ bool getNSEC3Denial(time_t now, std::shared_ptr<LockGuarded<ZoneEntry>>& zoneEntry, std::vector<DNSRecord>& soaSet, std::vector<std::shared_ptr<const RRSIGRecordContent>>& soaSignatures, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, const OptLog&, pdns::validation::ValidationContext& validationContext);
bool synthesizeFromNSEC3Wildcard(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, ZoneEntry::CacheEntry& nextCloser, const DNSName& wildcardName, const OptLog&);
bool synthesizeFromNSECWildcard(time_t now, const DNSName& name, const QType& type, std::vector<DNSRecord>& ret, int& res, bool doDNSSEC, ZoneEntry::CacheEntry& nsec, const DNSName& wildcardName, const OptLog&);
res = RCode::ServFail;
break;
}
+ catch (const pdns::validation::TooManySEC3IterationsException& e) {
+ if (g_logCommonErrors) {
+ SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << comboWriter->getRemote() << " during resolve of the custom filter policy '" << appliedPolicy.getName() << "' while resolving '" << comboWriter->d_mdp.d_qname << "' because: " << e.what() << endl,
+ resolver.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during resolve of the custom filter policy",
+ "policyName", Logging::Loggable(appliedPolicy.getName()), "exception", Logging::Loggable("TooManySEC3IterationsException")));
+ }
+ res = RCode::ServFail;
+ break;
+ }
catch (const PolicyHitException& e) {
if (g_logCommonErrors) {
SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << comboWriter->getRemote() << " during resolve of the custom filter policy '" << appliedPolicy.getName() << "' while resolving '" << comboWriter->d_mdp.d_qname << "' because another RPZ policy was hit" << endl,
}
res = RCode::ServFail;
}
+ catch (const pdns::validation::TooManySEC3IterationsException& e) {
+ if (g_logCommonErrors) {
+ SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << comboWriter->getRemote() << " during resolve of '" << comboWriter->d_mdp.d_qname << "' because: " << e.what() << endl,
+ resolver.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during resolve"));
+ }
+ res = RCode::ServFail;
+ }
catch (const SendTruncatedAnswerException& e) {
ret.clear();
resolver.d_appliedPolicy.addSOAtoRPZResult(ret);
}
goto sendit; // NOLINT(cppcoreguidelines-avoid-goto)
}
+ catch (const pdns::validation::TooManySEC3IterationsException& e) {
+ if (g_logCommonErrors) {
+ SLOG(g_log << Logger::Notice << "Sending SERVFAIL to " << comboWriter->getRemote() << " during validation of '" << comboWriter->d_mdp.d_qname << "|" << QType(comboWriter->d_mdp.d_qtype) << "' because: " << e.what() << endl,
+ resolver.d_slog->error(Logr::Notice, e.what(), "Sending SERVFAIL during validation", "exception", Logging::Loggable("TooManySEC3IterationsException")));
+ }
+ goto sendit; // NOLINT(cppcoreguidelines-avoid-goto)
+ }
}
if (!ret.empty()) {
g_dnssecLogBogus = ::arg().mustDo("dnssec-log-bogus");
g_maxNSEC3Iterations = ::arg().asNum("nsec3-max-iterations");
+ g_maxRRSIGsPerRecordToConsider = ::arg().asNum("max-rrsigs-per-record");
+ g_maxNSEC3sPerRecordToConsider = ::arg().asNum("max-nsec3s-per-record");
+ g_maxDNSKEYsToConsider = ::arg().asNum("max-dnskeys");
+ g_maxDSsToConsider = ::arg().asNum("max-ds-per-zone");
vector<string> nums;
bool automatic = true;
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");
}
}
+ AggressiveNSECCache::s_nsec3DenialProofMaxCost = ::arg().asNum("aggressive-cache-max-nsec3-hash-cost");
AggressiveNSECCache::s_maxNSEC3CommonPrefix = static_cast<uint8_t>(std::round(std::log2(::arg().asNum("aggressive-cache-min-nsec3-hit-ratio"))));
SLOG(g_log << Logger::Debug << "NSEC3 aggressive cache tuning: aggressive-cache-min-nsec3-hit-ratio: " << ::arg().asNum("aggressive-cache-min-nsec3-hit-ratio") << " max common prefix bits: " << std::to_string(AggressiveNSECCache::s_maxNSEC3CommonPrefix) << endl,
log->info(Logr::Debug, "NSEC3 aggressive cache tuning", "aggressive-cache-min-nsec3-hit-ratio", Logging::Loggable(::arg().asNum("aggressive-cache-min-nsec3-hit-ratio")), "maxCommonPrefixBits", Logging::Loggable(AggressiveNSECCache::s_maxNSEC3CommonPrefix)));
vState ZoneData::dnssecValidate(pdns::ZoneMD& zonemd, size_t& zonemdCount) const
{
+ pdns::validation::ValidationContext validationContext;
+ validationContext.d_nsec3IterationsRemainingQuota = std::numeric_limits<decltype(validationContext.d_nsec3IterationsRemainingQuota)>::max();
zonemdCount = 0;
SyncRes resolver({d_now, 0});
}
skeyset_t validKeys;
- vState dnsKeyState = validateDNSKeysAgainstDS(d_now, d_zone, dsmap, dnsKeys, records, zonemd.getRRSIGs(), validKeys, std::nullopt);
+ vState dnsKeyState = validateDNSKeysAgainstDS(d_now, d_zone, dsmap, dnsKeys, records, zonemd.getRRSIGs(), validKeys, std::nullopt, validationContext);
if (dnsKeyState != vState::Secure) {
return dnsKeyState;
}
if (!nsecs.records.empty() && !nsecs.signatures.empty()) {
// Valdidate the NSEC
- nsecValidationStatus = validateWithKeySet(d_now, d_zone, nsecs.records, nsecs.signatures, validKeys, std::nullopt);
+ nsecValidationStatus = validateWithKeySet(d_now, d_zone, nsecs.records, nsecs.signatures, validKeys, std::nullopt, validationContext);
csp.emplace(std::pair(d_zone, QType::NSEC), nsecs);
}
else if (!nsec3s.records.empty() && !nsec3s.signatures.empty()) {
for (const auto& rec : zonemd.getNSEC3Params()) {
records.emplace(rec);
}
- nsecValidationStatus = validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys, std::nullopt);
+ nsecValidationStatus = validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys, std::nullopt, validationContext);
if (nsecValidationStatus != vState::Secure) {
d_log->info(Logr::Warning, "NSEC3PARAMS records did not validate");
return nsecValidationStatus;
}
// Valdidate the NSEC3
- nsecValidationStatus = validateWithKeySet(d_now, zonemd.getNSEC3Label(), nsec3s.records, nsec3s.signatures, validKeys, std::nullopt);
+ nsecValidationStatus = validateWithKeySet(d_now, zonemd.getNSEC3Label(), nsec3s.records, nsec3s.signatures, validKeys, std::nullopt, validationContext);
csp.emplace(std::pair(zonemd.getNSEC3Label(), QType::NSEC3), nsec3s);
}
else {
return nsecValidationStatus;
}
- auto denial = getDenial(csp, d_zone, QType::ZONEMD, false, false, std::nullopt, true);
+ auto denial = getDenial(csp, d_zone, QType::ZONEMD, false, false, validationContext, std::nullopt, true);
if (denial == dState::NXQTYPE) {
d_log->info(Logr::Info, "Validated denial of existence of ZONEMD record");
return vState::Secure;
for (const auto& rec : zonemdRecords) {
records.emplace(rec);
}
- return validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys, std::nullopt);
+ return validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys, std::nullopt, validationContext);
}
void ZoneData::ZoneToCache(const RecZoneToCache::Config& config)
.. note::
DoT probing is an experimental feature.
- Please test thoroughly to determine if it is suitable in your specific production environment before enabling.
+ Please test thoroughly to determine if it is suitable in your specific production environment before enabling.
''',
'versionadded': '4.7.0'
},
'versionchanged': [('4.5.2', 'Default is now 150, was 2500 before.'),
('5.0.0', 'Default is now 50, was 150 before.')]
},
+ {
+ 'name' : 'max_rrsigs_per_record',
+ 'section' : 'dnssec',
+ 'type' : LType.Uint64,
+ 'default' : '2',
+ 'help' : 'Maximum number of RRSIGs to consider when validating a given record',
+ 'doc' : '''
+Maximum number of RRSIGs we are willing to cryptographically check when validating a given record. Expired or not yet incepted RRSIGs do not count toward to this limit.
+ ''',
+ 'versionadded': ['5.0.2', '4.9.3', '4.8.6'],
+ },
+ {
+ 'name' : 'max_nsec3s_per_record',
+ 'section' : 'dnssec',
+ 'type' : LType.Uint64,
+ 'default' : '10',
+ 'help' : 'Maximum number of NSEC3s to consider when validating a given denial of existence',
+ 'doc' : '''
+Maximum number of NSEC3s to consider when validating a given denial of existence.
+ ''',
+ 'versionadded': ['5.0.2', '4.9.3', '4.8.6'],
+ },
+ {
+ 'name' : 'max_signature_validations_per_query',
+ 'section' : 'dnssec',
+ 'type' : LType.Uint64,
+ 'default' : '30',
+ 'help' : 'Maximum number of RRSIG signatures we are willing to validate per incoming query',
+ 'doc' : '''
+Maximum number of RRSIG signatures we are willing to validate per incoming query.
+ ''',
+ 'versionadded': ['5.0.2', '4.9.3', '4.8.6'],
+ },
+ {
+ 'name' : 'max_nsec3_hash_computations_per_query',
+ 'section' : 'dnssec',
+ 'type' : LType.Uint64,
+ 'default' : '600',
+ 'help' : 'Maximum number of NSEC3 hashes that we are willing to compute during DNSSEC validation, per incoming query',
+ 'doc' : '''
+Maximum number of NSEC3 hashes that we are willing to compute during DNSSEC validation, per incoming query.
+ ''',
+ 'versionadded': ['5.0.2', '4.9.3', '4.8.6'],
+ },
+ {
+ 'name' : 'aggressive_cache_max_nsec3_hash_cost',
+ 'section' : 'dnssec',
+ 'type' : LType.Uint64,
+ 'default' : '150',
+ 'help' : 'Maximum estimated NSEC3 cost for a given query to consider aggressive use of the NSEC3 cache',
+ 'doc' : '''
+Maximum estimated NSEC3 cost for a given query to consider aggressive use of the NSEC3 cache. The cost is estimated based on a heuristic taking the zone's NSEC3 salt and iterations parameters into account, as well at the number of labels of the requested name. For example a query for a name like a.b.c.d.e.f.example.com. in an example.com zone. secured with NSEC3 and 10 iterations (NSEC3 iterations count of 9) and an empty salt will have an estimated worst-case cost of 10 (iterations) * 6 (number of labels) = 60. The aggressive NSEC cache is an optimization to reduce the number of queries to authoritative servers, which is especially useful when a zone is under pseudo-random subdomain attack, and we want to skip it the zone parameters make it expensive.
+''',
+ 'versionadded': ['5.0.2', '4.9.3', '4.8.6'],
+ },
+ {
+ 'name' : 'max_ds_per_zone',
+ 'section' : 'dnssec',
+ 'type' : LType.Uint64,
+ 'default' : '8',
+ 'help' : 'Maximum number of DS records to consider per zone',
+ 'doc' : '''
+Maximum number of DS records to consider when validating records inside a zone..
+ ''',
+ 'versionadded': ['5.0.2', '4.9.3', '4.8.6'],
+ },
+ {
+ 'name' : 'max_dnskeys',
+ 'section' : 'dnssec',
+ 'type' : LType.Uint64,
+ 'default' : '2',
+ 'help' : 'Maximum number of DNSKEYs with the same algorithm and tag to consider when validating a given record',
+ 'doc' : '''
+Maximum number of DNSKEYs with the same algorithm and tag to consider when validating a given record. Setting this value to 1 effectively denies DNSKEY tag collisions in a zone.
+ ''',
+ 'versionadded': ['5.0.2', '4.9.3', '4.8.6'],
+ },
{
'name' : 'ttl',
'section' : 'packetcache',
'help' : 'If set, change group id to this gid for more security',
'doc' : '''
PowerDNS can change its user and group id after binding to its socket.
-Can be used for better :doc:`security <security>`.
+Can be used for better :doc:`security <security>`.
'''
},
{
unsigned int SyncRes::s_nonresolvingnsmaxfails;
unsigned int SyncRes::s_nonresolvingnsthrottletime;
unsigned int SyncRes::s_ecscachelimitttl;
+unsigned int SyncRes::s_maxvalidationsperq;
+unsigned int SyncRes::s_maxnsec3iterationsperq;
pdns::stat_t SyncRes::s_ecsqueries;
pdns::stat_t SyncRes::s_ecsresponses;
std::map<uint8_t, pdns::stat_t> SyncRes::s_ecsResponsesBySubnetSize4;
SyncRes::SyncRes(const struct timeval& now) :
d_authzonequeries(0), d_outqueries(0), d_tcpoutqueries(0), d_dotoutqueries(0), d_throttledqueries(0), d_timeouts(0), d_unreachables(0), d_totUsec(0), d_fixednow(now), d_now(now), d_cacheonly(false), d_doDNSSEC(false), d_doEDNS0(false), d_qNameMinimization(s_qnameminimization), d_lm(s_lm)
-
{
+ d_validationContext.d_nsec3IterationsRemainingQuota = s_maxnsec3iterationsperq > 0 ? s_maxnsec3iterationsperq : std::numeric_limits<decltype(d_validationContext.d_nsec3IterationsRemainingQuota)>::max();
}
static void allowAdditionalEntry(std::unordered_set<DNSName>& allowedAdditionals, const DNSRecord& rec);
/* let's check if we have a NSEC covering that record */
if (g_aggressiveNSECCache && !wasForwardedOrAuthZone) {
- if (g_aggressiveNSECCache->getDenial(d_now.tv_sec, qname, qtype, ret, res, d_cacheRemote, d_routingTag, d_doDNSSEC, LogObject(prefix))) {
+ if (g_aggressiveNSECCache->getDenial(d_now.tv_sec, qname, qtype, ret, res, d_cacheRemote, d_routingTag, d_doDNSSEC, d_validationContext, LogObject(prefix))) {
context.state = vState::Secure;
if (s_addExtendedResolutionDNSErrors) {
context.extendedError = EDNSExtendedError{static_cast<uint16_t>(EDNSExtendedError::code::Synthesized), "Result synthesized from aggressive NSEC cache (RFC8198)"};
- a delegation to a non-DNSSEC signed zone
- no delegation, we stay in the same zone
*/
- if (gotCNAME || denialProvesNoDelegation(zone, dsrecords)) {
+ if (gotCNAME || denialProvesNoDelegation(zone, dsrecords, d_validationContext)) {
/* we are still inside the same zone */
if (foundCut != nullptr) {
LOG(prefix << zone << ": Trying to validate " << std::to_string(tentativeKeys.size()) << " DNSKEYs with " << std::to_string(dsMap.size()) << " DS" << endl);
skeyset_t validatedKeys;
- auto state = validateDNSKeysAgainstDS(d_now.tv_sec, zone, dsMap, tentativeKeys, toSign, signatures, validatedKeys, LogObject(prefix));
+ auto state = validateDNSKeysAgainstDS(d_now.tv_sec, zone, dsMap, tentativeKeys, toSign, signatures, validatedKeys, LogObject(prefix), d_validationContext);
+
+ if (s_maxvalidationsperq != 0 && d_validationContext.d_validationsCounter > s_maxvalidationsperq) {
+ throw ImmediateServFailException("Server Failure while validating DNSKEYs, too many signature validations for this query");
+ }
LOG(prefix << zone << ": We now have " << std::to_string(validatedKeys.size()) << " DNSKEYs" << endl);
}
LOG(prefix << name << ": Going to validate " << recordcontents.size() << " record contents with " << signatures.size() << " sigs and " << keys.size() << " keys for " << name << "|" << type.toString() << endl);
- vState state = validateWithKeySet(d_now.tv_sec, name, recordcontents, signatures, keys, LogObject(prefix), false);
+ vState state = validateWithKeySet(d_now.tv_sec, name, recordcontents, signatures, keys, LogObject(prefix), d_validationContext, false);
+ if (s_maxvalidationsperq != 0 && d_validationContext.d_validationsCounter > s_maxvalidationsperq) {
+ throw ImmediateServFailException("Server Failure while validating records, too many signature validations for this query");
+ }
+
if (state == vState::Secure) {
LOG(prefix << name << ": Secure!" << endl);
return vState::Secure;
dState SyncRes::getDenialValidationState(const NegCache::NegCacheEntry& negEntry, const dState expectedState, bool referralToUnsigned, const string& prefix)
{
cspmap_t csp = harvestCSPFromNE(negEntry);
- return getDenial(csp, negEntry.d_name, negEntry.d_qtype.getCode(), referralToUnsigned, expectedState == dState::NXQTYPE, LogObject(prefix));
+ return getDenial(csp, negEntry.d_name, negEntry.d_qtype.getCode(), referralToUnsigned, expectedState == dState::NXQTYPE, d_validationContext, LogObject(prefix));
}
bool SyncRes::processRecords(const std::string& prefix, const DNSName& qname, const QType qtype, const DNSName& auth, LWResult& lwr, const bool sendRDQuery, vector<DNSRecord>& ret, set<DNSName>& nsset, DNSName& newtarget, DNSName& newauth, bool& realreferral, bool& negindic, vState& state, const bool needWildcardProof, const bool gatherWildcardProof, const unsigned int wildcardLabelsCount, int& rcode, bool& negIndicHasSignatures, unsigned int depth) // // NOLINT(readability-function-cognitive-complexity)
as described in section 5.3.4 of RFC 4035 and 5.3 of RFC 7129.
*/
cspmap_t csp = harvestCSPFromNE(negEntry);
- dState res = getDenial(csp, qname, negEntry.d_qtype.getCode(), false, false, LogObject(prefix), false, wildcardLabelsCount);
+ dState res = getDenial(csp, qname, negEntry.d_qtype.getCode(), false, false, d_validationContext, LogObject(prefix), false, wildcardLabelsCount);
if (res != dState::NXDOMAIN) {
vState tmpState = vState::BogusInvalidDenial;
if (res == dState::INSECURE || res == dState::OPTOUT) {
static unsigned int s_nonresolvingnsmaxfails;
static unsigned int s_nonresolvingnsthrottletime;
static unsigned int s_unthrottle_n;
-
static unsigned int s_ecscachelimitttl;
+ static unsigned int s_maxvalidationsperq;
+ static unsigned int s_maxnsec3iterationsperq;
static uint8_t s_ecsipv4limit;
static uint8_t s_ecsipv6limit;
static uint8_t s_ecsipv4cachelimit;
std::shared_ptr<std::vector<std::unique_ptr<RemoteLogger>>> d_outgoingProtobufServers;
std::shared_ptr<std::vector<std::unique_ptr<FrameStreamLogger>>> d_frameStreamServers;
boost::optional<const boost::uuids::uuid&> d_initialRequestId;
+ pdns::validation::ValidationContext d_validationContext;
asyncresolve_t d_asyncResolve{nullptr};
// d_now is initialized in the constructor and updates after outgoing requests in lwres.cc:asyncresolve
struct timeval d_now;
free(line); // NOLINT: it's the API.
}
+static bool getDenialWrapper(std::unique_ptr<AggressiveNSECCache>& cache, time_t now, const DNSName& name, const QType& qtype, const std::optional<int> expectedResult = std::nullopt, const std::optional<size_t> expectedRecordsCount = std::nullopt)
+{
+ int res;
+ std::vector<DNSRecord> results;
+ pdns::validation::ValidationContext validationContext;
+ validationContext.d_nsec3IterationsRemainingQuota = std::numeric_limits<decltype(validationContext.d_nsec3IterationsRemainingQuota)>::max();
+ bool found = cache->getDenial(now, name, qtype, results, res, ComboAddress("192.0.2.1"), boost::none, true, validationContext);
+ if (expectedResult) {
+ BOOST_CHECK_EQUAL(res, *expectedResult);
+ }
+ if (expectedRecordsCount) {
+ BOOST_CHECK_EQUAL(results.size(), *expectedRecordsCount);
+ }
+ return found;
+}
+
BOOST_AUTO_TEST_CASE(test_aggressive_nsec3_rollover)
{
/* test that we don't compare a hash using the wrong (former) salt or iterations count in case of a rollover,
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);
+ 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)
+{
+ AggressiveNSECCache::s_maxNSEC3CommonPrefix = 159;
+ g_recCache = std::make_unique<MemRecursorCache>();
+
+ const DNSName zone("powerdns.com");
+ time_t now = time(nullptr);
+
+ /* first we need a SOA */
+ std::vector<DNSRecord> records;
+ time_t ttd = now + 30;
+ DNSRecord drSOA;
+ drSOA.d_name = zone;
+ drSOA.d_type = QType::SOA;
+ drSOA.d_class = QClass::IN;
+ drSOA.setContent(std::make_shared<SOARecordContent>("pdns-public-ns1.powerdns.com. pieter\\.lexis.powerdns.com. 2017032301 10800 3600 604800 3600"));
+ drSOA.d_ttl = static_cast<uint32_t>(ttd); // XXX truncation
+ drSOA.d_place = DNSResourceRecord::ANSWER;
+ records.push_back(drSOA);
+
+ g_recCache->replace(now, zone, QType(QType::SOA), records, {}, {}, true, zone, boost::none, boost::none, vState::Secure);
+ BOOST_CHECK_EQUAL(g_recCache->size(), 1U);
+
+ auto insertNSEC3s = [zone, now](std::unique_ptr<AggressiveNSECCache>& cache, const std::string& salt, unsigned int iterationsCount) -> void {
+ {
+ /* insert a NSEC3 matching the apex (will be the closest encloser) */
+ DNSName name("powerdns.com");
+ std::string hashed = hashQNameWithSalt(salt, iterationsCount, name);
+ DNSRecord rec;
+ rec.d_name = DNSName(toBase32Hex(hashed)) + zone;
+ rec.d_type = QType::NSEC3;
+ rec.d_ttl = now + 10;
+
+ NSEC3RecordContent nrc;
+ nrc.d_algorithm = 1;
+ nrc.d_flags = 0;
+ nrc.d_iterations = iterationsCount;
+ nrc.d_salt = salt;
+ nrc.d_nexthash = hashed;
+ incrementHash(nrc.d_nexthash);
+ for (const auto& type : {QType::A}) {
+ nrc.set(type);
+ }
+
+ rec.setContent(std::make_shared<NSEC3RecordContent>(nrc));
+ auto rrsig = std::make_shared<RRSIGRecordContent>("NSEC3 5 3 10 20370101000000 20370101000000 24567 powerdns.com. data");
+ cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true);
+ }
+ {
+ /* insert a NSEC3 matching *.powerdns.com (wildcard) */
+ DNSName name("*.powerdns.com");
+ std::string hashed = hashQNameWithSalt(salt, iterationsCount, name);
+ auto before = hashed;
+ decrementHash(before);
+ DNSRecord rec;
+ rec.d_name = DNSName(toBase32Hex(before)) + zone;
+ rec.d_type = QType::NSEC3;
+ rec.d_ttl = now + 10;
+
+ NSEC3RecordContent nrc;
+ nrc.d_algorithm = 1;
+ nrc.d_flags = 0;
+ nrc.d_iterations = iterationsCount;
+ nrc.d_salt = salt;
+ nrc.d_nexthash = hashed;
+ incrementHash(nrc.d_nexthash);
+ for (const auto& type : {QType::A}) {
+ nrc.set(type);
+ }
+
+ rec.setContent(std::make_shared<NSEC3RecordContent>(nrc));
+ auto rrsig = std::make_shared<RRSIGRecordContent>("NSEC3 5 3 10 20370101000000 20370101000000 24567 powerdns.com. data");
+ cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true);
+ }
+ {
+ /* insert a NSEC3 matching sub.powerdns.com (next closer) */
+ DNSName name("sub.powerdns.com");
+ std::string hashed = hashQNameWithSalt(salt, iterationsCount, name);
+ auto before = hashed;
+ decrementHash(before);
+ DNSRecord rec;
+ rec.d_name = DNSName(toBase32Hex(before)) + zone;
+ rec.d_type = QType::NSEC3;
+ rec.d_ttl = now + 10;
+
+ NSEC3RecordContent nrc;
+ nrc.d_algorithm = 1;
+ nrc.d_flags = 0;
+ nrc.d_iterations = iterationsCount;
+ nrc.d_salt = salt;
+ nrc.d_nexthash = hashed;
+ incrementHash(nrc.d_nexthash);
+ for (const auto& type : {QType::A}) {
+ nrc.set(type);
+ }
+
+ rec.setContent(std::make_shared<NSEC3RecordContent>(nrc));
+ auto rrsig = std::make_shared<RRSIGRecordContent>("NSEC3 5 3 10 20370101000000 20370101000000 24567 powerdns.com. data");
+ cache->insertNSEC(zone, rec.d_name, rec, {rrsig}, true);
+ }
+ BOOST_CHECK_EQUAL(cache->getEntriesCount(), 3U);
+ };
+
+ {
+ /* zone with cheap parameters */
+ const std::string salt;
+ const unsigned int iterationsCount = 0;
+ AggressiveNSECCache::s_nsec3DenialProofMaxCost = 10;
+
+ auto cache = make_unique<AggressiveNSECCache>(10000);
+ insertNSEC3s(cache, salt, iterationsCount);
+
+ /* the cache should now be able to deny everything below sub.powerdns.com,
+ IF IT DOES NOT EXCEED THE COST */
+ {
+ /* short name: 10 labels below the zone apex */
+ DNSName lookupName("a.b.c.d.e.f.g.h.i.sub.powerdns.com.");
+ BOOST_CHECK_EQUAL(lookupName.countLabels() - zone.countLabels(), 10U);
+ BOOST_CHECK_LE(getNSEC3DenialProofWorstCaseIterationsCount(lookupName.countLabels() - zone.countLabels(), iterationsCount, salt.size()), AggressiveNSECCache::s_nsec3DenialProofMaxCost);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, lookupName, QType::AAAA, RCode::NXDomain, 7U), true);
+ }
+ {
+ /* longer name: 11 labels below the zone apex */
+ DNSName lookupName("a.b.c.d.e.f.g.h.i.j.sub.powerdns.com.");
+ BOOST_CHECK_EQUAL(lookupName.countLabels() - zone.countLabels(), 11U);
+ BOOST_CHECK_GT(getNSEC3DenialProofWorstCaseIterationsCount(lookupName.countLabels() - zone.countLabels(), iterationsCount, salt.size()), AggressiveNSECCache::s_nsec3DenialProofMaxCost);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, lookupName, QType::AAAA), false);
+ }
+ }
+
+ {
+ /* zone with expensive parameters */
+ const std::string salt("deadbeef");
+ const unsigned int iterationsCount = 50;
+ AggressiveNSECCache::s_nsec3DenialProofMaxCost = 100;
+
+ auto cache = make_unique<AggressiveNSECCache>(10000);
+ insertNSEC3s(cache, salt, iterationsCount);
+
+ /* the cache should now be able to deny everything below sub.powerdns.com,
+ IF IT DOES NOT EXCEED THE COST */
+ {
+ /* short name: 1 label below the zone apex */
+ DNSName lookupName("sub.powerdns.com.");
+ BOOST_CHECK_EQUAL(lookupName.countLabels() - zone.countLabels(), 1U);
+ BOOST_CHECK_LE(getNSEC3DenialProofWorstCaseIterationsCount(lookupName.countLabels() - zone.countLabels(), iterationsCount, salt.size()), AggressiveNSECCache::s_nsec3DenialProofMaxCost);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, lookupName, QType::AAAA, RCode::NXDomain, 7U), true);
+ }
+ {
+ /* longer name: 2 labels below the zone apex */
+ DNSName lookupName("a.sub.powerdns.com.");
+ BOOST_CHECK_EQUAL(lookupName.countLabels() - zone.countLabels(), 2U);
+ BOOST_CHECK_GT(getNSEC3DenialProofWorstCaseIterationsCount(lookupName.countLabels() - zone.countLabels(), iterationsCount, salt.size()), AggressiveNSECCache::s_nsec3DenialProofMaxCost);
+ BOOST_CHECK_EQUAL(getDenialWrapper(cache, now, lookupName, QType::AAAA), false);
+ }
}
}
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.setContent(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.setKey(std::move(dcke), 256);
std::vector<std::shared_ptr<const RRSIGRecordContent>> sigs;
sigs.push_back(std::make_shared<RRSIGRecordContent>(rrc));
- BOOST_CHECK(validateWithKeySet(now, qname, recordcontents, sigs, keyset, std::nullopt) == vState::Secure);
+ pdns::validation::ValidationContext validationContext;
+ BOOST_CHECK(validateWithKeySet(now, qname, recordcontents, sigs, keyset, std::nullopt, validationContext) == vState::Secure);
+ BOOST_CHECK_EQUAL(validationContext.d_validationsCounter, 1U);
}
BOOST_AUTO_TEST_CASE(test_dnssec_root_validation_csk)
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;
dcke->create(dcke->getBits());
DNSSECPrivateKey dpk;
dpk.setKey(std::move(dcke), 256);
- DSRecordContent uselessdrc = makeDSFromDNSKey(target, dpk.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
+ DSRecordContent seconddrc = makeDSFromDNSKey(target, dpk.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
dskeys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dskey, drc);
- keys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dpk, uselessdrc);
+ keys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dpk, seconddrc);
/* Set the root DS */
auto luaconfsCopy = g_luaconfs.getCopy();
BOOST_CHECK_EQUAL(queriesCount, 4U);
}
+BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_dss)
+{
+ std::unique_ptr<SyncRes> sr;
+ initSR(sr, true);
+
+ setDNSSECValidation(sr, DNSSECMode::Process);
+
+ primeHints();
+ const DNSName target(".");
+ testkeysset_t keys;
+
+ g_maxDSsToConsider = 1;
+
+ auto luaconfsCopy = g_luaconfs.getCopy();
+ luaconfsCopy.dsAnchors.clear();
+ /* generate more DSs for the zone than we are willing to consider: only the last one will be used to generate DNSKEY records */
+ for (size_t idx = 0; idx < (g_maxDSsToConsider + 10U); idx++) {
+ generateKeyMaterial(g_rootdnsname, DNSSECKeeper::RSASHA512, DNSSECKeeper::DIGEST_SHA384, keys, luaconfsCopy.dsAnchors);
+ }
+ g_luaconfs.setState(luaconfsCopy);
+
+ size_t queriesCount = 0;
+
+ sr->setAsyncCallback([target, &queriesCount, keys](const ComboAddress& /* ip */, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+ queriesCount++;
+
+ if (domain == target && type == QType::NS) {
+
+ setLWResult(res, 0, true, false, true);
+ char addr[] = "a.root-servers.net.";
+ for (char idx = 'a'; idx <= 'm'; idx++) {
+ addr[0] = idx;
+ addRecordToLW(res, domain, QType::NS, std::string(addr), DNSResourceRecord::ANSWER, 3600);
+ }
+
+ addRRSIG(keys, res->d_records, domain, 300);
+
+ addRecordToLW(res, "a.root-servers.net.", QType::A, "198.41.0.4", DNSResourceRecord::ADDITIONAL, 3600);
+ addRecordToLW(res, "a.root-servers.net.", QType::AAAA, "2001:503:ba3e::2:30", DNSResourceRecord::ADDITIONAL, 3600);
+
+ return LWResult::Result::Success;
+ }
+ else if (domain == target && type == QType::DNSKEY) {
+
+ setLWResult(res, 0, true, false, true);
+
+ addDNSKEY(keys, domain, 300, res->d_records);
+ addRRSIG(keys, res->d_records, domain, 300);
+
+ return LWResult::Result::Success;
+ }
+
+ return LWResult::Result::Timeout;
+ });
+
+ /* === with validation enabled === */
+ sr->setDNSSECValidationRequested(true);
+ vector<DNSRecord> ret;
+ int res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidDNSKEY);
+ /* 13 NS + 1 RRSIG */
+ BOOST_REQUIRE_EQUAL(ret.size(), 14U);
+ BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+ /* again, to test the cache */
+ ret.clear();
+ res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidDNSKEY);
+ BOOST_REQUIRE_EQUAL(ret.size(), 14U);
+ BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+ g_maxDNSKEYsToConsider = 0;
+}
+
+BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_dnskeys)
+{
+ std::unique_ptr<SyncRes> sr;
+ initSR(sr, true);
+
+ setDNSSECValidation(sr, DNSSECMode::Process);
+
+ primeHints();
+ const DNSName target(".");
+ testkeysset_t dskeys;
+ testkeysset_t keys;
+
+ DNSKEYRecordContent dnskeyRecordContent;
+ dnskeyRecordContent.d_algorithm = 13;
+ /* Generate key material for "." */
+ auto dckeDS = DNSCryptoKeyEngine::makeFromISCString(dnskeyRecordContent, R"PKEY(Private-key-format: v1.2
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: Ovj4pzrSh0U6aEVoKaPFhK1D4NMG0xrymj9+6TpwC8o=)PKEY");
+ DNSSECPrivateKey dskey;
+ dskey.setKey(std::move(dckeDS), 257);
+ assert(dskey.getTag() == 31337);
+ DSRecordContent drc = makeDSFromDNSKey(target, dskey.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
+ dskeys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dskey, drc);
+
+ /* Different key, same tag */
+ auto dcke = DNSCryptoKeyEngine::makeFromISCString(dnskeyRecordContent, R"PKEY(Private-key-format: v1.2
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: n7SRA4n6NejhZBWQOhjTaICYSpkTl6plJn1ATFG23FI=)PKEY");
+ DNSSECPrivateKey dpk;
+ dpk.setKey(std::move(dcke), 256);
+ assert(dpk.getTag() == dskey.getTag());
+ DSRecordContent uselessdrc = makeDSFromDNSKey(target, dpk.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
+ keys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dpk, uselessdrc);
+
+ /* Set the root DS (one of them!) */
+ auto luaconfsCopy = g_luaconfs.getCopy();
+ luaconfsCopy.dsAnchors.clear();
+ luaconfsCopy.dsAnchors[g_rootdnsname].insert(drc);
+ g_luaconfs.setState(luaconfsCopy);
+
+ size_t queriesCount = 0;
+
+ sr->setAsyncCallback([target, &queriesCount, keys, dskeys](const ComboAddress& /* ip */, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+ queriesCount++;
+
+ if (domain == target && type == QType::NS) {
+
+ setLWResult(res, 0, true, false, true);
+ char addr[] = "a.root-servers.net.";
+ for (char idx = 'a'; idx <= 'm'; idx++) {
+ addr[0] = idx;
+ addRecordToLW(res, domain, QType::NS, std::string(addr), DNSResourceRecord::ANSWER, 3600);
+ }
+
+ addRRSIG(dskeys, res->d_records, domain, 300);
+
+ addRecordToLW(res, "a.root-servers.net.", QType::A, "198.41.0.4", DNSResourceRecord::ADDITIONAL, 3600);
+ addRecordToLW(res, "a.root-servers.net.", QType::AAAA, "2001:503:ba3e::2:30", DNSResourceRecord::ADDITIONAL, 3600);
+
+ return LWResult::Result::Success;
+ }
+ else if (domain == target && type == QType::DNSKEY) {
+
+ setLWResult(res, 0, true, false, true);
+
+ addDNSKEY(keys, domain, 300, res->d_records);
+ addDNSKEY(dskeys, domain, 300, res->d_records);
+ addRRSIG(keys, res->d_records, domain, 300);
+ addRRSIG(dskeys, res->d_records, domain, 300);
+
+ return LWResult::Result::Success;
+ }
+
+ return LWResult::Result::Timeout;
+ });
+
+ g_maxDNSKEYsToConsider = 1;
+
+ /* === with validation enabled === */
+ sr->setDNSSECValidationRequested(true);
+ vector<DNSRecord> ret;
+ int res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidDNSKEY);
+ /* 13 NS + 1 RRSIG */
+ BOOST_REQUIRE_EQUAL(ret.size(), 14U);
+ BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+ /* again, to test the cache */
+ ret.clear();
+ res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidDNSKEY);
+ BOOST_REQUIRE_EQUAL(ret.size(), 14U);
+ BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+ g_maxDNSKEYsToConsider = 0;
+}
+
+BOOST_AUTO_TEST_CASE(test_dnssec_bogus_too_many_dnskeys_while_checking_signature)
+{
+ std::unique_ptr<SyncRes> sr;
+ initSR(sr, true);
+
+ setDNSSECValidation(sr, DNSSECMode::Process);
+
+ primeHints();
+ const DNSName target(".");
+ testkeysset_t dskeys;
+ testkeysset_t keys;
+ testkeysset_t otherkeys;
+
+ DNSKEYRecordContent dnskeyRecordContent;
+ dnskeyRecordContent.d_algorithm = 13;
+ /* Generate key material for "." */
+ auto dckeDS = DNSCryptoKeyEngine::makeFromISCString(dnskeyRecordContent, R"PKEY(Private-key-format: v1.2
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: Ovj4pzrSh0U6aEVoKaPFhK1D4NMG0xrymj9+6TpwC8o=)PKEY");
+ DNSSECPrivateKey dskey;
+ dskey.setKey(std::move(dckeDS), 257);
+ assert(dskey.getTag() == 31337);
+ DSRecordContent drc = makeDSFromDNSKey(target, dskey.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
+ dskeys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dskey, drc);
+
+ /* Different key, same tag */
+ auto dcke = DNSCryptoKeyEngine::makeFromISCString(dnskeyRecordContent, R"PKEY(Private-key-format: v1.2
+Algorithm: 13 (ECDSAP256SHA256)
+PrivateKey: pTaMJcvNrPIIiQiHGvCLZZASroyQpUwew5FvCgjHNsk=)PKEY");
+ DNSSECPrivateKey dpk;
+ // why 258, you may ask? We need this record to be sorted AFTER the other one in a sortedRecords_t
+ // so that the validation of the DNSKEY rrset succeeds
+ dpk.setKey(std::move(dcke), 258);
+ assert(dpk.getTag() == dskey.getTag());
+ DSRecordContent uselessdrc = makeDSFromDNSKey(target, dpk.getDNSKEY(), DNSSECKeeper::DIGEST_SHA256);
+ keys[target] = std::pair<DNSSECPrivateKey, DSRecordContent>(dpk, uselessdrc);
+
+ /* Set the root DSs (only one of them) */
+ auto luaconfsCopy = g_luaconfs.getCopy();
+ luaconfsCopy.dsAnchors.clear();
+ luaconfsCopy.dsAnchors[g_rootdnsname].insert(drc);
+ g_luaconfs.setState(luaconfsCopy);
+
+ size_t queriesCount = 0;
+
+ sr->setAsyncCallback([target, &queriesCount, keys, dskeys](const ComboAddress& /* ip */, const DNSName& domain, int type, bool /* doTCP */, bool /* sendRDQuery */, int /* EDNS0Level */, struct timeval* /* now */, boost::optional<Netmask>& /* srcmask */, boost::optional<const ResolveContext&> /* context */, LWResult* res, bool* /* chained */) {
+ queriesCount++;
+
+ if (domain == target && type == QType::NS) {
+
+ setLWResult(res, 0, true, false, true);
+ char addr[] = "a.root-servers.net.";
+ for (char idx = 'a'; idx <= 'm'; idx++) {
+ addr[0] = idx;
+ addRecordToLW(res, domain, QType::NS, std::string(addr), DNSResourceRecord::ANSWER, 3600);
+ }
+
+ addRRSIG(keys, res->d_records, domain, 300);
+ addRRSIG(dskeys, res->d_records, domain, 300);
+
+ addRecordToLW(res, "a.root-servers.net.", QType::A, "198.41.0.4", DNSResourceRecord::ADDITIONAL, 3600);
+ addRecordToLW(res, "a.root-servers.net.", QType::AAAA, "2001:503:ba3e::2:30", DNSResourceRecord::ADDITIONAL, 3600);
+
+ return LWResult::Result::Success;
+ }
+ else if (domain == target && type == QType::DNSKEY) {
+
+ setLWResult(res, 0, true, false, true);
+
+ addDNSKEY(dskeys, domain, 300, res->d_records);
+ addDNSKEY(keys, domain, 300, res->d_records);
+ addRRSIG(dskeys, res->d_records, domain, 300);
+
+ return LWResult::Result::Success;
+ }
+
+ return LWResult::Result::Timeout;
+ });
+
+ g_maxDNSKEYsToConsider = 1;
+
+ /* === with validation enabled === */
+ sr->setDNSSECValidationRequested(true);
+ vector<DNSRecord> ret;
+ int res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidRRSIG);
+ /* 13 NS + 1 RRSIG */
+ BOOST_REQUIRE_EQUAL(ret.size(), 15U);
+ BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+ /* again, to test the cache */
+ ret.clear();
+ res = sr->beginResolve(target, QType(QType::NS), QClass::IN, ret);
+ BOOST_CHECK_EQUAL(res, RCode::NoError);
+ BOOST_CHECK_EQUAL(sr->getValidationState(), vState::BogusNoValidRRSIG);
+ BOOST_REQUIRE_EQUAL(ret.size(), 15U);
+ BOOST_CHECK_EQUAL(queriesCount, 2U);
+
+ g_maxDNSKEYsToConsider = 0;
+}
+
BOOST_AUTO_TEST_CASE(test_dnssec_bogus_rrsig_signed_with_unknown_dnskey)
{
std::unique_ptr<SyncRes> sr;
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, const OptLog& log = std::nullopt, bool needWildcardProof = true, unsigned int wildcardLabelsCount = 0)
+{
+ pdns::validation::ValidationContext context;
+ context.d_nsec3IterationsRemainingQuota = std::numeric_limits<decltype(context.d_nsec3IterationsRemainingQuota)>::max();
+ return getDenial(validrrsets, qname, qtype, referralToUnsigned, wantsNoDataProof, context, log, needWildcardProof, wildcardLabelsCount);
+}
+
BOOST_AUTO_TEST_CASE(test_nsec_denial_nowrap)
{
initSR();
sortedRecords_t recordContents;
vector<shared_ptr<const RRSIGRecordContent>> signatureContents;
-
- addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::A}, 600, records);
+ const unsigned int nbIterations = 10;
+ addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", {QType::A}, 600, records, nbIterations);
recordContents.insert(records.at(0).getContent());
addRRSIG(keys, records, DNSName("powerdns.com."), 300);
signatureContents.push_back(getRR<RRSIGRecordContent>(records.at(1)));
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)
time_t g_signatureInceptionSkew{0};
uint16_t g_maxNSEC3Iterations{0};
+uint16_t g_maxRRSIGsPerRecordToConsider{0};
+uint16_t g_maxNSEC3sPerRecordToConsider{0};
+uint16_t g_maxDNSKEYsToConsider{0};
+uint16_t g_maxDSsToConsider{0};
static bool isAZoneKey(const DNSKEYRecordContent& key)
{
return begin.canonCompare(name) && next != name && next.isPartOf(name);
}
-using nsec3HashesCache = std::map<std::tuple<DNSName, std::string, uint16_t>, std::string>;
-
-static std::string getHashFromNSEC3(const DNSName& qname, const NSEC3RecordContent& nsec3, nsec3HashesCache& cache)
+[[nodiscard]] std::string getHashFromNSEC3(const DNSName& qname, uint16_t iterations, const std::string& salt, pdns::validation::ValidationContext& context)
{
std::string result;
- if (g_maxNSEC3Iterations != 0 && nsec3.d_iterations > g_maxNSEC3Iterations) {
+ if (g_maxNSEC3Iterations != 0 && iterations > g_maxNSEC3Iterations) {
return result;
}
- auto key = std::tuple(qname, nsec3.d_salt, nsec3.d_iterations);
- auto iter = cache.find(key);
- if (iter != cache.end())
- {
+ auto key = std::tuple(qname, salt, iterations);
+ auto iter = context.d_nsec3Cache.find(key);
+ if (iter != context.d_nsec3Cache.end()) {
return iter->second;
}
- result = hashQNameWithSalt(nsec3.d_salt, nsec3.d_iterations, qname);
- cache[key] = result;
+ if (context.d_nsec3IterationsRemainingQuota < iterations) {
+ // we throw here because we cannot take the risk that the result
+ // be cached, since a different query can try to validate the
+ // same result with a bigger NSEC3 iterations quota
+ throw pdns::validation::TooManySEC3IterationsException();
+ }
+
+ result = hashQNameWithSalt(salt, iterations, qname);
+ context.d_nsec3IterationsRemainingQuota -= iterations;
+ context.d_nsec3Cache[key] = result;
return result;
}
+[[nodiscard]] static std::string getHashFromNSEC3(const DNSName& qname, const NSEC3RecordContent& nsec3, pdns::validation::ValidationContext& context)
+{
+ return getHashFromNSEC3(qname, nsec3.d_iterations, nsec3.d_salt, context);
+}
+
/* There is no delegation at this exact point if:
- the name exists but the NS type is not set
- the name does not exist
One exception, if the name is covered by an opt-out NSEC3
it doesn't prove that an insecure delegation doesn't exist.
*/
-bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>& dsrecords)
+bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>& dsrecords, pdns::validation::ValidationContext& context)
{
- nsec3HashesCache cache;
+ uint16_t nsec3sConsidered = 0;
for (const auto& record : dsrecords) {
if (record.d_type == QType::NSEC) {
continue;
}
- const string hash = getHashFromNSEC3(zone, *nsec3, cache);
+ if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+ return false;
+ }
+ nsec3sConsidered++;
+
+ const string hash = getHashFromNSEC3(zone, *nsec3, context);
if (hash.empty()) {
return false;
}
If `wildcardExists` is not NULL, if will be set to true if a wildcard exists
for this qname but doesn't have this qtype.
*/
-static bool provesNSEC3NoWildCard(const DNSName& closestEncloser, uint16_t const qtype, const cspmap_t& validrrsets, bool* wildcardExists, nsec3HashesCache& cache, const OptLog& log)
+static bool provesNSEC3NoWildCard(const DNSName& closestEncloser, uint16_t const qtype, const cspmap_t& validrrsets, bool* wildcardExists, const OptLog& log, pdns::validation::ValidationContext& context)
{
auto wildcard = g_wildcarddnsname + closestEncloser;
VLOG(log, closestEncloser << ": Trying to prove that there is no wildcard for "<<wildcard<<"/"<<QType(qtype)<<endl);
continue;
}
- string hash = getHashFromNSEC3(wildcard, *nsec3, cache);
+ string hash = getHashFromNSEC3(wildcard, *nsec3, context);
if (hash.empty()) {
+ VLOG(log, closestEncloser << ": Unsupported hash, ignoring"<<endl);
return false;
}
VLOG(log, closestEncloser << ":\tWildcard hash: "<<toBase32Hex(hash)<<endl);
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, const OptLog& log, bool needWildcardProof, unsigned int wildcardLabelsCount) // NOLINT(readability-function-cognitive-complexity): https://github.com/PowerDNS/pdns/issues/12791
+dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, pdns::validation::ValidationContext& context, const OptLog& log, bool needWildcardProof, unsigned int wildcardLabelsCount) // NOLINT(readability-function-cognitive-complexity): https://github.com/PowerDNS/pdns/issues/12791
{
- nsec3HashesCache cache;
bool nsec3Seen = false;
if (!needWildcardProof && wildcardLabelsCount == 0) {
throw PDNSException("Invalid wildcard labels count for the validation of a positive answer synthesized from a wildcard");
}
+ uint8_t numberOfLabelsOfParentZone{std::numeric_limits<uint8_t>::max()};
+ uint16_t nsec3sConsidered = 0;
for (const auto& validset : validrrsets) {
VLOG(log, qname << ": Do have: "<<validset.first.first<<"/"<<DNSRecordContent::NumberToType(validset.first.second)<<endl);
VLOG(log, qname << ": Owner "<<hashedOwner<<" is not part of the signer "<<signer<<", ignoring"<<endl);
continue;
}
+ numberOfLabelsOfParentZone = std::min(numberOfLabelsOfParentZone, static_cast<uint8_t>(signer.countLabels()));
if (qtype == QType::DS && !qname.isRoot() && signer == qname) {
VLOG(log, qname << ": A NSEC3 RR from the child zone cannot deny the existence of a DS"<<endl);
continue;
}
- string hash = getHashFromNSEC3(qname, *nsec3, cache);
+ if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+ VLOG(log, qname << ": Too many NSEC3s for this record"<<endl);
+ return dState::NODENIAL;
+ }
+ nsec3sConsidered++;
+
+ string hash = getHashFromNSEC3(qname, *nsec3, context);
if (hash.empty()) {
VLOG(log, qname << ": Unsupported hash, ignoring"<<endl);
return dState::INSECURE;
DNSName closestEncloser(qname);
bool found = false;
-
if (needWildcardProof) {
+ nsec3sConsidered = 0;
/* We now need to look for a NSEC3 covering the closest (provable) encloser
RFC 5155 section-7.2.1
RFC 7129 section-5.5
*/
VLOG(log, qname << ": Now looking for the closest encloser for "<<qname<<endl);
- while (!found && closestEncloser.chopOff()) {
+ while (!found && closestEncloser.chopOff() && closestEncloser.countLabels() >= numberOfLabelsOfParentZone) {
for(const auto& validset : validrrsets) {
if(validset.first.second==QType::NSEC3) {
continue;
}
- string hash = getHashFromNSEC3(closestEncloser, *nsec3, cache);
+ if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+ VLOG(log, qname << ": Too many NSEC3s for this record"<<endl);
+ return dState::NODENIAL;
+ }
+ nsec3sConsidered++;
+
+ string hash = getHashFromNSEC3(closestEncloser, *nsec3, context);
if (hash.empty()) {
+ VLOG(log, qname << ": Unsupported hash, ignoring"<<endl);
return dState::INSECURE;
}
if (labelIdx >= 1) {
DNSName nextCloser(closestEncloser);
nextCloser.prependRawLabel(qname.getRawLabel(labelIdx - 1));
+ nsec3sConsidered = 0;
VLOG(log, qname << ":Looking for a NSEC3 covering the next closer name "<<nextCloser<<endl);
- for(const auto& validset : validrrsets) {
- if(validset.first.second==QType::NSEC3) {
- for(const auto& record : validset.second.records) {
+ for (const auto& validset : validrrsets) {
+ if (validset.first.second == QType::NSEC3) {
+ for (const auto& record : validset.second.records) {
VLOG(log, qname << ":\t"<<record->getZoneRepresentation()<<endl);
auto nsec3 = std::dynamic_pointer_cast<const NSEC3RecordContent>(record);
if (!nsec3) {
continue;
}
- string hash = getHashFromNSEC3(nextCloser, *nsec3, cache);
+ if (g_maxNSEC3sPerRecordToConsider > 0 && nsec3sConsidered >= g_maxNSEC3sPerRecordToConsider) {
+ VLOG(log, qname << ": Too many NSEC3s for this record"<<endl);
+ return dState::NODENIAL;
+ }
+ nsec3sConsidered++;
+
+ string hash = getHashFromNSEC3(nextCloser, *nsec3, context);
if (hash.empty()) {
+ VLOG(log, qname << ": Unsupported hash, ignoring"<<endl);
return dState::INSECURE;
}
if (nextCloserFound) {
bool wildcardExists = false;
/* RFC 7129 section-5.6 */
- if (needWildcardProof && !provesNSEC3NoWildCard(closestEncloser, qtype, validrrsets, &wildcardExists, cache, log)) {
+ if (needWildcardProof && !provesNSEC3NoWildCard(closestEncloser, qtype, validrrsets, &wildcardExists, log, context)) {
if (!isOptOut) {
VLOG(log, qname << ": But the existence of a wildcard is not denied for "<<qname<<"/"<<QType(qtype)<<endl);
return dState::NODENIAL;
return sig.d_siginception - g_signatureInceptionSkew <= now;
}
-static bool checkSignatureWithKey(const DNSName& qname, time_t now, const RRSIGRecordContent& sig, const DNSKEYRecordContent& key, const std::string& msg, vState& ede, const OptLog& log)
+namespace {
+[[nodiscard]] bool checkSignatureInceptionAndExpiry(const DNSName& qname, time_t now, const RRSIGRecordContent& sig, vState& ede, const OptLog& log)
+{
+ /* rfc4035:
+ - The validator's notion of the current time MUST be less than or equal to the time listed in the RRSIG RR's Expiration field.
+ - The validator's notion of the current time MUST be greater than or equal to the time listed in the RRSIG RR's Inception field.
+ */
+ if (isRRSIGIncepted(now, sig) && isRRSIGNotExpired(now, sig)) {
+ return true;
+ }
+ ede = ((sig.d_siginception - g_signatureInceptionSkew) > now) ? vState::BogusSignatureNotYetValid : vState::BogusSignatureExpired;
+ VLOG(log, qname << ": Signature is "<<(ede == vState::BogusSignatureNotYetValid ? "not yet valid" : "expired")<<" (inception: "<<sig.d_siginception<<", inception skew: "<<g_signatureInceptionSkew<<", expiration: "<<sig.d_sigexpire<<", now: "<<now<<")"<<endl);
+ return false;
+}
+
+[[nodiscard]] bool checkSignatureWithKey(const DNSName& qname, const RRSIGRecordContent& sig, const DNSKEYRecordContent& key, const std::string& msg, vState& ede, const OptLog& log)
{
bool result = false;
try {
- /* rfc4035:
- - The validator's notion of the current time MUST be less than or equal to the time listed in the RRSIG RR's Expiration field.
- - The validator's notion of the current time MUST be greater than or equal to the time listed in the RRSIG RR's Inception field.
- */
- if (isRRSIGIncepted(now, sig) && isRRSIGNotExpired(now, sig)) {
- auto dke = DNSCryptoKeyEngine::makeFromPublicKeyString(key.d_algorithm, key.d_key);
- result = dke->verify(msg, sig.d_signature);
- VLOG(log, qname << ": Signature by key with tag "<<sig.d_tag<<" and algorithm "<<DNSSECKeeper::algorithm2name(sig.d_algorithm)<<" was " << (result ? "" : "NOT ")<<"valid"<<endl);
- if (!result) {
- ede = vState::BogusNoValidRRSIG;
- }
+ auto dke = DNSCryptoKeyEngine::makeFromPublicKeyString(key.d_algorithm, key.d_key);
+ result = dke->verify(msg, sig.d_signature);
+ VLOG(log, qname << ": Signature by key with tag "<<sig.d_tag<<" and algorithm "<<DNSSECKeeper::algorithm2name(sig.d_algorithm)<<" was " << (result ? "" : "NOT ")<<"valid"<<endl);
+ if (!result) {
+ ede = vState::BogusNoValidRRSIG;
}
- else {
- ede = ((sig.d_siginception - g_signatureInceptionSkew) > now) ? vState::BogusSignatureNotYetValid : vState::BogusSignatureExpired;
- VLOG(log, qname << ": Signature is "<<(ede == vState::BogusSignatureNotYetValid ? "not yet valid" : "expired")<<" (inception: "<<sig.d_siginception<<", inception skew: "<<g_signatureInceptionSkew<<", expiration: "<<sig.d_sigexpire<<", now: "<<now<<")"<<endl);
- }
}
catch (const std::exception& e) {
VLOG(log, qname << ": Could not make a validator for signature: "<<e.what()<<endl);
return result;
}
-vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& toSign, const vector<shared_ptr<const RRSIGRecordContent> >& signatures, const skeyset_t& keys, const OptLog& log, bool validateAllSigs)
+}
+
+vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& toSign, const vector<shared_ptr<const RRSIGRecordContent> >& signatures, const skeyset_t& keys, const OptLog& log, pdns::validation::ValidationContext& context, bool validateAllSigs)
{
- bool foundKey = false;
+ bool missingKey = false;
bool isValid = false;
bool allExpired = true;
bool noneIncepted = true;
+ uint16_t signaturesConsidered = 0;
- for(const auto& signature : signatures) {
+ for (const auto& signature : signatures) {
unsigned int labelCount = name.countLabels();
if (signature->d_labels > labelCount) {
VLOG(log, name<<": Discarding invalid RRSIG whose label count is "<<signature->d_labels<<" while the RRset owner name has only "<<labelCount<<endl);
continue;
}
+ vState ede = vState::Indeterminate;
+ if (!checkSignatureInceptionAndExpiry(name, now, *signature, ede, log)) {
+ if (isRRSIGIncepted(now, *signature)) {
+ noneIncepted = false;
+ }
+ if (isRRSIGNotExpired(now, *signature)) {
+ allExpired = false;
+ }
+ continue;
+ }
+
+ if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) {
+ VLOG(log, name<<": We have already considered "<<std::to_string(signaturesConsidered)<<" RRSIG"<<addS(signaturesConsidered)<<" for this record, stopping now"<<endl;);
+ // possibly going Bogus, the RRSIGs have not been validated so Insecure would be wrong
+ break;
+ }
+ signaturesConsidered++;
+ context.d_validationsCounter++;
+
auto keysMatchingTag = getByTag(keys, signature->d_tag, signature->d_algorithm, log);
if (keysMatchingTag.empty()) {
- VLOG(log, name<<": No key provided for "<<signature->d_tag<<" and algorithm "<<std::to_string(signature->d_algorithm)<<endl;);
+ VLOG(log, name << ": No key provided for "<<signature->d_tag<<" and algorithm "<<std::to_string(signature->d_algorithm)<<endl;);
+ missingKey = true;
continue;
}
string msg = getMessageForRRSET(name, *signature, toSign, true);
+ uint16_t dnskeysConsidered = 0;
for (const auto& key : keysMatchingTag) {
- vState ede = vState::Indeterminate;
- bool signIsValid = checkSignatureWithKey(name, now, *signature, *key, msg, ede, log);
- foundKey = true;
+ if (g_maxDNSKEYsToConsider > 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) {
+ VLOG(log, name << ": We have already considered "<<std::to_string(dnskeysConsidered)<<" DNSKEY"<<addS(dnskeysConsidered)<<" for tag "<<std::to_string(signature->d_tag)<<" and algorithm "<<std::to_string(signature->d_algorithm)<<", not considering the remaining ones for this signature"<<endl;);
+ return isValid ? vState::Secure : vState::BogusNoValidRRSIG;
+ }
+ dnskeysConsidered++;
+
+ bool signIsValid = checkSignatureWithKey(name, *signature, *key, msg, ede, log);
if (signIsValid) {
isValid = true;
if (isValid) {
return vState::Secure;
}
- if (!foundKey) {
+ if (missingKey) {
return vState::BogusNoValidRRSIG;
}
if (noneIncepted) {
return true;
}
-vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector<shared_ptr<const RRSIGRecordContent> >& sigs, skeyset_t& validkeys, const OptLog& log)
+vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector<shared_ptr<const RRSIGRecordContent> >& sigs, skeyset_t& validkeys, const OptLog& log, pdns::validation::ValidationContext& context)
{
/*
* Check all DNSKEY records against all DS records and place all DNSKEY records
* that have DS records (that we support the algo for) in the tentative key storage
*/
- for (const auto& dsrc : dsmap)
- {
+ uint16_t dssConsidered = 0;
+ for (const auto& dsrc : dsmap) {
+ if (g_maxDSsToConsider > 0 && dssConsidered > g_maxDSsToConsider) {
+ VLOG(log, zone << ": We have already considered "<<std::to_string(dssConsidered)<<" DS"<<addS(dssConsidered)<<", not considering the remaining ones"<<endl;);
+ return vState::BogusNoValidDNSKEY;
+ }
+ ++dssConsidered;
+
+ uint16_t dnskeysConsidered = 0;
auto record = getByTag(tkeys, dsrc.d_tag, dsrc.d_algorithm, log);
// cerr<<"looking at DS with tag "<<dsrc.d_tag<<", algo "<<DNSSECKeeper::algorithm2name(dsrc.d_algorithm)<<", digest "<<std::to_string(dsrc.d_digesttype)<<" for "<<zone<<", got "<<r.size()<<" DNSKEYs for tag"<<endl;
- for (const auto& drc : record)
- {
+ for (const auto& drc : record) {
bool isValid = false;
bool dsCreated = false;
DSRecordContent dsrc2;
+
+ if (g_maxDNSKEYsToConsider > 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) {
+ VLOG(log, zone << ": We have already considered "<<std::to_string(dnskeysConsidered)<<" DNSKEY"<<addS(dnskeysConsidered)<<" for tag "<<std::to_string(dsrc.d_tag)<<" and algorithm "<<std::to_string(dsrc.d_algorithm)<<", not considering the remaining ones for this DS"<<endl;);
+ // we need to break because we can have a partially validated set
+ // where the KSK signs the ZSK(s), and even if we don't
+ // we are going to try to get the correct EDE status (revoked, expired, ...)
+ break;
+ }
+ dnskeysConsidered++;
+
try {
dsrc2 = makeDSFromDNSKey(zone, *drc, dsrc.d_digesttype);
dsCreated = true;
// cerr<<"got "<<validkeys.size()<<"/"<<tkeys.size()<<" valid/tentative keys"<<endl;
// these counts could be off if we somehow ended up with
// duplicate keys. Should switch to a type that prevents that.
- if (validkeys.size() < tkeys.size())
- {
+ if (!tkeys.empty() && validkeys.size() < tkeys.size()) {
// this should mean that we have one or more DS-validated DNSKEYs
// but not a fully validated DNSKEY set, yet
// one of these valid DNSKEYs should be able to validate the
// whole set
- for (const auto& sig : sigs)
- {
+ uint16_t signaturesConsidered = 0;
+ for (const auto& sig : sigs) {
+ if (!checkSignatureInceptionAndExpiry(zone, now, *sig, ede, log)) {
+ continue;
+ }
+
// cerr<<"got sig for keytag "<<i->d_tag<<" matching "<<getByTag(tkeys, i->d_tag).size()<<" keys of which "<<getByTag(validkeys, i->d_tag).size()<<" valid"<<endl;
auto bytag = getByTag(validkeys, sig->d_tag, sig->d_algorithm, log);
continue;
}
+ if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) {
+ VLOG(log, zone << ": We have already considered "<<std::to_string(signaturesConsidered)<<" RRSIG"<<addS(signaturesConsidered)<<" for this record, stopping now"<<endl;);
+ // possibly going Bogus, the RRSIGs have not been validated so Insecure would be wrong
+ return vState::BogusNoValidDNSKEY;
+ }
+
string msg = getMessageForRRSET(zone, *sig, toSign);
+ uint16_t dnskeysConsidered = 0;
for (const auto& key : bytag) {
+ if (g_maxDNSKEYsToConsider > 0 && dnskeysConsidered >= g_maxDNSKEYsToConsider) {
+ VLOG(log, zone << ": We have already considered "<<std::to_string(dnskeysConsidered)<<" DNSKEY"<<addS(dnskeysConsidered)<<" for tag "<<std::to_string(sig->d_tag)<<" and algorithm "<<std::to_string(sig->d_algorithm)<<", not considering the remaining ones for this signature"<<endl;);
+ return vState::BogusNoValidDNSKEY;
+ }
+ dnskeysConsidered++;
+
+ if (g_maxRRSIGsPerRecordToConsider > 0 && signaturesConsidered >= g_maxRRSIGsPerRecordToConsider) {
+ VLOG(log, zone << ": We have already considered "<<std::to_string(signaturesConsidered)<<" RRSIG"<<addS(signaturesConsidered)<<" for this record, stopping now"<<endl;);
+ // possibly going Bogus, the RRSIGs have not been validated so Insecure would be wrong
+ return vState::BogusNoValidDNSKEY;
+ }
// cerr<<"validating : ";
- bool signIsValid = checkSignatureWithKey(zone, now, *sig, *key, msg, ede, log);
+ bool signIsValid = checkSignatureWithKey(zone, *sig, *key, msg, ede, log);
+ signaturesConsidered++;
+ context.d_validationsCounter++;
if (signIsValid) {
VLOG(log, zone << ": Validation succeeded - whole DNSKEY set is valid"<<endl);
}
VLOG(log, zone << ": Validation did not succeed!"<<endl);
}
+
+ if (validkeys.size() == tkeys.size()) {
+ // we validated the whole DNSKEY set already */
+ break;
+ }
// if(validkeys.empty()) cerr<<"did not manage to validate DNSKEY set based on DS-validated KSK, only passing KSK on"<<endl;
}
}
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 };
using skeyset_t = set<shared_ptr<const DNSKEYRecordContent>, sharedDNSKeyRecordContentCompare>;
+namespace pdns::validation
+{
+using Nsec3HashesCache = std::map<std::tuple<DNSName, std::string, uint16_t>, std::string>;
+
+struct ValidationContext
+{
+ Nsec3HashesCache d_nsec3Cache;
+ unsigned int d_validationsCounter{0};
+ unsigned int d_nsec3IterationsRemainingQuota{0};
+};
+
+class TooManySEC3IterationsException : public std::runtime_error
+{
+public:
+ TooManySEC3IterationsException(): std::runtime_error("Too many NSEC3 hash computations per query")
+ {
+ }
+};
+
+}
-vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& toSign, const vector<shared_ptr<const RRSIGRecordContent> >& signatures, const skeyset_t& keys, const OptLog& log, bool validateAllSigs=true);
+vState validateWithKeySet(time_t now, const DNSName& name, const sortedRecords_t& toSign, const vector<shared_ptr<const RRSIGRecordContent> >& signatures, const skeyset_t& keys, const OptLog& log, pdns::validation::ValidationContext& context, bool validateAllSigs=true);
bool isCoveredByNSEC(const DNSName& name, const DNSName& begin, const DNSName& next);
bool isCoveredByNSEC3Hash(const std::string& hash, const std::string& beginHash, const std::string& nextHash);
bool isCoveredByNSEC3Hash(const DNSName& name, const DNSName& beginHash, const DNSName& nextHash);
cspmap_t harvestCSPFromRecs(const vector<DNSRecord>& recs);
bool getTrustAnchor(const map<DNSName,dsmap_t>& anchors, const DNSName& zone, dsmap_t &res);
bool haveNegativeTrustAnchor(const map<DNSName,std::string>& negAnchors, const DNSName& zone, std::string& reason);
-vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector<shared_ptr<const RRSIGRecordContent> >& sigs, skeyset_t& validkeys, const OptLog&);
-dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, const OptLog& log = std::nullopt, bool needWildcardProof=true, unsigned int wildcardLabelsCount=0);
+vState validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, const sortedRecords_t& toSign, const vector<shared_ptr<const RRSIGRecordContent> >& sigs, skeyset_t& validkeys, const OptLog&, pdns::validation::ValidationContext& context);
+dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, pdns::validation::ValidationContext& context, const OptLog& log = std::nullopt, bool needWildcardProof=true, unsigned int wildcardLabelsCount=0);
bool isSupportedDS(const DSRecordContent& dsRecordContent, const OptLog&);
DNSName getSigner(const std::vector<std::shared_ptr<const RRSIGRecordContent> >& signatures);
-bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>& dsrecords);
+bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>& dsrecords, pdns::validation::ValidationContext& context);
bool isRRSIGNotExpired(time_t now, const RRSIGRecordContent& sig);
bool isRRSIGIncepted(time_t now, const RRSIGRecordContent& sig);
bool isWildcardExpanded(unsigned int labelCount, const RRSIGRecordContent& sign);
bool isNSEC3AncestorDelegation(const DNSName& signer, const DNSName& owner, const NSEC3RecordContent& nsec3);
DNSName getNSECOwnerName(const DNSName& initialOwner, const std::vector<std::shared_ptr<const RRSIGRecordContent> >& signatures);
DNSName getClosestEncloserFromNSEC(const DNSName& name, const DNSName& owner, const DNSName& next);
+[[nodiscard]] uint64_t getNSEC3DenialProofWorstCaseIterationsCount(uint8_t maxLabels, uint16_t iterations, size_t saltLength);
+[[nodiscard]] std::string getHashFromNSEC3(const DNSName& qname, uint16_t iterations, const std::string& salt, pdns::validation::ValidationContext& context);
template <typename NSEC> bool isTypeDenied(const NSEC& nsec, const QType& type)
{
_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