Browse Source

[master] Merge branch 'trac1574b'

JINMEI Tatuya 13 years ago
parent
commit
548926e319

+ 43 - 63
src/lib/datasrc/datasrc_messages.mes

@@ -16,6 +16,13 @@ $NAMESPACE isc::datasrc
 
 # \brief Messages for the data source library
 
+% DATASRC_BAD_NSEC3_NAME NSEC3 record has a bad owner name '%1'
+The software refuses to load NSEC3 records into a wildcard domain or
+the owner name has two or more labels below the zone origin.
+It isn't explicitly forbidden, but no sane zone wouldn have such names
+for NSEC3.  BIND 9 also refuses NSEC3 at wildcard, so this behavior is
+compatible with BIND 9.
+
 % DATASRC_CACHE_CREATE creating the hotspot cache
 This is a debug message issued during startup when the hotspot cache
 is created.
@@ -143,6 +150,34 @@ were found to be different. This isn't allowed on the wire and is considered
 an error, so we set it to the lowest value we found (but we don't modify the
 database). The data in database should be checked and fixed.
 
+% DATASRC_DATABASE_JOURNALREADER_END %1/%2 on %3 from %4 to %5
+This is a debug message indicating that the program (successfully)
+reaches the end of sequences of a zone's differences.  The zone's name
+and class, database name, and the start and end serials are shown in
+the message.
+
+% DATASRC_DATABASE_JOURNALREADER_NEXT %1/%2 in %3/%4 on %5
+This is a debug message indicating that the program retrieves one
+difference in difference sequences of a zone and successfully converts
+it to an RRset.  The zone's name and class, database name, and the
+name and RR type of the retrieved diff are shown in the message.
+
+% DATASRC_DATABASE_JOURNALREADER_START %1/%2 on %3 from %4 to %5
+This is a debug message indicating that the program starts reading
+a zone's difference sequences from a database-based data source.  The
+zone's name and class, database name, and the start and end serials
+are shown in the message.
+
+% DATASRC_DATABASE_JOURNALREADR_BADDATA failed to convert a diff to RRset in %1/%2 on %3 between %4 and %5: %6
+This is an error message indicating that a zone's diff is broken and
+the data source library failed to convert it to a valid RRset.  The
+most likely cause of this is that someone has manually modified the
+zone's diff in the database and inserted invalid data as a result.
+The zone's name and class, database name, and the start and end
+serials, and an additional detail of the error are shown in the
+message.  The administrator should examine the diff in the database
+to find any invalid data and fix it.
+
 % DATASRC_DATABASE_NO_MATCH not match for %2/%3/%4 in %1
 No match (not even a wildcard) was found in the named data source for the given
 name/type/class in the data source.
@@ -307,6 +342,14 @@ Debug information. The content of master file is being loaded into the memory.
 % DATASRC_MEM_NOT_FOUND requested domain '%1' not found
 Debug information. The requested domain does not exist.
 
+% DATASRC_MEM_NO_NSEC3PARAM NSEC3PARAM is missing for NSEC3-signed zone %1/%2
+The in-memory data source has loaded a zone signed with NSEC3 RRs,
+but it doesn't have a NSEC3PARAM RR at the zone origin.  It's likely that
+the zone is somehow broken, but this RR is not necessarily needed for
+handling lookups with NSEC3 in this data source, so it accepts the given
+content of the zone.  Nevertheless the administrator should look into
+the integrity of the zone data.
+
 % DATASRC_MEM_NS_ENCOUNTERED encountered a NS
 Debug information. While searching for the requested domain, a NS was
 encountered on the way (a delegation). This may lead to stop of the search.
@@ -671,66 +714,3 @@ data source.
 % DATASRC_UNEXPECTED_QUERY_STATE unexpected query state
 This indicates a programming error. An internal task of unknown type was
 generated.
-
-% DATASRC_DATABASE_UPDATER_CREATED zone updater created for '%1/%2' on %3
-Debug information.  A zone updater object is created to make updates to
-the shown zone on the shown backend database.
-
-% DATASRC_DATABASE_UPDATER_DESTROYED zone updater destroyed for '%1/%2' on %3
-Debug information.  A zone updater object is destroyed, either successfully
-or after failure of, making updates to the shown zone on the shown backend
-database.
-
-%DATASRC_DATABASE_UPDATER_ROLLBACK zone updates roll-backed for '%1/%2' on %3
-A zone updater is being destroyed without committing the changes.
-This would typically mean the update attempt was aborted due to some
-error, but may also be a bug of the application that forgets committing
-the changes.  The intermediate changes made through the updater won't
-be applied to the underlying database.  The zone name, its class, and
-the underlying database name are shown in the log message.
-
-%DATASRC_DATABASE_UPDATER_ROLLBACKFAIL failed to roll back zone updates for '%1/%2' on %3: %4
-A zone updater is being destroyed without committing the changes to
-the database, and attempts to rollback incomplete updates, but it
-unexpectedly fails.  The higher level implementation does not expect
-it to fail, so this means either a serious operational error in the
-underlying data source (such as a system failure of a database) or
-software bug in the underlying data source implementation.  In either
-case if this message is logged the administrator should carefully
-examine the underlying data source to see what exactly happens and
-whether the data is still valid.  The zone name, its class, and the
-underlying database name as well as the error message thrown from the
-database module are shown in the log message.
-
-% DATASRC_DATABASE_UPDATER_COMMIT updates committed for '%1/%2' on %3
-Debug information.  A set of updates to a zone has been successfully
-committed to the corresponding database backend.  The zone name,
-its class and the database name are printed.
-
-% DATASRC_DATABASE_JOURNALREADER_START %1/%2 on %3 from %4 to %5
-This is a debug message indicating that the program starts reading
-a zone's difference sequences from a database-based data source.  The
-zone's name and class, database name, and the start and end serials
-are shown in the message.
-
-% DATASRC_DATABASE_JOURNALREADER_NEXT %1/%2 in %3/%4 on %5
-This is a debug message indicating that the program retrieves one
-difference in difference sequences of a zone and successfully converts
-it to an RRset.  The zone's name and class, database name, and the
-name and RR type of the retrieved diff are shown in the message.
-
-% DATASRC_DATABASE_JOURNALREADER_END %1/%2 on %3 from %4 to %5
-This is a debug message indicating that the program (successfully)
-reaches the end of sequences of a zone's differences.  The zone's name
-and class, database name, and the start and end serials are shown in
-the message.
-
-% DATASRC_DATABASE_JOURNALREADR_BADDATA failed to convert a diff to RRset in %1/%2 on %3 between %4 and %5: %6
-This is an error message indicating that a zone's diff is broken and
-the data source library failed to convert it to a valid RRset.  The
-most likely cause of this is that someone has manually modified the
-zone's diff in the database and inserted invalid data as a result.
-The zone's name and class, database name, and the start and end
-serials, and an additional detail of the error are shown in the
-message.  The administrator should examine the diff in the database
-to find any invalid data and fix it.

+ 260 - 54
src/lib/datasrc/memory_datasrc.cc

@@ -12,15 +12,20 @@
 // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 // PERFORMANCE OF THIS SOFTWARE.
 
+#include <algorithm>
 #include <map>
+#include <utility>
+#include <cctype>
 #include <cassert>
 #include <boost/shared_ptr.hpp>
+#include <boost/scoped_ptr.hpp>
 #include <boost/bind.hpp>
 #include <boost/foreach.hpp>
 
 #include <exceptions/exceptions.h>
 
 #include <dns/name.h>
+#include <dns/nsec3hash.h>
 #include <dns/rdataclass.h>
 #include <dns/rrclass.h>
 #include <dns/rrsetlist.h>
@@ -39,6 +44,7 @@ using namespace std;
 using namespace isc::dns;
 using namespace isc::dns::rdata;
 using namespace isc::data;
+using boost::scoped_ptr;
 
 namespace isc {
 namespace datasrc {
@@ -62,30 +68,63 @@ typedef boost::shared_ptr<Domain> DomainPtr;
 // The tree stores domains
 typedef RBTree<Domain> DomainTree;
 typedef RBNode<Domain> DomainNode;
-}
 
-// Private data and hidden methods of InMemoryZoneFinder
-struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
-    // Constructor
-    InMemoryZoneFinderImpl(const RRClass& zone_class, const Name& origin) :
-        zone_class_(zone_class), origin_(origin), origin_data_(NULL),
-        domains_(true)
-    {
+// Separate storage for NSEC3 RRs (and their RRSIGs).  It's an STL map
+// from string to the NSEC3 RRset.  The map key is the first label
+// (upper cased) of the owner name of the corresponding NSEC3 (i.e., map
+// value).  We can use  the standard string comparison (if the comparison
+// target is also upper cased) due to the nature of NSEC3 owner names.
+typedef map<string, ConstRRsetPtr> NSEC3Map;
+typedef NSEC3Map::value_type NSEC3Pair;
+
+// Actual zone data: Essentially a set of zone's RRs.  This is defined as
+// a separate structure so that it'll be replaceable on reload.
+struct ZoneData {
+    ZoneData(const Name& origin) : domains_(true), origin_data_(NULL) {
         // We create the node for origin (it needs to exist anyway in future)
         domains_.insert(origin, &origin_data_);
         DomainPtr origin_domain(new Domain);
         origin_data_->setData(origin_domain);
     }
+
+    // The main data (name + RRsets)
+    DomainTree domains_;
+
+    // Shortcut to the origin node, which should always exist
+    DomainNode* origin_data_;
+
+    // The optional NSEC3 related data
+    struct NSEC3Data {
+        NSEC3Data(const generic::NSEC3PARAM& nsec3param) :
+            hash_(NSEC3Hash::create(nsec3param))
+        {}
+        NSEC3Data(const generic::NSEC3& nsec3) :
+            hash_(NSEC3Hash::create(nsec3))
+        {}
+        NSEC3Map map_;    // Actual NSEC3 RRs
+        const scoped_ptr<NSEC3Hash> hash_; // hash parameter/calculator
+    };
+    scoped_ptr<NSEC3Data> nsec3_data_; // non NULL only when it's NSEC3 signed
+};
+}
+
+// Private data and hidden methods of InMemoryZoneFinder
+struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
+    // Constructor
+    InMemoryZoneFinderImpl(const RRClass& zone_class, const Name& origin) :
+        zone_class_(zone_class), origin_(origin),
+        zone_data_(new ZoneData(origin_))
+    {}
+
     static const DomainNode::Flags DOMAINFLAG_WILD = DomainNode::FLAG_USER1;
 
     // Information about the zone
     RRClass zone_class_;
     Name origin_;
-    DomainNode* origin_data_;
     string file_name_;
 
     // The actual zone data
-    DomainTree domains_;
+    scoped_ptr<ZoneData> zone_data_;
 
     // Add the necessary magic for any wildcard contained in 'name'
     // (including itself) to be found in the zone.
@@ -201,6 +240,14 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
             isc_throw(AddError, "multiple RRs of singleton type for "
                       << rrset->getName());
         }
+        // NSEC3/NSEC3PARAM is not a "singleton" per protocol, but this
+        // implementation requests it be so at the moment.
+        if ((rrset->getType() == RRType::NSEC3() ||
+             rrset->getType() == RRType::NSEC3PARAM()) &&
+            rrset->getRdataCount() > 1) {
+            isc_throw(AddError, "Multiple NSEC3/NSEC3PARAM RDATA is given for "
+                      << rrset->getName() << " which isn't supported");
+        }
 
         NameComparisonResult compare(origin_.compare(rrset->getName()));
         if (compare.getRelation() != NameComparisonResult::SUPERDOMAIN &&
@@ -234,19 +281,25 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
                           rrset->getName());
             }
         }
-    }
 
-    result::Result addRRsig(const ConstRRsetPtr sig_rrset,
-                            DomainTree& domains)
-    {
-        DomainNode* node = NULL;
-        if (domains.find(sig_rrset->getName(), &node) !=
-            DomainTree::EXACTMATCH || node == NULL || !node->getData()) {
-            isc_throw(AddError,
-                      "RRSIG is being added, but no RR to be covered: "
-                      << sig_rrset->getName());
+        // Owner names of NSEC3 have special format as defined in RFC5155,
+        // and cannot be a wildcard name or must be one label longer than
+        // the zone origin.  While the RFC doesn't prohibit other forms of
+        // names, no sane zone would have such names for NSEC3.
+        // BIND 9 also refuses NSEC3 at wildcard.
+        if (rrset->getType() == RRType::NSEC3() &&
+            (rrset->getName().isWildcard() ||
+             rrset->getName().getLabelCount() !=
+             origin_.getLabelCount() + 1)) {
+            LOG_ERROR(logger, DATASRC_BAD_NSEC3_NAME).
+                arg(rrset->getName());
+            isc_throw(AddError, "Invalid NSEC3 owner name: " <<
+                      rrset->getName());
         }
+    }
 
+    result::Result addRRsig(const ConstRRsetPtr sig_rrset, ZoneData& zone_data)
+    {
         // Check consistency of the type covered.
         // We know the RRset isn't empty, so the following check is safe.
         RdataIteratorPtr rit = sig_rrset->getRdataIterator();
@@ -262,16 +315,50 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
 
         // Find the RRset to be covered; if not found, treat it as an error
         // for now.
-        const Domain::const_iterator it = node->getData()->find(covered);
-        if (it == node->getData()->end()) {
-            isc_throw(AddError,
-                      "RRSIG is being added, but no RR of covered type found: "
-                      << sig_rrset->toText());
+        ConstRRsetPtr covered_rrset;
+        if (covered != RRType::NSEC3()) {
+            DomainNode* node = NULL;
+            if (zone_data.domains_.find(sig_rrset->getName(), &node) !=
+                DomainTree::EXACTMATCH || node == NULL || !node->getData()) {
+                isc_throw(AddError,
+                          "RRSIG is being added, but no RR to be covered: "
+                          << sig_rrset->getName());
+            }
+            const Domain::const_iterator it = node->getData()->find(covered);
+            if (it != node->getData()->end()) {
+                covered_rrset = it->second;
+            }
+        } else {
+            // In case of NSEC3 if something is found it must be NSEC3 RRset
+            // under the assumption of our current implementation.
+            if (zone_data.nsec3_data_) {
+                // Convert the first label to upper-cased text.  Note that
+                // for a valid NSEC3 RR the label should only consist of
+                // positive 8-bit char values, so using toupper(int) should be
+                // safe (if it's a bogus label for NSEC3 the zone won't work
+                // anyway).  Also note the '::' below: g++'s STL implementation
+                // seems to require it to toupper to make this compile.
+                string fst_label =
+                    sig_rrset->getName().split(0, 1).toText(true);
+                transform(fst_label.begin(), fst_label.end(),
+                          fst_label.begin(), ::toupper);
+
+                NSEC3Map::const_iterator found =
+                    zone_data.nsec3_data_->map_.find(fst_label);
+                if (found != zone_data.nsec3_data_->map_.end()) {
+                    covered_rrset = found->second;
+                    assert(covered_rrset->getType() == covered);
+                }
+            }
+        }
+        if (!covered_rrset) {
+            isc_throw(AddError, "RRSIG is being added, but no RR of "
+                      "covered type found: " << sig_rrset->toText());
         }
 
         // The current implementation doesn't allow an existing RRSIG to be
         // overridden (or updated with additional ones).
-        if ((it->second)->getRRsig()) {
+        if (covered_rrset->getRRsig()) {
             isc_throw(AddError,
                       "RRSIG is being added to override an existing one: "
                       << sig_rrset->toText());
@@ -286,17 +373,47 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
         // Note: there's a slight chance of getting an exception.
         // As noted in add(), we give up strong exception guarantee in such
         // cases.
-        boost::const_pointer_cast<RRset>(it->second)->addRRsig(sig_rrset);
+        boost::const_pointer_cast<RRset>(covered_rrset)->addRRsig(sig_rrset);
 
         return (result::SUCCESS);
     }
 
+    result::Result addNSEC3(const ConstRRsetPtr rrset, ZoneData& zone_data) {
+        // We know rrset has exactly one RDATA
+        const generic::NSEC3& nsec3_rdata =
+            dynamic_cast<const generic::NSEC3&>(
+                rrset->getRdataIterator()->getCurrent());
+
+        // If we've not done any NSEC3 setup for the zone, do it now;
+        // otherwise check parameter consistency.
+        if (!zone_data.nsec3_data_) {
+            zone_data.nsec3_data_.reset(new ZoneData::NSEC3Data(nsec3_rdata));
+        } else if (!zone_data.nsec3_data_->hash_->match(nsec3_rdata)) {
+            isc_throw(AddError, "NSEC3 with inconsistent parameters: " <<
+                      rrset->toText());
+        }
+
+        string fst_label = rrset->getName().split(0, 1).toText(true);
+        transform(fst_label.begin(), fst_label.end(), fst_label.begin(),
+                  ::toupper);
+
+        // Our current implementation doesn't allow an existing NSEC3 to be
+        // updated/overridden.
+        if (zone_data.nsec3_data_->map_.find(fst_label) !=
+            zone_data.nsec3_data_->map_.end()) {
+            return (result::EXIST);
+        }
+
+        zone_data.nsec3_data_->map_.insert(NSEC3Pair(fst_label, rrset));
+        return (result::SUCCESS);
+    }
+
     /*
      * Implementation of longer methods. We put them here, because the
      * access is without the impl_-> and it will get inlined anyway.
      */
     // Implementation of InMemoryZoneFinder::add
-    result::Result add(const ConstRRsetPtr& rrset, DomainTree* domains) {
+    result::Result add(const ConstRRsetPtr& rrset, ZoneData& zone_data) {
         // Sanitize input.  This will cause an exception to be thrown
         // if the input RRset is empty.
         addValidation(rrset);
@@ -305,21 +422,26 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
         LOG_DEBUG(logger, DBG_TRACE_DATA, DATASRC_MEM_ADD_RRSET).
             arg(rrset->getName()).arg(rrset->getType()).arg(origin_);
 
+        if (rrset->getType() == RRType::NSEC3()) {
+            return (addNSEC3(rrset, zone_data));
+        }
+
         // RRSIGs are special in various points, so we handle it in a
         // separate dedicated method.
         if (rrset->getType() == RRType::RRSIG()) {
-            return (addRRsig(rrset, *domains));
+            return (addRRsig(rrset, zone_data));
         }
 
         // Add wildcards possibly contained in the owner name to the domain
         // tree.
         // Note: this can throw an exception, breaking strong exception
         // guarantee.  (see also the note for contextCheck() below).
-        addWildcards(*domains, rrset->getName());
+        addWildcards(zone_data.domains_, rrset->getName());
 
         // Get the node
         DomainNode* node;
-        DomainTree::Result result = domains->insert(rrset->getName(), &node);
+        DomainTree::Result result = zone_data.domains_.insert(rrset->getName(),
+                                                              &node);
         // Just check it returns reasonable results
         assert((result == DomainTree::SUCCESS ||
                 result == DomainTree::ALREADYEXISTS) && node!= NULL);
@@ -355,6 +477,23 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
                 node->setFlag(DomainNode::FLAG_CALLBACK);
             }
 
+            // If we've added NSEC3PARAM at zone origin, set up NSEC3 specific
+            // data or check consistency with already set up parameters.
+            if (rrset->getType() == RRType::NSEC3PARAM() &&
+                rrset->getName() == origin_) {
+                // We know rrset has exactly one RDATA
+                const generic::NSEC3PARAM& param =
+                    dynamic_cast<const generic::NSEC3PARAM&>(
+                        rrset->getRdataIterator()->getCurrent());
+
+                if (!zone_data.nsec3_data_) {
+                    zone_data.nsec3_data_.reset(
+                        new ZoneData::NSEC3Data(param));
+                } else if (!zone_data.nsec3_data_->hash_->match(param)) {
+                    isc_throw(AddError, "NSEC3PARAM with inconsistent "
+                              "parameters: " << rrset->toText());
+                }
+            }
             return (result::SUCCESS);
         } else {
             // The RRSet of given type was already there
@@ -366,18 +505,18 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
      * Same as above, but it checks the return value and if it already exists,
      * it throws.
      */
-    void addFromLoad(const ConstRRsetPtr& set, DomainTree* domains) {
-            switch (add(set, domains)) {
-                case result::EXIST:
-                    LOG_ERROR(logger, DATASRC_MEM_DUP_RRSET).
-                        arg(set->getName()).arg(set->getType());
-                    isc_throw(dns::MasterLoadError, "Duplicate rrset: " <<
-                        set->toText());
-                case result::SUCCESS:
-                    return;
-                default:
-                    assert(0);
-            }
+    void addFromLoad(const ConstRRsetPtr& set, ZoneData* zone_data) {
+        switch (add(set, *zone_data)) {
+        case result::EXIST:
+            LOG_ERROR(logger, DATASRC_MEM_DUP_RRSET).
+                arg(set->getName()).arg(set->getType());
+            isc_throw(dns::MasterLoadError, "Duplicate rrset: " <<
+                      set->toText());
+        case result::SUCCESS:
+            return;
+        default:
+            assert(0);
+        }
     }
 
     // Maintain intermediate data specific to the search context used in
@@ -499,7 +638,8 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
         FindState state(options);
         RBTreeNodeChain<Domain> node_path;
         bool rename(false);
-        switch (domains_.find(name, &node, node_path, cutCallback, &state)) {
+        switch (zone_data_->domains_.find(name, &node, node_path, cutCallback,
+                                          &state)) {
             case DomainTree::PARTIALMATCH:
                 /*
                  * In fact, we could use a single variable instead of
@@ -584,7 +724,8 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
                     }
                     Name wildcard(Name("*").concatenate(
                         node_path.getAbsoluteName()));
-                    DomainTree::Result result(domains_.find(wildcard, &node));
+                    DomainTree::Result result =
+                        zone_data_->domains_.find(wildcard, &node);
                     /*
                      * Otherwise, why would the DOMAINFLAG_WILD be there if
                      * there was no wildcard under it?
@@ -626,8 +767,8 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
         // has a NS RR, we should return a delegation, but not in the apex.
         // There is one exception: the case for DS query, which should always
         // be considered in-zone lookup.
-        if (node->getFlag(DomainNode::FLAG_CALLBACK) && node != origin_data_ &&
-            type != RRType::DS()) {
+        if (node->getFlag(DomainNode::FLAG_CALLBACK) &&
+            node != zone_data_->origin_data_ && type != RRType::DS()) {
             found = node->getData()->find(RRType::NS());
             if (found != node->getData()->end()) {
                 LOG_DEBUG(logger, DBG_TRACE_DATA,
@@ -717,9 +858,57 @@ InMemoryZoneFinder::findNSEC3(const Name&, bool) {
               "data source");
 }
 
+ZoneFinder::FindNSEC3Result
+InMemoryZoneFinder::findNSEC3Tmp(const Name& name, bool recursive) {
+    if (!impl_->zone_data_->nsec3_data_) {
+        isc_throw(Unexpected, "findNSEC3 is called for non NSEC3 zone");
+    }
+    if (recursive) {
+        isc_throw(Unexpected, "recursive mode isn't expected in tests");
+    }
+
+    // A temporary workaround for testing: convert the original name to
+    // NSEC3-hashed name using hardcoded mapping.
+    string hname_text;
+    if (name == Name("example.org")) {
+        hname_text = "0P9MHAVEQVM6T7VBL5LOP2U3T2RP3TOM";
+    } else if (name == Name("www.example.org")) {
+        hname_text = "2S9MHAVEQVM6T7VBL5LOP2U3T2RP3TOM";
+    } else if (name == Name("xxx.example.org")) {
+        hname_text = "Q09MHAVEQVM6T7VBL5LOP2U3T2RP3TOM";
+    } else if (name == Name("yyy.example.org")) {
+        hname_text = "0A9MHAVEQVM6T7VBL5LOP2U3T2RP3TOM";
+    } else {
+        isc_throw(Unexpected, "unexpected name for NSEC3 test: " << name);
+    }
+
+    // Below we assume the map is not empty for simplicity.
+    NSEC3Map::const_iterator found =
+        impl_->zone_data_->nsec3_data_->map_.lower_bound(hname_text);
+    if (found != impl_->zone_data_->nsec3_data_->map_.end() &&
+        found->first == hname_text) {
+        // exact match
+        return (FindNSEC3Result(true, 2, found->second, ConstRRsetPtr()));
+    } else if (found == impl_->zone_data_->nsec3_data_->map_.end() ||
+               found == impl_->zone_data_->nsec3_data_->map_.begin()) {
+        // the search key is "smaller" than the smallest or "larger" than
+        // largest.  In either case "previous" is the largest one.
+        return (FindNSEC3Result(false, 2,
+                                impl_->zone_data_->nsec3_data_->map_.
+                                rbegin()->second, ConstRRsetPtr()));
+    } else {
+        // Otherwise, H(found_domain-1) < given_hash < H(found_domain)
+        // The covering proof is the first one.
+        return (FindNSEC3Result(false, 2, (--found)->second, ConstRRsetPtr()));
+    }
+
+    // We should have covered all cases.
+    isc_throw(Unexpected, "Impossible NSEC3 search result for " << name);
+}
+
 result::Result
 InMemoryZoneFinder::add(const ConstRRsetPtr& rrset) {
-    return (impl_->add(rrset, &impl_->domains_));
+    return (impl_->add(rrset, *impl_->zone_data_));
 }
 
 
@@ -727,13 +916,29 @@ void
 InMemoryZoneFinder::load(const string& filename) {
     LOG_DEBUG(logger, DBG_TRACE_BASIC, DATASRC_MEM_LOAD).arg(getOrigin()).
         arg(filename);
-    // Load it into a temporary tree
-    DomainTree tmp;
+    // Load it into temporary zone data
+    scoped_ptr<ZoneData> tmp(new ZoneData(getOrigin()));
+
     masterLoad(filename.c_str(), getOrigin(), getClass(),
-        boost::bind(&InMemoryZoneFinderImpl::addFromLoad, impl_, _1, &tmp));
+               boost::bind(&InMemoryZoneFinderImpl::addFromLoad, impl_,
+                           _1, tmp.get()));
+
+    // If the zone is NSEC3-signed, check if it has NSEC3PARAM
+    if (tmp->nsec3_data_) {
+        // Note: origin_data_ is set on creation of ZoneData, and the load
+        // process only adds new nodes (and their data), so this assertion
+        // should hold.
+        assert(tmp->origin_data_ != NULL && !tmp->origin_data_->isEmpty());
+        if (tmp->origin_data_->getData()->find(RRType::NSEC3PARAM()) ==
+            tmp->origin_data_->getData()->end()) {
+            LOG_WARN(logger, DATASRC_MEM_NO_NSEC3PARAM).
+                arg(getOrigin()).arg(getClass());
+        }
+    }
+
     // If it went well, put it inside
     impl_->file_name_ = filename;
-    tmp.swap(impl_->domains_);
+    tmp.swap(impl_->zone_data_);
     // And let the old data die with tmp
 }
 
@@ -924,8 +1129,9 @@ InMemoryClient::getIterator(const Name& name, bool separate_rrs) const {
         isc_throw(Unexpected, "The zone at " + name.toText() +
                   " is not InMemoryZoneFinder");
     }
-    return (ZoneIteratorPtr(new MemoryIterator(zone->impl_->domains_, name,
-                                               separate_rrs)));
+    return (ZoneIteratorPtr(new MemoryIterator(
+                                zone->impl_->zone_data_->domains_, name,
+                                separate_rrs)));
 }
 
 ZoneUpdaterPtr

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

@@ -89,6 +89,16 @@ public:
     virtual FindNSEC3Result
     findNSEC3(const isc::dns::Name& name, bool recursive);
 
+    // A temporary fake version of findNSEC3 for tests
+    //
+    // This method intentionally has the same interface as findNSEC3 but
+    // uses internally hardcoded hash values and offers a limited set
+    // of functionality for the convenience of tests.  This is a temporary
+    // workaround until #1577 is completed.  At that point this method
+    // should be removed.
+    FindNSEC3Result
+    findNSEC3Tmp(const isc::dns::Name& name, bool recursive);
+
     /// \brief Imelementation of the ZoneFinder::findPreviousName method
     ///
     /// This one throws NotImplemented exception, as InMemory doesn't

+ 2 - 0
src/lib/datasrc/tests/Makefile.am

@@ -110,6 +110,8 @@ endif
 EXTRA_DIST =  testdata/brokendb.sqlite3
 EXTRA_DIST += testdata/example.com.signed
 EXTRA_DIST += testdata/example.org
+EXTRA_DIST += testdata/example.org.nsec3-signed
+EXTRA_DIST += testdata/example.org.nsec3-signed-noparam
 EXTRA_DIST += testdata/example.org.sqlite3
 EXTRA_DIST += testdata/example2.com
 EXTRA_DIST += testdata/example2.com.sqlite3

+ 245 - 0
src/lib/datasrc/tests/memory_datasrc_unittest.cc

@@ -1307,4 +1307,249 @@ TEST_F(InMemoryZoneFinderTest, addbadRRsig) {
     EXPECT_THROW(zone_finder_.add(textToRRset(rrsig_a_txt)),
                  InMemoryZoneFinder::AddError);
 }
+
+//
+// (Faked) NSEC3 hash data.  Arbitrarily borrowed from RFC515 examples.
+//
+// Commonly used NSEC3 suffix.  It's incorrect to use it for all NSEC3s, but
+// doesn't matter for the purpose of our tests.
+const char* const nsec3_common = " 300 IN NSEC3 1 1 12 aabbccdd "
+    "2T7B4G4VSA5SMI47K61MV5BV1A22BOJR A RRSIG";
+// Likewise, common RRSIG suffix for NSEC3s.
+const char* const nsec3_rrsig_common = " 300 IN RRSIG NSEC3 5 3 3600 "
+    "20000101000000 20000201000000 12345 example.org. FAKEFAKEFAKE";
+
+// For apex (example.org)
+const char* const apex_hash = "0P9MHAVEQVM6T7VBL5LOP2U3T2RP3TOM";
+const char* const apex_hash_lower = "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom";
+// For ns1.example.org
+const char* const ns1_hash = "2T7B4G4VSA5SMI47K61MV5BV1A22BOJR";
+// For x.y.w.example.org (lower-cased)
+const char* const xrw_hash = "2vptu5timamqttgl4luu9kg21e0aor3s";
+
+void
+nsec3Check(bool expected_matched, const string& expected_rrsets_txt,
+           const ZoneFinder::FindNSEC3Result& result,
+           bool expected_sig = false)
+{
+    vector<ConstRRsetPtr> actual_rrsets;
+    EXPECT_EQ(expected_matched, result.matched);
+    ASSERT_TRUE(result.closest_proof);
+    if (expected_sig) {
+        ASSERT_TRUE(result.closest_proof->getRRsig());
+    }
+    actual_rrsets.push_back(result.closest_proof);
+    if (expected_sig) {
+        actual_rrsets.push_back(result.closest_proof->getRRsig());
+    }
+    rrsetsCheck(expected_rrsets_txt, actual_rrsets.begin(),
+                actual_rrsets.end());
+}
+
+// In the following tests we use a temporary faked version of findNSEC3
+// as the real version isn't implemented yet (it's a task for #1577).
+// When #1577 is done the tests should be updated using the real version.
+// If we can use fake hash calculator (see #1575), we should be able to
+// just replace findNSEC3Tmp with findNSEC3.
+
+TEST_F(InMemoryZoneFinderTest, addNSEC3) {
+    const string nsec3_text = string(apex_hash) + ".example.org." +
+        string(nsec3_common);
+    // This name shouldn't be found in the normal domain tree.
+    EXPECT_EQ(result::SUCCESS, zone_finder_.add(textToRRset(nsec3_text)));
+    EXPECT_EQ(ZoneFinder::NXDOMAIN,
+              zone_finder_.find(Name(string(apex_hash) + ".example.org"),
+                                RRType::NSEC3()).code);
+    // Dedicated NSEC3 find should be able to find it.
+    nsec3Check(true, nsec3_text,
+               zone_finder_.findNSEC3Tmp(Name("example.org"), false));
+
+    // This implementation rejects duplicate/update add of the same hash name
+    EXPECT_EQ(result::EXIST,
+              zone_finder_.add(textToRRset(
+                                   string(apex_hash) + ".example.org." +
+                                   string(nsec3_common) + " AAAA")));
+    // The original NSEC3 should be intact
+    nsec3Check(true, nsec3_text,
+               zone_finder_.findNSEC3Tmp(Name("example.org"), false));
+
+    // NSEC3-like name but of ordinary RR type should go to normal tree.
+    const string nonsec3_text = string(apex_hash) + ".example.org. " +
+        "300 IN A 192.0.2.1";
+    EXPECT_EQ(result::SUCCESS, zone_finder_.add(textToRRset(nonsec3_text)));
+    EXPECT_EQ(ZoneFinder::SUCCESS,
+              zone_finder_.find(Name(string(apex_hash) + ".example.org"),
+                                RRType::A()).code);
+}
+
+TEST_F(InMemoryZoneFinderTest, addNSEC3Lower) {
+    // Similar to the previous case, but NSEC3 owner name is lower-cased.
+    const string nsec3_text = string(apex_hash_lower) + ".example.org." +
+        string(nsec3_common);
+    EXPECT_EQ(result::SUCCESS, zone_finder_.add(textToRRset(nsec3_text)));
+    nsec3Check(true, nsec3_text,
+               zone_finder_.findNSEC3Tmp(Name("example.org"), false));
+}
+
+TEST_F(InMemoryZoneFinderTest, addNSEC3Ordering) {
+    // Check that the internal storage ensures comparison based on the NSEC3
+    // semantics, regardless of the add order or the letter-case of hash.
+
+    // Adding "0P..", "2v..", then "2T..".
+    const string smallest = string(apex_hash) + ".example.org." +
+        string(nsec3_common);
+    const string middle = string(ns1_hash) + ".example.org." +
+        string(nsec3_common);
+    const string largest = string(xrw_hash) + ".example.org." +
+        string(nsec3_common);
+    zone_finder_.add(textToRRset(smallest));
+    zone_finder_.add(textToRRset(largest));
+    zone_finder_.add(textToRRset(middle));
+
+    // Then look for NSEC3 that covers a name whose hash is "2S.."
+    // The covering NSEC3 should be "0P.."
+    nsec3Check(false, smallest,
+               zone_finder_.findNSEC3Tmp(Name("www.example.org"), false));
+
+    // Look for NSEC3 that covers names whose hash are "Q0.." and "0A.."
+    // The covering NSEC3 should be "2v.." in both cases
+    nsec3Check(false, largest,
+               zone_finder_.findNSEC3Tmp(Name("xxx.example.org"), false));
+    nsec3Check(false, largest,
+               zone_finder_.findNSEC3Tmp(Name("yyy.example.org"), false));
+}
+
+TEST_F(InMemoryZoneFinderTest, badNSEC3Name) {
+    // Our implementation refuses to load NSEC3 at a wildcard name
+    EXPECT_THROW(zone_finder_.add(textToRRset("*.example.org." +
+                                              string(nsec3_common))),
+                 InMemoryZoneFinder::AddError);
+
+    // Likewise, if the owner name of NSEC3 has too many labels, it's refused.
+    EXPECT_THROW(zone_finder_.add(textToRRset("a." + string(apex_hash) +
+                                              ".example.org." +
+                                              string(nsec3_common))),
+                 InMemoryZoneFinder::AddError);
+}
+
+TEST_F(InMemoryZoneFinderTest, addMultiNSEC3) {
+    // In this current implementation multiple NSEC3 RDATA isn't supported.
+    RRsetPtr nsec3(new RRset(Name(string(apex_hash) + ".example.org"),
+                             RRClass::IN(), RRType::NSEC3(), RRTTL(300)));
+    nsec3->addRdata(
+        generic::NSEC3("1 0 12 aabbccdd 2T7B4G4VSA5SMI47K61MV5BV1A22BOJR A"));
+    nsec3->addRdata(
+        generic::NSEC3("1 1 1 ddccbbaa 2T7B4G4VSA5SMI47K61MV5BV1A22BOJR A"));
+    EXPECT_THROW(zone_finder_.add(nsec3), InMemoryZoneFinder::AddError);
+}
+
+TEST_F(InMemoryZoneFinderTest, addNSEC3WithRRSIG) {
+    // Adding NSEC3 and its RRSIG
+    const string nsec3_text = string(apex_hash) + ".example.org." +
+        string(nsec3_common);
+    EXPECT_EQ(result::SUCCESS, zone_finder_.add(textToRRset(nsec3_text)));
+    const string nsec3_rrsig_text = string(apex_hash) + ".example.org." +
+        string(nsec3_rrsig_common);
+    EXPECT_EQ(result::SUCCESS, zone_finder_.add(textToRRset(nsec3_rrsig_text)));
+
+    // Then look for it.  The NSEC3 should have the RRSIG that was just added.
+    nsec3Check(true, nsec3_text + "\n" + nsec3_rrsig_text,
+               zone_finder_.findNSEC3Tmp(Name("example.org"), false), true);
+
+    // Duplicate add of RRSIG for the same NSEC3 is prohibited.
+    EXPECT_THROW(zone_finder_.add(textToRRset(nsec3_rrsig_text)),
+                 InMemoryZoneFinder::AddError);
+
+    // Same check using the lower-cased name.  This also confirms matching
+    // is case-insensitive.
+    EXPECT_THROW(zone_finder_.add(textToRRset(string(apex_hash_lower) +
+                                              ".example.org."
+                                              + string(nsec3_rrsig_common))),
+                 InMemoryZoneFinder::AddError);
+}
+
+TEST_F(InMemoryZoneFinderTest, badRRsigForNSEC3) {
+    // adding RRSIG for NSEC3 even before adding any NSEC3 (internally,
+    // a space for NSEC3 namespace isn't yet allocated)
+    EXPECT_THROW(zone_finder_.add(textToRRset(string(apex_hash) +
+                                              ".example.org." +
+                                              string(nsec3_rrsig_common))),
+                 InMemoryZoneFinder::AddError);
+
+    // Add an NSEC3
+    EXPECT_EQ(result::SUCCESS, zone_finder_.add(
+                  textToRRset(string(apex_hash) + ".example.org." +
+                              string(nsec3_common))));
+
+    // Then add an NSEC3 for a non existent NSEC3.  It should fail in the
+    // current implementation.
+    EXPECT_THROW(zone_finder_.add(textToRRset(string(ns1_hash) +
+                                              ".example.org." +
+                                              string(nsec3_rrsig_common))),
+                 InMemoryZoneFinder::AddError);
+}
+
+TEST_F(InMemoryZoneFinderTest, paramConsistencyWithNSEC3PARAM) {
+    // First, add an NSEC3PARAM RR
+    EXPECT_EQ(result::SUCCESS,
+              zone_finder_.add(textToRRset("example.org. 300 IN NSEC3PARAM "
+                                           "1 0 12 aabbccdd")));
+    // Adding an NSEC3 that has matching parameters is okay.
+    EXPECT_EQ(result::SUCCESS, zone_finder_.add(
+                  textToRRset(string(apex_hash) + ".example.org." +
+                              string(nsec3_common))));
+    // NSEC3 with inconsistent parameter will be rejected
+    EXPECT_THROW(zone_finder_.add(
+                     textToRRset("a.example.org. 300 IN NSEC3 1 0 1 aabbccdd "
+                                 "2T7B4G4VSA5SMI47K61MV5BV1A22BOJR A RRSIG")),
+                 InMemoryZoneFinder::AddError);
+}
+
+TEST_F(InMemoryZoneFinderTest, paramConsistencyWithNSEC3) {
+    // Add an NSEC3 without adding NSEC3PARAM
+    EXPECT_EQ(result::SUCCESS, zone_finder_.add(
+                  textToRRset(string(apex_hash) + ".example.org." +
+                              string(nsec3_common))));
+    // Adding an NSEC3 with inconsistent parameter will be rejected at this pt.
+    EXPECT_THROW(zone_finder_.add(
+                     textToRRset("a.example.org. 300 IN NSEC3 1 0 1 aabbccdd "
+                                 "2T7B4G4VSA5SMI47K61MV5BV1A22BOJR A RRSIG")),
+                 InMemoryZoneFinder::AddError);
+
+    // Likewise, NSEC3PARAM with inconsistent parameter will be rejected.
+    EXPECT_THROW(zone_finder_.add(textToRRset("example.org. 300 IN NSEC3PARAM "
+                                              "1 0 1 aabbccdd")),
+                 InMemoryZoneFinder::AddError);
+}
+
+TEST_F(InMemoryZoneFinderTest, multiNSEC3PARAM) {
+    // In this current implementation multiple NSEC3PARAM isn't supported.
+    RRsetPtr nsec3param(new RRset(Name("example.org"), RRClass::IN(),
+                                  RRType::NSEC3PARAM(), RRTTL(300)));
+    nsec3param->addRdata(generic::NSEC3PARAM("1 0 12 aabbccdd"));
+    nsec3param->addRdata(generic::NSEC3PARAM("1 1 1 ddccbbaa"));
+    EXPECT_THROW(zone_finder_.add(nsec3param), InMemoryZoneFinder::AddError);
+}
+
+TEST_F(InMemoryZoneFinderTest, nonOriginNSEC3PARAM) {
+    // This is a normal NSEC3PARAM at the zone origin
+    EXPECT_EQ(result::SUCCESS,
+              zone_finder_.add(textToRRset("example.org. 300 IN NSEC3PARAM "
+                                           "1 0 12 aabbccdd")));
+    // Add another (with different param) at a non origin node.  This is
+    // awkward, but the implementation accepts it as an ordinary RR.
+    EXPECT_EQ(result::SUCCESS,
+              zone_finder_.add(textToRRset("a.example.org. 300 IN NSEC3PARAM "
+                                           "1 1 1 aabbccdd")));
+}
+
+TEST_F(InMemoryZoneFinderTest, loadNSEC3Zone) {
+    // Check if it can load validly NSEC3-signed zone.  At this moment
+    // it's sufficient to see it doesn't crash
+    zone_finder_.load(TEST_DATA_DIR "/example.org.nsec3-signed");
+
+    // Reload the zone with a version that doesn't have NSEC3PARAM.
+    // This is an abnormal case, but the implementation accepts it.
+    zone_finder_.load(TEST_DATA_DIR "/example.org.nsec3-signed-noparam");
+}
 }

+ 14 - 0
src/lib/datasrc/tests/testdata/example.org.nsec3-signed

@@ -0,0 +1,14 @@
+example.org.				      86400 IN SOA	ns.example.org. ns.example.org. 2012013000 7200 3600 2592000 1200
+example.org.				      86400 IN RRSIG	SOA 7 2 86400 20120301040838 20120131040838 19562 example.org. Jt9wCRLS5TQxZH0IBqrM9uMGD453rIoxYopfM9AjjRZfEx+HGlBpOZeR pGN7yLcN+URnicOD0ydLHiakaBODiZyNoYCKYG5d2ZOhL+026REnDKNM 0m5T3X3sczP+l55An/GITheTdrKt3Y1Ouc2yKI8ro8JjOxV/a4nGDWjK x9A=
+example.org.				      86400 IN NS	ns.example.org.
+example.org.				      86400 IN RRSIG	NS 7 2 86400 20120301040838 20120131040838 19562 example.org. gYXL3xK4IFdJU6TtiVuzqDBb2MeA8xB3AKtHlJGFTfTRNHyuej0ZGovx TeUYsLYmoiGYaJG66iD1tYYFq0qdj0xWq+LEa53ACtKvYf9IIwK4ijJs k0g6xCNavc6/qPppymDhN7MvoFVkW59uJa0HPWlsIIuRlEAr7xyt85vq yoA=
+example.org.				      86400 IN DNSKEY	256 3 7 AwEAAbrBkKf2gmGtG4aogZY4svIZCrOLLZlQzVHwz7WxJdTR8iEnvz/x Q/jReDroS5CBZWvzwLlhPIpsJAojx0oj0RvfJNsz3+6LN8q7x9u6+86B 85CYjTk3dcFOebgkF4fXr7/kkOX+ZY94Zk0Z1+pUC3eY4gkKcyME/Uxm O18PBTeB
+example.org.				      86400 IN RRSIG	DNSKEY 7 2 86400 20120301040838 20120131040838 19562 example.org. d0eLF8JqNHaGuBSX0ashU5c1O/wyWU43UUsKGrMQIoBDiJ588MWQOnas rwvW6vdkLNqRqCsP/B4epV/EtLL0tBsk5SHkTYbNo80gGrBufQ6YrWRr Ile8Z+h+MR4y9DybbjmuNKqaO4uQMg/X6+4HqRAKx1lmZMTcrcVeOwDM ZA4=
+example.org.				      0	IN NSEC3PARAM	1 0 10 AABBCCDD
+example.org.				      0	IN RRSIG	NSEC3PARAM 7 2 0 20120301040838 20120131040838 19562 example.org. Ggs5MiQDlXXt22Fz9DNg3Ujc0T6MBfumlRkd8/enBbJwLmqw2QXAzDEk pjUeGstCEHKzxJDJstavGoCpTDJgoV4Fd9szooMx69rzPrq9tdoM46jG xZHqw+Pv2fkRGC6aP7ZX1r3Qnpwpk47AQgATftbO4G6KcMcO8JoKE47x XLM=
+ns.example.org.				      86400 IN A	192.0.2.1
+ns.example.org.				      86400 IN RRSIG	A 7 3 86400 20120301040838 20120131040838 19562 example.org. dOH+Dxib8VcGnjLrKILsqDhS1wki6BWk1dZwpOGUGHyLWcLNW8ygWY2o r29jPhHtaFCNWpn46JebgnXDPRiQjaY3dQqL8rcf2QX1e3+Cicw1OSrs S0sUDE5DmMNEac+ZCUQ0quCStZTCldl05hlspV2RS92TpApnoOK0nXMp Uak=
+09GM5T42SMIMT7R8DF6RTG80SFMS1NLU.example.org. 1200 IN NSEC3	1 0 10 AABBCCDD RKOF8QMFRB5F2V9EJHFBVB2JPVSA0DJD A RRSIG
+09GM5T42SMIMT7R8DF6RTG80SFMS1NLU.example.org. 1200 IN RRSIG	NSEC3 7 3 1200 20120301040838 20120131040838 19562 example.org. EdwMeepLf//lV+KpCAN+213Scv1rrZyj4i2OwoCP4XxxS3CWGSuvYuKO yfZc8wKRcrD/4YG6nZVXE0s5O8NahjBJmDIyVt4WkfZ6QthxGg8ggLVv cD3dFksPyiKHf/WrTOZPSsxvN5m/i1Ey6+YWS01Gf3WDCMWDauC7Nmh3 CTM=
+RKOF8QMFRB5F2V9EJHFBVB2JPVSA0DJD.example.org. 1200 IN NSEC3	1 0 10 AABBCCDD 09GM5T42SMIMT7R8DF6RTG80SFMS1NLU NS SOA RRSIG DNSKEY NSEC3PARAM
+RKOF8QMFRB5F2V9EJHFBVB2JPVSA0DJD.example.org. 1200 IN RRSIG	NSEC3 7 3 1200 20120301040838 20120131040838 19562 example.org. j7d8GL4YqX035FBcPPsEcSWHjWcKdlQMHLL4TB67xVNFnl4SEFQCp4OO AtPap5tkKakwgWxoQVN9XjnqrBz+oQhofDkB3aTatAjIIkcwcnrm3AYQ rTI3E03ySiRwuCPKVmHOLUV2cG6O4xzcmP+MYZcvPTS8V3F5LlaU22i7 A3E=

+ 15 - 0
src/lib/datasrc/tests/testdata/example.org.nsec3-signed-noparam

@@ -0,0 +1,15 @@
+;; This file intentionally removes NSEC3PARAM from example.org.nsec3-signed
+example.org.				      86400 IN SOA	ns.example.org. ns.example.org. 2012013000 7200 3600 2592000 1200
+example.org.				      86400 IN RRSIG	SOA 7 2 86400 20120301040838 20120131040838 19562 example.org. Jt9wCRLS5TQxZH0IBqrM9uMGD453rIoxYopfM9AjjRZfEx+HGlBpOZeR pGN7yLcN+URnicOD0ydLHiakaBODiZyNoYCKYG5d2ZOhL+026REnDKNM 0m5T3X3sczP+l55An/GITheTdrKt3Y1Ouc2yKI8ro8JjOxV/a4nGDWjK x9A=
+example.org.				      86400 IN NS	ns.example.org.
+example.org.				      86400 IN RRSIG	NS 7 2 86400 20120301040838 20120131040838 19562 example.org. gYXL3xK4IFdJU6TtiVuzqDBb2MeA8xB3AKtHlJGFTfTRNHyuej0ZGovx TeUYsLYmoiGYaJG66iD1tYYFq0qdj0xWq+LEa53ACtKvYf9IIwK4ijJs k0g6xCNavc6/qPppymDhN7MvoFVkW59uJa0HPWlsIIuRlEAr7xyt85vq yoA=
+example.org.				      86400 IN DNSKEY	256 3 7 AwEAAbrBkKf2gmGtG4aogZY4svIZCrOLLZlQzVHwz7WxJdTR8iEnvz/x Q/jReDroS5CBZWvzwLlhPIpsJAojx0oj0RvfJNsz3+6LN8q7x9u6+86B 85CYjTk3dcFOebgkF4fXr7/kkOX+ZY94Zk0Z1+pUC3eY4gkKcyME/Uxm O18PBTeB
+example.org.				      86400 IN RRSIG	DNSKEY 7 2 86400 20120301040838 20120131040838 19562 example.org. d0eLF8JqNHaGuBSX0ashU5c1O/wyWU43UUsKGrMQIoBDiJ588MWQOnas rwvW6vdkLNqRqCsP/B4epV/EtLL0tBsk5SHkTYbNo80gGrBufQ6YrWRr Ile8Z+h+MR4y9DybbjmuNKqaO4uQMg/X6+4HqRAKx1lmZMTcrcVeOwDM ZA4=
+;; example.org.				      0	IN NSEC3PARAM	1 0 10 AABBCCDD
+;; example.org.				      0	IN RRSIG	NSEC3PARAM 7 2 0 20120301040838 20120131040838 19562 example.org. Ggs5MiQDlXXt22Fz9DNg3Ujc0T6MBfumlRkd8/enBbJwLmqw2QXAzDEk pjUeGstCEHKzxJDJstavGoCpTDJgoV4Fd9szooMx69rzPrq9tdoM46jG xZHqw+Pv2fkRGC6aP7ZX1r3Qnpwpk47AQgATftbO4G6KcMcO8JoKE47x XLM=
+ns.example.org.				      86400 IN A	192.0.2.1
+ns.example.org.				      86400 IN RRSIG	A 7 3 86400 20120301040838 20120131040838 19562 example.org. dOH+Dxib8VcGnjLrKILsqDhS1wki6BWk1dZwpOGUGHyLWcLNW8ygWY2o r29jPhHtaFCNWpn46JebgnXDPRiQjaY3dQqL8rcf2QX1e3+Cicw1OSrs S0sUDE5DmMNEac+ZCUQ0quCStZTCldl05hlspV2RS92TpApnoOK0nXMp Uak=
+09GM5T42SMIMT7R8DF6RTG80SFMS1NLU.example.org. 1200 IN NSEC3	1 0 10 AABBCCDD RKOF8QMFRB5F2V9EJHFBVB2JPVSA0DJD A RRSIG
+09GM5T42SMIMT7R8DF6RTG80SFMS1NLU.example.org. 1200 IN RRSIG	NSEC3 7 3 1200 20120301040838 20120131040838 19562 example.org. EdwMeepLf//lV+KpCAN+213Scv1rrZyj4i2OwoCP4XxxS3CWGSuvYuKO yfZc8wKRcrD/4YG6nZVXE0s5O8NahjBJmDIyVt4WkfZ6QthxGg8ggLVv cD3dFksPyiKHf/WrTOZPSsxvN5m/i1Ey6+YWS01Gf3WDCMWDauC7Nmh3 CTM=
+RKOF8QMFRB5F2V9EJHFBVB2JPVSA0DJD.example.org. 1200 IN NSEC3	1 0 10 AABBCCDD 09GM5T42SMIMT7R8DF6RTG80SFMS1NLU NS SOA RRSIG DNSKEY NSEC3PARAM
+RKOF8QMFRB5F2V9EJHFBVB2JPVSA0DJD.example.org. 1200 IN RRSIG	NSEC3 7 3 1200 20120301040838 20120131040838 19562 example.org. j7d8GL4YqX035FBcPPsEcSWHjWcKdlQMHLL4TB67xVNFnl4SEFQCp4OO AtPap5tkKakwgWxoQVN9XjnqrBz+oQhofDkB3aTatAjIIkcwcnrm3AYQ rTI3E03ySiRwuCPKVmHOLUV2cG6O4xzcmP+MYZcvPTS8V3F5LlaU22i7 A3E=

+ 39 - 5
src/lib/dns/nsec3hash.cc

@@ -15,6 +15,7 @@
 #include <stdint.h>
 
 #include <cassert>
+#include <cstring>
 #include <string>
 #include <vector>
 
@@ -55,10 +56,10 @@ private:
     static const uint8_t NSEC3_HASH_SHA1 = 1;
 
 public:
-    NSEC3HashRFC5155(const generic::NSEC3PARAM& param) :
-        algorithm_(param.getHashalg()),
-        iterations_(param.getIterations()),
-        salt_(param.getSalt()), digest_(SHA1_HASHSIZE), obuf_(Name::MAX_WIRE)
+    NSEC3HashRFC5155(uint8_t algorithm, uint16_t iterations,
+                     const vector<uint8_t>& salt) :
+        algorithm_(algorithm), iterations_(iterations),
+        salt_(salt), digest_(SHA1_HASHSIZE), obuf_(Name::MAX_WIRE)
     {
         if (algorithm_ != NSEC3_HASH_SHA1) {
             isc_throw(UnknownNSEC3HashAlgorithm, "Unknown NSEC3 algorithm: " <<
@@ -69,6 +70,11 @@ public:
 
     virtual std::string calculate(const Name& name) const;
 
+    virtual bool match(const generic::NSEC3& nsec3) const;
+    virtual bool match(const generic::NSEC3PARAM& nsec3param) const;
+    bool match(uint8_t algorithm, uint16_t iterations,
+               const vector<uint8_t>& salt) const;
+
 private:
     const uint8_t algorithm_;
     const uint16_t iterations_;
@@ -115,6 +121,27 @@ NSEC3HashRFC5155::calculate(const Name& name) const {
 
     return (encodeBase32Hex(digest_));
 }
+
+bool
+NSEC3HashRFC5155::match(uint8_t algorithm, uint16_t iterations,
+                        const vector<uint8_t>& salt) const
+{
+    return (algorithm_ == algorithm && iterations_ == iterations &&
+            salt_.size() == salt.size() &&
+            (salt_.empty() || memcmp(&salt_[0], &salt[0], salt_.size()) == 0));
+}
+
+bool
+NSEC3HashRFC5155::match(const generic::NSEC3& nsec3) const {
+    return (match(nsec3.getHashalg(), nsec3.getIterations(),
+                  nsec3.getSalt()));
+}
+
+bool
+NSEC3HashRFC5155::match(const generic::NSEC3PARAM& nsec3param) const {
+    return (match(nsec3param.getHashalg(), nsec3param.getIterations(),
+                  nsec3param.getSalt()));
+}
 } // end of unnamed namespace
 
 namespace isc {
@@ -122,7 +149,14 @@ namespace dns {
 
 NSEC3Hash*
 NSEC3Hash::create(const generic::NSEC3PARAM& param) {
-    return (new NSEC3HashRFC5155(param));
+    return (new NSEC3HashRFC5155(param.getHashalg(), param.getIterations(),
+                                 param.getSalt()));
+}
+
+NSEC3Hash*
+NSEC3Hash::create(const generic::NSEC3& nsec3) {
+    return (new NSEC3HashRFC5155(nsec3.getHashalg(), nsec3.getIterations(),
+                                 nsec3.getSalt()));
 }
 
 } // namespace dns

+ 29 - 0
src/lib/dns/nsec3hash.h

@@ -25,6 +25,7 @@ class Name;
 
 namespace rdata {
 namespace generic {
+class NSEC3;
 class NSEC3PARAM;
 }
 }
@@ -108,6 +109,12 @@ public:
     /// \return A pointer to a concrete derived object of \c NSEC3Hash.
     static NSEC3Hash* create(const rdata::generic::NSEC3PARAM& param);
 
+    /// \brief Factory method of NSECHash from NSEC3 RDATA.
+    ///
+    /// This is similar to the other version, but extracts the parameters
+    /// for hash calculation from an NSEC3 RDATA object.
+    static NSEC3Hash* create(const rdata::generic::NSEC3& nsec3);
+
     /// \brief The destructor.
     virtual ~NSEC3Hash() {}
 
@@ -123,6 +130,28 @@ public:
     /// calculated.
     /// \return Base32hex-encoded string of the hash value.
     virtual std::string calculate(const Name& name) const = 0;
+
+    /// \brief Match given NSEC3 parameters with that of the hash.
+    ///
+    /// This method compares NSEC3 parameters used for hash calculation
+    /// in the object with those in the given NSEC3 RDATA, and return
+    /// true iff they completely match.  In the current implementation
+    /// only the algorithm, iterations and salt are compared; the flags
+    /// are ignored (as they don't affect hash calculation per RFC5155).
+    ///
+    /// \throw None
+    ///
+    /// \param nsec3 An NSEC3 RDATA object whose hash parameters are to be
+    /// matched
+    /// \return true If the given parameters match the local ones; false
+    /// otherwise.
+    virtual bool match(const rdata::generic::NSEC3& nsec3) const = 0;
+
+    /// \brief Match given NSEC3PARAM parameters with that of the hash.
+    ///
+    /// This is similar to the other version, but extracts the parameters
+    /// to compare from an NSEC3PARAM RDATA object.
+    virtual bool match(const rdata::generic::NSEC3PARAM& nsec3param) const = 0;
 };
 
 }

+ 62 - 5
src/lib/dns/python/nsec3hash_python.cc

@@ -55,13 +55,24 @@ NSEC3Hash_init(PyObject* po_self, PyObject* args, PyObject*) {
         if (PyArg_ParseTuple(args, "O", &po_rdata)) {
             if (!PyRdata_Check(po_rdata)) {
                 PyErr_Format(PyExc_TypeError,
-                             "param must be an Rdata of type NSEC3HASH, "
-                             "not %.200s", po_rdata->ob_type->tp_name);
+                             "param must be an Rdata of type NSEC3/NSEC3PARAM,"
+                             " not %.200s", po_rdata->ob_type->tp_name);
+                return (-1);
+            }
+            const Rdata& rdata = PyRdata_ToRdata(po_rdata);
+            const generic::NSEC3PARAM* nsec3param =
+                dynamic_cast<const generic::NSEC3PARAM*>(&rdata);
+            const generic::NSEC3* nsec3 =
+                dynamic_cast<const generic::NSEC3*>(&rdata);
+            if (nsec3param != NULL) {
+                self->cppobj = NSEC3Hash::create(*nsec3param);
+            } else if (nsec3 != NULL) {
+                self->cppobj = NSEC3Hash::create(*nsec3);
+            } else {
+                PyErr_Format(PyExc_TypeError,
+                             "param must be an Rdata of type NSEC3/NSEC3HASH");
                 return (-1);
             }
-            self->cppobj = NSEC3Hash::create(
-                dynamic_cast<const generic::NSEC3PARAM&>(
-                    PyRdata_ToRdata(po_rdata)));
             return (0);
         }
     } catch (const UnknownNSEC3HashAlgorithm& ex) {
@@ -118,6 +129,51 @@ NSEC3Hash_calculate(PyObject* po_self, PyObject* args) {
     return (NULL);
 }
 
+PyObject*
+NSEC3Hash_match(PyObject* po_self, PyObject* args) {
+    s_NSEC3Hash* const self = static_cast<s_NSEC3Hash*>(po_self);
+
+    try {
+        PyObject* po_rdata;
+        if (PyArg_ParseTuple(args, "O", &po_rdata)) {
+            if (!PyRdata_Check(po_rdata)) {
+                PyErr_Format(PyExc_TypeError,
+                             "param must be an Rdata of type NSEC3/NSEC3PARAM,"
+                             " not %.200s", po_rdata->ob_type->tp_name);
+                return (NULL);
+            }
+            const Rdata& rdata = PyRdata_ToRdata(po_rdata);
+            const generic::NSEC3PARAM* nsec3param =
+                dynamic_cast<const generic::NSEC3PARAM*>(&rdata);
+            const generic::NSEC3* nsec3 =
+                dynamic_cast<const generic::NSEC3*>(&rdata);
+            bool matched;
+            if (nsec3param != NULL) {
+                matched = self->cppobj->match(*nsec3param);
+            } else if (nsec3 != NULL) {
+                matched = self->cppobj->match(*nsec3);
+            } else {
+                PyErr_Format(PyExc_TypeError,
+                             "param must be an Rdata of type NSEC3/NSEC3HASH");
+                return (NULL);
+            }
+            PyObject* ret = matched ? Py_True : Py_False;
+            Py_INCREF(ret);
+            return (ret);
+        }
+    } catch (const exception& ex) {
+        const string ex_what = "Unexpected failure in NSEC3Hash.match: " +
+            string(ex.what());
+        PyErr_SetString(po_IscException, ex_what.c_str());
+        return (NULL);
+    } catch (...) {
+        PyErr_SetString(PyExc_SystemError, "Unexpected C++ exception");
+        return (NULL);
+    }
+
+    return (NULL);
+}
+
 // This list contains the actual set of functions we have in
 // python. Each entry has
 // 1. Python method name
@@ -126,6 +182,7 @@ NSEC3Hash_calculate(PyObject* po_self, PyObject* args) {
 // 4. Documentation
 PyMethodDef NSEC3Hash_methods[] = {
     { "calculate", NSEC3Hash_calculate, METH_VARARGS, NSEC3Hash_calculate_doc },
+    { "match", NSEC3Hash_match, METH_VARARGS, NSEC3Hash_match_doc },
     { NULL, NULL, 0, NULL }
 };
 } // end of unnamed namespace

+ 26 - 3
src/lib/dns/python/nsec3hash_python_inc.cc

@@ -10,7 +10,7 @@ NSEC3 hash values as defined in RFC5155.\n\
 \n\
 NSEC3Hash(param)\n\
 \n\
-    Constructor from NSEC3PARAM RDATA.\n\
+    Constructor.\n\
 \n\
     The hash algorithm given via param must be known to the\n\
     implementation. Otherwise UnknownNSEC3HashAlgorithm exception will\n\
@@ -21,12 +21,13 @@ NSEC3Hash(param)\n\
                  unknown.\n\
 \n\
     Parameters:\n\
-      param      NSEC3 parameters used for subsequent calculation.\n\
+      param      NSEC3PARAM or NSEC3 Rdata object whose parameters are\n\
+                 to be used for subsequent calculation.\n\
 \n\
 ";
 
 const char* const NSEC3Hash_calculate_doc = "\
-calculate(Name) -> string\n\
+calculate(name) -> string\n\
 \n\
 Calculate the NSEC3 hash.\n\
 \n\
@@ -42,4 +43,26 @@ Parameters:\n\
 \n\
 Return Value(s): Base32hex-encoded string of the hash value.\n\
 ";
+
+const char* const NSEC3Hash_match_doc = "\
+match(rdata) -> bool\n                   \
+\n\
+Match given NSEC3 or NSEC3PARAM parameters with that of the hash.\n\
+\n\
+This method compares NSEC3 parameters used for hash calculation in the\n\
+object with those in the given RDATA, and return true iff they\n\
+completely match. In the current implementation only the algorithm,\n\
+iterations and salt are compared; the flags are ignored (as they don't\n\
+affect hash calculation per RFC5155).\n\
+\n\
+Exceptions:\n\
+  None\n\
+\n\
+Parameters:\n\
+  rdata      An NSEC3 or NSEC3PARAM Rdata object whose hash parameters\n\
+             are to be matched\n\
+\n\
+Return Value(s): true If the given parameters match the local ones;\n\
+false otherwise.\n\
+";
 } // unnamed namespace

+ 62 - 11
src/lib/dns/python/tests/nsec3hash_python_test.py

@@ -23,9 +23,12 @@ class NSEC3HashTest(unittest.TestCase):
     '''
 
     def setUp(self):
+        self.nsec3_common = "2T7B4G4VSA5SMI47K61MV5BV1A22BOJR A RRSIG"
         self.test_hash = NSEC3Hash(Rdata(RRType.NSEC3PARAM(), RRClass.IN(),
                                          "1 0 12 aabbccdd"))
-
+        self.test_hash_nsec3 = NSEC3Hash(Rdata(RRType.NSEC3(), RRClass.IN(),
+                                               "1 0 12 aabbccdd " +
+                                               self.nsec3_common))
     def test_bad_construct(self):
         # missing parameter
         self.assertRaises(TypeError, NSEC3Hash)
@@ -38,28 +41,33 @@ class NSEC3HashTest(unittest.TestCase):
                                                       RRClass.IN(),
                                                       "1 0 12 aabbccdd"), 1)
 
+        # Invaid type of RDATA
+        self.assertRaises(TypeError, NSEC3Hash, Rdata(RRType.A(), RRClass.IN(),
+                                                      "192.0.2.1"))
+
     def test_unknown_algorithm(self):
         self.assertRaises(UnknownNSEC3HashAlgorithm, NSEC3Hash,
                           Rdata(RRType.NSEC3PARAM(), RRClass.IN(),
                                 "2 0 12 aabbccdd"))
+        self.assertRaises(UnknownNSEC3HashAlgorithm, NSEC3Hash,
+                          Rdata(RRType.NSEC3(), RRClass.IN(),
+                                "2 0 12 aabbccdd " + self.nsec3_common))
 
-    def test_calculate(self):
+    def calculate_check(self, hash):
         # A couple of normal cases from the RFC5155 example.
         self.assertEqual("0P9MHAVEQVM6T7VBL5LOP2U3T2RP3TOM",
-                         self.test_hash.calculate(Name("example")))
+                         hash.calculate(Name("example")))
         self.assertEqual("35MTHGPGCU1QG68FAB165KLNSNK3DPVL",
-                         self.test_hash.calculate(Name("a.example")))
+                         hash.calculate(Name("a.example")))
 
         # Check case-insensitiveness
         self.assertEqual("0P9MHAVEQVM6T7VBL5LOP2U3T2RP3TOM",
-                         self.test_hash.calculate(Name("EXAMPLE")))
+                         hash.calculate(Name("EXAMPLE")))
 
-        # Some boundary cases: 0-iteration and empty salt.  Borrowed from the
-        # .com zone data.
-        self.test_hash = NSEC3Hash(Rdata(RRType.NSEC3PARAM(),
-                                         RRClass.IN(),"1 0 0 -"))
-        self.assertEqual("CK0POJMG874LJREF7EFN8430QVIT8BSM",
-                         self.test_hash.calculate(Name("com")))
+
+    def test_calculate(self):
+        self.calculate_check(self.test_hash)
+        self.calculate_check(self.test_hash_nsec3)
 
         # Using unusually large iterations, something larger than the 8-bit
         #range.  (expected hash value generated by BIND 9's dnssec-signzone)
@@ -68,10 +76,53 @@ class NSEC3HashTest(unittest.TestCase):
         self.assertEqual("COG6A52MJ96MNMV3QUCAGGCO0RHCC2Q3",
                          self.test_hash.calculate(Name("example.org")))
 
+        # Some boundary cases: 0-iteration and empty salt.  Borrowed from the
+        # .com zone data.
+        self.test_hash = NSEC3Hash(Rdata(RRType.NSEC3PARAM(),
+                                         RRClass.IN(),"1 0 0 -"))
+        self.assertEqual("CK0POJMG874LJREF7EFN8430QVIT8BSM",
+                         self.test_hash.calculate(Name("com")))
+
     def test_calculate_badparam(self):
         self.assertRaises(TypeError, self.test_hash.calculate, "example")
         self.assertRaises(TypeError, self.test_hash.calculate)
         self.assertRaises(TypeError, self.test_hash.calculate, Name("."), 1)
 
+    def check_match(self, hash, rrtype, postfix):
+        # If all parameters match, it's considered to be matched.
+        self.assertTrue(hash.match(Rdata(rrtype, RRClass.IN(),
+                                         "1 0 12 aabbccdd" + postfix)))
+        # Algorithm doesn't match
+        self.assertFalse(hash.match(Rdata(rrtype, RRClass.IN(),
+                                          "2 0 12 aabbccdd" + postfix)))
+        # Iterations doesn't match
+        self.assertFalse(hash.match(Rdata(rrtype, RRClass.IN(),
+                                          "1 0 1 aabbccdd" + postfix)))
+        # Salt doesn't match
+        self.assertFalse(hash.match(Rdata(rrtype, RRClass.IN(),
+                                          "1 0 12 aabbccde" + postfix)))
+        # Salt doesn't match: the other has an empty salt
+        self.assertFalse(hash.match(Rdata(rrtype, RRClass.IN(),
+                                          "1 0 12 -" + postfix)))
+        # Flag doesn't matter
+        self.assertTrue(hash.match(Rdata(rrtype, RRClass.IN(),
+                                         "1 1 12 aabbccdd" + postfix)))
+
+    def test_match(self):
+        self.check_match(self.test_hash, RRType.NSEC3(),
+                         " " + self.nsec3_common)
+        self.check_match(self.test_hash_nsec3, RRType.NSEC3(),
+                         " " + self.nsec3_common)
+        self.check_match(self.test_hash, RRType.NSEC3PARAM(), "")
+        self.check_match(self.test_hash_nsec3, RRType.NSEC3PARAM(), "")
+
+        # bad parameter checks
+        self.assertRaises(TypeError, self.test_hash.match, 1)
+        self.assertRaises(TypeError, self.test_hash.match,
+                          Rdata(RRType.NSEC3(), RRClass.IN(),
+                                "1 0 12 aabbccdd " + self.nsec3_common), 1)
+        self.assertRaises(TypeError, self.test_hash.match,
+                          Rdata(RRType.A(), RRClass.IN(), "192.0.2.1"))
+
 if __name__ == '__main__':
     unittest.main()

+ 77 - 5
src/lib/dns/tests/nsec3hash_unittest.cc

@@ -12,6 +12,8 @@
 // OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
 // PERFORMANCE OF THIS SOFTWARE.
 
+#include <string>
+
 #include <gtest/gtest.h>
 
 #include <boost/scoped_ptr.hpp>
@@ -20,22 +22,32 @@
 #include <dns/rdataclass.h>
 
 using boost::scoped_ptr;
+using namespace std;
 using namespace isc::dns;
 using namespace isc::dns::rdata;
 
 namespace {
 typedef scoped_ptr<NSEC3Hash> NSEC3HashPtr;
 
+// Commonly used NSEC3 suffix, defined to reduce amount of type
+const char* const nsec3_common = "2T7B4G4VSA5SMI47K61MV5BV1A22BOJR A RRSIG";
+
 class NSEC3HashTest : public ::testing::Test {
 protected:
     NSEC3HashTest() :
-        test_hash(NSEC3Hash::create(generic::NSEC3PARAM("1 0 12 aabbccdd")))
+        test_hash(NSEC3Hash::create(generic::NSEC3PARAM("1 0 12 aabbccdd"))),
+        test_hash_nsec3(NSEC3Hash::create(generic::NSEC3
+                                          ("1 0 12 aabbccdd " +
+                                           string(nsec3_common))))
     {}
 
     // An NSEC3Hash object commonly used in tests.  Parameters are borrowed
     // from the RFC5155 example.  Construction of this object implicitly
     // checks a successful case of the creation.
     NSEC3HashPtr test_hash;
+
+    // Similar to test_hash, but created from NSEC3 RR.
+    NSEC3HashPtr test_hash_nsec3;
 };
 
 TEST_F(NSEC3HashTest, unknownAlgorithm) {
@@ -43,18 +55,36 @@ TEST_F(NSEC3HashTest, unknownAlgorithm) {
                      NSEC3Hash::create(
                          generic::NSEC3PARAM("2 0 12 aabbccdd"))),
                      UnknownNSEC3HashAlgorithm);
+    EXPECT_THROW(NSEC3HashPtr(
+                     NSEC3Hash::create(
+                         generic::NSEC3("2 0 12 aabbccdd " +
+                                        string(nsec3_common)))),
+                     UnknownNSEC3HashAlgorithm);
 }
 
-TEST_F(NSEC3HashTest, calculate) {
+// Common checks for NSEC3 hash calculation
+void
+calculateCheck(NSEC3Hash& hash) {
     // A couple of normal cases from the RFC5155 example.
     EXPECT_EQ("0P9MHAVEQVM6T7VBL5LOP2U3T2RP3TOM",
-              test_hash->calculate(Name("example")));
+              hash.calculate(Name("example")));
     EXPECT_EQ("35MTHGPGCU1QG68FAB165KLNSNK3DPVL",
-              test_hash->calculate(Name("a.example")));
+              hash.calculate(Name("a.example")));
 
     // Check case-insensitiveness
     EXPECT_EQ("0P9MHAVEQVM6T7VBL5LOP2U3T2RP3TOM",
-              test_hash->calculate(Name("EXAMPLE")));
+              hash.calculate(Name("EXAMPLE")));
+}
+
+TEST_F(NSEC3HashTest, calculate) {
+    {
+        SCOPED_TRACE("calculate check with NSEC3PARAM based hash");
+        calculateCheck(*test_hash);
+    }
+    {
+        SCOPED_TRACE("calculate check with NSEC3 based hash");
+        calculateCheck(*test_hash_nsec3);
+    }
 
     // Some boundary cases: 0-iteration and empty salt.  Borrowed from the
     // .com zone data.
@@ -70,4 +100,46 @@ TEST_F(NSEC3HashTest, calculate) {
               ->calculate(Name("example.org")));
 }
 
+// Common checks for match cases
+template <typename RDATAType>
+void
+matchCheck(NSEC3Hash& hash, const string& postfix) {
+    // If all parameters match, it's considered to be matched.
+    EXPECT_TRUE(hash.match(RDATAType("1 0 12 aabbccdd" + postfix)));
+
+    // Algorithm doesn't match
+    EXPECT_FALSE(hash.match(RDATAType("2 0 12 aabbccdd" + postfix)));
+    // Iterations doesn't match
+    EXPECT_FALSE(hash.match(RDATAType("1 0 1 aabbccdd" + postfix)));
+    // Salt doesn't match
+    EXPECT_FALSE(hash.match(RDATAType("1 0 12 aabbccde" + postfix)));
+    // Salt doesn't match: the other has an empty salt
+    EXPECT_FALSE(hash.match(RDATAType("1 0 12 -" + postfix)));
+    // Flags don't matter
+    EXPECT_TRUE(hash.match(RDATAType("1 1 12 aabbccdd" + postfix)));
+}
+
+TEST_F(NSEC3HashTest, matchWithNSEC3) {
+    {
+        SCOPED_TRACE("match NSEC3PARAM based hash against NSEC3 parameters");
+        matchCheck<generic::NSEC3>(*test_hash, " " + string(nsec3_common));
+    }
+    {
+        SCOPED_TRACE("match NSEC3 based hash against NSEC3 parameters");
+        matchCheck<generic::NSEC3>(*test_hash_nsec3,
+                                   " " + string(nsec3_common));
+    }
+}
+
+TEST_F(NSEC3HashTest, matchWithNSEC3PARAM) {
+    {
+        SCOPED_TRACE("match NSEC3PARAM based hash against NSEC3 parameters");
+        matchCheck<generic::NSEC3PARAM>(*test_hash, "");
+    }
+    {
+        SCOPED_TRACE("match NSEC3 based hash against NSEC3 parameters");
+        matchCheck<generic::NSEC3PARAM>(*test_hash_nsec3, "");
+    }
+}
+
 } // end namespace