]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Add Lua config and do DNSSEC validation of ZONEMD record
authorOtto Moerbeek <otto.moerbeek@open-xchange.com>
Tue, 18 Jan 2022 11:36:42 +0000 (12:36 +0100)
committerOtto Moerbeek <otto.moerbeek@open-xchange.com>
Fri, 21 Jan 2022 11:06:01 +0000 (12:06 +0100)
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.

pdns/rec-lua-conf.cc
pdns/recursordist/rec-zonetocache.cc
pdns/recursordist/rec-zonetocache.hh
pdns/recursordist/test-rec-zonetocache.cc
pdns/zonemd.cc
pdns/zonemd.hh

index 713740e725ef1b8912a5ed8e619577a997bcf640..59af03d20c8933699b476d4aa776539c6d0d3d3f 100644 (file)
@@ -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()) {
index 15d63954584b6e20cd3410d186e0a55ee262f401..0f4c4e911cf384b0497d752b866dc7ee5431b885 100644 (file)
@@ -21,6 +21,7 @@
  */
 
 #include "rec-zonetocache.hh"
+#include <algorithm>
 
 #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<std::string>& lines, const RecZoneToCache::Config& config);
+  pdns::ZoneMD::Result getByAXFR(const RecZoneToCache::Config&, pdns::ZoneMD&);
+  pdns::ZoneMD::Result processLines(const std::vector<std::string>& 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<std::string> getURL(const RecZoneToCache::Config& config)
   return lines;
 }
 
-pdns::ZoneMD::Result ZoneData::processLines(const vector<string>& lines, const RecZoneToCache::Config& config)
+pdns::ZoneMD::Result ZoneData::processLines(const vector<string>& 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<string>& lines, const R
   return pdns::ZoneMD::Result::OK;
 }
 
+vState ZoneData::dnssecValidate(pdns::ZoneMD &zonemd) const
+{
+  vector<DNSRecord> 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<DSRecordContent>(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<string> 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;
     }
   }
index 11064cad44064275ea0a4fbc58a7eb721c04070e..4b48809f2e4a127fb9b3f7db20b7a74f586a153f 100644 (file)
@@ -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);
 };
index 3dde7d1237a662b7b11219621db08a397f8fba37..2081124ab536532729a369a98fa9a01352de5db5 100644 (file)
@@ -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<MemRecursorCache>();
index c90186da0382533325410f493f480846a3c1402e..3173996bf048464b42928ced338f235f275f3196 100644 (file)
@@ -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<SOARecordContent>(drc);
-    }
-    if (dnsResourceRecord.qtype == QType::ZONEMD && dnsResourceRecord.qname == d_zone) {
-      auto zonemd = std::dynamic_pointer_cast<ZONEMDRecordContent>(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<SOARecordContent>(drc);
+        break;
+      case QType::DNSKEY:
+        d_dnskeys.emplace(std::dynamic_pointer_cast<DNSKEYRecordContent>(drc));
+        break;
+      case QType::ZONEMD :{
+        auto zonemd = std::dynamic_pointer_cast<ZONEMDRecordContent>(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<RRSIGRecordContent>(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<SOARecordContent>(record.d_content);
-  }
-  if (record.d_type == QType::ZONEMD && record.d_name == d_zone) {
-    auto zonemd = std::dynamic_pointer_cast<ZONEMDRecordContent>(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<SOARecordContent>(record.d_content);
+      break;
+    case QType::DNSKEY:
+      d_dnskeys.emplace(std::dynamic_pointer_cast<DNSKEYRecordContent>(record.d_content));
+      break;
+    case QType::ZONEMD: {
+      auto zonemd = std::dynamic_pointer_cast<ZONEMDRecordContent>(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<RRSIGRecordContent>(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];
index 6f8de71d14610f47dc7e9f81f6ca3937efa130f6..962b99a3148960ab1d4572aa199f13527e9ba496 100644 (file)
@@ -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<shared_ptr<DNSKEYRecordContent>>& getDNSKEYs() const
+  {
+    return d_dnskeys;
+  }
+
+  // Return the zone's apex RRSIGs
+  const std::vector<shared_ptr<RRSIGRecordContent>>& getRRSIGs() const
+  {
+    return d_rrsigs;
+  }
+
+  // Return the zone's apex ZONEMDs
+  std::vector<shared_ptr<ZONEMDRecordContent>> getZONEMDs() const
+  {
+    std::vector<shared_ptr<ZONEMDRecordContent>> ret;
+    for (const auto& zonemd : d_zonemdRecords) {
+      ret.emplace_back(zonemd.second.record);
+    }
+    return ret;
   }
 
 private:
@@ -99,6 +121,8 @@ private:
   std::map<RRSetKey_t, uint32_t> d_resourceRecordSetTTLs;
 
   std::shared_ptr<SOARecordContent> d_soaRecordContent;
+  std::set<shared_ptr<DNSKEYRecordContent>> d_dnskeys;
+  std::vector<shared_ptr<RRSIGRecordContent>> d_rrsigs;
   const DNSName d_zone;
 };