Browse Source

[master] Merge branch 'trac1431'

JINMEI Tatuya 13 years ago
parent
commit
30ef882f31

+ 65 - 33
src/bin/auth/query.cc

@@ -30,6 +30,42 @@ using namespace isc::dns;
 using namespace isc::datasrc;
 using namespace isc::dns::rdata;
 
+namespace {
+// This is a temporary wrapper to convert old format of FindResult to new
+// one, until we update all supported data source implementations.
+ZoneFinder::FindResult
+findWrapper(const ZoneFinder::FindResult& orig_result) {
+    // Retrieve the original flags
+    ZoneFinder::FindResultFlags flags = ZoneFinder::RESULT_DEFAULT;
+    if (orig_result.isWildcard()) {
+        flags = flags | ZoneFinder::RESULT_WILDCARD;
+    }
+    if (orig_result.isNSECSigned()) {
+        flags = flags | ZoneFinder::RESULT_NSEC_SIGNED;
+    }
+    if (orig_result.isNSEC3Signed()) {
+        flags = flags | ZoneFinder::RESULT_NSEC3_SIGNED;
+    }
+
+    // Convert older code to new one, adjusting flags if necessary
+    ZoneFinder::Result code = orig_result.code;
+    if (code == ZoneFinder::WILDCARD) {
+        code = ZoneFinder::SUCCESS;
+        flags = flags | ZoneFinder::RESULT_WILDCARD;
+    } else if (code == ZoneFinder::WILDCARD_CNAME) {
+        code = ZoneFinder::CNAME;
+        flags = flags | ZoneFinder::RESULT_WILDCARD;
+    } else if (code == ZoneFinder::WILDCARD_NXRRSET) {
+        code = ZoneFinder::NXRRSET;
+        flags = flags | ZoneFinder::RESULT_WILDCARD;
+    }
+    if (orig_result.rrset && orig_result.rrset->getType() == RRType::NSEC()) {
+        flags = flags | ZoneFinder::RESULT_NSEC_SIGNED;
+    }
+    return (ZoneFinder::FindResult(code, orig_result.rrset, flags));
+}
+}
+
 namespace isc {
 namespace auth {
 
@@ -69,8 +105,8 @@ Query::addAdditionalAddrs(ZoneFinder& zone, const Name& qname,
 
     // Find A rrset
     if (qname_ != qname || qtype_ != RRType::A()) {
-        ZoneFinder::FindResult a_result = zone.find(qname, RRType::A(),
-                                                    options | dnssec_opt_);
+        ZoneFinder::FindResult a_result =
+            findWrapper(zone.find(qname, RRType::A(), options | dnssec_opt_));
         if (a_result.code == ZoneFinder::SUCCESS) {
             response_.addRRset(Message::SECTION_ADDITIONAL,
                     boost::const_pointer_cast<RRset>(a_result.rrset), dnssec_);
@@ -80,7 +116,8 @@ Query::addAdditionalAddrs(ZoneFinder& zone, const Name& qname,
     // Find AAAA rrset
     if (qname_ != qname || qtype_ != RRType::AAAA()) {
         ZoneFinder::FindResult aaaa_result =
-            zone.find(qname, RRType::AAAA(), options | dnssec_opt_);
+            findWrapper(zone.find(qname, RRType::AAAA(),
+                                  options | dnssec_opt_));
         if (aaaa_result.code == ZoneFinder::SUCCESS) {
             response_.addRRset(Message::SECTION_ADDITIONAL,
                     boost::const_pointer_cast<RRset>(aaaa_result.rrset),
@@ -91,8 +128,9 @@ Query::addAdditionalAddrs(ZoneFinder& zone, const Name& qname,
 
 void
 Query::addSOA(ZoneFinder& finder) {
-    ZoneFinder::FindResult soa_result(finder.find(finder.getOrigin(),
-        RRType::SOA(), dnssec_opt_));
+    ZoneFinder::FindResult soa_result =
+        findWrapper(finder.find(finder.getOrigin(),
+                                RRType::SOA(), dnssec_opt_));
     if (soa_result.code != ZoneFinder::SUCCESS) {
         isc_throw(NoSOA, "There's no SOA record in zone " <<
             finder.getOrigin().toText());
@@ -147,9 +185,8 @@ Query::addNXDOMAINProof(ZoneFinder& finder, ConstRRsetPtr nsec) {
     // 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(),
-                                                       dnssec_opt_);
+    const ZoneFinder::FindResult fresult =
+        findWrapper(finder.find(wildname, RRType::NSEC(), dnssec_opt_));
     if (fresult.code != ZoneFinder::NXDOMAIN || !fresult.rrset ||
         fresult.rrset->getRdataCount() == 0) {
         isc_throw(BadNSEC, "Unexpected result for wildcard NXDOMAIN proof");
@@ -173,8 +210,8 @@ Query::addWildcardProof(ZoneFinder& finder) {
     // substitution.  Confirm that by specifying NO_WILDCARD.  It should result
     // in NXDOMAIN and an NSEC RR that proves it should be returned.
     const ZoneFinder::FindResult fresult =
-        finder.find(qname_, RRType::NSEC(),
-                    dnssec_opt_ | ZoneFinder::NO_WILDCARD);
+        findWrapper(finder.find(qname_, RRType::NSEC(),
+                                dnssec_opt_ | ZoneFinder::NO_WILDCARD));
     if (fresult.code != ZoneFinder::NXDOMAIN || !fresult.rrset ||
         fresult.rrset->getRdataCount() == 0) {
         isc_throw(BadNSEC, "Unexpected result for wildcard proof");
@@ -191,13 +228,10 @@ Query::addWildcardNXRRSETProof(ZoneFinder& finder, ConstRRsetPtr nsec) {
     if (nsec->getRdataCount() == 0) {
         isc_throw(BadNSEC, "NSEC for WILDCARD_NXRRSET is empty");
     }
-    // Add this NSEC RR to authority section.
-    response_.addRRset(Message::SECTION_AUTHORITY,
-                      boost::const_pointer_cast<RRset>(nsec), dnssec_);
     
     const ZoneFinder::FindResult fresult =
-        finder.find(qname_, RRType::NSEC(),
-                    dnssec_opt_ | ZoneFinder::NO_WILDCARD);
+        findWrapper(finder.find(qname_, RRType::NSEC(),
+                                dnssec_opt_ | ZoneFinder::NO_WILDCARD));
     if (fresult.code != ZoneFinder::NXDOMAIN || !fresult.rrset ||
         fresult.rrset->getRdataCount() == 0) {
         isc_throw(BadNSEC, "Unexpected result for no match QNAME proof");
@@ -214,8 +248,9 @@ Query::addWildcardNXRRSETProof(ZoneFinder& finder, ConstRRsetPtr nsec) {
 void
 Query::addAuthAdditional(ZoneFinder& finder) {
     // Fill in authority and addtional sections.
-    ZoneFinder::FindResult ns_result = finder.find(finder.getOrigin(),
-                                                   RRType::NS(), dnssec_opt_);
+    ZoneFinder::FindResult ns_result =
+        findWrapper(finder.find(finder.getOrigin(), RRType::NS(),
+                                dnssec_opt_));
     // zone origin name should have NS records
     if (ns_result.code != ZoneFinder::SUCCESS) {
         isc_throw(NoApexNS, "There's no apex NS records in zone " <<
@@ -260,7 +295,7 @@ Query::process() {
         find = boost::bind(&ZoneFinder::find, &zfinder, qname_, qtype_,
                            dnssec_opt_);
     }
-    ZoneFinder::FindResult db_result(find());
+    ZoneFinder::FindResult db_result(findWrapper(find()));
     switch (db_result.code) {
         case ZoneFinder::DNAME: {
             // First, put the dname into the answer
@@ -306,7 +341,6 @@ Query::process() {
             break;
         }
         case ZoneFinder::CNAME:
-        case ZoneFinder::WILDCARD_CNAME:
             /*
              * We don't do chaining yet. Therefore handling a CNAME is
              * mostly the same as handling SUCCESS, but we didn't get
@@ -322,12 +356,11 @@ Query::process() {
 
             // If the answer is a result of wildcard substitution,
             // add a proof that there's no closer name.
-            if (dnssec_ && db_result.code == ZoneFinder::WILDCARD_CNAME) {
+            if (dnssec_ && db_result.isWildcard()) {
                 addWildcardProof(*result.zone_finder);
             }
             break;
         case ZoneFinder::SUCCESS:
-        case ZoneFinder::WILDCARD:
             if (qtype_is_any) {
                 // If quety type is ANY, insert all RRs under the domain
                 // into answer section.
@@ -357,7 +390,7 @@ Query::process() {
 
             // If the answer is a result of wildcard substitution,
             // add a proof that there's no closer name.
-            if (dnssec_ && db_result.code == ZoneFinder::WILDCARD) {
+            if (dnssec_ && db_result.isWildcard()) {
                 addWildcardProof(*result.zone_finder);
             }
             break;
@@ -377,17 +410,16 @@ Query::process() {
             break;
         case ZoneFinder::NXRRSET:
             addSOA(*result.zone_finder);
-            if (dnssec_ && db_result.rrset) {
-                response_.addRRset(Message::SECTION_AUTHORITY,
-                                   boost::const_pointer_cast<RRset>(
-                                       db_result.rrset),
-                                   dnssec_);
-            }
-            break;
-        case ZoneFinder::WILDCARD_NXRRSET:
-            addSOA(*result.zone_finder);
-            if (dnssec_ && db_result.rrset) {
-                addWildcardNXRRSETProof(zfinder, db_result.rrset);
+            if (dnssec_) {
+                if (db_result.isNSECSigned() && db_result.rrset) {
+                    response_.addRRset(Message::SECTION_AUTHORITY,
+                                       boost::const_pointer_cast<RRset>(
+                                           db_result.rrset),
+                                       dnssec_);
+                    if (db_result.isWildcard()) {
+                        addWildcardNXRRSETProof(zfinder, db_result.rrset);
+                    }
+                }
             }
             break;
         default:

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

@@ -19,6 +19,8 @@
 #include <boost/bind.hpp>
 #include <boost/scoped_ptr.hpp>
 
+#include <exceptions/exceptions.h>
+
 #include <dns/masterload.h>
 #include <dns/message.h>
 #include <dns/name.h>
@@ -153,6 +155,14 @@ const char* const nsec_www_txt =
 // Authoritative data without NSEC
 const char* const nonsec_a_txt = "nonsec.example.com. 3600 IN A 192.0.2.0\n";
 
+// NSEC3 RRs.  You may also need to add mapping to MockZoneFinder::hash_map_.
+const char* const nsec3_apex_txt =
+    "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example.com. 3600 IN NSEC3 1 1 12 "
+    "aabbccdd 2t7b4g4vsa5smi47k61mv5bv1a22bojr NS SOA NSEC3PARAM RRSIG\n";
+const char* const nsec3_www_txt =
+    "q04jkcevqvmu85r014c7dkba38o0ji5r.example.com. 3600 IN NSEC3 1 1 12 "
+    "aabbccdd r53bq7cc2uvmubfu5ocmm6pers9tk9en A RRSIG\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
@@ -185,6 +195,7 @@ public:
         has_apex_NS_(true),
         rrclass_(RRClass::IN()),
         include_rrsig_anyway_(false),
+        use_nsec3_(false),
         nsec_name_(origin_)
     {
         stringstream zone_stream;
@@ -197,7 +208,8 @@ public:
             wild_txt << nsec_wild_txt << cnamewild_txt << nsec_cnamewild_txt <<
             wild_txt_nxrrset << nsec_wild_txt_nxrrset << wild_txt_next <<
             nsec_wild_txt_next << empty_txt << nsec_empty_txt <<
-            empty_prev_txt << nsec_empty_prev_txt;
+            empty_prev_txt << nsec_empty_prev_txt <<
+            nsec3_apex_txt << nsec3_www_txt;
 
         masterLoad(zone_stream, origin_, rrclass_,
                    boost::bind(&MockZoneFinder::loadRRset, this, _1));
@@ -206,6 +218,21 @@ public:
                                                     RRClass::IN(),
                                                     RRType::NSEC(),
                                                     RRTTL(3600)));
+
+        // (Faked) NSEC3 hash map.  For convenience we use hardcoded built-in
+        // map instead of calculating and using actual hash.
+        // The used hash values are borrowed from RFC5155 examples.
+        hash_map_[Name("example.com")] = "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom";
+        hash_map_[Name("nxdomain.example.com")] =
+            "v644ebqk9bibcna874givr6joj62mlhv";
+        hash_map_[Name("nx.domain.example.com")] =
+            "v644ebqk9bibcna874givr6joj62mlhv";
+        hash_map_[Name("domain.example.com")] =
+            "v644ebqk9bibcna874givr6joj62mlhv";
+        hash_map_[Name("nxdomain2.example.com")] =
+            "q00jkcevqvmu85r014c7dkba38o0ji5r";
+        hash_map_[Name("nxdomain3.example.com")] =
+            "009mhaveqvm6t7vbl5lop2u3t2rp3tom";
     }
     virtual isc::dns::Name getOrigin() const { return (origin_); }
     virtual isc::dns::RRClass getClass() const { return (rrclass_); }
@@ -216,6 +243,9 @@ public:
                                std::vector<ConstRRsetPtr>& target,
                                const FindOptions options = FIND_DEFAULT);
 
+    virtual ZoneFinder::FindNSEC3Result
+    findNSEC3(const Name& name, bool recursive);
+
     // If false is passed, it makes the zone broken as if it didn't have the
     // SOA.
     void setSOAFlag(bool on) { has_SOA_ = on; }
@@ -236,6 +266,10 @@ public:
         nsec_result_.reset(new ZoneFinder::FindResult(code, rrset));
     }
 
+    // If true is passed return an empty NSEC3 RRset for some negative
+    // answers when DNSSEC is required.
+    void setNSEC3Flag(bool on) { use_nsec3_ = on; }
+
     Name findPreviousName(const Name&) const {
         isc_throw(isc::NotImplemented, "Mock doesn't support previous name");
     }
@@ -249,7 +283,19 @@ private:
     typedef map<RRType, ConstRRsetPtr> RRsetStore;
     typedef map<Name, RRsetStore> Domains;
     Domains domains_;
+    Domains nsec3_domains_;
     void loadRRset(RRsetPtr rrset) {
+        if (rrset->getType() == RRType::NSEC3()) {
+            // NSEC3 should go to the dedicated table
+            nsec3_domains_[rrset->getName()][rrset->getType()] = rrset;
+
+            // By nature it should have RRSIG.  (We may want to selectively
+            // omit this to test pathological cases).
+            rrset->addRRsig(RdataPtr(new generic::RRSIG(
+                                         getCommonRRSIGText(rrset->getType().
+                                                            toText()))));
+            return;
+        }
         domains_[rrset->getName()][rrset->getType()] = rrset;
         if (rrset->getName() == delegation_name_ &&
             rrset->getType() == RRType::NS()) {
@@ -283,9 +329,11 @@ private:
     ConstRRsetPtr dname_rrset_;
     const RRClass rrclass_;
     bool include_rrsig_anyway_;
+    bool use_nsec3_;
     // The following two will be used for faked NSEC cases
     Name nsec_name_;
     boost::scoped_ptr<ZoneFinder::FindResult> nsec_result_;
+    map<Name, string> hash_map_;
 };
 
 // A helper function that generates a new RRset based on "wild_rrset",
@@ -327,6 +375,51 @@ MockZoneFinder::findAll(const Name& name, std::vector<ConstRRsetPtr>& target,
     return (result);
 }
 
+ZoneFinder::FindNSEC3Result
+MockZoneFinder::findNSEC3(const Name& name, bool recursive) {
+    ConstRRsetPtr covering_proof;
+    const int labels = name.getLabelCount();
+
+    // For brevity, we assume several things below: maps should have an
+    // expected entry when operator[] is used; maps are not empty.
+    for (int i = 0; i < labels; ++i) {
+        const string hlabel = hash_map_[name.split(i, labels - i)];
+        const Name hname = Name(hlabel + ".example.com");
+        // We don't use const_iterator so that we can use operator[] below
+        Domains::iterator found_domain = nsec3_domains_.lower_bound(hname);
+
+        // If the given hash is larger than the largest stored hash or
+        // the first label doesn't match the target, identify the "previous"
+        // hash value and remember it as the candidate next closer proof.
+        if (found_domain == nsec3_domains_.end() ||
+            found_domain->first.split(0, 1).toText(true) != hlabel) {
+            // If the given hash is larger or smaller than everything,
+            // the covering proof is the NSEC3 that has the largest hash.
+            if (found_domain == nsec3_domains_.end() ||
+                found_domain == nsec3_domains_.begin()) {
+                covering_proof =
+                    nsec3_domains_.rbegin()->second[RRType::NSEC3()];
+            } else {
+                // Otherwise, H(found_domain-1) < given_hash < H(found_domain)
+                // The covering proof is the first one.
+                covering_proof = (--found_domain)->second[RRType::NSEC3()];
+            }
+            if (!recursive) {   // in non recursive mode, we are done.
+                return (ZoneFinder::FindNSEC3Result(false,
+                                                    name.getLabelCount(),
+                                                    covering_proof,
+                                                    ConstRRsetPtr()));
+            }
+        } else {                // exact match
+            return (ZoneFinder::FindNSEC3Result(
+                        true, name.getLabelCount() - i,
+                        found_domain->second[RRType::NSEC3()],
+                        covering_proof));
+        }
+    }
+    isc_throw(isc::Unexpected, "findNSEC3() isn't expected to fail");
+}
+
 ZoneFinder::FindResult
 MockZoneFinder::find(const Name& name, const RRType& type,
                      const FindOptions options)
@@ -362,7 +455,7 @@ MockZoneFinder::find(const Name& name, const RRType& type,
             ConstRRsetPtr rrset;
             // Strip whatever signature there is in case DNSSEC is not required
             // Just to make sure the Query asks for it when it is needed
-            if (options & ZoneFinder::FIND_DNSSEC ||
+            if ((options & ZoneFinder::FIND_DNSSEC) != 0 ||
                 include_rrsig_anyway_ ||
                 !found_rrset->second->getRRsig()) {
                 rrset = found_rrset->second;
@@ -389,12 +482,16 @@ MockZoneFinder::find(const Name& name, const RRType& type,
 
         // Otherwise it's NXRRSET case.
         if ((options & FIND_DNSSEC) != 0) {
+            if (use_nsec3_) {
+                return (FindResult(NXRRSET, RRsetPtr(), RESULT_NSEC3_SIGNED));
+            }
             found_rrset = found_domain->second.find(RRType::NSEC());
             if (found_rrset != found_domain->second.end()) {
-                return (FindResult(NXRRSET, found_rrset->second));
+                return (FindResult(NXRRSET, found_rrset->second,
+                                   RESULT_NSEC_SIGNED));
             }
         }
-        return (FindResult(NXRRSET, RRsetPtr()));
+        return (FindResult(NXRRSET, RRsetPtr(), RESULT_NSEC_SIGNED));
     }
 
     // query name isn't found in our domains.
@@ -413,10 +510,14 @@ MockZoneFinder::find(const Name& name, const RRType& type,
         // the origin of the zone)
         --domain;               // reset domain to the "previous name"
         if ((options & FIND_DNSSEC) != 0) {
+            if (use_nsec3_) {
+                return (FindResult(NXRRSET, RRsetPtr(), RESULT_NSEC3_SIGNED));
+            }
             RRsetStore::const_iterator found_rrset =
                 (*domain).second.find(RRType::NSEC());
             if (found_rrset != (*domain).second.end()) {
-                return (FindResult(NXRRSET, found_rrset->second));
+                return (FindResult(NXRRSET, found_rrset->second,
+                                   RESULT_NSEC_SIGNED));
             }
         }
         return (FindResult(NXRRSET, RRsetPtr()));
@@ -436,36 +537,57 @@ MockZoneFinder::find(const Name& name, const RRType& type,
                 domain = domains_.find(Name("*").concatenate(wild_suffix));
                 // Matched the QNAME
                 if (domain != domains_.end()) {
-                   RRsetStore::const_iterator found_rrset =
-                       domain->second.find(type);
-                   // Matched the QTYPE
-                   if(found_rrset != domain->second.end()) {
-                    return (FindResult(WILDCARD,
-                            substituteWild(*found_rrset->second, name)));
-                   } else {
-                   // No matched QTYPE, this case is for WILDCARD_NXRRSET
-                     found_rrset = domain->second.find(RRType::NSEC());
-                     assert(found_rrset != domain->second.end());
-                     Name newName = Name("*").concatenate(wild_suffix);
-                     return (FindResult(WILDCARD_NXRRSET,
-                           substituteWild(*found_rrset->second,newName)));
-                   }
-                 } else {
+                    RRsetStore::const_iterator found_rrset =
+                        domain->second.find(type);
+                    // Matched the QTYPE
+                    if(found_rrset != domain->second.end()) {
+                        return (FindResult(SUCCESS,
+                                           substituteWild(
+                                               *found_rrset->second, name),
+                                           RESULT_WILDCARD |
+                                           (use_nsec3_ ?
+                                            RESULT_NSEC3_SIGNED :
+                                            RESULT_NSEC_SIGNED)));
+                    } else {
+                        // No matched QTYPE, this case is for WILDCARD_NXRRSET
+                        if (use_nsec3_) {
+                            return (FindResult(NXRRSET, RRsetPtr(),
+                                               RESULT_WILDCARD |
+                                               RESULT_NSEC3_SIGNED));
+                        }
+                        const Name new_name =
+                            Name("*").concatenate(wild_suffix);
+                        found_rrset = domain->second.find(RRType::NSEC());
+                        assert(found_rrset != domain->second.end());
+                        return (FindResult(NXRRSET,
+                                           substituteWild(
+                                               *found_rrset->second,
+                                               new_name),
+                                           RESULT_WILDCARD |
+                                           RESULT_NSEC_SIGNED));
+                    }
+                } else {
                     // This is empty non terminal name case on wildcard.
-                    Name emptyName = Name("*").concatenate(wild_suffix);
+                    const Name empty_name = Name("*").concatenate(wild_suffix);
+                    if (use_nsec3_) {
+                        return (FindResult(NXRRSET, RRsetPtr(),
+                                           RESULT_WILDCARD |
+                                           RESULT_NSEC3_SIGNED));
+                    }
                     for (Domains::reverse_iterator it = domains_.rbegin();
-                        it != domains_.rend();
-                        ++it) {
-                            RRsetStore::const_iterator nsec_it;
-                            if ((*it).first < emptyName &&
+                         it != domains_.rend();
+                         ++it) {
+                        RRsetStore::const_iterator nsec_it;
+                        if ((*it).first < empty_name &&
                             (nsec_it = (*it).second.find(RRType::NSEC()))
                             != (*it).second.end()) {
-                                return (FindResult(WILDCARD_NXRRSET,
-                                                   (*nsec_it).second));
-                            }
+                            return (FindResult(NXRRSET, (*nsec_it).second,
+                                               RESULT_WILDCARD |
+                                               RESULT_NSEC_SIGNED));
                         }
+                    }
                 }
-                return (FindResult(WILDCARD_NXRRSET,RRsetPtr()));
+                return (FindResult(NXRRSET, RRsetPtr(), RESULT_WILDCARD));
              }
         }
         const Name cnamewild_suffix("cnamewild.example.com");
@@ -476,8 +598,11 @@ MockZoneFinder::find(const Name& name, const RRType& type,
             RRsetStore::const_iterator found_rrset =
                 domain->second.find(RRType::CNAME());
             assert(found_rrset != domain->second.end());
-            return (FindResult(WILDCARD_CNAME,
-                               substituteWild(*found_rrset->second, name)));
+            return (FindResult(CNAME,
+                               substituteWild(*found_rrset->second, name),
+                               RESULT_WILDCARD |
+                               (use_nsec3_ ? RESULT_NSEC3_SIGNED :
+                                RESULT_NSEC_SIGNED)));
         }
     }
 
@@ -489,6 +614,10 @@ MockZoneFinder::find(const Name& name, const RRType& type,
     // we don't care about pathological cases such as the name is "smaller"
     // than the origin)
     if ((options & FIND_DNSSEC) != 0) {
+        if (use_nsec3_) {
+            return (FindResult(NXDOMAIN, RRsetPtr(), RESULT_NSEC3_SIGNED));
+        }
+
         // Emulate a broken DataSourceClient for some special names.
         if (nsec_result_ && nsec_name_ == name) {
             return (*nsec_result_);
@@ -504,7 +633,8 @@ MockZoneFinder::find(const Name& name, const RRType& type,
             if ((*it).first < name &&
                 (nsec_it = (*it).second.find(RRType::NSEC()))
                 != (*it).second.end()) {
-                return (FindResult(NXDOMAIN, (*nsec_it).second));
+                return (FindResult(NXDOMAIN, (*nsec_it).second,
+                                   RESULT_NSEC_SIGNED));
             }
         }
     }
@@ -1317,4 +1447,117 @@ TEST_F(QueryTest, MaxLenDNAME) {
     EXPECT_TRUE(ok) << "The synthetized CNAME not found";
 }
 
+// Test for this test module itself
+void
+nsec3Check(bool expected_matched, uint8_t expected_labels,
+           const string& expected_rrsets_txt,
+           const ZoneFinder::FindNSEC3Result& result)
+{
+    vector<ConstRRsetPtr> actual_rrsets;
+    EXPECT_EQ(expected_matched, result.matched);
+    // Convert to int so the error messages would be more readable:
+    EXPECT_EQ(static_cast<int>(expected_labels),
+              static_cast<int>(result.closest_labels));
+    if (result.closest_proof) {
+        actual_rrsets.push_back(result.closest_proof);
+    }
+    if (result.next_proof) {
+        actual_rrsets.push_back(result.next_proof);
+    }
+    rrsetsCheck(expected_rrsets_txt, actual_rrsets.begin(),
+                actual_rrsets.end());
+}
+
+TEST_F(QueryTest, findNSEC3) {
+    // In all test cases in the recursive mode, the closest encloser is the
+    // apex, and result's closest_labels should be the number of apex labels.
+    // (In non recursive mode closest_labels should be the # labels of the
+    // query name)
+    const uint8_t expected_closest_labels =
+        Name("example.com").getLabelCount();
+
+    // Apex name.  It should have a matching NSEC3
+    nsec3Check(true, expected_closest_labels, nsec3_apex_txt,
+               mock_finder->findNSEC3(Name("example.com"), false));
+
+    // Recursive mode doesn't change the result in this case.
+    nsec3Check(true, expected_closest_labels, nsec3_apex_txt,
+               mock_finder->findNSEC3(Name("example.com"), true)); 
+
+    // Non existent name.  Disabling recursion, a covering NSEC3 should be
+    // returned.
+    nsec3Check(false, 4, nsec3_www_txt,
+               mock_finder->findNSEC3(Name("nxdomain.example.com"), false));
+
+    // Non existent name.  The closest provable encloser is the apex,
+    // and next closer is the query name.
+    nsec3Check(true, expected_closest_labels,
+               string(nsec3_apex_txt) + string(nsec3_www_txt),
+               mock_finder->findNSEC3(Name("nxdomain.example.com"), true));
+
+    // Similar to the previous case, but next closer name is different
+    // (is the parent) of the non existent name.
+    nsec3Check(true, expected_closest_labels,
+               string(nsec3_apex_txt) + string(nsec3_www_txt),
+               mock_finder->findNSEC3(Name("nx.domain.example.com"), true));
+
+    // In the rest of test we check hash comparison for wrap around cases.
+    nsec3Check(false, 4, nsec3_apex_txt,
+               mock_finder->findNSEC3(Name("nxdomain2.example.com"), false));
+    nsec3Check(false, 4, nsec3_www_txt,
+               mock_finder->findNSEC3(Name("nxdomain3.example.com"), false));
+}
+
+// The following are tentative tests until we really add tests for the
+// query logic for these cases.  At that point it's probably better to
+// clean them up.
+TEST_F(QueryTest, nxdomainWithNSEC3) {
+    mock_finder->setNSEC3Flag(true);
+    ZoneFinder::FindResult result = mock_finder->find(
+        Name("nxdomain.example.com"), RRType::A(), ZoneFinder::FIND_DNSSEC);
+    EXPECT_EQ(ZoneFinder::NXDOMAIN, result.code);
+    EXPECT_FALSE(result.rrset);
+    EXPECT_TRUE(result.isNSEC3Signed());
+    EXPECT_FALSE(result.isWildcard());
+}
+
+TEST_F(QueryTest, nxrrsetWithNSEC3) {
+    mock_finder->setNSEC3Flag(true);
+    ZoneFinder::FindResult result = mock_finder->find(
+        Name("www.example.com"), RRType::TXT(), ZoneFinder::FIND_DNSSEC);
+    EXPECT_EQ(ZoneFinder::NXRRSET, result.code);
+    EXPECT_FALSE(result.rrset);
+    EXPECT_TRUE(result.isNSEC3Signed());
+    EXPECT_FALSE(result.isWildcard());
+}
+
+TEST_F(QueryTest, emptyNameWithNSEC3) {
+    mock_finder->setNSEC3Flag(true);
+    ZoneFinder::FindResult result = mock_finder->find(
+        Name("no.example.com"), RRType::A(), ZoneFinder::FIND_DNSSEC);
+    EXPECT_EQ(ZoneFinder::NXRRSET, result.code);
+    EXPECT_FALSE(result.rrset);
+    EXPECT_TRUE(result.isNSEC3Signed());
+    EXPECT_FALSE(result.isWildcard());
+}
+
+TEST_F(QueryTest, wildcardNxrrsetWithNSEC3) {
+    mock_finder->setNSEC3Flag(true);
+    ZoneFinder::FindResult result = mock_finder->find(
+        Name("www1.uwild.example.com"), RRType::TXT(),
+        ZoneFinder::FIND_DNSSEC);
+    EXPECT_EQ(ZoneFinder::NXRRSET, result.code);
+    EXPECT_FALSE(result.rrset);
+    EXPECT_TRUE(result.isNSEC3Signed());
+    EXPECT_TRUE(result.isWildcard());
+}
+
+TEST_F(QueryTest, wildcardEmptyWithNSEC3) {
+    mock_finder->setNSEC3Flag(true);
+    ZoneFinder::FindResult result = mock_finder->find(
+        Name("a.t.example.com"), RRType::A(), ZoneFinder::FIND_DNSSEC);
+    EXPECT_EQ(ZoneFinder::NXRRSET, result.code);
+    EXPECT_TRUE(result.isNSEC3Signed());
+    EXPECT_TRUE(result.isWildcard());
+}
 }

+ 6 - 0
src/lib/datasrc/database.cc

@@ -892,6 +892,12 @@ DatabaseClient::Finder::findInternal(const isc::dns::Name& name,
     }
 }
 
+ZoneFinder::FindNSEC3Result
+DatabaseClient::Finder::findNSEC3(const Name&, bool) {
+    isc_throw(NotImplemented, "findNSEC3 is not yet implemented for database "
+              "data source");
+}
+
 Name
 DatabaseClient::Finder::findPreviousName(const Name& name) const {
     const string str(accessor_->findPreviousName(zone_id_,

+ 6 - 0
src/lib/datasrc/database.h

@@ -754,6 +754,12 @@ public:
         virtual isc::dns::Name findPreviousName(const isc::dns::Name& query)
             const;
 
+        /// Look for NSEC3 for proving (non)existence of given name.
+        ///
+        /// See documentation in \c Zone.
+        virtual FindNSEC3Result
+        findNSEC3(const isc::dns::Name& name, bool recursive);
+
         /// \brief The zone ID
         ///
         /// This function provides the stored zone ID as passed to the

+ 6 - 0
src/lib/datasrc/memory_datasrc.cc

@@ -641,6 +641,12 @@ InMemoryZoneFinder::findAll(const Name& name,
     return (impl_->find(name, RRType::ANY(), &target, options));
 }
 
+ZoneFinder::FindNSEC3Result
+InMemoryZoneFinder::findNSEC3(const Name&, bool) {
+    isc_throw(NotImplemented, "findNSEC3 is not yet implemented for in memory "
+              "data source");
+}
+
 result::Result
 InMemoryZoneFinder::add(const ConstRRsetPtr& rrset) {
     return (impl_->add(rrset, &impl_->domains_));

+ 6 - 0
src/lib/datasrc/memory_datasrc.h

@@ -83,6 +83,12 @@ public:
                                std::vector<isc::dns::ConstRRsetPtr>& target,
                                const FindOptions options = FIND_DEFAULT);
 
+    /// Look for NSEC3 for proving (non)existence of given name.
+    ///
+    /// See documentation in \c Zone.
+    virtual FindNSEC3Result
+    findNSEC3(const isc::dns::Name& name, bool recursive);
+
     /// \brief Imelementation of the ZoneFinder::findPreviousName method
     ///
     /// This one throws NotImplemented exception, as InMemory doesn't

+ 181 - 9
src/lib/datasrc/zone.h

@@ -15,6 +15,9 @@
 #ifndef __ZONE_H
 #define __ZONE_H 1
 
+#include <utility>
+#include <vector>
+
 #include <dns/rrset.h>
 #include <dns/rrsetlist.h>
 
@@ -139,19 +142,41 @@ public:
         WILDCARD_NXRRSET ///< NXRRSET on wildcard, for DNSSEC
     };
 
+    /// Special attribute flags on the result of the \c find() method
+    ///
+    /// The flag values defined here are intended to signal to the caller
+    /// that it may need special handling on the result.  This is particularly
+    /// of concern when DNSSEC is requested.  For example, for negative
+    /// responses the caller would want to know whether the zone is signed
+    /// with NSEC or NSEC3 so that it can subsequently provide necessary
+    /// proof of the result.
+    ///
+    /// The caller is generally expected to get access to the information
+    /// via read-only getter methods of \c FindResult so that it won't rely
+    /// on specific details of the representation of the flags.  So these
+    /// definitions are basically only meaningful for data source
+    /// implementations.
+    enum FindResultFlags {
+        RESULT_DEFAULT = 0,       ///< The default flags
+        RESULT_WILDCARD = 1,      ///< find() resulted in a wildcard match
+        RESULT_NSEC_SIGNED = 2,   ///< The zone is signed with NSEC RRs
+        RESULT_NSEC3_SIGNED = 4   ///< The zone is signed with NSEC3 RRs
+    };
+
     /// A helper structure to represent the search result of \c find().
     ///
     /// This is a straightforward tuple of the result code and a pointer
-    /// to the found RRset to represent the result of \c find()
-    /// (there will be more members in the future - see the class
-    /// description).
+    /// (and optionally special flags) to the found RRset to represent the
+    /// result of \c find() (there will be more members in the future -
+    /// see the class description).
     /// We use this in order to avoid overloading the return value for both
     /// the result code ("success" or "not found") and the found object,
     /// i.e., avoid using \c NULL to mean "not found", etc.
     ///
     /// This is a simple value class whose internal state never changes,
-    /// so for convenience we allow the applications to refer to the members
-    /// directly.
+    /// so for convenience we allow the applications to refer to some of the
+    /// members directly.  For others we provide read-only accessor methods
+    /// to hide specific representation.
     ///
     /// Note: we should eventually include a notion of "zone node", which
     /// corresponds to a particular domain name of the zone, so that we can
@@ -162,11 +187,39 @@ public:
     /// optimize including the NSEC for no-wildcard proof (FWIW NSD does that).
     struct FindResult {
         FindResult(Result param_code,
-                   const isc::dns::ConstRRsetPtr param_rrset) :
-            code(param_code), rrset(param_rrset)
+                   const isc::dns::ConstRRsetPtr param_rrset,
+                   FindResultFlags param_flags = RESULT_DEFAULT) :
+            code(param_code), rrset(param_rrset), flags(param_flags)
         {}
         const Result code;
         const isc::dns::ConstRRsetPtr rrset;
+
+        /// Return true iff find() results in a wildcard match.
+        bool isWildcard() const { return ((flags & RESULT_WILDCARD) != 0); }
+
+        /// Return true when the underlying zone is signed with NSEC.
+        ///
+        /// The \c find() implementation allow this to return false if
+        /// \c FIND_DNSSEC isn't specified regardless of whether the zone
+        /// is signed or which of NSEC/NSEC3 is used.
+        ///
+        /// When this is returned, the implementation of find() must ensure
+        /// that \c rrset be a valid NSEC RRset as described in \c find()
+        /// documentation.
+        bool isNSECSigned() const {
+            return ((flags & RESULT_NSEC_SIGNED) != 0);
+        }
+
+        /// Return true when the underlying zone is signed with NSEC3.
+        ///
+        /// The \c find() implementation allow this to return false if
+        /// \c FIND_DNSSEC isn't specified regardless of whether the zone
+        /// is signed or which of NSEC/NSEC3 is used.
+        bool isNSEC3Signed() const {
+            return ((flags & RESULT_NSEC3_SIGNED) != 0);
+        }
+    private:
+        FindResultFlags flags;
     };
 
     /// Find options.
@@ -226,10 +279,15 @@ public:
     ///   of \c DELEGATION and the NS RRset at the zone cut.
     /// - If there is no matching name, it returns the code of \c NXDOMAIN,
     ///   and, if DNSSEC is requested, the NSEC RRset that proves the
-    ///   non-existence.
+    ///   non-existence if the zone is signed with NSEC.
     /// - If there is a matching name but no RRset of the search type, it
     ///   returns the code of \c NXRRSET, and, if DNSSEC is required,
-    ///   the NSEC RRset for that name.
+    ///   the NSEC RRset for that name if the zone is signed with NSEC.
+    /// - If there is no matching name but there is a matching wild card name,
+    ///   but it doesn't have a requested type of RR, and if DNSSEC is
+    ///   required, then it returns the code of \c WILDCARD_NXRRSET.
+    ///   If the zone is signed with NSEC, it returns corresponding NSEC
+    ///   (see the description of \c Result).
     /// - If there is a CNAME RR of the searched name but there is no
     ///   RR of the searched type of the name (so this type is different from
     ///   CNAME), it returns the code of \c CNAME and that CNAME RR.
@@ -306,6 +364,108 @@ public:
                                std::vector<isc::dns::ConstRRsetPtr> &target,
                                const FindOptions options = FIND_DEFAULT) = 0;
 
+    /// A helper structure to represent the search result of \c findNSEC3().
+    ///
+    /// The idea is similar to that of \c FindResult, but \c findNSEC3() has
+    /// special interface and semantics, we use a different structure to
+    /// represent the result.
+    struct FindNSEC3Result {
+        FindNSEC3Result(bool param_matched, uint8_t param_closest_labels,
+                        isc::dns::ConstRRsetPtr param_closest_proof,
+                        isc::dns::ConstRRsetPtr param_next_proof) :
+            matched(param_matched), closest_labels(param_closest_labels),
+            closest_proof(param_closest_proof),
+            next_proof(param_next_proof)
+        {}
+
+        /// true iff closest_proof is a matching NSEC3
+        const bool matched;
+
+        /// The number of labels of the identified closest encloser.
+        ///
+        const uint8_t closest_labels;
+
+        /// Either the NSEC3 for the closest provable encloser of the given
+        /// name or NSEC3 that covers the name
+        const isc::dns::ConstRRsetPtr closest_proof;
+
+        /// When non NULL, NSEC3 for the next closer name.
+        const isc::dns::ConstRRsetPtr next_proof;
+    };
+
+    /// Search the zone for the NSEC3 RR(s) that prove existence or non
+    /// existence of a give name.
+    ///
+    /// It searches the NSEC3 namespace of the zone (how that namespace is
+    /// implemented can vary in specific data source implementation) for NSEC3
+    /// RRs that match or cover the NSEC3 hash value for the given name.
+    ///
+    /// If \c recursive is false, it will first look for the NSEC3 that has
+    /// a matching hash.  If it doesn't exist, it identifies the covering NSEC3
+    /// for the hash.  In either case the search stops at that point and the
+    /// found NSEC3 RR(set) will be returned in the closest_proof member of
+    /// \c FindNSEC3Result.  \c matched is true or false depending on
+    /// the found NSEC3 is a matched one or covering one.  \c next_proof
+    /// is always NULL.  closest_labels must be equal to the number of
+    /// labels of \c name (and therefore meaningless).
+    ///
+    /// If \c recursive is true, it will continue the search toward the zone
+    /// apex (origin name) until it finds a provable encloser, that is,
+    /// an ancestor of \c name that has a matching NSEC3.  This is the closest
+    /// provable encloser of \c name as defined in RFC5155.  In this case,
+    /// if the found encloser is not equal to \c name, the search should
+    /// have seen a covering NSEC3 for the immediate child of the found
+    /// encloser.  That child name is the next closer name as defined in
+    /// RFC5155.  In this case, this method returns the NSEC3 for the
+    /// closest encloser in \c closest_proof, and the NSEC3 for the next
+    /// closer name in \c next_proof of \c FindNSEC3Result.  This set of
+    /// NSEC3 RRs provide the closest encloser proof as defined in RFC5155.
+    /// closest_labels will be set to the number of labels of the identified
+    /// closest encloser.  This will be useful when the caller needs to
+    /// construct the closest encloser name from the original \c name.
+    /// If, on the other hand, the found closest name is equal to \c name,
+    /// this method simply returns it in \c closest_proof.  \c next_proof
+    /// is set to NULL.  In all cases \c matched is set to true.
+    /// closest_labels will be set to the number of labels of \c name.
+    ///
+    /// When looking for NSEC3, this method retrieves NSEC3 parameters from
+    /// the corresponding zone to calculate hash values.  Actual implementation
+    /// of how to do this will differ in different data sources.  If the
+    /// NSEC3 parameters are not available \c DataSourceError exception
+    /// will be thrown.
+    ///
+    /// \note This implicitly means this method assumes the zone does not
+    /// have more than one set of parameters.  This assumption should be
+    /// reasonable in actual deployment and will help simplify the interface
+    /// and implementation.  But if there's a real need for supporting
+    /// multiple sets of parameters in a single zone, we will have to
+    /// extend this method so that, e.g., the caller can specify the parameter
+    /// set.
+    ///
+    /// In general, this method expects the zone is properly signed with NSEC3
+    /// RRs.  Specifically, it assumes at least the apex node has a matching
+    /// NSEC3 RR (so the search in the recursive mode must always succeed);
+    /// it also assumes that it can retrieve NSEC parameters (iterations,
+    /// algorithm, and salt) from the zone as noted above.  If these
+    /// assumptions aren't met, \c DataSourceError exception will be thrown.
+    ///
+    /// \exception InvalidParameter name is not a subdomain of the zone origin
+    /// \exception DataSourceError Low-level or internal datasource errors
+    /// happened, or the zone isn't properly signed with NSEC3
+    /// (NSEC3 parameters cannot be found, no NSEC3s are available, etc).
+    /// \exception std::bad_alloc The underlying implementation involves
+    /// memory allocation and it fails
+    ///
+    /// \param name The name for which NSEC3 RRs are to be found.  It must
+    /// be a subdomain of the zone.
+    /// \param recursive Whether or not search should continue until it finds
+    /// a provable encloser (see above).
+    ///
+    /// \return The search result and whether or not the closest_proof is
+    /// a matching NSEC3, in the form of \c FindNSEC3Result object.
+    virtual FindNSEC3Result
+    findNSEC3(const isc::dns::Name& name, bool recursive) = 0;
+
     /// \brief Get previous name in the zone
     ///
     /// Gets the previous name in the DNSSEC order. This can be used
@@ -346,6 +506,18 @@ inline ZoneFinder::FindOptions operator |(ZoneFinder::FindOptions a,
                                                  static_cast<unsigned>(b)));
 }
 
+/// \brief Operator to combine FindResultFlags
+///
+/// Similar to the same operator for \c FindOptions.  Refer to the description
+/// of that function.
+inline ZoneFinder::FindResultFlags operator |(
+    ZoneFinder::FindResultFlags a,
+    ZoneFinder::FindResultFlags b)
+{
+    return (static_cast<ZoneFinder::FindResultFlags>(
+                static_cast<unsigned>(a) | static_cast<unsigned>(b)));
+}
+
 /// \brief A pointer-like type pointing to a \c ZoneFinder object.
 typedef boost::shared_ptr<ZoneFinder> ZoneFinderPtr;