From: Otto Moerbeek Date: Tue, 18 Jan 2022 11:36:42 +0000 (+0100) Subject: Add Lua config and do DNSSEC validation of ZONEMD record X-Git-Tag: auth-4.7.0-alpha1~42^2~15 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e5163239bd6fb387771a48fab8e24eb8977c95bf;p=thirdparty%2Fpdns.git Add Lua config and do DNSSEC validation of ZONEMD record Missing: - TA/NTA/DS processing (we assume it's in the record cache for the moment) - Valdition of of absense of ZONEMD records by proof of non-existence - Details of processing of DNSSEC validation result (depending on config) Solving the first part likely involes running the zone-to-cache tasks in a recursorThread context. --- diff --git a/pdns/rec-lua-conf.cc b/pdns/rec-lua-conf.cc index 713740e725..59af03d20c 100644 --- a/pdns/rec-lua-conf.cc +++ b/pdns/rec-lua-conf.cc @@ -452,7 +452,7 @@ void loadRecursorLuaConfig(const std::string& fname, luaConfigDelayedThreads& de { "logonly", pdns::ZoneMD::Config::LogOnly}, { "required", pdns::ZoneMD::Config::Required}, { "requiredWithDNSSEC", pdns::ZoneMD::Config::RequiredWithDNSSEC}, - { "requiredIgnoreDNSSEC", pdns::ZoneMD::Config::RequiredIgnoreDNSSEC}, + { "requiredButIgnoreDNSSEC", pdns::ZoneMD::Config::RequiredButIgnoreDNSSEC}, }; auto it = nameToVal.find(zonemdValidation); if (it == nameToVal.end()) { diff --git a/pdns/recursordist/rec-zonetocache.cc b/pdns/recursordist/rec-zonetocache.cc index 15d6395458..0f4c4e911c 100644 --- a/pdns/recursordist/rec-zonetocache.cc +++ b/pdns/recursordist/rec-zonetocache.cc @@ -21,6 +21,7 @@ */ #include "rec-zonetocache.hh" +#include #include "syncres.hh" #include "zoneparser-tng.hh" @@ -31,6 +32,7 @@ #include "threadname.hh" #include "rec-lua-conf.hh" #include "zonemd.hh" +#include "validate.hh" #ifdef HAVE_LIBCURL #include "minicurl.hh" @@ -59,9 +61,10 @@ struct ZoneData bool isRRSetAuth(const DNSName& qname, QType qtype) const; void parseDRForCache(DNSRecord& dr); - pdns::ZoneMD::Result getByAXFR(const RecZoneToCache::Config&); - pdns::ZoneMD::Result processLines(const std::vector& lines, const RecZoneToCache::Config& config); + pdns::ZoneMD::Result getByAXFR(const RecZoneToCache::Config&, pdns::ZoneMD&); + pdns::ZoneMD::Result processLines(const std::vector& lines, const RecZoneToCache::Config& config, pdns::ZoneMD&); void ZoneToCache(const RecZoneToCache::Config& config, uint64_t gen); + vState dnssecValidate(pdns::ZoneMD&) const; }; bool ZoneData::isRRSetAuth(const DNSName& qname, QType qtype) const @@ -127,7 +130,7 @@ void ZoneData::parseDRForCache(DNSRecord& dr) } } -pdns::ZoneMD::Result ZoneData::getByAXFR(const RecZoneToCache::Config& config) +pdns::ZoneMD::Result ZoneData::getByAXFR(const RecZoneToCache::Config& config, pdns::ZoneMD& zonemd) { ComboAddress primary = ComboAddress(config.d_sources.at(0), 53); uint16_t axfrTimeout = config.d_timeout; @@ -144,7 +147,6 @@ pdns::ZoneMD::Result ZoneData::getByAXFR(const RecZoneToCache::Config& config) time_t axfrStart = time(nullptr); time_t axfrNow = time(nullptr); - auto zonemd = pdns::ZoneMD(d_zone); while (axfr.getChunk(nop, &chunk, (axfrStart + axfrTimeout - axfrNow))) { for (auto& dr : chunk) { if (config.d_zonemd != pdns::ZoneMD::Config::Ignore) { @@ -207,14 +209,13 @@ static std::vector getURL(const RecZoneToCache::Config& config) return lines; } -pdns::ZoneMD::Result ZoneData::processLines(const vector& lines, const RecZoneToCache::Config& config) +pdns::ZoneMD::Result ZoneData::processLines(const vector& lines, const RecZoneToCache::Config& config, pdns::ZoneMD& zonemd) { DNSResourceRecord drr; ZoneParserTNG zpt(lines, d_zone); zpt.setMaxGenerateSteps(1); zpt.setMaxIncludes(0); - auto zonemd = pdns::ZoneMD(d_zone); while (zpt.get(drr)) { DNSRecord dr(drr); if (config.d_zonemd != pdns::ZoneMD::Config::Ignore) { @@ -235,21 +236,76 @@ pdns::ZoneMD::Result ZoneData::processLines(const vector& lines, const R return pdns::ZoneMD::Result::OK; } +vState ZoneData::dnssecValidate(pdns::ZoneMD &zonemd) const +{ + vector dsRecords; + vState dsState = vState::Indeterminate; + + // XXX TA/NTA processsing, plus we have nog guarantee the DS is + // is in the cache. We assume for now it's cached magically + + // Get the DS records + if (g_recCache->get(d_now, d_zone, QType::DS, false, &dsRecords, ComboAddress(), false, boost::none, nullptr, nullptr, nullptr, &dsState) <= 0) { + return vState::BogusUnableToGetDSs; + } + if (dsState != vState::Secure) { + return vState::BogusUnableToGetDSs; // XXX is this the right status? + } + + // Collect DNSKEYs and validate them using the DS records + dsmap_t dsRecordContents; + for (const auto& ds : dsRecords) { + dsRecordContents.insert(*getRR(ds).get()); + } + + skeyset_t dnsKeys; + sortedRecords_t records; + if (zonemd.getDNSKEYs().size() == 0) { + return vState::BogusUnableToGetDNSKEYs; + } + for (const auto& key: zonemd.getDNSKEYs()) { + dnsKeys.emplace(key); + records.emplace(key); + } + + skeyset_t validKeys; + vState dnsKeyState = validateDNSKeysAgainstDS(d_now, d_zone, dsRecordContents, dnsKeys, records, zonemd.getRRSIGs(), validKeys); + if (dnsKeyState != vState::Secure) { + return dnsKeyState; + } + + if (validKeys.size() == 0) { + return vState::BogusNoValidDNSKEY; + } + if (zonemd.getZONEMDs().size() == 0) { + // XXX is this the right status? Also per RFC we should actuelly prove non-existence of ZONEMD + // as downgrade attacks could be possible by an in the middle party zapping ZONEMDs + return dnsKeyState; + } + records.clear(); + + // Collect the ZONEMD records and validate them using the validted DNSSKEYs + for (const auto& rec : zonemd.getZONEMDs()) { + records.emplace(rec); + } + return validateWithKeySet(d_now, d_zone, records, zonemd.getRRSIGs(), validKeys); +} + void ZoneData::ZoneToCache(const RecZoneToCache::Config& config, uint64_t configGeneration) { if (config.d_sources.size() > 1) { d_log->info("Multiple sources not yet supported, using first"); } - // We do not do validation, it will happen on-demand if an Indeterminate record is encountered when the caches are queried // First scan all records collecting info about delegations ans sigs // A this moment, we ignore NSEC and NSEC3 records. It is not clear to me yet under which conditions // they could be entered in into the (neg)cache. + auto zonemd = pdns::ZoneMD(DNSName(config.d_zone)); pdns::ZoneMD::Result result = pdns::ZoneMD::Result::OK; if (config.d_method == "axfr") { d_log->info("Getting zone by AXFR"); - result = getByAXFR(config); + result = getByAXFR(config, zonemd); } else { vector lines; @@ -261,28 +317,46 @@ void ZoneData::ZoneToCache(const RecZoneToCache::Config& config, uint64_t config d_log->info("Getting zone from file"); lines = getLinesFromFile(config.d_sources.at(0)); } - result = processLines(lines, config); + result = processLines(lines, config, zonemd); + } + + if (config.d_zonemd == pdns::ZoneMD::Config::RequiredWithDNSSEC && g_dnssecmode == DNSSECMode::Off) { + throw PDNSException("ZONEMD DNSSEC validation failure: dnssec is switched of but required by ztc"); + } + + // Validate DNSKEYs and ZONEMD, rest of records are validated on-demand by SyncRes + if (config.d_zonemd == pdns::ZoneMD::Config::RequiredWithDNSSEC || + (g_dnssecmode == DNSSECMode::ValidateAll && config.d_zonemd != pdns::ZoneMD::Config::RequiredButIgnoreDNSSEC)) { + auto validationStatus = dnssecValidate(zonemd); + d_log->info("ZONEMD DNSSEC validation done", "validationStatus", Logging::Loggable(validationStatus)); + if (config.d_zonemd == pdns::ZoneMD::Config::RequiredWithDNSSEC) { + if (validationStatus != vState::Secure) { + throw PDNSException("ZONEMD required DNSSEC validation failed"); + } + } + // XXX handle other cases } if (pdns::ZoneMD::validationRequired(config.d_zonemd) && result != pdns::ZoneMD::Result::OK) { // We do not accept NoValidationDone in this case - throw PDNSException("ZoneMD validation failure"); + throw PDNSException("ZONEMD validation failure"); return; } if (config.d_zonemd == pdns::ZoneMD::Config::Process && result == pdns::ZoneMD::Result::ValidationFailure) { - throw PDNSException("ZoneMD validation failure"); + throw PDNSException("ZONEMD digest validation failure"); return; } + if (config.d_zonemd == pdns::ZoneMD::Config::LogOnly) { switch (result) { case pdns::ZoneMD::Result::ValidationFailure: - d_log->info("ZoneMD failure (ignored)"); + d_log->info("ZONEMD digest failure (ignored)"); break; case pdns::ZoneMD::Result::NoValidationDone: - d_log->info("No ZoneMD validation done"); + d_log->info("No ZONEMD digest validation done"); break; case pdns::ZoneMD::Result::OK: - d_log->info("ZoneMD validation succeeded"); + d_log->info("ZONEMD digest validation succeeded"); break; } } diff --git a/pdns/recursordist/rec-zonetocache.hh b/pdns/recursordist/rec-zonetocache.hh index 11064cad44..4b48809f2e 100644 --- a/pdns/recursordist/rec-zonetocache.hh +++ b/pdns/recursordist/rec-zonetocache.hh @@ -41,7 +41,7 @@ public: time_t d_retryOnError{60}; // Retry on error time_t d_refreshPeriod{24 * 3600}; // Time between refetch uint32_t d_timeout{20}; // timeout in seconds - pdns::ZoneMD::Config d_zonemd{pdns::ZoneMD::Config::LogOnly}; + pdns::ZoneMD::Config d_zonemd{pdns::ZoneMD::Config::Process}; }; static void ZoneToCache(Config config, uint64_t gen); }; diff --git a/pdns/recursordist/test-rec-zonetocache.cc b/pdns/recursordist/test-rec-zonetocache.cc index 3dde7d1237..2081124ab5 100644 --- a/pdns/recursordist/test-rec-zonetocache.cc +++ b/pdns/recursordist/test-rec-zonetocache.cc @@ -71,6 +71,7 @@ BOOST_AUTO_TEST_CASE(test_zonetocache) RecZoneToCache::Config config{".", "file", {temp}, ComboAddress(), TSIGTriplet()}; config.d_refreshPeriod = 0; + config.d_retryOnError = 0; // Start with a new, empty cache g_recCache = std::make_unique(); diff --git a/pdns/zonemd.cc b/pdns/zonemd.cc index c90186da03..3173996bf0 100644 --- a/pdns/zonemd.cc +++ b/pdns/zonemd.cc @@ -28,15 +28,27 @@ void pdns::ZoneMD::readRecords(ZoneParserTNG& zpt) std::string err = "Bad record content in record for '" + dnsResourceRecord.qname.toStringNoDot() + "|" + dnsResourceRecord.qtype.toString() + "': " + e.what(); throw PDNSException(err); } - if (dnsResourceRecord.qtype == QType::SOA && dnsResourceRecord.qname == d_zone) { - d_soaRecordContent = std::dynamic_pointer_cast(drc); - } - if (dnsResourceRecord.qtype == QType::ZONEMD && dnsResourceRecord.qname == d_zone) { - auto zonemd = std::dynamic_pointer_cast(drc); - auto inserted = d_zonemdRecords.insert({pair(zonemd->d_scheme, zonemd->d_hashalgo), {zonemd, false}}); - if (!inserted.second) { - // Mark as duplicate - inserted.first->second.duplicate = true; + + if (dnsResourceRecord.qname == d_zone) { + switch (dnsResourceRecord.qtype) { + case QType::SOA: + d_soaRecordContent = std::dynamic_pointer_cast(drc); + break; + case QType::DNSKEY: + d_dnskeys.emplace(std::dynamic_pointer_cast(drc)); + break; + case QType::ZONEMD :{ + auto zonemd = std::dynamic_pointer_cast(drc); + auto inserted = d_zonemdRecords.insert({pair(zonemd->d_scheme, zonemd->d_hashalgo), {zonemd, false}}); + if (!inserted.second) { + // Mark as duplicate + inserted.first->second.duplicate = true; + } + break; + } + case QType::RRSIG: + d_rrsigs.emplace_back(std::dynamic_pointer_cast(drc)); + break; } } RRSetKey_t key = std::pair(dnsResourceRecord.qname, dnsResourceRecord.qtype); @@ -56,20 +68,31 @@ void pdns::ZoneMD::readRecord(const DNSRecord& record) { if (!record.d_name.isPartOf(d_zone) && record.d_name != d_zone) { return; - } + } if (record.d_type == QType::SOA && d_soaRecordContent) { return; } - if (record.d_type == QType::SOA && record.d_name == d_zone) { - d_soaRecordContent = std::dynamic_pointer_cast(record.d_content); - } - if (record.d_type == QType::ZONEMD && record.d_name == d_zone) { - auto zonemd = std::dynamic_pointer_cast(record.d_content); - auto inserted = d_zonemdRecords.insert({pair(zonemd->d_scheme, zonemd->d_hashalgo), {zonemd, false}}); - if (!inserted.second) { - // Mark as duplicate - inserted.first->second.duplicate = true; + if (record.d_name == d_zone) { + switch (record.d_type) { + case QType::SOA: + d_soaRecordContent = std::dynamic_pointer_cast(record.d_content); + break; + case QType::DNSKEY: + d_dnskeys.emplace(std::dynamic_pointer_cast(record.d_content)); + break; + case QType::ZONEMD: { + auto zonemd = std::dynamic_pointer_cast(record.d_content); + auto inserted = d_zonemdRecords.insert({pair(zonemd->d_scheme, zonemd->d_hashalgo), {zonemd, false}}); + if (!inserted.second) { + // Mark as duplicate + inserted.first->second.duplicate = true; + } + break; + } + case QType::RRSIG: + d_rrsigs.emplace_back(std::dynamic_pointer_cast(record.d_content)); + break; } } RRSetKey_t key = std::pair(record.d_name, record.d_type); @@ -145,6 +168,10 @@ void pdns::ZoneMD::verify(bool& validationDone, bool& validationOK) sorted.insert(rr); } + if (sorted.empty()) { + // continue; + } + if (qtype != QType::RRSIG) { RRSIGRecordContent rrc; rrc.d_originalttl = d_resourceRecordSetTTLs[rrset.first]; diff --git a/pdns/zonemd.hh b/pdns/zonemd.hh index 6f8de71d14..962b99a314 100644 --- a/pdns/zonemd.hh +++ b/pdns/zonemd.hh @@ -43,7 +43,7 @@ public: LogOnly, Required, RequiredWithDNSSEC, - RequiredIgnoreDNSSEC, + RequiredButIgnoreDNSSEC, }; enum class Result : uint8_t { @@ -62,7 +62,29 @@ public: static bool validationRequired(Config config) { - return config == Config::Required || config == Config::RequiredWithDNSSEC || config == Config::RequiredIgnoreDNSSEC; + return config == Config::Required || config == Config::RequiredWithDNSSEC || config == Config::RequiredButIgnoreDNSSEC; + } + + // Return the zone's apex DNSKEYs + const std::set>& getDNSKEYs() const + { + return d_dnskeys; + } + + // Return the zone's apex RRSIGs + const std::vector>& getRRSIGs() const + { + return d_rrsigs; + } + + // Return the zone's apex ZONEMDs + std::vector> getZONEMDs() const + { + std::vector> ret; + for (const auto& zonemd : d_zonemdRecords) { + ret.emplace_back(zonemd.second.record); + } + return ret; } private: @@ -99,6 +121,8 @@ private: std::map d_resourceRecordSetTTLs; std::shared_ptr d_soaRecordContent; + std::set> d_dnskeys; + std::vector> d_rrsigs; const DNSName d_zone; };