]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
rec: Better aggressive NSEC/NSEC3. Needs tests, refactoring, perhaps wildcard synth
authorRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 30 Dec 2020 17:27:17 +0000 (18:27 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 22 Feb 2021 17:42:04 +0000 (18:42 +0100)
pdns/dnsrecords.hh
pdns/recursordist/aggressive_nsec.cc
pdns/recursordist/test-syncres_cc8.cc
pdns/syncres.cc
pdns/validate.cc
pdns/validate.hh

index 551d421b30a69e13aca0339bf8d18f52346eb68e..588a96670a19199e3fd8b70d31df37a8d3c4535f 100644 (file)
@@ -726,6 +726,10 @@ public:
   {
     return d_bitmap.count();
   }
+  bool isOptOut() const
+  {
+    return d_flags & 1;
+  }
 
 private:
   NSECBitmap d_bitmap;
index 068ad27cb5a8ef721d2180441fda8061aa5b5b6a..984b627e8b77f9faccad81ea19d2d5eacd8a5575 100644 (file)
@@ -71,6 +71,10 @@ std::shared_ptr<AggressiveNSECCache::ZoneEntry> AggressiveNSECCache::getZone(con
 
 void AggressiveNSECCache::insertNSEC(const DNSName& zone, const DNSName& owner, const DNSRecord& record, const std::vector<std::shared_ptr<RRSIGRecordContent>>& signatures, bool nsec3)
 {
+  if (signatures.empty()) {
+    return;
+  }
+
   std::shared_ptr<AggressiveNSECCache::ZoneEntry> entry = getZone(zone);
   {
     std::lock_guard<std::mutex> lock(entry->d_lock);
@@ -86,23 +90,41 @@ void AggressiveNSECCache::insertNSEC(const DNSName& zone, const DNSName& owner,
         throw std::runtime_error("Error getting the content from a NSEC record");
       }
       next = content->d_next;
+      if (next.canonCompare(owner) && next != zone) {
+        /* not accepting a NSEC whose next domain name is before the owner
+           unless the next domain name is the apex, sorry */
+        return;
+      }
     }
     else {
       auto content = getRR<NSEC3RecordContent>(record);
       if (!content) {
         throw std::runtime_error("Error getting the content from a NSEC3 record");
       }
-      next = DNSName(toBase32Hex(content->d_nexthash)) + zone;
+
+      if (content->isOptOut()) {
+        /* doesn't prove anything, sorry */
+        return;
+      }
+
       if (g_maxNSEC3Iterations && content->d_iterations > g_maxNSEC3Iterations) {
+        /* can't use that */
         return;
       }
 
+      next = DNSName(toBase32Hex(content->d_nexthash)) + zone;
       entry->d_iterations = content->d_iterations;
       entry->d_salt = content->d_salt;
     }
 
     /* the TTL is already a TTD by now */
-    entry->d_entries.insert({record.d_content, signatures, owner, std::move(next), record.d_ttl});
+    if (!nsec3 && isWildcardExpanded(owner.countLabels(), signatures.at(0))) {
+      DNSName realOwner = getNSECOwnerName(owner, signatures);
+      entry->d_entries.insert({record.d_content, signatures, std::move(realOwner), std::move(next), record.d_ttl});
+    }
+    else {
+      entry->d_entries.insert({record.d_content, signatures, owner, std::move(next), record.d_ttl});
+    }
   }
 }
 
@@ -274,12 +296,24 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<AggressiveN
       return false;
     }
 
+    const DNSName signer = getSigner(exactNSEC3.d_signatures);
+    if (type != QType::DS && isNSEC3AncestorDelegation(signer, exactNSEC3.d_owner, nsec3)) {
+      /* RFC 6840 section 4.1 "Clarifications on Nonexistence Proofs":
+         Ancestor delegation NSEC or NSEC3 RRs MUST NOT be used to assume
+         nonexistence of any RRs below that zone cut, which include all RRs at
+         that (original) owner name other than DS RRs, and all RRs below that
+         owner name regardless of type.
+      */
+      return false;
+    }
+
     cerr<<"Direct match, done!"<<endl;
+    res = RCode::NoError;
     addToRRSet(now, soaSet, soaSignatures, zoneEntry->d_zone, doDNSSEC, ret);
-    addRecordToRRSet(now, exactNSEC3.d_owner, QType::NSEC, exactNSEC3.d_ttd - now, exactNSEC3.d_record, exactNSEC3.d_signatures, doDNSSEC, ret);
+    addRecordToRRSet(now, exactNSEC3.d_owner, QType::NSEC3, exactNSEC3.d_ttd - now, exactNSEC3.d_record, exactNSEC3.d_signatures, doDNSSEC, ret);
     return true;
   }
-#warning FIXME opt-out / ENT
+
 #warning FIXME ancestor delegation
   cerr<<"no direct match, looking for closest encloser"<<endl;
   DNSName closestEncloser(name);
@@ -346,12 +380,15 @@ bool AggressiveNSECCache::getNSEC3Denial(time_t now, std::shared_ptr<AggressiveN
     if (nsec3->isSet(QType::CNAME)) {
       return false;
     }
+
+    res = RCode::NoError;
   }
   else {
     if (!isCoveredByNSEC3Hash(DNSName(wcHash) + zone, wcEntry.d_owner, wcEntry.d_next)) {
       cerr<<"no covering record found for the wildcard in aggressive cache"<<endl;
       return false;
     }
+    res = RCode::NXDomain;
   }
 
   addToRRSet(now, soaSet, soaSignatures, zoneEntry->d_zone, doDNSSEC, ret);
@@ -436,8 +473,26 @@ bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType
       res = RCode::NXDomain;
     }
     else {
+      auto nsecContent = std::dynamic_pointer_cast<NSECRecordContent>(wcEntry.d_record);
+      denial = matchesNSEC(wc, type.getCode(), wcEntry.d_owner, nsecContent, wcEntry.d_signatures);
+      if (denial == dState::NODENIAL) {
+        /* too complicated for now */
+        return false;
+      }
+      else if (denial == dState::NXQTYPE) {
+        covered = true;
+        res = RCode::NoError;
+      }
+      else if (denial == dState::NXDOMAIN) {
+        covered = true;
+        res = RCode::NXDomain;
+      }
+
+      if (wcEntry.d_owner != wc) {
+        needWildcard = true;
+      }
+#if 0
       if (wcEntry.d_owner == wc) {
-        auto nsecContent = std::dynamic_pointer_cast<NSECRecordContent>(wcEntry.d_record);
         if (!nsecContent) {
           return false;
         }
@@ -459,6 +514,7 @@ bool AggressiveNSECCache::getDenial(time_t now, const DNSName& name, const QType
         res = RCode::NXDomain;
         needWildcard = true;
       }
+#endif
     }
   }
 
index 1dd464c5e7a1f268050b2df457257f2c38124dca..efc33e9a28ee3bf0802d1b17647831c8db31ac9e 100644 (file)
@@ -421,7 +421,151 @@ BOOST_AUTO_TEST_CASE(test_nsec3_nxdomain_denial_missing_wildcard)
   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);
+  dState denialState = getDenial(denialMap, DNSName("a.powerdns.com."), QType::A, false, false);
+  BOOST_CHECK_EQUAL(denialState, dState::NODENIAL);
+}
+
+BOOST_AUTO_TEST_CASE(test_nsec_wildcard_with_cname)
+{
+  initSR();
+
+  testkeysset_t keys;
+  generateKeyMaterial(DNSName("example.org."), DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys);
+
+  vector<DNSRecord> records;
+
+  sortedRecords_t recordContents;
+  vector<shared_ptr<RRSIGRecordContent>> signatureContents;
+
+  /* proves that b.example.com does not exist */
+  addNSECRecordToLW(DNSName("a.example.org."), DNSName("d.example.org"), {QType::A, QType::TXT, QType::RRSIG, QType::NSEC}, 600, records);
+  recordContents.insert(records.at(0).d_content);
+  addRRSIG(keys, records, DNSName("example.org."), 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.example.org."), QType::NSEC)] = pair;
+
+  /* add a NSEC proving that a wildcard exists, without a CNAME type */
+  recordContents.clear();
+  signatureContents.clear();
+  addNSECRecordToLW(DNSName("*.example.org."), DNSName("+.example.org"), {QType::A, QType::TXT, QType::RRSIG, QType::NSEC }, 600, records);
+  recordContents.insert(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;
+
+  /* A does exist at the wildcard, AAAA does not */
+  dState denialState = getDenial(denialMap, DNSName("b.example.org."), QType::A, false, true);
+  BOOST_CHECK_EQUAL(denialState, dState::NODENIAL);
+
+  denialState = getDenial(denialMap, DNSName("b.example.org."), QType::AAAA, false, true);
+  BOOST_CHECK_EQUAL(denialState, dState::NXQTYPE);
+
+  /* now we replace the wildcard by one with a CNAME */
+  recordContents.clear();
+  signatureContents.clear();
+  addNSECRecordToLW(DNSName("*.example.org."), DNSName("+.example.org"), {QType::CNAME, QType::RRSIG, QType::NSEC }, 600, records);
+  recordContents.insert(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;
+
+  /* A and AAAA do not exist but we have a CNAME so at the wildcard */
+  denialState = getDenial(denialMap, DNSName("b.example.org."), QType::A, false, true);
+  BOOST_CHECK_EQUAL(denialState, dState::NODENIAL);
+
+  denialState = getDenial(denialMap, DNSName("b.example.org."), QType::AAAA, false, true);
+  BOOST_CHECK_EQUAL(denialState, dState::NODENIAL);
+}
+
+BOOST_AUTO_TEST_CASE(test_nsec3_wildcard_with_cname)
+{
+  initSR();
+
+  testkeysset_t keys;
+  generateKeyMaterial(DNSName("example.org."), DNSSECKeeper::ECDSA256, DNSSECKeeper::DIGEST_SHA256, keys);
+
+  vector<DNSRecord> records;
+
+  sortedRecords_t recordContents;
+  vector<shared_ptr<RRSIGRecordContent>> signatureContents;
+
+  /* proves that b.example.com does not exist */
+  addNSEC3NarrowRecordToLW(DNSName("b.example.org"), DNSName("example.org."), {QType::A, QType::TXT, QType::RRSIG, QType::NSEC3}, 600, records);
+  recordContents.insert(records.at(0).d_content);
+  addRRSIG(keys, records, DNSName("example.org."), 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("example.org."), DNSName("example.org."), "whatever", {QType::A, QType::TXT, QType::RRSIG, QType::NSEC}, 600, records);
+  recordContents.insert(records.at(0).d_content);
+  addRRSIG(keys, records, DNSName("example.org."), 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;
+
+  /* add wildcard, without a CNAME type */
+  recordContents.clear();
+  signatureContents.clear();
+  records.clear();
+  addNSEC3UnhashedRecordToLW(DNSName("*.example.org."), DNSName("example.org"), "whatever", {QType::A, QType::TXT, QType::RRSIG, QType::NSEC3}, 600, records);
+  recordContents.insert(records.at(0).d_content);
+  addRRSIG(keys, records, DNSName("example.org."), 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;
+
+  /* A does exist at the wildcard, AAAA does not */
+  dState denialState = getDenial(denialMap, DNSName("b.example.org."), QType::A, false, true);
+  BOOST_CHECK_EQUAL(denialState, dState::NODENIAL);
+
+  denialState = getDenial(denialMap, DNSName("b.example.org."), QType::AAAA, false, true);
+  BOOST_CHECK_EQUAL(denialState, dState::NXQTYPE);
+
+  /* now we replace the wildcard by one with a CNAME */
+  recordContents.clear();
+  signatureContents.clear();
+  records.clear();
+  addNSEC3UnhashedRecordToLW(DNSName("*.example.org."), DNSName("example.org"), "whatever", {QType::CNAME, QType::RRSIG, QType::NSEC3}, 600, records);
+  recordContents.insert(records.at(0).d_content);
+  addRRSIG(keys, records, DNSName("example.org."), 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;
+
+  /* A and AAAA do not exist but we have a CNAME so at the wildcard */
+  denialState = getDenial(denialMap, DNSName("b.example.org."), QType::A, false, true);
+  BOOST_CHECK_EQUAL(denialState, dState::NODENIAL);
+
+  denialState = getDenial(denialMap, DNSName("b.example.org."), QType::AAAA, false, true);
   BOOST_CHECK_EQUAL(denialState, dState::NODENIAL);
 }
 
@@ -522,7 +666,7 @@ BOOST_AUTO_TEST_CASE(test_nsec3_ancestor_nxqtype_denial)
   */
 
   dState denialState = getDenial(denialMap, DNSName("a."), QType::A, false, true);
-  /* no data means the qname/qtype is not denied, because an ancestor
+  /* no denial means the qname/qtype is not denied, because an ancestor
      delegation NSEC3 can only deny the DS */
   BOOST_CHECK_EQUAL(denialState, dState::NODENIAL);
 
index 18a8e81bb333e1b40d6dcda32c9ca270fb611cb8..4052e1f5a3bd36cd2c1b58d79ac5b706628d2adf 100644 (file)
@@ -1951,9 +1951,6 @@ bool SyncRes::doCacheCheck(const DNSName &qname, const DNSName& authname, bool w
       return true;
     }
   }
-  else {
-    cerr<<"no cache"<<endl;
-  }
 
   return false;
 }
index bf8d689d504c8323ff411efe59852fdaee1e5eb9..b36acf9dea827b88c4e8f5bc6bdcda821102c5d4 100644 (file)
@@ -152,7 +152,7 @@ bool denialProvesNoDelegation(const DNSName& zone, const std::vector<DNSRecord>&
       }
 
       if (isCoveredByNSEC3Hash(h, beginHash, nsec3->d_nexthash)) {
-        return !(nsec3->d_flags & 1);
+        return !(nsec3->isOptOut());
       }
     }
   }
@@ -207,7 +207,8 @@ static bool isWildcardExpandedOntoItself(const DNSName& owner, const std::vector
 
 /* 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)
+#warning we should not need to export this
+DNSName getNSECOwnerName(const DNSName& initialOwner, const std::vector<std::shared_ptr<RRSIGRecordContent> >& signatures)
 {
   DNSName result = initialOwner;
 
@@ -237,7 +238,8 @@ static bool isNSECAncestorDelegation(const DNSName& signer, const DNSName& owner
     signer.countLabels() < owner.countLabels();
 }
 
-static bool isNSEC3AncestorDelegation(const DNSName& signer, const DNSName& owner, const std::shared_ptr<NSEC3RecordContent>& nsec3)
+#warning FIXME: should not be exported
+bool isNSEC3AncestorDelegation(const DNSName& signer, const DNSName& owner, const std::shared_ptr<NSEC3RecordContent>& nsec3)
 {
   return nsec3->isSet(QType::NS) &&
     !nsec3->isSet(QType::SOA) &&
@@ -269,7 +271,7 @@ static bool provesNoDataWildCard(const DNSName& qname, const uint16_t qtype, con
 
         if (qname.isPartOf(wildcard)) {
           LOG("\tWildcard matches");
-          if (qtype == 0 || !nsec->isSet(qtype)) {
+          if (qtype == 0 || (!nsec->isSet(qtype) && !nsec->isSet(QType::CNAME))) {
             LOG(" and proves that the type did not exist"<<endl);
             return true;
           }
@@ -306,13 +308,13 @@ static bool provesNoWildCard(const DNSName& qname, const uint16_t qtype, const c
           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();
+        const unsigned int commonLabelsCount = commonLabels.countLabels();
 
         DNSName wildcard(qname);
         unsigned int wildcardLabelsCount = wildcard.countLabels();
         while (wildcard.chopOff() && wildcardLabelsCount >= commonLabelsCount) {
           DNSName target = g_wildcarddnsname + wildcard;
-          #warning BUG?? should we decerement wildcardLabelsCount??
+          --wildcardLabelsCount;
 
           LOG("Comparing owner: "<<owner<<" with target: "<<target<<endl);
 
@@ -366,7 +368,7 @@ static bool provesNSEC3NoWildCard(DNSName wildcard, uint16_t const qtype, const
           if (wildcardExists) {
             *wildcardExists = true;
           }
-          if (qtype == 0 || !nsec3->isSet(qtype)) {
+          if (qtype == 0 || (!nsec3->isSet(qtype) && !nsec3->isSet(QType::CNAME))) {
             LOG(" and proves that the type did not exist"<<endl);
             return true;
           }
@@ -742,7 +744,7 @@ dState getDenial(const cspmap_t &validrrsets, const DNSName& qname, const uint16
               LOG("Denies existence of name "<<qname<<"/"<<QType(qtype).getName());
               nextCloserFound = true;
 
-              if ((qtype == QType::DS || qtype == 0) && nsec3->d_flags & 1) {
+              if ((qtype == QType::DS || qtype == 0) && nsec3->isOptOut()) {
                 LOG(" but is opt-out!");
                 isOptOut = true;
               }
index 7f7df41c6a867a7cf6e2206ffa0b79edb67828fe..434e7ab358ef1c5b655f0f7949533647070fa29a 100644 (file)
@@ -95,3 +95,5 @@ void updateDNSSECValidationState(vState& state, const vState stateUpdate);
 
 dState matchesNSEC(const DNSName& name, uint16_t qtype, const DNSName& nsecOwner, const std::shared_ptr<NSECRecordContent>& nsecRecord, const std::vector<std::shared_ptr<RRSIGRecordContent>>& signatures);
 
+bool isNSEC3AncestorDelegation(const DNSName& signer, const DNSName& owner, const std::shared_ptr<NSEC3RecordContent>& nsec3);
+DNSName getNSECOwnerName(const DNSName& initialOwner, const std::vector<std::shared_ptr<RRSIGRecordContent> >& signatures);