security: add validator for signed Interests

refs #4804

Change-Id: Ie6b64e9758591b54639c67e565abb3fdf40fb652
diff --git a/ndn-cxx/security/interest-signer.cpp b/ndn-cxx/security/interest-signer.cpp
index 479800e..36c131e 100644
--- a/ndn-cxx/security/interest-signer.cpp
+++ b/ndn-cxx/security/interest-signer.cpp
@@ -27,18 +27,32 @@
 
 InterestSigner::InterestSigner(KeyChain& keyChain)
   : m_keyChain(keyChain)
+  , m_lastUsedSeqNum(-1) // Will wrap around to 0 on next Interest
 {
 }
 
 void
-InterestSigner::makeSignedInterest(Interest& interest, SigningInfo params)
+InterestSigner::makeSignedInterest(Interest& interest, SigningInfo params, uint32_t signingFlags)
 {
   SignatureInfo info = params.getSignatureInfo();
-  info.setTime(getFreshTimestamp());
 
-  std::vector<uint8_t> nonce(8);
-  random::generateSecureBytes(nonce.data(), nonce.size());
-  info.setNonce(nonce);
+  if ((signingFlags & (WantNonce | WantTime | WantSeqNum)) == 0) {
+    NDN_THROW(std::invalid_argument("No signature elements specified"));
+  }
+
+  if (signingFlags & WantNonce) {
+    std::vector<uint8_t> nonce(8);
+    random::generateSecureBytes(nonce.data(), nonce.size());
+    info.setNonce(nonce);
+  }
+
+  if (signingFlags & WantTime) {
+    info.setTime(getFreshTimestamp());
+  }
+
+  if (signingFlags & WantSeqNum) {
+    info.setSeqNum(++m_lastUsedSeqNum);
+  }
 
   params.setSignatureInfo(info);
   params.setSignedInterestFormat(SignedInterestFormat::V03);
diff --git a/ndn-cxx/security/interest-signer.hpp b/ndn-cxx/security/interest-signer.hpp
index f04dc83..d7a120d 100644
--- a/ndn-cxx/security/interest-signer.hpp
+++ b/ndn-cxx/security/interest-signer.hpp
@@ -30,21 +30,39 @@
 /**
  * @brief Helper class to create signed Interests
  *
- * The signer generates timestamp and nonce elements for an Interest and signs it with the KeyChain.
+ * The signer generates signature elements for an Interest and signs it with the KeyChain.
  */
 class InterestSigner
 {
 public:
+  /**
+   * @brief Flags to indicate which elements to include in Interest signatures created with
+   *        makeSignedInterest.
+   * @sa https://named-data.net/doc/NDN-packet-spec/0.3/signature.html#interest-signature
+   */
+  enum SigningFlags : uint32_t {
+    WantNonce = 1 << 0,
+    WantTime = 1 << 1,
+    WantSeqNum = 1 << 2,
+  };
+
+public:
   explicit
   InterestSigner(KeyChain& keyChain);
 
   /**
-   * @brief Signs a signed Interest (following Packet Specification v0.3 or newer)
+   * @brief Signs an Interest (following Packet Specification v0.3 or newer)
+   * @param interest Interest to sign
+   * @param params SigningInfo that provides parameters on how to sign the Interest.
+   * @param signingFlags Indicates which elements to include in the signature. At least one element
+   *                     must be specified for inclusion.
+   * @throw std::invalid_argument No signature elements were specified for inclusion.
    *
-   * This generates a nonce and timestamp for the signed Interest.
    */
   void
-  makeSignedInterest(Interest& interest, SigningInfo params = SigningInfo());
+  makeSignedInterest(Interest& interest,
+                     SigningInfo params = SigningInfo(),
+                     uint32_t signingFlags = WantNonce | WantTime);
 
   /**
    * @brief Creates and signs a command Interest
@@ -66,6 +84,7 @@
 private:
   KeyChain& m_keyChain;
   time::system_clock::TimePoint m_lastUsedTimestamp;
+  uint64_t m_lastUsedSeqNum;
 };
 
 } // namespace security
diff --git a/ndn-cxx/security/validation-policy-signed-interest.cpp b/ndn-cxx/security/validation-policy-signed-interest.cpp
new file mode 100644
index 0000000..a439ea7
--- /dev/null
+++ b/ndn-cxx/security/validation-policy-signed-interest.cpp
@@ -0,0 +1,189 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2020 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 "ndn-cxx/security/validation-policy-signed-interest.hpp"
+
+namespace ndn {
+namespace security {
+inline namespace v2 {
+
+ValidationPolicySignedInterest::ValidationPolicySignedInterest(unique_ptr<ValidationPolicy> inner,
+                                                               const Options& options)
+  : m_options(options)
+  , m_byKeyName(m_container.get<0>())
+  , m_byLastRefreshed(m_container.get<1>())
+{
+  if (inner == nullptr) {
+    NDN_THROW(std::invalid_argument("Inner policy is missing"));
+  }
+  setInnerPolicy(std::move(inner));
+
+  m_options.timestampGracePeriod = std::max(m_options.timestampGracePeriod, 0_ns);
+}
+
+void
+ValidationPolicySignedInterest::checkPolicy(const Data& data,
+                                            const shared_ptr<ValidationState>& state,
+                                            const ValidationContinuation& continueValidation)
+{
+  getInnerPolicy().checkPolicy(data, state, continueValidation);
+}
+
+void
+ValidationPolicySignedInterest::checkPolicy(const Interest& interest,
+                                            const shared_ptr<ValidationState>& state,
+                                            const ValidationContinuation& continueValidation)
+{
+  auto fmt = state->getTag<SignedInterestFormatTag>();
+  BOOST_ASSERT(fmt);
+
+  if (!state->getOutcome()) {
+    return;
+  }
+
+  if (*fmt == SignedInterestFormat::V03 && !checkIncomingInterest(state, interest)) {
+    return;
+  }
+
+  getInnerPolicy().checkPolicy(interest, state, std::bind(continueValidation, _1, _2));
+}
+
+bool
+ValidationPolicySignedInterest::checkIncomingInterest(const shared_ptr<ValidationState>& state,
+                                                      const Interest& interest)
+{
+  // Extract information from Interest
+  BOOST_ASSERT(interest.getSignatureInfo());
+  Name keyName = getKeyLocatorName(interest, *state);
+  auto timestamp = interest.getSignatureInfo()->getTime();
+  auto seqNum = interest.getSignatureInfo()->getSeqNum();
+  auto nonce = interest.getSignatureInfo()->getNonce();
+
+  auto record = m_byKeyName.find(keyName);
+
+  if (m_options.shouldValidateTimestamps) {
+    if (!timestamp.has_value()) {
+      state->fail({ValidationError::POLICY_ERROR,
+                   "Timestamp is required by policy but is not present"});
+      return false;
+    }
+
+    auto now = time::system_clock::now();
+    if (time::abs(now - *timestamp) > m_options.timestampGracePeriod) {
+      state->fail({ValidationError::POLICY_ERROR,
+                   "Timestamp is outside the grace period for key " + keyName.toUri()});
+      return false;
+    }
+
+    if (record != m_byKeyName.end() && record->timestamp.has_value() && timestamp <= record->timestamp) {
+      state->fail({ValidationError::POLICY_ERROR,
+                   "Timestamp is reordered for key " + keyName.toUri()});
+      return false;
+    }
+  }
+
+  if (m_options.shouldValidateSeqNums) {
+    if (!seqNum.has_value()) {
+      state->fail({ValidationError::POLICY_ERROR,
+                   "Sequence number is required by policy but is not present"});
+      return false;
+    }
+
+    if (record != m_byKeyName.end() && record->seqNum.has_value() && seqNum <= record->seqNum) {
+      state->fail({ValidationError::POLICY_ERROR,
+                   "Sequence number is reordered for key " + keyName.toUri()});
+      return false;
+    }
+  }
+
+  if (m_options.shouldValidateNonces) {
+    if (!nonce.has_value()) {
+      state->fail({ValidationError::POLICY_ERROR, "Nonce is required by policy but is not present"});
+      return false;
+    }
+
+    if (record != m_byKeyName.end() && record->observedNonces.get<NonceSet>().count(*nonce) > 0) {
+      state->fail({ValidationError::POLICY_ERROR,
+                   "Nonce matches previously-seen nonce for key " + keyName.toUri()});
+      return false;
+    }
+  }
+
+  if (m_options.maxRecordCount != 0) {
+    auto interestState = dynamic_pointer_cast<InterestValidationState>(state);
+    BOOST_ASSERT(interestState != nullptr);
+    interestState->afterSuccess.connect([=] (const Interest&) {
+      insertRecord(keyName, timestamp, seqNum, nonce);
+    });
+  }
+  return true;
+}
+
+void
+ValidationPolicySignedInterest::insertRecord(const Name& keyName,
+                                             optional<time::system_clock::TimePoint> timestamp,
+                                             optional<uint64_t> seqNum,
+                                             optional<SigNonce> nonce)
+{
+  // If key record exists, update last refreshed time. Otherwise, create new record.
+  Container::nth_index<0>::type::iterator it;
+  bool isOk;
+  std::tie(it, isOk) = m_byKeyName.emplace(keyName, timestamp, seqNum);
+  if (!isOk) {
+    // There was already a record for this key, we just need to update it
+    isOk = m_byKeyName.modify(it, [&] (LastInterestRecord& record) {
+      record.lastRefreshed = time::steady_clock::now();
+      if (timestamp.has_value()) {
+        record.timestamp = timestamp;
+      }
+      if (seqNum.has_value()) {
+        record.seqNum = seqNum;
+      }
+    });
+    BOOST_VERIFY(isOk);
+  }
+
+  // If has nonce and max nonce list size > 0 (or unlimited), append to observed nonce list
+  if (m_options.shouldValidateNonces && m_options.maxNonceRecordCount != 0 && nonce.has_value()) {
+    isOk = m_byKeyName.modify(it, [this, &nonce] (LastInterestRecord& record) {
+      auto& sigNonceList = record.observedNonces.get<NonceList>();
+      sigNonceList.push_back(*nonce);
+      // Ensure observed nonce list is at or below max nonce list size
+      if (m_options.maxNonceRecordCount >= 0 &&
+          sigNonceList.size() > static_cast<size_t>(m_options.maxNonceRecordCount)) {
+        BOOST_ASSERT(sigNonceList.size() == static_cast<size_t>(m_options.maxNonceRecordCount) + 1);
+        sigNonceList.pop_front();
+      }
+    });
+    BOOST_VERIFY(isOk);
+  }
+
+  // Ensure record count is at or below max
+  if (m_options.maxRecordCount >= 0 &&
+      m_byLastRefreshed.size() > static_cast<size_t>(m_options.maxRecordCount)) {
+    BOOST_ASSERT(m_byLastRefreshed.size() == static_cast<size_t>(m_options.maxRecordCount) + 1);
+    m_byLastRefreshed.erase(m_byLastRefreshed.begin());
+  }
+}
+
+} // inline namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/ndn-cxx/security/validation-policy-signed-interest.hpp b/ndn-cxx/security/validation-policy-signed-interest.hpp
new file mode 100644
index 0000000..84553fc
--- /dev/null
+++ b/ndn-cxx/security/validation-policy-signed-interest.hpp
@@ -0,0 +1,219 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2020 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_SECURITY_VALIDATION_POLICY_SIGNED_INTEREST_HPP
+#define NDN_SECURITY_VALIDATION_POLICY_SIGNED_INTEREST_HPP
+
+#include "ndn-cxx/security/validation-policy.hpp"
+
+#include <boost/multi_index_container.hpp>
+#include <boost/multi_index/hashed_index.hpp>
+#include <boost/multi_index/key_extractors.hpp>
+#include <boost/multi_index/ordered_index.hpp>
+#include <boost/multi_index/sequenced_index.hpp>
+
+namespace ndn {
+namespace security {
+inline namespace v2 {
+
+/** \brief Validation policy for signed Interests
+ *
+ *  This policy checks the timestamp, sequence number, and nonce fields of signed Interests.
+ */
+class ValidationPolicySignedInterest : public ValidationPolicy
+{
+private:
+  using SigNonce = std::vector<uint8_t>;
+  struct NonceSet {};
+  struct NonceList {};
+
+public:
+  class Options
+  {
+  public:
+    Options()
+    {
+    }
+
+  public:
+    /** \brief Whether to validate timestamps in signed Interests by ensuring they are not
+     *         reordered for a given public key and are within a given grace period of the current
+     *         time
+     *
+     *  If set to false, timestamps checks will be skipped when validating signed Interests.
+     *
+     *  The grace period is controlled by #timestampGracePeriod.
+     */
+    bool shouldValidateTimestamps = true;
+
+    /** \brief Tolerance of timestamp differences from the current time
+     *
+     *  A signed Interest is considered "initial" if the validator does not currently store a
+     *  record for its associated public key -- entries may be erased due to age or storage
+     *  limitations. For such "initial" signed Interests, their timestamp will be compared with the
+     *  current system clock, and a signed Interest will be rejected if the absolute difference of
+     *  its timestamp from the clock time is greater than this grace interval.
+     *
+     *  This value should be positive. Setting this option to 0 or to a negative value causes the
+     *  validator to require exactly the same timestamp as the system clock, which will most likely
+     *  reject all signed Interests due to network delay and clock skew. Therefore, it is not
+     *  recommended to set this value to zero or less or to a very small interval.
+     */
+    time::nanoseconds timestampGracePeriod = 2_min;
+
+    /** \brief Whether to validate sequence numbers in signed Interests by ensuring they are present
+     *         and are strictly increasing for a given public key
+     *
+     *  If set to false, sequence numbers checks will be skipped when validating signed Interests.
+     */
+    bool shouldValidateSeqNums = false;
+
+    /** \brief Whether to validate nonces by ensuring that they are present and do not overlap with
+     *         one of the last n nonces for a given public key
+     *
+     *  If set to false, nonce checks will be skipped when validating signed Interests.
+     *
+     *  The number of previous nonces to check for uniqueness against is controlled by
+     *  #maxNonceRecordCount.
+     */
+    bool shouldValidateNonces = true;
+
+    /** \brief Number of previous nonces to track for each public key
+     *
+     *  If nonce checks are enabled, incoming Interests will be dropped if their nonce matches one
+     *  of the last n nonces for their associated public key, where n is the value of this option.
+     *
+     *  Setting this option to -1 allows an unlimited number of nonces to be tracked for each
+     *  public key.
+     *  Setting this option to 0 will cause last-n nonce matching to not be performed. However, if
+     *  #shouldValidateNonces is set to true, signed Interests will still fail validation if they do
+     *  not contain a nonce.
+     */
+    ssize_t maxNonceRecordCount = 1000;
+
+    /** \brief Max number of distinct public keys to track
+     *
+     *  The validator records a "last" timestamp and sequence number, along with the last n nonces,
+     *  for every public key. For subsequent signed Interest associated with the same public key,
+     *  depending upon the other validator options, their timestamps, sequence numbers, and/or
+     *  nonces will be compared to the last timestamp, last sequence number, and last n nonces
+     *  observed from signed Interests using that public key. Depending upon the enabled checks, a
+     *  signed Interest will be rejected if its timestamp or sequence number is less than or equal
+     *  to the respective last value for the associated public key, or if its nonce matches of the
+     *  last n nonces observed for the associated public key.
+     *
+     *  This option limits the number of distinct public keys that can be tracked. If this limit is
+     *  exceeded, the records will be deleted until the number of records is less than or
+     *  equal to this limit in LRU order (by the time the record was last refreshed).
+     *
+     *  Setting this option to -1 allows an unlimited number of public keys to be tracked.
+     *  Setting this option to 0 disables last timestamp, sequence number, and nonce records and
+     *  will cause every signed Interest to be treated as being associated with a
+     *  previously-unobserved public key -- this is not recommended for obvious security reasons.
+     */
+    ssize_t maxRecordCount = 1000;
+  };
+
+  /** \brief Constructor
+   *  \param inner Validator for signed Interest and Data validation. This must not be nullptr.
+   *  \param options Signed Interest validation options
+   *  \throw std::invalid_argument Inner policy is nullptr
+   */
+  explicit
+  ValidationPolicySignedInterest(unique_ptr<ValidationPolicy> inner, const Options& options = {});
+
+protected:
+  void
+  checkPolicy(const Data& data, const shared_ptr<ValidationState>& state,
+              const ValidationContinuation& continueValidation) override;
+
+  void
+  checkPolicy(const Interest& interest, const shared_ptr<ValidationState>& state,
+              const ValidationContinuation& continueValidation) override;
+
+private:
+  bool
+  checkIncomingInterest(const shared_ptr<ValidationState>& state, const Interest& interest);
+
+  void
+  insertRecord(const Name& keyName,
+               optional<time::system_clock::TimePoint> timestamp,
+               optional<uint64_t> seqNum,
+               optional<SigNonce> nonce);
+
+private:
+  Options m_options;
+
+  using NonceContainer = boost::multi_index_container<
+    SigNonce,
+    boost::multi_index::indexed_by<
+      boost::multi_index::hashed_unique<
+        boost::multi_index::tag<NonceSet>,
+        boost::multi_index::identity<SigNonce>
+      >,
+      boost::multi_index::sequenced<
+        boost::multi_index::tag<NonceList>
+      >
+    >
+  >;
+
+  struct LastInterestRecord
+  {
+    LastInterestRecord(const Name& keyName,
+                       optional<time::system_clock::TimePoint> timestamp,
+                       optional<uint64_t> seqNum)
+      : keyName(keyName)
+      , timestamp(timestamp)
+      , seqNum(seqNum)
+      , lastRefreshed(time::steady_clock::now())
+    {
+    }
+
+    Name keyName;
+    optional<time::system_clock::TimePoint> timestamp;
+    optional<uint64_t> seqNum;
+    NonceContainer observedNonces;
+    time::steady_clock::TimePoint lastRefreshed;
+  };
+
+  using Container = boost::multi_index_container<
+    LastInterestRecord,
+    boost::multi_index::indexed_by<
+      boost::multi_index::ordered_unique<
+        boost::multi_index::member<LastInterestRecord, Name, &LastInterestRecord::keyName>
+      >,
+      boost::multi_index::ordered_non_unique<
+        boost::multi_index::member<LastInterestRecord, time::steady_clock::TimePoint,
+                                   &LastInterestRecord::lastRefreshed>
+      >
+    >
+  >;
+
+  Container m_container;
+  Container::nth_index<0>::type& m_byKeyName;
+  Container::nth_index<1>::type& m_byLastRefreshed;
+};
+
+} // inline namespace v2
+} // namespace security
+} // namespace ndn
+
+#endif // NDN_SECURITY_VALIDATION_POLICY_SIGNED_INTEREST_HPP
diff --git a/ndn-cxx/security/validator-config.cpp b/ndn-cxx/security/validator-config.cpp
index d8c3219..cb19348 100644
--- a/ndn-cxx/security/validator-config.cpp
+++ b/ndn-cxx/security/validator-config.cpp
@@ -25,16 +25,23 @@
 namespace ndn {
 namespace security {
 
-ValidatorConfig::ValidatorConfig(std::unique_ptr<CertificateFetcher> fetcher, const Options& options)
-  : Validator(make_unique<ValidationPolicyCommandInterest>(make_unique<ValidationPolicyConfig>(),
-                                                           options),
+ValidatorConfig::ValidatorConfig(std::unique_ptr<CertificateFetcher> fetcher,
+                                 const CommandInterestOptions& ciOptions,
+                                 const SignedInterestOptions& siOptions)
+  : Validator(make_unique<ValidationPolicySignedInterest>(
+                make_unique<ValidationPolicyCommandInterest>(
+                  make_unique<ValidationPolicyConfig>(),
+                  ciOptions),
+                siOptions),
               std::move(fetcher))
-  , m_policyConfig(static_cast<ValidationPolicyConfig&>(getPolicy().getInnerPolicy()))
+  , m_policyConfig(static_cast<ValidationPolicyConfig&>(getPolicy().getInnerPolicy().getInnerPolicy()))
 {
 }
 
-ValidatorConfig::ValidatorConfig(Face& face, const Options& options)
-  : ValidatorConfig(make_unique<CertificateFetcherFromNetwork>(face), options)
+ValidatorConfig::ValidatorConfig(Face& face,
+                                 const CommandInterestOptions& ciOptions,
+                                 const SignedInterestOptions& siOptions)
+  : ValidatorConfig(make_unique<CertificateFetcherFromNetwork>(face), ciOptions, siOptions)
 {
 }
 
diff --git a/ndn-cxx/security/validator-config.hpp b/ndn-cxx/security/validator-config.hpp
index 3583349..a5d6d51 100644
--- a/ndn-cxx/security/validator-config.hpp
+++ b/ndn-cxx/security/validator-config.hpp
@@ -25,24 +25,31 @@
 #include "ndn-cxx/security/validator.hpp"
 #include "ndn-cxx/security/validation-policy-command-interest.hpp"
 #include "ndn-cxx/security/validation-policy-config.hpp"
+#include "ndn-cxx/security/validation-policy-signed-interest.hpp"
 
 namespace ndn {
 namespace security {
 
 /**
- * @brief Helper for validator that uses CommandInterest + Config policy and NetworkFetcher
+ * @brief Helper for validator that uses SignedInterest + CommandInterest + Config policy and
+ *        NetworkFetcher
  */
 class ValidatorConfig : public Validator
 {
 public:
   using Validator::Validator;
-  using Options = ValidationPolicyCommandInterest::Options;
+  using SignedInterestOptions = ValidationPolicySignedInterest::Options;
+  using CommandInterestOptions = ValidationPolicyCommandInterest::Options;
 
   explicit
-  ValidatorConfig(std::unique_ptr<CertificateFetcher> fetcher, const Options& options = Options());
+  ValidatorConfig(std::unique_ptr<CertificateFetcher> fetcher,
+                  const CommandInterestOptions& ciOptions = {},
+                  const SignedInterestOptions& siOptions = {});
 
   explicit
-  ValidatorConfig(Face& face, const Options& options = Options());
+  ValidatorConfig(Face& face,
+                  const CommandInterestOptions& ciOptions = {},
+                  const SignedInterestOptions& siOptions = {});
 
 public: // helpers for ValidationPolicyConfig
   void
diff --git a/tests/unit/security/interest-signer.t.cpp b/tests/unit/security/interest-signer.t.cpp
index 9362c76..6d054ff 100644
--- a/tests/unit/security/interest-signer.t.cpp
+++ b/tests/unit/security/interest-signer.t.cpp
@@ -41,24 +41,28 @@
   InterestSigner signer(m_keyChain);
   Interest i1 = signer.makeCommandInterest("/hello/world");
   BOOST_REQUIRE_EQUAL(i1.getName().size(), 6);
-  BOOST_CHECK_EQUAL(i1.getName().at(command_interest::POS_SIG_VALUE).blockFromValue().type(), tlv::SignatureValue);
-  BOOST_CHECK_EQUAL(i1.getName().at(command_interest::POS_SIG_INFO).blockFromValue().type(), tlv::SignatureInfo);
+  BOOST_TEST(i1.getName().at(command_interest::POS_SIG_VALUE).blockFromValue().type() == tlv::SignatureValue);
+  BOOST_TEST(i1.getName().at(command_interest::POS_SIG_INFO).blockFromValue().type() == tlv::SignatureInfo);
 
   time::milliseconds timestamp = toUnixTimestamp(time::system_clock::now());
-  BOOST_CHECK_EQUAL(i1.getName().at(command_interest::POS_TIMESTAMP).toNumber(), timestamp.count());
+  BOOST_TEST(i1.getName().at(command_interest::POS_TIMESTAMP).toNumber() == timestamp.count());
 
   Interest i2 = signer.makeCommandInterest("/hello/world/!", signingByIdentity("/test"));
   BOOST_REQUIRE_EQUAL(i2.getName().size(), 7);
-  BOOST_CHECK_EQUAL(i2.getName().at(command_interest::POS_SIG_VALUE).blockFromValue().type(), tlv::SignatureValue);
-  BOOST_CHECK_EQUAL(i2.getName().at(command_interest::POS_SIG_INFO).blockFromValue().type(), tlv::SignatureInfo);
-  BOOST_CHECK_GT(i2.getName().at(command_interest::POS_TIMESTAMP), i1.getName().at(command_interest::POS_TIMESTAMP));
+  BOOST_TEST(i2.getName().at(command_interest::POS_SIG_VALUE).blockFromValue().type() == tlv::SignatureValue);
+  BOOST_TEST(i2.getName().at(command_interest::POS_SIG_INFO).blockFromValue().type() == tlv::SignatureInfo);
+  // These doesn't play well with BOOST_TEST for some reason
+  BOOST_CHECK_GT(i2.getName().at(command_interest::POS_TIMESTAMP),
+                 i1.getName().at(command_interest::POS_TIMESTAMP));
   BOOST_CHECK_NE(i2.getName().at(command_interest::POS_RANDOM_VAL),
                  i1.getName().at(command_interest::POS_RANDOM_VAL)); // this sometimes can fail
 
   advanceClocks(100_s);
 
   i2 = signer.makeCommandInterest("/hello/world/!");
-  BOOST_CHECK_GT(i2.getName().at(command_interest::POS_TIMESTAMP), i1.getName().at(command_interest::POS_TIMESTAMP));
+  // This doesn't play well with BOOST_TEST for some reason
+  BOOST_CHECK_GT(i2.getName().at(command_interest::POS_TIMESTAMP),
+                 i1.getName().at(command_interest::POS_TIMESTAMP));
 }
 
 BOOST_AUTO_TEST_CASE(V03)
@@ -68,29 +72,51 @@
   InterestSigner signer(m_keyChain);
   Interest i1("/hello/world");
   i1.setCanBePrefix(false);
-  signer.makeSignedInterest(i1);
-  BOOST_CHECK_EQUAL(i1.isSigned(), true);
-  BOOST_REQUIRE_EQUAL(i1.getName().size(), 3);
-  BOOST_REQUIRE(i1.getSignatureInfo());
+  signer.makeSignedInterest(i1, SigningInfo(),
+                            InterestSigner::SigningFlags::WantNonce |
+                              InterestSigner::SigningFlags::WantTime);
+  BOOST_TEST(i1.isSigned() == true);
+  BOOST_TEST_REQUIRE(i1.getName().size() == 3);
+  BOOST_TEST_REQUIRE(i1.getSignatureInfo().has_value());
 
+  BOOST_TEST(i1.getSignatureInfo()->getNonce().has_value() == true);
   BOOST_TEST(*i1.getSignatureInfo()->getTime() == time::system_clock::now());
+  BOOST_TEST(i1.getSignatureInfo()->getSeqNum().has_value() == false);
 
   Interest i2("/hello/world/!");
   i2.setCanBePrefix(false);
-  signer.makeSignedInterest(i2, signingByIdentity("/test"));
-  BOOST_CHECK_EQUAL(i2.isSigned(), true);
+  signer.makeSignedInterest(i2, signingByIdentity("/test"),
+                            InterestSigner::SigningFlags::WantNonce |
+                              InterestSigner::SigningFlags::WantTime |
+                              InterestSigner::SigningFlags::WantSeqNum);
+  BOOST_TEST(i2.isSigned() == true);
   BOOST_REQUIRE_EQUAL(i2.getName().size(), 4);
   BOOST_REQUIRE(i2.getSignatureInfo());
 
-  BOOST_TEST(*i2.getSignatureInfo()->getTime() > *i1.getSignatureInfo()->getTime());
   BOOST_TEST(*i2.getSignatureInfo()->getNonce() != *i1.getSignatureInfo()->getNonce());
+  BOOST_TEST(*i2.getSignatureInfo()->getTime() > *i1.getSignatureInfo()->getTime());
+  BOOST_TEST_REQUIRE(i2.getSignatureInfo()->getSeqNum().has_value() == true);
 
   advanceClocks(100_s);
 
-  signer.makeSignedInterest(i2);
-  BOOST_CHECK_EQUAL(i2.isSigned(), true);
+  Interest i3("/hello/world/2");
+  i3.setCanBePrefix(false);
+  signer.makeSignedInterest(i3, SigningInfo(), InterestSigner::SigningFlags::WantSeqNum);
+  BOOST_TEST(i3.isSigned() == true);
+  BOOST_REQUIRE_EQUAL(i3.getName().size(), 4);
+  BOOST_REQUIRE(i3.getSignatureInfo());
 
-  BOOST_TEST(*i2.getSignatureInfo()->getTime() == time::system_clock::now());
+  BOOST_TEST(i3.getSignatureInfo()->getNonce().has_value() == false);
+  BOOST_TEST(i3.getSignatureInfo()->getTime().has_value() == false);
+  BOOST_TEST_REQUIRE(i3.getSignatureInfo()->getSeqNum().has_value() == true);
+  BOOST_TEST(*i3.getSignatureInfo()->getSeqNum() > *i2.getSignatureInfo()->getSeqNum());
+
+  signer.makeSignedInterest(i3);
+  BOOST_TEST(i3.isSigned() == true);
+
+  BOOST_TEST(*i3.getSignatureInfo()->getTime() == time::system_clock::now());
+
+  BOOST_CHECK_THROW(signer.makeSignedInterest(i3, SigningInfo(), 0), std::invalid_argument);
 }
 
 BOOST_AUTO_TEST_SUITE_END() // TestInterestSigner
diff --git a/tests/unit/security/validation-policy-command-interest.t.cpp b/tests/unit/security/validation-policy-command-interest.t.cpp
index af9bc2a..9f32056 100644
--- a/tests/unit/security/validation-policy-command-interest.t.cpp
+++ b/tests/unit/security/validation-policy-command-interest.t.cpp
@@ -41,7 +41,7 @@
 
 BOOST_AUTO_TEST_SUITE(Security)
 
-class DefaultOptions
+class CommandInterestDefaultOptions
 {
 public:
   static ValidationPolicyCommandInterest::Options
@@ -89,7 +89,8 @@
   CommandInterestSigner m_signer;
 };
 
-BOOST_FIXTURE_TEST_SUITE(TestValidationPolicyCommandInterest, ValidationPolicyCommandInterestFixture<DefaultOptions>)
+BOOST_FIXTURE_TEST_SUITE(TestValidationPolicyCommandInterest,
+                         ValidationPolicyCommandInterestFixture<CommandInterestDefaultOptions>)
 
 BOOST_AUTO_TEST_SUITE(Accepts)
 
@@ -130,7 +131,7 @@
   VALIDATE_SUCCESS(d1, "Should succeed (fallback on inner validation policy for data)");
 }
 
-using ValidationPolicyAcceptAllCommands = ValidationPolicyCommandInterestFixture<DefaultOptions,
+using ValidationPolicyAcceptAllCommands = ValidationPolicyCommandInterestFixture<CommandInterestDefaultOptions,
                                                                                  ValidationPolicyAcceptAll>;
 
 BOOST_FIXTURE_TEST_CASE(SignedWithSha256, ValidationPolicyAcceptAllCommands) // Bug 4635
diff --git a/tests/unit/security/validation-policy-signed-interest.t.cpp b/tests/unit/security/validation-policy-signed-interest.t.cpp
new file mode 100644
index 0000000..f46e5ad
--- /dev/null
+++ b/tests/unit/security/validation-policy-signed-interest.t.cpp
@@ -0,0 +1,567 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2013-2020 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 "ndn-cxx/security/validation-policy-signed-interest.hpp"
+#include "ndn-cxx/security/interest-signer.hpp"
+#include "ndn-cxx/security/signing-helpers.hpp"
+#include "ndn-cxx/security/validation-policy-accept-all.hpp"
+#include "ndn-cxx/security/validation-policy-simple-hierarchy.hpp"
+
+#include "tests/boost-test.hpp"
+#include "tests/make-interest-data.hpp"
+#include "tests/unit/security/validator-fixture.hpp"
+
+#include <boost/lexical_cast.hpp>
+#include <boost/mpl/vector.hpp>
+
+namespace ndn {
+namespace security {
+inline namespace v2 {
+namespace tests {
+
+using namespace ndn::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+
+class SignedInterestDefaultOptions
+{
+public:
+  static ValidationPolicySignedInterest::Options
+  getOptions()
+  {
+    return {};
+  }
+};
+
+template<class T, class InnerPolicy>
+class SignedInterestPolicyWrapper : public ValidationPolicySignedInterest
+{
+public:
+  SignedInterestPolicyWrapper()
+    : ValidationPolicySignedInterest(make_unique<InnerPolicy>(), T::getOptions())
+  {
+  }
+};
+
+template<class T, class InnerPolicy = ValidationPolicySimpleHierarchy>
+class ValidationPolicySignedInterestFixture
+  : public HierarchicalValidatorFixture<SignedInterestPolicyWrapper<T, InnerPolicy>>
+{
+public:
+  ValidationPolicySignedInterestFixture()
+    : m_signer(this->m_keyChain)
+  {
+  }
+
+  Interest
+  makeSignedInterest(const Identity& identity,
+                     uint32_t signingFlags = InterestSigner::WantNonce | InterestSigner::WantTime)
+  {
+    Interest i(Name(identity.getName()).append("CMD"));
+    i.setCanBePrefix(false);
+    m_signer.makeSignedInterest(i, signingByIdentity(identity), signingFlags);
+    return i;
+  }
+
+public:
+  InterestSigner m_signer;
+
+  static constexpr uint32_t WantAll = InterestSigner::WantNonce |
+                                        InterestSigner::WantTime |
+                                        InterestSigner::WantSeqNum;
+};
+
+BOOST_FIXTURE_TEST_SUITE(TestValidationPolicySignedInterest,
+                         ValidationPolicySignedInterestFixture<SignedInterestDefaultOptions>)
+
+BOOST_AUTO_TEST_CASE(BasicV3)
+{
+  auto i1 = makeSignedInterest(identity, WantAll);
+  VALIDATE_SUCCESS(i1, "Should succeed (within grace period)");
+  VALIDATE_FAILURE(i1, "Should fail (replay attack)");
+
+  advanceClocks(5_ms);
+  auto i2 = makeSignedInterest(identity, WantAll);
+  VALIDATE_SUCCESS(i2, "Should succeed (timestamp and sequence number larger than previous)");
+
+  Interest i3(Name(identity.getName()).append("CMD"));
+  i3.setCanBePrefix(false);
+  m_signer.makeSignedInterest(i3, signingWithSha256());
+  VALIDATE_FAILURE(i3, "Should fail (Sha256 signature violates policy)");
+}
+
+BOOST_AUTO_TEST_CASE(DataPassthrough)
+{
+  Data d1("/Security/ValidatorFixture/Sub1");
+  m_keyChain.sign(d1);
+  VALIDATE_SUCCESS(d1, "Should succeed (fallback on inner validation policy for data)");
+}
+
+BOOST_AUTO_TEST_CASE(InnerPolicyReject)
+{
+  auto i1 = makeSignedInterest(otherIdentity);
+  VALIDATE_FAILURE(i1, "Should fail (inner policy should reject)");
+}
+
+class LimitedRecordsOptions
+{
+public:
+  static ValidationPolicySignedInterest::Options
+  getOptions()
+  {
+    ValidationPolicySignedInterest::Options options;
+    options.timestampGracePeriod = 15_s;
+    options.maxRecordCount = 3;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(LimitedRecords, ValidationPolicySignedInterestFixture<LimitedRecordsOptions>)
+{
+  Identity id1 = addSubCertificate("/Security/ValidatorFixture/Sub1", identity);
+  cache.insert(id1.getDefaultKey().getDefaultCertificate());
+  Identity id2 = addSubCertificate("/Security/ValidatorFixture/Sub2", identity);
+  cache.insert(id2.getDefaultKey().getDefaultCertificate());
+  Identity id3 = addSubCertificate("/Security/ValidatorFixture/Sub3", identity);
+  cache.insert(id3.getDefaultKey().getDefaultCertificate());
+  Identity id4 = addSubCertificate("/Security/ValidatorFixture/Sub4", identity);
+  cache.insert(id4.getDefaultKey().getDefaultCertificate());
+
+  auto i1 = makeSignedInterest(id2);
+  auto i2 = makeSignedInterest(id3);
+  auto i3 = makeSignedInterest(id4);
+  auto i00 = makeSignedInterest(id1); // signed at 0s
+  advanceClocks(1_s);
+  auto i01 = makeSignedInterest(id1); // signed at 1s
+  advanceClocks(1_s);
+  auto i02 = makeSignedInterest(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 ValidationPolicySignedInterest::Options
+  getOptions()
+  {
+    ValidationPolicySignedInterest::Options options;
+    options.timestampGracePeriod = 15_s;
+    options.maxRecordCount = -1;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(UnlimitedRecords, ValidationPolicySignedInterestFixture<UnlimitedRecordsOptions>)
+{
+  std::vector<Identity> identities;
+  for (int i = 0; i < 20; ++i) {
+    Identity id = addSubCertificate("/Security/ValidatorFixture/Sub" + to_string(i), identity);
+    cache.insert(id.getDefaultKey().getDefaultCertificate());
+    identities.push_back(id);
+  }
+
+  auto i1 = makeSignedInterest(identities.at(0)); // signed at 0s
+  advanceClocks(1_s);
+  for (int 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)");
+}
+
+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>)
+{
+  auto i1 = makeSignedInterest(identity); // signed at 0s
+  advanceClocks(1_s);
+  auto i2 = makeSignedInterest(identity); // signed at +1s
+  VALIDATE_SUCCESS(i2, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i1, "Should succeed despite timestamp reordering, as records aren't kept");
+}
+
+BOOST_AUTO_TEST_SUITE(TimestampValidation)
+
+BOOST_AUTO_TEST_CASE(MissingTimestamp)
+{
+  auto i1 = makeSignedInterest(identity, InterestSigner::WantSeqNum);
+  VALIDATE_FAILURE(i1, "Should fail (timestamp missing)");
+}
+
+class DisabledTimestampValidationOptions
+{
+public:
+  static ValidationPolicySignedInterest::Options
+  getOptions()
+  {
+    ValidationPolicySignedInterest::Options options;
+    options.shouldValidateTimestamps = false;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(Disabled,
+                        ValidationPolicySignedInterestFixture<DisabledTimestampValidationOptions>)
+{
+  auto i1 = makeSignedInterest(identity); // signed at 0ms
+  advanceClocks(100_ms);
+  VALIDATE_SUCCESS(i1, "Should succeed");
+
+  auto i2 = makeSignedInterest(identity); // signed at +100ms
+  // Set i2 to have same timestamp as i1
+  auto si2 = i2.getSignatureInfo();
+  si2->setTime(i2.getSignatureInfo()->getTime());
+  i2.setSignatureInfo(*si2);
+  VALIDATE_SUCCESS(i2, "Should succeed");
+}
+
+class GracePeriod15Sec
+{
+public:
+  static ValidationPolicySignedInterest::Options
+  getOptions()
+  {
+    ValidationPolicySignedInterest::Options options;
+    options.timestampGracePeriod = 15_s;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(TimestampTooOld, ValidationPolicySignedInterestFixture<GracePeriod15Sec>)
+{
+  auto i1 = makeSignedInterest(identity); // signed at 0s
+  advanceClocks(16_s); // verifying at +16s
+  VALIDATE_FAILURE(i1, "Should fail (timestamp outside the grace period)");
+  rewindClockAfterValidation();
+
+  auto i2 = makeSignedInterest(identity); // signed at +16s
+  VALIDATE_SUCCESS(i2, "Should succeed");
+}
+
+BOOST_FIXTURE_TEST_CASE(TimestampTooNew, ValidationPolicySignedInterestFixture<GracePeriod15Sec>)
+{
+  auto i1 = makeSignedInterest(identity); // signed at 0s
+  advanceClocks(1_s);
+  auto i2 = makeSignedInterest(identity); // signed at +1s
+  advanceClocks(1_s);
+  auto i3 = makeSignedInterest(identity); // signed at +2s
+
+  systemClock->advance(-18_s); // verifying at -16s
+  VALIDATE_FAILURE(i1, "Should fail (timestamp outside the grace period)");
+  rewindClockAfterValidation();
+
+  // SignedInterestValidator should not remember i1's timestamp
+  VALIDATE_FAILURE(i2, "Should fail (timestamp outside the grace period)");
+  rewindClockAfterValidation();
+
+  // SignedInterestValidator 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 = makeSignedInterest(identity); // signed at 0s
+  VALIDATE_SUCCESS(i1, "Should succeed");
+
+  auto i2 = makeSignedInterest(identity); // signed at 0s
+  auto si2 = i2.getSignatureInfo();
+  si2->setTime(i1.getSignatureInfo()->getTime());
+  i2.setSignatureInfo(*si2);
+  VALIDATE_FAILURE(i2, "Should fail (timestamp reordered)");
+
+  advanceClocks(2_s);
+  auto i3 = makeSignedInterest(identity); // signed at +2s
+  VALIDATE_SUCCESS(i3, "Should succeed");
+}
+
+BOOST_AUTO_TEST_CASE(TimestampReorderNegative)
+{
+  auto i2 = makeSignedInterest(identity); // signed at 0ms
+  advanceClocks(200_ms);
+  auto i3 = makeSignedInterest(identity); // signed at +200ms
+  advanceClocks(900_ms);
+  auto i1 = makeSignedInterest(identity); // signed at +1100ms
+  advanceClocks(300_ms);
+  auto i4 = makeSignedInterest(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();
+
+  // SignedInterestValidator 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");
+}
+
+template<class T>
+class GracePeriod
+{
+public:
+  static ValidationPolicySignedInterest::Options
+  getOptions()
+  {
+    ValidationPolicySignedInterest::Options options;
+    options.timestampGracePeriod = 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,
+                                 ValidationPolicySignedInterestFixture<GracePeriod>)
+{
+  auto i1 = this->makeSignedInterest(this->identity); // signed at 0ms
+  auto i2 = this->makeSignedInterest(this->subIdentity); // signed at 0ms
+  // ensure timestamps are exactly 0ms
+  for (auto interest : {&i1, &i2}) {
+    auto si = interest->getSignatureInfo();
+    si->setTime(time::system_clock::now());
+    interest->setSignatureInfo(*si);
+  }
+
+  VALIDATE_SUCCESS(i1, "Should succeed when validating at 0ms");
+  this->rewindClockAfterValidation();
+
+  this->advanceClocks(1_ms);
+  VALIDATE_FAILURE(i2, "Should fail when validating at 1ms");
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TimestampValidation
+
+BOOST_AUTO_TEST_SUITE(SeqNumValidation)
+
+// By default, sequence number validation is disabled
+BOOST_AUTO_TEST_CASE(Disabled)
+{
+  auto i1 = makeSignedInterest(identity, WantAll); // signed at 0ms
+  VALIDATE_SUCCESS(i1, "Should succeed");
+
+  auto i2 = makeSignedInterest(identity, WantAll); // signed at +100ms
+  // Set i2 to have same seq num as i1
+  auto si2 = i2.getSignatureInfo();
+  si2->setSeqNum(i2.getSignatureInfo()->getSeqNum());
+  i2.setSignatureInfo(*si2);
+  VALIDATE_SUCCESS(i2, "Should succeed");
+}
+
+class SeqNumValidationOptions
+{
+public:
+  static ValidationPolicySignedInterest::Options
+  getOptions()
+  {
+    ValidationPolicySignedInterest::Options options;
+    options.shouldValidateSeqNums = true;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(MissingSeqNum,
+                        ValidationPolicySignedInterestFixture<SeqNumValidationOptions>)
+{
+  auto i1 = makeSignedInterest(identity, InterestSigner::WantTime);
+  VALIDATE_FAILURE(i1, "Should fail (sequence number missing");
+}
+
+BOOST_FIXTURE_TEST_CASE(SeqNumReorder,
+                        ValidationPolicySignedInterestFixture<SeqNumValidationOptions>)
+{
+  auto i1 = makeSignedInterest(identity, WantAll); // seq num is i
+  VALIDATE_SUCCESS(i1, "Should succeed");
+
+  auto i2 = makeSignedInterest(identity, WantAll); // seq num is i+1
+  auto si2 = i2.getSignatureInfo();
+  si2->setSeqNum(i1.getSignatureInfo()->getSeqNum());
+  i2.setSignatureInfo(*si2);
+  VALIDATE_FAILURE(i2, "Should fail (sequence number reordered)");
+
+  auto i3 = makeSignedInterest(identity, WantAll); // seq num is i+2
+  VALIDATE_SUCCESS(i3, "Should succeed");
+}
+
+BOOST_AUTO_TEST_SUITE_END() // SeqNumValidation
+
+BOOST_AUTO_TEST_SUITE(NonceValidation)
+
+BOOST_AUTO_TEST_CASE(MissingNonce)
+{
+  auto i1 = makeSignedInterest(identity, InterestSigner::WantTime); // Specifically exclude nonce
+  VALIDATE_FAILURE(i1, "Should fail (nonce missing)");
+}
+
+BOOST_AUTO_TEST_CASE(DuplicateNonce)
+{
+  auto i1 = makeSignedInterest(identity, WantAll);
+  VALIDATE_SUCCESS(i1, "Should succeed");
+
+  auto i2 = makeSignedInterest(identity, WantAll);
+  auto si2 = i2.getSignatureInfo();
+  si2->setNonce(i1.getSignatureInfo()->getNonce());
+  i2.setSignatureInfo(*si2);
+  VALIDATE_FAILURE(i2, "Should fail (duplicate nonce)");
+
+  auto i3 = makeSignedInterest(identity, WantAll);
+  // On the off chance that the generated nonce is identical to i1
+  while (i3.getSignatureInfo()->getNonce() == i1.getSignatureInfo()->getNonce()) {
+    i3 = makeSignedInterest(identity, WantAll);
+  }
+  VALIDATE_SUCCESS(i3, "Should succeed");
+}
+
+class DisabledNonceValidationOptions
+{
+public:
+  static ValidationPolicySignedInterest::Options
+  getOptions()
+  {
+    ValidationPolicySignedInterest::Options options;
+    options.shouldValidateNonces = false;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(Disabled,
+                        ValidationPolicySignedInterestFixture<DisabledNonceValidationOptions>)
+{
+  auto i1 = makeSignedInterest(identity, WantAll ^ InterestSigner::WantNonce);
+  VALIDATE_SUCCESS(i1, "Should succeed");
+
+  // Ensure still works when a nonce is present
+  auto i2 = makeSignedInterest(identity, WantAll);
+  VALIDATE_SUCCESS(i2, "Should succeed");
+
+  // Ensure a duplicate still succeeds
+  auto i3 = makeSignedInterest(identity, WantAll);
+  auto si3 = i3.getSignatureInfo();
+  si3->setNonce(i2.getSignatureInfo()->getNonce());
+  i3.setSignatureInfo(*si3);
+  m_keyChain.sign(i3, signingByIdentity(identity).setSignedInterestFormat(SignedInterestFormat::V03)
+                                                 .setSignatureInfo(*si3));
+  VALIDATE_SUCCESS(i3, "Should succeed");
+}
+
+class NonceLimit2Options
+{
+public:
+  static ValidationPolicySignedInterest::Options
+  getOptions()
+  {
+    ValidationPolicySignedInterest::Options options;
+    options.shouldValidateTimestamps = false;
+    options.shouldValidateSeqNums = false;
+    options.maxNonceRecordCount = 2;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(NonceRecordLimit,
+                        ValidationPolicySignedInterestFixture<NonceLimit2Options>)
+{
+  auto i1 = makeSignedInterest(identity, WantAll);
+  VALIDATE_SUCCESS(i1, "Should succeed");
+
+  auto i2 = makeSignedInterest(identity, WantAll);
+  // On the off chance that the generated nonce is identical to i1
+  while (i2.getSignatureInfo()->getNonce() == i1.getSignatureInfo()->getNonce()) {
+    i2 = makeSignedInterest(identity, WantAll);
+  }
+  VALIDATE_SUCCESS(i2, "Should succeed");
+
+  auto i3 = makeSignedInterest(identity, WantAll);
+  auto si3 = i3.getSignatureInfo();
+  si3->setNonce(i1.getSignatureInfo()->getNonce());
+  i3.setSignatureInfo(*si3);
+  m_keyChain.sign(i3, signingByIdentity(identity).setSignedInterestFormat(SignedInterestFormat::V03)
+                                                 .setSignatureInfo(*si3));
+  VALIDATE_FAILURE(i3, "Should fail (duplicate nonce)");
+
+  // Pop i1's nonce off the list
+  auto i4 = makeSignedInterest(identity, WantAll);
+  // On the off chance that the generated nonce is identical to i1 or i2
+  while (i4.getSignatureInfo()->getNonce() == i1.getSignatureInfo()->getNonce() ||
+         i4.getSignatureInfo()->getNonce() == i2.getSignatureInfo()->getNonce()) {
+    i4 = makeSignedInterest(identity, WantAll);
+  }
+  VALIDATE_SUCCESS(i4, "Should succeed");
+
+  // Now i3 should succeed because i1's nonce has been popped off the list
+  auto i5 = makeSignedInterest(identity, WantAll);
+  auto si5 = i5.getSignatureInfo();
+  si5->setNonce(i1.getSignatureInfo()->getNonce());
+  i5.setSignatureInfo(*si5);
+  m_keyChain.sign(i5, signingByIdentity(identity).setSignedInterestFormat(SignedInterestFormat::V03)
+                                                 .setSignatureInfo(*si5));
+  VALIDATE_SUCCESS(i5, "Should succeed");
+}
+
+BOOST_AUTO_TEST_SUITE_END() // NonceValidation
+
+BOOST_AUTO_TEST_SUITE_END() // TestValidationPolicySignedInterest
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // inline namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit/security/validator-config.t.cpp b/tests/unit/security/validator-config.t.cpp
index b4654ac..dac6913 100644
--- a/tests/unit/security/validator-config.t.cpp
+++ b/tests/unit/security/validator-config.t.cpp
@@ -132,7 +132,7 @@
 {
   validator.load(configFile);
 
-  CommandInterestSigner signer(m_keyChain);
+  InterestSigner signer(m_keyChain);
   auto i = signer.makeCommandInterest("/hello/world/CMD", signingWithSha256());
   size_t nValidated = 0, nFailed = 0;
 
@@ -150,6 +150,53 @@
   BOOST_CHECK_EQUAL(nFailed, 1);
 }
 
+BOOST_FIXTURE_TEST_CASE(ValidateSignedInterest, ValidatorConfigFixture)
+{
+  validator.load(configFile);
+
+  InterestSigner signer(m_keyChain);
+  Interest i1("/hello/world");
+  i1.setCanBePrefix(false);
+  signer.makeSignedInterest(i1);
+  size_t nValidated = 0, nFailed = 0;
+
+  validator.validate(i1, [&] (auto&&...) { ++nValidated; }, [&] (auto&&...) { ++nFailed; });
+  BOOST_CHECK_EQUAL(nValidated, 1);
+  BOOST_CHECK_EQUAL(nFailed, 0);
+
+  validator.validate(i1, [&] (auto&&...) { ++nValidated; }, [&] (auto&&...) { ++nFailed; });
+  BOOST_CHECK_EQUAL(nValidated, 1);
+  BOOST_CHECK_EQUAL(nFailed, 1);
+
+  Interest i2("/hello/world");
+  i2.setCanBePrefix(false);
+  signer.makeSignedInterest(i2, signingWithSha256());
+  validator.validate(i2, [&] (auto&&...) { ++nValidated; }, [&] (auto&&...) { ++nFailed; });
+  BOOST_CHECK_EQUAL(nValidated, 2);
+  BOOST_CHECK_EQUAL(nFailed, 1);
+}
+
+BOOST_FIXTURE_TEST_CASE(ValidateCommandInterest, ValidatorConfigFixture)
+{
+  validator.load(configFile);
+
+  InterestSigner signer(m_keyChain);
+  auto i1 = signer.makeCommandInterest("/hello/world");
+  size_t nValidated = 0, nFailed = 0;
+
+  validator.validate(i1, [&] (auto&&...) { ++nValidated; }, [&] (auto&&...) { ++nFailed; });
+  BOOST_CHECK_EQUAL(nValidated, 1);
+  BOOST_CHECK_EQUAL(nFailed, 0);
+
+  validator.validate(i1, [&] (auto&&...) { ++nValidated; }, [&] (auto&&...) { ++nFailed; });
+  BOOST_CHECK_EQUAL(nValidated, 1);
+  BOOST_CHECK_EQUAL(nFailed, 1);
+
+  auto i2 = signer.makeCommandInterest("/hello/world");
+  validator.validate(i2, [&] (auto&&...) { ++nValidated; }, [&] (auto&&...) { ++nFailed; });
+  BOOST_CHECK_EQUAL(nValidated, 2);
+  BOOST_CHECK_EQUAL(nFailed, 1);
+}
 
 BOOST_AUTO_TEST_SUITE_END() // TestValidatorConfig
 BOOST_AUTO_TEST_SUITE_END() // Security