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;