catalog: implement trust model
Change-Id: I28a9ead71357eb180316cd1707138518b5fb8b88
refs: #3020
diff --git a/.jenkins.d/10-ndn-cxx.sh b/.jenkins.d/10-ndn-cxx.sh
index 3ca0932..089ef14 100755
--- a/.jenkins.d/10-ndn-cxx.sh
+++ b/.jenkins.d/10-ndn-cxx.sh
@@ -23,9 +23,11 @@
pushd ndn-cxx >/dev/null
-./waf configure -j1 --color=yes --without-osx-keychain --disable-static --enable-shared
+./waf configure -j1 --color=yes --without-osx-keychain
./waf -j1 --color=yes
sudo ./waf install -j1 --color=yes
+(echo -e '/usr/local/lib\n/usr/local/lib64' | sudo tee /etc/ld.so.conf.d/ndn-cxx.conf) || true
+sudo ldconfig || true
popd >/dev/null
popd >/dev/null
diff --git a/.jenkins.d/11-chronoSync.sh b/.jenkins.d/11-chronoSync.sh
index 50318d4..4b0ee2b 100755
--- a/.jenkins.d/11-chronoSync.sh
+++ b/.jenkins.d/11-chronoSync.sh
@@ -27,6 +27,7 @@
/usr/local/lib32/pkgconfig:\
/usr/local/lib64/pkgconfig
+sudo ldconfig || true
./waf configure -j1 --color=yes
./waf -j1 --color=yes
sudo ./waf install -j1 --color=yes
diff --git a/catalog.conf.sample.in b/catalog.conf.sample.in
index 64d2558..11b11d0 100644
--- a/catalog.conf.sample.in
+++ b/catalog.conf.sample.in
@@ -31,10 +31,32 @@
; ; Set the identity that signs published data
; signingId ndn:/cmip5/test/publish/identity
- ; ; The published_file_security section contains the rules for the adapter to verify the
+ ; ; The security section contains the rules for the adapter to verify the
; ; published files indeed come from a valid publisher.
- ; published_file_security
+ ; security
; {
+ ; rule
+ ; {
+ ; id "NDN Hierarchy Test Rule"
+ ; for data ; rule for Data (to validate NDN certificates)
+ ; filter
+ ; {
+ ; type name ; condition on data name
+ ; regex ^(<>*)$
+ ; }
+ ; checker
+ ; {
+ ; type hierarchical ; the certificate name of the signing key and
+ ; ; the data name must follow the hierarchical model
+ ; sig-type rsa-sha256 ; data must have a rsa-sha256 signature
+ ; }
+ ; }
+ ; trust-anchor
+ ; {
+ ; type file
+ ; file-name /directory/to/the/root.ndncert ; the file name, by default this file should be
+ ; ; in same folder as this config file.
+ ; }
; }
; The database section contains settings of database
diff --git a/catalog/src/publish/publish-adapter.hpp b/catalog/src/publish/publish-adapter.hpp
index 9d29434..492bcff 100644
--- a/catalog/src/publish/publish-adapter.hpp
+++ b/catalog/src/publish/publish-adapter.hpp
@@ -21,6 +21,7 @@
#include "util/catalog-adapter.hpp"
#include "util/mysql-util.hpp"
+#include <mysql/mysql.h>
#include <json/reader.h>
#include <json/value.h>
@@ -31,8 +32,7 @@
#include <ndn-cxx/interest-filter.hpp>
#include <ndn-cxx/name.hpp>
#include <ndn-cxx/security/key-chain.hpp>
-#include <ndn-cxx/security/validator.hpp>
-#include "mysql/mysql.h"
+#include <ndn-cxx/security/validator-config.hpp>
#include <memory>
#include <string>
@@ -41,6 +41,7 @@
namespace atmos {
namespace publish {
+
/**
* PublishAdapter handles the Publish usecases for the catalog
*/
@@ -107,13 +108,23 @@
void
setFilters();
+ /**
+ * Function to validate publication changes against the trust model, which is, all file
+ * names must be under the publisher's prefix. This function should be called by a callback
+ * function invoked by validator
+ *
+ * @param data: received data from the publisher
+ */
+ bool
+ validatePublicationChanges(const std::shared_ptr<const ndn::Data>& data);
+
protected:
typedef std::unordered_map<ndn::Name, const ndn::RegisteredPrefixId*> RegisteredPrefixList;
// Prefix for ChronoSync
ndn::Name m_syncPrefix;
// Handle to the Catalog's database
std::shared_ptr<DatabaseHandler> m_databaseHandler;
- std::shared_ptr<ndn::Validator> m_validaor;
+ std::unique_ptr<ndn::ValidatorConfig> m_publishValidator;
RegisteredPrefixList m_registeredPrefixList;
};
@@ -130,13 +141,14 @@
PublishAdapter<DatabaseHandler>::setFilters()
{
ndn::Name publishPrefix = ndn::Name(m_prefix).append("publish");
- m_registeredPrefixList[publishPrefix] = m_face->setInterestFilter(publishPrefix,
- bind(&publish::PublishAdapter<DatabaseHandler>::onPublishInterest,
- this, _1, _2),
- bind(&publish::PublishAdapter<DatabaseHandler>::onRegisterSuccess,
- this, _1),
- bind(&publish::PublishAdapter<DatabaseHandler>::onRegisterFailure,
- this, _1, _2));
+ m_registeredPrefixList[publishPrefix] =
+ m_face->setInterestFilter(publishPrefix,
+ bind(&PublishAdapter<DatabaseHandler>::onPublishInterest,
+ this, _1, _2),
+ bind(&publish::PublishAdapter<DatabaseHandler>::onRegisterSuccess,
+ this, _1),
+ bind(&publish::PublishAdapter<DatabaseHandler>::onRegisterFailure,
+ this, _1, _2));
}
template <typename DatabaseHandler>
@@ -184,9 +196,12 @@
" in \"publish\" section");
}
}
-
- // @todo: parse the published_file_security section
-
+ else if (item->first == "security") {
+ // when use, the validator must specify the callback func to handle the validated data
+ // it should be called when the Data packet that contains the published file names is received
+ m_publishValidator.reset(new ndn::ValidatorConfig(m_face.get()));
+ m_publishValidator->load(item->second, filename);
+ }
else if (item->first == "database") {
const util::ConfigSection& databaseSection = item->second;
for (auto subItem = databaseSection.begin();
@@ -279,7 +294,41 @@
PublishAdapter<DatabaseHandler>::onPublishedData(const ndn::Interest& interest,
const ndn::Data& data)
{
- // @todo handle publishing the data
+ // @todo handle data publication
+}
+
+template<typename DatabaseHandler>
+bool
+PublishAdapter<DatabaseHandler>::validatePublicationChanges(const std::shared_ptr<const ndn::Data>& data)
+{
+ // The data name must be "/<publisher-prefix>/<nonce>"
+ // the prefix is the data name removes the last component
+ ndn::Name publisherPrefix = data->getName().getPrefix(-1);
+
+ const std::string payload(reinterpret_cast<const char*>(data->getContent().value()),
+ data->getContent().value_size());
+ Json::Value parsedFromString;
+ Json::Reader reader;
+ if (!reader.parse(payload, parsedFromString)) {
+ // parse error, log events
+ std::cout << "Cannot parse the published data " << data->getName() << " into Json" << std::endl;
+ return false;
+ }
+
+ // validate added files...
+ for (size_t i = 0; i < parsedFromString["add"].size(); i++) {
+ if (!publisherPrefix.isPrefixOf(
+ ndn::Name(parsedFromString["add"][static_cast<int>(i)].asString())))
+ return false;
+ }
+
+ // validate removed files ...
+ for (size_t i = 0; i < parsedFromString["remove"].size(); i++) {
+ if (!publisherPrefix.isPrefixOf(
+ ndn::Name(parsedFromString["remove"][static_cast<int>(i)].asString())))
+ return false;
+ }
+ return true;
}
} // namespace publish
diff --git a/catalog/tests/unit-tests/publish/test-publish-adapter.cpp b/catalog/tests/unit-tests/publish/test-publish-adapter.cpp
new file mode 100644
index 0000000..7d6a894
--- /dev/null
+++ b/catalog/tests/unit-tests/publish/test-publish-adapter.cpp
@@ -0,0 +1,235 @@
+/** NDN-Atmos: Cataloging Service for distributed data originally developed
+ * for atmospheric science data
+ * Copyright (C) 2015 Colorado State University
+ *
+ * NDN-Atmos is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * NDN-Atmos 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with NDN-Atmos. If not, see <http://www.gnu.org/licenses/>.
+**/
+
+#include "publish/publish-adapter.hpp"
+#include "boost-test.hpp"
+#include "../../unit-test-time-fixture.hpp"
+#include "util/config-file.hpp"
+
+#include <boost/mpl/list.hpp>
+#include <boost/thread.hpp>
+#include <ndn-cxx/util/dummy-client-face.hpp>
+#include <boost/property_tree/info_parser.hpp>
+
+namespace atmos{
+namespace tests{
+ using ndn::util::DummyClientFace;
+ using ndn::util::makeDummyClientFace;
+
+ class PublishAdapterTest : public publish::PublishAdapter<std::string>
+ {
+ public:
+ PublishAdapterTest(const std::shared_ptr<ndn::util::DummyClientFace>& face,
+ const std::shared_ptr<ndn::KeyChain>& keyChain)
+ : publish::PublishAdapter<std::string>(face, keyChain)
+ {
+ }
+
+ virtual
+ ~PublishAdapterTest()
+ {
+ }
+
+ const ndn::Name
+ getPrefix()
+ {
+ return m_prefix;
+ }
+
+ const ndn::Name
+ getSigningId()
+ {
+ return m_signingId;
+ }
+
+ const ndn::Name
+ getSyncPrefix()
+ {
+ return m_syncPrefix;
+ }
+
+ void
+ configAdapter(const util::ConfigSection& section,
+ const ndn::Name& prefix)
+ {
+ onConfig(section, false, std::string("test.txt"), prefix);
+ }
+
+ bool
+ testValidatePublicationChanges(const std::shared_ptr<const ndn::Data>& data)
+ {
+ return validatePublicationChanges(data);
+ }
+ };
+
+ class PublishAdapterFixture : public UnitTestTimeFixture
+ {
+ public:
+ PublishAdapterFixture()
+ : face(makeDummyClientFace(io))
+ , keyChain(new ndn::KeyChain())
+ , publishAdapterTest1(face, keyChain)
+ , publishAdapterTest2(face, keyChain)
+ {
+ }
+
+ virtual
+ ~PublishAdapterFixture()
+ {
+ }
+
+ protected:
+ void
+ initializePublishAdapterTest1()
+ {
+ util::ConfigSection section;
+ try {
+ std::stringstream ss;
+ ss << "database \
+ { \
+ dbServer localhost \
+ dbName testdb \
+ dbUser testuser \
+ dbPasswd testpwd \
+ } \
+ sync \
+ { \
+ prefix ndn:/ndn/broadcast1 \
+ }";
+ boost::property_tree::read_info(ss, section);
+ }
+ catch (boost::property_tree::info_parser_error &e) {
+ std::cout << "Failed to read config file " << e.what() << std::endl;
+ }
+ publishAdapterTest1.configAdapter(section, ndn::Name("/test"));
+ }
+
+ void
+ initializePublishAdapterTest2()
+ {
+ util::ConfigSection section;
+ try {
+ std::stringstream ss;
+ ss << "\
+ signingId /prefix/signingId \
+ database \
+ { \
+ dbServer localhost \
+ dbName testdb \
+ dbUser testuser \
+ dbPasswd testpwd \
+ } \
+ sync \
+ { \
+ }";
+ boost::property_tree::read_info(ss, section);
+ }
+ catch (boost::property_tree::info_parser_error &e) {
+ std::cout << "Failed to read config file " << e.what() << std::endl;;
+ }
+ publishAdapterTest2.configAdapter(section, ndn::Name("/test"));
+ }
+
+ protected:
+ std::shared_ptr<DummyClientFace> face;
+ std::shared_ptr<ndn::KeyChain> keyChain;
+ PublishAdapterTest publishAdapterTest1;
+ PublishAdapterTest publishAdapterTest2;
+ };
+
+ BOOST_FIXTURE_TEST_SUITE(PublishAdapterTestSuite, PublishAdapterFixture)
+
+ BOOST_AUTO_TEST_CASE(BasicPublishAdapterTest1)
+ {
+ BOOST_CHECK(publishAdapterTest1.getPrefix() == ndn::Name());
+ BOOST_CHECK(publishAdapterTest1.getSigningId() == ndn::Name());
+ BOOST_CHECK(publishAdapterTest1.getSyncPrefix() == ndn::Name());
+ }
+
+ BOOST_AUTO_TEST_CASE(BasicPublishAdapterTest2)
+ {
+ initializePublishAdapterTest1();
+ BOOST_CHECK(publishAdapterTest1.getPrefix() == ndn::Name("/test"));
+ BOOST_CHECK(publishAdapterTest1.getSigningId() == ndn::Name());
+ BOOST_CHECK(publishAdapterTest1.getSyncPrefix() == ndn::Name("ndn:/ndn/broadcast1"));
+
+ initializePublishAdapterTest2();
+ BOOST_CHECK(publishAdapterTest2.getPrefix() == ndn::Name("/test"));
+ BOOST_CHECK(publishAdapterTest2.getSigningId() == ndn::Name("/prefix/signingId"));
+ BOOST_CHECK(publishAdapterTest2.getSyncPrefix() ==
+ ndn::Name("ndn:/ndn-atmos/broadcast/chronosync"));
+ }
+
+ BOOST_AUTO_TEST_CASE(PublishAdapterValidateDataTestSuccess)
+ {
+ ndn::Name dataName("/test/publisher/12345"); // data name must be prefix+nonce
+ Json::Value testJson;
+ testJson["add"][0] = "/test/publisher/1";
+ testJson["add"][1] = "/test/publisher/2";
+ testJson["remove"][0] = "/test/publisher/5";
+
+ Json::FastWriter fastWriter;
+ const std::string jsonMessage = fastWriter.write(testJson);
+ const char* payload = jsonMessage.c_str();
+ size_t payLoadLength = jsonMessage.size() + 1;
+
+ std::shared_ptr<ndn::Data> data = std::make_shared<ndn::Data>(dataName);
+ data->setContent(reinterpret_cast<const uint8_t*>(payload), payLoadLength);
+ data->setFreshnessPeriod(ndn::time::milliseconds(10000));
+
+ BOOST_CHECK_EQUAL(true, publishAdapterTest1.testValidatePublicationChanges(data));
+
+ ndn::Name dataName2("/"); // short data name
+ Json::Value testJson2;
+ testJson2["add"][0] = "/test/publisher2/1";
+ testJson2["remove"][0] = "/test/publisher/1/2/3";
+
+ const std::string jsonMessage2 = fastWriter.write(testJson2);
+ const char* payload2 = jsonMessage2.c_str();
+ size_t payLoadLength2 = jsonMessage2.size() + 1;
+
+ std::shared_ptr<ndn::Data> data2 = std::make_shared<ndn::Data>(dataName2);
+ data2->setContent(reinterpret_cast<const uint8_t*>(payload2), payLoadLength2);
+ data2->setFreshnessPeriod(ndn::time::milliseconds(10000));
+ BOOST_CHECK_EQUAL(true, publishAdapterTest1.testValidatePublicationChanges(data2));
+ }
+
+ BOOST_AUTO_TEST_CASE(PublishAdapterValidateDataTestFail)
+ {
+ ndn::Name dataName1("test/publisher2/12345"); // data name must be prefix+nonce
+ Json::Value testJson1;
+ testJson1["add"][0] = "/test/publisher2/1";
+ testJson1["remove"][0] = "/test/publisher/1/2/3";
+ testJson1["remove"][1] = "/test/publisher2/4";
+
+ Json::FastWriter fastWriter;
+ const std::string jsonMessage1 = fastWriter.write(testJson1);
+ const char* payload1 = jsonMessage1.c_str();
+ size_t payLoadLength1 = jsonMessage1.size() + 1;
+
+ std::shared_ptr<ndn::Data> data1 = std::make_shared<ndn::Data>(dataName1);
+ data1->setContent(reinterpret_cast<const uint8_t*>(payload1), payLoadLength1);
+ data1->setFreshnessPeriod(ndn::time::milliseconds(10000));
+
+ BOOST_CHECK_EQUAL(false, publishAdapterTest1.testValidatePublicationChanges(data1));
+ }
+
+ BOOST_AUTO_TEST_SUITE_END()
+
+}//tests
+}//atmos
diff --git a/wscript b/wscript
index d348f4a..28c51be 100644
--- a/wscript
+++ b/wscript
@@ -90,7 +90,7 @@
features='cxx',
source=bld.path.ant_glob(['catalog/src/**/*.cpp'],
excl=['catalog/src/main.cpp']),
- use='NDN_CXX BOOST JSON MYSQL',
+ use='NDN_CXX BOOST JSON MYSQL SYNC',
includes='catalog/src .',
export_includes='catalog/src .'
)