From 670a52f2a30461c824926cc1108ba209fc4227eb Mon Sep 17 00:00:00 2001 From: Jeremy Clark Date: Sat, 29 Aug 2020 21:48:26 -0400 Subject: [PATCH] security: add certificate bundle decoder Refs: #5004 Change-Id: I0f035caf8f6975ba2322a7f6629312d3dcab910d --- .mailmap | 1 + ndn-cxx/security/certificate.cpp | 2 +- .../detail/certificate-bundle-decoder.cpp | 67 ++++++ .../detail/certificate-bundle-decoder.hpp | 76 ++++++ .../detail/certificate-bundle-decoder.t.cpp | 219 ++++++++++++++++++ .../security/pib/impl/identity-impl.t.cpp | 4 +- tests/unit/security/pib/impl/key-impl.t.cpp | 4 +- 7 files changed, 366 insertions(+), 7 deletions(-) create mode 100644 ndn-cxx/security/detail/certificate-bundle-decoder.cpp create mode 100644 ndn-cxx/security/detail/certificate-bundle-decoder.hpp create mode 100644 tests/unit/security/detail/certificate-bundle-decoder.t.cpp diff --git a/.mailmap b/.mailmap index 967715ed..cacbeb8b 100644 --- a/.mailmap +++ b/.mailmap @@ -6,3 +6,4 @@ Teng Liang Hila Ben Abraham Laqin Fan +Jeremy Clark diff --git a/ndn-cxx/security/certificate.cpp b/ndn-cxx/security/certificate.cpp index 36dbc399..b54c56da 100644 --- a/ndn-cxx/security/certificate.cpp +++ b/ndn-cxx/security/certificate.cpp @@ -51,7 +51,7 @@ Certificate::Certificate() } Certificate::Certificate(Data&& data) - : Data(data) + : Data(std::move(data)) { if (!isValidName(getName())) { NDN_THROW(Data::Error("Name does not follow the naming convention for certificate")); diff --git a/ndn-cxx/security/detail/certificate-bundle-decoder.cpp b/ndn-cxx/security/detail/certificate-bundle-decoder.cpp new file mode 100644 index 00000000..19fba7f3 --- /dev/null +++ b/ndn-cxx/security/detail/certificate-bundle-decoder.cpp @@ -0,0 +1,67 @@ +/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ +/* + * Copyright (c) 2013-2020 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 + * . + * + * See AUTHORS.md for complete list of ndn-cxx authors and contributors. + */ + +#include "ndn-cxx/security/detail/certificate-bundle-decoder.hpp" +#include "ndn-cxx/util/scope.hpp" + +namespace ndn { +namespace security { +namespace detail { + +void +CertificateBundleDecoder::append(const Block& segment) +{ + if (m_hasError) { + NDN_THROW(tlv::Error("Unrecoverable decoding error")); + } + + m_bufferedData.insert(m_bufferedData.end(), segment.value_begin(), segment.value_end()); + decode(); +} + +void +CertificateBundleDecoder::decode() +{ + auto onThrow = make_scope_fail([this] { m_hasError = true; }); + + while (!m_bufferedData.empty()) { + bool isOk; + Block element; + std::tie(isOk, element) = Block::fromBuffer(m_bufferedData.data(), m_bufferedData.size()); + if (!isOk) { + return; + } + + m_bufferedData.erase(m_bufferedData.begin(), m_bufferedData.begin() + element.size()); + + if (element.type() == tlv::Data) { + onCertDecoded(Certificate(element)); + } + else if (tlv::isCriticalType(element.type())) { + NDN_THROW(tlv::Error("Unrecognized element of critical type " + to_string(element.type()))); + } + // unrecognized non-critical elements are silently skipped + } +} + +} // namespace detail +} // namespace security +} // namespace ndn diff --git a/ndn-cxx/security/detail/certificate-bundle-decoder.hpp b/ndn-cxx/security/detail/certificate-bundle-decoder.hpp new file mode 100644 index 00000000..17f88ca7 --- /dev/null +++ b/ndn-cxx/security/detail/certificate-bundle-decoder.hpp @@ -0,0 +1,76 @@ +/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ +/* + * Copyright (c) 2013-2020 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 + * . + * + * See AUTHORS.md for complete list of ndn-cxx authors and contributors. + */ + +#ifndef NDN_CXX_SECURITY_DETAIL_CERTIFICATE_BUNDLE_DECODER_HPP +#define NDN_CXX_SECURITY_DETAIL_CERTIFICATE_BUNDLE_DECODER_HPP + +#include "ndn-cxx/security/certificate.hpp" +#include "ndn-cxx/util/signal.hpp" + +namespace ndn { +namespace security { +namespace detail { + +/** @brief Helper class to decode a certificate bundle. + */ +class CertificateBundleDecoder +{ +public: + /** + * @brief Append a bundle segment to the internal decoding buffer and trigger decoding. + * @param block Content element of the segment to be appended + * @throw tlv::Error When the decoder encounters an unrecognized element with critical type. + * No longer accepts segments after throwing. + * @warning Must not be called from a handler of #onCertDecoded. + * @warning Segments must be appended in order, otherwise the behavior is undefined. + */ + void + append(const Block& block); + + /** + * @brief Whether the decoder has encountered an unrecoverable error. + */ + bool + hasError() const noexcept + { + return m_hasError; + } + +public: + /** + * @brief Emitted every time a certificate is successfully decoded. + */ + util::Signal onCertDecoded; + +private: + void + decode(); + +private: + std::vector m_bufferedData; + bool m_hasError = false; +}; + +} // namespace detail +} // namespace security +} // namespace ndn + +#endif // NDN_CXX_SECURITY_DETAIL_CERTIFICATE_BUNDLE_DECODER_HPP diff --git a/tests/unit/security/detail/certificate-bundle-decoder.t.cpp b/tests/unit/security/detail/certificate-bundle-decoder.t.cpp new file mode 100644 index 00000000..b6691eec --- /dev/null +++ b/tests/unit/security/detail/certificate-bundle-decoder.t.cpp @@ -0,0 +1,219 @@ +/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ +/* + * Copyright (c) 2013-2020 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 + * . + * + * See AUTHORS.md for complete list of ndn-cxx authors and contributors. + */ + +#include "ndn-cxx/security/detail/certificate-bundle-decoder.hpp" + +#include "tests/boost-test.hpp" +#include "tests/identity-management-fixture.hpp" + +namespace ndn { +namespace security { +namespace detail { +namespace tests { + +class CertificateBundleDecoderFixture : public ndn::tests::IdentityManagementFixture +{ +protected: + CertificateBundleDecoderFixture() + { + auto id1 = addIdentity("/hello/world1"); + auto cert1 = id1.getDefaultKey().getDefaultCertificate(); + certBlock1 = cert1.wireEncode(); + m_certs.push_back(certBlock1); + + auto id2 = addIdentity("/hello/world2"); + auto cert2 = id2.getDefaultKey().getDefaultCertificate(); + certBlock2 = cert2.wireEncode(); + m_certs.push_back(certBlock2); + + cbd.onCertDecoded.connect([this] (const Certificate& receivedCert) { + BOOST_CHECK_EQUAL(receivedCert.wireEncode(), m_certs.at(nCertsCompleted)); + ++nCertsCompleted; + }); + } + +protected: + CertificateBundleDecoder cbd; + Block certBlock1; + Block certBlock2; + int nCertsCompleted = 0; + +private: + std::vector m_certs; +}; + +BOOST_AUTO_TEST_SUITE(Security) +BOOST_FIXTURE_TEST_SUITE(TestCertificateBundleDecoder, CertificateBundleDecoderFixture) + +BOOST_AUTO_TEST_CASE(EmptySegment) +{ + BOOST_CHECK_EQUAL(cbd.hasError(), false); + cbd.append(Block(tlv::Content)); + BOOST_CHECK_EQUAL(cbd.hasError(), false); + BOOST_CHECK_EQUAL(nCertsCompleted, 0); +} + +BOOST_AUTO_TEST_CASE(OneCertOneSegment) +{ + // Segment contains full certificate + Data d; + d.setContent(certBlock1); + + cbd.append(d.getContent()); + BOOST_CHECK_EQUAL(cbd.hasError(), false); + BOOST_CHECK_EQUAL(nCertsCompleted, 1); +} + +BOOST_AUTO_TEST_CASE(TwoCertsOneSegment) +{ + // Segment contains two full certificates + auto buf = std::make_shared(certBlock1.begin(), certBlock1.end()); + buf->insert(buf->end(), certBlock2.begin(), certBlock2.end()); + Data d; + d.setContent(std::move(buf)); + + cbd.append(d.getContent()); + BOOST_CHECK_EQUAL(cbd.hasError(), false); + BOOST_CHECK_EQUAL(nCertsCompleted, 2); +} + +BOOST_AUTO_TEST_CASE(TwoCertsMultipleSegments) +{ + // First segment contains first 250 bytes of cert1 + Data d; + d.setContent(certBlock1.wire(), 250); + + // Second segment contains the rest of cert1 and the first 100 bytes of cert2 + auto buf = std::make_shared(certBlock1.begin() + 250, certBlock1.end()); + buf->insert(buf->end(), certBlock2.begin(), certBlock2.begin() + 100); + Data d2; + d2.setContent(std::move(buf)); + + // Third segment contains the rest of cert2 + Data d3; + d3.setContent(std::make_shared(certBlock2.begin() + 100, certBlock2.end())); + + cbd.append(d.getContent()); + BOOST_CHECK_EQUAL(cbd.hasError(), false); + BOOST_CHECK_EQUAL(nCertsCompleted, 0); + + cbd.append(d2.getContent()); + BOOST_CHECK_EQUAL(cbd.hasError(), false); + BOOST_CHECK_EQUAL(nCertsCompleted, 1); + + cbd.append(d3.getContent()); + BOOST_CHECK_EQUAL(cbd.hasError(), false); + BOOST_CHECK_EQUAL(nCertsCompleted, 2); +} + +BOOST_AUTO_TEST_CASE(InvalidCert) +{ + // First segment contains all of cert1 + Data d; + d.setContent(certBlock1); + + const uint8_t buf[] = { + 0x06, 0x20, // Data + 0x07, 0x11, // Name + 0x08, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f, // GenericNameComponent 'hello' + 0x08, 0x01, 0x31, // GenericNameComponent '1' + 0x08, 0x05, 0x77, 0x6f, 0x72, 0x6c, 0x64, // GenericNameComponent 'world' + 0x14, 0x00, // MetaInfo empty + 0x15, 0x00, // Content empty + 0x16, 0x05, // SignatureInfo + 0x1b, 0x01, 0x01, // SignatureType RSA + 0x1c, 0x00, // KeyLocator empty + 0x17, 0x00 // SignatureValue empty + }; + // Second segment contains non-Certificate data + Data d2; + d2.setContent(buf, sizeof(buf)); + + // Third segment contains all of cert2 + Data d3; + d3.setContent(certBlock2); + + cbd.append(d.getContent()); + BOOST_CHECK_EQUAL(cbd.hasError(), false); + BOOST_CHECK_EQUAL(nCertsCompleted, 1); + + BOOST_CHECK_EXCEPTION(cbd.append(d2.getContent()), tlv::Error, [] (const auto& e) { + return e.what() == "Name does not follow the naming convention for certificate"s; + }); + BOOST_CHECK_EQUAL(cbd.hasError(), true); + BOOST_CHECK_EQUAL(nCertsCompleted, 1); + + BOOST_CHECK_EXCEPTION(cbd.append(d3.getContent()), tlv::Error, [] (const auto& e) { + return e.what() == "Unrecoverable decoding error"s; + }); + BOOST_CHECK_EQUAL(cbd.hasError(), true); + BOOST_CHECK_EQUAL(nCertsCompleted, 1); +} + +BOOST_AUTO_TEST_CASE(UnrecognizedCritical) +{ + // First segment contains an unrecognized critical element + Data d; + d.setContent("050B07030102030A0404050607"_block); + + // Second segment contains cert1 + Data d2; + d2.setContent(certBlock1); + + BOOST_CHECK_EXCEPTION(cbd.append(d.getContent()), tlv::Error, [] (const auto& e) { + return e.what() == "Unrecognized element of critical type 5"s; + }); + BOOST_CHECK_EQUAL(cbd.hasError(), true); + BOOST_CHECK_EQUAL(nCertsCompleted, 0); + + BOOST_CHECK_EXCEPTION(cbd.append(d2.getContent()), tlv::Error, [] (const auto& e) { + return e.what() == "Unrecoverable decoding error"s; + }); + BOOST_CHECK_EQUAL(cbd.hasError(), true); + BOOST_CHECK_EQUAL(nCertsCompleted, 0); +} + +BOOST_AUTO_TEST_CASE(UnrecognizedNonCritical) +{ + // First segment contains an unrecognized non-critical element + Data d; + d.setContent("4202CAFE"_block); + + // Second segment contains cert1 + Data d2; + d2.setContent(certBlock1); + + cbd.append(d.getContent()); + BOOST_CHECK_EQUAL(cbd.hasError(), false); + BOOST_CHECK_EQUAL(nCertsCompleted, 0); + + cbd.append(d2.getContent()); + BOOST_CHECK_EQUAL(cbd.hasError(), false); + BOOST_CHECK_EQUAL(nCertsCompleted, 1); +} + +BOOST_AUTO_TEST_SUITE_END() // TestCertificateBundleEncoderDecoder +BOOST_AUTO_TEST_SUITE_END() // Security + +} // namespace tests +} // namespace detail +} // namespace security +} // namespace ndn diff --git a/tests/unit/security/pib/impl/identity-impl.t.cpp b/tests/unit/security/pib/impl/identity-impl.t.cpp index bb523f4f..3890ddcc 100644 --- a/tests/unit/security/pib/impl/identity-impl.t.cpp +++ b/tests/unit/security/pib/impl/identity-impl.t.cpp @@ -1,6 +1,6 @@ /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ /* - * Copyright (c) 2013-2019 Regents of the University of California. + * Copyright (c) 2013-2020 Regents of the University of California. * * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions). * @@ -34,7 +34,6 @@ namespace tests { BOOST_AUTO_TEST_SUITE(Security) BOOST_AUTO_TEST_SUITE(Pib) -BOOST_AUTO_TEST_SUITE(Detail) BOOST_FIXTURE_TEST_SUITE(TestIdentityImpl, ndn::security::tests::PibDataFixture) using security::Pib; @@ -149,7 +148,6 @@ BOOST_AUTO_TEST_CASE(Errors) } BOOST_AUTO_TEST_SUITE_END() // TestIdentityImpl -BOOST_AUTO_TEST_SUITE_END() // Detail BOOST_AUTO_TEST_SUITE_END() // Pib BOOST_AUTO_TEST_SUITE_END() // Security diff --git a/tests/unit/security/pib/impl/key-impl.t.cpp b/tests/unit/security/pib/impl/key-impl.t.cpp index 2a6c641f..b2dfd969 100644 --- a/tests/unit/security/pib/impl/key-impl.t.cpp +++ b/tests/unit/security/pib/impl/key-impl.t.cpp @@ -1,6 +1,6 @@ /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ /* - * Copyright (c) 2013-2019 Regents of the University of California. + * Copyright (c) 2013-2020 Regents of the University of California. * * This file is part of ndn-cxx library (NDN C++ library with eXperimental eXtensions). * @@ -35,7 +35,6 @@ namespace tests { BOOST_AUTO_TEST_SUITE(Security) BOOST_AUTO_TEST_SUITE(Pib) -BOOST_AUTO_TEST_SUITE(Detail) BOOST_FIXTURE_TEST_SUITE(TestKeyImpl, security::tests::PibDataFixture) using security::Pib; @@ -189,7 +188,6 @@ BOOST_AUTO_TEST_CASE(Errors) } BOOST_AUTO_TEST_SUITE_END() // TestKeyImpl -BOOST_AUTO_TEST_SUITE_END() // Detail BOOST_AUTO_TEST_SUITE_END() // Pib BOOST_AUTO_TEST_SUITE_END() // Security