security: various enhancements to Interest and Data validation

 * Avoid decoding SignatureInfo multiple times while validating an Interest
 * Fix handling of signed Interests with malformed InterestSignatureInfo
 * Report a NO_SIGNATURE error when SignatureInfo is missing or malformed
 * Fail with POLICY_ERROR in ValidationPolicySimpleHierarchy when the
   signing identity violates the policy
 * Reduce code duplication
 * Expand unit test coverage

Change-Id: I1c9d532b2307d5df8f4bd75152af57a4e10835aa
diff --git a/tests/unit/security/certificate-fetcher-direct-fetch.t.cpp b/tests/unit/security/certificate-fetcher-direct-fetch.t.cpp
index 183a947..7c9ba90 100644
--- a/tests/unit/security/certificate-fetcher-direct-fetch.t.cpp
+++ b/tests/unit/security/certificate-fetcher-direct-fetch.t.cpp
@@ -21,11 +21,10 @@
 
 #include "ndn-cxx/security/certificate-fetcher-direct-fetch.hpp"
 
-#include "ndn-cxx/lp/nack.hpp"
 #include "ndn-cxx/lp/tags.hpp"
 #include "ndn-cxx/security/validation-policy-simple-hierarchy.hpp"
 
-#include "tests/boost-test.hpp"
+#include "tests/test-common.hpp"
 #include "tests/unit/security/validator-fixture.hpp"
 
 #include <boost/range/adaptor/sliced.hpp>
@@ -133,9 +132,7 @@
 void
 CertificateFetcherDirectFetchFixture<Nack>::makeResponse(const Interest& interest)
 {
-  lp::Nack nack(interest);
-  nack.setHeader(lp::NackHeader().setReason(lp::NackReason::NO_ROUTE));
-  face.receive(nack);
+  face.receive(makeNack(interest, lp::NackReason::NO_ROUTE));
 }
 
 using Failures = boost::mpl::vector<Timeout, Nack>;
@@ -174,9 +171,10 @@
 BOOST_FIXTURE_TEST_CASE_TEMPLATE(ValidateFailureData, T, Failures, CertificateFetcherDirectFetchFixture<T>)
 {
   VALIDATE_FAILURE(this->data, "Should fail, as all interests either NACKed or timeout");
+  BOOST_TEST(this->lastError.getCode() == ValidationError::CANNOT_RETRIEVE_CERT);
   // Direct fetcher sends two interests each time - to network and face
   // 3 retries on nack or timeout (2 * (1 + 3) = 8)
-  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 8);
+  BOOST_TEST(this->face.sentInterests.size() == 8);
 
   // odd interests
   for (const auto& sentInterest : this->face.sentInterests | boost::adaptors::strided(2)) {
@@ -197,9 +195,10 @@
   static_cast<CertificateFetcherDirectFetch&>(this->validator.getFetcher()).setSendDirectInterestOnly(true);
 
   VALIDATE_FAILURE(this->data, "Should fail, as all interests either NACKed or timeout");
+  BOOST_TEST(this->lastError.getCode() == ValidationError::CANNOT_RETRIEVE_CERT);
   // Direct fetcher sends two interests each time - to network and face
   // 3 retries on nack or timeout (1 + 3 = 4)
-  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 4);
+  BOOST_TEST(this->face.sentInterests.size() == 4);
 
   for (const auto& sentInterest : this->face.sentInterests) {
     BOOST_CHECK(sentInterest.template getTag<lp::NextHopFaceIdTag>() != nullptr);
@@ -215,8 +214,8 @@
   this->interest.template removeTag<lp::IncomingFaceIdTag>();
 
   VALIDATE_FAILURE(this->data, "Should fail, as no interests are expected");
-  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 0);
-  BOOST_CHECK_NE(this->lastError.getCode(), ValidationError::IMPLEMENTATION_ERROR);
+  BOOST_TEST(this->lastError.getCode() == ValidationError::CANNOT_RETRIEVE_CERT);
+  BOOST_TEST(this->face.sentInterests.size() == 0);
 }
 
 BOOST_FIXTURE_TEST_CASE(ValidateSuccessInterest, CertificateFetcherDirectFetchFixture<Cert>)
@@ -240,9 +239,10 @@
 BOOST_FIXTURE_TEST_CASE_TEMPLATE(ValidateFailureInterest, T, Failures, CertificateFetcherDirectFetchFixture<T>)
 {
   VALIDATE_FAILURE(this->interest, "Should fail, as all interests either NACKed or timeout");
+  BOOST_TEST(this->lastError.getCode() == ValidationError::CANNOT_RETRIEVE_CERT);
   // 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);
+  BOOST_TEST(this->face.sentInterests.size() == 8);
 
   // odd interests
   for (const auto& sentInterest : this->face.sentInterests | boost::adaptors::strided(2)) {
diff --git a/tests/unit/security/certificate-fetcher-from-network.t.cpp b/tests/unit/security/certificate-fetcher-from-network.t.cpp
index 29d8243..cb3e0b5 100644
--- a/tests/unit/security/certificate-fetcher-from-network.t.cpp
+++ b/tests/unit/security/certificate-fetcher-from-network.t.cpp
@@ -119,15 +119,17 @@
 BOOST_FIXTURE_TEST_CASE_TEMPLATE(ValidateFailure, T, Failures, CertificateFetcherFromNetworkFixture<T>)
 {
   VALIDATE_FAILURE(this->data, "Should fail, as interests don't bring data");
+  BOOST_TEST(this->lastError.getCode() == ValidationError::CANNOT_RETRIEVE_CERT);
   // first interest + 3 retries
-  BOOST_CHECK_EQUAL(this->face.sentInterests.size(), 4);
+  BOOST_TEST(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_TEST(this->lastError.getCode() == ValidationError::CANNOT_RETRIEVE_CERT);
+  BOOST_TEST(this->face.sentInterests.size() == 4);
 }
 
 BOOST_AUTO_TEST_SUITE_END() // TestCertificateFetcherFromNetwork
diff --git a/tests/unit/security/certificate-fetcher-offline.t.cpp b/tests/unit/security/certificate-fetcher-offline.t.cpp
index f674993..f48971f 100644
--- a/tests/unit/security/certificate-fetcher-offline.t.cpp
+++ b/tests/unit/security/certificate-fetcher-offline.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2020 Regents of the University of California.
+ * Copyright (c) 2013-2022 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -54,7 +54,8 @@
   auto packet = Packet::makePacket(name);
   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);
+  BOOST_TEST(this->lastError.getCode() == ValidationError::CANNOT_RETRIEVE_CERT);
+  BOOST_TEST(this->face.sentInterests.size() == 0);
 
   packet = Packet::makePacket(name);
   m_keyChain.sign(packet, signingByIdentity(identity));
diff --git a/tests/unit/security/validation-policy-command-interest.t.cpp b/tests/unit/security/validation-policy-command-interest.t.cpp
index cf91ee3..d6fd74f 100644
--- a/tests/unit/security/validation-policy-command-interest.t.cpp
+++ b/tests/unit/security/validation-policy-command-interest.t.cpp
@@ -28,7 +28,6 @@
 #include "tests/test-common.hpp"
 #include "tests/unit/security/validator-fixture.hpp"
 
-#include <boost/lexical_cast.hpp>
 #include <boost/mpl/vector.hpp>
 
 namespace ndn {
@@ -40,9 +39,8 @@
 
 BOOST_AUTO_TEST_SUITE(Security)
 
-class CommandInterestDefaultOptions
+struct CommandInterestDefaultOptions
 {
-public:
   static ValidationPolicyCommandInterest::Options
   getOptions()
   {
@@ -61,62 +59,82 @@
 };
 
 template<class T, class InnerPolicy = ValidationPolicySimpleHierarchy>
-class ValidationPolicyCommandInterestFixture : public HierarchicalValidatorFixture<CommandInterestPolicyWrapper<T, InnerPolicy>>
+class ValidationPolicyCommandInterestFixture
+  : public HierarchicalValidatorFixture<CommandInterestPolicyWrapper<T, InnerPolicy>>
 {
-public:
+protected:
   Interest
-  makeCommandInterest(const Identity& identity, bool wantV3 = false)
+  makeCommandInterest(const Identity& identity, SignedInterestFormat format = SignedInterestFormat::V02)
   {
-    if (wantV3) {
-      Interest i(Name(identity.getName()).append("CMD"));
-      m_signer.makeSignedInterest(i, signingByIdentity(identity));
-      return i;
+    Name name = identity.getName();
+    name.append("CMD");
+    switch (format) {
+      case SignedInterestFormat::V02:
+        return m_signer.makeCommandInterest(name, signingByIdentity(identity));
+      case SignedInterestFormat::V03: {
+        Interest interest(name);
+        m_signer.makeSignedInterest(interest, signingByIdentity(identity));
+        return interest;
+      }
     }
-    else {
-      return m_signer.makeCommandInterest(Name(identity.getName()).append("CMD"),
-                                          signingByIdentity(identity));
-    }
+    NDN_CXX_UNREACHABLE;
   }
 
-public:
+protected:
   InterestSigner m_signer{this->m_keyChain};
 };
 
 BOOST_FIXTURE_TEST_SUITE(TestValidationPolicyCommandInterest,
                          ValidationPolicyCommandInterestFixture<CommandInterestDefaultOptions>)
 
+template<int secs>
+struct GracePeriodSeconds
+{
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = time::seconds(secs);
+    return options;
+  }
+};
+
 BOOST_AUTO_TEST_SUITE(Accepts)
 
-BOOST_AUTO_TEST_CASE(Basic)
+BOOST_AUTO_TEST_CASE(BasicV02)
 {
-  auto i1 = makeCommandInterest(identity);
+  auto i1 = makeCommandInterest(identity, SignedInterestFormat::V02);
   VALIDATE_SUCCESS(i1, "Should succeed (within grace period)");
   VALIDATE_FAILURE(i1, "Should fail (replay attack)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 
   advanceClocks(5_ms);
-  auto i2 = makeCommandInterest(identity);
+  auto i2 = makeCommandInterest(identity, SignedInterestFormat::V02);
   VALIDATE_SUCCESS(i2, "Should succeed (timestamp larger than previous)");
 
-  auto i3 =  m_signer.makeCommandInterest(Name(identity.getName()).append("CMD"), signingWithSha256());
+  auto i3 = m_signer.makeCommandInterest(Name(identity.getName()).append("CMD"), signingWithSha256());
   VALIDATE_FAILURE(i3, "Should fail (Sha256 signature violates policy)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 }
 
-BOOST_AUTO_TEST_CASE(BasicV3)
+BOOST_AUTO_TEST_CASE(BasicV03)
 {
-  auto i1 = makeCommandInterest(identity, true);
+  auto i1 = makeCommandInterest(identity, SignedInterestFormat::V03);
   VALIDATE_SUCCESS(i1, "Should succeed (within grace period)");
   VALIDATE_FAILURE(i1, "Should fail (replay attack)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 
   advanceClocks(5_ms);
-  auto i2 = makeCommandInterest(identity, true);
+  auto i2 = makeCommandInterest(identity, SignedInterestFormat::V03);
   VALIDATE_SUCCESS(i2, "Should succeed (timestamp larger than previous)");
 
   Interest i3(Name(identity.getName()).append("CMD"));
   m_signer.makeSignedInterest(i3, signingWithSha256());
   VALIDATE_FAILURE(i3, "Should fail (Sha256 signature violates policy)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 }
 
-BOOST_AUTO_TEST_CASE(DataPassthru)
+BOOST_AUTO_TEST_CASE(DataPassthrough)
 {
   Data d1("/Security/ValidatorFixture/Sub1");
   m_keyChain.sign(d1);
@@ -131,6 +149,7 @@
   auto i1 = m_signer.makeCommandInterest("/hello/world/CMD", signingWithSha256());
   VALIDATE_SUCCESS(i1, "Should succeed (within grace period)");
   VALIDATE_FAILURE(i1, "Should fail (replay attack)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 
   advanceClocks(5_ms);
   auto i2 = m_signer.makeCommandInterest("/hello/world/CMD", signingWithSha256());
@@ -141,89 +160,118 @@
 
 BOOST_AUTO_TEST_SUITE(Rejects)
 
-BOOST_AUTO_TEST_CASE(NameTooShort)
+BOOST_AUTO_TEST_CASE(NotSigned)
 {
-  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)");
+  auto i1 = Interest("/short");
+  BOOST_TEST_REQUIRE(i1.getName().size() < signed_interest::MIN_SIZE);
+  VALIDATE_FAILURE(i1, "Should fail (not signed / name too short)");
+  BOOST_TEST(lastError.getCode() == ValidationError::NO_SIGNATURE);
 }
 
 BOOST_AUTO_TEST_CASE(BadSigInfo)
 {
-  auto i1 = makeCommandInterest(identity);
+  auto i1 = makeCommandInterest(identity, SignedInterestFormat::V02);
   setNameComponent(i1, command_interest::POS_SIG_INFO, "not-SignatureInfo");
+  BOOST_TEST_REQUIRE(i1.getName().size() >= command_interest::MIN_SIZE);
   VALIDATE_FAILURE(i1, "Should fail (signature info is missing)");
+  BOOST_TEST(lastError.getCode() == ValidationError::NO_SIGNATURE);
 }
 
-BOOST_AUTO_TEST_CASE(MissingKeyLocator)
+BOOST_AUTO_TEST_CASE(BadTimestampV02)
 {
-  auto i1 = makeCommandInterest(identity);
+  // i1 is a valid signed interest (in v0.2 format) but is not a valid command interest
+  auto i1 = Interest("/short");
+  m_keyChain.sign(i1, signingByIdentity(identity).setSignedInterestFormat(SignedInterestFormat::V02));
+  BOOST_TEST_REQUIRE((i1.getName().size() >= signed_interest::MIN_SIZE &&
+                      i1.getName().size() < command_interest::MIN_SIZE));
+  VALIDATE_FAILURE(i1, "Should fail (timestamp is missing)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
+
+  auto i2 = makeCommandInterest(identity, SignedInterestFormat::V02);
+  setNameComponent(i2, command_interest::POS_TIMESTAMP, "not-timestamp");
+  BOOST_TEST_REQUIRE(i2.getName().size() >= command_interest::MIN_SIZE);
+  VALIDATE_FAILURE(i2, "Should fail (timestamp is malformed)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
+}
+
+BOOST_AUTO_TEST_CASE(BadTimestampV03)
+{
+  auto i1 = makeCommandInterest(identity, SignedInterestFormat::V03);
+  auto si = i1.getSignatureInfo().value();
+  si.setTime(nullopt);
+  i1.setSignatureInfo(si);
+  VALIDATE_FAILURE(i1, "Should fail (timestamp is missing)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
+}
+
+BOOST_AUTO_TEST_CASE(MissingKeyLocatorV02)
+{
+  auto i1 = makeCommandInterest(identity, SignedInterestFormat::V02);
   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_TEST(lastError.getCode() == ValidationError::INVALID_KEY_LOCATOR);
+}
+
+BOOST_AUTO_TEST_CASE(MissingKeyLocatorV03)
+{
+  auto i1 = makeCommandInterest(identity, SignedInterestFormat::V03);
+  auto si = i1.getSignatureInfo().value();
+  si.setKeyLocator(nullopt);
+  i1.setSignatureInfo(si);
+  VALIDATE_FAILURE(i1, "Should fail (missing KeyLocator)");
+  BOOST_TEST(lastError.getCode() == ValidationError::INVALID_KEY_LOCATOR);
 }
 
 BOOST_AUTO_TEST_CASE(BadKeyLocatorType)
 {
-  auto i1 = makeCommandInterest(identity);
+  auto i1 = makeCommandInterest(identity, SignedInterestFormat::V02);
   KeyLocator kl;
   kl.setKeyDigest(makeBinaryBlock(tlv::KeyDigest, {0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD}));
-  SignatureInfo sigInfo(tlv::SignatureSha256WithRsa);
-  sigInfo.setKeyLocator(kl);
+  SignatureInfo sigInfo(tlv::SignatureSha256WithRsa, kl);
   setNameComponent(i1, command_interest::POS_SIG_INFO,
                    sigInfo.wireEncode().begin(), sigInfo.wireEncode().end());
   VALIDATE_FAILURE(i1, "Should fail (bad KeyLocator type)");
+  BOOST_TEST(lastError.getCode() == ValidationError::INVALID_KEY_LOCATOR);
 }
 
 BOOST_AUTO_TEST_CASE(BadCertName)
 {
-  auto i1 = makeCommandInterest(identity);
-  KeyLocator kl;
-  kl.setName("/bad/cert/name");
-  SignatureInfo sigInfo(tlv::SignatureSha256WithRsa);
-  sigInfo.setKeyLocator(kl);
+  auto i1 = makeCommandInterest(identity, SignedInterestFormat::V02);
+  SignatureInfo sigInfo(tlv::SignatureSha256WithEcdsa, KeyLocator("/bad/cert/name"));
   setNameComponent(i1, command_interest::POS_SIG_INFO,
                    sigInfo.wireEncode().begin(), sigInfo.wireEncode().end());
   VALIDATE_FAILURE(i1, "Should fail (bad certificate name)");
+  BOOST_TEST(lastError.getCode() == ValidationError::INVALID_KEY_LOCATOR);
 }
 
 BOOST_AUTO_TEST_CASE(InnerPolicyReject)
 {
-  auto i1 = makeCommandInterest(otherIdentity);
+  auto i1 = makeCommandInterest(otherIdentity, SignedInterestFormat::V02);
   VALIDATE_FAILURE(i1, "Should fail (inner policy should reject)");
+  BOOST_TEST(lastError.getCode() == ValidationError::LOOP_DETECTED);
+
+  auto i2 = makeCommandInterest(otherIdentity, SignedInterestFormat::V03);
+  VALIDATE_FAILURE(i2, "Should fail (inner policy should reject)");
+  BOOST_TEST(lastError.getCode() == ValidationError::LOOP_DETECTED);
 }
 
-class GracePeriod15Sec
-{
-public:
-  static ValidationPolicyCommandInterest::Options
-  getOptions()
-  {
-    ValidationPolicyCommandInterest::Options options;
-    options.gracePeriod = 15_s;
-    return options;
-  }
-};
-
-BOOST_FIXTURE_TEST_CASE(TimestampOutOfGracePositive, ValidationPolicyCommandInterestFixture<GracePeriod15Sec>)
+BOOST_FIXTURE_TEST_CASE(TimestampOutOfGracePositive,
+                        ValidationPolicyCommandInterestFixture<GracePeriodSeconds<15>>)
 {
   auto i1 = makeCommandInterest(identity); // signed at 0s
   advanceClocks(16_s); // verifying at +16s
   VALIDATE_FAILURE(i1, "Should fail (timestamp outside the grace period)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
   rewindClockAfterValidation();
 
   auto i2 = makeCommandInterest(identity); // signed at +16s
   VALIDATE_SUCCESS(i2, "Should succeed");
 }
 
-BOOST_FIXTURE_TEST_CASE(TimestampOutOfGraceNegative, ValidationPolicyCommandInterestFixture<GracePeriod15Sec>)
+BOOST_FIXTURE_TEST_CASE(TimestampOutOfGraceNegative,
+                        ValidationPolicyCommandInterestFixture<GracePeriodSeconds<15>>)
 {
   auto i1 = makeCommandInterest(identity); // signed at 0s
   advanceClocks(1_s);
@@ -233,10 +281,12 @@
 
   m_systemClock->advance(-18_s); // verifying at -16s
   VALIDATE_FAILURE(i1, "Should fail (timestamp outside the grace period)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
   rewindClockAfterValidation();
 
   // CommandInterestValidator should not remember i1's timestamp
   VALIDATE_FAILURE(i2, "Should fail (timestamp outside the grace period)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
   rewindClockAfterValidation();
 
   // CommandInterestValidator should not remember i2's timestamp, and should treat i3 as initial
@@ -250,9 +300,9 @@
   VALIDATE_SUCCESS(i1, "Should succeed");
 
   auto i2 = makeCommandInterest(identity); // signed at 0s
-  setNameComponent(i2, command_interest::POS_TIMESTAMP,
-                   i1.getName()[command_interest::POS_TIMESTAMP]);
+  setNameComponent(i2, command_interest::POS_TIMESTAMP, i1.getName()[command_interest::POS_TIMESTAMP]);
   VALIDATE_FAILURE(i2, "Should fail (timestamp reordered)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 
   advanceClocks(2_s);
   auto i3 = makeCommandInterest(identity); // signed at +2s
@@ -275,11 +325,13 @@
 
   m_systemClock->advance(-1100_ms); // verifying at 0ms
   VALIDATE_FAILURE(i2, "Should fail (timestamp reordered)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
   rewindClockAfterValidation();
 
   // CommandInterestValidator should not remember i2's timestamp
   advanceClocks(200_ms); // verifying at +200ms
   VALIDATE_FAILURE(i3, "Should fail (timestamp reordered)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
   rewindClockAfterValidation();
 
   advanceClocks(1200_ms); // verifying at 1400ms
@@ -290,25 +342,12 @@
 
 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;
-  }
-};
+using NonPositiveGracePeriods = boost::mpl::vector<
+  GracePeriodSeconds<0>,
+  GracePeriodSeconds<-1>
+>;
 
-typedef boost::mpl::vector<
-  GracePeriod<boost::mpl::int_<0>>,
-  GracePeriod<boost::mpl::int_<-1>>
-> GraceNonPositiveValues;
-
-BOOST_FIXTURE_TEST_CASE_TEMPLATE(GraceNonPositive, GracePeriod, GraceNonPositiveValues,
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(GraceNonPositive, GracePeriod, NonPositiveGracePeriods,
                                  ValidationPolicyCommandInterestFixture<GracePeriod>)
 {
   auto i1 = this->makeCommandInterest(this->identity); // signed at 0ms
@@ -323,6 +362,7 @@
 
   this->advanceClocks(1_ms);
   VALIDATE_FAILURE(i2, "Should fail when validating at 1ms");
+  BOOST_TEST(this->lastError.getCode() == ValidationError::POLICY_ERROR);
 }
 
 class LimitedRecordsOptions
@@ -402,11 +442,12 @@
   advanceClocks(1_s);
   for (size_t 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)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 }
 
 class ZeroRecordsOptions
diff --git a/tests/unit/security/validation-policy-signed-interest.t.cpp b/tests/unit/security/validation-policy-signed-interest.t.cpp
index c3d37e6..58f502e 100644
--- a/tests/unit/security/validation-policy-signed-interest.t.cpp
+++ b/tests/unit/security/validation-policy-signed-interest.t.cpp
@@ -28,7 +28,6 @@
 #include "tests/test-common.hpp"
 #include "tests/unit/security/validator-fixture.hpp"
 
-#include <boost/lexical_cast.hpp>
 #include <boost/mpl/vector.hpp>
 
 namespace ndn {
@@ -40,9 +39,8 @@
 
 BOOST_AUTO_TEST_SUITE(Security)
 
-class SignedInterestDefaultOptions
+struct SignedInterestDefaultOptions
 {
-public:
   static ValidationPolicySignedInterest::Options
   getOptions()
   {
@@ -64,12 +62,7 @@
 class ValidationPolicySignedInterestFixture
   : public HierarchicalValidatorFixture<SignedInterestPolicyWrapper<T, InnerPolicy>>
 {
-public:
-  ValidationPolicySignedInterestFixture()
-    : m_signer(this->m_keyChain)
-  {
-  }
-
+protected:
   Interest
   makeSignedInterest(const Identity& identity,
                      uint32_t signingFlags = InterestSigner::WantNonce | InterestSigner::WantTime)
@@ -79,8 +72,8 @@
     return i;
   }
 
-public:
-  InterestSigner m_signer;
+protected:
+  InterestSigner m_signer{this->m_keyChain};
 
   static constexpr uint32_t WantAll = InterestSigner::WantNonce |
                                         InterestSigner::WantTime |
@@ -90,11 +83,12 @@
 BOOST_FIXTURE_TEST_SUITE(TestValidationPolicySignedInterest,
                          ValidationPolicySignedInterestFixture<SignedInterestDefaultOptions>)
 
-BOOST_AUTO_TEST_CASE(BasicV3)
+BOOST_AUTO_TEST_CASE(Basic)
 {
   auto i1 = makeSignedInterest(identity, WantAll);
   VALIDATE_SUCCESS(i1, "Should succeed (within grace period)");
   VALIDATE_FAILURE(i1, "Should fail (replay attack)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 
   advanceClocks(5_ms);
   auto i2 = makeSignedInterest(identity, WantAll);
@@ -103,6 +97,7 @@
   Interest i3(Name(identity.getName()).append("CMD"));
   m_signer.makeSignedInterest(i3, signingWithSha256());
   VALIDATE_FAILURE(i3, "Should fail (Sha256 signature violates policy)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 }
 
 BOOST_AUTO_TEST_CASE(DataPassthrough)
@@ -116,22 +111,23 @@
 {
   auto i1 = makeSignedInterest(otherIdentity);
   VALIDATE_FAILURE(i1, "Should fail (inner policy should reject)");
+  BOOST_TEST(lastError.getCode() == ValidationError::LOOP_DETECTED);
 }
 
-class LimitedRecordsOptions
+template<ssize_t count>
+struct MaxRecordCount
 {
-public:
   static ValidationPolicySignedInterest::Options
   getOptions()
   {
     ValidationPolicySignedInterest::Options options;
     options.timestampGracePeriod = 15_s;
-    options.maxRecordCount = 3;
+    options.maxRecordCount = count;
     return options;
   }
 };
 
-BOOST_FIXTURE_TEST_CASE(LimitedRecords, ValidationPolicySignedInterestFixture<LimitedRecordsOptions>)
+BOOST_FIXTURE_TEST_CASE(LimitedRecords, ValidationPolicySignedInterestFixture<MaxRecordCount<3>>)
 {
   Identity id1 = addSubCertificate("/Security/ValidatorFixture/Sub1", identity);
   cache.insert(id1.getDefaultKey().getDefaultCertificate());
@@ -169,20 +165,7 @@
   VALIDATE_SUCCESS(i01, "Should succeed despite timestamp is reordered, because record has been evicted");
 }
 
-class UnlimitedRecordsOptions
-{
-public:
-  static ValidationPolicySignedInterest::Options
-  getOptions()
-  {
-    ValidationPolicySignedInterest::Options options;
-    options.timestampGracePeriod = 15_s;
-    options.maxRecordCount = -1;
-    return options;
-  }
-};
-
-BOOST_FIXTURE_TEST_CASE(UnlimitedRecords, ValidationPolicySignedInterestFixture<UnlimitedRecordsOptions>)
+BOOST_FIXTURE_TEST_CASE(UnlimitedRecords, ValidationPolicySignedInterestFixture<MaxRecordCount<-1>>)
 {
   std::vector<Identity> identities;
   for (size_t i = 0; i < 20; ++i) {
@@ -195,27 +178,15 @@
   advanceClocks(1_s);
   for (size_t i = 0; i < 20; ++i) {
     auto i2 = makeSignedInterest(identities.at(i)); // signed at +1s
-
     VALIDATE_SUCCESS(i2, "Should succeed");
     rewindClockAfterValidation();
   }
+
   VALIDATE_FAILURE(i1, "Should fail (timestamp reorder)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 }
 
-class ZeroRecordsOptions
-{
-public:
-  static ValidationPolicySignedInterest::Options
-  getOptions()
-  {
-    ValidationPolicySignedInterest::Options options;
-    options.timestampGracePeriod = 15_s;
-    options.maxRecordCount = 0;
-    return options;
-  }
-};
-
-BOOST_FIXTURE_TEST_CASE(ZeroRecords, ValidationPolicySignedInterestFixture<ZeroRecordsOptions>)
+BOOST_FIXTURE_TEST_CASE(ZeroRecords, ValidationPolicySignedInterestFixture<MaxRecordCount<0>>)
 {
   auto i1 = makeSignedInterest(identity); // signed at 0s
   advanceClocks(1_s);
@@ -232,11 +203,11 @@
 {
   auto i1 = makeSignedInterest(identity, InterestSigner::WantSeqNum);
   VALIDATE_FAILURE(i1, "Should fail (timestamp missing)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 }
 
-class DisabledTimestampValidationOptions
+struct DisabledTimestampValidationOptions
 {
-public:
   static ValidationPolicySignedInterest::Options
   getOptions()
   {
@@ -261,30 +232,31 @@
   VALIDATE_SUCCESS(i2, "Should succeed");
 }
 
-class GracePeriod15Sec
+template<int secs>
+struct GracePeriodSeconds
 {
-public:
   static ValidationPolicySignedInterest::Options
   getOptions()
   {
     ValidationPolicySignedInterest::Options options;
-    options.timestampGracePeriod = 15_s;
+    options.timestampGracePeriod = time::seconds(secs);
     return options;
   }
 };
 
-BOOST_FIXTURE_TEST_CASE(TimestampTooOld, ValidationPolicySignedInterestFixture<GracePeriod15Sec>)
+BOOST_FIXTURE_TEST_CASE(TimestampTooOld, ValidationPolicySignedInterestFixture<GracePeriodSeconds<15>>)
 {
   auto i1 = makeSignedInterest(identity); // signed at 0s
   advanceClocks(16_s); // verifying at +16s
   VALIDATE_FAILURE(i1, "Should fail (timestamp outside the grace period)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
   rewindClockAfterValidation();
 
   auto i2 = makeSignedInterest(identity); // signed at +16s
   VALIDATE_SUCCESS(i2, "Should succeed");
 }
 
-BOOST_FIXTURE_TEST_CASE(TimestampTooNew, ValidationPolicySignedInterestFixture<GracePeriod15Sec>)
+BOOST_FIXTURE_TEST_CASE(TimestampTooNew, ValidationPolicySignedInterestFixture<GracePeriodSeconds<15>>)
 {
   auto i1 = makeSignedInterest(identity); // signed at 0s
   advanceClocks(1_s);
@@ -294,10 +266,12 @@
 
   m_systemClock->advance(-18_s); // verifying at -16s
   VALIDATE_FAILURE(i1, "Should fail (timestamp outside the grace period)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
   rewindClockAfterValidation();
 
   // SignedInterestValidator should not remember i1's timestamp
   VALIDATE_FAILURE(i2, "Should fail (timestamp outside the grace period)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
   rewindClockAfterValidation();
 
   // SignedInterestValidator should not remember i2's timestamp, and should treat i3 as initial
@@ -315,6 +289,7 @@
   si2->setTime(i1.getSignatureInfo()->getTime());
   i2.setSignatureInfo(*si2);
   VALIDATE_FAILURE(i2, "Should fail (timestamp reordered)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 
   advanceClocks(2_s);
   auto i3 = makeSignedInterest(identity); // signed at +2s
@@ -337,36 +312,25 @@
 
   m_systemClock->advance(-1100_ms); // verifying at 0ms
   VALIDATE_FAILURE(i2, "Should fail (timestamp reordered)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
   rewindClockAfterValidation();
 
   // SignedInterestValidator should not remember i2's timestamp
   advanceClocks(200_ms); // verifying at +200ms
   VALIDATE_FAILURE(i3, "Should fail (timestamp reordered)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
   rewindClockAfterValidation();
 
   advanceClocks(1200_ms); // verifying at 1400ms
   VALIDATE_SUCCESS(i4, "Should succeed");
 }
 
-template<class T>
-class GracePeriod
-{
-public:
-  static ValidationPolicySignedInterest::Options
-  getOptions()
-  {
-    ValidationPolicySignedInterest::Options options;
-    options.timestampGracePeriod = time::seconds(T::value);
-    return options;
-  }
-};
+using NonPositiveGracePeriods = boost::mpl::vector<
+  GracePeriodSeconds<0>,
+  GracePeriodSeconds<-1>
+>;
 
-typedef boost::mpl::vector<
-  GracePeriod<boost::mpl::int_<0>>,
-  GracePeriod<boost::mpl::int_<-1>>
-> GraceNonPositiveValues;
-
-BOOST_FIXTURE_TEST_CASE_TEMPLATE(GraceNonPositive, GracePeriod, GraceNonPositiveValues,
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(GraceNonPositive, GracePeriod, NonPositiveGracePeriods,
                                  ValidationPolicySignedInterestFixture<GracePeriod>)
 {
   auto i1 = this->makeSignedInterest(this->identity); // signed at 0ms
@@ -383,6 +347,7 @@
 
   this->advanceClocks(1_ms);
   VALIDATE_FAILURE(i2, "Should fail when validating at 1ms");
+  BOOST_TEST(this->lastError.getCode() == ValidationError::POLICY_ERROR);
 }
 
 BOOST_AUTO_TEST_SUITE_END() // TimestampValidation
@@ -403,9 +368,8 @@
   VALIDATE_SUCCESS(i2, "Should succeed");
 }
 
-class SeqNumValidationOptions
+struct SeqNumValidationOptions
 {
-public:
   static ValidationPolicySignedInterest::Options
   getOptions()
   {
@@ -420,6 +384,7 @@
 {
   auto i1 = makeSignedInterest(identity, InterestSigner::WantTime);
   VALIDATE_FAILURE(i1, "Should fail (sequence number missing");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 }
 
 BOOST_FIXTURE_TEST_CASE(SeqNumReorder,
@@ -433,6 +398,7 @@
   si2->setSeqNum(i1.getSignatureInfo()->getSeqNum());
   i2.setSignatureInfo(*si2);
   VALIDATE_FAILURE(i2, "Should fail (sequence number reordered)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 
   auto i3 = makeSignedInterest(identity, WantAll); // seq num is i+2
   VALIDATE_SUCCESS(i3, "Should succeed");
@@ -446,6 +412,7 @@
 {
   auto i1 = makeSignedInterest(identity, InterestSigner::WantTime); // Specifically exclude nonce
   VALIDATE_FAILURE(i1, "Should fail (nonce missing)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 }
 
 BOOST_AUTO_TEST_CASE(DuplicateNonce)
@@ -458,6 +425,7 @@
   si2->setNonce(i1.getSignatureInfo()->getNonce());
   i2.setSignatureInfo(*si2);
   VALIDATE_FAILURE(i2, "Should fail (duplicate nonce)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 
   auto i3 = makeSignedInterest(identity, WantAll);
   // On the off chance that the generated nonce is identical to i1
@@ -467,9 +435,8 @@
   VALIDATE_SUCCESS(i3, "Should succeed");
 }
 
-class DisabledNonceValidationOptions
+struct DisabledNonceValidationOptions
 {
-public:
   static ValidationPolicySignedInterest::Options
   getOptions()
   {
@@ -499,9 +466,8 @@
   VALIDATE_SUCCESS(i3, "Should succeed");
 }
 
-class NonceLimit2Options
+struct NonceLimit2Options
 {
-public:
   static ValidationPolicySignedInterest::Options
   getOptions()
   {
@@ -533,6 +499,7 @@
   m_keyChain.sign(i3, signingByIdentity(identity).setSignedInterestFormat(SignedInterestFormat::V03)
                                                  .setSignatureInfo(*si3));
   VALIDATE_FAILURE(i3, "Should fail (duplicate nonce)");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 
   // Pop i1's nonce off the list
   auto i4 = makeSignedInterest(identity, WantAll);
diff --git a/tests/unit/security/validation-policy-simple-hierarchy.t.cpp b/tests/unit/security/validation-policy-simple-hierarchy.t.cpp
index 70fb49e..38ab30c 100644
--- a/tests/unit/security/validation-policy-simple-hierarchy.t.cpp
+++ b/tests/unit/security/validation-policy-simple-hierarchy.t.cpp
@@ -43,10 +43,13 @@
 
   auto packet = Packet::makePacket(name);
   VALIDATE_FAILURE(packet, "Unsigned");
+  BOOST_TEST((lastError.getCode() == ValidationError::NO_SIGNATURE ||        // Interest
+              lastError.getCode() == ValidationError::INVALID_KEY_LOCATOR)); // Data
 
   packet = Packet::makePacket(name);
   m_keyChain.sign(packet, signingWithSha256());
   VALIDATE_FAILURE(packet, "Should not be accepted, name not prefix of /localhost/identity/digest-sha256");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 
   packet = Packet::makePacket("/localhost/identity/digest-sha256/foobar");
   m_keyChain.sign(packet, signingWithSha256());
@@ -63,15 +66,17 @@
   packet = Packet::makePacket(name);
   m_keyChain.sign(packet, signingByIdentity(otherIdentity));
   VALIDATE_FAILURE(packet, "Should fail, as signed by the policy-violating cert");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
 
   packet = Packet::makePacket(name);
   m_keyChain.sign(packet, signingByIdentity(subSelfSignedIdentity));
   VALIDATE_FAILURE(packet, "Should fail, because subSelfSignedIdentity is not a trust anchor");
+  BOOST_TEST(lastError.getCode() == ValidationError::LOOP_DETECTED);
 
   // TODO add checks with malformed packets
 }
 
-BOOST_AUTO_TEST_CASE(NonKeyNameInsideLocator)
+BOOST_AUTO_TEST_CASE(CertNameInKeyLocator)
 {
 //  auto cert = identity.getDefaultKey().getDefaultCertificate().wireEncode();
 //  std::cerr << "Certificate idCert{\"" << toHex(cert) << "\"_block};" << std::endl;
@@ -107,14 +112,13 @@
       "483046022100BDD3E0EF2385658825EB73E87A02D1A16AA8ACE50840C1B91782836164AACA3B0221008007B3EBA9"
       "B7638BD204766B08AF6E4221CDB88156CC7DA13CD916610D6D3AED"_block};
 
+  BOOST_REQUIRE_EQUAL(packet.getKeyLocator().value().getName(),
+                      "/Security/ValidatorFixture/Sub1/KEY/%D7j1%B0%1E%14%09%2B/parent/%FD%00%00%01I%9DY%8C%A0");
+
   this->cache.insert(idCert);
   this->cache.insert(subIdCert);
   this->validator.loadAnchor("", std::move(idCert));
 
-  BOOST_REQUIRE(packet.getKeyLocator());
-  BOOST_CHECK_EQUAL(packet.getKeyLocator()->getName(),
-                    "/Security/ValidatorFixture/Sub1/KEY/%D7j1%B0%1E%14%09%2B/parent/%FD%00%00%01I%9DY%8C%A0");
-
   VALIDATE_SUCCESS(packet, "Should get accepted, as signed by the policy-compliant cert");
 }
 
diff --git a/tests/unit/security/validation-policy.t.cpp b/tests/unit/security/validation-policy.t.cpp
index e982f72..3d76f62 100644
--- a/tests/unit/security/validation-policy.t.cpp
+++ b/tests/unit/security/validation-policy.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2013-2020 Regents of the University of California.
+ * Copyright (c) 2013-2022 Regents of the University of California.
  *
  * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
  *
@@ -35,17 +35,21 @@
 BOOST_AUTO_TEST_CASE(ExtractIdentityNameFromKeyLocator)
 {
   auto id = m_keyChain.createIdentity("/random/identity");
-
   auto keyName = id.getDefaultKey().getName();
   auto certName = id.getDefaultKey().getDefaultCertificate().getName();
-  auto partialCertName = id.getDefaultKey().getDefaultCertificate().getName().getPrefix(-1);
+  auto partialCertName = certName.getPrefix(-1);
 
   BOOST_CHECK_EQUAL(extractIdentityNameFromKeyLocator(keyName), "/random/identity");
   BOOST_CHECK_EQUAL(extractIdentityNameFromKeyLocator(certName), "/random/identity");
   BOOST_CHECK_EQUAL(extractIdentityNameFromKeyLocator(partialCertName), "/random/identity");
+  BOOST_CHECK_EQUAL(extractIdentityNameFromKeyLocator("/KEY"), "/");
 
-  BOOST_CHECK_THROW(extractIdentityNameFromKeyLocator(Name("/name/without/key/component")), std::runtime_error);
-  BOOST_CHECK_THROW(extractIdentityNameFromKeyLocator(Name("/name/with/KEY/but/in/a/wrong/place")), std::runtime_error);
+  BOOST_CHECK_THROW(extractIdentityNameFromKeyLocator(Name("/short/name")),
+                    KeyLocator::Error);
+  BOOST_CHECK_THROW(extractIdentityNameFromKeyLocator(Name("/name/without/key/component")),
+                    KeyLocator::Error);
+  BOOST_CHECK_THROW(extractIdentityNameFromKeyLocator(Name("/name/with/KEY/but/in/a/wrong/place")),
+                    KeyLocator::Error);
 }
 
 BOOST_AUTO_TEST_SUITE_END() // TestValidationPolicy
diff --git a/tests/unit/security/validator-fixture.cpp b/tests/unit/security/validator-fixture.cpp
index 8048fc4..8579fc8 100644
--- a/tests/unit/security/validator-fixture.cpp
+++ b/tests/unit/security/validator-fixture.cpp
@@ -56,6 +56,12 @@
   advanceClocks(s_mockPeriod, s_mockTimes);
 }
 
+void
+ValidatorFixtureBase::rewindClockAfterValidation()
+{
+  m_systemClock->advance(s_mockPeriod * s_mockTimes * -1);
+}
+
 Identity
 ValidatorFixtureBase::addSubCertificate(const Name& subIdentityName, const Identity& issuer)
 {
diff --git a/tests/unit/security/validator-fixture.hpp b/tests/unit/security/validator-fixture.hpp
index b225c53..d8d357f 100644
--- a/tests/unit/security/validator-fixture.hpp
+++ b/tests/unit/security/validator-fixture.hpp
@@ -44,13 +44,11 @@
   void
   mockNetworkOperations();
 
-  /** \brief undo clock advancement of mockNetworkOperations()
+  /**
+   * @brief Undo clock advancement of mockNetworkOperations()
    */
   void
-  rewindClockAfterValidation()
-  {
-    m_systemClock->advance(s_mockPeriod * s_mockTimes * -1);
-  }
+  rewindClockAfterValidation();
 
   /**
    * @brief Issues a certificate for @p subIdentityName signed by @p issuer
@@ -71,8 +69,8 @@
   ValidationError lastError{ValidationError::NO_ERROR};
 
 private:
-  const static time::milliseconds s_mockPeriod;
-  const static int s_mockTimes;
+  static const time::milliseconds s_mockPeriod;
+  static const int s_mockTimes;
 };
 
 template<class ValidationPolicyT, class CertificateFetcherT = CertificateFetcherFromNetwork>
@@ -93,6 +91,7 @@
     size_t nCallbacks = 0;
     this->validator.validate(packet,
       [&] (const Packet&) {
+        lastError = ValidationError::NO_ERROR;
         ++nCallbacks;
         BOOST_CHECK_MESSAGE(expectSuccess,
                             (expectSuccess ? "OK: " : "FAILED: ") + detailedInfo);
diff --git a/tests/unit/security/validator.t.cpp b/tests/unit/security/validator.t.cpp
index 4acbe4d..842f2f5 100644
--- a/tests/unit/security/validator.t.cpp
+++ b/tests/unit/security/validator.t.cpp
@@ -22,7 +22,7 @@
 #include "ndn-cxx/security/validator.hpp"
 #include "ndn-cxx/security/validation-policy-simple-hierarchy.hpp"
 
-#include "tests/boost-test.hpp"
+#include "tests/test-common.hpp"
 #include "tests/unit/security/validator-fixture.hpp"
 
 namespace ndn {
@@ -46,33 +46,75 @@
   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_CHECK_THROW(validator.getPolicy().setInnerPolicy(nullptr), std::invalid_argument);
 }
 
-BOOST_AUTO_TEST_CASE(Timeouts)
+BOOST_AUTO_TEST_CASE(BadSignatureInfo)
 {
-  processInterest = nullptr; // no response for all interests
+  Interest interest("/Security/ValidatorFixture/Sub1/Sub2/Interest");
+  m_keyChain.sign(interest, signingByIdentity(subIdentity)
+                            .setSignedInterestFormat(SignedInterestFormat::V03));
+
+  // add an unrecognized critical element inside InterestSignatureInfo
+  auto si = interest.getSignatureInfo().value();
+  si.addCustomTlv("7F00"_block);
+  interest.setSignatureInfo(si);
+  BOOST_REQUIRE_THROW(interest.getSignatureInfo(), tlv::Error);
+  BOOST_REQUIRE_NO_THROW(interest.getSignatureValue());
+
+  VALIDATE_FAILURE(interest, "InterestSignatureInfo decoding should fail");
+  BOOST_TEST(lastError.getCode() == ValidationError::NO_SIGNATURE);
+  BOOST_TEST(face.sentInterests.size() == 0);
+}
+
+BOOST_AUTO_TEST_CASE(BadSignatureValue)
+{
+  const uint8_t sv[] = {0x12, 0x34, 0x56, 0x78};
+
+  Interest interest("/Security/ValidatorFixture/Sub1/Sub2/Interest");
+  m_keyChain.sign(interest, signingByIdentity(subIdentity)
+                            .setSignedInterestFormat(SignedInterestFormat::V03));
+  interest.setSignatureValue(sv);
+
+  VALIDATE_FAILURE(interest, "Signature check should fail");
+  BOOST_TEST(lastError.getCode() == ValidationError::INVALID_SIGNATURE);
+  BOOST_TEST(face.sentInterests.size() == 1);
+
+  Data data("/Security/ValidatorFixture/Sub1/Sub2/Data");
+  m_keyChain.sign(data, signingByIdentity(subIdentity));
+  data.setSignatureValue(sv);
+
+  VALIDATE_FAILURE(data, "Signature check should fail");
+  BOOST_TEST(lastError.getCode() == ValidationError::INVALID_SIGNATURE);
+  BOOST_TEST(face.sentInterests.size() == 1);
+}
+
+BOOST_AUTO_TEST_CASE(Timeout)
+{
+  processInterest = nullptr; // no response for any interest
 
   Data data("/Security/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_TEST(lastError.getCode() == ValidationError::CANNOT_RETRIEVE_CERT);
+  BOOST_TEST(face.sentInterests.size() == 4);
 }
 
-BOOST_AUTO_TEST_CASE(NackedInterests)
+BOOST_AUTO_TEST_CASE(Nack)
 {
   processInterest = [this] (const Interest& interest) {
-    lp::Nack nack(interest);
-    nack.setReason(lp::NackReason::NO_ROUTE);
-    face.receive(nack);
+    face.receive(makeNack(interest, lp::NackReason::NO_ROUTE));
   };
 
   Data data("/Security/ValidatorFixture/Sub1/Sub2/Data");
   m_keyChain.sign(data, signingByIdentity(subIdentity));
 
   VALIDATE_FAILURE(data, "All interests should get NACKed");
+  BOOST_TEST(lastError.getCode() == ValidationError::CANNOT_RETRIEVE_CERT);
   // 1 for the first interest, 3 for the retries on nack
-  BOOST_CHECK_EQUAL(face.sentInterests.size(), 4);
+  BOOST_TEST(face.sentInterests.size() == 4);
 }
 
 BOOST_AUTO_TEST_CASE(MalformedCert)
@@ -84,7 +126,7 @@
   BOOST_REQUIRE_THROW(Certificate(malformedCert.wireEncode()), tlv::Error);
 
   auto originalProcessInterest = processInterest;
-  processInterest = [this, &originalProcessInterest, &malformedCert] (const Interest& interest) {
+  processInterest = [&] (const Interest& interest) {
     if (interest.getName().isPrefixOf(malformedCert.getName())) {
       face.receive(malformedCert);
     }
@@ -97,20 +139,20 @@
   m_keyChain.sign(data, signingByIdentity(subIdentity));
 
   VALIDATE_FAILURE(data, "Signed by a malformed certificate");
-  BOOST_CHECK_EQUAL(face.sentInterests.size(), 1);
+  BOOST_TEST(lastError.getCode() == ValidationError::MALFORMED_CERT);
+  BOOST_TEST(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));
+  info.setValidityPeriod(ValidityPeriod::makeRelative(-2_h, -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) {
+  processInterest = [&] (const Interest& interest) {
     if (interest.getName().isPrefixOf(expiredCert.getName())) {
       face.receive(expiredCert);
     }
@@ -123,7 +165,8 @@
   m_keyChain.sign(data, signingByIdentity(subIdentity));
 
   VALIDATE_FAILURE(data, "Signed by an expired certificate");
-  BOOST_CHECK_EQUAL(face.sentInterests.size(), 1);
+  BOOST_TEST(lastError.getCode() == ValidationError::EXPIRED_CERT);
+  BOOST_TEST(face.sentInterests.size() == 1);
 }
 
 BOOST_AUTO_TEST_CASE(ResetAnchors)
@@ -133,6 +176,7 @@
   Data data("/Security/ValidatorFixture/Sub1/Sub2/Data");
   m_keyChain.sign(data, signingByIdentity(subIdentity));
   VALIDATE_FAILURE(data, "Should fail, as no anchors configured");
+  BOOST_TEST(lastError.getCode() == ValidationError::LOOP_DETECTED);
 }
 
 BOOST_AUTO_TEST_CASE(TrustedCertCaching)
@@ -141,23 +185,23 @@
   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);
+  BOOST_TEST(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);
+  BOOST_TEST(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_TEST(lastError.getCode() == ValidationError::CANNOT_RETRIEVE_CERT);
+  BOOST_TEST(face.sentInterests.size() > 1);
 }
 
-BOOST_AUTO_TEST_CASE(ResetVerifiedCertificates)
+BOOST_AUTO_TEST_CASE(ResetVerifiedCerts)
 {
   Data data("/Security/ValidatorFixture/Sub1/Sub2/Data");
   m_keyChain.sign(data, signingByIdentity(subIdentity));
@@ -170,6 +214,7 @@
   // reset trusted cache
   validator.resetVerifiedCertificates();
   VALIDATE_FAILURE(data, "Should fail, as no trusted cache or anchors");
+  BOOST_TEST(lastError.getCode() == ValidationError::LOOP_DETECTED);
 }
 
 BOOST_AUTO_TEST_CASE(UntrustedCertCaching)
@@ -178,20 +223,22 @@
   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);
+  BOOST_TEST(lastError.getCode() == ValidationError::LOOP_DETECTED);
+  BOOST_TEST(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);
+  BOOST_TEST(lastError.getCode() == ValidationError::LOOP_DETECTED);
+  BOOST_TEST(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();
+  BOOST_TEST(lastError.getCode() == ValidationError::CANNOT_RETRIEVE_CERT);
+  BOOST_TEST(face.sentInterests.size() > 1);
 }
 
 class ValidationPolicySimpleHierarchyForInterestOnly : public ValidationPolicySimpleHierarchy
@@ -209,41 +256,45 @@
                         HierarchicalValidatorFixture<ValidationPolicySimpleHierarchyForInterestOnly>)
 {
   Interest interest("/Security/ValidatorFixture/Sub1/Sub2/Interest");
-  Data data("/Security/ValidatorFixture/Sub1/Sub2/Interest");
+  Data data("/Security/ValidatorFixture/Sub1/Sub2/Data");
 
   VALIDATE_FAILURE(interest, "Unsigned");
-  VALIDATE_SUCCESS(data, "Policy requests validation bypassing for all data");
-  BOOST_CHECK_EQUAL(face.sentInterests.size(), 0);
+  BOOST_TEST(lastError.getCode() == ValidationError::NO_SIGNATURE);
+  VALIDATE_SUCCESS(data, "Policy bypasses validation for all data");
+  BOOST_TEST(face.sentInterests.size() == 0);
   face.sentInterests.clear();
 
   interest = Interest("/Security/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);
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
+  VALIDATE_SUCCESS(data, "Policy bypasses validation for all data");
+  BOOST_TEST(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);
+  VALIDATE_SUCCESS(data, "Policy bypasses validation for all data");
+  BOOST_TEST(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);
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
+  VALIDATE_SUCCESS(data, "Policy bypasses validation for all data");
+  BOOST_TEST(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");
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
+  VALIDATE_SUCCESS(data, "Policy bypasses validation for all data");
   // no network operations expected, as certificate is not validated by the policy
-  BOOST_CHECK_EQUAL(face.sentInterests.size(), 0);
+  BOOST_TEST(face.sentInterests.size() == 0);
   face.sentInterests.clear();
 
   advanceClocks(1_h, 2); // expire trusted cache
@@ -251,8 +302,9 @@
   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);
+  BOOST_TEST(lastError.getCode() == ValidationError::POLICY_ERROR);
+  VALIDATE_SUCCESS(data, "Policy bypasses validation for all data");
+  BOOST_TEST(face.sentInterests.size() == 1);
   face.sentInterests.clear();
 }
 
@@ -272,13 +324,15 @@
   };
 
   Data data("/Security/ValidatorFixture/Sub1/Sub2/Data");
-  m_keyChain.sign(data, signingByIdentity(subIdentity).setSignatureInfo(
-                        SignatureInfo().setKeyLocator(subIdentity.getDefaultKey().getName())));
+  m_keyChain.sign(data, signingByIdentity(subIdentity)
+                        .setSignatureInfo(SignatureInfo()
+                                          .setKeyLocator(subIdentity.getDefaultKey().getName())));
 
   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);
+  BOOST_TEST(lastError.getCode() == ValidationError::EXCEEDED_DEPTH_LIMIT);
+  BOOST_TEST(face.sentInterests.size() == 40);
   face.sentInterests.clear();
 
   advanceClocks(1_h, 5); // expire caches
@@ -286,7 +340,8 @@
   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_TEST(lastError.getCode() == ValidationError::EXCEEDED_DEPTH_LIMIT);
+  BOOST_TEST(face.sentInterests.size() == 30);
 }
 
 BOOST_AUTO_TEST_CASE(LoopedCertChain)
@@ -313,7 +368,8 @@
   Data data("/loop/Data");
   m_keyChain.sign(data, signingByKey(k1));
   VALIDATE_FAILURE(data, "Should fail, as certificate chain loops");
-  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 3);
+  BOOST_TEST(lastError.getCode() == ValidationError::LOOP_DETECTED);
+  BOOST_TEST_REQUIRE(face.sentInterests.size() == 3);
   BOOST_CHECK_EQUAL(face.sentInterests[0].getName(), k1.getDefaultCertificate().getName());
   BOOST_CHECK_EQUAL(face.sentInterests[1].getName(), k2.getName());
   BOOST_CHECK_EQUAL(face.sentInterests[2].getName(), k3.getName());