]> granicus.if.org Git - pdns/commitdiff
rec: Fix validation of denial proofs
authorRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 27 Oct 2017 08:34:38 +0000 (10:34 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 27 Oct 2017 08:34:38 +0000 (10:34 +0200)
pdns/dnsname.cc
pdns/dnsname.hh
pdns/recursordist/test-syncres_cc.cc
pdns/syncres.cc
pdns/test-dnsname_cc.cc
pdns/validate.cc
pdns/validate.hh

index 51115c9306887c63f0f7b1d29d6ff65ca8030f50..feabbac7edfd79156456d60447b575aac0028b4a 100644 (file)
@@ -253,6 +253,24 @@ void DNSName::makeUsRelative(const DNSName& zone)
     clear();
 }
 
+DNSName DNSName::getCommonLabels(const DNSName& other) const
+{
+  DNSName result;
+
+  const std::vector<std::string> ours = getRawLabels();
+  const std::vector<std::string> others = other.getRawLabels();
+
+  for (size_t pos = 0; ours.size() > pos && others.size() > pos; pos++) {
+    if (ours.at(ours.size() - pos - 1) != others.at(others.size() - pos - 1)) {
+      break;
+    }
+
+    result.prependRawLabel(ours.at(ours.size() - pos - 1));
+  }
+
+  return result;
+}
+
 DNSName DNSName::labelReverse() const
 {
   DNSName ret;
index 0c32afd1d7a3fccb21cacf54c7819cc1bce4132e..7307da4edd6b7f66bedc679954d6a1b688740f34 100644 (file)
@@ -94,6 +94,7 @@ public:
     }
   }
   void makeUsRelative(const DNSName& zone);
+  DNSName getCommonLabels(const DNSName& other) const; //!< Return the list of common labels from the top, for example 'c.d' for 'a.b.c.d' and 'x.y.c.d'
   DNSName labelReverse() const;
   bool isWildcard() const;
   bool isHostname() const;
index 3527a1770c8b69436ab47b9a094197ab18100ca1..2bb537c1dbed4e64529bdebdc64d861fbfe3d228 100644 (file)
@@ -361,26 +361,25 @@ static void addNSEC3RecordToLW(const DNSName& hashedName, const std::string& has
   records.push_back(rec);
 }
 
-static void addNSEC3UnhashedRecordToLW(const DNSName& domain, const std::string& next, const std::set<uint16_t>& types,  uint32_t ttl, std::vector<DNSRecord>& records)
+static void addNSEC3UnhashedRecordToLW(const DNSName& domain, const DNSName& zone, const std::string& next, const std::set<uint16_t>& types,  uint32_t ttl, std::vector<DNSRecord>& records)
 {
   static const std::string salt = "deadbeef";
   static const unsigned int iterations = 10;
   std::string hashed = hashQNameWithSalt(salt, iterations, domain);
 
-  addNSEC3RecordToLW(DNSName(toBase32Hex(hashed)), next, salt, iterations, types, ttl, records);
+  addNSEC3RecordToLW(DNSName(toBase32Hex(hashed)) + zone, next, salt, iterations, types, ttl, records);
 }
 
-void addNSEC3NarrowRecordToLW(const DNSName& domain, const std::set<uint16_t>& types,  uint32_t ttl, std::vector<DNSRecord>& records)
+static void addNSEC3NarrowRecordToLW(const DNSName& domain, const DNSName& zone, const std::set<uint16_t>& types,  uint32_t ttl, std::vector<DNSRecord>& records)
 {
   static const std::string salt = "deadbeef";
   static const unsigned int iterations = 10;
   std::string hashed = hashQNameWithSalt(salt, iterations, domain);
-
   std::string hashedNext(hashed);
   incrementHash(hashedNext);
   decrementHash(hashed);
 
-  addNSEC3RecordToLW(DNSName(toBase32Hex(hashed)), hashedNext, salt, iterations, types, ttl, records);
+  addNSEC3RecordToLW(DNSName(toBase32Hex(hashed)) + zone, hashedNext, salt, iterations, types, ttl, records);
 }
 
 static void generateKeyMaterial(const DNSName& name, unsigned int algo, uint8_t digest, testkeysset_t& keys)
@@ -1078,6 +1077,58 @@ BOOST_AUTO_TEST_CASE(test_following_cname) {
   BOOST_CHECK_EQUAL(ret[1].d_name, cnameTarget);
 }
 
+BOOST_AUTO_TEST_CASE(test_cname_nxdomain) {
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr);
+
+  primeHints();
+
+  const DNSName target("cname.powerdns.com.");
+  const DNSName cnameTarget("cname-target.powerdns.com");
+
+  sr->setAsyncCallback([target, cnameTarget](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, std::shared_ptr<RemoteLogger> outgoingLogger, LWResult* res) {
+
+      if (isRootServer(ip)) {
+        setLWResult(res, 0, false, false, true);
+        addRecordToLW(res, "powerdns.com.", QType::NS, "a.gtld-servers.net.", DNSResourceRecord::AUTHORITY, 172800);
+        addRecordToLW(res, "a.gtld-servers.net.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600);
+        return 1;
+      } else if (ip == ComboAddress("192.0.2.1:53")) {
+
+        if (domain == target) {
+          setLWResult(res, RCode::NXDomain, true, false, false);
+          addRecordToLW(res, domain, QType::CNAME, cnameTarget.toString());
+          addRecordToLW(res, "powerdns.com.", QType::SOA, "a.powerdns.com. nstld.verisign-grs.com. 2017032800 1800 900 604800 86400", DNSResourceRecord::AUTHORITY, 86400);
+        } else if (domain == cnameTarget) {
+          setLWResult(res, RCode::NXDomain, true, false, false);
+          addRecordToLW(res, "powerdns.com.", QType::SOA, "a.powerdns.com. nstld.verisign-grs.com. 2017032800 1800 900 604800 86400", DNSResourceRecord::AUTHORITY, 86400);
+          return 1;
+        }
+
+        return 1;
+      }
+
+      return 0;
+    });
+
+  vector<DNSRecord> ret;
+  int res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NXDomain);
+  BOOST_REQUIRE_EQUAL(ret.size(), 2);
+  BOOST_CHECK(ret[0].d_type == QType::CNAME);
+  BOOST_CHECK_EQUAL(ret[0].d_name, target);
+  BOOST_CHECK(ret[1].d_type == QType::SOA);
+
+  /* a second time, to check the cache */
+  ret.clear();
+  res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NXDomain);
+  BOOST_REQUIRE_EQUAL(ret.size(), 2);
+  BOOST_CHECK(ret[0].d_type == QType::CNAME);
+  BOOST_CHECK_EQUAL(ret[0].d_name, target);
+  BOOST_CHECK(ret[1].d_type == QType::SOA);
+}
+
 BOOST_AUTO_TEST_CASE(test_included_poisonous_cname) {
   std::unique_ptr<SyncRes> sr;
   initSR(sr);
@@ -4705,6 +4756,9 @@ BOOST_AUTO_TEST_CASE(test_dnssec_validation_nxdomain_nsec) {
             addRRSIG(keys, res->d_records, auth, 300);
             addNSECRecordToLW(DNSName("nw.powerdns.com."), DNSName("ny.powerdns.com."), { QType::RRSIG, QType::NSEC }, 600, res->d_records);
             addRRSIG(keys, res->d_records, auth, 300);
+            /* add wildcard denial */
+            addNSECRecordToLW(DNSName("powerdns.com."), DNSName("a.powerdns.com."), { QType::RRSIG, QType::NSEC }, 600, res->d_records);
+            addRRSIG(keys, res->d_records, auth, 300);
           }
           return 1;
         }
@@ -4717,7 +4771,7 @@ BOOST_AUTO_TEST_CASE(test_dnssec_validation_nxdomain_nsec) {
   int res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
   BOOST_CHECK_EQUAL(res, RCode::NXDomain);
   BOOST_CHECK_EQUAL(sr->getValidationState(), Secure);
-  BOOST_REQUIRE_EQUAL(ret.size(), 4);
+  BOOST_REQUIRE_EQUAL(ret.size(), 6);
   BOOST_CHECK_EQUAL(queriesCount, 9);
 
   /* again, to test the cache */
@@ -4725,7 +4779,7 @@ BOOST_AUTO_TEST_CASE(test_dnssec_validation_nxdomain_nsec) {
   res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
   BOOST_CHECK_EQUAL(res, RCode::NXDomain);
   BOOST_CHECK_EQUAL(sr->getValidationState(), Secure);
-  BOOST_REQUIRE_EQUAL(ret.size(), 4);
+  BOOST_REQUIRE_EQUAL(ret.size(), 6);
   BOOST_CHECK_EQUAL(queriesCount, 9);
 }
 
@@ -4810,6 +4864,7 @@ BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec_wildcard) {
           else {
             addRecordToLW(res, domain, QType::A, "192.0.2.42");
             addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300, false, boost::none, DNSName("*.powerdns.com"));
+            /* we need to add the proof that this name does not exist, so the wildcard may apply */
             addNSECRecordToLW(DNSName("a.powerdns.com."), DNSName("wwz.powerdns.com."), { QType::A, QType::NSEC, QType::RRSIG }, 600, res->d_records);
             addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300);
           }
@@ -4836,6 +4891,392 @@ BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec_wildcard) {
   BOOST_CHECK_EQUAL(queriesCount, 9);
 }
 
+BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec_nodata_nowildcard) {
+  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::SHA256, keys, luaconfsCopy.dsAnchors);
+  generateKeyMaterial(DNSName("com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::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, std::shared_ptr<RemoteLogger> outgoingLogger, LWResult* res) {
+      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 NSEC denying the DS AND the existence of a cut (no NS) */
+          addNSECRecordToLW(domain, DNSName("z") + domain, { QType::NSEC }, 600, res->d_records);
+          addRRSIG(keys, res->d_records, auth, 300);
+          return 1;
+        }
+        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 1;
+        }
+        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 */
+          addNSECRecordToLW(DNSName("wwv.com."), DNSName("wwx.com."), { QType::NSEC, QType::RRSIG }, 600, res->d_records);
+          addRRSIG(keys, res->d_records, DNSName("com."), 300);
+          /* a wildcard matches but has no record for this type */
+          addNSECRecordToLW(DNSName("*.com."), DNSName("com."), { QType::AAAA, QType::NSEC, QType::RRSIG }, 600, res->d_records);
+          addRRSIG(keys, res->d_records, DNSName("com"), 300, false, boost::none, DNSName("*.com"));
+          return 1;
+        }
+      }
+
+      return 0;
+    });
+
+  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(), Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 6);
+  BOOST_CHECK_EQUAL(queriesCount, 6);
+
+  /* again, to test the cache */
+  ret.clear();
+  res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 6);
+  BOOST_CHECK_EQUAL(queriesCount, 6);
+}
+
+BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec3_nodata_nowildcard) {
+  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::SHA256, keys, luaconfsCopy.dsAnchors);
+  generateKeyMaterial(DNSName("com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::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, std::shared_ptr<RemoteLogger> outgoingLogger, LWResult* res) {
+      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 1;
+        }
+        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 1;
+        }
+        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 1;
+        }
+      }
+
+      return 0;
+    });
+
+  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(), Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 8);
+  BOOST_CHECK_EQUAL(queriesCount, 6);
+
+  /* again, to test the cache */
+  ret.clear();
+  res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 8);
+  BOOST_CHECK_EQUAL(queriesCount, 6);
+}
+
+BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec3_wildcard) {
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+
+  setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+  primeHints();
+  const DNSName target("www.powerdns.com.");
+  testkeysset_t keys;
+
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::SHA256, keys, luaconfsCopy.dsAnchors);
+  generateKeyMaterial(DNSName("com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::SHA256, keys);
+  generateKeyMaterial(DNSName("powerdns.com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::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, std::shared_ptr<RemoteLogger> outgoingLogger, LWResult* res) {
+      queriesCount++;
+
+      if (type == QType::DS || type == QType::DNSKEY) {
+        if (type == QType::DS && domain == target) {
+          setLWResult(res, RCode::NoError, true, false, true);
+          addRecordToLW(res, DNSName("powerdns.com."), QType::SOA, "pdns-public-ns1.powerdns.com. pieter\\.lexis.powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+          addNSECRecordToLW(DNSName("www.powerdns.com."), DNSName("wwz.powerdns.com."), { QType::A, QType::NSEC, QType::RRSIG }, 600, res->d_records);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300);
+          return 1;
+        }
+        else {
+          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 1;
+        }
+        else if (ip == ComboAddress("192.0.2.1:53")) {
+          if (domain == DNSName("com.")) {
+            setLWResult(res, 0, true, false, true);
+            addRecordToLW(res, domain, QType::NS, "a.gtld-servers.com.");
+            addRRSIG(keys, res->d_records, domain, 300);
+            addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600);
+            addRRSIG(keys, res->d_records, domain, 300);
+          }
+          else {
+            setLWResult(res, 0, false, false, true);
+            addRecordToLW(res, "powerdns.com.", QType::NS, "ns1.powerdns.com.", DNSResourceRecord::AUTHORITY, 3600);
+            addDS(DNSName("powerdns.com."), 300, res->d_records, keys);
+            addRRSIG(keys, res->d_records, DNSName("com."), 300);
+            addRecordToLW(res, "ns1.powerdns.com.", QType::A, "192.0.2.2", DNSResourceRecord::ADDITIONAL, 3600);
+          }
+          return 1;
+        }
+        else if (ip == ComboAddress("192.0.2.2:53")) {
+          setLWResult(res, 0, true, false, true);
+          if (type == QType::NS) {
+            if (domain == DNSName("powerdns.com.")) {
+              addRecordToLW(res, domain, QType::NS, "ns1.powerdns.com.");
+              addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300);
+              addRecordToLW(res, "ns1.powerdns.com.", QType::A, "192.0.2.2", DNSResourceRecord::ADDITIONAL, 3600);
+              addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300);
+            }
+            else {
+              addRecordToLW(res, domain, QType::SOA, "pdns-public-ns1.powerdns.com. pieter\\.lexis.powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+              addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300);
+              addNSECRecordToLW(DNSName("www.powerdns.com."), DNSName("wwz.powerdns.com."), { QType::A, QType::NSEC, QType::RRSIG }, 600, res->d_records);
+              addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300);
+            }
+          }
+          else {
+            addRecordToLW(res, domain, QType::A, "192.0.2.42");
+            addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300, false, boost::none, DNSName("*.powerdns.com"));
+            /* we need to add the proof that this name does not exist, so the wildcard may apply */
+            /* first the closest encloser */
+            addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", { QType::A, QType::TXT, QType::RRSIG, QType::NSEC }, 600, res->d_records);
+            addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+            /* then the next closer */
+            addNSEC3NarrowRecordToLW(DNSName("www.powerdns.com."), DNSName("powerdns.com."), { QType::A, QType::TXT, QType::RRSIG, QType::NSEC }, 600, res->d_records);
+            addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+          }
+          return 1;
+        }
+      }
+
+      return 0;
+    });
+
+  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(), Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 6);
+  BOOST_CHECK_EQUAL(queriesCount, 9);
+
+  /* again, to test the cache */
+  ret.clear();
+  res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), Secure);
+  BOOST_REQUIRE_EQUAL(ret.size(), 6);
+  BOOST_CHECK_EQUAL(queriesCount, 9);
+}
+
+BOOST_AUTO_TEST_CASE(test_dnssec_validation_nsec_wildcard_missing) {
+  std::unique_ptr<SyncRes> sr;
+  initSR(sr, true);
+
+  setDNSSECValidation(sr, DNSSECMode::ValidateAll);
+
+  primeHints();
+  const DNSName target("www.powerdns.com.");
+  testkeysset_t keys;
+
+  auto luaconfsCopy = g_luaconfs.getCopy();
+  luaconfsCopy.dsAnchors.clear();
+  generateKeyMaterial(g_rootdnsname, DNSSECKeeper::ECDSA256, DNSSECKeeper::SHA256, keys, luaconfsCopy.dsAnchors);
+  generateKeyMaterial(DNSName("com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::SHA256, keys);
+  generateKeyMaterial(DNSName("powerdns.com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::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, std::shared_ptr<RemoteLogger> outgoingLogger, LWResult* res) {
+      queriesCount++;
+
+      if (type == QType::DS || type == QType::DNSKEY) {
+        if (type == QType::DS && domain == target) {
+          setLWResult(res, RCode::NoError, true, false, true);
+          addRecordToLW(res, DNSName("powerdns.com."), QType::SOA, "pdns-public-ns1.powerdns.com. pieter\\.lexis.powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com."), 300);
+          addNSECRecordToLW(DNSName("www.powerdns.com."), DNSName("wwz.powerdns.com."), { QType::A, QType::NSEC, QType::RRSIG }, 600, res->d_records);
+          addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300);
+          return 1;
+        }
+        else {
+          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 1;
+        }
+        else if (ip == ComboAddress("192.0.2.1:53")) {
+          if (domain == DNSName("com.")) {
+            setLWResult(res, 0, true, false, true);
+            addRecordToLW(res, domain, QType::NS, "a.gtld-servers.com.");
+            addRRSIG(keys, res->d_records, domain, 300);
+            addRecordToLW(res, "a.gtld-servers.com.", QType::A, "192.0.2.1", DNSResourceRecord::ADDITIONAL, 3600);
+            addRRSIG(keys, res->d_records, domain, 300);
+          }
+          else {
+            setLWResult(res, 0, false, false, true);
+            addRecordToLW(res, "powerdns.com.", QType::NS, "ns1.powerdns.com.", DNSResourceRecord::AUTHORITY, 3600);
+            addDS(DNSName("powerdns.com."), 300, res->d_records, keys);
+            addRRSIG(keys, res->d_records, DNSName("com."), 300);
+            addRecordToLW(res, "ns1.powerdns.com.", QType::A, "192.0.2.2", DNSResourceRecord::ADDITIONAL, 3600);
+          }
+          return 1;
+        }
+        else if (ip == ComboAddress("192.0.2.2:53")) {
+          setLWResult(res, 0, true, false, true);
+          if (type == QType::NS) {
+            if (domain == DNSName("powerdns.com.")) {
+              addRecordToLW(res, domain, QType::NS, "ns1.powerdns.com.");
+              addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300);
+              addRecordToLW(res, "ns1.powerdns.com.", QType::A, "192.0.2.2", DNSResourceRecord::ADDITIONAL, 3600);
+              addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300);
+            }
+            else {
+              addRecordToLW(res, domain, QType::SOA, "pdns-public-ns1.powerdns.com. pieter\\.lexis.powerdns.com. 2017032301 10800 3600 604800 3600", DNSResourceRecord::AUTHORITY, 3600);
+              addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300);
+              addNSECRecordToLW(DNSName("www.powerdns.com."), DNSName("wwz.powerdns.com."), { QType::A, QType::NSEC, QType::RRSIG }, 600, res->d_records);
+              addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300);
+            }
+          }
+          else {
+            addRecordToLW(res, domain, QType::A, "192.0.2.42");
+            addRRSIG(keys, res->d_records, DNSName("powerdns.com"), 300, false, boost::none, DNSName("*.powerdns.com"));
+          }
+          return 1;
+        }
+      }
+
+      return 0;
+    });
+
+  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(), Bogus);
+  BOOST_REQUIRE_EQUAL(ret.size(), 2);
+  BOOST_CHECK_EQUAL(queriesCount, 9);
+
+  /* again, to test the cache */
+  ret.clear();
+  res = sr->beginResolve(target, QType(QType::A), QClass::IN, ret);
+  BOOST_CHECK_EQUAL(res, RCode::NoError);
+  BOOST_CHECK_EQUAL(sr->getValidationState(), Bogus);
+  BOOST_REQUIRE_EQUAL(ret.size(), 2);
+  BOOST_CHECK_EQUAL(queriesCount, 9);
+}
+
 BOOST_AUTO_TEST_CASE(test_dnssec_no_ds_on_referral_secure) {
   std::unique_ptr<SyncRes> sr;
   initSR(sr, true);
@@ -6988,6 +7429,19 @@ BOOST_AUTO_TEST_CASE(test_nsec_denial_nowrap) {
   cspmap_t denialMap;
   denialMap[std::make_pair(DNSName("a.example.org."), QType::NSEC)] = pair;
 
+  /* add wildcard denial */
+  recordContents.clear();
+  signatureContents.clear();
+  addNSECRecordToLW(DNSName("example.org."), DNSName("+.example.org"), { QType::A, QType::TXT, QType::RRSIG, QType::NSEC }, 600, records);
+  recordContents.push_back(records.at(0).d_content);
+  addRRSIG(keys, records, DNSName("example.org."), 300);
+  signatureContents.push_back(getRR<RRSIGRecordContent>(records.at(1)));
+  records.clear();
+
+  pair.records = recordContents;
+  pair.signatures = signatureContents;
+  denialMap[std::make_pair(DNSName("example.org."), QType::NSEC)] = pair;
+
   dState denialState = getDenial(denialMap, DNSName("b.example.org."), QType::A, false, false);
   BOOST_CHECK_EQUAL(denialState, NXDOMAIN);
 
@@ -7128,6 +7582,19 @@ BOOST_AUTO_TEST_CASE(test_nsec_root_nxd_denial) {
   cspmap_t denialMap;
   denialMap[std::make_pair(DNSName("a."), QType::NSEC)] = pair;
 
+  /* add wildcard denial */
+  recordContents.clear();
+  signatureContents.clear();
+  addNSECRecordToLW(DNSName("."), DNSName("+"), { QType::A, QType::TXT, QType::RRSIG, QType::NSEC }, 600, records);
+  recordContents.push_back(records.at(0).d_content);
+  addRRSIG(keys, records, DNSName("."), 300);
+  signatureContents.push_back(getRR<RRSIGRecordContent>(records.at(1)));
+  records.clear();
+
+  pair.records = recordContents;
+  pair.signatures = signatureContents;
+  denialMap[std::make_pair(DNSName("."), QType::NSEC)] = pair;
+
   dState denialState = getDenial(denialMap, DNSName("b."), QType::A, false, false);
   BOOST_CHECK_EQUAL(denialState, NXDOMAIN);
 }
@@ -7218,6 +7685,128 @@ BOOST_AUTO_TEST_CASE(test_nsec_insecure_delegation_denial) {
   BOOST_CHECK_EQUAL(denialState, INSECURE);
 }
 
+BOOST_AUTO_TEST_CASE(test_nsec_nxqtype_cname) {
+  init();
+
+  testkeysset_t keys;
+  generateKeyMaterial(DNSName("powerdns.com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::SHA256, keys);
+
+  vector<DNSRecord> records;
+
+  vector<shared_ptr<DNSRecordContent>> recordContents;
+  vector<shared_ptr<RRSIGRecordContent>> signatureContents;
+
+  addNSECRecordToLW(DNSName("a.powerdns.com."), DNSName("a.c.powerdns.com."), { QType::CNAME }, 600, records);
+  recordContents.push_back(records.at(0).d_content);
+  addRRSIG(keys, records, DNSName("powerdns.com."), 300);
+  signatureContents.push_back(getRR<RRSIGRecordContent>(records.at(1)));
+  records.clear();
+
+  ContentSigPair pair;
+  pair.records = recordContents;
+  pair.signatures = signatureContents;
+  cspmap_t denialMap;
+  denialMap[std::make_pair(DNSName("a.powerdns.com."), QType::NSEC)] = pair;
+
+  /* this NSEC is not valid to deny a.powerdns.com|A since it states that a CNAME exists */
+  dState denialState = getDenial(denialMap, DNSName("a.powerdns.com."), QType::A, true, true);
+  BOOST_CHECK_EQUAL(denialState, NODATA);
+}
+
+BOOST_AUTO_TEST_CASE(test_nsec3_nxqtype_cname) {
+  init();
+
+  testkeysset_t keys;
+  generateKeyMaterial(DNSName("powerdns.com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::SHA256, keys);
+
+  vector<DNSRecord> records;
+
+  vector<shared_ptr<DNSRecordContent>> recordContents;
+  vector<shared_ptr<RRSIGRecordContent>> signatureContents;
+
+  addNSEC3UnhashedRecordToLW(DNSName("a.powerdns.com."), DNSName("powerdns.com."), "whatever", { QType::CNAME }, 600, records);
+  recordContents.push_back(records.at(0).d_content);
+  addRRSIG(keys, records, DNSName("powerdns.com."), 300);
+  signatureContents.push_back(getRR<RRSIGRecordContent>(records.at(1)));
+
+  ContentSigPair pair;
+  pair.records = recordContents;
+  pair.signatures = signatureContents;
+  cspmap_t denialMap;
+  denialMap[std::make_pair(records.at(0).d_name, records.at(0).d_type)] = pair;
+  records.clear();
+
+  /* this NSEC3 is not valid to deny a.powerdns.com|A since it states that a CNAME exists */
+  dState denialState = getDenial(denialMap, DNSName("a.powerdns.com."), QType::A, false, true);
+  BOOST_CHECK_EQUAL(denialState, NODATA);
+}
+
+BOOST_AUTO_TEST_CASE(test_nsec_nxdomain_denial_missing_wildcard) {
+  init();
+
+  testkeysset_t keys;
+  generateKeyMaterial(DNSName("powerdns.com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::SHA256, keys);
+
+  vector<DNSRecord> records;
+
+  vector<shared_ptr<DNSRecordContent>> recordContents;
+  vector<shared_ptr<RRSIGRecordContent>> signatureContents;
+
+  addNSECRecordToLW(DNSName("a.powerdns.com."), DNSName("d.powerdns.com"), { QType::A, QType::TXT, QType::RRSIG, QType::NSEC }, 600, records);
+  recordContents.push_back(records.at(0).d_content);
+  addRRSIG(keys, records, DNSName("powerdns.com."), 300);
+  signatureContents.push_back(getRR<RRSIGRecordContent>(records.at(1)));
+  records.clear();
+
+  ContentSigPair pair;
+  pair.records = recordContents;
+  pair.signatures = signatureContents;
+  cspmap_t denialMap;
+  denialMap[std::make_pair(DNSName("a.powerdns.com."), QType::NSEC)] = pair;
+
+  dState denialState = getDenial(denialMap, DNSName("b.powerdns.com."), QType::A, false, false);
+  BOOST_CHECK_EQUAL(denialState, NODATA);
+}
+
+BOOST_AUTO_TEST_CASE(test_nsec3_nxdomain_denial_missing_wildcard) {
+  init();
+
+  testkeysset_t keys;
+  generateKeyMaterial(DNSName("powerdns.com."), DNSSECKeeper::ECDSA256, DNSSECKeeper::SHA256, keys);
+
+  vector<DNSRecord> records;
+
+  vector<shared_ptr<DNSRecordContent>> recordContents;
+  vector<shared_ptr<RRSIGRecordContent>> signatureContents;
+
+  addNSEC3NarrowRecordToLW(DNSName("a.powerdns.com."), DNSName("powerdns.com."), { QType::A, QType::TXT, QType::RRSIG, QType::NSEC }, 600, records);
+  recordContents.push_back(records.at(0).d_content);
+  addRRSIG(keys, records, DNSName("powerdns.com."), 300);
+  signatureContents.push_back(getRR<RRSIGRecordContent>(records.at(1)));
+
+  ContentSigPair pair;
+  pair.records = recordContents;
+  pair.signatures = signatureContents;
+  cspmap_t denialMap;
+  denialMap[std::make_pair(records.at(0).d_name, records.at(0).d_type)] = pair;
+
+  /* Add NSEC3 for the closest encloser */
+  recordContents.clear();
+  signatureContents.clear();
+  records.clear();
+  addNSEC3UnhashedRecordToLW(DNSName("powerdns.com."), DNSName("powerdns.com."), "whatever", { QType::A, QType::TXT, QType::RRSIG, QType::NSEC }, 600, records);
+  recordContents.push_back(records.at(0).d_content);
+  addRRSIG(keys, records, DNSName("powerdns.com."), 300);
+  signatureContents.push_back(getRR<RRSIGRecordContent>(records.at(1)));
+
+  pair.records = recordContents;
+  pair.signatures = signatureContents;
+  denialMap[std::make_pair(records.at(0).d_name, records.at(0).d_type)] = pair;
+
+  dState denialState = getDenial(denialMap, DNSName("b.powerdns.com."), QType::A, false, false);
+  BOOST_CHECK_EQUAL(denialState, NODATA);
+}
+
 BOOST_AUTO_TEST_CASE(test_nsec_ent_denial) {
   init();
 
@@ -7241,18 +7830,34 @@ BOOST_AUTO_TEST_CASE(test_nsec_ent_denial) {
   cspmap_t denialMap;
   denialMap[std::make_pair(DNSName("a.powerdns.com."), QType::NSEC)] = pair;
 
-  /* this NSEC is not valid to prove a NXQTYPE at b.powerdns.com */
-  dState denialState = getDenial(denialMap, DNSName("b.powerdns.com."), QType::AAAA, true, true);
-  BOOST_CHECK_EQUAL(denialState, NXDOMAIN);
-
-  /* it is valid to prove a NXQTYPE at c.powerdns.com because it proves that
+  /* this NSEC is valid to prove a NXQTYPE at c.powerdns.com because it proves that
      it is an ENT */
-  denialState = getDenial(denialMap, DNSName("c.powerdns.com."), QType::AAAA, true, true);
+  dState denialState = getDenial(denialMap, DNSName("c.powerdns.com."), QType::AAAA, true, true);
   BOOST_CHECK_EQUAL(denialState, NXQTYPE);
 
+  /* this NSEC is not valid to prove a NXQTYPE at b.powerdns.com,
+     it could prove a NXDOMAIN if it had an additional wildcard denial */
+  denialState = getDenial(denialMap, DNSName("b.powerdns.com."), QType::AAAA, true, true);
+  BOOST_CHECK_EQUAL(denialState, NODATA);
+
   /* this NSEC is not valid to prove a NXQTYPE for QType::A at a.c.powerdns.com either */
   denialState = getDenial(denialMap, DNSName("a.c.powerdns.com."), QType::A, true, true);
   BOOST_CHECK_EQUAL(denialState, NODATA);
+
+  /* if we add the wildcard denial proof, we should get a NXDOMAIN proof for b.powerdns.com */
+  recordContents.clear();
+  signatureContents.clear();
+  addNSECRecordToLW(DNSName(").powerdns.com."), DNSName("+.powerdns.com."), { }, 600, records);
+  recordContents.push_back(records.at(0).d_content);
+  addRRSIG(keys, records, DNSName("powerdns.com."), 300);
+  signatureContents.push_back(getRR<RRSIGRecordContent>(records.at(1)));
+  records.clear();
+  pair.records = recordContents;
+  pair.signatures = signatureContents;
+  denialMap[std::make_pair(DNSName(").powerdns.com."), QType::NSEC)] = pair;
+
+  denialState = getDenial(denialMap, DNSName("b.powerdns.com."), QType::A, true, true);
+  BOOST_CHECK_EQUAL(denialState, NXDOMAIN);
 }
 
 BOOST_AUTO_TEST_CASE(test_nsec3_ancestor_nxqtype_denial) {
@@ -7272,7 +7877,7 @@ BOOST_AUTO_TEST_CASE(test_nsec3_ancestor_nxqtype_denial) {
     signer field that is shorter than the owner name of the NSEC RR) it can't
     be used to deny anything except the whole name or a DS.
   */
-  addNSEC3UnhashedRecordToLW(DNSName("a."), "whatever", { QType::NS }, 600, records);
+  addNSEC3UnhashedRecordToLW(DNSName("a."), DNSName("."), "whatever", { QType::NS }, 600, records);
   recordContents.push_back(records.at(0).d_content);
   addRRSIG(keys, records, DNSName("."), 300);
   signatureContents.push_back(getRR<RRSIGRecordContent>(records.at(1)));
@@ -7323,7 +7928,7 @@ BOOST_AUTO_TEST_CASE(test_nsec3_insecure_delegation_denial) {
     NS should be set if it was proving an insecure delegation, let's check that
     we correctly detect that it's not.
   */
-  addNSEC3UnhashedRecordToLW(DNSName("a."), "whatever", { }, 600, records);
+  addNSEC3UnhashedRecordToLW(DNSName("a."), DNSName("."), "whatever", { }, 600, records);
   recordContents.push_back(records.at(0).d_content);
   addRRSIG(keys, records, DNSName("."), 300);
   signatureContents.push_back(getRR<RRSIGRecordContent>(records.at(1)));
index 55d5d57a36cb9bc94b7ae4bff8baa0e87ef5243f..8611d5e2759ae7f2d8cb6a7a8928dc92888b44d1 100644 (file)
@@ -1821,6 +1821,9 @@ RCode::rcodes_ SyncRes::updateCacheFromRecords(unsigned int depth, LWResult& lwr
       isCNAMEAnswer = true;
     }
 
+    /* if we have a positive answer synthetized from a wildcard,
+       we need to store the corresponding NSEC/NSEC3 records proving
+       that the exact name did not exist in the negative cache */
     if(needWildcardProof) {
       if (nsecTypes.count(rec.d_type)) {
         authorityRecs.push_back(std::make_shared<DNSRecord>(rec));
@@ -1837,10 +1840,10 @@ RCode::rcodes_ SyncRes::updateCacheFromRecords(unsigned int depth, LWResult& lwr
       if (rrsig) {
         /* As illustrated in rfc4035's Appendix B.6, the RRSIG label
            count can be lower than the name's label count if it was
-           synthesized from the wildcard. Note that the difference might
+           synthetized from the wildcard. Note that the difference might
            be > 1. */
         if (rec.d_name == qname && rrsig->d_labels < labelCount) {
-          LOG(prefix<<qname<<": RRSIG indicates the name was expanded from a wildcard, we need a wildcard proof"<<endl);
+          LOG(prefix<<qname<<": RRSIG indicates the name was synthetized from a wildcard, we need a wildcard proof"<<endl);
           needWildcardProof = true;
         }
 
@@ -2049,7 +2052,9 @@ bool SyncRes::processRecords(const std::string& prefix, const DNSName& qname, co
       NegCache::NegCacheEntry ne;
 
       uint32_t lowestTTL = rec.d_ttl;
-      ne.d_name = qname;
+      /* if we get an NXDomain answer with a CNAME, the name
+         does exist but the target does not */
+      ne.d_name = newtarget.empty() ? qname : newtarget;
       ne.d_qtype = QType(0); // this encodes 'whole record'
       ne.d_auth = rec.d_name;
       harvestNXRecords(lwr.d_records, ne, d_now.tv_sec, &lowestTTL);
@@ -2063,7 +2068,12 @@ bool SyncRes::processRecords(const std::string& prefix, const DNSName& qname, co
         ne.d_validationState = state;
       }
 
-      if(!wasVariable()) {
+      /* if we get an NXDomain answer with a CNAME, let's not cache the
+         target, even the server was authoritative for it,
+         and do an additional query for the CNAME target.
+         We have a regression test making sure we do exactly that.
+      */
+      if(!wasVariable() && newtarget.empty()) {
         t_sstorage.negcache.add(ne);
         if(s_rootNXTrust && ne.d_auth.isRoot() && auth.isRoot()) {
           ne.d_name = ne.d_name.getLastLabel();
@@ -2079,6 +2089,9 @@ bool SyncRes::processRecords(const std::string& prefix, const DNSName& qname, co
         newtarget=content->getTarget();
       }
     }
+    /* if we have a positive answer synthetized from a wildcard, we need to
+       return the corresponding NSEC/NSEC3 records from the AUTHORITY section
+       proving that the exact name did not exist */
     else if(needWildcardProof && (rec.d_type==QType::RRSIG || rec.d_type==QType::NSEC || rec.d_type==QType::NSEC3) && rec.d_place==DNSResourceRecord::AUTHORITY) {
       ret.push_back(rec); // enjoy your DNSSEC
     }
@@ -2093,6 +2106,28 @@ bool SyncRes::processRecords(const std::string& prefix, const DNSName& qname, co
 
       done=true;
       ret.push_back(rec);
+
+      if (state == Secure && needWildcardProof) {
+        /* We have a positive answer synthetized from a wildcard, we need to check that we have
+           proof that the exact name doesn't exist so the wildcard can be used,
+           as described in section 5.3.4 of RFC 4035 and 5.3 of FRC 7129.
+        */
+        NegCache::NegCacheEntry ne;
+
+        uint32_t lowestTTL = rec.d_ttl;
+        ne.d_name = qname;
+        ne.d_qtype = QType(0); // this encodes 'whole record'
+        harvestNXRecords(lwr.d_records, ne, d_now.tv_sec, &lowestTTL);
+
+        cspmap_t csp = harvestCSPFromNE(ne);
+        dState res = getDenial(csp, qname, ne.d_qtype.getCode(), false, false, false);
+        if (res != NXDOMAIN) {
+          LOG(d_prefix<<"Invalid denial in wildcard expanded positive response found for "<<qname<<", returning Bogus, res="<<res<<endl);
+          updateValidationState(state, Bogus);
+          /* we already store the record with a different validation status, let's fix it */
+          t_RC->updateValidationStatus(d_now.tv_sec, qname, qtype, d_incomingECSFound ? d_incomingECSNetwork : d_requestor, lwr.d_aabit, Bogus);
+        }
+      }
     }
     else if((rec.d_type==QType::RRSIG || rec.d_type==QType::NSEC || rec.d_type==QType::NSEC3) && rec.d_place==DNSResourceRecord::ANSWER) {
       if(rec.d_type != QType::RRSIG || rec.d_name == qname)
index 771bfb84bd448a320a5f8cb425ed5e1394bbd8e6..c95e8fde6bc08ced9b7e33dc348b202fe6373e7f 100644 (file)
@@ -868,4 +868,22 @@ BOOST_AUTO_TEST_CASE(test_getlastlabel) {
   // Check if the last label is indeed returned
   BOOST_CHECK_EQUAL(ans, DNSName("com"));
 }
+
+BOOST_AUTO_TEST_CASE(test_getcommonlabels) {
+  const DNSName name1("www.powerdns.com");
+  const DNSName name2("a.long.list.of.labels.powerdns.com");
+
+  BOOST_CHECK_EQUAL(name1.getCommonLabels(name1), name1);
+  BOOST_CHECK_EQUAL(name2.getCommonLabels(name2), name2);
+
+  BOOST_CHECK_EQUAL(name1.getCommonLabels(name2), DNSName("powerdns.com"));
+  BOOST_CHECK_EQUAL(name2.getCommonLabels(name1), DNSName("powerdns.com"));
+
+  const DNSName name3("www.powerdns.org");
+  BOOST_CHECK_EQUAL(name1.getCommonLabels(name3), DNSName());
+  BOOST_CHECK_EQUAL(name2.getCommonLabels(name3), DNSName());
+  BOOST_CHECK_EQUAL(name3.getCommonLabels(name1), DNSName());
+  BOOST_CHECK_EQUAL(name3.getCommonLabels(name2), DNSName());
+}
+
 BOOST_AUTO_TEST_SUITE_END()
index 3df1c741288693dda47ad98ddace2c5de80e38df..4f49ae8f370d089024a81a6ecfdc56038587ffdc 100644 (file)
@@ -62,6 +62,12 @@ static std::string getHashFromNSEC3(const DNSName& qname, const std::shared_ptr<
   return hashQNameWithSalt(nsec3->d_salt, nsec3->d_iterations, qname);
 }
 
+/* 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)
 {
   for (const auto& record : dsrecords) {
@@ -104,8 +110,180 @@ bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>&
   return false;
 }
 
-dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof)
+/* RFC 4035 section-5.3.4:
+   "If the number of labels in an RRset's owner name is greater than the
+   Labels field of the covering RRSIG RR, then the RRset and its
+   covering RRSIG RR were created as a result of wildcard expansion."
+*/
+static bool isWildcardExpanded(const DNSName& owner, const std::vector<std::shared_ptr<RRSIGRecordContent> >& signatures)
 {
+  if (signatures.empty()) {
+    return false;
+  }
+
+  const auto& sign = signatures.at(0);
+  unsigned int labelsCount = owner.countLabels();
+  if (sign && sign->d_labels < labelsCount) {
+    return true;
+  }
+
+  return false;
+}
+
+/* if this is a wildcard NSEC, the owner name has been modified
+   to match the name. Make sure we use the original '*' form. */
+static DNSName getNSECOwnerName(const DNSName& initialOwner, const std::vector<std::shared_ptr<RRSIGRecordContent> >& signatures)
+{
+  DNSName result = initialOwner;
+
+  if (signatures.empty()) {
+    return result;
+  }
+
+  const auto& sign = signatures.at(0);
+  unsigned int labelsCount = initialOwner.countLabels();
+  if (sign && sign->d_labels < labelsCount) {
+    do {
+      result.chopOff();
+      labelsCount--;
+    }
+    while (sign->d_labels < labelsCount);
+
+    result = g_wildcarddnsname + result;
+  }
+
+  return result;
+}
+
+/*
+  This function checks whether the existence of a wildcard covering qname|qtype
+  is proven by the NSEC records in validrrsets.
+  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 provesNoWildCard(const DNSName& qname, const uint16_t qtype, const cspmap_t & validrrsets, bool* wildcardExists=nullptr)
+{
+  LOG("Trying to prove that there is no wildcard for "<<qname<<"/"<<QType(qtype).getName()<<endl);
+  for (const auto& v : validrrsets) {
+    LOG("Do have: "<<v.first.first<<"/"<<DNSRecordContent::NumberToType(v.first.second)<<endl);
+    if (v.first.second == QType::NSEC) {
+      for (const auto& r : v.second.records) {
+        LOG("\t"<<r->getZoneRepresentation()<<endl);
+        auto nsec = std::dynamic_pointer_cast<NSECRecordContent>(r);
+        if (!nsec) {
+          continue;
+        }
+
+        const DNSName owner = getNSECOwnerName(v.first.first, v.second.signatures);
+        /*
+          A NSEC can only prove the non-existence of a wildcard with at least the same
+          number of labels than the intersection of its owner name and next name.
+        */
+        const DNSName commonLabels = owner.getCommonLabels(nsec->d_next);
+        unsigned int commonLabelsCount = commonLabels.countLabels();
+
+        DNSName wildcard(qname);
+        unsigned int wildcardLabelsCount = wildcard.countLabels();
+        while (wildcard.chopOff() && wildcardLabelsCount >= commonLabelsCount) {
+          DNSName target = g_wildcarddnsname + wildcard;
+
+          if (owner == target) {
+            LOG("\tWildcard matches");
+            if (wildcardExists) {
+              *wildcardExists = true;
+            }
+            if (qtype == 0 || !nsec->d_set.count(qtype)) {
+              LOG(" and proves that the type did not exist"<<endl);
+              return true;
+            }
+            LOG(" BUT the type did exist!"<<endl);
+            return false;
+          }
+
+          if (isCoveredByNSEC(g_wildcarddnsname + wildcard, owner, nsec->d_next)) {
+            LOG("\tWildcard is covered"<<endl);
+            return true;
+          }
+        }
+      }
+    }
+  }
+
+  return false;
+}
+
+/*
+  This function checks whether the existence of a wildcard covering qname|qtype
+  is proven by the NSEC3 records in validrrsets.
+  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(DNSName wildcard, uint16_t const qtype, const cspmap_t & validrrsets, bool * wildcardExists=nullptr)
+{
+  wildcard = g_wildcarddnsname + wildcard;
+  LOG("Trying to prove that there is no wildcard for "<<wildcard<<"/"<<QType(qtype).getName()<<endl);
+
+  for (const auto& v : validrrsets) {
+    LOG("Do have: "<<v.first.first<<"/"<<DNSRecordContent::NumberToType(v.first.second)<<endl);
+    if (v.first.second == QType::NSEC3) {
+      for (const auto& r : v.second.records) {
+        LOG("\t"<<r->getZoneRepresentation()<<endl);
+        auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(r);
+        if (!nsec3) {
+          continue;
+        }
+
+        const DNSName signer = getSigner(v.second.signatures);
+        if (!v.first.first.isPartOf(signer))
+          continue;
+
+        string h = getHashFromNSEC3(wildcard, nsec3);
+        if (h.empty()) {
+          return false;
+        }
+        LOG("\tWildcard hash: "<<toBase32Hex(h)<<endl);
+        string beginHash=fromBase32Hex(v.first.first.getRawLabels()[0]);
+        LOG("\tNSEC3 hash: "<<toBase32Hex(beginHash)<<" -> "<<toBase32Hex(nsec3->d_nexthash)<<endl);
+
+        if (beginHash == h) {
+          LOG("\tWildcard hash matches");
+          if (wildcardExists) {
+            *wildcardExists = true;
+          }
+          if (qtype == 0 || !nsec3->d_set.count(qtype)) {
+            LOG(" and proves that the type did not exist"<<endl);
+            return true;
+          }
+          LOG(" BUT the type did exist!"<<endl);
+          return false;
+        }
+
+        if (isCoveredByNSEC3Hash(h, beginHash, nsec3->d_nexthash)) {
+          LOG("\tWildcard hash is covered"<<endl);
+          return true;
+        }
+      }
+    }
+  }
+
+  return false;
+}
+
+/*
+  This function checks whether the existence of qname|qtype is denied by the NSEC and NSEC3
+  in validrrsets.
+  - If `referralToUnsigned` is true and qtype is QType::DS, this functions returns Insecure
+  if a NSEC or NSEC3 proves that the name exists but no NS type exists, as specified in RFC 5155 section 8.9.
+  - If `wantsNoDataProof` is set but a NSEC proves that the whole name does not exist, the function will return
+  NXQTYPE is the name is proven to be ENT and NXDOMAIN otherwise.
+  - If `needWildcardProof` is false, the proof that a wildcard covering this qname|qtype is not checked. It is
+  useful when we have a positive answer synthetized from a wildcard and we only need to prove that the exact
+  name does not exist.
+*/
+dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, bool needWildcardProof)
+{
+  bool nsec3Seen = false;
+
   for(const auto& v : validrrsets) {
     LOG("Do have: "<<v.first.first<<"/"<<DNSRecordContent::NumberToType(v.first.second)<<endl);
 
@@ -145,6 +323,12 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
 
           LOG("Denies existence of type "<<QType(qtype).getName()<<endl);
 
+          /* RFC 6840 section 4.3 */
+          if (nsec->d_set.count(QType::CNAME)) {
+            LOG("However a CNAME exists"<<endl);
+            return NODATA;
+          }
+
           /*
            * RFC 4035 Section 2.3:
            * The bitmap for the NSEC RR at a delegation point requires special
@@ -156,19 +340,39 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
             return INSECURE;
           }
 
-          return NXQTYPE;
+          /* we know that the name exists (but this qtype doesn't) so except
+             if the answer was generated by a wildcard expansion, no wildcard
+             could have matched (rfc4035 section 5.4 bullet 1) */
+          if (!isWildcardExpanded(v.first.first, v.second.signatures)) {
+            needWildcardProof = false;
+          }
+
+          if (!needWildcardProof || provesNoWildCard(qname, qtype, validrrsets)) {
+            return NXQTYPE;
+          }
+
+          LOG("But the existence of a wildcard is not denied for "<<qname<<"/"<<endl);
+          return NODATA;
         }
 
         /* check if the whole NAME is denied existing */
         if(isCoveredByNSEC(qname, v.first.first, nsec->d_next)) {
-
+          /* if the name is an ENT and we received a NODATA answer,
+             we are fine with a NSEC proving that the name does not exist. */
           if (wantsNoDataProof && nsecProvesENT(qname, v.first.first, nsec->d_next)) {
             LOG("Denies existence of type "<<qname<<"/"<<QType(qtype).getName()<<" by proving that "<<qname<<" is an ENT"<<endl);
             return NXQTYPE;
           }
 
-          LOG("Denies existence of name "<<qname<<"/"<<QType(qtype).getName()<<endl);
-          return NXDOMAIN;
+          bool wildcardExists = false;
+          if (!needWildcardProof || provesNoWildCard(qname, qtype, validrrsets, &wildcardExists)) {
+            if (wildcardExists) {
+              return NXQTYPE;
+            }
+            return NXDOMAIN;
+          }
+          LOG("But the existence of a wildcard is not denied for "<<qname<<"/"<<QType(qtype).getName()<<endl);
+          return NODATA;
         }
 
         LOG("Did not deny existence of "<<QType(qtype).getName()<<", "<<v.first.first<<"?="<<qname<<", "<<nsec->d_set.count(qtype)<<", next: "<<nsec->d_next<<endl);
@@ -181,14 +385,19 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
           continue;
 
         const DNSName signer = getSigner(v.second.signatures);
-        if (!v.first.first.isPartOf(signer))
+        if (!v.first.first.isPartOf(signer)) {
+          LOG("Owner "<<v.first.first<<" is not part of the signer "<<signer<<", ignoring"<<endl);
           continue;
+        }
 
         string h = getHashFromNSEC3(qname, nsec3);
         if (h.empty()) {
+          LOG("Unsupported hash, ignoring"<<endl);
           return INSECURE;
         }
 
+        nsec3Seen = true;
+
         //              cerr<<"Salt length: "<<nsec3->d_salt.length()<<", iterations: "<<nsec3->d_iterations<<", hashed: "<<qname<<endl;
         LOG("\tquery hash: "<<toBase32Hex(h)<<endl);
         string beginHash=fromBase32Hex(v.first.first.getRawLabels()[0]);
@@ -217,6 +426,13 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
           }
 
           LOG("Denies existence of type "<<QType(qtype).getName()<<" for name "<<qname<<" (not opt-out)."<<endl);
+
+          /* RFC 6840 section 4.3 */
+          if (nsec3->d_set.count(QType::CNAME)) {
+            LOG("However a CNAME exists"<<endl);
+            return NODATA;
+          }
+
           /*
            * RFC 5155 section 8.9:
            * If there is an NSEC3 RR present in the response that matches the
@@ -228,66 +444,78 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
             LOG("However, no NS record exists at this level!"<<endl);
             return INSECURE;
           }
-          LOG(endl);
-          return NXQTYPE;
-        }
 
-        /* check if the whole NAME does not exist */
-        if(isCoveredByNSEC3Hash(h, beginHash, nsec3->d_nexthash)) {
-          LOG("Denies existence of name "<<qname<<"/"<<QType(qtype).getName());
-          if (qtype == QType::DS && nsec3->d_flags & 1) {
-            LOG(" but is opt-out!"<<endl);
-            return OPTOUT;
-          }
-          LOG(endl);
-          return NXDOMAIN;
+          return NXQTYPE;
         }
-
-        LOG("Did not cover us ("<<qname<<"), start="<<v.first.first<<", us="<<toBase32Hex(h)<<", end="<<toBase32Hex(nsec3->d_nexthash)<<endl);
       }
     }
   }
 
-  /* check closest encloser */
-  LOG("Now looking for the closest encloser for "<<qname<<endl);
-  DNSName sname(qname);
+  /* if we have no NSEC3 records, we are done */
+  if (!nsec3Seen) {
+    return NODATA;
+  }
+
+  DNSName closestEncloser(qname);
   bool found = false;
 
-  while (found == false && sname.chopOff()) {
-    for(const auto& v : validrrsets) {
-      if(v.first.second==QType::NSEC3) {
-        for(const auto& r : v.second.records) {
-          LOG("\t"<<r->getZoneRepresentation()<<endl);
-          auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(r);
-          if(!nsec3)
-            continue;
+  if (needWildcardProof) {
+    /* We now need to look for a NSEC3 covering the closest (provable) encloser
+       RFC 5155 section-7.2.1
+       FRC 7129 section-5.5
+    */
+    LOG("Now looking for the closest encloser for "<<qname<<endl);
 
-          if (g_maxNSEC3Iterations && nsec3->d_iterations > g_maxNSEC3Iterations) {
-            return INSECURE;
-          }
+    while (found == false && closestEncloser.chopOff()) {
+      for(const auto& v : validrrsets) {
+        if(v.first.second==QType::NSEC3) {
+          for(const auto& r : v.second.records) {
+            LOG("\t"<<r->getZoneRepresentation()<<endl);
+            auto nsec3 = std::dynamic_pointer_cast<NSEC3RecordContent>(r);
+            if(!nsec3)
+              continue;
 
-          string h = hashQNameWithSalt(nsec3->d_salt, nsec3->d_iterations, sname);
-          string beginHash=fromBase32Hex(v.first.first.getRawLabels()[0]);
+            if (g_maxNSEC3Iterations && nsec3->d_iterations > g_maxNSEC3Iterations) {
+              return INSECURE;
+            }
+
+            string h = hashQNameWithSalt(nsec3->d_salt, nsec3->d_iterations, closestEncloser);
+            string beginHash=fromBase32Hex(v.first.first.getRawLabels()[0]);
 
-          LOG("Comparing "<<toBase32Hex(h)<<" against "<<toBase32Hex(beginHash)<<endl);
-          if(beginHash == h) {
-            LOG("Closest encloser for "<<qname<<" is "<<sname<<endl);
-            found = true;
-            break;
+            LOG("Comparing "<<toBase32Hex(h)<<" ("<<closestEncloser<<") against "<<toBase32Hex(beginHash)<<endl);
+            if(beginHash == h) {
+              LOG("Closest encloser for "<<qname<<" is "<<closestEncloser<<endl);
+              found = true;
+              break;
+            }
           }
         }
-      }
-      if (found == true) {
-        break;
+        if (found == true) {
+          break;
+        }
       }
     }
   }
+  else {
+    /* RFC 5155 section-7.2.6:
+       "It is not necessary to return an NSEC3 RR that matches the closest encloser,
+       as the existence of this closest encloser is proven by the presence of the
+       expanded wildcard in the response.
+    */
+    found = true;
+    closestEncloser.chopOff();
+  }
+
+  bool nextCloserFound = false;
+  bool isOptOut = false;
 
   if (found == true) {
-    /* we now need a NSEC3 RR covering the next closer name */
-    unsigned int labelIdx = qname.countLabels() - sname.countLabels();
+    /* now that we have found the closest (provable) encloser,
+       we can construct the next closer (FRC7129 section-5.5) name
+       and look for a NSEC3 RR covering it */
+    unsigned int labelIdx = qname.countLabels() - closestEncloser.countLabels();
     if (labelIdx >= 1) {
-      DNSName nextCloser(sname);
+      DNSName nextCloser(closestEncloser);
       nextCloser.prependRawLabel(qname.getRawLabel(labelIdx - 1));
       LOG("Looking for a NSEC3 covering the next closer name "<<nextCloser<<endl);
 
@@ -305,19 +533,46 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
             string h = hashQNameWithSalt(nsec3->d_salt, nsec3->d_iterations, nextCloser);
             string beginHash=fromBase32Hex(v.first.first.getRawLabels()[0]);
 
-            LOG("Comparing "<<toBase32Hex(h)<<" against "<<toBase32Hex(beginHash)<<endl);
+            LOG("Comparing "<<toBase32Hex(h)<<" against "<<toBase32Hex(beginHash)<<" -> "<<toBase32Hex(nsec3->d_nexthash)<<endl);
             if(isCoveredByNSEC3Hash(h, beginHash, nsec3->d_nexthash)) {
               LOG("Denies existence of name "<<qname<<"/"<<QType(qtype).getName());
+              nextCloserFound = true;
+
               if (qtype == QType::DS && nsec3->d_flags & 1) {
-                LOG(" but is opt-out!"<<endl);
-                return OPTOUT;
+                LOG(" but is opt-out!");
+                isOptOut = true;
               }
               LOG(endl);
-              return NXDOMAIN;
+              break;
             }
+            LOG("Did not cover us ("<<qname<<"), start="<<v.first.first<<", us="<<toBase32Hex(h)<<", end="<<toBase32Hex(nsec3->d_nexthash)<<endl);
           }
         }
+        if (nextCloserFound) {
+          break;
+        }
+      }
+    }
+  }
+
+  if (nextCloserFound) {
+    bool wildcardExists = false;
+    /* RFC 7129 section-5.6 */
+    if (needWildcardProof && !provesNSEC3NoWildCard(closestEncloser, qtype, validrrsets, &wildcardExists)) {
+      if (!isOptOut) {
+        LOG("But the existence of a wildcard is not denied for "<<qname<<"/"<<QType(qtype).getName()<<endl);
+        return NODATA;
+      }
+    }
+
+    if (isOptOut) {
+      return OPTOUT;
+    }
+    else {
+      if (wildcardExists) {
+        return NXQTYPE;
       }
+      return NXDOMAIN;
     }
   }
 
@@ -770,7 +1025,9 @@ bool isSupportedDS(const DSRecordContent& ds)
 DNSName getSigner(const std::vector<std::shared_ptr<RRSIGRecordContent> >& signatures)
 {
   for (const auto sig : signatures) {
-    return sig->d_signer;
+    if (sig) {
+      return sig->d_signer;
+    }
   }
 
   return DNSName();
index b9b0f44447b8b3b6468582b1f69d5cb1dc0b4d71..1c647c8792b41fc6ac3ec54be8501b6d32965bf4 100644 (file)
@@ -71,7 +71,7 @@ vState getKeysFor(DNSRecordOracle& dro, const DNSName& zone, skeyset_t& keyset);
 bool getTrustAnchor(const map<DNSName,dsmap_t>& anchors, const DNSName& zone, dsmap_t &res);
 bool haveNegativeTrustAnchor(const map<DNSName,std::string>& negAnchors, const DNSName& zone, std::string& reason);
 void validateDNSKeysAgainstDS(time_t now, const DNSName& zone, const dsmap_t& dsmap, const skeyset_t& tkeys, vector<shared_ptr<DNSRecordContent> >& toSign, const vector<shared_ptr<RRSIGRecordContent> >& sigs, skeyset_t& validkeys);
-dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof);
+dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16_t qtype, bool referralToUnsigned, bool wantsNoDataProof, bool needsWildcardProof=true);
 bool isSupportedDS(const DSRecordContent& ds);
 DNSName getSigner(const std::vector<std::shared_ptr<RRSIGRecordContent> >& signatures);
 bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>& dsrecords);