Rename 'tests/unit-tests' directory to 'tests/unit'

Change-Id: I78ea29938259fac288781bed12fb2399ac7eba26
diff --git a/tests/unit/security/v2/additional-description.t.cpp b/tests/unit/security/v2/additional-description.t.cpp
new file mode 100644
index 0000000..7ceee8c
--- /dev/null
+++ b/tests/unit/security/v2/additional-description.t.cpp
@@ -0,0 +1,103 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/additional-description.hpp"
+
+#include "boost-test.hpp"
+
+namespace ndn {
+namespace security {
+namespace tests {
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(TestAdditionalDescription)
+
+const uint8_t description[] = {
+  0xfd, 0x01, 0x02, 0x28,
+    0xfd, 0x02, 0x00, 0x10, // DescriptionEntry
+      0xfd, 0x02, 0x01, 0x04, // DescriptionKey
+        0x6b, 0x65, 0x79, 0x31, // "key1"
+      0xfd, 0x02, 0x02, 0x04, // DescriptionValue
+        0x76, 0x61, 0x6c, 0x31, // "val1"
+    0xfd, 0x02, 0x00, 0x10, // DescriptionEntry
+      0xfd, 0x02, 0x01, 0x04, // DescriptionKey
+        0x6b, 0x65, 0x79, 0x32, // "key2"
+      0xfd, 0x02, 0x02, 0x04, // DescriptionValue
+        0x76, 0x61, 0x6c, 0x32, // "val2"
+};
+
+const std::string text = "((key1:val1), (key2:val2))";
+
+BOOST_AUTO_TEST_CASE(Basic)
+{
+  AdditionalDescription aDescription;
+
+  aDescription.set("key2", "val2");
+  aDescription.set("key1", "val1");
+
+  BOOST_REQUIRE_NO_THROW(aDescription.get("key1"));
+  BOOST_REQUIRE_NO_THROW(aDescription.get("key2"));
+  BOOST_REQUIRE_THROW(aDescription.get("key3"), AdditionalDescription::Error);
+
+  BOOST_CHECK_EQUAL(aDescription.has("key1"), true);
+  BOOST_CHECK_EQUAL(aDescription.has("key2"), true);
+  BOOST_CHECK_EQUAL(aDescription.has("key3"), false);
+
+  auto val1 = aDescription.get("key1");
+  auto val2 = aDescription.get("key2");
+
+  BOOST_CHECK_EQUAL(val1, "val1");
+  BOOST_CHECK_EQUAL(val2, "val2");
+
+  auto it = aDescription.begin();
+  BOOST_CHECK_EQUAL(it->second, "val1");
+  it++;
+  BOOST_CHECK_EQUAL(it->second, "val2");
+  it++;
+  BOOST_CHECK(it == aDescription.end());
+
+  BOOST_CHECK_EQUAL_COLLECTIONS(aDescription.wireEncode().wire(),
+                                aDescription.wireEncode().wire() + aDescription.wireEncode().size(),
+                                description,
+                                description + sizeof(description));
+
+  BOOST_REQUIRE_NO_THROW(AdditionalDescription(Block(description, sizeof(description))));
+  AdditionalDescription aDescription2(Block(description, sizeof(description)));
+
+  BOOST_CHECK_EQUAL(aDescription2, aDescription);
+
+  AdditionalDescription aDescription3;
+  aDescription3.set("key3", "val3");
+  aDescription3.set("key2", "val2");
+
+  BOOST_CHECK_NE(aDescription2, aDescription3);
+
+  std::ostringstream os;
+  os << aDescription;
+  BOOST_CHECK_EQUAL(os.str(), text);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestAdditionalDescription
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/certificate-bundle-fetcher.t.cpp b/tests/unit/security/v2/certificate-bundle-fetcher.t.cpp
new file mode 100644
index 0000000..d2e0565
--- /dev/null
+++ b/tests/unit/security/v2/certificate-bundle-fetcher.t.cpp
@@ -0,0 +1,191 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/certificate-bundle-fetcher.hpp"
+#include "security/v2/validation-policy-simple-hierarchy.hpp"
+#include "util/regex/regex-pattern-list-matcher.hpp"
+#include "lp/nack.hpp"
+
+#include "boost-test.hpp"
+#include "validator-fixture.hpp"
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+using namespace ndn::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+BOOST_AUTO_TEST_SUITE(TestCertificateBundleFetcher)
+
+class CertificateBundleFetcherWrapper : public CertificateBundleFetcher
+{
+public:
+  CertificateBundleFetcherWrapper(Face& face)
+    : CertificateBundleFetcher(make_unique<CertificateFetcherFromNetwork>(face), face)
+  {
+  }
+};
+
+class Bundle
+{
+};
+
+class Cert
+{
+};
+
+class Timeout
+{
+};
+
+class Nack
+{
+};
+
+template<class Response>
+class CertificateBundleFetcherFixture : public HierarchicalValidatorFixture<ValidationPolicySimpleHierarchy,
+                                                                            CertificateBundleFetcherWrapper>
+{
+public:
+  CertificateBundleFetcherFixture()
+    : data("/Security/V2/ValidatorFixture/Sub1/Sub3/Data")
+  {
+    subSubIdentity = addSubCertificate("/Security/V2/ValidatorFixture/Sub1/Sub3", subIdentity);
+    cache.insert(subSubIdentity.getDefaultKey().getDefaultCertificate());
+
+    m_keyChain.sign(data, signingByIdentity(subSubIdentity));
+    bundleRegexMatcher = make_shared<RegexPatternListMatcher>("<>*<_BUNDLE><>*", nullptr);
+    processInterest = [this] (const Interest& interest) {
+      // check if the interest is for Bundle or individual certificates
+      if (bundleRegexMatcher->match(interest.getName(), 0, interest.getName().size())) {
+        makeResponse(interest);
+      }
+      else {
+        auto cert = cache.find(interest);
+        if (cert == nullptr) {
+          return;
+        }
+        face.receive(*cert);
+      }
+    };
+  }
+
+  void
+  makeResponse(const Interest& interest);
+
+public:
+  Data data;
+  Identity subSubIdentity;
+  shared_ptr<RegexPatternListMatcher> bundleRegexMatcher;
+};
+
+template<>
+void
+CertificateBundleFetcherFixture<Bundle>::makeResponse(const Interest& interest)
+{
+  Block certList = Block(tlv::Content);
+  Name bundleName(interest.getName());
+
+  if (!bundleName.get(-1).isSegment() || bundleName.get(-1).toSegment() == 0) {
+    Block subSubCert = subSubIdentity.getDefaultKey().getDefaultCertificate().wireEncode();
+    certList.push_back(subSubCert);
+
+    if (!bundleName.get(-1).isSegment()) {
+      bundleName
+        .appendVersion()
+        .appendSegment(0);
+    }
+  }
+  else {
+    Block subCert = subIdentity.getDefaultKey().getDefaultCertificate().wireEncode();
+    Block anchor = identity.getDefaultKey().getDefaultCertificate().wireEncode();
+    certList.push_back(subCert);
+    certList.push_back(anchor);
+  }
+
+  shared_ptr<Data> certBundle = make_shared<Data>();
+  certBundle->setName(bundleName);
+  certBundle->setFreshnessPeriod(100_s);
+  certBundle->setContent(certList);
+  certBundle->setFinalBlock(name::Component::fromSegment(1));
+
+  m_keyChain.sign(*certBundle, signingWithSha256());
+
+  face.receive(*certBundle);
+}
+
+template<>
+void
+CertificateBundleFetcherFixture<Timeout>::makeResponse(const Interest& interest)
+{
+  this->advanceClocks(200_s);
+}
+
+template<>
+void
+CertificateBundleFetcherFixture<Nack>::makeResponse(const Interest& interest)
+{
+  lp::Nack nack(interest);
+  nack.setHeader(lp::NackHeader().setReason(lp::NackReason::NO_ROUTE));
+  face.receive(nack);
+}
+
+BOOST_FIXTURE_TEST_CASE(ValidateSuccessWithBundle, CertificateBundleFetcherFixture<Bundle>)
+{
+  VALIDATE_SUCCESS(this->data, "Should get accepted, as interest brings the bundle segments");
+  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 2); // produced bundle has 2 segments
+
+  for (const auto& sentInterest : this->face.sentInterests) {
+    BOOST_CHECK(this->bundleRegexMatcher->match(sentInterest.getName(), 0, sentInterest.getName().size()));
+  }
+}
+
+using SuccessWithoutBundle = boost::mpl::vector<Nack, Timeout>;
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(ValidateSuccessWithoutBundle, T, SuccessWithoutBundle, CertificateBundleFetcherFixture<T>)
+{
+  VALIDATE_SUCCESS(this->data, "Should get accepted, as interest brings the certs");
+  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 4); // since interest for Bundle fails, each cert is retrieved
+
+  bool toggle = true;
+  for (const auto& sentInterest : this->face.sentInterests) {
+    if (toggle) {
+      // every alternate interest is going to be that of a bundle
+      BOOST_CHECK(this->bundleRegexMatcher->match(sentInterest.getName(), 0, sentInterest.getName().size()));
+    }
+    else {
+      BOOST_CHECK(!this->bundleRegexMatcher->match(sentInterest.getName(), 0, sentInterest.getName().size()));
+    }
+    toggle = !toggle;
+  }
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestCertificateBundleFetcher
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/certificate-cache.t.cpp b/tests/unit/security/v2/certificate-cache.t.cpp
new file mode 100644
index 0000000..4ae3505
--- /dev/null
+++ b/tests/unit/security/v2/certificate-cache.t.cpp
@@ -0,0 +1,108 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/certificate-cache.hpp"
+
+#include "../../identity-management-time-fixture.hpp"
+#include "boost-test.hpp"
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+
+class CertificateCacheFixture : public ndn::tests::IdentityManagementTimeFixture
+{
+public:
+  CertificateCacheFixture()
+    : certCache(10_s)
+  {
+    identity = addIdentity("/TestCertificateCache/");
+    cert = identity.getDefaultKey().getDefaultCertificate();
+  }
+
+public:
+  CertificateCache certCache;
+  Identity identity;
+  Certificate cert;
+};
+
+BOOST_FIXTURE_TEST_SUITE(TestCertificateCache, CertificateCacheFixture)
+
+BOOST_AUTO_TEST_CASE(RemovalTime)
+{
+  // Cache lifetime is capped to 10 seconds during cache construction
+
+  BOOST_CHECK_NO_THROW(certCache.insert(cert));
+  BOOST_CHECK(certCache.find(cert.getName()) != nullptr);
+
+  advanceClocks(11_s, 1);
+  BOOST_CHECK(certCache.find(cert.getName()) == nullptr);
+
+  BOOST_CHECK_NO_THROW(certCache.insert(cert));
+  BOOST_CHECK(certCache.find(cert.getName()) != nullptr);
+
+  advanceClocks(5_s);
+  BOOST_CHECK(certCache.find(cert.getName()) != nullptr);
+
+  advanceClocks(15_s);
+  BOOST_CHECK(certCache.find(cert.getName()) == nullptr);
+}
+
+BOOST_AUTO_TEST_CASE(FindByInterest)
+{
+  BOOST_CHECK_NO_THROW(certCache.insert(cert));
+
+  // Find by interest
+  BOOST_CHECK(certCache.find(Interest(cert.getIdentity())) != nullptr);
+  BOOST_CHECK(certCache.find(Interest(cert.getKeyName())) != nullptr);
+  BOOST_CHECK(certCache.find(Interest(Name(cert.getName()).appendVersion())) == nullptr);
+
+  advanceClocks(12_s);
+  BOOST_CHECK(certCache.find(Interest(cert.getIdentity())) == nullptr);
+
+  Certificate cert3 = addCertificate(identity.getDefaultKey(), "3");
+  Certificate cert4 = addCertificate(identity.getDefaultKey(), "4");
+  Certificate cert5 = addCertificate(identity.getDefaultKey(), "5");
+
+  certCache.insert(cert3);
+  certCache.insert(cert4);
+  certCache.insert(cert5);
+
+  Interest interest4(cert3.getKeyName());
+  interest4.setExclude(Exclude().excludeOne(cert3.getName().at(Certificate::ISSUER_ID_OFFSET)));
+  BOOST_CHECK(certCache.find(interest4) != nullptr);
+  BOOST_CHECK_NE(certCache.find(interest4)->getName(), cert3.getName());
+
+  // TODO cover more cases with different interests
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestCertificateCache
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/certificate-fetcher-direct-fetch.t.cpp b/tests/unit/security/v2/certificate-fetcher-direct-fetch.t.cpp
new file mode 100644
index 0000000..68fe6b1
--- /dev/null
+++ b/tests/unit/security/v2/certificate-fetcher-direct-fetch.t.cpp
@@ -0,0 +1,205 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/certificate-fetcher-direct-fetch.hpp"
+#include "security/v2/validation-policy-simple-hierarchy.hpp"
+#include "lp/nack.hpp"
+#include "lp/tags.hpp"
+
+#include "boost-test.hpp"
+#include "validator-fixture.hpp"
+
+#include <boost/range/adaptor/strided.hpp>
+#include <boost/range/adaptor/sliced.hpp>
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+using namespace ndn::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+BOOST_AUTO_TEST_SUITE(TestCertificateFetcherDirectFetch)
+
+class Cert
+{
+};
+
+class Timeout
+{
+};
+
+class Nack
+{
+};
+
+template<class Response>
+class CertificateFetcherDirectFetchFixture : public HierarchicalValidatorFixture<ValidationPolicySimpleHierarchy,
+                                                                                 CertificateFetcherDirectFetch>
+{
+public:
+  CertificateFetcherDirectFetchFixture()
+    : data("/Security/V2/ValidatorFixture/Sub1/Sub3/Data")
+    , interest("/Security/V2/ValidatorFixture/Sub1/Sub3/Interest")
+    , interestNoTag("/Security/V2/ValidatorFixture/Sub1/Sub3/Interest2")
+  {
+    Identity subSubIdentity = addSubCertificate("/Security/V2/ValidatorFixture/Sub1/Sub3", subIdentity);
+    cache.insert(subSubIdentity.getDefaultKey().getDefaultCertificate());
+
+    m_keyChain.sign(data, signingByIdentity(subSubIdentity));
+    m_keyChain.sign(interest, signingByIdentity(subSubIdentity));
+    m_keyChain.sign(interestNoTag, signingByIdentity(subSubIdentity));
+
+    data.setTag(make_shared<lp::IncomingFaceIdTag>(123));
+    interest.setTag(make_shared<lp::IncomingFaceIdTag>(123));
+
+    processInterest = [this] (const Interest& interest) {
+      auto nextHopFaceIdTag = interest.template getTag<lp::NextHopFaceIdTag>();
+      if (nextHopFaceIdTag == nullptr) {
+        makeResponse(interest); // respond only to the "infrastructure" interest
+      }
+    };
+  }
+
+  void
+  makeResponse(const Interest& interest);
+
+public:
+  Data data;
+  Interest interest;
+  Interest interestNoTag;
+};
+
+template<>
+void
+CertificateFetcherDirectFetchFixture<Cert>::makeResponse(const Interest& interest)
+{
+  auto cert = cache.find(interest);
+  if (cert == nullptr) {
+    return;
+  }
+  face.receive(*cert);
+}
+
+template<>
+void
+CertificateFetcherDirectFetchFixture<Timeout>::makeResponse(const Interest& interest)
+{
+  // do nothing
+}
+
+template<>
+void
+CertificateFetcherDirectFetchFixture<Nack>::makeResponse(const Interest& interest)
+{
+  lp::Nack nack(interest);
+  nack.setHeader(lp::NackHeader().setReason(lp::NackReason::NO_ROUTE));
+  face.receive(nack);
+}
+
+using Failures = boost::mpl::vector<Timeout, Nack>;
+
+BOOST_FIXTURE_TEST_CASE(ValidateSuccessData, CertificateFetcherDirectFetchFixture<Cert>)
+{
+  VALIDATE_SUCCESS(this->data, "Should get accepted, normal and/or direct interests bring certs");
+  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 4);
+
+  // odd interests
+  for (const auto& sentInterest : this->face.sentInterests | boost::adaptors::strided(2)) {
+    BOOST_CHECK(sentInterest.template getTag<lp::NextHopFaceIdTag>() != nullptr);
+  }
+
+  // even interests
+  for (const auto& sentInterest : this->face.sentInterests |
+                                    boost::adaptors::sliced(1, this->face.sentInterests.size()) |
+                                    boost::adaptors::strided(2)) {
+    BOOST_CHECK(sentInterest.template getTag<lp::NextHopFaceIdTag>() == nullptr);
+  }
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(ValidateFailureData, T, Failures, CertificateFetcherDirectFetchFixture<T>)
+{
+  VALIDATE_FAILURE(this->data, "Should fail, as all interests either NACKed or timeout");
+  // Direct fetcher sends two interests each time - to network and face
+  // 3 retries on nack or timeout (2 * (1 + 3) = 4)
+  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 8);
+
+  // odd interests
+  for (const auto& sentInterest : this->face.sentInterests | boost::adaptors::strided(2)) {
+    BOOST_CHECK(sentInterest.template getTag<lp::NextHopFaceIdTag>() != nullptr);
+  }
+
+  // even interests
+  for (const auto& sentInterest : this->face.sentInterests |
+                                    boost::adaptors::sliced(1, this->face.sentInterests.size()) |
+                                    boost::adaptors::strided(2)) {
+    BOOST_CHECK(sentInterest.template getTag<lp::NextHopFaceIdTag>() == nullptr);
+  }
+}
+
+BOOST_FIXTURE_TEST_CASE(ValidateSuccessInterest, CertificateFetcherDirectFetchFixture<Cert>)
+{
+  VALIDATE_SUCCESS(this->interest, "Should get accepted, normal and/or direct interests bring certs");
+  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 4);
+
+  // odd interests
+  for (const auto& sentInterest : this->face.sentInterests | boost::adaptors::strided(2)) {
+    BOOST_CHECK(sentInterest.template getTag<lp::NextHopFaceIdTag>() != nullptr);
+  }
+
+  // even interests
+  for (const auto& sentInterest : this->face.sentInterests |
+                                    boost::adaptors::sliced(1, this->face.sentInterests.size()) |
+                                    boost::adaptors::strided(2)) {
+    BOOST_CHECK(sentInterest.template getTag<lp::NextHopFaceIdTag>() == nullptr);
+  }
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(ValidateFailureInterest, T, Failures, CertificateFetcherDirectFetchFixture<T>)
+{
+  VALIDATE_FAILURE(this->interest, "Should fail, as all interests either NACKed or timeout");
+  // Direct fetcher sends two interests each time - to network and face
+  // 3 retries on nack or timeout (2 * (1 + 3) = 4)
+  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 8);
+
+  // odd interests
+  for (const auto& sentInterest : this->face.sentInterests | boost::adaptors::strided(2)) {
+    BOOST_CHECK(sentInterest.template getTag<lp::NextHopFaceIdTag>() != nullptr);
+  }
+
+  // even interests
+  for (const auto& sentInterest : this->face.sentInterests |
+                                    boost::adaptors::sliced(1, this->face.sentInterests.size()) |
+                                    boost::adaptors::strided(2)) {
+    BOOST_CHECK(sentInterest.template getTag<lp::NextHopFaceIdTag>() == nullptr);
+  }
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestCertificateFetcherDirectFetch
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/certificate-fetcher-from-network.t.cpp b/tests/unit/security/v2/certificate-fetcher-from-network.t.cpp
new file mode 100644
index 0000000..5a6b395
--- /dev/null
+++ b/tests/unit/security/v2/certificate-fetcher-from-network.t.cpp
@@ -0,0 +1,140 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/certificate-fetcher-from-network.hpp"
+#include "security/v2/validation-policy-simple-hierarchy.hpp"
+#include "lp/nack.hpp"
+
+#include "boost-test.hpp"
+#include "validator-fixture.hpp"
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+using namespace ndn::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+BOOST_AUTO_TEST_SUITE(TestCertificateFetcherFromNetwork)
+
+class Cert
+{
+};
+
+class Timeout
+{
+};
+
+class Nack
+{
+};
+
+template<class Response>
+class CertificateFetcherFromNetworkFixture : public HierarchicalValidatorFixture<ValidationPolicySimpleHierarchy,
+                                                                                 CertificateFetcherFromNetwork>
+{
+public:
+  CertificateFetcherFromNetworkFixture()
+    : data("/Security/V2/ValidatorFixture/Sub1/Sub3/Data")
+    , interest("/Security/V2/ValidatorFixture/Sub1/Sub3/Interest")
+  {
+    Identity subSubIdentity = addSubCertificate("/Security/V2/ValidatorFixture/Sub1/Sub3", subIdentity);
+    cache.insert(subSubIdentity.getDefaultKey().getDefaultCertificate());
+
+    m_keyChain.sign(data, signingByIdentity(subSubIdentity));
+    m_keyChain.sign(interest, signingByIdentity(subSubIdentity));
+
+    processInterest = bind(&CertificateFetcherFromNetworkFixture<Response>::makeResponse, this, _1);
+  }
+
+  void
+  makeResponse(const Interest& interest);
+
+public:
+  Data data;
+  Interest interest;
+};
+
+template<>
+void
+CertificateFetcherFromNetworkFixture<Cert>::makeResponse(const Interest& interest)
+{
+  auto cert = cache.find(interest);
+  if (cert == nullptr) {
+    return;
+  }
+  face.receive(*cert);
+}
+
+template<>
+void
+CertificateFetcherFromNetworkFixture<Timeout>::makeResponse(const Interest& interest)
+{
+  // do nothing
+}
+
+template<>
+void
+CertificateFetcherFromNetworkFixture<Nack>::makeResponse(const Interest& interest)
+{
+  lp::Nack nack(interest);
+  nack.setHeader(lp::NackHeader().setReason(lp::NackReason::NO_ROUTE));
+  face.receive(nack);
+}
+
+using Failures = boost::mpl::vector<Timeout, Nack>;
+
+BOOST_FIXTURE_TEST_CASE(ValidateSuccess, CertificateFetcherFromNetworkFixture<Cert>)
+{
+  VALIDATE_SUCCESS(this->data, "Should get accepted, as normal interests bring cert");
+  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 2);
+  this->face.sentInterests.clear();
+
+  this->advanceClocks(1_h, 2); // expire validator caches
+
+  VALIDATE_SUCCESS(this->interest, "Should get accepted, as interests bring certs");
+  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 2);
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(ValidateFailure, T, Failures, CertificateFetcherFromNetworkFixture<T>)
+{
+  VALIDATE_FAILURE(this->data, "Should fail, as interests don't bring data");
+  // first interest + 3 retries
+  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 4);
+
+  this->face.sentInterests.clear();
+
+  this->advanceClocks(1_h, 2); // expire validator caches
+
+  VALIDATE_FAILURE(this->interest, "Should fail, as interests don't bring data");
+  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 4);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestCertificateFetcherFromNetwork
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/certificate-fetcher-offline.t.cpp b/tests/unit/security/v2/certificate-fetcher-offline.t.cpp
new file mode 100644
index 0000000..102a308
--- /dev/null
+++ b/tests/unit/security/v2/certificate-fetcher-offline.t.cpp
@@ -0,0 +1,75 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/certificate-fetcher-offline.hpp"
+#include "security/v2/validation-policy-simple-hierarchy.hpp"
+
+#include "boost-test.hpp"
+#include "validator-fixture.hpp"
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+using namespace ndn::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+
+class CertificateFetcherOfflineWrapper : public CertificateFetcherOffline
+{
+public:
+  CertificateFetcherOfflineWrapper(Face&)
+  {
+  }
+};
+
+using CertificateFetcherOfflineFixture = HierarchicalValidatorFixture<ValidationPolicySimpleHierarchy,
+                                                                      CertificateFetcherOfflineWrapper>;
+
+BOOST_FIXTURE_TEST_SUITE(TestCertificateFetcherOffline, CertificateFetcherOfflineFixture)
+
+typedef boost::mpl::vector<Interest, Data> Packets;
+
+BOOST_AUTO_TEST_CASE_TEMPLATE(Validate, Packet, Packets)
+{
+  Packet unsignedPacket("/Security/V2/ValidatorFixture/Sub1/Packet");
+
+  Packet packet = unsignedPacket;
+  m_keyChain.sign(packet, signingByIdentity(subIdentity));
+  VALIDATE_FAILURE(packet, "Should fail, as no cert should be requested");
+  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 0);
+
+  packet = unsignedPacket;
+  m_keyChain.sign(packet, signingByIdentity(identity));
+  VALIDATE_SUCCESS(packet, "Should succeed, as signed by trust anchor");
+  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 0);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestCertificateFetcherOffline
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/certificate.t.cpp b/tests/unit/security/v2/certificate.t.cpp
new file mode 100644
index 0000000..e677aed
--- /dev/null
+++ b/tests/unit/security/v2/certificate.t.cpp
@@ -0,0 +1,291 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ *
+ * @author Zhiyi Zhang <dreamerbarrychang@gmail.com>
+ */
+
+#include "security/v2/certificate.hpp"
+
+#include "boost-test.hpp"
+#include "../../unit-test-time-fixture.hpp"
+
+#include <boost/lexical_cast.hpp>
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+using namespace ndn::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+BOOST_FIXTURE_TEST_SUITE(TestCertificate, UnitTestTimeFixture)
+
+const uint8_t PUBLIC_KEY[] = {
+  0x30, 0x81, 0x9d, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01,
+  0x01, 0x05, 0x00, 0x03, 0x81, 0x8b, 0x00, 0x30, 0x81, 0x87, 0x02, 0x81, 0x81, 0x00, 0x9e,
+  0x06, 0x3e, 0x47, 0x85, 0xb2, 0x34, 0x37, 0xaa, 0x85, 0x47, 0xac, 0x03, 0x24, 0x83, 0xb5,
+  0x9c, 0xa8, 0x05, 0x3a, 0x24, 0x1e, 0xeb, 0x89, 0x01, 0xbb, 0xe9, 0x9b, 0xb2, 0xc3, 0x22,
+  0xac, 0x68, 0xe3, 0xf0, 0x6c, 0x02, 0xce, 0x68, 0xa6, 0xc4, 0xd0, 0xa7, 0x06, 0x90, 0x9c,
+  0xaa, 0x1b, 0x08, 0x1d, 0x8b, 0x43, 0x9a, 0x33, 0x67, 0x44, 0x6d, 0x21, 0xa3, 0x1b, 0x88,
+  0x9a, 0x97, 0x5e, 0x59, 0xc4, 0x15, 0x0b, 0xd9, 0x2c, 0xbd, 0x51, 0x07, 0x61, 0x82, 0xad,
+  0xc1, 0xb8, 0xd7, 0xbf, 0x9b, 0xcf, 0x7d, 0x24, 0xc2, 0x63, 0xf3, 0x97, 0x17, 0xeb, 0xfe,
+  0x62, 0x25, 0xba, 0x5b, 0x4d, 0x8a, 0xc2, 0x7a, 0xbd, 0x43, 0x8a, 0x8f, 0xb8, 0xf2, 0xf1,
+  0xc5, 0x6a, 0x30, 0xd3, 0x50, 0x8c, 0xc8, 0x9a, 0xdf, 0xef, 0xed, 0x35, 0xe7, 0x7a, 0x62,
+  0xea, 0x76, 0x7c, 0xbb, 0x08, 0x26, 0xc7, 0x02, 0x01, 0x11
+};
+
+const uint8_t SIG_INFO[] = {
+  0x16, 0x55, 0x1B, 0x01, 0x01, 0x1C, 0x26, 0x07, 0x24, 0x08, 0x03, 0x6E, 0x64, 0x6E, 0x08, 0x05,
+  0x73, 0x69, 0x74, 0x65, 0x31, 0x08, 0x11, 0x6B, 0x73, 0x6B, 0x2D, 0x32, 0x35, 0x31, 0x36, 0x34,
+  0x32, 0x35, 0x33, 0x37, 0x37, 0x30, 0x39, 0x34, 0x08, 0x03, 0x4B, 0x45, 0x59, 0xFD, 0x00, 0xFD,
+  0x26, 0xFD, 0x00, 0xFE, 0x0F, 0x32, 0x30, 0x31, 0x35, 0x30, 0x38, 0x31, 0x34, 0x54, 0x32, 0x32,
+  0x33, 0x37, 0x33, 0x39, 0xFD, 0x00, 0xFF, 0x0F, 0x32, 0x30, 0x31, 0x35, 0x30, 0x38, 0x31, 0x38,
+  0x54, 0x32, 0x32, 0x33, 0x37, 0x33, 0x38
+};
+
+const uint8_t SIG_VALUE[] = {
+  0x17, 0x80, // SignatureValue
+    0x2f, 0xd6, 0xf1, 0x6e, 0x80, 0x6f, 0x10, 0xbe, 0xb1, 0x6f, 0x3e, 0x31, 0xec,
+    0xe3, 0xb9, 0xea, 0x83, 0x30, 0x40, 0x03, 0xfc, 0xa0, 0x13, 0xd9, 0xb3, 0xc6,
+    0x25, 0x16, 0x2d, 0xa6, 0x58, 0x41, 0x69, 0x62, 0x56, 0xd8, 0xb3, 0x6a, 0x38,
+    0x76, 0x56, 0xea, 0x61, 0xb2, 0x32, 0x70, 0x1c, 0xb6, 0x4d, 0x10, 0x1d, 0xdc,
+    0x92, 0x8e, 0x52, 0xa5, 0x8a, 0x1d, 0xd9, 0x96, 0x5e, 0xc0, 0x62, 0x0b, 0xcf,
+    0x3a, 0x9d, 0x7f, 0xca, 0xbe, 0xa1, 0x41, 0x71, 0x85, 0x7a, 0x8b, 0x5d, 0xa9,
+    0x64, 0xd6, 0x66, 0xb4, 0xe9, 0x8d, 0x0c, 0x28, 0x43, 0xee, 0xa6, 0x64, 0xe8,
+    0x55, 0xf6, 0x1c, 0x19, 0x0b, 0xef, 0x99, 0x25, 0x1e, 0xdc, 0x78, 0xb3, 0xa7,
+    0xaa, 0x0d, 0x14, 0x58, 0x30, 0xe5, 0x37, 0x6a, 0x6d, 0xdb, 0x56, 0xac, 0xa3,
+    0xfc, 0x90, 0x7a, 0xb8, 0x66, 0x9c, 0x0e, 0xf6, 0xb7, 0x64, 0xd1
+};
+
+const uint8_t CERT[] = {
+  0x06, 0xFD, 0x01, 0xBB, // Data
+    0x07, 0x33, // Name /ndn/site1/KEY/ksk-1416425377094/0123/%FD%00%00%01I%C9%8B
+      0x08, 0x03, 0x6E, 0x64, 0x6E,
+      0x08, 0x05, 0x73, 0x69, 0x74, 0x65, 0x31,
+      0x08, 0x03, 0x4B, 0x45, 0x59,
+      0x08, 0x11,
+        0x6B, 0x73, 0x6B, 0x2D, 0x31, 0x34, 0x31, 0x36, 0x34, 0x32, 0x35, 0x33, 0x37, 0x37, 0x30, 0x39,
+        0x34,
+      0x08, 0x04, 0x30, 0x31, 0x32, 0x33,
+      0x08, 0x07, 0xFD, 0x00, 0x00, 0x01, 0x49, 0xC9, 0x8B,
+    0x14, 0x09, // MetaInfo
+      0x18, 0x01, 0x02, // ContentType = Key
+      0x19, 0x04, 0x00, 0x36, 0xEE, 0x80, // FreshnessPeriod = 3600000 ms
+    0x15, 0xA0, // Content
+      0x30, 0x81, 0x9D, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01,
+      0x05, 0x00, 0x03, 0x81, 0x8B, 0x00, 0x30, 0x81, 0x87, 0x02, 0x81, 0x81, 0x00, 0x9E, 0x06, 0x3E,
+      0x47, 0x85, 0xB2, 0x34, 0x37, 0xAA, 0x85, 0x47, 0xAC, 0x03, 0x24, 0x83, 0xB5, 0x9C, 0xA8, 0x05,
+      0x3A, 0x24, 0x1E, 0xEB, 0x89, 0x01, 0xBB, 0xE9, 0x9B, 0xB2, 0xC3, 0x22, 0xAC, 0x68, 0xE3, 0xF0,
+      0x6C, 0x02, 0xCE, 0x68, 0xA6, 0xC4, 0xD0, 0xA7, 0x06, 0x90, 0x9C, 0xAA, 0x1B, 0x08, 0x1D, 0x8B,
+      0x43, 0x9A, 0x33, 0x67, 0x44, 0x6D, 0x21, 0xA3, 0x1B, 0x88, 0x9A, 0x97, 0x5E, 0x59, 0xC4, 0x15,
+      0x0B, 0xD9, 0x2C, 0xBD, 0x51, 0x07, 0x61, 0x82, 0xAD, 0xC1, 0xB8, 0xD7, 0xBF, 0x9B, 0xCF, 0x7D,
+      0x24, 0xC2, 0x63, 0xF3, 0x97, 0x17, 0xEB, 0xFE, 0x62, 0x25, 0xBA, 0x5B, 0x4D, 0x8A, 0xC2, 0x7A,
+      0xBD, 0x43, 0x8A, 0x8F, 0xB8, 0xF2, 0xF1, 0xC5, 0x6A, 0x30, 0xD3, 0x50, 0x8C, 0xC8, 0x9A, 0xDF,
+      0xEF, 0xED, 0x35, 0xE7, 0x7A, 0x62, 0xEA, 0x76, 0x7C, 0xBB, 0x08, 0x26, 0xC7, 0x02, 0x01, 0x11,
+    0x16, 0x55, // SignatureInfo
+      0x1B, 0x01, 0x01, // SignatureType
+      0x1C, 0x26, // KeyLocator: /ndn/site1/KEY/ksk-2516425377094
+        0x07, 0x24,
+          0x08, 0x03, 0x6E, 0x64, 0x6E,
+          0x08, 0x05, 0x73, 0x69, 0x74, 0x65, 0x31,
+          0x08, 0x03, 0x4B, 0x45, 0x59,
+          0x08, 0x11,
+            0x6B, 0x73, 0x6B, 0x2D, 0x32, 0x35, 0x31, 0x36, 0x34, 0x32, 0x35, 0x33, 0x37, 0x37, 0x30, 0x39,
+            0x34,
+      0xFD, 0x00, 0xFD, 0x26, // ValidityPeriod: (20150814T223739, 20150818T223738)
+        0xFD, 0x00, 0xFE, 0x0F,
+          0x32, 0x30, 0x31, 0x35, 0x30, 0x38, 0x31, 0x34, 0x54, 0x32, 0x32, 0x33, 0x37, 0x33, 0x39,
+        0xFD, 0x00, 0xFF, 0x0F,
+          0x32, 0x30, 0x31, 0x35, 0x30, 0x38, 0x31, 0x38, 0x54, 0x32, 0x32, 0x33, 0x37, 0x33, 0x38,
+    0x17, 0x80, // SignatureValue
+      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
+};
+
+Signature
+generateFakeSignature()
+{
+  Block block1(SIG_INFO, sizeof(SIG_INFO));
+  SignatureInfo signatureInfo(block1);
+
+  Name keyLocatorName("/ndn/site1/KEY/ksk-2516425377094");
+  KeyLocator keyLocator(keyLocatorName);
+  signatureInfo.setKeyLocator(keyLocator);
+
+  ValidityPeriod period(time::fromIsoString("20141111T050000"), time::fromIsoString("20141111T060000"));
+  signatureInfo.setValidityPeriod(period);
+
+  Signature signature(signatureInfo);
+  Block block2(SIG_VALUE, sizeof(SIG_VALUE));
+  signature.setValue(block2);
+
+  return signature;
+}
+
+BOOST_AUTO_TEST_CASE(Construction)
+{
+  Block block(CERT, sizeof(CERT));
+  Certificate certificate(block);
+
+  BOOST_CHECK_EQUAL(certificate.getName(), "/ndn/site1/KEY/ksk-1416425377094/0123/%FD%00%00%01I%C9%8B");
+  BOOST_CHECK_EQUAL(certificate.getKeyName(), "/ndn/site1/KEY/ksk-1416425377094");
+  BOOST_CHECK_EQUAL(certificate.getIdentity(), "/ndn/site1");
+  BOOST_CHECK_EQUAL(certificate.getIssuerId(), name::Component("0123"));
+  BOOST_CHECK_EQUAL(certificate.getKeyId(), name::Component("ksk-1416425377094"));
+  BOOST_CHECK_EQUAL(certificate.getSignature().getKeyLocator().getName(), "/ndn/site1/KEY/ksk-2516425377094");
+  BOOST_CHECK_EQUAL(boost::lexical_cast<std::string>(certificate.getValidityPeriod()), "(20150814T223739, 20150818T223738)");
+
+  BOOST_CHECK_THROW(certificate.getExtension(12345), ndn::SignatureInfo::Error);
+  BOOST_CHECK_NO_THROW(certificate.getPublicKey());
+
+  Data data(block);
+  Certificate certificate2(std::move(data));
+  BOOST_CHECK_EQUAL(certificate, certificate2);
+}
+
+BOOST_AUTO_TEST_CASE(Setters)
+{
+  Certificate certificate;
+  certificate.setName("/ndn/site1/KEY/ksk-1416425377094/0123/%FD%00%00%01I%C9%8B");
+  certificate.setFreshnessPeriod(1_h);
+  certificate.setContent(PUBLIC_KEY, sizeof(PUBLIC_KEY));
+  certificate.setSignature(generateFakeSignature());
+
+  BOOST_CHECK_EQUAL(certificate.getName(), "/ndn/site1/KEY/ksk-1416425377094/0123/%FD%00%00%01I%C9%8B");
+  BOOST_CHECK_EQUAL(certificate.getKeyName(), "/ndn/site1/KEY/ksk-1416425377094");
+  BOOST_CHECK_EQUAL(certificate.getIdentity(), "/ndn/site1");
+  BOOST_CHECK_EQUAL(certificate.getIssuerId(), name::Component("0123"));
+  BOOST_CHECK_EQUAL(certificate.getKeyId(), name::Component("ksk-1416425377094"));
+  BOOST_CHECK_EQUAL(certificate.getSignature().getKeyLocator().getName(), "/ndn/site1/KEY/ksk-2516425377094");
+  BOOST_CHECK_EQUAL(boost::lexical_cast<std::string>(certificate.getValidityPeriod()), "(20141111T050000, 20141111T060000)");
+
+  BOOST_CHECK_THROW(certificate.getExtension(12345), ndn::SignatureInfo::Error);
+  BOOST_CHECK_NO_THROW(certificate.getPublicKey());
+}
+
+BOOST_AUTO_TEST_CASE(ValidityPeriodChecking)
+{
+  Certificate certificate;
+  certificate.setName("/ndn/site1/KEY/ksk-1416425377094/0123/%FD%00%00%01I%C9%8B");
+  certificate.setFreshnessPeriod(1_h);
+  certificate.setContent(PUBLIC_KEY, sizeof(PUBLIC_KEY));
+  certificate.setSignature(generateFakeSignature());
+
+  BOOST_CHECK_EQUAL(certificate.isValid(), true);
+  BOOST_CHECK_EQUAL(certificate.isValid(time::fromIsoString("20141111T045959")), false);
+  BOOST_CHECK_EQUAL(certificate.isValid(time::fromIsoString("20141111T060001")), false);
+}
+
+// This fixture prepares a well-formed certificate. A test case then modifies one of the
+// fields, and verifies the Certificate class correctly identifies the certificate as
+// malformed.
+class InvalidCertFixture
+{
+public:
+  InvalidCertFixture()
+  {
+    Certificate certBase(Block(CERT, sizeof(CERT)));
+    BOOST_CHECK_NO_THROW((Certificate(certBase)));
+
+    m_certBase = Data(certBase);
+    m_certBase.setSignature(generateFakeSignature());
+
+    BOOST_CHECK_NO_THROW((Certificate(m_certBase)));
+  }
+
+public:
+  Data m_certBase;
+};
+
+BOOST_FIXTURE_TEST_CASE(InvalidName, InvalidCertFixture)
+{
+  Data data(m_certBase);
+  data.setName("/ndn/site1/ksk-1416425377094/0123/%FD%00%00%01I%C9%8B");
+  data.setSignature(generateFakeSignature());
+
+  BOOST_CHECK_THROW((Certificate(data)), Certificate::Error);
+  BOOST_CHECK_THROW((Certificate(std::move(data))), Certificate::Error);
+}
+
+BOOST_FIXTURE_TEST_CASE(InvalidType, InvalidCertFixture)
+{
+  Data data(m_certBase);
+  data.setContentType(tlv::ContentType_Blob);
+  data.setSignature(generateFakeSignature());
+
+  BOOST_CHECK_THROW((Certificate(data)), Certificate::Error);
+  BOOST_CHECK_THROW((Certificate(std::move(data))), Certificate::Error);
+}
+
+BOOST_FIXTURE_TEST_CASE(EmptyContent, InvalidCertFixture)
+{
+  Data data(m_certBase);
+  data.setContent(nullptr, 0);
+  data.setSignature(generateFakeSignature());
+
+  BOOST_CHECK_THROW((Certificate(data)), Certificate::Error);
+  BOOST_CHECK_THROW((Certificate(std::move(data))), Certificate::Error);
+
+  Certificate cert(m_certBase);
+  cert.setContent(nullptr, 0);
+  cert.setSignature(generateFakeSignature());
+  BOOST_CHECK_THROW(cert.getPublicKey(), Certificate::Error);
+}
+
+BOOST_AUTO_TEST_CASE(PrintCertificateInfo)
+{
+  const std::string expectedCertificateInfo = std::string(R"INFO(
+Certificate name:
+  /ndn/site1/KEY/ksk-1416425377094/0123/%FD%00%00%01I%C9%8B
+Validity:
+  NotBefore: 20150814T223739
+  NotAfter: 20150818T223738
+Public key bits:
+  MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQCeBj5HhbI0N6qFR6wDJIO1nKgF
+  OiQe64kBu+mbssMirGjj8GwCzmimxNCnBpCcqhsIHYtDmjNnRG0hoxuImpdeWcQV
+  C9ksvVEHYYKtwbjXv5vPfSTCY/OXF+v+YiW6W02Kwnq9Q4qPuPLxxWow01CMyJrf
+  7+0153pi6nZ8uwgmxwIBEQ==
+Signature Information:
+  Signature Type: SignatureSha256WithRsa
+  Key Locator: Name=/ndn/site1/KEY/ksk-2516425377094
+)INFO").substr(1);
+
+  Certificate certificate(Block(CERT, sizeof(CERT)));
+  BOOST_CHECK_EQUAL(boost::lexical_cast<std::string>(certificate), expectedCertificateInfo);
+
+  // @todo Check output formats of other certificates
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestCertificate
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/key-chain.t.cpp b/tests/unit/security/v2/key-chain.t.cpp
new file mode 100644
index 0000000..649ef7b
--- /dev/null
+++ b/tests/unit/security/v2/key-chain.t.cpp
@@ -0,0 +1,445 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/key-chain.hpp"
+#include "security/signing-helpers.hpp"
+#include "security/verification-helpers.hpp"
+
+#include "boost-test.hpp"
+#include "identity-management-fixture.hpp"
+#include "../../test-home-env-saver.hpp"
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+using namespace ndn::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+BOOST_FIXTURE_TEST_SUITE(TestKeyChain, TestHomeEnvSaver)
+
+template<class Path>
+class TestHomeAndPibFixture : public TestHomeFixture<Path>
+{
+public:
+  TestHomeAndPibFixture()
+  {
+    unsetenv("NDN_CLIENT_PIB");
+    unsetenv("NDN_CLIENT_TPM");
+  }
+
+  ~TestHomeAndPibFixture()
+  {
+    try {
+      const_cast<std::string&>(KeyChain::getDefaultPibLocator()).clear();
+    }
+    catch (const KeyChain::Error&) {
+      // ignore
+    }
+
+    try {
+      const_cast<std::string&>(KeyChain::getDefaultTpmLocator()).clear();
+    }
+    catch (const KeyChain::Error&) {
+      // ignore
+    }
+  }
+};
+
+struct PibPathConfigFileHome
+{
+  const std::string PATH = "build/config-file-home/";
+};
+
+BOOST_FIXTURE_TEST_CASE(ConstructorNormalConfig, TestHomeAndPibFixture<PibPathConfigFileHome>)
+{
+  createClientConf({"pib=pib-memory:", "tpm=tpm-memory:"});
+
+  BOOST_REQUIRE_NO_THROW(KeyChain());
+
+  KeyChain keyChain;
+  BOOST_CHECK_EQUAL(keyChain.getPib().getPibLocator(), "pib-memory:");
+  BOOST_CHECK_EQUAL(keyChain.getPib().getTpmLocator(), "tpm-memory:");
+  BOOST_CHECK_EQUAL(keyChain.getTpm().getTpmLocator(), "tpm-memory:");
+}
+
+struct PibPathConfigFileEmptyHome
+{
+  const std::string PATH = "build/config-file-empty-home/";
+};
+
+BOOST_FIXTURE_TEST_CASE(ConstructorEmptyConfig, TestHomeAndPibFixture<PibPathConfigFileEmptyHome>)
+{
+  createClientConf({"pib=pib-memory:"});
+
+#if defined(NDN_CXX_HAVE_OSX_FRAMEWORKS)
+  std::string oldHOME;
+  if (std::getenv("OLD_HOME"))
+    oldHOME = std::getenv("OLD_HOME");
+
+  std::string HOME;
+  if (std::getenv("HOME"))
+    HOME = std::getenv("HOME");
+
+  if (!oldHOME.empty())
+    setenv("HOME", oldHOME.c_str(), 1);
+  else
+    unsetenv("HOME");
+#endif
+
+  BOOST_REQUIRE_NO_THROW(KeyChain());
+  KeyChain keyChain;
+  BOOST_CHECK_EQUAL(keyChain.getPib().getPibLocator(), "pib-memory:");
+
+#if defined(NDN_CXX_HAVE_OSX_FRAMEWORKS)
+  BOOST_CHECK_EQUAL(keyChain.getPib().getTpmLocator(), "tpm-osxkeychain:");
+  BOOST_CHECK_EQUAL(keyChain.getTpm().getTpmLocator(), "tpm-osxkeychain:");
+#else
+  BOOST_CHECK_EQUAL(keyChain.getPib().getTpmLocator(), "tpm-file:");
+  BOOST_CHECK_EQUAL(keyChain.getTpm().getTpmLocator(), "tpm-file:");
+#endif
+
+#if defined(NDN_CXX_HAVE_OSX_FRAMEWORKS)
+  if (!HOME.empty())
+    setenv("HOME", HOME.c_str(), 1);
+  else
+    unsetenv("HOME");
+
+  if (!oldHOME.empty())
+    setenv("OLD_HOME", oldHOME.c_str(), 1);
+  else
+    unsetenv("OLD_HOME");
+#endif
+}
+
+struct PibPathConfigFileEmpty2Home
+{
+  const std::string PATH = "build/config-file-empty2-home/";
+};
+
+BOOST_FIXTURE_TEST_CASE(ConstructorEmpty2Config, TestHomeAndPibFixture<PibPathConfigFileEmpty2Home>)
+{
+  createClientConf({"tpm=tpm-memory:"});
+
+  BOOST_REQUIRE_NO_THROW(KeyChain());
+
+  KeyChain keyChain;
+  BOOST_CHECK_EQUAL(keyChain.getPib().getPibLocator(), "pib-sqlite3:");
+  BOOST_CHECK_EQUAL(keyChain.getPib().getTpmLocator(), "tpm-memory:");
+  BOOST_CHECK_EQUAL(keyChain.getTpm().getTpmLocator(), "tpm-memory:");
+}
+
+struct PibPathConfigFileMalformedHome
+{
+  const std::string PATH = "build/config-file-malformed-home/";
+};
+
+BOOST_FIXTURE_TEST_CASE(ConstructorMalConfig, TestHomeAndPibFixture<PibPathConfigFileMalformedHome>)
+{
+  createClientConf({"pib=lord", "tpm=ring"});
+
+  BOOST_REQUIRE_THROW(KeyChain(), KeyChain::Error); // Wrong configuration. Error expected.
+}
+
+struct PibPathConfigFileMalformed2Home
+{
+  const std::string PATH = "build/config-file-malformed2-home/";
+};
+
+BOOST_FIXTURE_TEST_CASE(ConstructorMal2Config, TestHomeAndPibFixture<PibPathConfigFileMalformed2Home>)
+{
+  createClientConf({"pib=pib-sqlite3:%PATH%", "tpm=just-wrong"});
+
+  BOOST_REQUIRE_THROW(KeyChain(), KeyChain::Error); // Wrong configuration. Error expected.
+}
+
+struct PibPathConfigFileNonCanonicalTpm
+{
+  const std::string PATH = "build/config-file-non-canonical-tpm/";
+};
+
+BOOST_FIXTURE_TEST_CASE(ConstructorNonCanonicalTpm, TestHomeAndPibFixture<PibPathConfigFileNonCanonicalTpm>) // Bug 4297
+{
+  createClientConf({"pib=pib-sqlite3:", "tpm=tpm-file"});
+
+  {
+    KeyChain keyChain;
+    keyChain.createIdentity("/test");
+    BOOST_CHECK_EQUAL(keyChain.getPib().getPibLocator(), "pib-sqlite3:");
+    BOOST_CHECK_EQUAL(keyChain.getTpm().getTpmLocator(), "tpm-file:");
+  }
+
+  {
+    KeyChain keyChain;
+    BOOST_CHECK_EQUAL(keyChain.getPib().getPibLocator(), "pib-sqlite3:");
+    BOOST_CHECK_EQUAL(keyChain.getTpm().getTpmLocator(), "tpm-file:");
+    BOOST_CHECK(keyChain.getPib().getIdentities().find("/test") != keyChain.getPib().getIdentities().end());
+  }
+}
+
+BOOST_AUTO_TEST_CASE(KeyChainWithCustomTpmAndPib)
+{
+  BOOST_REQUIRE_NO_THROW((KeyChain("pib-memory", "tpm-memory")));
+  BOOST_REQUIRE_NO_THROW((KeyChain("pib-memory:", "tpm-memory:")));
+  BOOST_REQUIRE_NO_THROW((KeyChain("pib-memory:/something", "tpm-memory:/something")));
+
+  KeyChain keyChain("pib-memory", "tpm-memory");
+  BOOST_CHECK_EQUAL(keyChain.getPib().getPibLocator(), "pib-memory:");
+  BOOST_CHECK_EQUAL(keyChain.getPib().getTpmLocator(), "tpm-memory:");
+  BOOST_CHECK_EQUAL(keyChain.getTpm().getTpmLocator(), "tpm-memory:");
+}
+
+BOOST_FIXTURE_TEST_CASE(Management, IdentityManagementFixture)
+{
+  Name identityName("/test/id");
+  Name identity2Name("/test/id2");
+
+  BOOST_CHECK_EQUAL(m_keyChain.getPib().getIdentities().size(), 0);
+  BOOST_REQUIRE_THROW(m_keyChain.getPib().getDefaultIdentity(), Pib::Error);
+
+  // Create identity
+  Identity id = m_keyChain.createIdentity(identityName);
+  BOOST_CHECK(id);
+  BOOST_CHECK(m_keyChain.getPib().getIdentities().find(identityName) != m_keyChain.getPib().getIdentities().end());
+  // The first added identity becomes the default identity
+  BOOST_REQUIRE_NO_THROW(m_keyChain.getPib().getDefaultIdentity());
+  // The default key of the added identity must exist
+  Key key;
+  BOOST_REQUIRE_NO_THROW(key = id.getDefaultKey());
+  // The default certificate of the default key must exist
+  BOOST_REQUIRE_NO_THROW(key.getDefaultCertificate());
+
+  // Delete key
+  Name key1Name = key.getName();
+  BOOST_CHECK_NO_THROW(id.getKey(key1Name));
+  BOOST_CHECK_EQUAL(id.getKeys().size(), 1);
+  m_keyChain.deleteKey(id, key);
+  // The key instance should not be valid any more
+  BOOST_CHECK(!key);
+  BOOST_CHECK_THROW(id.getKey(key1Name), Pib::Error);
+  BOOST_CHECK_EQUAL(id.getKeys().size(), 0);
+
+  // Create another key
+  m_keyChain.createKey(id);
+  // The added key becomes the default key.
+  BOOST_REQUIRE_NO_THROW(id.getDefaultKey());
+  Key key2 = id.getDefaultKey();
+  BOOST_REQUIRE(key2);
+  BOOST_CHECK_NE(key2.getName(), key1Name);
+  BOOST_CHECK_EQUAL(id.getKeys().size(), 1);
+  BOOST_REQUIRE_NO_THROW(key2.getDefaultCertificate());
+
+  // Create the third key
+  Key key3 = m_keyChain.createKey(id);
+  BOOST_CHECK_NE(key3.getName(), key2.getName());
+  // The added key will not be the default key, because the default key already exists
+  BOOST_CHECK_EQUAL(id.getDefaultKey().getName(), key2.getName());
+  BOOST_CHECK_EQUAL(id.getKeys().size(), 2);
+  BOOST_REQUIRE_NO_THROW(key3.getDefaultCertificate());
+
+  // Delete cert
+  BOOST_CHECK_EQUAL(key3.getCertificates().size(), 1);
+  Certificate key3Cert1 = *key3.getCertificates().begin();
+  Name key3CertName = key3Cert1.getName();
+  m_keyChain.deleteCertificate(key3, key3CertName);
+  BOOST_CHECK_EQUAL(key3.getCertificates().size(), 0);
+  BOOST_REQUIRE_THROW(key3.getDefaultCertificate(), Pib::Error);
+
+  // Add cert
+  m_keyChain.addCertificate(key3, key3Cert1);
+  BOOST_CHECK_EQUAL(key3.getCertificates().size(), 1);
+  BOOST_REQUIRE_NO_THROW(key3.getDefaultCertificate());
+  m_keyChain.addCertificate(key3, key3Cert1); // overwriting the cert should work
+  BOOST_CHECK_EQUAL(key3.getCertificates().size(), 1);
+  // Add another cert
+  Certificate key3Cert2 = key3Cert1;
+  Name key3Cert2Name = key3.getName();
+  key3Cert2Name.append("Self");
+  key3Cert2Name.appendVersion();
+  key3Cert2.setName(key3Cert2Name);
+  m_keyChain.addCertificate(key3, key3Cert2);
+  BOOST_CHECK_EQUAL(key3.getCertificates().size(), 2);
+
+  // Default certificate setting
+  BOOST_CHECK_EQUAL(key3.getDefaultCertificate().getName(), key3CertName);
+  m_keyChain.setDefaultCertificate(key3, key3Cert2);
+  BOOST_CHECK_EQUAL(key3.getDefaultCertificate().getName(), key3Cert2Name);
+
+  // Default key setting
+  BOOST_CHECK_EQUAL(id.getDefaultKey().getName(), key2.getName());
+  m_keyChain.setDefaultKey(id, key3);
+  BOOST_CHECK_EQUAL(id.getDefaultKey().getName(), key3.getName());
+
+  // Default identity setting
+  Identity id2 = m_keyChain.createIdentity(identity2Name);
+  BOOST_CHECK_EQUAL(m_keyChain.getPib().getDefaultIdentity().getName(), id.getName());
+  m_keyChain.setDefaultIdentity(id2);
+  BOOST_CHECK_EQUAL(m_keyChain.getPib().getDefaultIdentity().getName(), id2.getName());
+
+  // Delete identity
+  m_keyChain.deleteIdentity(id);
+  // The identity instance should not be valid any more
+  BOOST_CHECK(!id);
+  BOOST_REQUIRE_THROW(m_keyChain.getPib().getIdentity(identityName), Pib::Error);
+  BOOST_CHECK(m_keyChain.getPib().getIdentities().find(identityName) == m_keyChain.getPib().getIdentities().end());
+}
+
+BOOST_FIXTURE_TEST_CASE(GeneralSigningInterface, IdentityManagementFixture)
+{
+  Identity id = addIdentity("/id");
+  Key key = id.getDefaultKey();
+  Certificate cert = key.getDefaultCertificate();
+
+  std::list<SigningInfo> signingInfos = {
+    SigningInfo(),
+
+    SigningInfo(SigningInfo::SIGNER_TYPE_ID, id.getName()),
+    signingByIdentity(id.getName()),
+
+    SigningInfo(id),
+    signingByIdentity(id),
+
+    SigningInfo(SigningInfo::SIGNER_TYPE_KEY, key.getName()),
+    signingByKey(key.getName()),
+
+    SigningInfo(key),
+    signingByKey(key),
+
+    SigningInfo(SigningInfo::SIGNER_TYPE_CERT, cert.getName()),
+    signingByCertificate(cert.getName()),
+    signingByCertificate(cert),
+
+    SigningInfo(SigningInfo::SIGNER_TYPE_SHA256),
+    signingWithSha256()
+  };
+
+  for (const auto& signingInfo : signingInfos) {
+    BOOST_TEST_MESSAGE("SigningInfo: " << signingInfo);
+    Data data("/data");
+    Interest interest("/interest");
+
+    if (signingInfo.getSignerType() == SigningInfo::SIGNER_TYPE_NULL) {
+      m_keyChain.sign(data);
+      m_keyChain.sign(interest);
+    }
+    else {
+      m_keyChain.sign(data, signingInfo);
+      m_keyChain.sign(interest, signingInfo);
+    }
+
+    Signature interestSignature(interest.getName()[-2].blockFromValue(), interest.getName()[-1].blockFromValue());
+
+    if (signingInfo.getSignerType() == SigningInfo::SIGNER_TYPE_SHA256) {
+      BOOST_CHECK_EQUAL(data.getSignature().getType(), tlv::DigestSha256);
+      BOOST_CHECK_EQUAL(interestSignature.getType(), tlv::DigestSha256);
+
+      BOOST_CHECK(verifyDigest(data, DigestAlgorithm::SHA256));
+      BOOST_CHECK(verifyDigest(interest, DigestAlgorithm::SHA256));
+    }
+    else {
+      BOOST_CHECK_EQUAL(data.getSignature().getType(), tlv::SignatureSha256WithEcdsa);
+      BOOST_CHECK_EQUAL(interestSignature.getType(), tlv::SignatureSha256WithEcdsa);
+
+      BOOST_CHECK_EQUAL(data.getSignature().getKeyLocator().getName(), cert.getName().getPrefix(-2));
+      BOOST_CHECK_EQUAL(interestSignature.getKeyLocator().getName(), cert.getName().getPrefix(-2));
+
+      BOOST_CHECK(verifySignature(data, key));
+      BOOST_CHECK(verifySignature(interest, key));
+    }
+  }
+}
+
+BOOST_FIXTURE_TEST_CASE(PublicKeySigningDefaults, IdentityManagementFixture)
+{
+  Data data("/test/data");
+
+  // Identity will be created with generated key and self-signed cert with default parameters
+  BOOST_CHECK_THROW(m_keyChain.sign(data, signingByIdentity("/non-existing/identity")), KeyChain::InvalidSigningInfoError);
+
+  // Create identity with EC key and the corresponding self-signed certificate
+  Identity id = addIdentity("/ndn/test/ec", EcKeyParams());
+  BOOST_CHECK_NO_THROW(m_keyChain.sign(data, signingByIdentity(id.getName())));
+  BOOST_CHECK_EQUAL(data.getSignature().getType(),
+                    KeyChain::getSignatureType(EcKeyParams().getKeyType(), DigestAlgorithm::SHA256));
+  BOOST_CHECK(id.getName().isPrefixOf(data.getSignature().getKeyLocator().getName()));
+
+  // Create identity with RSA key and the corresponding self-signed certificate
+  id = addIdentity("/ndn/test/rsa", RsaKeyParams());
+  BOOST_CHECK_NO_THROW(m_keyChain.sign(data, signingByIdentity(id.getName())));
+  BOOST_CHECK_EQUAL(data.getSignature().getType(),
+                    KeyChain::getSignatureType(RsaKeyParams().getKeyType(), DigestAlgorithm::SHA256));
+  BOOST_CHECK(id.getName().isPrefixOf(data.getSignature().getKeyLocator().getName()));
+}
+
+BOOST_FIXTURE_TEST_CASE(ExportImport, IdentityManagementFixture)
+{
+  Identity id = addIdentity("/TestKeyChain/ExportIdentity/");
+  Certificate cert = id.getDefaultKey().getDefaultCertificate();
+
+  shared_ptr<SafeBag> exported = m_keyChain.exportSafeBag(cert, "1234", 4);
+  Block block = exported->wireEncode();
+
+  m_keyChain.deleteIdentity(id);
+  BOOST_CHECK_THROW(m_keyChain.exportSafeBag(cert, "1234", 4), KeyChain::Error);
+
+  BOOST_CHECK_EQUAL(m_keyChain.getTpm().hasKey(cert.getKeyName()), false);
+  BOOST_CHECK_EQUAL(m_keyChain.getPib().getIdentities().size(), 0);
+
+  SafeBag imported;
+  imported.wireDecode(block);
+  m_keyChain.importSafeBag(imported, "1234", 4);
+  BOOST_CHECK_THROW(m_keyChain.importSafeBag(imported, "1234", 4), KeyChain::Error);
+
+  BOOST_CHECK_EQUAL(m_keyChain.getTpm().hasKey(cert.getKeyName()), true);
+  BOOST_CHECK_EQUAL(m_keyChain.getPib().getIdentities().size(), 1);
+  BOOST_REQUIRE_NO_THROW(m_keyChain.getPib().getIdentity(cert.getIdentity()));
+  Identity newId = m_keyChain.getPib().getIdentity(cert.getIdentity());
+  BOOST_CHECK_EQUAL(newId.getKeys().size(), 1);
+  BOOST_REQUIRE_NO_THROW(newId.getKey(cert.getKeyName()));
+  Key newKey = newId.getKey(cert.getKeyName());
+  BOOST_CHECK_EQUAL(newKey.getCertificates().size(), 1);
+  BOOST_REQUIRE_NO_THROW(newKey.getCertificate(cert.getName()));
+
+  m_keyChain.deleteIdentity(newId);
+  BOOST_CHECK_EQUAL(m_keyChain.getPib().getIdentities().size(), 0);
+  BOOST_CHECK_EQUAL(m_keyChain.getTpm().hasKey(cert.getKeyName()), false);
+}
+
+BOOST_FIXTURE_TEST_CASE(SelfSignedCertValidity, IdentityManagementFixture)
+{
+  Certificate cert = addIdentity("/Security/V2/TestKeyChain/SelfSignedCertValidity")
+                       .getDefaultKey()
+                       .getDefaultCertificate();
+  BOOST_CHECK(cert.isValid());
+  BOOST_CHECK(cert.isValid(time::system_clock::now() + 10 * 365_days));
+  BOOST_CHECK_GT(cert.getValidityPeriod().getPeriod().second, time::system_clock::now() + 10 * 365_days);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestKeyChain
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/trust-anchor-container.t.cpp b/tests/unit/security/v2/trust-anchor-container.t.cpp
new file mode 100644
index 0000000..efd6ca3
--- /dev/null
+++ b/tests/unit/security/v2/trust-anchor-container.t.cpp
@@ -0,0 +1,198 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/trust-anchor-container.hpp"
+#include "util/io.hpp"
+
+#include "../../identity-management-time-fixture.hpp"
+#include "boost-test.hpp"
+
+#include <boost/filesystem.hpp>
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+using namespace ndn::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+
+/**
+ * This fixture creates a directory and prepares two certificates.
+ * cert1 is written to a file under the directory, while cert2 is not.
+ */
+class AnchorContainerTestFixture : public IdentityManagementTimeFixture
+{
+public:
+  AnchorContainerTestFixture()
+  {
+    namespace fs = boost::filesystem;
+
+    fs::create_directory(fs::path(UNIT_TEST_CONFIG_PATH));
+
+    certDirPath = fs::path(UNIT_TEST_CONFIG_PATH) / "test-cert-dir";
+    fs::create_directory(certDirPath);
+
+    certPath1 = fs::path(UNIT_TEST_CONFIG_PATH) / "test-cert-dir" / "trust-anchor-1.cert";
+    certPath2 = fs::path(UNIT_TEST_CONFIG_PATH) / "test-cert-dir" / "trust-anchor-2.cert";
+
+    identity1 = addIdentity("/TestAnchorContainer/First");
+    cert1 = identity1.getDefaultKey().getDefaultCertificate();
+    saveCertToFile(cert1, certPath1.string());
+
+    identity2 = addIdentity("/TestAnchorContainer/Second");
+    cert2 = identity2.getDefaultKey().getDefaultCertificate();
+    saveCertToFile(cert2, certPath2.string());
+  }
+
+  ~AnchorContainerTestFixture()
+  {
+    boost::filesystem::remove_all(UNIT_TEST_CONFIG_PATH);
+  }
+
+public:
+  TrustAnchorContainer anchorContainer;
+
+  boost::filesystem::path certDirPath;
+  boost::filesystem::path certPath1;
+  boost::filesystem::path certPath2;
+
+  Identity identity1;
+  Identity identity2;
+
+  Certificate cert1;
+  Certificate cert2;
+};
+
+BOOST_FIXTURE_TEST_SUITE(TestTrustAnchorContainer, AnchorContainerTestFixture)
+
+// one static group and one dynamic group created from file
+BOOST_AUTO_TEST_CASE(Insert)
+{
+  // Static
+  anchorContainer.insert("group1", Certificate(cert1));
+  BOOST_CHECK(anchorContainer.find(cert1.getName()) != nullptr);
+  BOOST_CHECK(anchorContainer.find(identity1.getName()) != nullptr);
+  const Certificate* cert = anchorContainer.find(cert1.getName());
+  BOOST_CHECK_NO_THROW(anchorContainer.insert("group1", Certificate(cert1)));
+  BOOST_CHECK_EQUAL(cert, anchorContainer.find(cert1.getName())); // still the same instance of the certificate
+  // cannot add dynamic group when static already exists
+  BOOST_CHECK_THROW(anchorContainer.insert("group1", certPath1.string(), 1_s), TrustAnchorContainer::Error);
+  BOOST_CHECK_EQUAL(anchorContainer.getGroup("group1").size(), 1);
+  BOOST_CHECK_EQUAL(anchorContainer.size(), 1);
+
+  // From file
+  anchorContainer.insert("group2", certPath2.string(), 1_s);
+  BOOST_CHECK(anchorContainer.find(cert2.getName()) != nullptr);
+  BOOST_CHECK(anchorContainer.find(identity2.getName()) != nullptr);
+  BOOST_CHECK_THROW(anchorContainer.insert("group2", Certificate(cert2)), TrustAnchorContainer::Error);
+  BOOST_CHECK_THROW(anchorContainer.insert("group2", certPath2.string(), 1_s), TrustAnchorContainer::Error);
+  BOOST_CHECK_EQUAL(anchorContainer.getGroup("group2").size(), 1);
+  BOOST_CHECK_EQUAL(anchorContainer.size(), 2);
+
+  boost::filesystem::remove(certPath2);
+  advanceClocks(1_s, 11);
+
+  BOOST_CHECK(anchorContainer.find(identity2.getName()) == nullptr);
+  BOOST_CHECK(anchorContainer.find(cert2.getName()) == nullptr);
+  BOOST_CHECK_EQUAL(anchorContainer.getGroup("group2").size(), 0);
+  BOOST_CHECK_EQUAL(anchorContainer.size(), 1);
+
+  TrustAnchorGroup& group = anchorContainer.getGroup("group1");
+  auto staticGroup = dynamic_cast<StaticTrustAnchorGroup*>(&group);
+  BOOST_REQUIRE(staticGroup != nullptr);
+  BOOST_CHECK_EQUAL(staticGroup->size(), 1);
+  staticGroup->remove(cert1.getName());
+  BOOST_CHECK_EQUAL(staticGroup->size(), 0);
+  BOOST_CHECK_EQUAL(anchorContainer.size(), 0);
+
+  BOOST_CHECK_THROW(anchorContainer.getGroup("non-existing-group"), TrustAnchorContainer::Error);
+}
+
+BOOST_AUTO_TEST_CASE(DynamicAnchorFromDir)
+{
+  boost::filesystem::remove(certPath2);
+
+  anchorContainer.insert("group", certDirPath.string(), 1_s, true /* isDir */);
+
+  BOOST_CHECK(anchorContainer.find(identity1.getName()) != nullptr);
+  BOOST_CHECK(anchorContainer.find(identity2.getName()) == nullptr);
+  BOOST_CHECK_EQUAL(anchorContainer.getGroup("group").size(), 1);
+
+  saveCertToFile(cert2, certPath2.string());
+
+  advanceClocks(100_ms, 11);
+
+  BOOST_CHECK(anchorContainer.find(identity1.getName()) != nullptr);
+  BOOST_CHECK(anchorContainer.find(identity2.getName()) != nullptr);
+  BOOST_CHECK_EQUAL(anchorContainer.getGroup("group").size(), 2);
+
+  boost::filesystem::remove_all(certDirPath);
+
+  advanceClocks(100_ms, 11);
+
+  BOOST_CHECK(anchorContainer.find(identity1.getName()) == nullptr);
+  BOOST_CHECK(anchorContainer.find(identity2.getName()) == nullptr);
+  BOOST_CHECK_EQUAL(anchorContainer.getGroup("group").size(), 0);
+}
+
+BOOST_FIXTURE_TEST_CASE(FindByInterest, AnchorContainerTestFixture)
+{
+  anchorContainer.insert("group1", certPath1.string(), 1_s);
+  Interest interest(identity1.getName());
+  BOOST_CHECK(anchorContainer.find(interest) != nullptr);
+  Interest interest1(identity1.getName().getPrefix(-1));
+  BOOST_CHECK(anchorContainer.find(interest1) != nullptr);
+  Interest interest2(Name(identity1.getName()).appendVersion());
+  BOOST_CHECK(anchorContainer.find(interest2) == nullptr);
+
+  Certificate cert3 = addCertificate(identity1.getDefaultKey(), "3");
+  Certificate cert4 = addCertificate(identity1.getDefaultKey(), "4");
+  Certificate cert5 = addCertificate(identity1.getDefaultKey(), "5");
+
+  Certificate cert3Copy = cert3;
+  anchorContainer.insert("group2", std::move(cert3Copy));
+  anchorContainer.insert("group3", std::move(cert4));
+  anchorContainer.insert("group4", std::move(cert5));
+
+  Interest interest3(cert3.getKeyName());
+  const Certificate* foundCert = anchorContainer.find(interest3);
+  BOOST_REQUIRE(foundCert != nullptr);
+  BOOST_CHECK(interest3.getName().isPrefixOf(foundCert->getName()));
+  BOOST_CHECK_EQUAL(foundCert->getName(), cert3.getName());
+
+  interest3.setExclude(Exclude().excludeOne(cert3.getName().at(Certificate::ISSUER_ID_OFFSET)));
+  foundCert = anchorContainer.find(interest3);
+  BOOST_REQUIRE(foundCert != nullptr);
+  BOOST_CHECK(interest3.getName().isPrefixOf(foundCert->getName()));
+  BOOST_CHECK_NE(foundCert->getName(), cert3.getName());
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestTrustAnchorContainer
+BOOST_AUTO_TEST_SUITE_END() // Detail
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/validation-error.t.cpp b/tests/unit/security/v2/validation-error.t.cpp
new file mode 100644
index 0000000..1db3aac
--- /dev/null
+++ b/tests/unit/security/v2/validation-error.t.cpp
@@ -0,0 +1,61 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/validation-error.hpp"
+
+#include "boost-test.hpp"
+#include <boost/lexical_cast.hpp>
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+BOOST_AUTO_TEST_SUITE(TestValidationError)
+
+BOOST_AUTO_TEST_CASE(Basic)
+{
+  ValidationError e1{ValidationError::Code::INVALID_SIGNATURE};
+  BOOST_CHECK_EQUAL(e1.getCode(), 1);
+  BOOST_CHECK_EQUAL(e1.getInfo(), "");
+  BOOST_CHECK_EQUAL(boost::lexical_cast<std::string>(e1), "Invalid signature");
+
+  ValidationError e2{ValidationError::Code::NO_SIGNATURE, "message"};
+  BOOST_CHECK_EQUAL(e2.getCode(), 2);
+  BOOST_CHECK_EQUAL(e2.getInfo(), "message");
+  BOOST_CHECK_EQUAL(boost::lexical_cast<std::string>(e2), "Missing signature (message)");
+
+  ValidationError e3{65535, "other message"};
+  BOOST_CHECK_EQUAL(e3.getCode(), 65535);
+  BOOST_CHECK_EQUAL(e3.getInfo(), "other message");
+  BOOST_CHECK_EQUAL(boost::lexical_cast<std::string>(e3), "Custom error code 65535 (other message)");
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestValidationError
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/validation-policy-accept-all.t.cpp b/tests/unit/security/v2/validation-policy-accept-all.t.cpp
new file mode 100644
index 0000000..61466d6
--- /dev/null
+++ b/tests/unit/security/v2/validation-policy-accept-all.t.cpp
@@ -0,0 +1,79 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/validation-policy-accept-all.hpp"
+
+#include "boost-test.hpp"
+#include "validator-fixture.hpp"
+
+#include <boost/mpl/vector.hpp>
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+using namespace ndn::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+
+class ValidationPolicyAcceptAllFixture : public ValidatorFixture<ValidationPolicyAcceptAll>
+{
+public:
+  ValidationPolicyAcceptAllFixture()
+  {
+    identity = addIdentity("/Security/V2/TestValidationPolicyAcceptAll");
+    // don't add trust anchors
+  }
+
+public:
+  Identity identity;
+};
+
+BOOST_FIXTURE_TEST_SUITE(TestValidationPolicyAcceptAll, ValidationPolicyAcceptAllFixture)
+
+typedef boost::mpl::vector<Interest, Data> Packets;
+
+BOOST_AUTO_TEST_CASE_TEMPLATE(Validate, Packet, Packets)
+{
+  Packet unsignedPacket("/Security/V2/TestValidationPolicyAcceptAll/Sub/Packet");
+
+  Packet packet = unsignedPacket;
+  VALIDATE_SUCCESS(packet, "Should accept unsigned");
+
+  packet = unsignedPacket;
+  m_keyChain.sign(packet, signingWithSha256());
+  VALIDATE_SUCCESS(packet, "Should accept Sha256Digest signature");
+
+  packet = unsignedPacket;
+  m_keyChain.sign(packet, signingByIdentity(identity));
+  VALIDATE_SUCCESS(packet, "Should accept signature while no trust anchors configured");
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestValidationPolicyAcceptAll
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/validation-policy-command-interest.t.cpp b/tests/unit/security/v2/validation-policy-command-interest.t.cpp
new file mode 100644
index 0000000..7bd42ea
--- /dev/null
+++ b/tests/unit/security/v2/validation-policy-command-interest.t.cpp
@@ -0,0 +1,485 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/validation-policy-command-interest.hpp"
+#include "security/v2/validation-policy-simple-hierarchy.hpp"
+#include "security/v2/validation-policy-accept-all.hpp"
+#include "security/command-interest-signer.hpp"
+#include "security/signing-helpers.hpp"
+
+#include "boost-test.hpp"
+#include "validator-fixture.hpp"
+#include "make-interest-data.hpp"
+
+#include <boost/lexical_cast.hpp>
+#include <boost/mpl/vector.hpp>
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+using namespace ndn::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+
+class DefaultOptions
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    return {};
+  }
+};
+
+template<class T, class InnerPolicy>
+class CommandInterestPolicyWrapper : public ValidationPolicyCommandInterest
+{
+public:
+  CommandInterestPolicyWrapper()
+    : ValidationPolicyCommandInterest(make_unique<InnerPolicy>(), T::getOptions())
+  {
+  }
+};
+
+template<class T, class InnerPolicy = ValidationPolicySimpleHierarchy>
+class ValidationPolicyCommandInterestFixture : public HierarchicalValidatorFixture<CommandInterestPolicyWrapper<T, InnerPolicy>>
+{
+public:
+  ValidationPolicyCommandInterestFixture()
+    : m_signer(this->m_keyChain)
+  {
+  }
+
+  Interest
+  makeCommandInterest(const Identity& identity)
+  {
+    return m_signer.makeCommandInterest(Name(identity.getName()).append("CMD"),
+                                        signingByIdentity(identity));
+  }
+
+public:
+  CommandInterestSigner m_signer;
+};
+
+BOOST_FIXTURE_TEST_SUITE(TestValidationPolicyCommandInterest, ValidationPolicyCommandInterestFixture<DefaultOptions>)
+
+BOOST_AUTO_TEST_SUITE(Accepts)
+
+BOOST_AUTO_TEST_CASE(Basic)
+{
+  auto i1 = makeCommandInterest(identity);
+  VALIDATE_SUCCESS(i1, "Should succeed (within grace period)");
+  VALIDATE_FAILURE(i1, "Should fail (replay attack)");
+
+  advanceClocks(5_ms);
+  auto i2 = makeCommandInterest(identity);
+  VALIDATE_SUCCESS(i2, "Should succeed (timestamp larger than previous)");
+
+  auto i3 =  m_signer.makeCommandInterest(Name(identity.getName()).append("CMD"), signingWithSha256());
+  VALIDATE_FAILURE(i3, "Should fail (Sha256 signature violates policy)");
+}
+
+BOOST_AUTO_TEST_CASE(DataPassthru)
+{
+  Data d1("/Security/V2/ValidatorFixture/Sub1");
+  m_keyChain.sign(d1);
+  VALIDATE_SUCCESS(d1, "Should succeed (fallback on inner validation policy for data)");
+}
+
+using ValidationPolicyAcceptAllCommands = ValidationPolicyCommandInterestFixture<DefaultOptions,
+                                                                                 ValidationPolicyAcceptAll>;
+
+BOOST_FIXTURE_TEST_CASE(SignedWithSha256, ValidationPolicyAcceptAllCommands) // Bug 4635
+{
+  auto i1 = m_signer.makeCommandInterest("/hello/world/CMD", signingWithSha256());
+  VALIDATE_SUCCESS(i1, "Should succeed (within grace period)");
+  VALIDATE_FAILURE(i1, "Should fail (replay attack)");
+
+  advanceClocks(5_ms);
+  auto i2 = m_signer.makeCommandInterest("/hello/world/CMD", signingWithSha256());
+  VALIDATE_SUCCESS(i2, "Should succeed (timestamp larger than previous)");
+}
+
+BOOST_AUTO_TEST_SUITE_END() // Accepts
+
+BOOST_AUTO_TEST_SUITE(Rejects)
+
+BOOST_AUTO_TEST_CASE(NameTooShort)
+{
+  auto i1 = makeInterest("/name/too/short");
+  VALIDATE_FAILURE(*i1, "Should fail (name is too short)");
+}
+
+BOOST_AUTO_TEST_CASE(BadTimestamp)
+{
+  auto i1 = makeCommandInterest(identity);
+  setNameComponent(i1, command_interest::POS_TIMESTAMP, "not-timestamp");
+  VALIDATE_FAILURE(i1, "Should fail (timestamp is missing)");
+}
+
+BOOST_AUTO_TEST_CASE(BadSigInfo)
+{
+  auto i1 = makeCommandInterest(identity);
+  setNameComponent(i1, command_interest::POS_SIG_INFO, "not-SignatureInfo");
+  VALIDATE_FAILURE(i1, "Should fail (signature info is missing)");
+}
+
+BOOST_AUTO_TEST_CASE(MissingKeyLocator)
+{
+  auto i1 = makeCommandInterest(identity);
+  SignatureInfo sigInfo(tlv::SignatureSha256WithRsa);
+  setNameComponent(i1, command_interest::POS_SIG_INFO,
+                   sigInfo.wireEncode().begin(), sigInfo.wireEncode().end());
+  VALIDATE_FAILURE(i1, "Should fail (missing KeyLocator)");
+}
+
+BOOST_AUTO_TEST_CASE(BadKeyLocatorType)
+{
+  auto i1 = makeCommandInterest(identity);
+  KeyLocator kl;
+  kl.setKeyDigest(makeBinaryBlock(tlv::KeyDigest, "\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD", 8));
+  SignatureInfo sigInfo(tlv::SignatureSha256WithRsa);
+  sigInfo.setKeyLocator(kl);
+  setNameComponent(i1, command_interest::POS_SIG_INFO,
+                   sigInfo.wireEncode().begin(), sigInfo.wireEncode().end());
+  VALIDATE_FAILURE(i1, "Should fail (bad KeyLocator type)");
+}
+
+BOOST_AUTO_TEST_CASE(BadCertName)
+{
+  auto i1 = makeCommandInterest(identity);
+  KeyLocator kl;
+  kl.setName("/bad/cert/name");
+  SignatureInfo sigInfo(tlv::SignatureSha256WithRsa);
+  sigInfo.setKeyLocator(kl);
+  setNameComponent(i1, command_interest::POS_SIG_INFO,
+                   sigInfo.wireEncode().begin(), sigInfo.wireEncode().end());
+  VALIDATE_FAILURE(i1, "Should fail (bad certificate name)");
+}
+
+BOOST_AUTO_TEST_CASE(InnerPolicyReject)
+{
+  auto i1 = makeCommandInterest(otherIdentity);
+  VALIDATE_FAILURE(i1, "Should fail (inner policy should reject)");
+}
+
+class GracePeriod15Sec
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = 15_s;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(TimestampOutOfGracePositive, ValidationPolicyCommandInterestFixture<GracePeriod15Sec>)
+{
+  auto i1 = makeCommandInterest(identity); // signed at 0s
+  advanceClocks(16_s); // verifying at +16s
+  VALIDATE_FAILURE(i1, "Should fail (timestamp outside the grace period)");
+  rewindClockAfterValidation();
+
+  auto i2 = makeCommandInterest(identity); // signed at +16s
+  VALIDATE_SUCCESS(i2, "Should succeed");
+}
+
+BOOST_FIXTURE_TEST_CASE(TimestampOutOfGraceNegative, ValidationPolicyCommandInterestFixture<GracePeriod15Sec>)
+{
+  auto i1 = makeCommandInterest(identity); // signed at 0s
+  advanceClocks(1_s);
+  auto i2 = makeCommandInterest(identity); // signed at +1s
+  advanceClocks(1_s);
+  auto i3 = makeCommandInterest(identity); // signed at +2s
+
+  systemClock->advance(-18_s); // verifying at -16s
+  VALIDATE_FAILURE(i1, "Should fail (timestamp outside the grace period)");
+  rewindClockAfterValidation();
+
+  // CommandInterestValidator should not remember i1's timestamp
+  VALIDATE_FAILURE(i2, "Should fail (timestamp outside the grace period)");
+  rewindClockAfterValidation();
+
+  // CommandInterestValidator should not remember i2's timestamp, and should treat i3 as initial
+  advanceClocks(18_s); // verifying at +2s
+  VALIDATE_SUCCESS(i3, "Should succeed");
+}
+
+BOOST_AUTO_TEST_CASE(TimestampReorderEqual)
+{
+  auto i1 = makeCommandInterest(identity); // signed at 0s
+  VALIDATE_SUCCESS(i1, "Should succeed");
+
+  auto i2 = makeCommandInterest(identity); // signed at 0s
+  setNameComponent(i2, command_interest::POS_TIMESTAMP,
+                   i1.getName()[command_interest::POS_TIMESTAMP]);
+  VALIDATE_FAILURE(i2, "Should fail (timestamp reordered)");
+
+  advanceClocks(2_s);
+  auto i3 = makeCommandInterest(identity); // signed at +2s
+  VALIDATE_SUCCESS(i3, "Should succeed");
+}
+
+BOOST_AUTO_TEST_CASE(TimestampReorderNegative)
+{
+  auto i2 = makeCommandInterest(identity); // signed at 0ms
+  advanceClocks(200_ms);
+  auto i3 = makeCommandInterest(identity); // signed at +200ms
+  advanceClocks(900_ms);
+  auto i1 = makeCommandInterest(identity); // signed at +1100ms
+  advanceClocks(300_ms);
+  auto i4 = makeCommandInterest(identity); // signed at +1400ms
+
+  systemClock->advance(-300_ms); // verifying at +1100ms
+  VALIDATE_SUCCESS(i1, "Should succeed");
+  rewindClockAfterValidation();
+
+  systemClock->advance(-1100_ms); // verifying at 0ms
+  VALIDATE_FAILURE(i2, "Should fail (timestamp reordered)");
+  rewindClockAfterValidation();
+
+  // CommandInterestValidator should not remember i2's timestamp
+  advanceClocks(200_ms); // verifying at +200ms
+  VALIDATE_FAILURE(i3, "Should fail (timestamp reordered)");
+  rewindClockAfterValidation();
+
+  advanceClocks(1200_ms); // verifying at 1400ms
+  VALIDATE_SUCCESS(i4, "Should succeed");
+}
+
+BOOST_AUTO_TEST_SUITE_END() // Rejects
+
+BOOST_AUTO_TEST_SUITE(Options)
+
+template<class T>
+class GracePeriod
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = time::seconds(T::value);
+    return options;
+  }
+};
+
+typedef boost::mpl::vector<
+  GracePeriod<boost::mpl::int_<0>>,
+  GracePeriod<boost::mpl::int_<-1>>
+> GraceNonPositiveValues;
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(GraceNonPositive, GracePeriod, GraceNonPositiveValues,
+                                 ValidationPolicyCommandInterestFixture<GracePeriod>)
+{
+  auto i1 = this->makeCommandInterest(this->identity); // signed at 0ms
+  auto i2 = this->makeCommandInterest(this->subIdentity); // signed at 0ms
+  for (auto interest : {&i1, &i2}) {
+    setNameComponent(*interest, command_interest::POS_TIMESTAMP,
+                     name::Component::fromNumber(time::toUnixTimestamp(time::system_clock::now()).count()));
+  } // ensure timestamps are exactly 0ms
+
+  VALIDATE_SUCCESS(i1, "Should succeed when validating at 0ms");
+  this->rewindClockAfterValidation();
+
+  this->advanceClocks(1_ms);
+  VALIDATE_FAILURE(i2, "Should fail when validating at 1ms");
+}
+
+class LimitedRecordsOptions
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = 15_s;
+    options.maxRecords = 3;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(LimitedRecords, ValidationPolicyCommandInterestFixture<LimitedRecordsOptions>)
+{
+  Identity id1 = this->addSubCertificate("/Security/V2/ValidatorFixture/Sub1", identity);
+  this->cache.insert(id1.getDefaultKey().getDefaultCertificate());
+  Identity id2 = this->addSubCertificate("/Security/V2/ValidatorFixture/Sub2", identity);
+  this->cache.insert(id2.getDefaultKey().getDefaultCertificate());
+  Identity id3 = this->addSubCertificate("/Security/V2/ValidatorFixture/Sub3", identity);
+  this->cache.insert(id3.getDefaultKey().getDefaultCertificate());
+  Identity id4 = this->addSubCertificate("/Security/V2/ValidatorFixture/Sub4", identity);
+  this->cache.insert(id4.getDefaultKey().getDefaultCertificate());
+
+  auto i1 = makeCommandInterest(id2);
+  auto i2 = makeCommandInterest(id3);
+  auto i3 = makeCommandInterest(id4);
+  auto i00 = makeCommandInterest(id1); // signed at 0s
+  advanceClocks(1_s);
+  auto i01 = makeCommandInterest(id1); // signed at 1s
+  advanceClocks(1_s);
+  auto i02 = makeCommandInterest(id1); // signed at 2s
+
+  VALIDATE_SUCCESS(i00, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i02, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i1, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i2, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i3, "Should succeed, forgets identity id1");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i01, "Should succeed despite timestamp is reordered, because record has been evicted");
+}
+
+class UnlimitedRecordsOptions
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = 15_s;
+    options.maxRecords = -1;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(UnlimitedRecords, ValidationPolicyCommandInterestFixture<UnlimitedRecordsOptions>)
+{
+  std::vector<Identity> identities;
+  for (int i = 0; i < 20; ++i) {
+    Identity id = this->addSubCertificate("/Security/V2/ValidatorFixture/Sub" + to_string(i), identity);
+    this->cache.insert(id.getDefaultKey().getDefaultCertificate());
+    identities.push_back(id);
+  }
+
+  auto i1 = makeCommandInterest(identities.at(0)); // signed at 0s
+  advanceClocks(1_s);
+  for (int i = 0; i < 20; ++i) {
+    auto i2 = makeCommandInterest(identities.at(i)); // signed at +1s
+
+    VALIDATE_SUCCESS(i2, "Should succeed");
+    rewindClockAfterValidation();
+  }
+  VALIDATE_FAILURE(i1, "Should fail (timestamp reorder)");
+}
+
+class ZeroRecordsOptions
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = 15_s;
+    options.maxRecords = 0;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(ZeroRecords, ValidationPolicyCommandInterestFixture<ZeroRecordsOptions>)
+{
+  auto i1 = makeCommandInterest(identity); // signed at 0s
+  advanceClocks(1_s);
+  auto i2 = makeCommandInterest(identity); // signed at +1s
+  VALIDATE_SUCCESS(i2, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i1, "Should succeed despite timestamp is reordered, because record isn't kept");
+}
+
+class LimitedRecordLifetimeOptions
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = 400_s;
+    options.recordLifetime = 300_s;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(LimitedRecordLifetime, ValidationPolicyCommandInterestFixture<LimitedRecordLifetimeOptions>)
+{
+  auto i1 = makeCommandInterest(identity); // signed at 0s
+  advanceClocks(240_s);
+  auto i2 = makeCommandInterest(identity); // signed at +240s
+  advanceClocks(120_s);
+  auto i3 = makeCommandInterest(identity); // signed at +360s
+
+  systemClock->advance(-360_s); // rewind system clock to 0s
+  VALIDATE_SUCCESS(i1, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i3, "Should succeed");
+  rewindClockAfterValidation();
+
+  advanceClocks(30_s, 301_s); // advance steady clock by 301s, and system clock to +301s
+  VALIDATE_SUCCESS(i2, "Should succeed despite timestamp is reordered, because record has been expired");
+}
+
+class ZeroRecordLifetimeOptions
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = 15_s;
+    options.recordLifetime = time::seconds::zero();
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(ZeroRecordLifetime, ValidationPolicyCommandInterestFixture<ZeroRecordLifetimeOptions>)
+{
+  auto i1 = makeCommandInterest(identity); // signed at 0s
+  advanceClocks(1_s);
+  auto i2 = makeCommandInterest(identity); // signed at +1s
+  VALIDATE_SUCCESS(i2, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i1, "Should succeed despite timestamp is reordered, because record has been expired");
+}
+
+BOOST_AUTO_TEST_SUITE_END() // Options
+
+BOOST_AUTO_TEST_SUITE_END() // TestValidationPolicyCommandInterest
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/validation-policy-config.t.cpp b/tests/unit/security/v2/validation-policy-config.t.cpp
new file mode 100644
index 0000000..cca3e7a
--- /dev/null
+++ b/tests/unit/security/v2/validation-policy-config.t.cpp
@@ -0,0 +1,539 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/validation-policy-config.hpp"
+#include "security/transform/base64-encode.hpp"
+#include "security/transform/buffer-source.hpp"
+#include "security/transform/stream-sink.hpp"
+#include "util/logger.hpp"
+#include "util/io.hpp"
+
+#include "boost-test.hpp"
+#include "validator-config/common.hpp"
+#include "validator-fixture.hpp"
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace validator_config {
+namespace tests {
+
+using namespace ndn::tests;
+using namespace ndn::security::v2::tests;
+
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+BOOST_AUTO_TEST_SUITE(TestValidationPolicyConfig)
+
+template<typename Packet>
+class PacketName;
+
+template<>
+class PacketName<Interest>
+{
+public:
+  static std::string
+  getName()
+  {
+    return "interest";
+  }
+};
+
+template<>
+class PacketName<Data>
+{
+public:
+  static std::string
+  getName()
+  {
+    return "data";
+  }
+};
+
+template<typename PacketType>
+class ValidationPolicyConfigFixture : public HierarchicalValidatorFixture<ValidationPolicyConfig>
+{
+public:
+  ValidationPolicyConfigFixture()
+    : path(boost::filesystem::path(UNIT_TEST_CONFIG_PATH) / "security" / "v2" / "validation-policy-config")
+  {
+    boost::filesystem::create_directories(path);
+    baseConfig = R"CONF(
+        rule
+        {
+          id test-rule-id
+          for )CONF" + PacketName<Packet>::getName() + R"CONF(
+          filter
+          {
+            type name
+            name )CONF" + identity.getName().toUri() + R"CONF(
+            relation is-prefix-of
+          }
+          checker
+          {
+            type hierarchical
+            sig-type rsa-sha256
+          }
+        }
+      )CONF";
+  }
+
+  ~ValidationPolicyConfigFixture()
+  {
+    boost::filesystem::remove_all(path);
+  }
+
+public:
+  using Packet = PacketType;
+
+  const boost::filesystem::path path;
+  std::string baseConfig;
+};
+
+template<typename PacketType>
+class LoadStringWithFileAnchor : public ValidationPolicyConfigFixture<PacketType>
+{
+public:
+  LoadStringWithFileAnchor()
+  {
+    BOOST_CHECK_EQUAL(this->policy.m_isConfigured, false);
+
+    this->saveCertificate(this->identity, (this->path / "identity.ndncert").string());
+    this->policy.load(this->baseConfig + R"CONF(
+        trust-anchor
+        {
+          type file
+          file-name "trust-anchor.ndncert"
+        }
+      )CONF", (this->path / "test-config").string());
+
+    BOOST_CHECK_EQUAL(this->policy.m_isConfigured, true);
+    BOOST_CHECK_EQUAL(this->policy.m_shouldBypass, false);
+  }
+};
+
+template<typename PacketType>
+class LoadFileWithFileAnchor : public ValidationPolicyConfigFixture<PacketType>
+{
+public:
+  LoadFileWithFileAnchor()
+  {
+    std::string configFile = (this->path / "config.conf").string();
+    {
+      std::ofstream config(configFile.c_str());
+      config << this->baseConfig << R"CONF(
+        trust-anchor
+        {
+          type file
+          file-name "trust-anchor.ndncert"
+        }
+      )CONF";
+    }
+    this->saveCertificate(this->identity, (this->path / "identity.ndncert").string());
+
+    BOOST_CHECK_EQUAL(this->policy.m_isConfigured, false);
+
+    this->policy.load(configFile);
+
+    BOOST_CHECK_EQUAL(this->policy.m_isConfigured, true);
+    BOOST_CHECK_EQUAL(this->policy.m_shouldBypass, false);
+  }
+};
+
+template<typename PacketType>
+class LoadSectionWithFileAnchor : public ValidationPolicyConfigFixture<PacketType>
+{
+public:
+  LoadSectionWithFileAnchor()
+  {
+    auto section = makeSection(this->baseConfig + R"CONF(
+        trust-anchor
+        {
+          type file
+          file-name "trust-anchor.ndncert"
+        }
+      )CONF");
+
+    this->saveCertificate(this->identity, (this->path / "identity.ndncert").string());
+
+    BOOST_CHECK_EQUAL(this->policy.m_isConfigured, false);
+
+    this->policy.load(section, (this->path / "test-config").string());
+
+    BOOST_CHECK_EQUAL(this->policy.m_isConfigured, true);
+    BOOST_CHECK_EQUAL(this->policy.m_shouldBypass, false);
+  }
+};
+
+template<typename PacketType>
+class LoadStringWithBase64Anchor : public ValidationPolicyConfigFixture<PacketType>
+{
+public:
+  LoadStringWithBase64Anchor()
+  {
+    BOOST_CHECK_EQUAL(this->policy.m_isConfigured, false);
+
+    std::ostringstream os;
+    using namespace ndn::security::transform;
+    const auto& cert = this->identity.getDefaultKey().getDefaultCertificate();
+    bufferSource(cert.wireEncode().wire(), cert.wireEncode().size()) >> base64Encode(false) >> streamSink(os);
+
+    this->policy.load(this->baseConfig + R"CONF(
+        trust-anchor
+        {
+          type base64
+          base64-string ")CONF" + os.str() + R"CONF("
+        }
+      )CONF", (this->path / "test-config").string());
+
+    BOOST_CHECK_EQUAL(this->policy.m_isConfigured, true);
+    BOOST_CHECK_EQUAL(this->policy.m_shouldBypass, false);
+  }
+};
+
+class NoRefresh
+{
+public:
+  static std::string
+  getRefreshString()
+  {
+    return "";
+  }
+};
+
+class Refresh1h
+{
+public:
+  static std::string
+  getRefreshString()
+  {
+    return "refresh 1h";
+  }
+
+  static time::milliseconds
+  getRefreshTime()
+  {
+    return 1_h;
+  }
+};
+
+class Refresh1m
+{
+public:
+  static std::string
+  getRefreshString()
+  {
+    return "refresh 1m";
+  }
+
+  static time::milliseconds
+  getRefreshTime()
+  {
+    return 1_min;
+  }
+};
+
+class Refresh1s
+{
+public:
+  static std::string
+  getRefreshString()
+  {
+    return "refresh 1s";
+  }
+
+  static time::milliseconds
+  getRefreshTime()
+  {
+    return 1_s;
+  }
+};
+
+template<typename PacketType, typename Refresh = NoRefresh>
+class LoadStringWithDirAnchor : public ValidationPolicyConfigFixture<PacketType>
+{
+public:
+  LoadStringWithDirAnchor()
+  {
+    BOOST_CHECK_EQUAL(this->policy.m_isConfigured, false);
+
+    boost::filesystem::create_directories(this->path / "keys");
+    this->saveCertificate(this->identity, (this->path / "keys" / "identity.ndncert").string());
+
+    this->policy.load(this->baseConfig + R"CONF(
+        trust-anchor
+        {
+          type dir
+          dir keys
+          )CONF" + Refresh::getRefreshString() + R"CONF(
+        }
+      )CONF", (this->path / "test-config").string());
+
+    BOOST_CHECK_EQUAL(this->policy.m_isConfigured, true);
+    BOOST_CHECK_EQUAL(this->policy.m_shouldBypass, false);
+  }
+};
+
+using DataPolicies = boost::mpl::vector<LoadStringWithFileAnchor<Data>,
+                                        LoadFileWithFileAnchor<Data>,
+                                        LoadSectionWithFileAnchor<Data>,
+                                        LoadStringWithBase64Anchor<Data>,
+                                        LoadStringWithDirAnchor<Data>,
+                                        LoadStringWithDirAnchor<Data, Refresh1h>,
+                                        LoadStringWithDirAnchor<Data, Refresh1m>,
+                                        LoadStringWithDirAnchor<Data, Refresh1s>
+                                        >;
+
+using InterestPolicies = boost::mpl::vector<LoadStringWithFileAnchor<Interest>,
+                                            LoadFileWithFileAnchor<Interest>,
+                                            LoadSectionWithFileAnchor<Interest>,
+                                            LoadStringWithBase64Anchor<Interest>,
+                                            LoadStringWithDirAnchor<Interest>,
+                                            LoadStringWithDirAnchor<Interest, Refresh1h>,
+                                            LoadStringWithDirAnchor<Interest, Refresh1m>,
+                                            LoadStringWithDirAnchor<Interest, Refresh1s>
+                                            >;
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(ValidateData, Policy, DataPolicies, Policy)
+{
+  BOOST_CHECK_EQUAL(this->policy.m_dataRules.size(), 1);
+  BOOST_CHECK_EQUAL(this->policy.m_interestRules.size(), 0);
+
+  using Packet = typename Policy::Packet;
+  Packet unsignedPacket("/Security/V2/ValidatorFixture/Sub1/Sub2/Packet");
+
+  Packet packet = unsignedPacket;
+  VALIDATE_FAILURE(packet, "Unsigned");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingWithSha256());
+  VALIDATE_FAILURE(packet, "Policy doesn't accept Sha256Digest signature");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingByIdentity(this->identity));
+  VALIDATE_SUCCESS(packet, "Should get accepted, as signed by the anchor");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingByIdentity(this->subIdentity));
+  VALIDATE_SUCCESS(packet, "Should get accepted, as signed by the policy-compliant cert");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingByIdentity(this->otherIdentity));
+  VALIDATE_FAILURE(packet, "Should fail, as signed by the policy-violating cert");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingByIdentity(this->subSelfSignedIdentity));
+  VALIDATE_FAILURE(packet, "Should fail, because subSelfSignedIdentity is not a trust anchor");
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(ValidateInterest, Policy, InterestPolicies, Policy)
+{
+  BOOST_CHECK_EQUAL(this->policy.m_dataRules.size(), 0);
+  BOOST_CHECK_EQUAL(this->policy.m_interestRules.size(), 1);
+
+  using Packet = typename Policy::Packet;
+  Packet unsignedPacket("/Security/V2/ValidatorFixture/Sub1/Sub2/Packet");
+
+  Packet packet = unsignedPacket;
+  VALIDATE_FAILURE(packet, "Unsigned");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingWithSha256());
+  VALIDATE_FAILURE(packet, "Policy doesn't accept Sha256Digest signature");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingByIdentity(this->identity));
+  VALIDATE_SUCCESS(packet, "Should get accepted, as signed by the anchor");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingByIdentity(this->subIdentity));
+  VALIDATE_FAILURE(packet, "Should fail, as there is no matching rule for data");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingByIdentity(this->otherIdentity));
+  VALIDATE_FAILURE(packet, "Should fail, as signed by the policy-violating cert");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingByIdentity(this->subSelfSignedIdentity));
+  VALIDATE_FAILURE(packet, "Should fail, because subSelfSignedIdentity is not a trust anchor");
+}
+
+BOOST_FIXTURE_TEST_CASE(Reload, HierarchicalValidatorFixture<ValidationPolicyConfig>)
+{
+  BOOST_CHECK_EQUAL(this->policy.m_isConfigured, false);
+  this->policy.load(R"CONF(
+      rule
+      {
+        id test-rule-data-id
+        for data
+        filter
+        {
+          type name
+          name /foo/bar
+          relation is-prefix-of
+        }
+        checker
+        {
+          type hierarchical
+          sig-type rsa-sha256
+        }
+      }
+      rule
+      {
+        id test-rule-interest-id
+        for interest
+        filter
+        {
+          type name
+          name /foo/bar
+          relation is-prefix-of
+        }
+        checker
+        {
+          type hierarchical
+          sig-type rsa-sha256
+        }
+      }
+      trust-anchor
+      {
+        type dir
+        dir keys
+        refresh 1h
+      }
+    )CONF", "test-config");
+  BOOST_CHECK_EQUAL(this->policy.m_isConfigured, true);
+  BOOST_CHECK_EQUAL(this->policy.m_shouldBypass, false);
+  BOOST_CHECK_EQUAL(this->policy.m_dataRules.size(), 1);
+  BOOST_CHECK_EQUAL(this->policy.m_interestRules.size(), 1);
+
+  this->policy.load(R"CONF(
+      trust-anchor
+      {
+        type any
+      }
+    )CONF", "test-config");
+  BOOST_CHECK_EQUAL(this->policy.m_isConfigured, true);
+  BOOST_CHECK_EQUAL(this->policy.m_shouldBypass, true);
+  BOOST_CHECK_EQUAL(this->policy.m_dataRules.size(), 0);
+  BOOST_CHECK_EQUAL(this->policy.m_interestRules.size(), 0);
+}
+
+using Packets = boost::mpl::vector<Interest, Data>;
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(TrustAnchorWildcard, Packet, Packets, ValidationPolicyConfigFixture<Packet>)
+{
+  this->policy.load(R"CONF(
+      trust-anchor
+      {
+        type any
+      }
+    )CONF", "test-config");
+
+  BOOST_CHECK_EQUAL(this->policy.m_isConfigured, true);
+  BOOST_CHECK_EQUAL(this->policy.m_shouldBypass, true);
+  BOOST_CHECK_EQUAL(this->policy.m_dataRules.size(), 0);
+  BOOST_CHECK_EQUAL(this->policy.m_interestRules.size(), 0);
+
+  Packet unsignedPacket("/Security/V2/ValidatorFixture/Sub1/Sub2/Packet");
+
+  Packet packet = unsignedPacket;
+  VALIDATE_SUCCESS(packet, "Policy should accept everything");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingWithSha256());
+  VALIDATE_SUCCESS(packet, "Policy should accept everything");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingByIdentity(this->identity));
+  VALIDATE_SUCCESS(packet, "Policy should accept everything");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingByIdentity(this->subIdentity));
+  VALIDATE_SUCCESS(packet, "Policy should accept everything");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingByIdentity(this->otherIdentity));
+  VALIDATE_SUCCESS(packet, "Policy should accept everything");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingByIdentity(this->subSelfSignedIdentity));
+  VALIDATE_SUCCESS(packet, "Policy should accept everything");
+}
+
+using RefreshPolicies = boost::mpl::vector<Refresh1h, Refresh1m, Refresh1s>;
+
+// Somehow, didn't work without this wrapper
+template<typename RefreshPolicy>
+class RefreshPolicyFixture : public LoadStringWithDirAnchor<Data, RefreshPolicy>
+{
+public:
+};
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(ValidateRefresh, Refresh, RefreshPolicies, RefreshPolicyFixture<Refresh>)
+{
+  using Packet = Data;
+  Packet unsignedPacket("/Security/V2/ValidatorFixture/Sub1/Sub2/Packet");
+
+  boost::filesystem::remove(this->path / "keys" / "identity.ndncert");
+  this->advanceClocks(Refresh::getRefreshTime(), 3);
+
+  Packet packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingByIdentity(this->identity));
+  VALIDATE_FAILURE(packet, "Should fail, as the trust anchor should no longer exist");
+
+  packet = unsignedPacket;
+  this->m_keyChain.sign(packet, signingByIdentity(this->subIdentity));
+  VALIDATE_FAILURE(packet, "Should fail, as the trust anchor should no longer exist");
+}
+
+BOOST_FIXTURE_TEST_CASE(OrphanedPolicyLoad, HierarchicalValidatorFixture<ValidationPolicyConfig>) // Bug #4758
+{
+  ValidationPolicyConfig policy1;
+  BOOST_CHECK_THROW(policy1.load("trust-anchor { type any }", "test-config"), Error);
+
+  // Reloading would have triggered a segfault
+  BOOST_CHECK_THROW(policy1.load("trust-anchor { type any }", "test-config"), Error);
+
+  ValidationPolicyConfig policy2;
+
+  std::string config = R"CONF(
+      trust-anchor
+      {
+        type dir
+        dir keys
+        refresh 1h
+      }
+    )CONF";
+
+  // Inserting trust anchor would have triggered a segfault
+  BOOST_CHECK_THROW(policy2.load(config, "test-config"), Error);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestValidationPolicyConfig
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace validator_config
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/validation-policy-simple-hierarchy.t.cpp b/tests/unit/security/v2/validation-policy-simple-hierarchy.t.cpp
new file mode 100644
index 0000000..8661ac5
--- /dev/null
+++ b/tests/unit/security/v2/validation-policy-simple-hierarchy.t.cpp
@@ -0,0 +1,80 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/validation-policy-simple-hierarchy.hpp"
+
+#include "boost-test.hpp"
+#include "validator-fixture.hpp"
+
+#include <boost/mpl/vector.hpp>
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+using namespace ndn::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+BOOST_FIXTURE_TEST_SUITE(TestValidationPolicySimpleHierarchy,
+                         HierarchicalValidatorFixture<ValidationPolicySimpleHierarchy>)
+
+typedef boost::mpl::vector<Interest, Data> Packets;
+
+BOOST_AUTO_TEST_CASE_TEMPLATE(Validate, Packet, Packets)
+{
+  Packet unsignedPacket("/Security/V2/ValidatorFixture/Sub1/Sub2/Packet");
+
+  Packet packet = unsignedPacket;
+  VALIDATE_FAILURE(packet, "Unsigned");
+
+  packet = unsignedPacket;
+  m_keyChain.sign(packet, signingWithSha256());
+  VALIDATE_FAILURE(packet, "Policy doesn't accept Sha256Digest signature");
+
+  packet = unsignedPacket;
+  m_keyChain.sign(packet, signingByIdentity(identity));
+  VALIDATE_SUCCESS(packet, "Should get accepted, as signed by the anchor");
+
+  packet = unsignedPacket;
+  m_keyChain.sign(packet, signingByIdentity(subIdentity));
+  VALIDATE_SUCCESS(packet, "Should get accepted, as signed by the policy-compliant cert");
+
+  packet = unsignedPacket;
+  m_keyChain.sign(packet, signingByIdentity(otherIdentity));
+  VALIDATE_FAILURE(packet, "Should fail, as signed by the policy-violating cert");
+
+  packet = unsignedPacket;
+  m_keyChain.sign(packet, signingByIdentity(subSelfSignedIdentity));
+  VALIDATE_FAILURE(packet, "Should fail, because subSelfSignedIdentity is not a trust anchor");
+
+  // TODO add checks with malformed packets
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestValidator
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/validator-config/checker.t.cpp b/tests/unit/security/v2/validator-config/checker.t.cpp
new file mode 100644
index 0000000..2170908
--- /dev/null
+++ b/tests/unit/security/v2/validator-config/checker.t.cpp
@@ -0,0 +1,374 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/validator-config/checker.hpp"
+#include "security/command-interest-signer.hpp"
+#include "security/v2/validation-policy.hpp"
+#include "security/v2/validation-state.hpp"
+
+#include "boost-test.hpp"
+#include "common.hpp"
+#include "identity-management-fixture.hpp"
+#include "../validator-fixture.hpp"
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace validator_config {
+namespace tests {
+
+using namespace ndn::tests;
+using namespace ndn::security::v2::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+BOOST_AUTO_TEST_SUITE(ValidatorConfig)
+
+class CheckerFixture : public IdentityManagementFixture
+{
+public:
+  CheckerFixture()
+  {
+    names.push_back("/foo/bar");
+    names.push_back("/foo/bar/bar");
+    names.push_back("/foo");
+    names.push_back("/other/prefix");
+  }
+
+  Name
+  makeSignedInterestName(const Name& name)
+  {
+    return Name(name).append("SignatureInfo").append("SignatureValue");
+  }
+
+  Name
+  makeKeyLocatorName(const Name& name)
+  {
+    return Name(name).append("KEY").append("v=1");
+  }
+
+public:
+  std::vector<Name> names;
+};
+
+BOOST_FIXTURE_TEST_SUITE(TestChecker, CheckerFixture)
+
+class NameRelationEqual : public CheckerFixture
+{
+public:
+  NameRelationEqual()
+    : checker("/foo/bar", NameRelation::EQUAL)
+  {
+  }
+
+public:
+  NameRelationChecker checker;
+  std::vector<std::vector<bool>> outcomes = {{true, false, false, false},
+                                             {true, false, false, false},
+                                             {true, false, false, false},
+                                             {true, false, false, false}};
+};
+
+class NameRelationIsPrefixOf : public CheckerFixture
+{
+public:
+  NameRelationIsPrefixOf()
+    : checker("/foo/bar", NameRelation::IS_PREFIX_OF)
+  {
+  }
+
+public:
+  NameRelationChecker checker;
+  std::vector<std::vector<bool>> outcomes = {{true, true, false, false},
+                                             {true, true, false, false},
+                                             {true, true, false, false},
+                                             {true, true, false, false}};
+};
+
+class NameRelationIsStrictPrefixOf : public CheckerFixture
+{
+public:
+  NameRelationIsStrictPrefixOf()
+    : checker("/foo/bar", NameRelation::IS_STRICT_PREFIX_OF)
+  {
+  }
+
+public:
+  NameRelationChecker checker;
+  std::vector<std::vector<bool>> outcomes = {{false, true, false, false},
+                                             {false, true, false, false},
+                                             {false, true, false, false},
+                                             {false, true, false, false}};
+};
+
+class RegexEqual : public CheckerFixture
+{
+public:
+  RegexEqual()
+    : checker(Regex("^<foo><bar><KEY><>$"))
+  {
+  }
+
+public:
+  RegexChecker checker;
+  std::vector<std::vector<bool>> outcomes = {{true, false, false, false},
+                                             {true, false, false, false},
+                                             {true, false, false, false},
+                                             {true, false, false, false}};
+};
+
+class RegexIsPrefixOf : public CheckerFixture
+{
+public:
+  RegexIsPrefixOf()
+    : checker(Regex("^<foo><bar><>*<KEY><>$"))
+  {
+  }
+
+public:
+  RegexChecker checker;
+  std::vector<std::vector<bool>> outcomes = {{true, true, false, false},
+                                             {true, true, false, false},
+                                             {true, true, false, false},
+                                             {true, true, false, false}};
+};
+
+class RegexIsStrictPrefixOf : public CheckerFixture
+{
+public:
+  RegexIsStrictPrefixOf()
+    : checker(Regex("^<foo><bar><>+<KEY><>$"))
+  {
+  }
+
+public:
+  RegexChecker checker;
+  std::vector<std::vector<bool>> outcomes = {{false, true, false, false},
+                                             {false, true, false, false},
+                                             {false, true, false, false},
+                                             {false, true, false, false}};
+};
+
+class HyperRelationEqual : public CheckerFixture
+{
+public:
+  HyperRelationEqual()
+    : checker("^(<>+)$", "\\1", "^(<>+)<KEY><>$", "\\1", NameRelation::EQUAL)
+  {
+  }
+
+public:
+  HyperRelationChecker checker;
+  std::vector<std::vector<bool>> outcomes = {{true,  false, false, false},
+                                             {false, true,  false, false},
+                                             {false, false, true,  false},
+                                             {false, false, false, true}};
+};
+
+class HyperRelationIsPrefixOf : public CheckerFixture
+{
+public:
+  HyperRelationIsPrefixOf()
+    : checker("^(<>+)$", "\\1", "^(<>+)<KEY><>$", "\\1", NameRelation::IS_PREFIX_OF)
+  {
+  }
+
+public:
+  HyperRelationChecker checker;
+  std::vector<std::vector<bool>> outcomes = {{true,  false, true,  false},
+                                             {true,  true,  true,  false},
+                                             {false, false, true,  false},
+                                             {false, false, false, true}};
+};
+
+class HyperRelationIsStrictPrefixOf : public CheckerFixture
+{
+public:
+  HyperRelationIsStrictPrefixOf()
+    : checker("^(<>+)$", "\\1", "^(<>+)<KEY><>$", "\\1", NameRelation::IS_STRICT_PREFIX_OF)
+  {
+  }
+
+public:
+  HyperRelationChecker checker;
+  std::vector<std::vector<bool>> outcomes = {{false, false, true,  false},
+                                             {true,  false, true,  false},
+                                             {false, false, false, false},
+                                             {false, false, false, false}};
+};
+
+class Hierarchical : public CheckerFixture
+{
+public:
+  Hierarchical()
+    : checkerPtr(Checker::create(makeSection(R"CONF(
+          type hierarchical
+          sig-type rsa-sha256
+        )CONF"), "test-config"))
+    , checker(*checkerPtr)
+  {
+  }
+
+public:
+  std::unique_ptr<Checker> checkerPtr;
+  Checker& checker;
+
+  std::vector<std::vector<bool>> outcomes = {{true,  false, true,  false},
+                                             {true,  true,  true,  false},
+                                             {false, false, true,  false},
+                                             {false, false, false, true}};
+};
+
+class CustomizedNameRelation : public CheckerFixture
+{
+public:
+  CustomizedNameRelation()
+    : checkerPtr(Checker::create(makeSection(R"CONF(
+          type customized
+          sig-type rsa-sha256
+          key-locator
+          {
+            type name
+            name /foo/bar
+            relation equal
+          }
+        )CONF"), "test-config"))
+    , checker(*checkerPtr)
+  {
+  }
+
+public:
+  std::unique_ptr<Checker> checkerPtr;
+  Checker& checker;
+
+  std::vector<std::vector<bool>> outcomes = {{true, false, false, false},
+                                             {true, false, false, false},
+                                             {true, false, false, false},
+                                             {true, false, false, false}};
+};
+
+class CustomizedRegex : public CheckerFixture
+{
+public:
+  CustomizedRegex()
+    : checkerPtr(Checker::create(makeSection(R"CONF(
+          type customized
+          sig-type rsa-sha256
+          key-locator
+          {
+            type name
+            regex ^<foo><bar><KEY><>$
+          }
+        )CONF"), "test-config"))
+    , checker(*checkerPtr)
+  {
+  }
+
+public:
+  std::unique_ptr<Checker> checkerPtr;
+  Checker& checker;
+
+  std::vector<std::vector<bool>> outcomes = {{true, false, false, false},
+                                             {true, false, false, false},
+                                             {true, false, false, false},
+                                             {true, false, false, false}};
+};
+
+class CustomizedHyperRelation : public CheckerFixture
+{
+public:
+  CustomizedHyperRelation()
+    : checkerPtr(Checker::create(makeSection(R"CONF(
+          type customized
+          sig-type rsa-sha256
+          key-locator
+          {
+            type name
+            hyper-relation
+            {
+              k-regex ^(<>+)<KEY><>$
+              k-expand \\1
+              h-relation is-prefix-of
+              p-regex ^(<>+)$
+              p-expand \\1
+            }
+          }
+        )CONF"), "test-config"))
+    , checker(*checkerPtr)
+  {
+  }
+
+public:
+  std::unique_ptr<Checker> checkerPtr;
+  Checker& checker;
+
+  std::vector<std::vector<bool>> outcomes = {{true,  false, true,  false},
+                                             {true,  true,  true,  false},
+                                             {false, false, true,  false},
+                                             {false, false, false, true}};
+};
+
+
+using Tests = boost::mpl::vector<NameRelationEqual, NameRelationIsPrefixOf, NameRelationIsStrictPrefixOf,
+                                 RegexEqual, RegexIsPrefixOf, RegexIsStrictPrefixOf,
+                                 HyperRelationEqual, HyperRelationIsPrefixOf, HyperRelationIsStrictPrefixOf,
+                                 Hierarchical,
+                                 CustomizedNameRelation, CustomizedRegex, CustomizedHyperRelation>;
+
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(Checks, T, Tests, T)
+{
+  BOOST_ASSERT(this->outcomes.size() == this->names.size());
+  for (size_t i = 0; i < this->names.size(); ++i) {
+    BOOST_ASSERT(this->outcomes[i].size() == this->names.size());
+    for (size_t j = 0; j < this->names.size(); ++j) {
+      const Name& pktName = this->names[i];
+      Name klName = this->makeKeyLocatorName(this->names[j]);
+      bool expectedOutcome = this->outcomes[i][j];
+
+      auto dataState = make_shared<DummyValidationState>();
+      BOOST_CHECK_EQUAL(this->checker.check(tlv::Data, pktName, klName, dataState), expectedOutcome);
+      BOOST_CHECK_EQUAL(boost::logic::indeterminate(dataState->getOutcome()), expectedOutcome);
+      if (boost::logic::indeterminate(dataState->getOutcome()) == !expectedOutcome) {
+        BOOST_CHECK_EQUAL(dataState->getOutcome(), !expectedOutcome);
+      }
+
+      auto interestState = make_shared<DummyValidationState>();
+      BOOST_CHECK_EQUAL(this->checker.check(tlv::Interest, this->makeSignedInterestName(pktName),
+                                            klName, interestState), expectedOutcome);
+      BOOST_CHECK_EQUAL(boost::logic::indeterminate(interestState->getOutcome()), expectedOutcome);
+      if (boost::logic::indeterminate(interestState->getOutcome()) == !expectedOutcome) {
+        BOOST_CHECK_EQUAL(interestState->getOutcome(), !expectedOutcome);
+      }
+    }
+  }
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestChecker
+BOOST_AUTO_TEST_SUITE_END() // ValidatorConfig
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace validator_config
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/validator-config/common.hpp b/tests/unit/security/v2/validator-config/common.hpp
new file mode 100644
index 0000000..231e8eb
--- /dev/null
+++ b/tests/unit/security/v2/validator-config/common.hpp
@@ -0,0 +1,48 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#ifndef NDN_TESTS_SECURITY_V2_VALIDATOR_CONFIG_COMMON_HPP
+#define NDN_TESTS_SECURITY_V2_VALIDATOR_CONFIG_COMMON_HPP
+
+#include <boost/property_tree/info_parser.hpp>
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace validator_config {
+namespace tests {
+
+inline ConfigSection
+makeSection(const std::string& config)
+{
+  std::istringstream inputStream(config);
+  ConfigSection section;
+  boost::property_tree::read_info(inputStream, section);
+  return section;
+}
+
+} // namespace tests
+} // namespace validator_config
+} // namespace v2
+} // namespace security
+} // namespace ndn
+
+#endif // NDN_TESTS_SECURITY_V2_VALIDATOR_CONFIG_COMMON_HPP
diff --git a/tests/unit/security/v2/validator-config/filter.t.cpp b/tests/unit/security/v2/validator-config/filter.t.cpp
new file mode 100644
index 0000000..fbd1264
--- /dev/null
+++ b/tests/unit/security/v2/validator-config/filter.t.cpp
@@ -0,0 +1,201 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/validator-config/filter.hpp"
+#include "security/command-interest-signer.hpp"
+
+#include "boost-test.hpp"
+#include "common.hpp"
+#include "identity-management-fixture.hpp"
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace validator_config {
+namespace tests {
+
+using namespace ndn::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+BOOST_AUTO_TEST_SUITE(ValidatorConfig)
+
+class FilterFixture : public IdentityManagementFixture
+{
+public:
+  Interest
+  makeSignedInterest(const Name& name)
+  {
+    Interest interest(name);
+    m_keyChain.sign(interest);
+    return interest;
+  }
+};
+
+BOOST_FIXTURE_TEST_SUITE(TestFilter, FilterFixture)
+
+#define CHECK_FOR_MATCHES(filter, same, longer, shorter, different)     \
+  BOOST_CHECK_EQUAL(filter.match(tlv::Interest, makeSignedInterest("/foo/bar").getName()), same); \
+  BOOST_CHECK_EQUAL(filter.match(tlv::Data, Data("/foo/bar").getName()), same);   \
+  BOOST_CHECK_EQUAL(filter.match(tlv::Interest, makeSignedInterest("/foo/bar/bar").getName()), longer); \
+  BOOST_CHECK_EQUAL(filter.match(tlv::Data, Data("/foo/bar/bar").getName()), longer);       \
+  BOOST_CHECK_EQUAL(filter.match(tlv::Interest, makeSignedInterest("/foo").getName()), shorter); \
+  BOOST_CHECK_EQUAL(filter.match(tlv::Data, Data("/foo").getName()), shorter);              \
+  BOOST_CHECK_EQUAL(filter.match(tlv::Interest, makeSignedInterest("/other/prefix").getName()), different); \
+  BOOST_CHECK_EQUAL(filter.match(tlv::Data, Data("/other/prefix").getName()), different);
+
+BOOST_AUTO_TEST_CASE(RelationName)
+{
+  RelationNameFilter f1("/foo/bar", NameRelation::EQUAL);
+  CHECK_FOR_MATCHES(f1, true, false, false, false);
+
+  RelationNameFilter f2("/foo/bar", NameRelation::IS_PREFIX_OF);
+  CHECK_FOR_MATCHES(f2, true, true, false, false);
+
+  RelationNameFilter f3("/foo/bar", NameRelation::IS_STRICT_PREFIX_OF);
+  CHECK_FOR_MATCHES(f3, false, true, false, false);
+}
+
+BOOST_AUTO_TEST_CASE(RegexName)
+{
+  RegexNameFilter f1(Regex("^<foo><bar>$"));
+  CHECK_FOR_MATCHES(f1, true, false, false, false);
+
+  RegexNameFilter f2(Regex("^<foo><bar><>*$"));
+  CHECK_FOR_MATCHES(f2, true, true, false, false);
+
+  RegexNameFilter f3(Regex("^<foo><bar><>+$"));
+  CHECK_FOR_MATCHES(f3, false, true, false, false);
+}
+
+BOOST_FIXTURE_TEST_SUITE(Create, FilterFixture)
+
+BOOST_AUTO_TEST_CASE(Errors)
+{
+  BOOST_CHECK_THROW(Filter::create(makeSection(""), "test-config"), Error);
+  BOOST_CHECK_THROW(Filter::create(makeSection("type unknown"), "test-config"), Error);
+  BOOST_CHECK_THROW(Filter::create(makeSection("type name"), "test-config"), Error);
+
+  std::string config = R"CONF(
+      type name
+      not-name-or-regex stuff
+    )CONF";
+  BOOST_CHECK_THROW(Filter::create(makeSection(config), "test-config"), Error);
+
+  config = R"CONF(
+      type name
+      name /foo/bar
+    )CONF";
+  BOOST_CHECK_THROW(Filter::create(makeSection(config), "test-config"), Error);
+
+  config = R"CONF(
+      type name
+      name /foo/bar
+      not-relation stuff
+    )CONF";
+  BOOST_CHECK_THROW(Filter::create(makeSection(config), "test-config"), Error);
+
+  config = R"CONF(
+      type name
+      name /foo/bar
+      relation equal
+      not-end stuff
+    )CONF";
+  BOOST_CHECK_THROW(Filter::create(makeSection(config), "test-config"), Error);
+
+  config = R"CONF(
+      type name
+      regex ^<foo><bar>$
+      not-end stuff
+    )CONF";
+  BOOST_CHECK_THROW(Filter::create(makeSection(config), "test-config"), Error);
+}
+
+BOOST_AUTO_TEST_CASE(NameFilter)
+{
+  std::string config = R"CONF(
+      type name
+      name /foo/bar
+      relation equal
+    )CONF";
+  auto f1 = Filter::create(makeSection(config), "test-config");
+  CHECK_FOR_MATCHES((*f1), true, false, false, false);
+
+  config = R"CONF(
+      type name
+      name /foo/bar
+      relation is-prefix-of
+    )CONF";
+  auto f2 = Filter::create(makeSection(config), "test-config");
+  CHECK_FOR_MATCHES((*f2), true, true, false, false);
+
+  config = R"CONF(
+      type name
+      name /foo/bar
+      relation is-strict-prefix-of
+    )CONF";
+  auto f3 = Filter::create(makeSection(config), "test-config");
+  CHECK_FOR_MATCHES((*f3), false, true, false, false);
+}
+
+BOOST_AUTO_TEST_CASE(RegexFilter)
+{
+  std::string config = R"CONF(
+      type name
+      regex ^<foo><bar>$
+    )CONF";
+  auto f1 = Filter::create(makeSection(config), "test-config");
+  CHECK_FOR_MATCHES((*f1), true, false, false, false);
+
+  config = R"CONF(
+      type name
+      regex ^<foo><bar><>*$
+    )CONF";
+  auto f2 = Filter::create(makeSection(config), "test-config");
+  CHECK_FOR_MATCHES((*f2), true, true, false, false);
+
+  config = R"CONF(
+      type name
+      regex ^<foo><bar><>+$
+    )CONF";
+  auto f3 = Filter::create(makeSection(config), "test-config");
+  CHECK_FOR_MATCHES((*f3), false, true, false, false);
+
+  config = R"CONF(
+      type name
+      regex ^<>*$
+    )CONF";
+  auto f4 = Filter::create(makeSection(config), "test-config");
+  CHECK_FOR_MATCHES((*f4), true, true, true, true);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // Create
+
+BOOST_AUTO_TEST_SUITE_END() // TestFilter
+BOOST_AUTO_TEST_SUITE_END() // ValidatorConfig
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace validator_config
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/validator-config/name-relation.t.cpp b/tests/unit/security/v2/validator-config/name-relation.t.cpp
new file mode 100644
index 0000000..157b55a
--- /dev/null
+++ b/tests/unit/security/v2/validator-config/name-relation.t.cpp
@@ -0,0 +1,79 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/validator-config/name-relation.hpp"
+
+#include "boost-test.hpp"
+
+#include <boost/lexical_cast.hpp>
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace validator_config {
+namespace tests {
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+BOOST_AUTO_TEST_SUITE(ValidatorConfig)
+BOOST_AUTO_TEST_SUITE(TestNameRelation)
+
+BOOST_AUTO_TEST_CASE(ToString)
+{
+  BOOST_CHECK_EQUAL(boost::lexical_cast<std::string>(NameRelation::EQUAL), "equal");
+  BOOST_CHECK_EQUAL(boost::lexical_cast<std::string>(NameRelation::IS_PREFIX_OF),
+                    "is-prefix-of");
+  BOOST_CHECK_EQUAL(boost::lexical_cast<std::string>(NameRelation::IS_STRICT_PREFIX_OF),
+                    "is-strict-prefix-of");
+}
+
+BOOST_AUTO_TEST_CASE(FromString)
+{
+  BOOST_CHECK_EQUAL(getNameRelationFromString("equal"), NameRelation::EQUAL);
+  BOOST_CHECK_EQUAL(getNameRelationFromString("is-prefix-of"), NameRelation::IS_PREFIX_OF);
+  BOOST_CHECK_EQUAL(getNameRelationFromString("is-strict-prefix-of"), NameRelation::IS_STRICT_PREFIX_OF);
+  BOOST_CHECK_THROW(getNameRelationFromString("unknown"), validator_config::Error);
+}
+
+BOOST_AUTO_TEST_CASE(CheckRelation)
+{
+  BOOST_CHECK(checkNameRelation(NameRelation::EQUAL, "/prefix", "/prefix"));
+  BOOST_CHECK(!checkNameRelation(NameRelation::EQUAL, "/prefix", "/prefix/other"));
+
+  BOOST_CHECK(checkNameRelation(NameRelation::IS_PREFIX_OF, "/prefix", "/prefix"));
+  BOOST_CHECK(checkNameRelation(NameRelation::IS_PREFIX_OF, "/prefix", "/prefix/other"));
+  BOOST_CHECK(!checkNameRelation(NameRelation::IS_PREFIX_OF, "/prefix/other", "/prefix"));
+
+  BOOST_CHECK(!checkNameRelation(NameRelation::IS_STRICT_PREFIX_OF, "/prefix", "/prefix"));
+  BOOST_CHECK(checkNameRelation(NameRelation::IS_STRICT_PREFIX_OF, "/prefix", "/prefix/other"));
+  BOOST_CHECK(!checkNameRelation(NameRelation::IS_STRICT_PREFIX_OF, "/prefix/other", "/prefix"));
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestNameRelation
+BOOST_AUTO_TEST_SUITE_END() // ValidatorConfig
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace validator_config
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/validator-config/rule.t.cpp b/tests/unit/security/v2/validator-config/rule.t.cpp
new file mode 100644
index 0000000..bb78dfe
--- /dev/null
+++ b/tests/unit/security/v2/validator-config/rule.t.cpp
@@ -0,0 +1,209 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/validator-config/rule.hpp"
+
+#include "boost-test.hpp"
+#include "common.hpp"
+#include "identity-management-fixture.hpp"
+#include "../validator-fixture.hpp"
+
+#include <boost/mpl/vector_c.hpp>
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace validator_config {
+namespace tests {
+
+using namespace ndn::tests;
+using namespace ndn::security::v2::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+BOOST_AUTO_TEST_SUITE(ValidatorConfig)
+
+template<uint32_t PktType>
+class RuleFixture : public IdentityManagementFixture
+{
+public:
+  RuleFixture()
+    : rule(ruleId, PktType)
+    , pktName("/foo/bar")
+  {
+    if (PktType == tlv::Interest) {
+      pktName = Name("/foo/bar/SigInfo/SigValue");
+    }
+  }
+
+public:
+  const std::string ruleId = "rule-id";
+  Rule rule;
+  Name pktName;
+};
+
+using PktTypes = boost::mpl::vector_c<uint32_t, tlv::Data, tlv::Interest>;
+
+BOOST_AUTO_TEST_SUITE(TestRule)
+
+BOOST_FIXTURE_TEST_CASE(Errors, RuleFixture<tlv::Data>)
+{
+  BOOST_CHECK_THROW(rule.match(tlv::Interest, this->pktName), Error);
+
+  auto state = make_shared<DummyValidationState>();
+  BOOST_CHECK_THROW(rule.check(tlv::Interest, this->pktName, "/foo/bar", state), Error);
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(Constructor, PktType, PktTypes, RuleFixture<PktType::value>)
+{
+  BOOST_CHECK_EQUAL(this->rule.getId(), this->ruleId);
+  BOOST_CHECK_EQUAL(this->rule.getPktType(), PktType::value);
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(EmptyRule, PktType, PktTypes, RuleFixture<PktType::value>)
+{
+  BOOST_CHECK_EQUAL(this->rule.match(PktType::value, this->pktName), true);
+
+  auto state = make_shared<DummyValidationState>();
+  BOOST_CHECK_EQUAL(this->rule.check(PktType::value, this->pktName, "/foo/bar", state), false);
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(Filters, PktType, PktTypes, RuleFixture<PktType::value>)
+{
+  this->rule.addFilter(make_unique<RegexNameFilter>(Regex("^<foo><bar>$")));
+
+  BOOST_CHECK_EQUAL(this->rule.match(PktType::value, this->pktName), true);
+  BOOST_CHECK_EQUAL(this->rule.match(PktType::value, "/not" + this->pktName.toUri()), false);
+
+  this->rule.addFilter(make_unique<RegexNameFilter>(Regex("^<not><foo><bar>$")));
+
+  BOOST_CHECK_EQUAL(this->rule.match(PktType::value, this->pktName), true);
+  BOOST_CHECK_EQUAL(this->rule.match(PktType::value, "/not" + this->pktName.toUri()), true);
+
+  auto state = make_shared<DummyValidationState>();
+  BOOST_CHECK_EQUAL(this->rule.check(PktType::value, this->pktName, "/foo/bar", state), false);
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(Checkers, PktType, PktTypes, RuleFixture<PktType::value>)
+{
+  this->rule.addChecker(make_unique<HyperRelationChecker>("^(<>+)$", "\\1",
+                                                        "^<not>?(<>+)$", "\\1",
+                                                        NameRelation::EQUAL));
+
+  auto state = make_shared<DummyValidationState>();
+  BOOST_CHECK_EQUAL(this->rule.check(PktType::value, this->pktName, "/foo/bar", state), true);
+
+  state = make_shared<DummyValidationState>();
+  BOOST_CHECK_EQUAL(this->rule.check(PktType::value, this->pktName, "/not/foo/bar", state), true);
+
+  this->rule.addChecker(make_unique<HyperRelationChecker>("^(<>+)$", "\\1",
+                                                        "^(<>+)$", "\\1",
+                                                        NameRelation::EQUAL));
+  state = make_shared<DummyValidationState>();
+  BOOST_CHECK_EQUAL(this->rule.check(PktType::value, this->pktName, "/foo/bar", state), true);
+
+  state = make_shared<DummyValidationState>();
+  BOOST_CHECK_EQUAL(this->rule.check(PktType::value, this->pktName, "/not/foo/bar", state), false);
+}
+
+BOOST_AUTO_TEST_SUITE(Create)
+
+BOOST_AUTO_TEST_CASE(Errors)
+{
+  BOOST_CHECK_THROW(Rule::create(makeSection(""), "test-config"), Error);
+
+  std::string config = R"CONF(
+      id rule-id
+      for something
+    )CONF";
+  BOOST_CHECK_THROW(Rule::create(makeSection(config), "test-config"), Error);
+
+  config = R"CONF(
+      id rule-id
+      for data
+    )CONF";
+  BOOST_CHECK_THROW(Rule::create(makeSection(config), "test-config"), Error); // at least one checker required
+
+  config = R"CONF(
+      id rule-id
+      for data
+      checker
+      {
+        type hierarchical
+        sig-type rsa-sha256
+      }
+      other stuff
+    )CONF";
+  BOOST_CHECK_THROW(Rule::create(makeSection(config), "test-config"), Error);
+}
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(FilterAndChecker, PktType, PktTypes, RuleFixture<PktType::value>)
+{
+  std::string config = R"CONF(
+      id rule-id
+      for )CONF" + (PktType::value == tlv::Data ? "data"s : "interest"s) + R"CONF(
+      filter
+      {
+        type name
+        regex ^<foo><bar>$
+      }
+      checker
+      {
+        type customized
+        sig-type rsa-sha256
+        key-locator
+        {
+          type name
+          hyper-relation
+          {
+            k-regex ^(<>+)$
+            k-expand \\1
+            h-relation equal
+            p-regex ^(<>+)$
+            p-expand \\1
+          }
+        }
+      }
+    )CONF";
+  auto rule = Rule::create(makeSection(config), "test-config");
+
+  BOOST_CHECK_EQUAL(rule->match(PktType::value, this->pktName), true);
+  BOOST_CHECK_EQUAL(rule->match(PktType::value, "/not" + this->pktName.toUri()), false);
+
+  auto state = make_shared<DummyValidationState>();
+  BOOST_CHECK_EQUAL(rule->check(PktType::value, this->pktName, "/foo/bar", state), true);
+
+  state = make_shared<DummyValidationState>();
+  BOOST_CHECK_EQUAL(rule->check(PktType::value, this->pktName, "/not/foo/bar", state), false);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // Create
+
+BOOST_AUTO_TEST_SUITE_END() // TestRule
+BOOST_AUTO_TEST_SUITE_END() // ValidatorConfig
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace validator_config
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/v2/validator-fixture.hpp b/tests/unit/security/v2/validator-fixture.hpp
new file mode 100644
index 0000000..c9a5763
--- /dev/null
+++ b/tests/unit/security/v2/validator-fixture.hpp
@@ -0,0 +1,183 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#ifndef NDN_TESTS_SECURITY_V2_VALIDATOR_FIXTURE_HPP
+#define NDN_TESTS_SECURITY_V2_VALIDATOR_FIXTURE_HPP
+
+#include "security/v2/validator.hpp"
+#include "security/v2/certificate-fetcher-from-network.hpp"
+#include "util/dummy-client-face.hpp"
+
+#include "../../identity-management-time-fixture.hpp"
+
+#include <boost/lexical_cast.hpp>
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+template<class ValidationPolicy, class CertificateFetcher = CertificateFetcherFromNetwork>
+class ValidatorFixture : public ndn::tests::IdentityManagementTimeFixture
+{
+public:
+  ValidatorFixture()
+    : face(io, {true, true})
+    , validator(make_unique<ValidationPolicy>(), make_unique<CertificateFetcher>(face))
+    , policy(static_cast<ValidationPolicy&>(validator.getPolicy()))
+    , cache(100_days)
+  {
+    processInterest = [this] (const Interest& interest) {
+      auto cert = cache.find(interest);
+      if (cert != nullptr) {
+        face.receive(*cert);
+      }
+    };
+  }
+
+  virtual
+  ~ValidatorFixture() = default;
+
+  template<class Packet>
+  void
+  validate(const Packet& packet, const std::string& msg, bool expectSuccess, int line)
+  {
+    std::string detailedInfo = msg + " on line " + to_string(line);
+    size_t nCallbacks = 0;
+    this->validator.validate(packet,
+      [&] (const Packet&) {
+        ++nCallbacks;
+        BOOST_CHECK_MESSAGE(expectSuccess,
+                            (expectSuccess ? "OK: " : "FAILED: ") + detailedInfo);
+      },
+      [&] (const Packet&, const ValidationError& error) {
+        ++nCallbacks;
+        BOOST_CHECK_MESSAGE(!expectSuccess,
+                            (!expectSuccess ? "OK: " : "FAILED: ") + detailedInfo +
+                            (expectSuccess ? " (" + boost::lexical_cast<std::string>(error) + ")" : ""));
+      });
+
+    mockNetworkOperations();
+    BOOST_CHECK_EQUAL(nCallbacks, 1);
+  }
+
+  void
+  mockNetworkOperations()
+  {
+    util::signal::ScopedConnection connection = face.onSendInterest.connect([this] (const Interest& interest) {
+        if (processInterest != nullptr) {
+          io.post(bind(processInterest, interest));
+        }
+      });
+    advanceClocks(time::milliseconds(s_mockPeriod), s_mockTimes);
+  }
+
+  /** \brief undo clock advancement of mockNetworkOperations
+   */
+  void
+  rewindClockAfterValidation()
+  {
+    this->systemClock->advance(time::milliseconds(s_mockPeriod * s_mockTimes * -1));
+  }
+
+public:
+  util::DummyClientFace face;
+  std::function<void(const Interest& interest)> processInterest;
+  Validator validator;
+  ValidationPolicy& policy;
+
+  CertificateCache cache;
+
+private:
+  const static int s_mockPeriod;
+  const static int s_mockTimes;
+};
+
+template<class ValidationPolicy, class CertificateFetcher>
+const int ValidatorFixture<ValidationPolicy, CertificateFetcher>::s_mockPeriod = 250;
+
+template<class ValidationPolicy, class CertificateFetcher>
+const int ValidatorFixture<ValidationPolicy, CertificateFetcher>::s_mockTimes = 200;
+
+template<class ValidationPolicy, class CertificateFetcher = CertificateFetcherFromNetwork>
+class HierarchicalValidatorFixture : public ValidatorFixture<ValidationPolicy, CertificateFetcher>
+{
+public:
+  HierarchicalValidatorFixture()
+  {
+    identity = this->addIdentity("/Security/V2/ValidatorFixture");
+    subIdentity = this->addSubCertificate("/Security/V2/ValidatorFixture/Sub1", identity);
+    subSelfSignedIdentity = this->addIdentity("/Security/V2/ValidatorFixture/Sub1/Sub2");
+    otherIdentity = this->addIdentity("/Security/V2/OtherIdentity");
+
+    this->validator.loadAnchor("", Certificate(identity.getDefaultKey().getDefaultCertificate()));
+
+    this->cache.insert(identity.getDefaultKey().getDefaultCertificate());
+    this->cache.insert(subIdentity.getDefaultKey().getDefaultCertificate());
+    this->cache.insert(subSelfSignedIdentity.getDefaultKey().getDefaultCertificate());
+    this->cache.insert(otherIdentity.getDefaultKey().getDefaultCertificate());
+  }
+
+public:
+  Identity identity;
+  Identity subIdentity;
+  Identity subSelfSignedIdentity;
+  Identity otherIdentity;
+};
+
+#define VALIDATE_SUCCESS(packet, message) this->template validate(packet, message, true, __LINE__)
+#define VALIDATE_FAILURE(packet, message) this->template validate(packet, message, false, __LINE__)
+
+class DummyValidationState : public ValidationState
+{
+public:
+  ~DummyValidationState()
+  {
+    m_outcome = false;
+  }
+
+  void
+  fail(const ValidationError& error) override
+  {
+    // BOOST_TEST_MESSAGE(error);
+    m_outcome = false;
+  }
+
+private:
+  void
+  verifyOriginalPacket(const Certificate& trustedCert) override
+  {
+    // do nothing
+  }
+
+  void
+  bypassValidation() override
+  {
+    // do nothing
+  }
+};
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn
+
+#endif // NDN_TESTS_SECURITY_V2_VALIDATOR_FIXTURE_HPP
diff --git a/tests/unit/security/v2/validator.t.cpp b/tests/unit/security/v2/validator.t.cpp
new file mode 100644
index 0000000..ff33984
--- /dev/null
+++ b/tests/unit/security/v2/validator.t.cpp
@@ -0,0 +1,344 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2018 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/validator.hpp"
+#include "security/v2/validation-policy-simple-hierarchy.hpp"
+
+#include "boost-test.hpp"
+#include "validator-fixture.hpp"
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+using namespace ndn::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+BOOST_FIXTURE_TEST_SUITE(TestValidator, HierarchicalValidatorFixture<ValidationPolicySimpleHierarchy>)
+
+BOOST_AUTO_TEST_CASE(ConstructorSetValidator)
+{
+  auto middlePolicy = make_unique<ValidationPolicySimpleHierarchy>();
+  auto innerPolicy = make_unique<ValidationPolicySimpleHierarchy>();
+
+  validator.getPolicy().setInnerPolicy(std::move(middlePolicy));
+  validator.getPolicy().setInnerPolicy(std::move(innerPolicy));
+
+  BOOST_CHECK(validator.getPolicy().m_validator != nullptr);
+  BOOST_CHECK(validator.getPolicy().getInnerPolicy().m_validator != nullptr);
+  BOOST_CHECK(validator.getPolicy().getInnerPolicy().getInnerPolicy().m_validator != nullptr);
+}
+
+BOOST_AUTO_TEST_CASE(Timeouts)
+{
+  processInterest = nullptr; // no response for all interests
+
+  Data data("/Security/V2/ValidatorFixture/Sub1/Sub2/Data");
+  m_keyChain.sign(data, signingByIdentity(subIdentity));
+
+  VALIDATE_FAILURE(data, "Should fail to retrieve certificate");
+  BOOST_CHECK_GT(face.sentInterests.size(), 1);
+}
+
+BOOST_AUTO_TEST_CASE(NackedInterests)
+{
+  processInterest = [this] (const Interest& interest) {
+    lp::Nack nack(interest);
+    nack.setReason(lp::NackReason::NO_ROUTE);
+    face.receive(nack);
+  };
+
+  Data data("/Security/V2/ValidatorFixture/Sub1/Sub2/Data");
+  m_keyChain.sign(data, signingByIdentity(subIdentity));
+
+  VALIDATE_FAILURE(data, "All interests should get NACKed");
+  // 1 for the first interest, 3 for the retries on nack
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 4);
+}
+
+BOOST_AUTO_TEST_CASE(MalformedCert)
+{
+  Data malformedCert = subIdentity.getDefaultKey().getDefaultCertificate();
+  malformedCert.setContentType(tlv::ContentType_Blob);
+  m_keyChain.sign(malformedCert, signingByIdentity(identity));
+  // wrong content type & missing ValidityPeriod
+  BOOST_REQUIRE_THROW(Certificate(malformedCert.wireEncode()), tlv::Error);
+
+  auto originalProcessInterest = processInterest;
+  processInterest = [this, &originalProcessInterest, &malformedCert] (const Interest& interest) {
+    if (interest.getName().isPrefixOf(malformedCert.getName())) {
+      face.receive(malformedCert);
+    }
+    else {
+      originalProcessInterest(interest);
+    }
+  };
+
+  Data data("/Security/V2/ValidatorFixture/Sub1/Sub2/Data");
+  m_keyChain.sign(data, signingByIdentity(subIdentity));
+
+  VALIDATE_FAILURE(data, "Signed by a malformed certificate");
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 1);
+}
+
+BOOST_AUTO_TEST_CASE(ExpiredCert)
+{
+  Data expiredCert = subIdentity.getDefaultKey().getDefaultCertificate();
+  SignatureInfo info;
+  info.setValidityPeriod(ValidityPeriod(time::system_clock::now() - 2_h,
+                                        time::system_clock::now() - 1_h));
+  m_keyChain.sign(expiredCert, signingByIdentity(identity).setSignatureInfo(info));
+  BOOST_REQUIRE_NO_THROW(Certificate(expiredCert.wireEncode()));
+
+  auto originalProcessInterest = processInterest;
+  processInterest = [this, &originalProcessInterest, &expiredCert] (const Interest& interest) {
+    if (interest.getName().isPrefixOf(expiredCert.getName())) {
+      face.receive(expiredCert);
+    }
+    else {
+      originalProcessInterest(interest);
+    }
+  };
+
+  Data data("/Security/V2/ValidatorFixture/Sub1/Sub2/Data");
+  m_keyChain.sign(data, signingByIdentity(subIdentity));
+
+  VALIDATE_FAILURE(data, "Signed by an expired certificate");
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 1);
+}
+
+BOOST_AUTO_TEST_CASE(ResetAnchors)
+{
+  validator.resetAnchors();
+
+  Data data("/Security/V2/ValidatorFixture/Sub1/Sub2/Data");
+  m_keyChain.sign(data, signingByIdentity(subIdentity));
+  VALIDATE_FAILURE(data, "Should fail, as no anchors configured");
+}
+
+BOOST_AUTO_TEST_CASE(TrustedCertCaching)
+{
+  Data data("/Security/V2/ValidatorFixture/Sub1/Sub2/Data");
+  m_keyChain.sign(data, signingByIdentity(subIdentity));
+
+  VALIDATE_SUCCESS(data, "Should get accepted, as signed by the policy-compliant cert");
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 1);
+  face.sentInterests.clear();
+
+  processInterest = nullptr; // disable data responses from mocked network
+
+  VALIDATE_SUCCESS(data, "Should get accepted, based on the cached trusted cert");
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 0);
+  face.sentInterests.clear();
+
+  advanceClocks(1_h, 2); // expire trusted cache
+
+  VALIDATE_FAILURE(data, "Should try and fail to retrieve certs");
+  BOOST_CHECK_GT(face.sentInterests.size(), 1);
+  face.sentInterests.clear();
+}
+
+BOOST_AUTO_TEST_CASE(ResetVerifiedCertificates)
+{
+  Data data("/Security/V2/ValidatorFixture/Sub1/Sub2/Data");
+  m_keyChain.sign(data, signingByIdentity(subIdentity));
+  VALIDATE_SUCCESS(data, "Should get accepted, as signed by the policy-compliant cert");
+
+  // reset anchors
+  validator.resetAnchors();
+  VALIDATE_SUCCESS(data, "Should get accepted, as signed by the cert in trusted cache");
+
+  // reset trusted cache
+  validator.resetVerifiedCertificates();
+  VALIDATE_FAILURE(data, "Should fail, as no trusted cache or anchors");
+}
+
+BOOST_AUTO_TEST_CASE(UntrustedCertCaching)
+{
+  Data data("/Security/V2/ValidatorFixture/Sub1/Sub2/Data");
+  m_keyChain.sign(data, signingByIdentity(subSelfSignedIdentity));
+
+  VALIDATE_FAILURE(data, "Should fail, as signed by the policy-violating cert");
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 1);
+  face.sentInterests.clear();
+
+  processInterest = nullptr; // disable data responses from mocked network
+
+  VALIDATE_FAILURE(data, "Should fail again, but no network operations expected");
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 0);
+  face.sentInterests.clear();
+
+  advanceClocks(10_min, 2); // expire untrusted cache
+
+  VALIDATE_FAILURE(data, "Should try and fail to retrieve certs");
+  BOOST_CHECK_GT(face.sentInterests.size(), 1);
+  face.sentInterests.clear();
+}
+
+class ValidationPolicySimpleHierarchyForInterestOnly : public ValidationPolicySimpleHierarchy
+{
+public:
+  void
+  checkPolicy(const Data& data, const shared_ptr<ValidationState>& state,
+              const ValidationContinuation& continueValidation) override
+  {
+    continueValidation(nullptr, state);
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(ValidateInterestsButBypassForData,
+                        HierarchicalValidatorFixture<ValidationPolicySimpleHierarchyForInterestOnly>)
+{
+  Interest interest("/Security/V2/ValidatorFixture/Sub1/Sub2/Interest");
+  Data data("/Security/V2/ValidatorFixture/Sub1/Sub2/Interest");
+
+  VALIDATE_FAILURE(interest, "Unsigned");
+  VALIDATE_SUCCESS(data, "Policy requests validation bypassing for all data");
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 0);
+  face.sentInterests.clear();
+
+  interest = Interest("/Security/V2/ValidatorFixture/Sub1/Sub2/Interest");
+  m_keyChain.sign(interest, signingWithSha256());
+  m_keyChain.sign(data, signingWithSha256());
+  VALIDATE_FAILURE(interest, "Required KeyLocator/Name missing (not passed to policy)");
+  VALIDATE_SUCCESS(data, "Policy requests validation bypassing for all data");
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 0);
+  face.sentInterests.clear();
+
+  m_keyChain.sign(interest, signingByIdentity(identity));
+  m_keyChain.sign(data, signingByIdentity(identity));
+  VALIDATE_SUCCESS(interest, "Should get accepted, as signed by the anchor");
+  VALIDATE_SUCCESS(data, "Policy requests validation bypassing for all data");
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 0);
+  face.sentInterests.clear();
+
+  m_keyChain.sign(interest, signingByIdentity(subIdentity));
+  m_keyChain.sign(data, signingByIdentity(subIdentity));
+  VALIDATE_FAILURE(interest, "Should fail, as policy is not allowed to create new trust anchors");
+  VALIDATE_SUCCESS(data, "Policy requests validation bypassing for all data");
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 1);
+  face.sentInterests.clear();
+
+  m_keyChain.sign(interest, signingByIdentity(otherIdentity));
+  m_keyChain.sign(data, signingByIdentity(otherIdentity));
+  VALIDATE_FAILURE(interest, "Should fail, as signed by the policy-violating cert");
+  VALIDATE_SUCCESS(data, "Policy requests validation bypassing for all data");
+  // no network operations expected, as certificate is not validated by the policy
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 0);
+  face.sentInterests.clear();
+
+  advanceClocks(1_h, 2); // expire trusted cache
+
+  m_keyChain.sign(interest, signingByIdentity(subSelfSignedIdentity));
+  m_keyChain.sign(data, signingByIdentity(subSelfSignedIdentity));
+  VALIDATE_FAILURE(interest, "Should fail, as policy is not allowed to create new trust anchors");
+  VALIDATE_SUCCESS(data, "Policy requests validation bypassing for all data");
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 1);
+  face.sentInterests.clear();
+}
+
+BOOST_AUTO_TEST_CASE(InfiniteCertChain)
+{
+  processInterest = [this] (const Interest& interest) {
+    // create another key for the same identity and sign it properly
+    Key parentKey = m_keyChain.createKey(subIdentity);
+    Key requestedKey = subIdentity.getKey(interest.getName());
+
+    Name certificateName = requestedKey.getName();
+    certificateName
+    .append("looper")
+    .appendVersion();
+    v2::Certificate certificate;
+    certificate.setName(certificateName);
+
+    // set metainfo
+    certificate.setContentType(tlv::ContentType_Key);
+    certificate.setFreshnessPeriod(1_h);
+
+    // set content
+    certificate.setContent(requestedKey.getPublicKey().data(), requestedKey.getPublicKey().size());
+
+    // set signature-info
+    SignatureInfo info;
+    info.setValidityPeriod(security::ValidityPeriod(time::system_clock::now() - 10_days,
+                                                    time::system_clock::now() + 10_days));
+
+    m_keyChain.sign(certificate, signingByKey(parentKey).setSignatureInfo(info));
+    face.receive(certificate);
+  };
+
+  Data data("/Security/V2/ValidatorFixture/Sub1/Sub2/Data");
+  m_keyChain.sign(data, signingByIdentity(subIdentity));
+
+  validator.setMaxDepth(40);
+  BOOST_CHECK_EQUAL(validator.getMaxDepth(), 40);
+  VALIDATE_FAILURE(data, "Should fail, as certificate should be looped");
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 40);
+  face.sentInterests.clear();
+
+  advanceClocks(1_h, 5); // expire caches
+
+  validator.setMaxDepth(30);
+  BOOST_CHECK_EQUAL(validator.getMaxDepth(), 30);
+  VALIDATE_FAILURE(data, "Should fail, as certificate chain is infinite");
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 30);
+}
+
+BOOST_AUTO_TEST_CASE(LoopedCertChain)
+{
+  auto s1 = addIdentity("/loop");
+  auto k1 = m_keyChain.createKey(s1, RsaKeyParams(name::Component("key1")));
+  auto k2 = m_keyChain.createKey(s1, RsaKeyParams(name::Component("key2")));
+  auto k3 = m_keyChain.createKey(s1, RsaKeyParams(name::Component("key3")));
+
+  auto makeCert = [this] (Key& key, const Key& signer) {
+    v2::Certificate request = key.getDefaultCertificate();
+    request.setName(Name(key.getName()).append("looper").appendVersion());
+
+    SignatureInfo info;
+    info.setValidityPeriod({time::system_clock::now() - 100_days,
+                            time::system_clock::now() + 100_days});
+    m_keyChain.sign(request, signingByKey(signer).setSignatureInfo(info));
+    m_keyChain.addCertificate(key, request);
+
+    cache.insert(request);
+  };
+
+  makeCert(k1, k2);
+  makeCert(k2, k3);
+  makeCert(k3, k1);
+
+  Data data("/loop/Data");
+  m_keyChain.sign(data, signingByKey(k1));
+  VALIDATE_FAILURE(data, "Should fail, as certificate chain loops");
+  BOOST_CHECK_EQUAL(face.sentInterests.size(), 3);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestValidator
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn