security: CommandInterestValidator

refs #2376

Change-Id: Ia1d2231a4fb7ad130e11dd0d0dd52d8007149470
diff --git a/src/security/command-interest-validator.cpp b/src/security/command-interest-validator.cpp
new file mode 100644
index 0000000..ddba90e
--- /dev/null
+++ b/src/security/command-interest-validator.cpp
@@ -0,0 +1,206 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2013-2016 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 "command-interest-validator.hpp"
+#include "identity-certificate.hpp"
+#include <boost/lexical_cast.hpp>
+
+namespace ndn {
+namespace security {
+
+std::ostream&
+operator<<(std::ostream& os, CommandInterestValidator::ErrorCode error)
+{
+  switch (error) {
+    case CommandInterestValidator::ErrorCode::NONE:
+      return os << "OK";
+    case CommandInterestValidator::ErrorCode::NAME_TOO_SHORT:
+      return os << "command Interest name is too short";
+    case CommandInterestValidator::ErrorCode::BAD_TIMESTAMP:
+      return os << "cannot parse timestamp";
+    case CommandInterestValidator::ErrorCode::BAD_SIG_INFO:
+      return os << "cannot parse SignatureInfo";
+    case CommandInterestValidator::ErrorCode::MISSING_KEY_LOCATOR:
+      return os << "KeyLocator is missing";
+    case CommandInterestValidator::ErrorCode::BAD_KEY_LOCATOR_TYPE:
+      return os << "KeyLocator type is not Name";
+    case CommandInterestValidator::ErrorCode::BAD_CERT_NAME:
+      return os << "cannot parse certificate name";
+    case CommandInterestValidator::ErrorCode::TIMESTAMP_OUT_OF_GRACE:
+      return os << "timestamp is out of grace period";
+    case CommandInterestValidator::ErrorCode::TIMESTAMP_REORDER:
+      return os << "timestamp is less than or equal to last timestamp";
+  }
+  return os;
+}
+
+static void
+invokeReject(const OnInterestValidationFailed& reject, const Interest& interest,
+             CommandInterestValidator::ErrorCode error)
+{
+  reject(interest.shared_from_this(), boost::lexical_cast<std::string>(error));
+}
+
+CommandInterestValidator::CommandInterestValidator(unique_ptr<Validator> inner,
+                                                   const Options& options)
+  : m_inner(std::move(inner))
+  , m_options(options)
+  , m_index(m_container.get<0>())
+  , m_queue(m_container.get<1>())
+{
+  if (m_inner == nullptr) {
+    BOOST_THROW_EXCEPTION(std::invalid_argument("inner validator is nullptr"));
+  }
+
+  m_options.gracePeriod = std::max(m_options.gracePeriod, time::nanoseconds::zero());
+}
+
+void
+CommandInterestValidator::checkPolicy(const Interest& interest, int nSteps,
+                                      const OnInterestValidated& accept,
+                                      const OnInterestValidationFailed& reject,
+                                      std::vector<shared_ptr<ValidationRequest>>& nextSteps)
+{
+  BOOST_ASSERT(nSteps == 0);
+  this->cleanup();
+
+  Name keyName;
+  uint64_t timestamp;
+  ErrorCode res = this->parseCommandInterest(interest, keyName, timestamp);
+  if (res != ErrorCode::NONE) {
+    return invokeReject(reject, interest, res);
+  }
+
+  time::system_clock::TimePoint receiveTime = time::system_clock::now();
+
+  m_inner->validate(interest,
+    [=] (const shared_ptr<const Interest>& interest) {
+      ErrorCode res = this->checkTimestamp(keyName, timestamp, receiveTime);
+      if (res != ErrorCode::NONE) {
+        return invokeReject(reject, *interest, res);
+      }
+      accept(interest);
+    }, reject);
+}
+
+void
+CommandInterestValidator::cleanup()
+{
+  time::steady_clock::TimePoint expiring = time::steady_clock::now() - m_options.timestampTtl;
+
+  while ((!m_queue.empty() && m_queue.front().lastRefreshed <= expiring) ||
+         (m_options.maxTimestamps >= 0 &&
+          m_queue.size() > static_cast<size_t>(m_options.maxTimestamps))) {
+    m_queue.pop_front();
+  }
+}
+
+CommandInterestValidator::ErrorCode
+CommandInterestValidator::parseCommandInterest(const Interest& interest, Name& keyName,
+                                               uint64_t& timestamp) const
+{
+  const Name& name = interest.getName();
+  if (name.size() < signed_interest::MIN_LENGTH) {
+    return ErrorCode::NAME_TOO_SHORT;
+  }
+
+  const name::Component& timestampComp = name[signed_interest::POS_TIMESTAMP];
+  if (!timestampComp.isNumber()) {
+    return ErrorCode::BAD_TIMESTAMP;
+  }
+  timestamp = timestampComp.toNumber();
+
+  SignatureInfo sig;
+  try {
+    sig.wireDecode(name[signed_interest::POS_SIG_INFO].blockFromValue());
+  }
+  catch (const tlv::Error&) {
+    return ErrorCode::BAD_SIG_INFO;
+  }
+
+  if (!sig.hasKeyLocator()) {
+    return ErrorCode::MISSING_KEY_LOCATOR;
+  }
+
+  const KeyLocator& keyLocator = sig.getKeyLocator();
+  if (keyLocator.getType() != KeyLocator::KeyLocator_Name) {
+    return ErrorCode::BAD_KEY_LOCATOR_TYPE;
+  }
+
+  try {
+    keyName = IdentityCertificate::certificateNameToPublicKeyName(keyLocator.getName());
+  }
+  catch (const IdentityCertificate::Error&) {
+    return ErrorCode::BAD_CERT_NAME;
+  }
+
+  return ErrorCode::NONE;
+}
+
+CommandInterestValidator::ErrorCode
+CommandInterestValidator::checkTimestamp(const Name& keyName, uint64_t timestamp,
+                                         time::system_clock::TimePoint receiveTime)
+{
+  time::steady_clock::TimePoint now = time::steady_clock::now();
+
+  // try to insert new record
+  Queue::iterator i = m_queue.end();
+  bool isNew = false;
+  std::tie(i, isNew) = m_queue.push_back({keyName, timestamp, now});
+
+  if (isNew) {
+    // check grace period
+    time::system_clock::TimePoint sigTime = time::fromUnixTimestamp(time::milliseconds(timestamp));
+    if (time::abs(sigTime - receiveTime) > m_options.gracePeriod) {
+      // out of grace period, delete new record
+      m_queue.erase(i);
+      return ErrorCode::TIMESTAMP_OUT_OF_GRACE;
+    }
+  }
+  else {
+    BOOST_ASSERT(i->keyName == keyName);
+
+    // compare timestamp with last timestamp
+    if (timestamp <= i->timestamp) {
+      return ErrorCode::TIMESTAMP_REORDER;
+    }
+
+    // set lastRefreshed field, and move to queue tail
+    m_queue.erase(i);
+    isNew = m_queue.push_back({keyName, timestamp, now}).second;
+    BOOST_ASSERT(isNew);
+  }
+
+  return ErrorCode::NONE;
+}
+
+void
+CommandInterestValidator::checkPolicy(const Data& data, int nSteps,
+                                      const OnDataValidated& accept,
+                                      const OnDataValidationFailed& reject,
+                                      std::vector<shared_ptr<ValidationRequest>>& nextSteps)
+{
+  BOOST_ASSERT(nSteps == 0);
+  m_inner->validate(data, accept, reject);
+}
+
+} // namespace security
+} // namespace ndn
diff --git a/src/security/command-interest-validator.hpp b/src/security/command-interest-validator.hpp
new file mode 100644
index 0000000..e789f09
--- /dev/null
+++ b/src/security/command-interest-validator.hpp
@@ -0,0 +1,194 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2013-2016 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_COMMAND_INTEREST_VALIDATOR_HPP
+#define NDN_SECURITY_COMMAND_INTEREST_VALIDATOR_HPP
+
+#include "validator.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 {
+
+/** \brief a validator for stop-and-wait command Interests
+ *  \sa https://redmine.named-data.net/projects/ndn-cxx/wiki/CommandInterest
+ *
+ *  This validator checks timestamp field of a stop-and-wait command Interest.
+ *  Signed Interest validation and Data validation requests are delegated to an inner validator.
+ */
+class CommandInterestValidator : public Validator
+{
+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 to record 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 maxTimestamps = 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 timestampTtl = time::hours(1);
+
+  };
+
+  /** \brief error codes
+   *  \todo #1872 assign numeric codes to these errors
+   */
+  enum class ErrorCode {
+    NONE = 0,
+    NAME_TOO_SHORT,
+    BAD_TIMESTAMP,
+    BAD_SIG_INFO,
+    MISSING_KEY_LOCATOR,
+    BAD_KEY_LOCATOR_TYPE,
+    BAD_CERT_NAME,
+    TIMESTAMP_OUT_OF_GRACE,
+    TIMESTAMP_REORDER
+  };
+
+  /** \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 inner is nullptr
+   */
+  explicit
+  CommandInterestValidator(unique_ptr<Validator> inner,
+                           const Options& options = Options());
+
+protected:
+  /** \brief validate command Interest
+   *
+   *  This function executes the following validation procedure:
+   *
+   *  1. parse the Interest as a command Interest, and extract the public key name
+   *  2. invoke inner validation to verify the signed Interest
+   *  3. classify the command Interest as either initial or subsequent,
+   *     and check the timestamp accordingly
+   *  4. record the timestamp as last timestamp of the public key name
+   *
+   *  The validation request is rejected if any step in this procedure fails.
+   */
+  virtual void
+  checkPolicy(const Interest& interest, int nSteps,
+              const OnInterestValidated& accept,
+              const OnInterestValidationFailed& reject,
+              std::vector<shared_ptr<ValidationRequest>>& nextSteps) override;
+
+  /** \brief validate Data
+   *
+   *  The validation request is redirected to the inner validator.
+   */
+  virtual void
+  checkPolicy(const Data& data, int nSteps,
+              const OnDataValidated& accept,
+              const OnDataValidationFailed& reject,
+              std::vector<shared_ptr<ValidationRequest>>& nextSteps) override;
+
+private:
+  void
+  cleanup();
+
+  ErrorCode
+  parseCommandInterest(const Interest& interest, Name& keyName, uint64_t& timestamp) const;
+
+  ErrorCode
+  checkTimestamp(const Name& keyName, uint64_t timestamp,
+                 time::system_clock::TimePoint receiveTime);
+
+private:
+  unique_ptr<Validator> m_inner;
+  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;
+};
+
+std::ostream&
+operator<<(std::ostream& os, CommandInterestValidator::ErrorCode error);
+
+} // namespace security
+} // namespace ndn
+
+
+#endif // NDN_SECURITY_COMMAND_INTEREST_VALIDATOR_HPP
diff --git a/src/util/time.hpp b/src/util/time.hpp
index 976a27b..f8bcabc 100644
--- a/src/util/time.hpp
+++ b/src/util/time.hpp
@@ -43,6 +43,18 @@
 
 using boost::chrono::duration_cast;
 
+/** \return the absolute value of the duration d
+ *  \note The function does not participate in the overload resolution
+ *        unless std::numeric_limits<Rep>::is_signed is true.
+ */
+template<typename Rep, typename Period,
+         typename = typename std::enable_if<duration<Rep, Period>::min() < duration<Rep, Period>::zero()>::type>
+constexpr duration<Rep, Period>
+abs(duration<Rep, Period> d)
+{
+  return d >= d.zero() ? d : -d;
+}
+
 /**
  * \brief System clock
  *
diff --git a/tests/unit-tests/security/command-interest-validator.t.cpp b/tests/unit-tests/security/command-interest-validator.t.cpp
new file mode 100644
index 0000000..6173274
--- /dev/null
+++ b/tests/unit-tests/security/command-interest-validator.t.cpp
@@ -0,0 +1,433 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2013-2016 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/command-interest-validator.hpp"
+#include "security/signing-helpers.hpp"
+#include <boost/lexical_cast.hpp>
+
+#include "boost-test.hpp"
+#include "../../dummy-validator.hpp"
+#include "../identity-management-time-fixture.hpp"
+#include "../make-interest-data.hpp"
+
+namespace ndn {
+namespace security {
+namespace tests {
+
+using namespace ndn::tests;
+
+class CommandInterestValidatorFixture : public IdentityManagementTimeFixture
+{
+protected:
+  CommandInterestValidatorFixture()
+  {
+    this->initialize(CommandInterestValidator::Options{});
+  }
+
+  void
+  initialize(const CommandInterestValidator::Options& options)
+  {
+    auto inner = make_unique<DummyValidator>();
+    this->inner = inner.get();
+    this->validator = make_unique<CommandInterestValidator>(std::move(inner), options);
+  }
+
+  Name
+  makeIdentity(int identity)
+  {
+    Name name("/localhost/CommandInterestValidatorIdentity");
+    name.appendSequenceNumber(identity);
+    BOOST_REQUIRE(m_keyChain.doesIdentityExist(name) || this->addIdentity(name));
+    return name;
+  }
+
+  shared_ptr<Interest>
+  makeCommandInterest(int identity = 0)
+  {
+    auto interest = makeInterest("/CommandInterestPrefix");
+    m_keyChain.sign(*interest, signingByIdentity(makeIdentity(identity)));
+    BOOST_TEST_MESSAGE("makeCommandInterest " << interest->getName());
+    return interest;
+  }
+
+  /** \brief check that validator accepts interest
+   *  \param interest to be validated
+   */
+  void
+  assertAccept(const Interest& interest)
+  {
+    BOOST_TEST_MESSAGE("assertAccept " << interest.getName());
+    int nAccepts = 0;
+    validator->validate(interest,
+      [&nAccepts] (const shared_ptr<const Interest>&) { ++nAccepts; },
+      [] (const shared_ptr<const Interest>&, const std::string& msg) {
+        BOOST_ERROR("validation request should succeed but fails with: " << msg);
+      });
+    BOOST_CHECK_EQUAL(nAccepts, 1);
+  }
+
+  /** \brief check that validator rejects interest
+   *  \param interest to be validated
+   *  \param error if not NONE, further check the error code matches \p error
+   *               if NONE, error code is not checked
+   */
+  void
+  assertReject(const Interest& interest, CommandInterestValidator::ErrorCode error)
+  {
+    BOOST_TEST_MESSAGE("assertReject " << interest.getName());
+    int nRejects = 0;
+    validator->validate(interest,
+      [] (const shared_ptr<const Interest>&) {
+        BOOST_ERROR("validation request should fail but succeeds");
+      },
+      [&nRejects, error] (const shared_ptr<const Interest>&, const std::string& msg) {
+        ++nRejects;
+        if (error != CommandInterestValidator::ErrorCode::NONE) {
+          BOOST_CHECK_EQUAL(msg, boost::lexical_cast<std::string>(error));
+        }
+      });
+    BOOST_CHECK_EQUAL(nRejects, 1);
+  }
+
+protected:
+  DummyValidator* inner;
+  unique_ptr<CommandInterestValidator> validator;
+};
+
+template<typename...A>
+void
+setNameComponent(Name& name, ssize_t index, const A& ...a)
+{
+  Name name2 = name.getPrefix(index);
+  name2.append(name::Component(a...));
+  name2.append(name.getSubName(index + 1));
+  name = name2;
+}
+
+BOOST_AUTO_TEST_SUITE(Security)
+BOOST_FIXTURE_TEST_SUITE(TestCommandInterestValidator, CommandInterestValidatorFixture)
+
+BOOST_AUTO_TEST_CASE(Normal)
+{
+  auto i1 = makeCommandInterest();
+  assertAccept(*i1);
+
+  advanceClocks(time::milliseconds(5));
+  auto i2 = makeCommandInterest();
+  assertAccept(*i2);
+
+  advanceClocks(time::seconds(2));
+  auto i3 = makeCommandInterest();
+  assertAccept(*i3);
+}
+
+BOOST_AUTO_TEST_CASE(DataPassthru)
+{
+  auto d1 = makeData("/data");
+  int nAccepts = 0;
+  validator->validate(*d1,
+    [&nAccepts] (const shared_ptr<const Data>&) { ++nAccepts; },
+    [] (const shared_ptr<const Data>&, const std::string& msg) {
+      BOOST_ERROR("validation request should succeed but fails with " << msg);
+    });
+  BOOST_CHECK_EQUAL(nAccepts, 1);
+}
+
+BOOST_AUTO_TEST_SUITE(Rejects)
+
+BOOST_AUTO_TEST_CASE(NameTooShort)
+{
+  auto i1 = makeInterest("/name/too/short");
+  assertReject(*i1, CommandInterestValidator::ErrorCode::NAME_TOO_SHORT);
+}
+
+BOOST_AUTO_TEST_CASE(BadTimestamp)
+{
+  auto i1 = makeCommandInterest();
+  Name n1 = i1->getName();
+  setNameComponent(n1, signed_interest::POS_TIMESTAMP, "not-timestamp");
+  i1->setName(n1);
+  assertReject(*i1, CommandInterestValidator::ErrorCode::BAD_TIMESTAMP);
+}
+
+BOOST_AUTO_TEST_CASE(BadSigInfo)
+{
+  auto i1 = makeCommandInterest();
+  Name n1 = i1->getName();
+  setNameComponent(n1, signed_interest::POS_SIG_INFO, "not-SignatureInfo");
+  i1->setName(n1);
+  assertReject(*i1, CommandInterestValidator::ErrorCode::BAD_SIG_INFO);
+}
+
+BOOST_AUTO_TEST_CASE(MissingKeyLocator)
+{
+  auto i1 = makeCommandInterest();
+  Name n1 = i1->getName();
+  SignatureInfo sigInfo;
+  setNameComponent(n1, signed_interest::POS_SIG_INFO,
+                   sigInfo.wireEncode().begin(), sigInfo.wireEncode().end());
+  i1->setName(n1);
+  assertReject(*i1, CommandInterestValidator::ErrorCode::MISSING_KEY_LOCATOR);
+}
+
+BOOST_AUTO_TEST_CASE(BadKeyLocatorType)
+{
+  auto i1 = makeCommandInterest();
+  Name n1 = i1->getName();
+  KeyLocator kl;
+  kl.setKeyDigest(makeBinaryBlock(tlv::KeyDigest, "\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD", 8));
+  SignatureInfo sigInfo;
+  sigInfo.setKeyLocator(kl);
+  setNameComponent(n1, signed_interest::POS_SIG_INFO,
+                   sigInfo.wireEncode().begin(), sigInfo.wireEncode().end());
+  i1->setName(n1);
+  assertReject(*i1, CommandInterestValidator::ErrorCode::BAD_KEY_LOCATOR_TYPE);
+}
+
+BOOST_AUTO_TEST_CASE(BadCertName)
+{
+  auto i1 = makeCommandInterest();
+  Name n1 = i1->getName();
+  KeyLocator kl;
+  kl.setName("/bad/cert/name");
+  SignatureInfo sigInfo;
+  sigInfo.setKeyLocator(kl);
+  setNameComponent(n1, signed_interest::POS_SIG_INFO,
+                   sigInfo.wireEncode().begin(), sigInfo.wireEncode().end());
+  i1->setName(n1);
+  assertReject(*i1, CommandInterestValidator::ErrorCode::BAD_CERT_NAME);
+}
+
+BOOST_AUTO_TEST_CASE(InnerReject)
+{
+  inner->setResult(false);
+  auto i1 = makeCommandInterest();
+  assertReject(*i1, CommandInterestValidator::ErrorCode::NONE);
+}
+
+BOOST_AUTO_TEST_CASE(TimestampOutOfGracePositive)
+{
+  CommandInterestValidator::Options options;
+  options.gracePeriod = time::seconds(15);
+  initialize(options);
+
+  auto i1 = makeCommandInterest(); // signed at 0s
+  advanceClocks(time::seconds(16)); // verifying at +16s
+  assertReject(*i1, CommandInterestValidator::ErrorCode::TIMESTAMP_OUT_OF_GRACE);
+
+  auto i2 = makeCommandInterest(); // signed at +16s
+  assertAccept(*i2); // verifying at +16s
+}
+
+BOOST_AUTO_TEST_CASE(TimestampOutOfGraceNegative)
+{
+  CommandInterestValidator::Options options;
+  options.gracePeriod = time::seconds(15);
+  initialize(options);
+
+  auto i1 = makeCommandInterest(); // signed at 0s
+  advanceClocks(time::seconds(1));
+  auto i2 = makeCommandInterest(); // signed at +1s
+  advanceClocks(time::seconds(1));
+  auto i3 = makeCommandInterest(); // signed at +2s
+
+  systemClock->advance(time::seconds(-18)); // verifying at -16s
+  assertReject(*i1, CommandInterestValidator::ErrorCode::TIMESTAMP_OUT_OF_GRACE);
+
+  // CommandInterestValidator should not remember i1's timestamp
+  assertReject(*i2, CommandInterestValidator::ErrorCode::TIMESTAMP_OUT_OF_GRACE);
+
+  // CommandInterestValidator should not remember i2's timestamp, and should treat i3 as initial
+  advanceClocks(time::seconds(18)); // verifying at +2s
+  assertAccept(*i3);
+}
+
+BOOST_AUTO_TEST_CASE(TimestampReorderEqual)
+{
+  auto i1 = makeCommandInterest(); // signed at 0s
+  assertAccept(*i1);
+
+  auto i2 = makeCommandInterest();
+  Name n1 = i1->getName();
+  Name n2 = i2->getName();
+  setNameComponent(n2, signed_interest::POS_TIMESTAMP,
+                   n1[signed_interest::POS_TIMESTAMP]);
+  i2->setName(n2); // signed at 0s
+  assertReject(*i2, CommandInterestValidator::ErrorCode::TIMESTAMP_REORDER);
+
+  advanceClocks(time::seconds(2));
+  auto i3 = makeCommandInterest(); // signed at +2s
+  assertAccept(*i3);
+}
+
+BOOST_AUTO_TEST_CASE(TimestampReorderNegative)
+{
+  auto i2 = makeCommandInterest(); // signed at 0ms
+  advanceClocks(time::milliseconds(200));
+  auto i3 = makeCommandInterest(); // signed at +200ms
+  advanceClocks(time::milliseconds(900));
+  auto i1 = makeCommandInterest(); // signed at +1100ms
+  advanceClocks(time::milliseconds(300));
+  auto i4 = makeCommandInterest(); // signed at +1400ms
+
+  systemClock->advance(time::milliseconds(-300)); // verifying at +1100ms
+  assertAccept(*i1);
+
+  systemClock->advance(time::milliseconds(-1100)); // verifying at 0ms
+  assertReject(*i2, CommandInterestValidator::ErrorCode::TIMESTAMP_REORDER);
+
+  // CommandInterestValidator should not remember i2's timestamp
+  advanceClocks(time::milliseconds(200)); // verifying at +200ms
+  assertReject(*i3, CommandInterestValidator::ErrorCode::TIMESTAMP_REORDER);
+
+  advanceClocks(time::milliseconds(1200)); // verifying at 1400ms
+  assertAccept(*i4);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // Rejects
+
+BOOST_AUTO_TEST_SUITE(Options)
+
+typedef boost::mpl::vector<
+  boost::mpl::int_<0>,
+  boost::mpl::int_<-1>
+> GraceNonPositiveValues;
+
+BOOST_AUTO_TEST_CASE_TEMPLATE(GraceNonPositive, VALUE, GraceNonPositiveValues)
+{
+  CommandInterestValidator::Options options;
+  options.gracePeriod = time::seconds(VALUE::value);
+  initialize(options);
+
+  auto i1 = makeCommandInterest(1); // signed at 0ms
+  auto i2 = makeCommandInterest(2); // signed at 0ms
+  for (auto interest : {i1, i2}) {
+    Name name = interest->getName();
+    setNameComponent(name, signed_interest::POS_TIMESTAMP,
+                     name::Component::fromNumber(time::toUnixTimestamp(time::system_clock::now()).count()));
+    interest->setName(name);
+  } // ensure timestamps are exactly 0ms
+
+  assertAccept(*i1); // verifying at 0ms
+
+  advanceClocks(time::milliseconds(1));
+  assertReject(*i2, CommandInterestValidator::ErrorCode::TIMESTAMP_OUT_OF_GRACE); // verifying at 1ms
+}
+
+BOOST_AUTO_TEST_CASE(TimestampsLimited)
+{
+  CommandInterestValidator::Options options;
+  options.gracePeriod = time::seconds(15);
+  options.maxTimestamps = 3;
+  initialize(options);
+
+  auto i1 = makeCommandInterest(1);
+  auto i2 = makeCommandInterest(2);
+  auto i3 = makeCommandInterest(3);
+  auto i00 = makeCommandInterest(0); // signed at 0s
+  advanceClocks(time::seconds(1));
+  auto i01 = makeCommandInterest(0); // signed at 1s
+  advanceClocks(time::seconds(1));
+  auto i02 = makeCommandInterest(0); // signed at 2s
+
+  assertAccept(*i00);
+  assertAccept(*i02);
+  assertAccept(*i1);
+  assertAccept(*i2);
+  assertAccept(*i3); // forgets identity 0
+  assertAccept(*i01); // accepted despite timestamp is reordered, because record has been evicted
+}
+
+BOOST_AUTO_TEST_CASE(TimestampsUnlimited)
+{
+  CommandInterestValidator::Options options;
+  options.gracePeriod = time::seconds(15);
+  options.maxTimestamps = -1;
+  initialize(options);
+
+  auto i1 = makeCommandInterest(0); // signed at 0s
+  advanceClocks(time::seconds(1));
+  for (int identity = 0; identity < 20; ++identity) {
+    auto i2 = makeCommandInterest(identity); // signed at +1s
+    assertAccept(*i2);
+  }
+  assertReject(*i1, CommandInterestValidator::ErrorCode::TIMESTAMP_REORDER);
+}
+
+BOOST_AUTO_TEST_CASE(TimestampsDisabled)
+{
+  CommandInterestValidator::Options options;
+  options.gracePeriod = time::seconds(15);
+  options.maxTimestamps = 0;
+  initialize(options);
+
+  auto i1 = makeCommandInterest(); // signed at 0s
+  advanceClocks(time::seconds(1));
+  auto i2 = makeCommandInterest(); // signed at +1s
+  assertAccept(*i2);
+
+  assertAccept(*i1); // accepted despite timestamp is reordered, because record isn't kept
+}
+
+BOOST_AUTO_TEST_CASE(TtlLimited)
+{
+  CommandInterestValidator::Options options;
+  options.gracePeriod = time::seconds(120);
+  options.timestampTtl = time::seconds(300);
+  initialize(options);
+
+  auto i1 = makeCommandInterest(); // signed at 0s
+  advanceClocks(time::seconds(240));
+  auto i2 = makeCommandInterest(); // signed at +240s
+  advanceClocks(time::seconds(120));
+  auto i3 = makeCommandInterest(); // signed at +360s
+
+  systemClock->advance(time::seconds(-360)); // rewind system clock to 0s
+  assertAccept(*i1);
+  assertAccept(*i3);
+
+  advanceClocks(time::seconds(30), time::seconds(301)); // advance steady clock by 301s, and system clock to +301s
+  assertAccept(*i2); // accepted despite timestamp is reordered, because record has been expired
+}
+
+BOOST_AUTO_TEST_CASE(TtlZero)
+{
+  CommandInterestValidator::Options options;
+  options.gracePeriod = time::seconds(15);
+  options.timestampTtl = time::seconds::zero();
+  initialize(options);
+
+  auto i1 = makeCommandInterest(); // signed at 0s
+  advanceClocks(time::seconds(1));
+  auto i2 = makeCommandInterest(); // signed at +1s
+  assertAccept(*i2);
+
+  assertAccept(*i1); // accepted despite timestamp is reordered, because record has been expired
+}
+
+BOOST_AUTO_TEST_SUITE_END() // Options
+
+BOOST_AUTO_TEST_SUITE_END() // TestCommandInterestValidator
+BOOST_AUTO_TEST_SUITE_END() // Security
+
+} // namespace tests
+} // namespace security
+} // namespace ndn