mgmt: CommandAuthenticator

refs #2063

Change-Id: I19a82d8d1fdfb3cc5a003166b1a8c1c32bbf24b5
diff --git a/daemon/mgmt/command-authenticator.cpp b/daemon/mgmt/command-authenticator.cpp
new file mode 100644
index 0000000..cd45b84
--- /dev/null
+++ b/daemon/mgmt/command-authenticator.cpp
@@ -0,0 +1,234 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2016,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD 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.
+ *
+ * NFD 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
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "command-authenticator.hpp"
+#include "core/logger.hpp"
+
+#include <ndn-cxx/security/identity-certificate.hpp>
+#include <ndn-cxx/security/validator-null.hpp>
+#include <ndn-cxx/util/io.hpp>
+
+#include <boost/filesystem.hpp>
+
+namespace nfd {
+
+NFD_LOG_INIT("CommandAuthenticator");
+// INFO: configuration change, etc
+// DEBUG: per authentication request result
+
+shared_ptr<CommandAuthenticator>
+CommandAuthenticator::create()
+{
+  return shared_ptr<CommandAuthenticator>(new CommandAuthenticator());
+}
+
+CommandAuthenticator::CommandAuthenticator()
+  : m_validator(make_unique<ndn::ValidatorNull>())
+{
+}
+
+void
+CommandAuthenticator::setConfigFile(ConfigFile& configFile)
+{
+  configFile.addSectionHandler("authorizations",
+    bind(&CommandAuthenticator::processConfig, this, _1, _2, _3));
+}
+
+void
+CommandAuthenticator::processConfig(const ConfigSection& section, bool isDryRun, const std::string& filename)
+{
+  if (!isDryRun) {
+    NFD_LOG_INFO("clear-authorizations");
+    for (auto& kv : m_moduleAuth) {
+      kv.second.allowAny = false;
+      kv.second.certs.clear();
+    }
+  }
+
+  if (section.empty()) {
+    BOOST_THROW_EXCEPTION(ConfigFile::Error("'authorize' is missing under 'authorizations'"));
+  }
+
+  int authSectionIndex = 0;
+  for (const auto& kv : section) {
+    if (kv.first != "authorize") {
+      BOOST_THROW_EXCEPTION(ConfigFile::Error(
+        "'" + kv.first + "' section is not permitted under 'authorizations'"));
+    }
+    const ConfigSection& authSection = kv.second;
+
+    std::string certfile;
+    try {
+      certfile = authSection.get<std::string>("certfile");
+    }
+    catch (const boost::property_tree::ptree_error&) {
+      BOOST_THROW_EXCEPTION(ConfigFile::Error(
+        "'certfile' is missing under authorize[" + to_string(authSectionIndex) + "]"));
+    }
+
+    bool isAny = false;
+    shared_ptr<ndn::IdentityCertificate> cert;
+    if (certfile == "any") {
+      isAny = true;
+      NFD_LOG_WARN("'certfile any' is intended for demo purposes only and "
+                   "SHOULD NOT be used in production environments");
+    }
+    else {
+      using namespace boost::filesystem;
+      path certfilePath = absolute(certfile, path(filename).parent_path());
+      cert = ndn::io::load<ndn::IdentityCertificate>(certfilePath.string());
+      if (cert == nullptr) {
+        BOOST_THROW_EXCEPTION(ConfigFile::Error(
+          "cannot load certfile " + certfilePath.string() +
+          " for authorize[" + to_string(authSectionIndex) + "]"));
+      }
+    }
+
+    const ConfigSection* privSection = nullptr;
+    try {
+      privSection = &authSection.get_child("privileges");
+    }
+    catch (const boost::property_tree::ptree_error&) {
+      BOOST_THROW_EXCEPTION(ConfigFile::Error(
+        "'privileges' is missing under authorize[" + to_string(authSectionIndex) + "]"));
+    }
+
+    if (privSection->empty()) {
+      NFD_LOG_WARN("No privileges granted to certificate " << certfile);
+    }
+    for (const auto& kv : *privSection) {
+      const std::string& module = kv.first;
+      auto found = m_moduleAuth.find(module);
+      if (found == m_moduleAuth.end()) {
+        BOOST_THROW_EXCEPTION(ConfigFile::Error(
+          "unknown module '" + module + "' under authorize[" + to_string(authSectionIndex) + "]"));
+      }
+
+      if (isDryRun) {
+        continue;
+      }
+
+      if (isAny) {
+        found->second.allowAny = true;
+        NFD_LOG_INFO("authorize module=" << module << " signer=any");
+      }
+      else {
+        const Name& keyName = cert->getPublicKeyName();
+        found->second.certs.emplace(keyName, cert->getPublicKeyInfo());
+        NFD_LOG_INFO("authorize module=" << module << " signer=" << keyName <<
+                     " certfile=" << certfile);
+      }
+    }
+
+    ++authSectionIndex;
+  }
+}
+
+ndn::mgmt::Authorization
+CommandAuthenticator::makeAuthorization(const std::string& module, const std::string& verb)
+{
+  m_moduleAuth[module]; // declares module, so that privilege is recognized
+
+  auto self = this->shared_from_this();
+  return [=] (const Name& prefix, const Interest& interest,
+              const ndn::mgmt::ControlParameters* params,
+              const ndn::mgmt::AcceptContinuation& accept,
+              const ndn::mgmt::RejectContinuation& reject) {
+    const AuthorizedCerts& authorized = self->m_moduleAuth.at(module);
+    if (authorized.allowAny) {
+      NFD_LOG_DEBUG("accept " << interest.getName() << " allowAny");
+      accept("*");
+      return;
+    }
+
+    bool isOk = false;
+    Name keyName;
+    std::tie(isOk, keyName) = CommandAuthenticator::extractKeyName(interest);
+    if (!isOk) {
+      NFD_LOG_DEBUG("reject " << interest.getName() << " bad-KeyLocator");
+      reject(ndn::mgmt::RejectReply::SILENT);
+      return;
+    }
+
+    auto found = authorized.certs.find(keyName);
+    if (found == authorized.certs.end()) {
+      NFD_LOG_DEBUG("reject " << interest.getName() << " signer=" << keyName << " not-authorized");
+      reject(ndn::mgmt::RejectReply::STATUS403);
+      return;
+    }
+
+    bool hasGoodSig = ndn::Validator::verifySignature(interest, found->second);
+    if (!hasGoodSig) {
+      NFD_LOG_DEBUG("reject " << interest.getName() << " signer=" << keyName << " bad-sig");
+      reject(ndn::mgmt::RejectReply::STATUS403);
+      return;
+    }
+
+    self->m_validator.validate(interest,
+      bind([=] {
+        NFD_LOG_DEBUG("accept " << interest.getName() << " signer=" << keyName);
+        accept(keyName.toUri());
+      }),
+      bind([=] {
+        NFD_LOG_DEBUG("reject " << interest.getName() << " signer=" << keyName << " invalid-timestamp");
+        reject(ndn::mgmt::RejectReply::STATUS403);
+      }));
+  };
+}
+
+std::pair<bool, Name>
+CommandAuthenticator::extractKeyName(const Interest& interest)
+{
+  const Name& name = interest.getName();
+  if (name.size() < ndn::signed_interest::MIN_LENGTH) {
+    return {false, Name()};
+  }
+
+  ndn::SignatureInfo sig;
+  try {
+    sig.wireDecode(name[ndn::signed_interest::POS_SIG_INFO].blockFromValue());
+  }
+  catch (const tlv::Error&) {
+    return {false, Name()};
+  }
+
+  if (!sig.hasKeyLocator()) {
+    return {false, Name()};
+  }
+
+  const ndn::KeyLocator& keyLocator = sig.getKeyLocator();
+  if (keyLocator.getType() != ndn::KeyLocator::KeyLocator_Name) {
+    return {false, Name()};
+  }
+
+  try {
+    return {true, ndn::IdentityCertificate::certificateNameToPublicKeyName(keyLocator.getName())};
+  }
+  catch (const ndn::IdentityCertificate::Error&) {
+    return {false, Name()};
+  }
+}
+
+} // namespace nfd
diff --git a/daemon/mgmt/command-authenticator.hpp b/daemon/mgmt/command-authenticator.hpp
new file mode 100644
index 0000000..69d0ec9
--- /dev/null
+++ b/daemon/mgmt/command-authenticator.hpp
@@ -0,0 +1,80 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2016,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD 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.
+ *
+ * NFD 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
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NFD_DAEMON_MGMT_COMMAND_AUTHENTICATOR_HPP
+#define NFD_DAEMON_MGMT_COMMAND_AUTHENTICATOR_HPP
+
+#include "core/config-file.hpp"
+#include <ndn-cxx/mgmt/dispatcher.hpp>
+#include <ndn-cxx/security/command-interest-validator.hpp>
+#include <ndn-cxx/security/public-key.hpp>
+
+namespace nfd {
+
+/** \brief provides ControlCommand authorization according to NFD configuration file
+ */
+class CommandAuthenticator : public enable_shared_from_this<CommandAuthenticator>, noncopyable
+{
+public:
+  static shared_ptr<CommandAuthenticator>
+  create();
+
+  void
+  setConfigFile(ConfigFile& configFile);
+
+  /** \return an Authorization function for module/verb command
+   *  \param module management module name
+   *  \param verb command verb; currently it's ignored
+   *  \note This must be called before parsing configuration file
+   */
+  ndn::mgmt::Authorization
+  makeAuthorization(const std::string& module, const std::string& verb);
+
+private:
+  CommandAuthenticator();
+
+  /** \brief process "authorizations" section
+   *  \throw ConfigFile::Error on parse error
+   */
+  void
+  processConfig(const ConfigSection& section, bool isDryRun, const std::string& filename);
+
+  static std::pair<bool, Name>
+  extractKeyName(const Interest& interest);
+
+private:
+  struct AuthorizedCerts
+  {
+    bool allowAny = false;
+    std::unordered_map<Name, ndn::PublicKey> certs; ///< keyName => publicKey
+  };
+  std::unordered_map<std::string, AuthorizedCerts> m_moduleAuth; ///< module => certs
+
+  ndn::security::CommandInterestValidator m_validator;
+};
+
+} // namespace nfd
+
+#endif // NFD_DAEMON_MGMT_COMMAND_AUTHENTICATOR_HPP
diff --git a/tests/daemon/mgmt/command-authenticator.t.cpp b/tests/daemon/mgmt/command-authenticator.t.cpp
new file mode 100644
index 0000000..c12fcfa
--- /dev/null
+++ b/tests/daemon/mgmt/command-authenticator.t.cpp
@@ -0,0 +1,478 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/**
+ * Copyright (c) 2014-2016,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD 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.
+ *
+ * NFD 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
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "mgmt/command-authenticator.hpp"
+#include <ndn-cxx/security/signing-helpers.hpp>
+#include <boost/filesystem.hpp>
+
+#include "tests/test-common.hpp"
+#include "tests/identity-management-fixture.hpp"
+
+namespace nfd {
+namespace tests {
+
+class CommandAuthenticatorFixture : public IdentityManagementTimeFixture
+{
+protected:
+  CommandAuthenticatorFixture()
+    : authenticator(CommandAuthenticator::create())
+  {
+  }
+
+  void
+  makeModules(std::initializer_list<std::string> modules)
+  {
+    for (const std::string& module : modules) {
+      authorizations.emplace(module, authenticator->makeAuthorization(module, "verb"));
+    }
+  }
+
+  void
+  loadConfig(const std::string& config)
+  {
+    boost::filesystem::path configPath = boost::filesystem::current_path() /=
+                                         "command-authenticator-test.conf";
+    ConfigFile cf;
+    authenticator->setConfigFile(cf);
+    cf.parse(config, false, configPath.c_str());
+  }
+
+  bool
+  authorize(const std::string& module, const Name& identity,
+            const function<void(Interest&)>& modifyInterest = nullptr)
+  {
+    auto interest = makeInterest(Name("/prefix").append(module).append("verb"));
+    m_keyChain.sign(*interest, signingByIdentity(identity));
+    if (modifyInterest != nullptr) {
+      modifyInterest(*interest);
+    }
+
+    ndn::mgmt::Authorization authorization = authorizations.at(module);
+
+    bool isAccepted = false;
+    bool isRejected = false;
+    authorization(Name("/prefix"), *interest, nullptr,
+      [this, &isAccepted, &isRejected] (const std::string& requester) {
+        BOOST_REQUIRE_MESSAGE(!isAccepted && !isRejected,
+                              "authorization function should invoke only one continuation");
+        isAccepted = true;
+        lastRequester = requester;
+      },
+      [this, &isAccepted, &isRejected] (ndn::mgmt::RejectReply act) {
+        BOOST_REQUIRE_MESSAGE(!isAccepted && !isRejected,
+                              "authorization function should invoke only one continuation");
+        isRejected = true;
+        lastRejectReply = act;
+      });
+
+    this->advanceClocks(time::milliseconds(1), 10);
+    BOOST_REQUIRE_MESSAGE(isAccepted || isRejected,
+                          "authorization function should invoke one continuation");
+    return isAccepted;
+  }
+
+protected:
+  shared_ptr<CommandAuthenticator> authenticator;
+  std::unordered_map<std::string, ndn::mgmt::Authorization> authorizations;
+  std::string lastRequester;
+  ndn::mgmt::RejectReply lastRejectReply;
+};
+
+BOOST_AUTO_TEST_SUITE(Mgmt)
+BOOST_FIXTURE_TEST_SUITE(TestCommandAuthenticator, CommandAuthenticatorFixture)
+
+BOOST_AUTO_TEST_CASE(Certs)
+{
+  Name id0("/localhost/CommandAuthenticator/0");
+  Name id1("/localhost/CommandAuthenticator/1");
+  Name id2("/localhost/CommandAuthenticator/2");
+  BOOST_REQUIRE(addIdentity(id0));
+  BOOST_REQUIRE(saveIdentityCertificate(id1, "1.ndncert", true));
+  BOOST_REQUIRE(saveIdentityCertificate(id2, "2.ndncert", true));
+
+  makeModules({"module0", "module1", "module2", "module3", "module4", "module5", "module6", "module7"});
+  const std::string& config = R"CONFIG(
+    authorizations
+    {
+      authorize
+      {
+        certfile any
+        privileges
+        {
+          module1
+          module3
+          module5
+          module7
+        }
+      }
+      authorize
+      {
+        certfile "1.ndncert"
+        privileges
+        {
+          module2
+          module3
+          module6
+          module7
+        }
+      }
+      authorize
+      {
+        certfile "2.ndncert"
+        privileges
+        {
+          module4
+          module5
+          module6
+          module7
+        }
+      }
+    }
+  )CONFIG";
+  loadConfig(config);
+
+  // module0: none
+  BOOST_CHECK_EQUAL(authorize("module0", id0), false);
+  BOOST_CHECK_EQUAL(authorize("module0", id1), false);
+  BOOST_CHECK_EQUAL(authorize("module0", id2), false);
+
+  // module1: any
+  BOOST_CHECK_EQUAL(authorize("module1", id0), true);
+  BOOST_CHECK_EQUAL(authorize("module1", id1), true);
+  BOOST_CHECK_EQUAL(authorize("module1", id2), true);
+
+  // module2: id1
+  BOOST_CHECK_EQUAL(authorize("module2", id0), false);
+  BOOST_CHECK_EQUAL(authorize("module2", id1), true);
+  BOOST_CHECK_EQUAL(authorize("module2", id2), false);
+
+  // module3: any,id1
+  BOOST_CHECK_EQUAL(authorize("module3", id0), true);
+  BOOST_CHECK_EQUAL(authorize("module3", id1), true);
+  BOOST_CHECK_EQUAL(authorize("module3", id2), true);
+
+  // module4: id2
+  BOOST_CHECK_EQUAL(authorize("module4", id0), false);
+  BOOST_CHECK_EQUAL(authorize("module4", id1), false);
+  BOOST_CHECK_EQUAL(authorize("module4", id2), true);
+
+  // module5: any,id2
+  BOOST_CHECK_EQUAL(authorize("module5", id0), true);
+  BOOST_CHECK_EQUAL(authorize("module5", id1), true);
+  BOOST_CHECK_EQUAL(authorize("module5", id2), true);
+
+  // module6: id1,id2
+  BOOST_CHECK_EQUAL(authorize("module6", id0), false);
+  BOOST_CHECK_EQUAL(authorize("module6", id1), true);
+  BOOST_CHECK_EQUAL(authorize("module6", id2), true);
+
+  // module7: any,id1,id2
+  BOOST_CHECK_EQUAL(authorize("module7", id0), true);
+  BOOST_CHECK_EQUAL(authorize("module7", id1), true);
+  BOOST_CHECK_EQUAL(authorize("module7", id2), true);
+}
+
+BOOST_AUTO_TEST_CASE(Requester)
+{
+  Name id0("/localhost/CommandAuthenticator/0");
+  Name id1("/localhost/CommandAuthenticator/1");
+  BOOST_REQUIRE(addIdentity(id0));
+  BOOST_REQUIRE(saveIdentityCertificate(id1, "1.ndncert", true));
+
+  makeModules({"module0", "module1"});
+  const std::string& config = R"CONFIG(
+    authorizations
+    {
+      authorize
+      {
+        certfile any
+        privileges
+        {
+          module0
+        }
+      }
+      authorize
+      {
+        certfile "1.ndncert"
+        privileges
+        {
+          module1
+        }
+      }
+    }
+  )CONFIG";
+  loadConfig(config);
+
+  // module0: any
+  BOOST_CHECK_EQUAL(authorize("module0", id0), true);
+  BOOST_CHECK_EQUAL(lastRequester, "*");
+  BOOST_CHECK_EQUAL(authorize("module0", id1), true);
+  BOOST_CHECK_EQUAL(lastRequester, "*");
+
+  // module1: id1
+  BOOST_CHECK_EQUAL(authorize("module1", id0), false);
+  BOOST_CHECK_EQUAL(authorize("module1", id1), true);
+  BOOST_CHECK(id1.isPrefixOf(lastRequester));
+}
+
+class IdentityAuthorizedFixture : public CommandAuthenticatorFixture
+{
+protected:
+  IdentityAuthorizedFixture()
+    : id1("/localhost/CommandAuthenticator/1")
+  {
+    BOOST_REQUIRE(saveIdentityCertificate(id1, "1.ndncert", true));
+
+    makeModules({"module1"});
+    const std::string& config = R"CONFIG(
+      authorizations
+      {
+        authorize
+        {
+          certfile "1.ndncert"
+          privileges
+          {
+            module1
+          }
+        }
+      }
+    )CONFIG";
+    loadConfig(config);
+  }
+
+  bool
+  authorize1(const function<void(Interest&)>& modifyInterest)
+  {
+    return authorize("module1", id1, modifyInterest);
+  }
+
+protected:
+  Name id1;
+};
+
+BOOST_FIXTURE_TEST_SUITE(Rejects, IdentityAuthorizedFixture)
+
+BOOST_AUTO_TEST_CASE(BadKeyLocator_NameTooShort)
+{
+  BOOST_CHECK_EQUAL(authorize1(
+    [] (Interest& interest) {
+      interest.setName("/prefix");
+    }
+  ), false);
+  BOOST_CHECK(lastRejectReply == ndn::mgmt::RejectReply::SILENT);
+}
+
+BOOST_AUTO_TEST_CASE(BadKeyLocator_BadSigInfo)
+{
+  BOOST_CHECK_EQUAL(authorize1(
+    [] (Interest& interest) {
+      setNameComponent(interest, ndn::signed_interest::POS_SIG_INFO, "not-SignatureInfo");
+    }
+  ), false);
+  BOOST_CHECK(lastRejectReply == ndn::mgmt::RejectReply::SILENT);
+}
+
+BOOST_AUTO_TEST_CASE(BadKeyLocator_MissingKeyLocator)
+{
+  BOOST_CHECK_EQUAL(authorize1(
+    [] (Interest& interest) {
+      ndn::SignatureInfo sigInfo;
+      setNameComponent(interest, ndn::signed_interest::POS_SIG_INFO,
+                       sigInfo.wireEncode().begin(), sigInfo.wireEncode().end());
+    }
+  ), false);
+  BOOST_CHECK(lastRejectReply == ndn::mgmt::RejectReply::SILENT);
+}
+
+BOOST_AUTO_TEST_CASE(BadKeyLocator_BadKeyLocatorType)
+{
+  BOOST_CHECK_EQUAL(authorize1(
+    [] (Interest& interest) {
+      ndn::KeyLocator kl;
+      kl.setKeyDigest(ndn::encoding::makeBinaryBlock(tlv::KeyDigest, "\xDD\xDD\xDD\xDD\xDD\xDD\xDD\xDD", 8));
+      ndn::SignatureInfo sigInfo;
+      sigInfo.setKeyLocator(kl);
+      setNameComponent(interest, ndn::signed_interest::POS_SIG_INFO,
+                       sigInfo.wireEncode().begin(), sigInfo.wireEncode().end());
+    }
+  ), false);
+  BOOST_CHECK(lastRejectReply == ndn::mgmt::RejectReply::SILENT);
+}
+
+BOOST_AUTO_TEST_CASE(BadKeyLocator_BadCertName)
+{
+  BOOST_CHECK_EQUAL(authorize1(
+    [] (Interest& interest) {
+      ndn::KeyLocator kl;
+      kl.setName("/bad/cert/name");
+      ndn::SignatureInfo sigInfo;
+      sigInfo.setKeyLocator(kl);
+      setNameComponent(interest, ndn::signed_interest::POS_SIG_INFO,
+                       sigInfo.wireEncode().begin(), sigInfo.wireEncode().end());
+    }
+  ), false);
+  BOOST_CHECK(lastRejectReply == ndn::mgmt::RejectReply::SILENT);
+}
+
+BOOST_AUTO_TEST_CASE(NotAuthorized)
+{
+  Name id0("/localhost/CommandAuthenticator/0");
+  BOOST_REQUIRE(addIdentity(id0));
+
+  BOOST_CHECK_EQUAL(authorize("module1", id0), false);
+  BOOST_CHECK(lastRejectReply == ndn::mgmt::RejectReply::STATUS403);
+}
+
+BOOST_AUTO_TEST_CASE(BadSig)
+{
+  BOOST_CHECK_EQUAL(authorize1(
+    [] (Interest& interest) {
+      setNameComponent(interest, ndn::signed_interest::POS_SIG_VALUE, "bad-signature-bits");
+    }
+  ), false);
+  BOOST_CHECK(lastRejectReply == ndn::mgmt::RejectReply::STATUS403);
+}
+
+BOOST_AUTO_TEST_CASE(InvalidTimestamp)
+{
+  name::Component timestampComp;
+  BOOST_CHECK_EQUAL(authorize1(
+    [&timestampComp] (const Interest& interest) {
+      timestampComp = interest.getName().at(ndn::signed_interest::POS_TIMESTAMP);
+    }
+  ), true); // accept first command
+  BOOST_CHECK_EQUAL(authorize1(
+    [&timestampComp] (Interest& interest) {
+      setNameComponent(interest, ndn::signed_interest::POS_TIMESTAMP, timestampComp);
+    }
+  ), false); // reject second command because timestamp equals first command
+  BOOST_CHECK(lastRejectReply == ndn::mgmt::RejectReply::STATUS403);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // Rejects
+
+BOOST_AUTO_TEST_SUITE(BadConfig)
+
+BOOST_AUTO_TEST_CASE(EmptyAuthorizationsSection)
+{
+  const std::string& config = R"CONFIG(
+    authorizations
+    {
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(loadConfig(config), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(UnrecognizedKey)
+{
+  const std::string& config = R"CONFIG(
+    authorizations
+    {
+      unrecognized_key
+      {
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(loadConfig(config), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(CertfileMissing)
+{
+  const std::string& config = R"CONFIG(
+    authorizations
+    {
+      authorize
+      {
+        privileges
+        {
+        }
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(loadConfig(config), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(CertUnreadable)
+{
+  const std::string& config = R"CONFIG(
+    authorizations
+    {
+      authorize
+      {
+        certfile "1.ndncert"
+        privileges
+        {
+        }
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(loadConfig(config), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(PrivilegesMissing)
+{
+  const std::string& config = R"CONFIG(
+    authorizations
+    {
+      authorize
+      {
+        certfile any
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(loadConfig(config), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_CASE(UnregisteredModule)
+{
+  const std::string& config = R"CONFIG(
+    authorizations
+    {
+      authorize
+      {
+        certfile any
+        privileges
+        {
+          nosuchmodule
+        }
+      }
+    }
+  )CONFIG";
+
+  BOOST_CHECK_THROW(loadConfig(config), ConfigFile::Error);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // BadConfig
+
+BOOST_AUTO_TEST_SUITE_END() // TestCommandAuthenticator
+BOOST_AUTO_TEST_SUITE_END() // Mgmt
+
+} // namespace tests
+} // namespace nfd
diff --git a/tests/identity-management-fixture.cpp b/tests/identity-management-fixture.cpp
index 1575fa8..3bb7252 100644
--- a/tests/identity-management-fixture.cpp
+++ b/tests/identity-management-fixture.cpp
@@ -24,6 +24,8 @@
  */
 
 #include "identity-management-fixture.hpp"
+#include <ndn-cxx/util/io.hpp>
+#include <boost/filesystem.hpp>
 
 namespace nfd {
 namespace tests {
@@ -31,18 +33,23 @@
 IdentityManagementFixture::IdentityManagementFixture()
   : m_keyChain("sqlite3", "file")
 {
-  m_keyChain.getDefaultCertificate(); // side effect: creation of the default cert if doesn't exist
+  m_keyChain.getDefaultCertificate(); // side effect: create a default cert if it doesn't exist
 }
 
 IdentityManagementFixture::~IdentityManagementFixture()
 {
-  for (auto&& id : m_identities) {
+  for (const auto& id : m_identities) {
     m_keyChain.deleteIdentity(id);
   }
+
+  boost::system::error_code ec;
+  for (const auto& certFile : m_certFiles) {
+    boost::filesystem::remove(certFile, ec); // ignore error
+  }
 }
 
 bool
-IdentityManagementFixture::addIdentity(const ndn::Name& identity, const ndn::KeyParams& params)
+IdentityManagementFixture::addIdentity(const Name& identity, const ndn::KeyParams& params)
 {
   try {
     m_keyChain.createIdentity(identity, params);
@@ -54,5 +61,29 @@
   }
 }
 
+bool
+IdentityManagementFixture::saveIdentityCertificate(const Name& identity, const std::string& filename, bool wantAdd)
+{
+  shared_ptr<ndn::IdentityCertificate> cert;
+  try {
+    cert = m_keyChain.getCertificate(m_keyChain.getDefaultCertificateNameForIdentity(identity));
+  }
+  catch (const ndn::SecPublicInfo::Error&) {
+    if (wantAdd && this->addIdentity(identity)) {
+      return this->saveIdentityCertificate(identity, filename, false);
+    }
+    return false;
+  }
+
+  m_certFiles.push_back(filename);
+  try {
+    ndn::io::save(*cert, filename);
+    return true;
+  }
+  catch (const ndn::io::Error&) {
+    return false;
+  }
+}
+
 } // namespace tests
 } // namespace nfd
diff --git a/tests/identity-management-fixture.hpp b/tests/identity-management-fixture.hpp
index 0f7d3c4..318cdd1 100644
--- a/tests/identity-management-fixture.hpp
+++ b/tests/identity-management-fixture.hpp
@@ -28,37 +28,48 @@
 
 #include "tests/test-common.hpp"
 #include <ndn-cxx/security/key-chain.hpp>
-#include <vector>
-
-#include "boost-test.hpp"
 
 namespace nfd {
 namespace tests {
 
-/**
- * @brief IdentityManagementFixture is a test suite level fixture.
- *
- * Test cases in the suite can use this fixture to create identities.
- * Identities added via addIdentity method are automatically deleted
- * during test teardown.
+/** \brief a fixture that cleans up KeyChain identities and certificate files upon destruction
  */
 class IdentityManagementFixture : public virtual BaseFixture
 {
 public:
   IdentityManagementFixture();
 
+  /** \brief deletes created identities and saved certificate files
+   */
   ~IdentityManagementFixture();
 
-  // @brief add identity, return true if succeed.
+  /** \brief add identity
+   *  \return whether successful
+   */
   bool
-  addIdentity(const ndn::Name& identity,
+  addIdentity(const Name& identity,
               const ndn::KeyParams& params = ndn::KeyChain::DEFAULT_KEY_PARAMS);
 
+  /** \brief save identity certificate to a file
+   *  \param identity identity name
+   *  \param filename file name, should be writable
+   *  \param wantAdd if true, add new identity when necessary
+   *  \return whether successful
+   */
+  bool
+  saveIdentityCertificate(const Name& identity, const std::string& filename, bool wantAdd = false);
+
 protected:
   ndn::KeyChain m_keyChain;
+
+private:
   std::vector<ndn::Name> m_identities;
+  std::vector<std::string> m_certFiles;
 };
 
+/** \brief convenience base class for inheriting from both UnitTestTimeFixture
+ *         and IdentityManagementFixture
+ */
 class IdentityManagementTimeFixture : public UnitTestTimeFixture
                                     , public IdentityManagementFixture
 {
diff --git a/tests/test-common.hpp b/tests/test-common.hpp
index 57727da..ffff22a 100644
--- a/tests/test-common.hpp
+++ b/tests/test-common.hpp
@@ -150,6 +150,30 @@
 lp::Nack
 makeNack(const Name& name, uint32_t nonce, lp::NackReason reason);
 
+/** \brief replace a name component
+ *  \param[inout] name name
+ *  \param index name component index
+ *  \param a arguments to name::Component constructor
+ */
+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(name2.size()));
+  name = name2;
+}
+
+template<typename Packet, typename...A>
+void
+setNameComponent(Packet& packet, ssize_t index, const A& ...a)
+{
+  Name name = packet.getName();
+  setNameComponent(name, index, a...);
+  packet.setName(name);
+}
+
 } // namespace tests
 } // namespace nfd