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
*