Browse Source

[master] Merge branch 'trac1307'

JINMEI Tatuya 13 years ago
parent
commit
431973bb16

+ 0 - 1
src/bin/auth/auth_messages.mes

@@ -260,4 +260,3 @@ NOTIFY request will not be honored.
 % AUTH_INVALID_STATISTICS_DATA invalid specification of statistics data specified
 % AUTH_INVALID_STATISTICS_DATA invalid specification of statistics data specified
 An error was encountered when the authoritiative server specified
 An error was encountered when the authoritiative server specified
 statistics data which is invalid for the auth specification file.
 statistics data which is invalid for the auth specification file.
-

+ 91 - 24
src/bin/auth/query.cc

@@ -12,6 +12,7 @@
 // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 // PERFORMANCE OF THIS SOFTWARE.
 // PERFORMANCE OF THIS SOFTWARE.
 
 
+#include <algorithm>            // for std::max
 #include <vector>
 #include <vector>
 #include <boost/foreach.hpp>
 #include <boost/foreach.hpp>
 
 
@@ -31,24 +32,24 @@ namespace isc {
 namespace auth {
 namespace auth {
 
 
 void
 void
-Query::getAdditional(ZoneFinder& zone, const RRset& rrset) const {
+Query::addAdditional(ZoneFinder& zone, const RRset& rrset) {
     RdataIteratorPtr rdata_iterator(rrset.getRdataIterator());
     RdataIteratorPtr rdata_iterator(rrset.getRdataIterator());
     for (; !rdata_iterator->isLast(); rdata_iterator->next()) {
     for (; !rdata_iterator->isLast(); rdata_iterator->next()) {
         const Rdata& rdata(rdata_iterator->getCurrent());
         const Rdata& rdata(rdata_iterator->getCurrent());
         if (rrset.getType() == RRType::NS()) {
         if (rrset.getType() == RRType::NS()) {
             // Need to perform the search in the "GLUE OK" mode.
             // Need to perform the search in the "GLUE OK" mode.
             const generic::NS& ns = dynamic_cast<const generic::NS&>(rdata);
             const generic::NS& ns = dynamic_cast<const generic::NS&>(rdata);
-            findAddrs(zone, ns.getNSName(), ZoneFinder::FIND_GLUE_OK);
+            addAdditionalAddrs(zone, ns.getNSName(), ZoneFinder::FIND_GLUE_OK);
         } else if (rrset.getType() == RRType::MX()) {
         } else if (rrset.getType() == RRType::MX()) {
             const generic::MX& mx(dynamic_cast<const generic::MX&>(rdata));
             const generic::MX& mx(dynamic_cast<const generic::MX&>(rdata));
-            findAddrs(zone, mx.getMXName());
+            addAdditionalAddrs(zone, mx.getMXName());
         }
         }
     }
     }
 }
 }
 
 
 void
 void
-Query::findAddrs(ZoneFinder& zone, const Name& qname,
-                 const ZoneFinder::FindOptions options) const
+Query::addAdditionalAddrs(ZoneFinder& zone, const Name& qname,
+                          const ZoneFinder::FindOptions options)
 {
 {
     // Out of zone name
     // Out of zone name
     NameComparisonResult result = zone.getOrigin().compare(qname);
     NameComparisonResult result = zone.getOrigin().compare(qname);
@@ -87,12 +88,12 @@ Query::findAddrs(ZoneFinder& zone, const Name& qname,
 }
 }
 
 
 void
 void
-Query::putSOA(ZoneFinder& zone) const {
-    ZoneFinder::FindResult soa_result(zone.find(zone.getOrigin(),
+Query::addSOA(ZoneFinder& finder) {
+    ZoneFinder::FindResult soa_result(finder.find(finder.getOrigin(),
         RRType::SOA(), NULL, dnssec_opt_));
         RRType::SOA(), NULL, dnssec_opt_));
     if (soa_result.code != ZoneFinder::SUCCESS) {
     if (soa_result.code != ZoneFinder::SUCCESS) {
         isc_throw(NoSOA, "There's no SOA record in zone " <<
         isc_throw(NoSOA, "There's no SOA record in zone " <<
-            zone.getOrigin().toText());
+            finder.getOrigin().toText());
     } else {
     } else {
         /*
         /*
          * FIXME:
          * FIXME:
@@ -104,26 +105,88 @@ Query::putSOA(ZoneFinder& zone) const {
     }
     }
 }
 }
 
 
+// Note: unless the data source client implementation or the zone content
+// is broken, 'nsec' should be a valid NSEC RR.  Likewise, the call to
+// find() in this method should result in NXDOMAIN and an NSEC RR that proves
+// the non existent of matching wildcard.  If these assumptions aren't met
+// due to a buggy data source implementation or a broken zone, we'll let
+// underlying libdns++ modules throw an exception, which would result in
+// either an SERVFAIL response or just ignoring the query.  We at least prevent
+// a complete crash due to such broken behavior.
 void
 void
-Query::getAuthAdditional(ZoneFinder& zone) const {
+Query::addNXDOMAINProof(ZoneFinder& finder, ConstRRsetPtr nsec) {
+    if (nsec->getRdataCount() == 0) {
+        isc_throw(BadNSEC, "NSEC for NXDOMAIN is empty");
+        return;
+    }
+
+    // Add the NSEC proving NXDOMAIN to the authority section.
+    response_.addRRset(Message::SECTION_AUTHORITY,
+                       boost::const_pointer_cast<RRset>(nsec), dnssec_);
+
+    // Next, identify the best possible wildcard name that would match
+    // the query name.  It's the longer common suffix with the qname
+    // between the owner or the next domain of the NSEC that proves NXDOMAIN,
+    // prefixed by the wildcard label, "*".  For example, for query name
+    // a.b.example.com, if the NXDOMAIN NSEC is
+    // b.example.com. NSEC c.example.com., the longer suffix is b.example.com.,
+    // and the best possible wildcard is *.b.example.com.  If the NXDOMAIN
+    // NSEC is a.example.com. NSEC c.b.example.com., the longer suffix
+    // is the next domain of the NSEC, and we get the same wildcard name.
+    const int qlabels = qname_.getLabelCount();
+    const int olabels = qname_.compare(nsec->getName()).getCommonLabels();
+    const int nlabels = qname_.compare(
+        dynamic_cast<const generic::NSEC&>(nsec->getRdataIterator()->
+                                           getCurrent()).
+        getNextName()).getCommonLabels();
+    const int common_labels = std::max(olabels, nlabels);
+    const Name wildname(Name("*").concatenate(qname_.split(qlabels -
+                                                           common_labels)));
+
+    // Confirm the wildcard doesn't exist (this should result in NXDOMAIN;
+    // otherwise we shouldn't have got NXDOMAIN for the original query in
+    // the first place).
+    const ZoneFinder::FindResult fresult = finder.find(wildname,
+                                                       RRType::NSEC(), NULL,
+                                                       dnssec_opt_);
+    if (fresult.code != ZoneFinder::NXDOMAIN || !fresult.rrset ||
+        fresult.rrset->getRdataCount() == 0) {
+        isc_throw(BadNSEC, "Unexpected result for wildcard NXDOMAIN proof");
+        return;
+    }
+
+    // Add the (no-) wildcard proof only when it's different from the NSEC
+    // that proves NXDOMAIN; sometimes they can be the same.
+    // Note: name comparison is relatively expensive.  When we are at the
+    // stage of performance optimization, we should consider optimizing this
+    // for some optimized data source implementations.
+    if (nsec->getName() != fresult.rrset->getName()) {
+        response_.addRRset(Message::SECTION_AUTHORITY,
+                           boost::const_pointer_cast<RRset>(fresult.rrset),
+                           dnssec_);
+    }
+}
+
+void
+Query::addAuthAdditional(ZoneFinder& finder) {
     // Fill in authority and addtional sections.
     // Fill in authority and addtional sections.
-    ZoneFinder::FindResult ns_result = zone.find(zone.getOrigin(),
-                                                 RRType::NS(), NULL,
-                                                 dnssec_opt_);
+    ZoneFinder::FindResult ns_result = finder.find(finder.getOrigin(),
+                                                   RRType::NS(), NULL,
+                                                   dnssec_opt_);
     // zone origin name should have NS records
     // zone origin name should have NS records
     if (ns_result.code != ZoneFinder::SUCCESS) {
     if (ns_result.code != ZoneFinder::SUCCESS) {
         isc_throw(NoApexNS, "There's no apex NS records in zone " <<
         isc_throw(NoApexNS, "There's no apex NS records in zone " <<
-                zone.getOrigin().toText());
+                finder.getOrigin().toText());
     } else {
     } else {
         response_.addRRset(Message::SECTION_AUTHORITY,
         response_.addRRset(Message::SECTION_AUTHORITY,
             boost::const_pointer_cast<RRset>(ns_result.rrset), dnssec_);
             boost::const_pointer_cast<RRset>(ns_result.rrset), dnssec_);
         // Handle additional for authority section
         // Handle additional for authority section
-        getAdditional(zone, *ns_result.rrset);
+        addAdditional(finder, *ns_result.rrset);
     }
     }
 }
 }
 
 
 void
 void
-Query::process() const {
+Query::process() {
     bool keep_doing = true;
     bool keep_doing = true;
     const bool qtype_is_any = (qtype_ == RRType::ANY());
     const bool qtype_is_any = (qtype_ == RRType::ANY());
 
 
@@ -141,6 +204,7 @@ Query::process() const {
         response_.setRcode(Rcode::REFUSED());
         response_.setRcode(Rcode::REFUSED());
         return;
         return;
     }
     }
+    ZoneFinder& zfinder = *result.zone_finder;
 
 
     // Found a zone which is the nearest ancestor to QNAME, set the AA bit
     // Found a zone which is the nearest ancestor to QNAME, set the AA bit
     response_.setHeaderFlag(Message::HEADERFLAG_AA);
     response_.setHeaderFlag(Message::HEADERFLAG_AA);
@@ -149,8 +213,7 @@ Query::process() const {
         keep_doing = false;
         keep_doing = false;
         std::auto_ptr<RRsetList> target(qtype_is_any ? new RRsetList : NULL);
         std::auto_ptr<RRsetList> target(qtype_is_any ? new RRsetList : NULL);
         const ZoneFinder::FindResult db_result(
         const ZoneFinder::FindResult db_result(
-            result.zone_finder->find(qname_, qtype_, target.get(),
-                                     dnssec_opt_));
+            zfinder.find(qname_, qtype_, target.get(), dnssec_opt_));
         switch (db_result.code) {
         switch (db_result.code) {
             case ZoneFinder::DNAME: {
             case ZoneFinder::DNAME: {
                 // First, put the dname into the answer
                 // First, put the dname into the answer
@@ -217,14 +280,14 @@ Query::process() const {
                         response_.addRRset(Message::SECTION_ANSWER, rrset,
                         response_.addRRset(Message::SECTION_ANSWER, rrset,
                                            dnssec_);
                                            dnssec_);
                         // Handle additional for answer section
                         // Handle additional for answer section
-                        getAdditional(*result.zone_finder, *rrset.get());
+                        addAdditional(*result.zone_finder, *rrset.get());
                     }
                     }
                 } else {
                 } else {
                     response_.addRRset(Message::SECTION_ANSWER,
                     response_.addRRset(Message::SECTION_ANSWER,
                         boost::const_pointer_cast<RRset>(db_result.rrset),
                         boost::const_pointer_cast<RRset>(db_result.rrset),
                         dnssec_);
                         dnssec_);
                     // Handle additional for answer section
                     // Handle additional for answer section
-                    getAdditional(*result.zone_finder, *db_result.rrset);
+                    addAdditional(*result.zone_finder, *db_result.rrset);
                 }
                 }
                 // If apex NS records haven't been provided in the answer
                 // If apex NS records haven't been provided in the answer
                 // section, insert apex NS records into the authority section
                 // section, insert apex NS records into the authority section
@@ -234,7 +297,7 @@ Query::process() const {
                     db_result.code != ZoneFinder::SUCCESS ||
                     db_result.code != ZoneFinder::SUCCESS ||
                     (qtype_ != RRType::NS() && !qtype_is_any))
                     (qtype_ != RRType::NS() && !qtype_is_any))
                 {
                 {
-                    getAuthAdditional(*result.zone_finder);
+                    addAuthAdditional(*result.zone_finder);
                 }
                 }
                 break;
                 break;
             case ZoneFinder::DELEGATION:
             case ZoneFinder::DELEGATION:
@@ -242,16 +305,20 @@ Query::process() const {
                 response_.addRRset(Message::SECTION_AUTHORITY,
                 response_.addRRset(Message::SECTION_AUTHORITY,
                     boost::const_pointer_cast<RRset>(db_result.rrset),
                     boost::const_pointer_cast<RRset>(db_result.rrset),
                     dnssec_);
                     dnssec_);
-                getAdditional(*result.zone_finder, *db_result.rrset);
+                addAdditional(*result.zone_finder, *db_result.rrset);
                 break;
                 break;
             case ZoneFinder::NXDOMAIN:
             case ZoneFinder::NXDOMAIN:
-                // Just empty answer with SOA in authority section
                 response_.setRcode(Rcode::NXDOMAIN());
                 response_.setRcode(Rcode::NXDOMAIN());
-                putSOA(*result.zone_finder);
+                addSOA(*result.zone_finder);
+
+                // If DNSSEC proof is requested and we've got it, add it.
+                if (dnssec_ && db_result.rrset) {
+                    addNXDOMAINProof(zfinder, db_result.rrset);
+                }
                 break;
                 break;
             case ZoneFinder::NXRRSET:
             case ZoneFinder::NXRRSET:
                 // Just empty answer with SOA in authority section
                 // Just empty answer with SOA in authority section
-                putSOA(*result.zone_finder);
+                addSOA(*result.zone_finder);
                 break;
                 break;
             default:
             default:
                 // These are new result codes (WILDCARD and WILDCARD_NXRRSET)
                 // These are new result codes (WILDCARD and WILDCARD_NXRRSET)

+ 30 - 13
src/bin/auth/query.h

@@ -69,10 +69,16 @@ private:
     /// Adds a SOA of the zone into the authority zone of response_.
     /// Adds a SOA of the zone into the authority zone of response_.
     /// Can throw NoSOA.
     /// Can throw NoSOA.
     ///
     ///
-    void putSOA(isc::datasrc::ZoneFinder& zone) const;
+    void addSOA(isc::datasrc::ZoneFinder& finder);
+
+    /// Add NSEC RRs that prove an NXDOMAIN result.
+    ///
+    /// This corresponds to Section 3.1.3.2 of RFC 4035.
+    void addNXDOMAINProof(isc::datasrc::ZoneFinder& finder,
+                          isc::dns::ConstRRsetPtr nsec);
 
 
     /// \brief Look up additional data (i.e., address records for the names
     /// \brief Look up additional data (i.e., address records for the names
-    /// included in NS or MX records).
+    /// included in NS or MX records) and add them to the additional section.
     ///
     ///
     /// Note: Any additional data which has already been provided in the
     /// Note: Any additional data which has already been provided in the
     /// answer section (i.e., if the original query happend to be for the
     /// answer section (i.e., if the original query happend to be for the
@@ -85,8 +91,8 @@ private:
     /// query is to be found.
     /// query is to be found.
     /// \param rrset The RRset (i.e., NS or MX rrset) which require additional
     /// \param rrset The RRset (i.e., NS or MX rrset) which require additional
     /// processing.
     /// processing.
-    void getAdditional(isc::datasrc::ZoneFinder& zone,
-                       const isc::dns::RRset& rrset) const;
+    void addAdditional(isc::datasrc::ZoneFinder& zone,
+                       const isc::dns::RRset& rrset);
 
 
     /// \brief Find address records for a specified name.
     /// \brief Find address records for a specified name.
     ///
     ///
@@ -104,13 +110,13 @@ private:
     /// be found.
     /// be found.
     /// \param qname The name in rrset RDATA.
     /// \param qname The name in rrset RDATA.
     /// \param options The search options.
     /// \param options The search options.
-    void findAddrs(isc::datasrc::ZoneFinder& zone,
-                   const isc::dns::Name& qname,
-                   const isc::datasrc::ZoneFinder::FindOptions options
-                   = isc::datasrc::ZoneFinder::FIND_DEFAULT) const;
+    void addAdditionalAddrs(isc::datasrc::ZoneFinder& zone,
+                            const isc::dns::Name& qname,
+                            const isc::datasrc::ZoneFinder::FindOptions options
+                            = isc::datasrc::ZoneFinder::FIND_DEFAULT);
 
 
     /// \brief Look up a zone's NS RRset and their address records for an
     /// \brief Look up a zone's NS RRset and their address records for an
-    /// authoritative answer.
+    /// authoritative answer, and add them to the additional section.
     ///
     ///
     /// On returning an authoritative answer, insert a zone's NS into the
     /// On returning an authoritative answer, insert a zone's NS into the
     /// authority section and AAAA/A RRs of each of the NS RDATA into the
     /// authority section and AAAA/A RRs of each of the NS RDATA into the
@@ -125,9 +131,9 @@ private:
     /// include AAAA/A RRs under a zone cut in additional section. (BIND 9
     /// include AAAA/A RRs under a zone cut in additional section. (BIND 9
     /// excludes under-cut RRs; NSD include them.)
     /// excludes under-cut RRs; NSD include them.)
     ///
     ///
-    /// \param zone The \c ZoneFinder through which the NS and additional data
-    /// for the query are to be found.
-    void getAuthAdditional(isc::datasrc::ZoneFinder& zone) const;
+    /// \param finder The \c ZoneFinder through which the NS and additional
+    /// data for the query are to be found.
+    void addAuthAdditional(isc::datasrc::ZoneFinder& finder);
 
 
 public:
 public:
     /// Constructor from query parameters.
     /// Constructor from query parameters.
@@ -176,7 +182,7 @@ public:
     /// This might throw BadZone or any of its specific subclasses, but that
     /// This might throw BadZone or any of its specific subclasses, but that
     /// shouldn't happen in real-life (as BadZone means wrong data, it should
     /// shouldn't happen in real-life (as BadZone means wrong data, it should
     /// have been rejected upon loading).
     /// have been rejected upon loading).
-    void process() const;
+    void process();
 
 
     /// \short Bad zone data encountered.
     /// \short Bad zone data encountered.
     ///
     ///
@@ -210,6 +216,17 @@ public:
         {}
         {}
     };
     };
 
 
+    /// An invalid result is given when a valid NSEC is expected
+    ///
+    // This can only happen when the underlying data source implementation or
+    /// the zone is broken.  By throwing an exception we treat such cases
+    /// as SERVFAIL.
+    struct BadNSEC : public BadZone {
+        BadNSEC(const char* file, size_t line, const char* what) :
+            BadZone(file, line, what)
+        {}
+    };
+
 private:
 private:
     const isc::datasrc::DataSourceClient& datasrc_client_;
     const isc::datasrc::DataSourceClient& datasrc_client_;
     const isc::dns::Name& qname_;
     const isc::dns::Name& qname_;

+ 237 - 32
src/bin/auth/tests/query_unittest.cc

@@ -17,6 +17,7 @@
 #include <map>
 #include <map>
 
 
 #include <boost/bind.hpp>
 #include <boost/bind.hpp>
+#include <boost/scoped_ptr.hpp>
 
 
 #include <dns/masterload.h>
 #include <dns/masterload.h>
 #include <dns/message.h>
 #include <dns/message.h>
@@ -91,8 +92,49 @@ const char* const other_zone_rrs =
     "cnamemailer.example.com. 3600 IN CNAME www.example.com.\n"
     "cnamemailer.example.com. 3600 IN CNAME www.example.com.\n"
     "cnamemx.example.com. 3600 IN MX 10 cnamemailer.example.com.\n"
     "cnamemx.example.com. 3600 IN MX 10 cnamemailer.example.com.\n"
     "mx.delegation.example.com. 3600 IN A 192.0.2.100\n";
     "mx.delegation.example.com. 3600 IN A 192.0.2.100\n";
-
-// This is a mock Zone class for testing.
+// Used in NXDOMAIN proof test.  We are going to test some unusual case where
+// the best possible wildcard is below the "next domain" of the NSEC RR that
+// proves the NXDOMAIN, i.e.,
+// mx.example.com. (exist)
+// (.no.example.com. (qname, NXDOMAIN)
+// ).no.example.com. (exist)
+// *.no.example.com. (best possible wildcard, not exist)
+const char* const no_txt =
+    ").no.example.com. 3600 IN AAAA 2001:db8::53\n";
+// NSEC records.
+const char* const nsec_apex_txt =
+    "example.com. 3600 IN NSEC cname.example.com. NS SOA NSEC RRSIG\n";
+const char* const nsec_mx_txt =
+    "mx.example.com. 3600 IN NSEC ).no.example.com. MX NSEC RRSIG\n";
+const char* const nsec_no_txt =
+    ").no.example.com. 3600 IN NSEC nz.no.example.com. AAAA NSEC RRSIG\n";
+
+// We'll also test the case where a single NSEC proves both NXDOMAIN and the
+// non existence of wildcard.  The following records will be used for that
+// test.
+// ).no.example.com. (exist, whose NSEC proves everything)
+// *.no.example.com. (best possible wildcard, not exist)
+// nx.no.example.com. (NXDOMAIN)
+// nz.no.example.com. (exist)
+const char* const nz_txt =
+    "nz.no.example.com. 3600 IN AAAA 2001:db8::5300\n";
+const char* const nsec_nz_txt =
+    "nz.no.example.com. 3600 IN NSEC noglue.example.com. AAAA NSEC RRSIG\n";
+const char* const nsec_nxdomain_txt =
+    "noglue.example.com. 3600 IN NSEC www.example.com. A\n";
+
+// A helper function that generates a textual representation of RRSIG RDATA
+// for the given covered type.  The resulting RRSIG may not necessarily make
+// sense in terms of the DNSSEC protocol, but for our testing purposes it's
+// okay.
+string
+getCommonRRSIGText(const string& type) {
+    return (type +
+            string(" 5 3 3600 20000101000000 20000201000000 12345 "
+                   "example.com. FAKEFAKEFAKE"));
+}
+
+// This is a mock Zone Finder class for testing.
 // It is a derived class of ZoneFinder for the convenient of tests.
 // It is a derived class of ZoneFinder for the convenient of tests.
 // Its find() method emulates the common behavior of protocol compliant
 // Its find() method emulates the common behavior of protocol compliant
 // ZoneFinder classes, but simplifies some minor cases and also supports broken
 // ZoneFinder classes, but simplifies some minor cases and also supports broken
@@ -112,16 +154,24 @@ public:
         has_SOA_(true),
         has_SOA_(true),
         has_apex_NS_(true),
         has_apex_NS_(true),
         rrclass_(RRClass::IN()),
         rrclass_(RRClass::IN()),
-        include_rrsig_anyway_(false)
+        include_rrsig_anyway_(false),
+        nsec_name_(origin_)
     {
     {
         stringstream zone_stream;
         stringstream zone_stream;
         zone_stream << soa_txt << zone_ns_txt << ns_addrs_txt <<
         zone_stream << soa_txt << zone_ns_txt << ns_addrs_txt <<
             delegation_txt << mx_txt << www_a_txt << cname_txt <<
             delegation_txt << mx_txt << www_a_txt << cname_txt <<
             cname_nxdom_txt << cname_out_txt << dname_txt << dname_a_txt <<
             cname_nxdom_txt << cname_out_txt << dname_txt << dname_a_txt <<
-            other_zone_rrs;
+            other_zone_rrs << no_txt << nz_txt <<
+            nsec_apex_txt << nsec_mx_txt << nsec_no_txt << nsec_nz_txt <<
+            nsec_nxdomain_txt;
 
 
         masterLoad(zone_stream, origin_, rrclass_,
         masterLoad(zone_stream, origin_, rrclass_,
                    boost::bind(&MockZoneFinder::loadRRset, this, _1));
                    boost::bind(&MockZoneFinder::loadRRset, this, _1));
+
+        empty_nsec_rrset_ = ConstRRsetPtr(new RRset(Name::ROOT_NAME(),
+                                                    RRClass::IN(),
+                                                    RRType::NSEC(),
+                                                    RRTTL(3600)));
     }
     }
     virtual isc::dns::Name getOrigin() const { return (origin_); }
     virtual isc::dns::Name getOrigin() const { return (origin_); }
     virtual isc::dns::RRClass getClass() const { return (rrclass_); }
     virtual isc::dns::RRClass getClass() const { return (rrclass_); }
@@ -141,10 +191,24 @@ public:
     // Turn this on if you want it to return RRSIGs regardless of FIND_GLUE_OK
     // Turn this on if you want it to return RRSIGs regardless of FIND_GLUE_OK
     void setIncludeRRSIGAnyway(bool on) { include_rrsig_anyway_ = on; }
     void setIncludeRRSIGAnyway(bool on) { include_rrsig_anyway_ = on; }
 
 
+    // Once called, this "faked" result will be returned when NSEC is expected
+    // for the specified query name.
+    void setNSECResult(const Name& nsec_name, Result code,
+                       ConstRRsetPtr rrset)
+    {
+        nsec_name_ = nsec_name;
+        nsec_result_.reset(new ZoneFinder::FindResult(code, rrset));
+    }
+
     Name findPreviousName(const Name&) const {
     Name findPreviousName(const Name&) const {
         isc_throw(isc::NotImplemented, "Mock doesn't support previous name");
         isc_throw(isc::NotImplemented, "Mock doesn't support previous name");
     }
     }
 
 
+public:
+    // We allow the tests to use these for convenience
+    ConstRRsetPtr delegation_rrset_;
+    ConstRRsetPtr empty_nsec_rrset_;
+
 private:
 private:
     typedef map<RRType, ConstRRsetPtr> RRsetStore;
     typedef map<RRType, ConstRRsetPtr> RRsetStore;
     typedef map<Name, RRsetStore> Domains;
     typedef map<Name, RRsetStore> Domains;
@@ -160,23 +224,17 @@ private:
         // Add some signatures
         // Add some signatures
         } else if (rrset->getName() == Name("example.com.") &&
         } else if (rrset->getName() == Name("example.com.") &&
                    rrset->getType() == RRType::NS()) {
                    rrset->getType() == RRType::NS()) {
-            rrset->addRRsig(RdataPtr(new generic::RRSIG("NS 5 3 3600 "
-                                                        "20000101000000 "
-                                                        "20000201000000 "
-                                                        "12345 example.com. "
-                                                        "FAKEFAKEFAKE")));
-        } else if (rrset->getType() == RRType::A()) {
-            rrset->addRRsig(RdataPtr(new generic::RRSIG("A 5 3 3600 "
-                                                        "20000101000000 "
-                                                        "20000201000000 "
-                                                        "12345 example.com. "
-                                                        "FAKEFAKEFAKE")));
-        } else if (rrset->getType() == RRType::AAAA()) {
-            rrset->addRRsig(RdataPtr(new generic::RRSIG("AAAA 5 3 3600 "
-                                                        "20000101000000 "
-                                                        "20000201000000 "
-                                                        "12345 example.com. "
-                                                        "FAKEFAKEFAKE")));
+            // For NS, we only have RRSIG for the origin name.
+            rrset->addRRsig(RdataPtr(new generic::RRSIG(
+                                         getCommonRRSIGText("NS"))));
+        } else {
+            // For others generate RRSIG unconditionally.  Technically this
+            // is wrong because we shouldn't have it for names under a zone
+            // cut.  But in our tests that doesn't matter, so we add them
+            // just for simplicity.
+            rrset->addRRsig(RdataPtr(new generic::RRSIG(
+                                         getCommonRRSIGText(rrset->getType().
+                                                            toText()))));
         }
         }
     }
     }
 
 
@@ -186,10 +244,12 @@ private:
     const Name dname_name_;
     const Name dname_name_;
     bool has_SOA_;
     bool has_SOA_;
     bool has_apex_NS_;
     bool has_apex_NS_;
-    ConstRRsetPtr delegation_rrset_;
     ConstRRsetPtr dname_rrset_;
     ConstRRsetPtr dname_rrset_;
     const RRClass rrclass_;
     const RRClass rrclass_;
     bool include_rrsig_anyway_;
     bool include_rrsig_anyway_;
+    // The following two will be used for faked NSEC cases
+    Name nsec_name_;
+    boost::scoped_ptr<ZoneFinder::FindResult> nsec_result_;
 };
 };
 
 
 ZoneFinder::FindResult
 ZoneFinder::FindResult
@@ -267,7 +327,31 @@ MockZoneFinder::find(const Name& name, const RRType& type,
         return (FindResult(NXRRSET, RRsetPtr()));
         return (FindResult(NXRRSET, RRsetPtr()));
     }
     }
 
 
-    // query name isn't found in our domains.  returns NXDOMAIN.
+    // query name isn't found in our domains.  This is an NXDOMAIN case.
+    // If we need DNSSEC proof, find the "previous name" that has an NSEC RR
+    // and return NXDOMAIN with the found NSEC.  Otherwise, just return the
+    // NXDOMAIN code and NULL.  If DNSSEC proof is requested but no NSEC is
+    // found, we return NULL, too.  (For simplicity under the test conditions
+    // we don't care about pathological cases such as the name is "smaller"
+    // than the origin)
+    if ((options & FIND_DNSSEC) != 0) {
+        // Emulate a broken DataSourceClient for some special names.
+        if (nsec_result_ && nsec_name_ == name) {
+            return (*nsec_result_);
+        }
+
+        // Normal case
+        for (Domains::const_reverse_iterator it = domains_.rbegin();
+             it != domains_.rend();
+             ++it) {
+            RRsetStore::const_iterator nsec_it;
+            if ((*it).first < name &&
+                (nsec_it = (*it).second.find(RRType::NSEC()))
+                != (*it).second.end()) {
+                return (FindResult(NXDOMAIN, (*nsec_it).second));
+            }
+        }
+    }
     return (FindResult(NXDOMAIN, RRsetPtr()));
     return (FindResult(NXDOMAIN, RRsetPtr()));
 }
 }
 
 
@@ -433,8 +517,9 @@ TEST_F(QueryTest, exactAnyMatch) {
     EXPECT_NO_THROW(Query(memory_client, Name("noglue.example.com"),
     EXPECT_NO_THROW(Query(memory_client, Name("noglue.example.com"),
                           RRType::ANY(), response).process());
                           RRType::ANY(), response).process());
 
 
-    responseCheck(response, Rcode::NOERROR(), AA_FLAG, 1, 3, 2,
-                  "noglue.example.com. 3600 IN A 192.0.2.53\n",
+    responseCheck(response, Rcode::NOERROR(), AA_FLAG, 2, 3, 2,
+                  (string("noglue.example.com. 3600 IN A 192.0.2.53\n") +
+                   string(nsec_nxdomain_txt)).c_str(),
                   zone_ns_txt,
                   zone_ns_txt,
                   "glue.delegation.example.com. 3600 IN A 192.0.2.153\n"
                   "glue.delegation.example.com. 3600 IN A 192.0.2.153\n"
                   "glue.delegation.example.com. 3600 IN AAAA 2001:db8::53\n");
                   "glue.delegation.example.com. 3600 IN AAAA 2001:db8::53\n");
@@ -445,19 +530,17 @@ TEST_F(QueryTest, apexAnyMatch) {
     // in the answer section from the additional.
     // in the answer section from the additional.
     EXPECT_NO_THROW(Query(memory_client, Name("example.com"),
     EXPECT_NO_THROW(Query(memory_client, Name("example.com"),
                           RRType::ANY(), response).process());
                           RRType::ANY(), response).process());
-    responseCheck(response, Rcode::NOERROR(), AA_FLAG, 4, 0, 3,
-                  "example.com. 3600 IN SOA . . 0 0 0 0 0\n"
-                  "example.com. 3600 IN NS glue.delegation.example.com.\n"
-                  "example.com. 3600 IN NS noglue.example.com.\n"
-                  "example.com. 3600 IN NS example.net.\n",
+    responseCheck(response, Rcode::NOERROR(), AA_FLAG, 5, 0, 3,
+                  (string(soa_txt) + string(zone_ns_txt) +
+                   string(nsec_apex_txt)).c_str(),
                   NULL, ns_addrs_txt, mock_finder->getOrigin());
                   NULL, ns_addrs_txt, mock_finder->getOrigin());
 }
 }
 
 
 TEST_F(QueryTest, mxANYMatch) {
 TEST_F(QueryTest, mxANYMatch) {
     EXPECT_NO_THROW(Query(memory_client, Name("mx.example.com"),
     EXPECT_NO_THROW(Query(memory_client, Name("mx.example.com"),
                           RRType::ANY(), response).process());
                           RRType::ANY(), response).process());
-    responseCheck(response, Rcode::NOERROR(), AA_FLAG, 3, 3, 4,
-                  mx_txt, zone_ns_txt,
+    responseCheck(response, Rcode::NOERROR(), AA_FLAG, 4, 3, 4,
+                  (string(mx_txt) + string(nsec_mx_txt)).c_str(), zone_ns_txt,
                   (string(ns_addrs_txt) + string(www_a_txt)).c_str());
                   (string(ns_addrs_txt) + string(www_a_txt)).c_str());
 }
 }
 
 
@@ -502,6 +585,128 @@ TEST_F(QueryTest, nxdomain) {
                   NULL, soa_txt, NULL, mock_finder->getOrigin());
                   NULL, soa_txt, NULL, mock_finder->getOrigin());
 }
 }
 
 
+TEST_F(QueryTest, nxdomainWithNSEC) {
+    // NXDOMAIN with DNSSEC proof.  We should have SOA, NSEC that proves
+    // NXDOMAIN and NSEC that proves nonexistence of matching wildcard,
+    // as well as their RRSIGs.
+    EXPECT_NO_THROW(Query(memory_client, Name("nxdomain.example.com"), qtype,
+                          response, true).process());
+    responseCheck(response, Rcode::NXDOMAIN(), AA_FLAG, 0, 6, 0,
+                  NULL, (string(soa_txt) +
+                         string("example.com. 3600 IN RRSIG ") +
+                         getCommonRRSIGText("SOA") + "\n" +
+                         string(nsec_nxdomain_txt) + "\n" +
+                         string("noglue.example.com. 3600 IN RRSIG ") +
+                         getCommonRRSIGText("NSEC") + "\n" +
+                         string(nsec_apex_txt) + "\n" +
+                         string("example.com. 3600 IN RRSIG ") +
+                         getCommonRRSIGText("NSEC")).c_str(),
+                  NULL, mock_finder->getOrigin());
+}
+
+TEST_F(QueryTest, nxdomainWithNSEC2) {
+    // See comments about no_txt.  In this case the best possible wildcard
+    // is derived from the next domain of the NSEC that proves NXDOMAIN, and
+    // the NSEC to provide the non existence of wildcard is different from
+    // the first NSEC.
+    Query(memory_client, Name("(.no.example.com"), qtype,
+          response, true).process();
+    responseCheck(response, Rcode::NXDOMAIN(), AA_FLAG, 0, 6, 0,
+                  NULL, (string(soa_txt) +
+                         string("example.com. 3600 IN RRSIG ") +
+                         getCommonRRSIGText("SOA") + "\n" +
+                         string(nsec_mx_txt) + "\n" +
+                         string("mx.example.com. 3600 IN RRSIG ") +
+                         getCommonRRSIGText("NSEC") + "\n" +
+                         string(nsec_no_txt) + "\n" +
+                         string(").no.example.com. 3600 IN RRSIG ") +
+                         getCommonRRSIGText("NSEC")).c_str(),
+                  NULL, mock_finder->getOrigin());
+}
+
+TEST_F(QueryTest, nxdomainWithNSECDuplicate) {
+    // See comments about nz_txt.  In this case we only need one NSEC,
+    // which proves both NXDOMAIN and the non existence of wildcard.
+    Query(memory_client, Name("nx.no.example.com"), qtype,
+          response, true).process();
+    responseCheck(response, Rcode::NXDOMAIN(), AA_FLAG, 0, 4, 0,
+                  NULL, (string(soa_txt) +
+                         string("example.com. 3600 IN RRSIG ") +
+                         getCommonRRSIGText("SOA") + "\n" +
+                         string(nsec_no_txt) + "\n" +
+                         string(").no.example.com. 3600 IN RRSIG ") +
+                         getCommonRRSIGText("NSEC")).c_str(),
+                  NULL, mock_finder->getOrigin());
+}
+
+TEST_F(QueryTest, nxdomainBadNSEC1) {
+    // ZoneFinder::find() returns NXDOMAIN with non NSEC RR.
+    mock_finder->setNSECResult(Name("badnsec.example.com"),
+                               ZoneFinder::NXDOMAIN,
+                               mock_finder->delegation_rrset_);
+    EXPECT_THROW(Query(memory_client, Name("badnsec.example.com"), qtype,
+                       response, true).process(),
+                 std::bad_cast);
+}
+
+TEST_F(QueryTest, nxdomainBadNSEC2) {
+    // ZoneFinder::find() returns NXDOMAIN with an empty NSEC RR.
+    mock_finder->setNSECResult(Name("emptynsec.example.com"),
+                               ZoneFinder::NXDOMAIN,
+                               mock_finder->empty_nsec_rrset_);
+    EXPECT_THROW(Query(memory_client, Name("emptynsec.example.com"), qtype,
+                       response, true).process(),
+                 Query::BadNSEC);
+}
+
+TEST_F(QueryTest, nxdomainBadNSEC3) {
+    // "no-wildcard proof" returns SUCCESS.  it should be NXDOMAIN.
+    mock_finder->setNSECResult(Name("*.example.com"),
+                               ZoneFinder::SUCCESS,
+                               mock_finder->delegation_rrset_);
+    EXPECT_THROW(Query(memory_client, Name("nxdomain.example.com"), qtype,
+                       response, true).process(),
+                 Query::BadNSEC);
+}
+
+TEST_F(QueryTest, nxdomainBadNSEC4) {
+    // "no-wildcard proof" doesn't return RRset.
+    mock_finder->setNSECResult(Name("*.example.com"),
+                               ZoneFinder::NXDOMAIN, ConstRRsetPtr());
+    EXPECT_THROW(Query(memory_client, Name("nxdomain.example.com"), qtype,
+                       response, true).process(),
+                 Query::BadNSEC);
+}
+
+TEST_F(QueryTest, nxdomainBadNSEC5) {
+    // "no-wildcard proof" returns non NSEC.
+    mock_finder->setNSECResult(Name("*.example.com"),
+                               ZoneFinder::NXDOMAIN,
+                               mock_finder->delegation_rrset_);
+    // This is a bit odd, but we'll simply include the returned RRset.
+    Query(memory_client, Name("nxdomain.example.com"), qtype,
+          response, true).process();
+    responseCheck(response, Rcode::NXDOMAIN(), AA_FLAG, 0, 8, 0,
+                  NULL, (string(soa_txt) +
+                         string("example.com. 3600 IN RRSIG ") +
+                         getCommonRRSIGText("SOA") + "\n" +
+                         string(nsec_nxdomain_txt) + "\n" +
+                         string("noglue.example.com. 3600 IN RRSIG ") +
+                         getCommonRRSIGText("NSEC") + "\n" +
+                         delegation_txt).c_str(),
+                  NULL, mock_finder->getOrigin());
+}
+
+TEST_F(QueryTest, nxdomainBadNSEC6) {
+    // "no-wildcard proof" returns empty NSEC.
+    mock_finder->setNSECResult(Name("*.example.com"),
+                               ZoneFinder::NXDOMAIN,
+                               mock_finder->empty_nsec_rrset_);
+    EXPECT_THROW(Query(memory_client, Name("nxdomain.example.com"), qtype,
+                       response, true).process(),
+                 Query::BadNSEC);
+}
+
 TEST_F(QueryTest, nxrrset) {
 TEST_F(QueryTest, nxrrset) {
     EXPECT_NO_THROW(Query(memory_client, Name("www.example.com"),
     EXPECT_NO_THROW(Query(memory_client, Name("www.example.com"),
                           RRType::TXT(), response).process());
                           RRType::TXT(), response).process());

+ 5 - 0
src/lib/dns/rdata/generic/nsec_47.cc

@@ -178,6 +178,11 @@ NSEC::toWire(AbstractMessageRenderer& renderer) const {
     renderer.writeData(&impl_->typebits_[0], impl_->typebits_.size());
     renderer.writeData(&impl_->typebits_[0], impl_->typebits_.size());
 }
 }
 
 
+const Name&
+NSEC::getNextName() const {
+    return (impl_->nextname_);
+}
+
 int
 int
 NSEC::compare(const Rdata& other) const {
 NSEC::compare(const Rdata& other) const {
     const NSEC& other_nsec = dynamic_cast<const NSEC&>(other);
     const NSEC& other_nsec = dynamic_cast<const NSEC&>(other);

+ 10 - 0
src/lib/dns/rdata/generic/nsec_47.h

@@ -38,6 +38,16 @@ public:
     // END_COMMON_MEMBERS
     // END_COMMON_MEMBERS
     NSEC& operator=(const NSEC& source);
     NSEC& operator=(const NSEC& source);
     ~NSEC();
     ~NSEC();
+
+    // specialized methods
+
+    /// Return the next domain name.
+    ///
+    /// \exception std::bad_alloc Resource allocation failure in name copy.
+    ///
+    /// \return The next domain name field in the form of \c Name object.
+    const Name& getNextName() const;
+
 private:
 private:
     NSECImpl* impl_;
     NSECImpl* impl_;
 };
 };

+ 6 - 0
src/lib/dns/tests/rdata_nsec_unittest.cc

@@ -89,4 +89,10 @@ TEST_F(Rdata_NSEC_Test, assign) {
     EXPECT_EQ(0, rdata_nsec.compare(rdata_nsec2));
     EXPECT_EQ(0, rdata_nsec.compare(rdata_nsec2));
 }
 }
 
 
+TEST_F(Rdata_NSEC_Test, getNextName) {
+    // The implementation is quite trivial, so we simply check it's actually
+    // defined and does work as intended in a simple case.
+    EXPECT_EQ(Name("www2.isc.org"), generic::NSEC((nsec_txt)).getNextName());
+}
+
 }
 }

+ 24 - 4
src/lib/testutils/dnsmessage_test.h

@@ -21,6 +21,7 @@
 #include <dns/message.h>
 #include <dns/message.h>
 #include <dns/name.h>
 #include <dns/name.h>
 #include <dns/masterload.h>
 #include <dns/masterload.h>
+#include <dns/rdataclass.h>
 #include <dns/rrclass.h>
 #include <dns/rrclass.h>
 #include <dns/rrset.h>
 #include <dns/rrset.h>
 
 
@@ -113,13 +114,32 @@ void rrsetCheck(isc::dns::ConstRRsetPtr expected_rrset,
 /// The definitions in this name space are not supposed to be used publicly,
 /// The definitions in this name space are not supposed to be used publicly,
 /// but are given here because they are used in templated functions.
 /// but are given here because they are used in templated functions.
 namespace detail {
 namespace detail {
-// Helper matching class used in rrsetsCheck()
+// Helper matching class used in rrsetsCheck().  Basically we only have to
+// check the equality of name, RR type and RR class, but for RRSIGs we need
+// special additional checks because they are essentially different if their
+// 'type covered' are different.  For simplicity, we only compare the types
+// of the first RRSIG RDATAs (and only check when they exist); if there's
+// further difference in the RDATA, the main comparison checks will detect it.
 struct RRsetMatch : public std::unary_function<isc::dns::ConstRRsetPtr, bool> {
 struct RRsetMatch : public std::unary_function<isc::dns::ConstRRsetPtr, bool> {
     RRsetMatch(isc::dns::ConstRRsetPtr target) : target_(target) {}
     RRsetMatch(isc::dns::ConstRRsetPtr target) : target_(target) {}
     bool operator()(isc::dns::ConstRRsetPtr rrset) const {
     bool operator()(isc::dns::ConstRRsetPtr rrset) const {
-        return (rrset->getType() == target_->getType() &&
-                rrset->getClass() == target_->getClass() &&
-                rrset->getName() == target_->getName());
+        if (rrset->getType() != target_->getType() ||
+            rrset->getClass() != target_->getClass() ||
+            rrset->getName() != target_->getName()) {
+            return (false);
+        }
+        if (rrset->getType() != isc::dns::RRType::RRSIG()) {
+            return (true);
+        }
+        if (rrset->getRdataCount() == 0 || target_->getRdataCount() == 0) {
+            return (true);
+        }
+        isc::dns::RdataIteratorPtr rdit = rrset->getRdataIterator();
+        isc::dns::RdataIteratorPtr targetit = target_->getRdataIterator();
+        return (dynamic_cast<const isc::dns::rdata::generic::RRSIG&>(
+                    rdit->getCurrent()).typeCovered() ==
+                dynamic_cast<const isc::dns::rdata::generic::RRSIG&>(
+                    targetit->getCurrent()).typeCovered());
     }
     }
     const isc::dns::ConstRRsetPtr target_;
     const isc::dns::ConstRRsetPtr target_;
 };
 };