security: Add ValidationPolicyCommandInterest

Refs: #3920

Change-Id: I978e68979e4f9cfb58561054b9f87f8d65083d5d
diff --git a/src/security/v2/validation-policy-command-interest.cpp b/src/security/v2/validation-policy-command-interest.cpp
new file mode 100644
index 0000000..5753ef3
--- /dev/null
+++ b/src/security/v2/validation-policy-command-interest.cpp
@@ -0,0 +1,173 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2013-2017 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 "validation-policy-command-interest.hpp"
+#include "../pib/key.hpp"
+
+namespace ndn {
+namespace security {
+namespace v2 {
+
+ValidationPolicyCommandInterest::ValidationPolicyCommandInterest(unique_ptr<ValidationPolicy> inner,
+                                                                 const Options& options)
+  : m_options(options)
+  , m_index(m_container.get<0>())
+  , m_queue(m_container.get<1>())
+{
+  if (inner == nullptr) {
+    BOOST_THROW_EXCEPTION(std::invalid_argument("inner policy is missing"));
+  }
+  setInnerPolicy(std::move(inner));
+
+  m_options.gracePeriod = std::max(m_options.gracePeriod, time::nanoseconds::zero());
+}
+
+void
+ValidationPolicyCommandInterest::checkPolicy(const Data& data, const shared_ptr<ValidationState>& state,
+                                             const ValidationContinuation& continueValidation)
+{
+  getInnerPolicy().checkPolicy(data, state, continueValidation);
+}
+
+void
+ValidationPolicyCommandInterest::checkPolicy(const Interest& interest, const shared_ptr<ValidationState>& state,
+                                             const ValidationContinuation& continueValidation)
+{
+  bool isOk = false;
+  Name keyName;
+  uint64_t timestamp = 0;
+  std::tie(isOk, keyName, timestamp) = parseCommandInterest(interest, state);
+  if (!isOk) {
+    return;
+  }
+
+  if (!checkTimestamp(state, keyName, timestamp)) {
+    return;
+  }
+  getInnerPolicy().checkPolicy(interest, state, std::bind(continueValidation, _1, _2));
+}
+
+void
+ValidationPolicyCommandInterest::cleanup()
+{
+  time::steady_clock::TimePoint expiring = time::steady_clock::now() - m_options.recordLifetime;
+
+  while ((!m_queue.empty() && m_queue.front().lastRefreshed <= expiring) ||
+         (m_options.maxRecords >= 0 &&
+          m_queue.size() > static_cast<size_t>(m_options.maxRecords))) {
+    m_queue.pop_front();
+  }
+}
+
+std::tuple<bool, Name, uint64_t>
+ValidationPolicyCommandInterest::parseCommandInterest(const Interest& interest,
+                                                      const shared_ptr<ValidationState>& state) const
+{
+  const Name& name = interest.getName();
+  if (name.size() < command_interest::MIN_SIZE) {
+    state->fail({ValidationError::POLICY_ERROR, "Command interest name `" +
+                 interest.getName().toUri() + "` is too short"});
+    return std::make_tuple(false, Name(), 0);
+  }
+
+  const name::Component& timestampComp = name.at(command_interest::POS_TIMESTAMP);
+  if (!timestampComp.isNumber()) {
+    state->fail({ValidationError::POLICY_ERROR, "Command interest `" +
+                 interest.getName().toUri() + "` doesn't include timestamp component"});
+    return std::make_tuple(false, Name(), 0);
+  }
+
+  SignatureInfo sig;
+  try {
+    sig.wireDecode(name[signed_interest::POS_SIG_INFO].blockFromValue());
+  }
+  catch (const tlv::Error&) {
+    state->fail({ValidationError::POLICY_ERROR, "Command interest `" +
+                 interest.getName().toUri() + "` does not include SignatureInfo component"});
+    return std::make_tuple(false, Name(), 0);
+  }
+
+  if (!sig.hasKeyLocator()) {
+    state->fail({ValidationError::INVALID_KEY_LOCATOR, "Command interest `" +
+                 interest.getName().toUri() + "` does not include KeyLocator"});
+    return std::make_tuple(false, Name(), 0);
+  }
+
+  const KeyLocator& keyLocator = sig.getKeyLocator();
+  if (keyLocator.getType() != KeyLocator::KeyLocator_Name) {
+    state->fail({ValidationError::INVALID_KEY_LOCATOR, "Command interest `" +
+                 interest.getName().toUri() + "` KeyLocator type is not Name"});
+    return std::make_tuple(false, Name(), 0);
+  }
+
+  return std::make_tuple(true, keyLocator.getName(), timestampComp.toNumber());
+}
+
+bool
+ValidationPolicyCommandInterest::checkTimestamp(const shared_ptr<ValidationState>& state,
+                                                const Name& keyName, uint64_t timestamp)
+{
+  this->cleanup();
+
+  time::system_clock::TimePoint now = time::system_clock::now();
+  time::system_clock::TimePoint timestampPoint = time::fromUnixTimestamp(time::milliseconds(timestamp));
+  if (timestampPoint < now - m_options.gracePeriod || timestampPoint > now + m_options.gracePeriod) {
+    state->fail({ValidationError::POLICY_ERROR, "Timestamp is outside the grace period for key " + keyName.toUri()});
+    return false;
+  }
+  auto it = m_index.find(keyName);
+  if (it != m_index.end()) {
+    if (timestamp <= it->timestamp) {
+      state->fail({ValidationError::POLICY_ERROR, "Timestamp is reordered for key " + keyName.toUri()});
+      return false;
+    }
+  }
+
+  shared_ptr<InterestValidationState> interestState = std::dynamic_pointer_cast<InterestValidationState>(state);
+  interestState->afterSuccess.connect(bind(&ValidationPolicyCommandInterest::insertNewRecord,
+                                           this, _1, keyName, timestamp));
+  return true;
+}
+
+void
+ValidationPolicyCommandInterest::insertNewRecord(const Interest& interest, const Name& keyName,
+                                                 uint64_t timestamp)
+{
+  // try to insert new record
+  time::steady_clock::TimePoint now = time::steady_clock::now();
+  Queue::iterator i = m_queue.end();
+  bool isNew = false;
+  LastTimestampRecord newRecord{keyName, timestamp, now};
+  std::tie(i, isNew) = m_queue.push_back(newRecord);
+
+  if (!isNew) {
+    BOOST_ASSERT(i->keyName == keyName);
+
+    // set lastRefreshed field, and move to queue tail
+    m_queue.erase(i);
+    isNew = m_queue.push_back(newRecord).second;
+    BOOST_VERIFY(isNew);
+  }
+}
+
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/src/security/v2/validation-policy-command-interest.hpp b/src/security/v2/validation-policy-command-interest.hpp
new file mode 100644
index 0000000..96a9012
--- /dev/null
+++ b/src/security/v2/validation-policy-command-interest.hpp
@@ -0,0 +1,161 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2013-2017 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_V2_VALIDATION_POLICY_COMMAND_INTEREST_HPP
+#define NDN_SECURITY_V2_VALIDATION_POLICY_COMMAND_INTEREST_HPP
+
+#include "validation-policy.hpp"
+#include <boost/multi_index_container.hpp>
+#include <boost/multi_index/ordered_index.hpp>
+#include <boost/multi_index/sequenced_index.hpp>
+#include <boost/multi_index/key_extractors.hpp>
+
+namespace ndn {
+namespace security {
+namespace v2 {
+
+/** \brief Validation policy for stop-and-wait command Interests
+ *  \sa https://redmine.named-data.net/projects/ndn-cxx/wiki/CommandInterest
+ *
+ *  This policy checks the timestamp field of a stop-and-wait command Interest.
+ *  Signed Interest validation and Data validation requests are delegated to an inner policy.
+ */
+class ValidationPolicyCommandInterest : public ValidationPolicy
+{
+public:
+  class Options
+  {
+  public:
+    Options()
+    {
+    }
+
+  public:
+    /** \brief tolerance of initial timestamp
+     *
+     *  A stop-and-wait command Interest is considered "initial" if the validator
+     *  has not recorded the last timestamp from the same public key, or when
+     *  such knowledge has been erased.
+     *  For an initial command Interest, its timestamp is compared to the current
+     *  system clock, and the command Interest is rejected if the absolute difference
+     *  is greater than the grace interval.
+     *
+     *  This should be positive.
+     *  Setting this option to 0 or negative causes the validator to require exactly same
+     *  timestamp as the system clock, which most likely rejects all command Interests.
+     */
+    time::nanoseconds gracePeriod = time::seconds(120);
+
+    /** \brief max number of distinct public keys of which to record the last timestamp
+     *
+     *  The validator records last timestamps for every public key.
+     *  For a subsequent command Interest using the same public key,
+     *  its timestamp is compared to the last timestamp from that public key,
+     *  and the command Interest is rejected if its timestamp is
+     *  less than or equal to the recorded timestamp.
+     *
+     *  This option limits the number of distinct public keys being tracked.
+     *  If the limit is exceeded, the oldest record is deleted.
+     *
+     *  Setting this option to -1 allows tracking unlimited public keys.
+     *  Setting this option to 0 disables last timestamp records and causes
+     *  every command Interest to be processed as initial.
+     */
+    ssize_t maxRecords = 1000;
+
+    /** \brief max lifetime of a last timestamp record
+     *
+     *  A last timestamp record expires and can be deleted if it has not been refreshed
+     *  within this duration.
+     *  Setting this option to 0 or negative makes last timestamp records expire immediately
+     *  and causes every command Interest to be processed as initial.
+     */
+    time::nanoseconds recordLifetime = time::hours(1);
+  };
+
+  /** \brief constructor
+   *  \param inner a Validator for signed Interest signature validation and Data validation;
+   *               this must not be nullptr
+   *  \param options stop-and-wait command Interest validation options
+   *  \throw std::invalid_argument inner policy is nullptr
+   */
+  explicit
+  ValidationPolicyCommandInterest(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:
+  void
+  cleanup();
+
+  std::tuple<bool, Name, uint64_t>
+  parseCommandInterest(const Interest& interest, const shared_ptr<ValidationState>& state) const;
+
+  bool
+  checkTimestamp(const shared_ptr<ValidationState>& state,
+                 const Name& keyName, uint64_t timestamp);
+
+  void
+  insertNewRecord(const Interest& interest, const Name& keyName,
+                  uint64_t timestamp);
+
+private:
+  unique_ptr<ValidationPolicy> m_innerPolicy;
+  Options m_options;
+
+  struct LastTimestampRecord
+  {
+    Name keyName;
+    uint64_t timestamp;
+    time::steady_clock::TimePoint lastRefreshed;
+  };
+
+  typedef boost::multi_index_container<
+    LastTimestampRecord,
+    boost::multi_index::indexed_by<
+      boost::multi_index::ordered_unique<
+        boost::multi_index::member<LastTimestampRecord, Name, &LastTimestampRecord::keyName>
+      >,
+      boost::multi_index::sequenced<>
+    >
+  > Container;
+  typedef Container::nth_index<0>::type Index;
+  typedef Container::nth_index<1>::type Queue;
+
+  Container m_container;
+  Index& m_index;
+  Queue& m_queue;
+};
+
+} // namespace v2
+} // namespace security
+} // namespace ndn
+
+
+#endif // NDN_SECURITY_V2_VALIDATION_POLICY_COMMAND_INTEREST_HPP
diff --git a/src/security/v2/validation-state.cpp b/src/security/v2/validation-state.cpp
index 269ef2d..4267c7f 100644
--- a/src/security/v2/validation-state.cpp
+++ b/src/security/v2/validation-state.cpp
@@ -149,10 +149,10 @@
                                                  const InterestValidationSuccessCallback& successCb,
                                                  const InterestValidationFailureCallback& failureCb)
   : m_interest(interest)
-  , m_successCb(successCb)
   , m_failureCb(failureCb)
 {
-  BOOST_ASSERT(m_successCb != nullptr);
+  afterSuccess.connect(successCb);
+  BOOST_ASSERT(successCb != nullptr);
   BOOST_ASSERT(m_failureCb != nullptr);
 }
 
@@ -169,7 +169,7 @@
 {
   if (verifySignature(m_interest, trustedCert)) {
     NDN_LOG_TRACE_DEPTH("OK signature for interest `" << m_interest.getName() << "`");
-    m_successCb(m_interest);
+    this->afterSuccess(m_interest);
     BOOST_ASSERT(!m_hasOutcome);
     m_hasOutcome = true;
   }
@@ -183,7 +183,7 @@
 InterestValidationState::bypassValidation()
 {
   NDN_LOG_TRACE_DEPTH("Signature verification bypassed for interest `" << m_interest.getName() << "`");
-  m_successCb(m_interest);
+  this->afterSuccess(m_interest);
   BOOST_ASSERT(!m_hasOutcome);
   m_hasOutcome = true;
 }
diff --git a/src/security/v2/validation-state.hpp b/src/security/v2/validation-state.hpp
index 2181fae..821b85c 100644
--- a/src/security/v2/validation-state.hpp
+++ b/src/security/v2/validation-state.hpp
@@ -25,6 +25,7 @@
 #include "../../tag-host.hpp"
 #include "validation-callback.hpp"
 #include "certificate.hpp"
+#include "../../util/signal.hpp"
 
 #include <unordered_set>
 #include <list>
@@ -223,6 +224,9 @@
   const Interest&
   getOriginalInterest() const;
 
+public:
+  util::Signal<InterestValidationState, Interest> afterSuccess;
+
 private:
   void
   verifyOriginalPacket(const Certificate& trustedCert) final;
diff --git a/tests/unit-tests/security/v2/validation-policy-command-interest.t.cpp b/tests/unit-tests/security/v2/validation-policy-command-interest.t.cpp
new file mode 100644
index 0000000..293bf26
--- /dev/null
+++ b/tests/unit-tests/security/v2/validation-policy-command-interest.t.cpp
@@ -0,0 +1,467 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2013-2017 Regents of the University of California.
+ *
+ * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions).
+ *
+ * ndn-cxx library is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free Software
+ * Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * ndn-cxx library is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+ * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
+ *
+ * You should have received copies of the GNU General Public License and GNU Lesser
+ * General Public License along with ndn-cxx, e.g., in COPYING.md file.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * See AUTHORS.md for complete list of ndn-cxx authors and contributors.
+ */
+
+#include "security/v2/validation-policy-command-interest.hpp"
+#include "security/v2/validation-policy-simple-hierarchy.hpp"
+#include "security/v2/validation-policy-accept-all.hpp"
+#include "security/command-interest-signer.hpp"
+#include "security/signing-helpers.hpp"
+
+#include "boost-test.hpp"
+#include "validator-fixture.hpp"
+#include "unit-tests/make-interest-data.hpp"
+
+#include <boost/lexical_cast.hpp>
+#include <boost/mpl/vector.hpp>
+
+namespace ndn {
+namespace security {
+namespace v2 {
+namespace tests {
+
+using namespace ndn::tests;
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_AUTO_TEST_SUITE(V2)
+
+class DefaultOptions
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    return {};
+  }
+};
+
+template<class T, class InnerPolicy>
+class CommandInterestPolicyWrapper : public ValidationPolicyCommandInterest
+{
+public:
+  CommandInterestPolicyWrapper()
+    : ValidationPolicyCommandInterest(make_unique<InnerPolicy>(), T::getOptions())
+  {
+  }
+};
+
+template<class T, class InnerPolicy = ValidationPolicySimpleHierarchy>
+class ValidationPolicyCommandInterestFixture : public HierarchicalValidatorFixture<CommandInterestPolicyWrapper<T, InnerPolicy>>
+{
+public:
+  ValidationPolicyCommandInterestFixture()
+    : m_signer(this->m_keyChain)
+  {
+  }
+
+  Interest
+  makeCommandInterest(const Identity& identity)
+  {
+    return m_signer.makeCommandInterest(Name(identity.getName()).append("CMD"),
+                                        signingByIdentity(identity));
+  }
+
+public:
+  CommandInterestSigner m_signer;
+};
+
+BOOST_FIXTURE_TEST_SUITE(TestValidationPolicyCommandInterest, ValidationPolicyCommandInterestFixture<DefaultOptions>)
+
+BOOST_AUTO_TEST_SUITE(Accepts)
+
+BOOST_AUTO_TEST_CASE(Basic)
+{
+  auto i1 = makeCommandInterest(identity);
+  VALIDATE_SUCCESS(i1, "Should succeed (within grace period)");
+
+  advanceClocks(time::milliseconds(5));
+  auto i2 = makeCommandInterest(identity);
+  VALIDATE_SUCCESS(i2, "Should succeed (timestamp larger than previous)");
+}
+
+BOOST_AUTO_TEST_CASE(DataPassthru)
+{
+  Data d1("/Security/V2/ValidatorFixture/Sub1");
+  m_keyChain.sign(d1);
+  VALIDATE_SUCCESS(d1, "Should succeed (fallback on inner validation policy for data)");
+}
+
+BOOST_AUTO_TEST_SUITE_END() // Accepts
+
+BOOST_AUTO_TEST_SUITE(Rejects)
+
+BOOST_AUTO_TEST_CASE(NameTooShort)
+{
+  auto i1 = makeInterest("/name/too/short");
+  VALIDATE_FAILURE(*i1, "Should fail (name is too short)");
+}
+
+BOOST_AUTO_TEST_CASE(BadTimestamp)
+{
+  auto i1 = makeCommandInterest(identity);
+  setNameComponent(i1, command_interest::POS_TIMESTAMP, "not-timestamp");
+  VALIDATE_FAILURE(i1, "Should fail (timestamp is missing)");
+}
+
+BOOST_AUTO_TEST_CASE(BadSigInfo)
+{
+  auto i1 = makeCommandInterest(identity);
+  setNameComponent(i1, command_interest::POS_SIG_INFO, "not-SignatureInfo");
+  VALIDATE_FAILURE(i1, "Should fail (signature info is missing)");
+}
+
+BOOST_AUTO_TEST_CASE(MissingKeyLocator)
+{
+  auto i1 = makeCommandInterest(identity);
+  SignatureInfo sigInfo;
+  setNameComponent(i1, command_interest::POS_SIG_INFO,
+                   sigInfo.wireEncode().begin(), sigInfo.wireEncode().end());
+  VALIDATE_FAILURE(i1, "Should fail (missing KeyLocator)");
+}
+
+BOOST_AUTO_TEST_CASE(BadKeyLocatorType)
+{
+  auto i1 = makeCommandInterest(identity);
+  KeyLocator kl;
+  kl.setKeyDigest(makeBinaryBlock(tlv::KeyDigest, "\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD", 8));
+  SignatureInfo sigInfo;
+  sigInfo.setKeyLocator(kl);
+  setNameComponent(i1, command_interest::POS_SIG_INFO,
+                   sigInfo.wireEncode().begin(), sigInfo.wireEncode().end());
+  VALIDATE_FAILURE(i1, "Should fail (bad KeyLocator type)");
+}
+
+BOOST_AUTO_TEST_CASE(BadCertName)
+{
+  auto i1 = makeCommandInterest(identity);
+  KeyLocator kl;
+  kl.setName("/bad/cert/name");
+  SignatureInfo sigInfo;
+  sigInfo.setKeyLocator(kl);
+  setNameComponent(i1, command_interest::POS_SIG_INFO,
+                   sigInfo.wireEncode().begin(), sigInfo.wireEncode().end());
+  VALIDATE_FAILURE(i1, "Should fail (bad certificate name)");
+}
+
+BOOST_AUTO_TEST_CASE(InnerPolicyReject)
+{
+  auto i1 = makeCommandInterest(otherIdentity);
+  VALIDATE_FAILURE(i1, "Should fail (inner policy should reject)");
+}
+
+class GracePeriod15Sec
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = time::seconds(15);
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(TimestampOutOfGracePositive, ValidationPolicyCommandInterestFixture<GracePeriod15Sec>)
+{
+  auto i1 = makeCommandInterest(identity); // signed at 0s
+  advanceClocks(time::seconds(16)); // verifying at +16s
+  VALIDATE_FAILURE(i1, "Should fail (timestamp outside the grace period)");
+  rewindClockAfterValidation();
+
+  auto i2 = makeCommandInterest(identity); // signed at +16s
+  VALIDATE_SUCCESS(i2, "Should succeed");
+}
+
+BOOST_FIXTURE_TEST_CASE(TimestampOutOfGraceNegative, ValidationPolicyCommandInterestFixture<GracePeriod15Sec>)
+{
+  auto i1 = makeCommandInterest(identity); // signed at 0s
+  advanceClocks(time::seconds(1));
+  auto i2 = makeCommandInterest(identity); // signed at +1s
+  advanceClocks(time::seconds(1));
+  auto i3 = makeCommandInterest(identity); // signed at +2s
+
+  systemClock->advance(time::seconds(-18)); // verifying at -16s
+  VALIDATE_FAILURE(i1, "Should fail (timestamp outside the grace period)");
+  rewindClockAfterValidation();
+
+  // CommandInterestValidator should not remember i1's timestamp
+  VALIDATE_FAILURE(i2, "Should fail (timestamp outside the grace period)");
+  rewindClockAfterValidation();
+
+  // CommandInterestValidator should not remember i2's timestamp, and should treat i3 as initial
+  advanceClocks(time::seconds(18)); // verifying at +2s
+  VALIDATE_SUCCESS(i3, "Should succeed");
+}
+
+BOOST_AUTO_TEST_CASE(TimestampReorderEqual)
+{
+  auto i1 = makeCommandInterest(identity); // signed at 0s
+  VALIDATE_SUCCESS(i1, "Should succeed");
+
+  auto i2 = makeCommandInterest(identity); // signed at 0s
+  setNameComponent(i2, command_interest::POS_TIMESTAMP,
+                   i1.getName()[command_interest::POS_TIMESTAMP]);
+  VALIDATE_FAILURE(i2, "Should fail (timestamp reordered)");
+
+  advanceClocks(time::seconds(2));
+  auto i3 = makeCommandInterest(identity); // signed at +2s
+  VALIDATE_SUCCESS(i3, "Should succeed");
+}
+
+BOOST_AUTO_TEST_CASE(TimestampReorderNegative)
+{
+  auto i2 = makeCommandInterest(identity); // signed at 0ms
+  advanceClocks(time::milliseconds(200));
+  auto i3 = makeCommandInterest(identity); // signed at +200ms
+  advanceClocks(time::milliseconds(900));
+  auto i1 = makeCommandInterest(identity); // signed at +1100ms
+  advanceClocks(time::milliseconds(300));
+  auto i4 = makeCommandInterest(identity); // signed at +1400ms
+
+  systemClock->advance(time::milliseconds(-300)); // verifying at +1100ms
+  VALIDATE_SUCCESS(i1, "Should succeed");
+  rewindClockAfterValidation();
+
+  systemClock->advance(time::milliseconds(-1100)); // verifying at 0ms
+  VALIDATE_FAILURE(i2, "Should fail (timestamp reordered)");
+  rewindClockAfterValidation();
+
+  // CommandInterestValidator should not remember i2's timestamp
+  advanceClocks(time::milliseconds(200)); // verifying at +200ms
+  VALIDATE_FAILURE(i3, "Should fail (timestamp reordered)");
+  rewindClockAfterValidation();
+
+  advanceClocks(time::milliseconds(1200)); // verifying at 1400ms
+  VALIDATE_SUCCESS(i4, "Should succeed");
+}
+
+BOOST_AUTO_TEST_SUITE_END() // Rejects
+
+BOOST_AUTO_TEST_SUITE(Options)
+
+template<class T>
+class GracePeriod
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = time::seconds(T::value);
+    return options;
+  }
+};
+
+typedef boost::mpl::vector<
+  GracePeriod<boost::mpl::int_<0>>,
+  GracePeriod<boost::mpl::int_<-1>>
+> GraceNonPositiveValues;
+
+BOOST_FIXTURE_TEST_CASE_TEMPLATE(GraceNonPositive, GracePeriod, GraceNonPositiveValues,
+                                 ValidationPolicyCommandInterestFixture<GracePeriod>)
+{
+  auto i1 = this->makeCommandInterest(this->identity); // signed at 0ms
+  auto i2 = this->makeCommandInterest(this->subIdentity); // signed at 0ms
+  for (auto interest : {&i1, &i2}) {
+    setNameComponent(*interest, command_interest::POS_TIMESTAMP,
+                     name::Component::fromNumber(time::toUnixTimestamp(time::system_clock::now()).count()));
+  } // ensure timestamps are exactly 0ms
+
+  VALIDATE_SUCCESS(i1, "Should succeed when validating at 0ms");
+  this->rewindClockAfterValidation();
+
+  this->advanceClocks(time::milliseconds(1));
+  VALIDATE_FAILURE(i2, "Should fail when validating at 1ms");
+}
+
+class LimitedRecordsOptions
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = time::seconds(15);
+    options.maxRecords = 3;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(LimitedRecords, ValidationPolicyCommandInterestFixture<LimitedRecordsOptions>)
+{
+  Identity id1 = this->addSubCertificate("/Security/V2/ValidatorFixture/Sub1", identity);
+  this->cache.insert(id1.getDefaultKey().getDefaultCertificate());
+  Identity id2 = this->addSubCertificate("/Security/V2/ValidatorFixture/Sub2", identity);
+  this->cache.insert(id2.getDefaultKey().getDefaultCertificate());
+  Identity id3 = this->addSubCertificate("/Security/V2/ValidatorFixture/Sub3", identity);
+  this->cache.insert(id3.getDefaultKey().getDefaultCertificate());
+  Identity id4 = this->addSubCertificate("/Security/V2/ValidatorFixture/Sub4", identity);
+  this->cache.insert(id4.getDefaultKey().getDefaultCertificate());
+
+  auto i1 = makeCommandInterest(id2);
+  auto i2 = makeCommandInterest(id3);
+  auto i3 = makeCommandInterest(id4);
+  auto i00 = makeCommandInterest(id1); // signed at 0s
+  advanceClocks(time::seconds(1));
+  auto i01 = makeCommandInterest(id1); // signed at 1s
+  advanceClocks(time::seconds(1));
+  auto i02 = makeCommandInterest(id1); // signed at 2s
+
+  VALIDATE_SUCCESS(i00, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i02, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i1, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i2, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i3, "Should succeed, forgets identity id1");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i01, "Should succeed despite timestamp is reordered, because record has been evicted");
+}
+
+class UnlimitedRecordsOptions
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = time::seconds(15);
+    options.maxRecords = -1;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(UnlimitedRecords, ValidationPolicyCommandInterestFixture<UnlimitedRecordsOptions>)
+{
+  std::vector<Identity> identities;
+  for (int i = 0; i < 20; ++i) {
+    Identity id = this->addSubCertificate("/Security/V2/ValidatorFixture/Sub" + to_string(i), identity);
+    this->cache.insert(id.getDefaultKey().getDefaultCertificate());
+    identities.push_back(id);
+  }
+
+  auto i1 = makeCommandInterest(identities.at(0)); // signed at 0s
+  advanceClocks(time::seconds(1));
+  for (int i = 0; i < 20; ++i) {
+    auto i2 = makeCommandInterest(identities.at(i)); // signed at +1s
+
+    VALIDATE_SUCCESS(i2, "Should succeed");
+    rewindClockAfterValidation();
+  }
+  VALIDATE_FAILURE(i1, "Should fail (timestamp reorder)");
+}
+
+class ZeroRecordsOptions
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = time::seconds(15);
+    options.maxRecords = 0;
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(ZeroRecords, ValidationPolicyCommandInterestFixture<ZeroRecordsOptions>)
+{
+  auto i1 = makeCommandInterest(identity); // signed at 0s
+  advanceClocks(time::seconds(1));
+  auto i2 = makeCommandInterest(identity); // signed at +1s
+  VALIDATE_SUCCESS(i2, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i1, "Should succeed despite timestamp is reordered, because record isn't kept");
+}
+
+class LimitedRecordLifetimeOptions
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = time::seconds(400);
+    options.recordLifetime = time::seconds(300);
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(LimitedRecordLifetime, ValidationPolicyCommandInterestFixture<LimitedRecordLifetimeOptions>)
+{
+  auto i1 = makeCommandInterest(identity); // signed at 0s
+  advanceClocks(time::seconds(240));
+  auto i2 = makeCommandInterest(identity); // signed at +240s
+  advanceClocks(time::seconds(120));
+  auto i3 = makeCommandInterest(identity); // signed at +360s
+
+  systemClock->advance(time::seconds(-360)); // rewind system clock to 0s
+  VALIDATE_SUCCESS(i1, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i3, "Should succeed");
+  rewindClockAfterValidation();
+
+  advanceClocks(time::seconds(30), time::seconds(301)); // advance steady clock by 301s, and system clock to +301s
+  VALIDATE_SUCCESS(i2, "Should succeed despite timestamp is reordered, because record has been expired");
+}
+
+class ZeroRecordLifetimeOptions
+{
+public:
+  static ValidationPolicyCommandInterest::Options
+  getOptions()
+  {
+    ValidationPolicyCommandInterest::Options options;
+    options.gracePeriod = time::seconds(15);
+    options.recordLifetime = time::seconds::zero();
+    return options;
+  }
+};
+
+BOOST_FIXTURE_TEST_CASE(ZeroRecordLifetime, ValidationPolicyCommandInterestFixture<ZeroRecordLifetimeOptions>)
+{
+  auto i1 = makeCommandInterest(identity); // signed at 0s
+  advanceClocks(time::seconds(1));
+  auto i2 = makeCommandInterest(identity); // signed at +1s
+  VALIDATE_SUCCESS(i2, "Should succeed");
+  rewindClockAfterValidation();
+
+  VALIDATE_SUCCESS(i1, "Should succeed despite timestamp is reordered, because record has been expired");
+}
+
+BOOST_AUTO_TEST_SUITE_END() // Options
+
+BOOST_AUTO_TEST_SUITE_END() // TestValidationPolicyCommandInterest
+BOOST_AUTO_TEST_SUITE_END() // V2
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace v2
+} // namespace security
+} // namespace ndn
diff --git a/tests/unit-tests/security/v2/validator-fixture.hpp b/tests/unit-tests/security/v2/validator-fixture.hpp
index aa512ea..fb7edda 100644
--- a/tests/unit-tests/security/v2/validator-fixture.hpp
+++ b/tests/unit-tests/security/v2/validator-fixture.hpp
@@ -62,17 +62,17 @@
     std::string detailedInfo = msg + " on line " + to_string(line);
     size_t nCallbacks = 0;
     this->validator.validate(packet,
-                       [&] (const Packet&) {
-                         ++nCallbacks;
-                         BOOST_CHECK_MESSAGE(expectSuccess,
-                                             (expectSuccess ? "OK: " : "FAILED: ") + detailedInfo);
-                       },
-                       [&] (const Packet&, const ValidationError& error) {
-                         ++nCallbacks;
-                         BOOST_CHECK_MESSAGE(!expectSuccess,
-                                             (!expectSuccess ? "OK: " : "FAILED: ") + detailedInfo +
-                                             (expectSuccess ? " (" + boost::lexical_cast<std::string>(error) + ")" : ""));
-                       });
+      [&] (const Packet&) {
+        ++nCallbacks;
+        BOOST_CHECK_MESSAGE(expectSuccess,
+                            (expectSuccess ? "OK: " : "FAILED: ") + detailedInfo);
+      },
+      [&] (const Packet&, const ValidationError& error) {
+        ++nCallbacks;
+        BOOST_CHECK_MESSAGE(!expectSuccess,
+                            (!expectSuccess ? "OK: " : "FAILED: ") + detailedInfo +
+                            (expectSuccess ? " (" + boost::lexical_cast<std::string>(error) + ")" : ""));
+      });
 
     mockNetworkOperations();
     BOOST_CHECK_EQUAL(nCallbacks, 1);
@@ -86,7 +86,15 @@
           io.post(bind(processInterest, interest));
         }
       });
-    advanceClocks(time::milliseconds(250), 200);
+    advanceClocks(time::milliseconds(s_mockPeriod), s_mockTimes);
+  }
+
+  /** \brief undo clock advancement of mockNetworkOperations
+   */
+  void
+  rewindClockAfterValidation()
+  {
+    this->systemClock->advance(time::milliseconds(s_mockPeriod * s_mockTimes * -1));
   }
 
 public:
@@ -95,8 +103,18 @@
   Validator validator;
 
   CertificateCache cache;
+
+private:
+  const static int s_mockPeriod;
+  const static int s_mockTimes;
 };
 
+template<class ValidationPolicy, class CertificateFetcher>
+const int ValidatorFixture<ValidationPolicy, CertificateFetcher>::s_mockPeriod = 250;
+
+template<class ValidationPolicy, class CertificateFetcher>
+const int ValidatorFixture<ValidationPolicy, CertificateFetcher>::s_mockTimes = 200;
+
 template<class ValidationPolicy, class CertificateFetcher = CertificateFetcherFromNetwork>
 class HierarchicalValidatorFixture : public ValidatorFixture<ValidationPolicy, CertificateFetcher>
 {