diff options
Diffstat (limited to 'src')
21 files changed, 2233 insertions, 42 deletions
diff --git a/src/bin/agent/ca_process.cc b/src/bin/agent/ca_process.cc index 418cc20307..8c47377a78 100644 --- a/src/bin/agent/ca_process.cc +++ b/src/bin/agent/ca_process.cc @@ -56,9 +56,11 @@ CtrlAgentProcess::run() { // Remove unused listeners within the main loop because new listeners // are created in within a callback method. This avoids removal the // listeners within a callback. - garbageCollectListeners(); + garbageCollectListeners(1); runIO(); } + // Done so removing all listeners. + garbageCollectListeners(0); stopIOService(); } catch (const std::exception& ex) { LOG_FATAL(agent_logger, CTRL_AGENT_FAILED).arg(ex.what()); @@ -192,13 +194,14 @@ CtrlAgentProcess::configure(isc::data::ConstElementPtr config_set, } void -CtrlAgentProcess::garbageCollectListeners() { +CtrlAgentProcess::garbageCollectListeners(size_t leaving) { // We expect only one active listener. If there are more (most likely 2), // it means we have just reconfigured the server and need to shut down all // listeners execept the most recently added. - if (http_listeners_.size() > 1) { + if (http_listeners_.size() > leaving) { // Stop no longer used listeners. - for (auto l = http_listeners_.begin(); l != http_listeners_.end() - 1; + for (auto l = http_listeners_.begin(); + l != http_listeners_.end() - leaving; ++l) { (*l)->stop(); } @@ -207,7 +210,7 @@ CtrlAgentProcess::garbageCollectListeners() { getIoService()->get_io_service().poll(); // Finally, we're ready to remove no longer used listeners. http_listeners_.erase(http_listeners_.begin(), - http_listeners_.end() - 1); + http_listeners_.end() - leaving); } } diff --git a/src/bin/agent/ca_process.h b/src/bin/agent/ca_process.h index 7a5835163f..3e6e08418b 100644 --- a/src/bin/agent/ca_process.h +++ b/src/bin/agent/ca_process.h @@ -1,4 +1,4 @@ -// Copyright (C) 2016-2017 Internet Systems Consortium, Inc. ("ISC") +// Copyright (C) 2016-2021 Internet Systems Consortium, Inc. ("ISC") // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this @@ -130,7 +130,10 @@ private: /// (no longer used because the listening address and port has changed as // a result of the reconfiguration). If there are no listeners additional /// to the one that is currently in use, the method has no effect. - void garbageCollectListeners(); + /// This method is reused to remove all listeners at shutdown time. + /// + /// @param leaving The number of listener to leave (default one). + void garbageCollectListeners(size_t leaving = 1); /// @brief Polls all ready handlers and then runs one handler if none /// handlers have been executed as a result of polling. diff --git a/src/bin/agent/tests/ca_controller_unittests.cc b/src/bin/agent/tests/ca_controller_unittests.cc index cc6709182d..f0f8848027 100644 --- a/src/bin/agent/tests/ca_controller_unittests.cc +++ b/src/bin/agent/tests/ca_controller_unittests.cc @@ -285,6 +285,21 @@ TEST_F(CtrlAgentControllerTest, successfulConfigUpdate) { " }" "}"; + // This check callback is called before the shutdown. + auto check_callback = [&] { + CtrlAgentProcessPtr process = getCtrlAgentProcess(); + ASSERT_TRUE(process); + + // Check that the HTTP listener still exists after reconfiguration. + ConstHttpListenerPtr listener = process->getHttpListener(); + ASSERT_TRUE(listener); + EXPECT_TRUE(process->isListening()); + + // The listener should have been reconfigured to use new address and port. + EXPECT_EQ("127.0.0.1", listener->getLocalAddress().toText()); + EXPECT_EQ(8080, listener->getLocalPort()); + }; + // Schedule reconfiguration. scheduleTimedWrite(second_config, 100); // Schedule SIGHUP signal to trigger reconfiguration. @@ -292,7 +307,9 @@ TEST_F(CtrlAgentControllerTest, successfulConfigUpdate) { // Start the server. time_duration elapsed_time; - runWithConfig(valid_agent_config, 500, elapsed_time); + runWithConfig(valid_agent_config, 500, + static_cast<const TestCallback&>(check_callback), + elapsed_time); CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext(); ASSERT_TRUE(ctx); @@ -305,17 +322,12 @@ TEST_F(CtrlAgentControllerTest, successfulConfigUpdate) { testUnixSocketInfo("dhcp4", "/second/dhcp4/socket"); testUnixSocketInfo("dhcp6", "/second/dhcp6/socket"); + // After the shutdown the HTTP listener no longer exists. CtrlAgentProcessPtr process = getCtrlAgentProcess(); ASSERT_TRUE(process); - - // Check that the HTTP listener still exists after reconfiguration. ConstHttpListenerPtr listener = process->getHttpListener(); - ASSERT_TRUE(listener); - EXPECT_TRUE(process->isListening()); - - // The listener should have been reconfigured to use new address and port. - EXPECT_EQ("127.0.0.1", listener->getLocalAddress().toText()); - EXPECT_EQ(8080, listener->getLocalPort()); + ASSERT_FALSE(listener); + EXPECT_FALSE(process->isListening()); } // Tests that the server continues to use an old configuration when the listener @@ -339,6 +351,20 @@ TEST_F(CtrlAgentControllerTest, unsuccessfulConfigUpdate) { " }" "}"; + // This check callback is called before the shutdown. + auto check_callback = [&] { + CtrlAgentProcessPtr process = getCtrlAgentProcess(); + ASSERT_TRUE(process); + + // We should still be using an original listener. + ConstHttpListenerPtr listener = process->getHttpListener(); + ASSERT_TRUE(listener); + EXPECT_TRUE(process->isListening()); + + EXPECT_EQ("127.0.0.1", listener->getLocalAddress().toText()); + EXPECT_EQ(8081, listener->getLocalPort()); + }; + // Schedule reconfiguration. scheduleTimedWrite(second_config, 100); // Schedule SIGHUP signal to trigger reconfiguration. @@ -346,7 +372,9 @@ TEST_F(CtrlAgentControllerTest, unsuccessfulConfigUpdate) { // Start the server. time_duration elapsed_time; - runWithConfig(valid_agent_config, 500, elapsed_time); + runWithConfig(valid_agent_config, 500, + static_cast<const TestCallback&>(check_callback), + elapsed_time); CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext(); ASSERT_TRUE(ctx); @@ -360,16 +388,12 @@ TEST_F(CtrlAgentControllerTest, unsuccessfulConfigUpdate) { testUnixSocketInfo("dhcp4", "/first/dhcp4/socket"); testUnixSocketInfo("dhcp6", "/first/dhcp6/socket"); + // After the shutdown the HTTP listener no longer exists. CtrlAgentProcessPtr process = getCtrlAgentProcess(); ASSERT_TRUE(process); - - // We should still be using an original listener. ConstHttpListenerPtr listener = process->getHttpListener(); - ASSERT_TRUE(listener); - EXPECT_TRUE(process->isListening()); - - EXPECT_EQ("127.0.0.1", listener->getLocalAddress().toText()); - EXPECT_EQ(8081, listener->getLocalPort()); + ASSERT_FALSE(listener); + EXPECT_FALSE(process->isListening()); } // Tests that it is possible to update the configuration in such a way that the @@ -393,6 +417,20 @@ TEST_F(CtrlAgentControllerTest, noListenerChange) { " }" "}"; + // This check callback is called before the shutdown. + auto check_callback = [&] { + CtrlAgentProcessPtr process = getCtrlAgentProcess(); + ASSERT_TRUE(process); + + // Check that the HTTP listener still exists after reconfiguration. + ConstHttpListenerPtr listener = process->getHttpListener(); + ASSERT_TRUE(listener); + EXPECT_TRUE(process->isListening()); + + EXPECT_EQ("127.0.0.1", listener->getLocalAddress().toText()); + EXPECT_EQ(8081, listener->getLocalPort()); + }; + // Schedule reconfiguration. scheduleTimedWrite(second_config, 100); // Schedule SIGHUP signal to trigger reconfiguration. @@ -400,7 +438,9 @@ TEST_F(CtrlAgentControllerTest, noListenerChange) { // Start the server. time_duration elapsed_time; - runWithConfig(valid_agent_config, 500, elapsed_time); + runWithConfig(valid_agent_config, 500, + static_cast<const TestCallback&>(check_callback), + elapsed_time); CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext(); ASSERT_TRUE(ctx); @@ -415,14 +455,9 @@ TEST_F(CtrlAgentControllerTest, noListenerChange) { CtrlAgentProcessPtr process = getCtrlAgentProcess(); ASSERT_TRUE(process); - - // The listener should keep listening. ConstHttpListenerPtr listener = process->getHttpListener(); - ASSERT_TRUE(listener); - EXPECT_TRUE(process->isListening()); - - EXPECT_EQ("127.0.0.1", listener->getLocalAddress().toText()); - EXPECT_EQ(8081, listener->getLocalPort()); + ASSERT_FALSE(listener); + EXPECT_FALSE(process->isListening()); } // Tests that registerCommands actually registers anything. diff --git a/src/lib/asiolink/Makefile.am b/src/lib/asiolink/Makefile.am index 1ee50f1cf8..831362130a 100644 --- a/src/lib/asiolink/Makefile.am +++ b/src/lib/asiolink/Makefile.am @@ -17,6 +17,7 @@ libkea_asiolink_la_LDFLAGS += $(CRYPTO_LDFLAGS) libkea_asiolink_la_SOURCES = asiolink.h libkea_asiolink_la_SOURCES += asio_wrapper.h libkea_asiolink_la_SOURCES += addr_utilities.cc addr_utilities.h +libkea_asiolink_la_SOURCES += botan_boost_tls.h botan_boost_wrapper.h libkea_asiolink_la_SOURCES += botan_tls.h libkea_asiolink_la_SOURCES += common_tls.cc common_tls.h libkea_asiolink_la_SOURCES += crypto_tls.h @@ -44,8 +45,12 @@ libkea_asiolink_la_SOURCES += unix_domain_socket_acceptor.h libkea_asiolink_la_SOURCES += unix_domain_socket_endpoint.h if HAVE_BOTAN +if HAVE_BOTAN_BOOST +libkea_asiolink_la_SOURCES += botan_boost_tls.cc +else libkea_asiolink_la_SOURCES += botan_tls.cc endif +endif if HAVE_OPENSSL libkea_asiolink_la_SOURCES += openssl_tls.cc endif @@ -63,6 +68,8 @@ libkea_asiolink_include_HEADERS = \ addr_utilities.h \ asio_wrapper.h \ asiolink.h \ + botan_boost_tls.h \ + botan_boost_wrapper.h \ botan_tls.h \ common_tls.h \ crypto_tls.h \ diff --git a/src/lib/asiolink/asiolink.dox b/src/lib/asiolink/asiolink.dox index 8d597a817c..8a74b8b0c0 100644 --- a/src/lib/asiolink/asiolink.dox +++ b/src/lib/asiolink/asiolink.dox @@ -80,6 +80,15 @@ certificates) and two new operations: no direct mapping between high level TLS operations and TCP I/O, e.g. a TLS read can involve a TCP write and the opposite. +@note TLS introduces a new error code "stream_truncated" which is +the TLS short read. + +To debug or extend the TLS support two tools are available: + + - client and server samples for both OpenSSL and Botan. + + - TLS unit tests (tls_unittest.cc file). + @section asiolinkMTConsiderations Multi-Threading Consideration for Boost ASIO Utilities By default Boost ASIO utilities are not thread safe even if Boost ASIO tools diff --git a/src/lib/asiolink/botan_boost_tls.cc b/src/lib/asiolink/botan_boost_tls.cc new file mode 100644 index 0000000000..7d698cc73b --- /dev/null +++ b/src/lib/asiolink/botan_boost_tls.cc @@ -0,0 +1,339 @@ +// Copyright (C) 2021 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +/// @file botan_boost_tls.cc Botan boost ASIO implementation of the TLS API. + +#if defined(WITH_BOTAN) && defined(WITH_BOTAN_BOOST) + +#include <asiolink/asio_wrapper.h> +#include <asiolink/crypto_tls.h> + +#include <botan/auto_rng.h> +#include <botan/certstor_flatfile.h> +#include <botan/data_src.h> +#include <botan/pem.h> +#include <botan/pkcs8.h> + +using namespace isc::cryptolink; + +namespace isc { +namespace asiolink { + +// Classes of Kea certificate stores. +using KeaCertificateStorePath = Botan::Certificate_Store_In_Memory; +using KeaCertificateStoreFile = Botan::Flatfile_Certificate_Store; + +// Class of Kea credential managers. +class KeaCredentialsManager : public Botan::Credentials_Manager { +public: + // Constructor. + KeaCredentialsManager() : store_(), use_stores_(true), certs_(), key_() { + } + + // Destructor. + virtual ~KeaCredentialsManager() { + } + + // CA certificate stores. + // nullptr means do not require or check peer certificate. + std::vector<Botan::Certificate_Store*> + trusted_certificate_authorities(const std::string&, + const std::string&) override { + std::vector<Botan::Certificate_Store*> result; + if (use_stores_ && store_) { + result.push_back(store_.get()); + } + return (result); + } + + // Certificate chain. + std::vector<Botan::X509_Certificate> + cert_chain(const std::vector<std::string>&, + const std::string&, + const std::string&) override { + return (certs_); + } + + // Private key. + Botan::Private_Key* + private_key_for(const Botan::X509_Certificate&, + const std::string&, + const std::string&) override { + return (key_.get()); + } + + // Set the store from a path. + void setStorePath(const std::string& path) { + store_.reset(new KeaCertificateStorePath(path)); + } + + // Set the store from a file. + void setStoreFile(const std::string& file) { + store_.reset(new KeaCertificateStoreFile(file)); + } + + // Get the use of CA certificate stores flag. + bool getUseStores() const { + return (use_stores_); + } + + // Set the use of CA certificate stores flag. + void setUseStores(bool use_stores) { + use_stores_ = use_stores; + } + + // Set the certificate chain. + void setCertChain(const std::string& file) { + Botan::DataSource_Stream source(file); + certs_.clear(); + while (!source.end_of_data()) { + std::string label; + std::vector<uint8_t> cert; + try { + cert = unlock(Botan::PEM_Code::decode(source, label)); + if ((label != "CERTIFICATE") && + (label != "X509 CERTIFICATE") && + (label != "TRUSTED CERTIFICATE")) { + isc_throw(LibraryError, "Expected a certificate, got '" + << label << "'"); + } + certs_.push_back(Botan::X509_Certificate(cert)); + } catch (const std::exception& ex) { + if (certs_.empty()) { + throw; + } + // Got one certificate so skipping garbage. + continue; + } + } + if (certs_.empty()) { + isc_throw(LibraryError, "Found no certificate?"); + } + } + + // Set the private key. + void setPrivateKey(const std::string& file, + Botan::RandomNumberGenerator& rng, + bool& is_rsa) { + key_.reset(Botan::PKCS8::load_key(file, rng)); + if (!key_) { + isc_throw(Unexpected, + "Botan::PKCS8::load_key failed but not threw?"); + } + is_rsa = (key_->algo_name() == "RSA"); + } + + // Pointer to the CA certificate store. + std::unique_ptr<Botan::Certificate_Store> store_; + + // Use the CA ceertificate store flag. + bool use_stores_; + + // The certificate chain. + std::vector<Botan::X509_Certificate> certs_; + + // Pointer to the private key. + std::unique_ptr<Botan::Private_Key> key_; +}; + +// Class of Kea policy. +// Use Strict_Policy? +class KeaPolicy : public Botan::TLS::Default_Policy { +public: + // Constructor. + KeaPolicy() : prefer_rsa_(true) { + } + + // Destructor. + virtual ~KeaPolicy() { + } + + // Allowed signature methods in preference order. + std::vector<std::string> allowed_signature_methods() const override { + if (prefer_rsa_) { + return (AllowedSignatureMethodsRSA); + } else { + return (AllowedSignatureMethodsECDSA); + } + } + + // Disable OSCP. + bool require_cert_revocation_info() const override { + return false; + } + + // Set the RSA preferred flag. + void setPrefRSA(bool prefer_rsa) { + prefer_rsa_ = prefer_rsa; + } + + // Prefer RSA preferred flag. + bool prefer_rsa_; + + // Allowed signature methods which prefers RSA. + static const std::vector<std::string> AllowedSignatureMethodsRSA; + + // Allowed signature methods which prefers ECDSA. + static const std::vector<std::string> AllowedSignatureMethodsECDSA; +}; + + +// Kea session manager. +using KeaSessionManager = Botan::TLS::Session_Manager_Noop; + +// Allowed signature methods which prefers RSA. +const std::vector<std::string> +KeaPolicy::AllowedSignatureMethodsRSA = { "RSA", "DSA", "ECDSA" }; + +// Allowed signature methods which prefers ECDSA. +const std::vector<std::string> +KeaPolicy::AllowedSignatureMethodsECDSA = { "ECDSA", "RSA", "DSA" }; + +// Class of Botan TLS context implementations. +class TlsContextImpl { +public: + // Constructor. + TlsContextImpl() : cred_mgr_(), rng_(), sess_mgr_(), policy_() { + } + + // Destructor. + virtual ~TlsContextImpl() { + } + + // Get the peer certificate requirement mode. + virtual bool getCertRequired() const { + return (cred_mgr_.getUseStores()); + } + + // Set the peer certificate requirement mode. + // + // With Botan this means to provide or not the CA certificate stores. + virtual void setCertRequired(bool cert_required) { + cred_mgr_.setUseStores(cert_required); + } + + // Load the trust anchor aka certificate authority (path). + virtual void loadCaPath(const std::string& ca_path) { + try { + cred_mgr_.setStorePath(ca_path); + } catch (const std::exception& ex) { + isc_throw(LibraryError, ex.what()); + } + } + + // Load the trust anchor aka certificate authority (file). + virtual void loadCaFile(const std::string& ca_file) { + try { + cred_mgr_.setStoreFile(ca_file); + } catch (const std::exception& ex) { + isc_throw(LibraryError, ex.what()); + } + } + + /// @brief Load the certificate file. + virtual void loadCertFile(const std::string& cert_file) { + try { + cred_mgr_.setCertChain(cert_file); + } catch (const std::exception& ex) { + isc_throw(LibraryError, ex.what()); + } + } + + /// @brief Load the private key file. + /// + /// As a side effect set the preference for RSA in the policy. + virtual void loadKeyFile(const std::string& key_file) { + try { + bool is_rsa = true; + cred_mgr_.setPrivateKey(key_file, rng_, is_rsa); + policy_.setPrefRSA(is_rsa); + } catch (const std::exception& ex) { + isc_throw(LibraryError, ex.what()); + } + } + + // Build the context if not yet done. + virtual void build() { + if (context_) { + return; + } + context_.reset(new Botan::TLS::Context(cred_mgr_, + rng_, + sess_mgr_, + policy_)); + } + + virtual Botan::TLS::Context& get() { + return (*context_); + } + + // Credentials Manager. + KeaCredentialsManager cred_mgr_; + + // Random Number Generator. + Botan::AutoSeeded_RNG rng_; + + // Session Manager. + KeaSessionManager sess_mgr_; + + KeaPolicy policy_; + + std::unique_ptr<Botan::TLS::Context> context_; +}; + +TlsContext::~TlsContext() { +} + +TlsContext::TlsContext(TlsRole role) + : TlsContextBase(role), impl_(new TlsContextImpl()) { +} + +Botan::TLS::Context& +TlsContext::getContext() { + impl_->build(); + return (impl_->get()); +} + +void +TlsContext::setCertRequired(bool cert_required) { + if (!cert_required && (getRole() == TlsRole::CLIENT)) { + isc_throw(BadValue, + "'cert-required' parameter must be true for a TLS client"); + } + impl_->setCertRequired(cert_required); +} + +bool +TlsContext::getCertRequired() const { + return (impl_->getCertRequired()); +} + +void +TlsContext::loadCaFile(const std::string& ca_file) { + impl_->loadCaFile(ca_file); +} + +void +TlsContext::loadCaPath(const std::string& ca_path) { + impl_->loadCaPath(ca_path); +} + +void +TlsContext::loadCertFile(const std::string& cert_file) { + impl_->loadCertFile(cert_file); +} + +void +TlsContext::loadKeyFile(const std::string& key_file) { + impl_->loadKeyFile(key_file); +} + +} // namespace asiolink +} // namespace isc + +#endif // WITH_BOTAN && WITH_BOTAN_BOOST diff --git a/src/lib/asiolink/botan_boost_tls.h b/src/lib/asiolink/botan_boost_tls.h new file mode 100644 index 0000000000..9037ebc353 --- /dev/null +++ b/src/lib/asiolink/botan_boost_tls.h @@ -0,0 +1,207 @@ +// Copyright (C) 2021 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Do not include this header directly: use crypto_tls.h instead. + +#ifndef BOTAN_BOOST_TLS_H +#define BOTAN_BOOST_TLS_H + +/// @file botan_boost_tls.h Botan boost ASIO implementation of the TLS API. + +#if defined(WITH_BOTAN) && defined(WITH_BOTAN_BOOST) + +#include <asiolink/asio_wrapper.h> +#include <asiolink/io_asio_socket.h> +#include <asiolink/io_service.h> +#include <asiolink/common_tls.h> +#include <exceptions/exceptions.h> + +#include <asiolink/botan_boost_wrapper.h> +#include <botan/asio_stream.h> + +namespace isc { +namespace asiolink { + +/// @brief Translate TLS role into implementation. +inline Botan::TLS::Connection_Side roleToImpl(TlsRole role) { + if (role == TlsRole::SERVER) { + return (Botan::TLS::Connection_Side::SERVER); + } else { + return (Botan::TLS::Connection_Side::CLIENT); + } +} + +/// @brief Forward declaration of Botan TLS context. +class TlsContextImpl; + +/// @brief Botan boost ASIO TLS context. +class TlsContext : public TlsContextBase { +public: + + /// @brief Destructor. + /// + /// @note The destructor can't be defined here because a unique + /// pointer to an incomplete type is used. + virtual ~TlsContext(); + + /// @brief Create a fresh context. + /// + /// @param role The TLS role client or server. + explicit TlsContext(TlsRole role); + + /// @brief Return the underlying context. + Botan::TLS::Context& getContext(); + + /// @brief Get the peer certificate requirement mode. + /// + /// @return True if peer certificates are required, false if they + /// are optional. + virtual bool getCertRequired() const; + +protected: + /// @brief Set the peer certificate requirement mode. + /// + /// @param cert_required True if peer certificates are required, + /// false if they are optional. + virtual void setCertRequired(bool cert_required); + + /// @brief Load the trust anchor aka certification authority. + /// + /// @param ca_file The certificate file name. + virtual void loadCaFile(const std::string& ca_file); + + /// @brief Load the trust anchor aka certification authority. + /// + /// @param ca_path The certificate directory name. + virtual void loadCaPath(const std::string& ca_path); + + /// @brief Load the certificate file. + /// + /// @param cert_file The certificate file name. + virtual void loadCertFile(const std::string& cert_file); + + /// @brief Load the private key from a file. + /// + /// @param key_file The private key file name. + virtual void loadKeyFile(const std::string& key_file); + + /// @brief Botan TLS context. + std::unique_ptr<TlsContextImpl> impl_; + + /// @brief Allow access to protected methods by the base class. + friend class TlsContextBase; +}; + +/// @brief The type of underlying TLS streams. +typedef Botan::TLS::Stream<boost::asio::ip::tcp::socket> TlsStreamImpl; + +/// @brief TlsStreamBase constructor. +/// +/// @tparam Callback The type of callbacks. +/// @tparam TlsStreamImpl The type of underlying TLS streams. +/// @param service I/O Service object used to manage the stream. +/// @param context Pointer to the TLS context. +/// @note The caller must not provide a null pointer to the TLS context. +template <typename Callback, typename TlsStreamImpl> +TlsStreamBase<Callback, TlsStreamImpl>:: +TlsStreamBase(IOService& service, TlsContextPtr context) + : TlsStreamImpl(service.get_io_service(), context->getContext()), + role_(context->getRole()) { +} + +/// @brief Botan boost ASIO TLS stream. +/// +/// @tparam callback The callback. +template <typename Callback> +class TlsStream : public TlsStreamBase<Callback, TlsStreamImpl> +{ +public: + + /// @brief Type of the base. + typedef TlsStreamBase<Callback, TlsStreamImpl> Base; + + /// @brief Constructor. + /// + /// @param service I/O Service object used to manage the stream. + /// @param context Pointer to the TLS context. + /// @note The caller must not provide a null pointer to the TLS context. + TlsStream(IOService& service, TlsContextPtr context) + : Base(service, context) { + } + + /// @brief Destructor. + virtual ~TlsStream() { } + + /// @brief TLS Handshake. + /// + /// @param callback Callback object. + virtual void handshake(Callback& callback) { + Base::async_handshake(roleToImpl(Base::getRole()), callback); + } + + /// @brief TLS shutdown. + /// + /// @param callback Callback object. + virtual void shutdown(Callback& callback) { + Base::async_shutdown(callback); + } + + /// @brief Clear the TLS object. + /// + /// @note The idea to reuse a TCP connection for a fresh TLS is at + /// least arguable. Currently it does nothing so the socket is + /// **not** reusable. + virtual void clear() { + } + + /// @brief Return the commonName part of the subjectName of + /// the peer certificate. + /// + /// First commonName when there are more than one, in UTF-8. + /// RFC 3280 provides as a commonName example "Susan Housley", + /// to idea to give access to this come from the Role Based + /// Access Control experiment. + /// + /// @return The commonName part of the subjectName or the empty string. + virtual std::string getSubject() { + const std::vector<Botan::X509_Certificate>& cert_chain = + Base::native_handle()->peer_cert_chain(); + if (cert_chain.empty()) { + return (""); + } + const Botan::X509_DN& subject = cert_chain[0].subject_dn(); + return (subject.get_first_attribute("CommonName")); + } + + /// @brief Return the commonName part of the issuerName of + /// the peer certificate. + /// + /// First commonName when there are more than one, in UTF-8. + /// The issuerName is the subjectName of the signing certificate + /// (the issue in PKIX terms). The idea is to encode a group as + /// members of an intermediate certification authority. + /// + /// @return The commonName part of the issuerName or the empty string. + virtual std::string getIssuer() { + const std::vector<Botan::X509_Certificate>& cert_chain = + Base::native_handle()->peer_cert_chain(); + if (cert_chain.empty()) { + return (""); + } + const Botan::X509_DN& issuer = cert_chain[0].issuer_dn(); + return (issuer.get_first_attribute("CommonName")); + } +}; + +// Stream truncated error code. +const int STREAM_TRUNCATED = Botan::TLS::StreamError::StreamTruncated; + +} // namespace asiolink +} // namespace isc + +#endif // WITH_BOTAN && WITH_BOTAN_BOOST + +#endif // BOTAN_BOOST_TLS_H diff --git a/src/lib/asiolink/botan_boost_wrapper.h b/src/lib/asiolink/botan_boost_wrapper.h new file mode 100644 index 0000000000..e244bbb9cd --- /dev/null +++ b/src/lib/asiolink/botan_boost_wrapper.h @@ -0,0 +1,32 @@ +// Copyright (C) 2021 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Do not include this header directly: use crypto_tls.h instead. + +#ifndef BOTAN_BOOST_WRAPPER_H +#define BOTAN_BOOST_WRAPPER_H + +/// @file botan_boost_wrapper.h Botan boost ASIO wrapper. + +#if defined(WITH_BOTAN) && defined(WITH_BOTAN_BOOST) + +/// The error classes do not define virtual destructors. +/// This workaround is taken from the boost header. + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnon-virtual-dtor" +#endif + +#include <botan/asio_error.h> + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic pop +#endif + +#endif // WITH_BOTAN && WITH_BOTAN_BOOST + +#endif // BOTAN_BOOST_WRAPPER_H diff --git a/src/lib/asiolink/botan_tls.cc b/src/lib/asiolink/botan_tls.cc index b1b4d50901..3cd18189d0 100644 --- a/src/lib/asiolink/botan_tls.cc +++ b/src/lib/asiolink/botan_tls.cc @@ -8,7 +8,7 @@ #include <config.h> -#ifdef WITH_BOTAN +#if defined(WITH_BOTAN) && !defined(WITH_BOTAN_BOOST) #include <asiolink/asio_wrapper.h> #include <asiolink/crypto_tls.h> @@ -57,4 +57,4 @@ TlsContext::loadKeyFile(const std::string&) { } // namespace asiolink } // namespace isc -#endif // WITH_BOTAN +#endif // WITH_BOTAN && !WITH_BOTAN_BOOST diff --git a/src/lib/asiolink/botan_tls.h b/src/lib/asiolink/botan_tls.h index b1af58f66a..7f564ca19c 100644 --- a/src/lib/asiolink/botan_tls.h +++ b/src/lib/asiolink/botan_tls.h @@ -11,7 +11,7 @@ /// @file botan_tls.h Botan fake implementation of the TLS API. -#ifdef WITH_BOTAN +#if defined(WITH_BOTAN) && !defined(WITH_BOTAN_BOOST) #include <asiolink/asio_wrapper.h> #include <asiolink/io_asio_socket.h> @@ -168,6 +168,6 @@ public: } // namespace asiolink } // namespace isc -#endif // WITH_BOTAN +#endif // WITH_BOTAN && !WITH_BOTAN_BOOST #endif // BOTAN_TLS_H diff --git a/src/lib/asiolink/crypto_tls.h b/src/lib/asiolink/crypto_tls.h index 256c476707..c3e899febb 100644 --- a/src/lib/asiolink/crypto_tls.h +++ b/src/lib/asiolink/crypto_tls.h @@ -15,6 +15,7 @@ #endif // Include different versions. +#include <asiolink/botan_boost_tls.h> #include <asiolink/botan_tls.h> #include <asiolink/openssl_tls.h> diff --git a/src/lib/asiolink/tests/Makefile.am b/src/lib/asiolink/tests/Makefile.am index 0c14a9c5d6..a402e92267 100644 --- a/src/lib/asiolink/tests/Makefile.am +++ b/src/lib/asiolink/tests/Makefile.am @@ -45,6 +45,11 @@ run_unittests_SOURCES += tls_unittest.cc run_unittests_SOURCES += tls_acceptor_unittest.cc run_unittests_SOURCES += tls_socket_unittest.cc endif +if HAVE_BOTAN_BOOST +run_unittests_SOURCES += tls_unittest.cc +run_unittests_SOURCES += tls_acceptor_unittest.cc +run_unittests_SOURCES += tls_socket_unittest.cc +endif run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) diff --git a/src/lib/asiolink/tests/tls_unittest.cc b/src/lib/asiolink/tests/tls_unittest.cc index 5ebf7ab539..90fba69ea7 100644 --- a/src/lib/asiolink/tests/tls_unittest.cc +++ b/src/lib/asiolink/tests/tls_unittest.cc @@ -89,7 +89,17 @@ public: /// /// Used to shared pointer to state to allow the callback object to /// be copied keeping the state member values. - TestCallback() : state_(new State()) { + TestCallback() + : state_(new State()), tcpp_(0) { + } + + /// @brief Close on error constructor. + /// + /// An overload which takes the stream to close on error. + /// + /// @param tcpp Pointer to the stream to close on error. + TestCallback(TlsStream<TestCallback>::lowest_layer_type* tcpp) + : state_(new State()), tcpp_(tcpp) { } /// @brief Destructor. @@ -102,6 +112,9 @@ public: void operator()(const boost::system::error_code& ec) { state_->called_ = true; state_->error_code_ = ec; + if (ec && tcpp_) { + tcpp_->close(); + } } /// @brief Callback function (two arguments). @@ -110,6 +123,9 @@ public: void operator()(const boost::system::error_code& ec, size_t) { state_->called_ = true; state_->error_code_ = ec; + if (ec && tcpp_) { + tcpp_->close(); + } } /// @brief Get called value. @@ -125,6 +141,9 @@ public: protected: /// @brief Pointer to state. boost::shared_ptr<State> state_; + + /// @brief Pointer to the stream to close on error. + TlsStream<TestCallback>::lowest_layer_type* tcpp_; }; /// @brief The type of a test to be run. @@ -135,7 +154,7 @@ typedef function<void()> Test; /// Some TLS tests can not use the standard GTEST macros because they /// show different behaviors depending on the crypto backend and the /// boost library versions. Worse in some cases the behavior can not -/// be deduced from them so #ifdef macros do not work... +/// be deduced from them so #ifdef's do not work... /// /// Until this is adopted / widespread the policy is to use these flexible /// expected behavior tests ONLY when needed. @@ -409,7 +428,6 @@ TEST(TLSTest, serverContext) { TEST(TLSTest, certRequired) { auto check = [] (TlsContext& ctx) -> bool { #ifdef WITH_BOTAN - /// @todo: Implement it return (ctx.getCertRequired()); #else // WITH_OPENSSL ::SSL_CTX* ssl_ctx = ctx.getNativeContext(); @@ -677,7 +695,7 @@ TEST(TLSTest, configureError) { string key = string(TEST_CA_DIR) + "/kea-client.key"; TlsContext::configure(ctx1, TlsRole::CLIENT, ca, cert, key, true); - // The context is reset on errors. + // The context is reseted on errors. EXPECT_FALSE(ctx1); }); if (Expecteds::displayErrMsg()) { @@ -1340,6 +1358,554 @@ TEST(TLSTest, selfSigned) { } //////////////////////////////////////////////////////////////////////// +// Close on error handshake failures // +//////////////////////////////////////////////////////////////////////// + +// Investigate what happens when a peer closes its streams when the +// handshake callback returns an error. In particular does still +// the other peer timeout? + +// Test what happens when handshake is forgotten. +TEST(TLSTest, noHandshakeCloseonError) { + IOService service; + + // Server part. + TlsContextPtr server_ctx; + test::configServer(server_ctx); + TlsStream<TestCallback> server(service, server_ctx); + + // Accept a client. + tcp::endpoint server_ep(tcp::endpoint(address::from_string(SERVER_ADDRESS), + SERVER_PORT)); + tcp::acceptor acceptor(service.get_io_service(), server_ep); + acceptor.set_option(tcp::acceptor::reuse_address(true)); + TestCallback accept_cb; + acceptor.async_accept(server.lowest_layer(), accept_cb); + + // Client part. + TlsContextPtr client_ctx; + test::configClient(client_ctx); + TlsStream<TestCallback> client(service, client_ctx); + + // Connect to. + client.lowest_layer().open(tcp::v4()); + TestCallback connect_cb; + client.lowest_layer().async_connect(server_ep, connect_cb); + + // Run accept and connect. + while (!accept_cb.getCalled() || !connect_cb.getCalled()) { + service.run_one(); + } + + // Verify the error codes. + if (accept_cb.getCode()) { + FAIL() << "accept error " << accept_cb.getCode().value() + << " '" << accept_cb.getCode().message() << "'"; + } + // Possible EINPROGRESS for the client. + if (connect_cb.getCode() && + (connect_cb.getCode().value() != EINPROGRESS)) { + FAIL() << "connect error " << connect_cb.getCode().value() + << " '" << connect_cb.getCode().message() << "'"; + } + + // Setup a timeout. + IntervalTimer timer1(service); + bool timeout = false; + timer1.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Send on the client. + char send_buf[] = "some text..."; + TestCallback send_cb(&client.lowest_layer()); + async_write(client, boost::asio::buffer(send_buf), send_cb); + while (!timeout && !send_cb.getCalled()) { + service.run_one(); + } + timer1.cancel(); + + Expecteds exps; + // Botan error. + exps.addError("InvalidObjectState"); + // OpenSSL error. + exps.addError("uninitialized"); + exps.checkAsync("send", send_cb); + if (Expecteds::displayErrMsg()) { + std::cout << "send: " << exps.getErrMsg() << "\n"; + } + + // Setup a second timeout. + IntervalTimer timer2(service); + timeout = false; + timer2.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Receive on the server. + vector<char> receive_buf(64); + TestCallback receive_cb; + server.async_read_some(boost::asio::buffer(receive_buf), receive_cb); + while (!timeout && !receive_cb.getCalled()) { + service.run_one(); + } + timer2.cancel(); + + exps.clear(); + // Botan and some OpenSSL. + exps.addError("stream truncated"); + // OpenSSL error, + exps.addError("uninitialized"); + exps.checkAsync("receive", receive_cb); + if (Expecteds::displayErrMsg()) { + std::cout << "receive: " << exps.getErrMsg() << "\n"; + } + + // Close client and server. + EXPECT_NO_THROW(client.lowest_layer().close()); + EXPECT_NO_THROW(server.lowest_layer().close()); +} + +// Test what happens when the server was not configured. +TEST(TLSTest, serverNotConfiguredCloseonError) { + IOService service; + + // Server part. + TlsContextPtr server_ctx(new TlsContext(TlsRole::SERVER)); + // Skip config. + TlsStream<TestCallback> server(service, server_ctx); + + // Accept a client. + tcp::endpoint server_ep(tcp::endpoint(address::from_string(SERVER_ADDRESS), + SERVER_PORT)); + tcp::acceptor acceptor(service.get_io_service(), server_ep); + acceptor.set_option(tcp::acceptor::reuse_address(true)); + TestCallback accept_cb; + acceptor.async_accept(server.lowest_layer(), accept_cb); + + // Client part. + TlsContextPtr client_ctx; + test::configClient(client_ctx); + TlsStream<TestCallback> client(service, client_ctx); + + // Connect to. + client.lowest_layer().open(tcp::v4()); + TestCallback connect_cb; + client.lowest_layer().async_connect(server_ep, connect_cb); + + // Run accept and connect. + while (!accept_cb.getCalled() || !connect_cb.getCalled()) { + service.run_one(); + } + + // Verify the error codes. + if (accept_cb.getCode()) { + FAIL() << "accept error " << accept_cb.getCode().value() + << " '" << accept_cb.getCode().message() << "'"; + } + // Possible EINPROGRESS for the client. + if (connect_cb.getCode() && + (connect_cb.getCode().value() != EINPROGRESS)) { + FAIL() << "connect error " << connect_cb.getCode().value() + << " '" << connect_cb.getCode().message() << "'"; + } + + // Setup a timeout. + IntervalTimer timer(service); + bool timeout = false; + timer.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Perform TLS handshakes. + TestCallback server_cb(&server.lowest_layer()); + server.handshake(server_cb); + TestCallback client_cb; + client.handshake(client_cb); + while (!timeout && (!server_cb.getCalled() || !client_cb.getCalled())) { + service.run_one(); + } + timer.cancel(); + + Expecteds exps; + // Botan error. + exps.addError("handshake_failure"); + // LibreSSL error. + exps.addError("no shared cipher"); + // OpenSSL error. + exps.addError("sslv3 alert handshake failure"); + exps.checkAsync("server", server_cb); + if (Expecteds::displayErrMsg()) { + std::cout << "server: " << exps.getErrMsg() << "\n"; + } + + exps.clear(); + // Botan and some OpenSSL. + exps.addError("stream truncated"); + // OpenSSL error. + exps.addError("sslv3 alert handshake failure"); + exps.checkAsync("client", client_cb); + if (Expecteds::displayErrMsg()) { + std::cout << "client: " << exps.getErrMsg() << "\n"; + } + + // Close client and server. + EXPECT_NO_THROW(client.lowest_layer().close()); + EXPECT_NO_THROW(server.lowest_layer().close()); +} + +// Test what happens when the client was not configured. +TEST(TLSTest, clientNotConfiguredCloseonError) { + IOService service; + + // Server part. + TlsContextPtr server_ctx; + test::configServer(server_ctx); + TlsStream<TestCallback> server(service, server_ctx); + + // Accept a client. + tcp::endpoint server_ep(tcp::endpoint(address::from_string(SERVER_ADDRESS), + SERVER_PORT)); + tcp::acceptor acceptor(service.get_io_service(), server_ep); + acceptor.set_option(tcp::acceptor::reuse_address(true)); + TestCallback accept_cb; + acceptor.async_accept(server.lowest_layer(), accept_cb); + + // Client part. + TlsContextPtr client_ctx(new TlsContext(TlsRole::CLIENT)); + // Skip config. + TlsStream<TestCallback> client(service, client_ctx); + + // Connect to. + client.lowest_layer().open(tcp::v4()); + TestCallback connect_cb; + client.lowest_layer().async_connect(server_ep, connect_cb); + + // Run accept and connect. + while (!accept_cb.getCalled() || !connect_cb.getCalled()) { + service.run_one(); + } + + // Verify the error codes. + if (accept_cb.getCode()) { + FAIL() << "accept error " << accept_cb.getCode().value() + << " '" << accept_cb.getCode().message() << "'"; + } + // Possible EINPROGRESS for the client. + if (connect_cb.getCode() && + (connect_cb.getCode().value() != EINPROGRESS)) { + FAIL() << "connect error " << connect_cb.getCode().value() + << " '" << connect_cb.getCode().message() << "'"; + } + + // Setup a timeout. + IntervalTimer timer(service); + bool timeout = false; + timer.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Perform TLS handshakes. + TestCallback server_cb; + server.async_handshake(roleToImpl(TlsRole::SERVER), server_cb); + TestCallback client_cb(&client.lowest_layer()); + client.async_handshake(roleToImpl(TlsRole::CLIENT), client_cb); + while (!timeout && (!server_cb.getCalled() || !client_cb.getCalled())) { + service.run_one(); + } + timer.cancel(); + + Expecteds exps; + // Botan and some OpenSSL. + exps.addError("stream truncated"); + // OpenSSL error. + exps.addError("tlsv1 alert unknown ca"); + exps.checkAsync("server", server_cb); + if (Expecteds::displayErrMsg()) { + std::cout << "server: " << exps.getErrMsg() << "\n"; + } + + exps.clear(); + // Botan error (unfortunately a bit generic). + exps.addError("bad_certificate"); + // LibreSSL error. + exps.addError("tlsv1 alert unknown ca"); + // OpenSSL error. + exps.addError("certificate verify failed"); + // The client should not hang. + exps.checkAsync("client", client_cb); + if (Expecteds::displayErrMsg()) { + std::cout << "client: " << exps.getErrMsg() << "\n"; + } + + // Close client and server. + EXPECT_NO_THROW(client.lowest_layer().close()); + EXPECT_NO_THROW(server.lowest_layer().close()); +} + +// Test what happens when the client is HTTP (vs HTTPS). +TEST(TLSTest, clientHTTPnoSCloseonError) { + IOService service; + + // Server part. + TlsContextPtr server_ctx; + test::configServer(server_ctx); + TlsStream<TestCallback> server(service, server_ctx); + + // Accept a client. + tcp::endpoint server_ep(tcp::endpoint(address::from_string(SERVER_ADDRESS), + SERVER_PORT)); + tcp::acceptor acceptor(service.get_io_service(), server_ep); + acceptor.set_option(tcp::acceptor::reuse_address(true)); + TestCallback accept_cb; + acceptor.async_accept(server.lowest_layer(), accept_cb); + + // Client part. + tcp::socket client(service.get_io_service()); + + // Connect to. + client.open(tcp::v4()); + TestCallback connect_cb; + client.async_connect(server_ep, connect_cb); + + // Run accept and connect. + while (!accept_cb.getCalled() || !connect_cb.getCalled()) { + service.run_one(); + } + + // Verify the error codes. + if (accept_cb.getCode()) { + FAIL() << "accept error " << accept_cb.getCode().value() + << " '" << accept_cb.getCode().message() << "'"; + } + // Possible EINPROGRESS for the client. + if (connect_cb.getCode() && + (connect_cb.getCode().value() != EINPROGRESS)) { + FAIL() << "connect error " << connect_cb.getCode().value() + << " '" << connect_cb.getCode().message() << "'"; + } + + // Setup a timeout. + IntervalTimer timer(service); + bool timeout = false; + timer.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Perform server TLS handshake. + TestCallback server_cb; + server.async_handshake(roleToImpl(TlsRole::SERVER), server_cb); + + // Client sending a HTTP GET. + char send_buf[] = "GET / HTTP/1.1\r\n"; + TestCallback client_cb; + client.async_send(boost::asio::buffer(send_buf), client_cb); + + while (!timeout && (!server_cb.getCalled() || !client_cb.getCalled())) { + service.run_one(); + } + timer.cancel(); + + Expecteds exps; + // Botan server still hangs. + // Reading the Botan code it really expects a TLS record and is fooled + // by the input and just want more... To summary it is a little naive. + exps.addTimeout(); + // LibreSSL error. + exps.addError("tlsv1 alert protocol version"); + // OpenSSL error (OpenSSL recognizes HTTP). + exps.addError("http request"); + // Another OpenSSL error (not all OpenSSL recognizes HTTP). + exps.addError("wrong version number"); + exps.checkAsync("server", server_cb); + if (Expecteds::displayErrMsg()) { + if (timeout) { + std::cout << "server timeout\n"; + } else { + std::cout << "server: " << exps.getErrMsg() << "\n"; + } + } + + // No error at the client. + EXPECT_TRUE(client_cb.getCalled()); + EXPECT_FALSE(client_cb.getCode()); + + // Close client and server. + EXPECT_NO_THROW(client.lowest_layer().close()); + EXPECT_NO_THROW(server.lowest_layer().close()); +} + +// Test what happens when the client uses a certificate from another CA. +TEST(TLSTest, anotherClientCloseonError) { + IOService service; + + // Server part. + TlsContextPtr server_ctx; + test::configServer(server_ctx); + TlsStream<TestCallback> server(service, server_ctx); + + // Accept a client. + tcp::endpoint server_ep(tcp::endpoint(address::from_string(SERVER_ADDRESS), + SERVER_PORT)); + tcp::acceptor acceptor(service.get_io_service(), server_ep); + acceptor.set_option(tcp::acceptor::reuse_address(true)); + TestCallback accept_cb; + acceptor.async_accept(server.lowest_layer(), accept_cb); + + // Client part using a certificate signed by another CA. + TlsContextPtr client_ctx; + test::configOther(client_ctx); + TlsStream<TestCallback> client(service, client_ctx); + + // Connect to. + client.lowest_layer().open(tcp::v4()); + TestCallback connect_cb; + client.lowest_layer().async_connect(server_ep, connect_cb); + + // Run accept and connect. + while (!accept_cb.getCalled() || !connect_cb.getCalled()) { + service.run_one(); + } + + // Verify the error codes. + if (accept_cb.getCode()) { + FAIL() << "accept error " << accept_cb.getCode().value() + << " '" << accept_cb.getCode().message() << "'"; + } + // Possible EINPROGRESS for the client. + if (connect_cb.getCode() && + (connect_cb.getCode().value() != EINPROGRESS)) { + FAIL() << "connect error " << connect_cb.getCode().value() + << " '" << connect_cb.getCode().message() << "'"; + } + + // Setup a timeout. + IntervalTimer timer(service); + bool timeout = false; + timer.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Perform TLS handshakes. + TestCallback server_cb(&server.lowest_layer()); + server.async_handshake(roleToImpl(TlsRole::SERVER), server_cb); + TestCallback client_cb; + client.async_handshake(roleToImpl(TlsRole::CLIENT), client_cb); + while (!timeout && (!server_cb.getCalled() || !client_cb.getCalled())) { + service.run_one(); + } + timer.cancel(); + + Expecteds exps; + // Botan error. + exps.addError("bad_certificate"); + // LibreSSL error. + exps.addError("tlsv1 alert unknown ca"); + // OpenSSL error. + // Full error is: + // error 20 at 0 depth lookup:unable to get local issuer certificate + exps.addError("certificate verify failed"); + exps.checkAsync("server", server_cb); + if (Expecteds::displayErrMsg()) { + std::cout << "server: " << exps.getErrMsg() << "\n"; + } + + exps.clear(); + // Botan and some OpenSSL. + exps.addError("stream truncated"); + // LibreSSL and recent OpenSSL do not fail. + exps.addNoError(); + // Old OpenSSL error. + exps.addError("tlsv1 alert unknown ca"); + exps.checkAsync("client", client_cb); + if (Expecteds::displayErrMsg() && exps.hasErrMsg()) { + std::cout << "client: " << exps.getErrMsg() << "\n"; + } + + // Close client and server. + EXPECT_NO_THROW(client.lowest_layer().close()); + EXPECT_NO_THROW(server.lowest_layer().close()); +} + +// Test what happens when the client uses a self-signed certificate. +TEST(TLSTest, selfSignedCloseonError) { + IOService service; + + // Server part. + TlsContextPtr server_ctx; + test::configServer(server_ctx); + TlsStream<TestCallback> server(service, server_ctx); + + // Accept a client. + tcp::endpoint server_ep(tcp::endpoint(address::from_string(SERVER_ADDRESS), + SERVER_PORT)); + tcp::acceptor acceptor(service.get_io_service(), server_ep); + acceptor.set_option(tcp::acceptor::reuse_address(true)); + TestCallback accept_cb; + acceptor.async_accept(server.lowest_layer(), accept_cb); + + // Client part using a self-signed certificate. + TlsContextPtr client_ctx; + test::configSelf(client_ctx); + TlsStream<TestCallback> client(service, client_ctx); + + // Connect to. + client.lowest_layer().open(tcp::v4()); + TestCallback connect_cb; + client.lowest_layer().async_connect(server_ep, connect_cb); + + // Run accept and connect. + while (!accept_cb.getCalled() || !connect_cb.getCalled()) { + service.run_one(); + } + + // Verify the error codes. + if (accept_cb.getCode()) { + FAIL() << "accept error " << accept_cb.getCode().value() + << " '" << accept_cb.getCode().message() << "'"; + } + // Possible EINPROGRESS for the client. + if (connect_cb.getCode() && + (connect_cb.getCode().value() != EINPROGRESS)) { + FAIL() << "connect error " << connect_cb.getCode().value() + << " '" << connect_cb.getCode().message() << "'"; + } + + // Setup a timeout. + IntervalTimer timer(service); + bool timeout = false; + timer.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Perform TLS handshakes. + TestCallback server_cb(&server.lowest_layer()); + server.async_handshake(roleToImpl(TlsRole::SERVER), server_cb); + TestCallback client_cb; + client.async_handshake(roleToImpl(TlsRole::CLIENT), client_cb); + while (!timeout && (!server_cb.getCalled() || !client_cb.getCalled())) { + service.run_one(); + } + timer.cancel(); + + Expecteds exps; + // Botan error. + exps.addError("bad_certificate"); + // LibreSSL error. + exps.addError("tlsv1 alert unknown ca"); + // OpenSSL error. + // Full error is: + // error 18 at 0 depth lookup:self signed certificate + exps.addError("certificate verify failed"); + exps.checkAsync("server", server_cb); + if (Expecteds::displayErrMsg()) { + std::cout << "server: " << exps.getErrMsg() << "\n"; + } + + exps.clear(); + // Botan and some OpenSSL. + exps.addError("stream truncated"); + // LibreSSL and recent OpenSSL do not fail. + exps.addNoError(); + // Old OpenSSL error. + exps.addError("tlsv1 alert unknown ca"); + exps.checkAsync("client", client_cb); + if (Expecteds::displayErrMsg() && exps.hasErrMsg()) { + std::cout << "client: " << exps.getErrMsg() << "\n"; + } + + // Close client and server. + EXPECT_NO_THROW(client.lowest_layer().close()); + EXPECT_NO_THROW(server.lowest_layer().close()); +} + +//////////////////////////////////////////////////////////////////////// // TLS handshake corner case // //////////////////////////////////////////////////////////////////////// @@ -1563,4 +2129,444 @@ TEST(TLSTest, trustedSelfSigned) { } #endif // WITH_OPENSSL +//////////////////////////////////////////////////////////////////////// +// TLS shutdown // +//////////////////////////////////////////////////////////////////////// + +// Investigate the TLS shutdown processing. + +// Test what happens when the shutdown receiver is inactive. +TEST(TLSTest, shutdownInactive) { + IOService service; + + // Server part. + TlsContextPtr server_ctx(new TlsContext(TlsRole::SERVER)); + test::configServer(server_ctx); + TlsStream<TestCallback> server(service, server_ctx); + + // Accept a client. + tcp::endpoint server_ep(tcp::endpoint(address::from_string(SERVER_ADDRESS), + SERVER_PORT)); + tcp::acceptor acceptor(service.get_io_service(), server_ep); + acceptor.set_option(tcp::acceptor::reuse_address(true)); + TestCallback accept_cb; + acceptor.async_accept(server.lowest_layer(), accept_cb); + + // Client part. + TlsContextPtr client_ctx; + test::configClient(client_ctx); + TlsStream<TestCallback> client(service, client_ctx); + + // Connect to. + client.lowest_layer().open(tcp::v4()); + TestCallback connect_cb; + client.lowest_layer().async_connect(server_ep, connect_cb); + + // Run accept and connect. + while (!accept_cb.getCalled() || !connect_cb.getCalled()) { + service.run_one(); + } + + // Verify the error codes. + if (accept_cb.getCode()) { + FAIL() << "accept error " << accept_cb.getCode().value() + << " '" << accept_cb.getCode().message() << "'"; + } + // Possible EINPROGRESS for the client. + if (connect_cb.getCode() && + (connect_cb.getCode().value() != EINPROGRESS)) { + FAIL() << "connect error " << connect_cb.getCode().value() + << " '" << connect_cb.getCode().message() << "'"; + } + + // Setup a timeout. + IntervalTimer timer(service); + bool timeout = false; + timer.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Perform TLS handshakes. + TestCallback server_cb(&server.lowest_layer()); + server.handshake(server_cb); + TestCallback client_cb; + client.handshake(client_cb); + while (!timeout && (!server_cb.getCalled() || !client_cb.getCalled())) { + service.run_one(); + } + timer.cancel(); + + // No problem is expected. + EXPECT_FALSE(timeout); + EXPECT_TRUE(server_cb.getCalled()); + EXPECT_FALSE(server_cb.getCode()); + EXPECT_TRUE(client_cb.getCalled()); + EXPECT_FALSE(client_cb.getCode()); + + // Setup a timeout for the shutdown. + IntervalTimer timer2(service); + timeout = false; + timer2.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Shutdown on the client leaving the server inactive. + TestCallback shutdown_cb; + client.shutdown(shutdown_cb); + while (!timeout && !shutdown_cb.getCalled()) { + service.run_one(); + } + timer2.cancel(); + + Expecteds exps; + // Botan gets no error. + exps.addNoError(); + // OpenSSL hangs. + exps.addTimeout(); + exps.checkAsync("shutdown", shutdown_cb); + if (Expecteds::displayErrMsg()) { + if (timeout) { + std::cout << "shutdown timeout\n"; + } else if (exps.hasErrMsg()) { + std::cout << "shutdown: " << exps.getErrMsg() << "\n"; + } + } + + // Close client and server. + EXPECT_NO_THROW(client.lowest_layer().close()); + EXPECT_NO_THROW(server.lowest_layer().close()); +} + +// Test what happens when the shutdown receiver is active. +TEST(TLSTest, shutdownActive) { + IOService service; + + // Server part. + TlsContextPtr server_ctx(new TlsContext(TlsRole::SERVER)); + test::configServer(server_ctx); + TlsStream<TestCallback> server(service, server_ctx); + + // Accept a client. + tcp::endpoint server_ep(tcp::endpoint(address::from_string(SERVER_ADDRESS), + SERVER_PORT)); + tcp::acceptor acceptor(service.get_io_service(), server_ep); + acceptor.set_option(tcp::acceptor::reuse_address(true)); + TestCallback accept_cb; + acceptor.async_accept(server.lowest_layer(), accept_cb); + + // Client part. + TlsContextPtr client_ctx; + test::configClient(client_ctx); + TlsStream<TestCallback> client(service, client_ctx); + + // Connect to. + client.lowest_layer().open(tcp::v4()); + TestCallback connect_cb; + client.lowest_layer().async_connect(server_ep, connect_cb); + + // Run accept and connect. + while (!accept_cb.getCalled() || !connect_cb.getCalled()) { + service.run_one(); + } + + // Verify the error codes. + if (accept_cb.getCode()) { + FAIL() << "accept error " << accept_cb.getCode().value() + << " '" << accept_cb.getCode().message() << "'"; + } + // Possible EINPROGRESS for the client. + if (connect_cb.getCode() && + (connect_cb.getCode().value() != EINPROGRESS)) { + FAIL() << "connect error " << connect_cb.getCode().value() + << " '" << connect_cb.getCode().message() << "'"; + } + + // Setup a timeout. + IntervalTimer timer(service); + bool timeout = false; + timer.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Perform TLS handshakes. + TestCallback server_cb(&server.lowest_layer()); + server.handshake(server_cb); + TestCallback client_cb; + client.handshake(client_cb); + while (!timeout && (!server_cb.getCalled() || !client_cb.getCalled())) { + service.run_one(); + } + timer.cancel(); + + // No problem is expected. + EXPECT_FALSE(timeout); + EXPECT_TRUE(server_cb.getCalled()); + EXPECT_FALSE(server_cb.getCode()); + EXPECT_TRUE(client_cb.getCalled()); + EXPECT_FALSE(client_cb.getCode()); + + // Setup a timeout for the shutdown and receive. + IntervalTimer timer2(service); + timeout = false; + timer2.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Receive on the server. + vector<char> receive_buf(64); + TestCallback receive_cb; + server.async_read_some(boost::asio::buffer(receive_buf), receive_cb); + + // Shutdown on the client. + TestCallback shutdown_cb; + client.shutdown(shutdown_cb); + while (!timeout && (!shutdown_cb.getCalled() || !receive_cb.getCalled())) { + service.run_one(); + } + timer2.cancel(); + + Expecteds exps; + // Botan gets no error. + exps.addNoError(); + // OpenSSL hangs. + exps.addTimeout(); + exps.checkAsync("shutdown", shutdown_cb); + if (Expecteds::displayErrMsg()) { + if (timeout) { + std::cout << "shutdown timeout\n"; + } else if (exps.hasErrMsg()) { + std::cout << "shutdown: " << exps.getErrMsg() << "\n"; + } + } + + exps.clear(); + // End of file on the receive side. + exps.addError("End of file"); + exps.checkAsync("receive", receive_cb); + if (Expecteds::displayErrMsg()) { + std::cout << "receive: " << exps.getErrMsg() << "\n"; + } + + // Close client and server. + EXPECT_NO_THROW(client.lowest_layer().close()); + EXPECT_NO_THROW(server.lowest_layer().close()); +} + +// Test what happens when the shutdown receiver is inactive on shutdown +// and immediate close. +TEST(TLSTest, shutdownCloseInactive) { + IOService service; + + // Server part. + TlsContextPtr server_ctx(new TlsContext(TlsRole::SERVER)); + test::configServer(server_ctx); + TlsStream<TestCallback> server(service, server_ctx); + + // Accept a client. + tcp::endpoint server_ep(tcp::endpoint(address::from_string(SERVER_ADDRESS), + SERVER_PORT)); + tcp::acceptor acceptor(service.get_io_service(), server_ep); + acceptor.set_option(tcp::acceptor::reuse_address(true)); + TestCallback accept_cb; + acceptor.async_accept(server.lowest_layer(), accept_cb); + + // Client part. + TlsContextPtr client_ctx; + test::configClient(client_ctx); + TlsStream<TestCallback> client(service, client_ctx); + + // Connect to. + client.lowest_layer().open(tcp::v4()); + TestCallback connect_cb; + client.lowest_layer().async_connect(server_ep, connect_cb); + + // Run accept and connect. + while (!accept_cb.getCalled() || !connect_cb.getCalled()) { + service.run_one(); + } + + // Verify the error codes. + if (accept_cb.getCode()) { + FAIL() << "accept error " << accept_cb.getCode().value() + << " '" << accept_cb.getCode().message() << "'"; + } + // Possible EINPROGRESS for the client. + if (connect_cb.getCode() && + (connect_cb.getCode().value() != EINPROGRESS)) { + FAIL() << "connect error " << connect_cb.getCode().value() + << " '" << connect_cb.getCode().message() << "'"; + } + + // Setup a timeout. + IntervalTimer timer(service); + bool timeout = false; + timer.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Perform TLS handshakes. + TestCallback server_cb(&server.lowest_layer()); + server.handshake(server_cb); + TestCallback client_cb; + client.handshake(client_cb); + while (!timeout && (!server_cb.getCalled() || !client_cb.getCalled())) { + service.run_one(); + } + timer.cancel(); + + // No problem is expected. + EXPECT_FALSE(timeout); + EXPECT_TRUE(server_cb.getCalled()); + EXPECT_FALSE(server_cb.getCode()); + EXPECT_TRUE(client_cb.getCalled()); + EXPECT_FALSE(client_cb.getCode()); + + // Setup a timeout for the shutdown. + IntervalTimer timer2(service); + timeout = false; + timer2.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Shutdown on the client leaving the server inactive. + TestCallback shutdown_cb; + client.shutdown(shutdown_cb); + + // Post a close which should be called after the shutdown. + service.post([&client] { client.lowest_layer().close(); }); + while (!timeout && !shutdown_cb.getCalled()) { + service.run_one(); + } + timer2.cancel(); + + Expecteds exps; + // Botan gets no error. + exps.addNoError(); + // LibreSSL and some old OpenSSL gets Operation canceled. + exps.addError("Operation canceled"); + // OpenSSL gets Bad file descriptor. + exps.addError("Bad file descriptor"); + exps.checkAsync("shutdown", shutdown_cb); + if (Expecteds::displayErrMsg()) { + if (timeout) { + std::cout << "shutdown timeout\n"; + } else if (exps.hasErrMsg()) { + std::cout << "shutdown: " << exps.getErrMsg() << "\n"; + } + } + + // Close client and server. + EXPECT_NO_THROW(client.lowest_layer().close()); + EXPECT_NO_THROW(server.lowest_layer().close()); +} + +// Test what happens when the shutdown receiver is active with an +// immediate close. +TEST(TLSTest, shutdownCloseActive) { + IOService service; + + // Server part. + TlsContextPtr server_ctx(new TlsContext(TlsRole::SERVER)); + test::configServer(server_ctx); + TlsStream<TestCallback> server(service, server_ctx); + + // Accept a client. + tcp::endpoint server_ep(tcp::endpoint(address::from_string(SERVER_ADDRESS), + SERVER_PORT)); + tcp::acceptor acceptor(service.get_io_service(), server_ep); + acceptor.set_option(tcp::acceptor::reuse_address(true)); + TestCallback accept_cb; + acceptor.async_accept(server.lowest_layer(), accept_cb); + + // Client part. + TlsContextPtr client_ctx; + test::configClient(client_ctx); + TlsStream<TestCallback> client(service, client_ctx); + + // Connect to. + client.lowest_layer().open(tcp::v4()); + TestCallback connect_cb; + client.lowest_layer().async_connect(server_ep, connect_cb); + + // Run accept and connect. + while (!accept_cb.getCalled() || !connect_cb.getCalled()) { + service.run_one(); + } + + // Verify the error codes. + if (accept_cb.getCode()) { + FAIL() << "accept error " << accept_cb.getCode().value() + << " '" << accept_cb.getCode().message() << "'"; + } + // Possible EINPROGRESS for the client. + if (connect_cb.getCode() && + (connect_cb.getCode().value() != EINPROGRESS)) { + FAIL() << "connect error " << connect_cb.getCode().value() + << " '" << connect_cb.getCode().message() << "'"; + } + + // Setup a timeout. + IntervalTimer timer(service); + bool timeout = false; + timer.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Perform TLS handshakes. + TestCallback server_cb(&server.lowest_layer()); + server.handshake(server_cb); + TestCallback client_cb; + client.handshake(client_cb); + while (!timeout && (!server_cb.getCalled() || !client_cb.getCalled())) { + service.run_one(); + } + timer.cancel(); + + // No problem is expected. + EXPECT_FALSE(timeout); + EXPECT_TRUE(server_cb.getCalled()); + EXPECT_FALSE(server_cb.getCode()); + EXPECT_TRUE(client_cb.getCalled()); + EXPECT_FALSE(client_cb.getCode()); + + // Setup a timeout for the shutdown and receive. + IntervalTimer timer2(service); + timeout = false; + timer2.setup([&timeout] { timeout = true; }, 100, IntervalTimer::ONE_SHOT); + + // Receive on the server. + vector<char> receive_buf(64); + TestCallback receive_cb; + server.async_read_some(boost::asio::buffer(receive_buf), receive_cb); + + // Shutdown on the client. + TestCallback shutdown_cb; + client.shutdown(shutdown_cb); + + // Post a close which should be called after the shutdown. + service.post([&client] { client.lowest_layer().close(); }); + while (!timeout && (!shutdown_cb.getCalled() || !receive_cb.getCalled())) { + service.run_one(); + } + timer2.cancel(); + + Expecteds exps; + // Botan gets no error. + exps.addNoError(); + // LibreSSL and some old OpenSSL gets Operation canceled. + exps.addError("Operation canceled"); + // OpenSSL gets Bad file descriptor. + exps.addError("Bad file descriptor"); + exps.checkAsync("shutdown", shutdown_cb); + if (Expecteds::displayErrMsg()) { + if (timeout) { + std::cout << "shutdown timeout\n"; + } else if (exps.hasErrMsg()) { + std::cout << "shutdown: " << exps.getErrMsg() << "\n"; + } + } + + // End of file on the receive side. + EXPECT_TRUE(receive_cb.getCalled()); + EXPECT_TRUE(receive_cb.getCode()); + EXPECT_EQ("End of file", receive_cb.getCode().message()); + if (Expecteds::displayErrMsg()) { + std::cout << "receive: " << receive_cb.getCode().message() << "\n"; + } + + // Close client and server. + EXPECT_NO_THROW(client.lowest_layer().close()); + EXPECT_NO_THROW(server.lowest_layer().close()); +} + +// Conclusion about the shutdown: do the close on completion (e.g. in the +// handler) or on timeout (i.e. simulate an asynchronous shutdown with +// timeout). + } // end of anonymous namespace. diff --git a/src/lib/asiolink/testutils/.gitignore b/src/lib/asiolink/testutils/.gitignore index 89d503c646..cc30b7cbc2 100644 --- a/src/lib/asiolink/testutils/.gitignore +++ b/src/lib/asiolink/testutils/.gitignore @@ -1,2 +1,4 @@ +/botan_boost_sample_client +/botan_boost_sample_server /openssl_sample_client /openssl_sample_server diff --git a/src/lib/asiolink/testutils/Makefile.am b/src/lib/asiolink/testutils/Makefile.am index f30ab88d46..cb06448a70 100644 --- a/src/lib/asiolink/testutils/Makefile.am +++ b/src/lib/asiolink/testutils/Makefile.am @@ -74,4 +74,20 @@ openssl_sample_server_CPPFLAGS = $(AM_CPPFLAGS) openssl_sample_server_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) openssl_sample_server_LDADD = $(BOOST_LIBS) $(CRYPTO_LIBS) endif + +if HAVE_BOTAN_BOOST +# Same samples ported to Botan boost ASIO. + +noinst_PROGRAMS = botan_boost_sample_client botan_boost_sample_server + +botan_boost_sample_client_SOURCES = botan_boost_sample_client.cc +botan_boost_sample_client_CPPFLAGS = $(AM_CPPFLAGS) +botan_boost_sample_client_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) +botan_boost_sample_client_LDADD = $(BOOST_LIBS) $(CRYPTO_LIBS) + +botan_boost_sample_server_SOURCES = botan_boost_sample_server.cc +botan_boost_sample_server_CPPFLAGS = $(AM_CPPFLAGS) +botan_boost_sample_server_LDFLAGS = $(AM_LDFLAGS) $(CRYPTO_LDFLAGS) +botan_boost_sample_server_LDADD = $(BOOST_LIBS) $(CRYPTO_LIBS) +endif endif diff --git a/src/lib/asiolink/testutils/botan_boost_sample_client.cc b/src/lib/asiolink/testutils/botan_boost_sample_client.cc new file mode 100644 index 0000000000..8049e965a8 --- /dev/null +++ b/src/lib/asiolink/testutils/botan_boost_sample_client.cc @@ -0,0 +1,229 @@ +// +// client.cpp +// ~~~~~~~~~~ +// +// Copyright (c) 2003-2020 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include <config.h> + +#include <cstdlib> +#include <cstring> +#include <functional> +#include <iostream> +#include <boost/asio.hpp> + +#include <asiolink/botan_boost_wrapper.h> +#include <botan/asio_stream.h> +#include <botan/certstor_flatfile.h> +#include <botan/pkcs8.h> +#include <botan/auto_rng.h> + +inline std::string CA_(const std::string& filename) { + return (std::string(TEST_CA_DIR) + "/" + filename); +} + +using boost::asio::ip::tcp; + +enum { max_length = 1024 }; + +using Client_Certificate_Store = Botan::Flatfile_Certificate_Store; + +class Client_Credentials_Manager : public Botan::Credentials_Manager +{ +public: + explicit Client_Credentials_Manager(Botan::RandomNumberGenerator& rng) + : stores_(), certs_(), + store_(new Client_Certificate_Store(CA_("kea-ca.crt"))), + cert_(Botan::X509_Certificate(CA_("kea-client.crt"))), + key_(Botan::PKCS8::load_key(CA_("kea-client.key"), rng)) + { + stores_.push_back(store_.get()); + certs_.push_back(cert_); + } + + virtual ~Client_Credentials_Manager() + { + } + + std::vector<Botan::Certificate_Store*> + trusted_certificate_authorities(const std::string&, + const std::string&) override + { + return stores_; + } + + std::vector<Botan::X509_Certificate> + cert_chain(const std::vector<std::string>&, + const std::string&, + const std::string&) override + { + return certs_; + } + + Botan::Private_Key* + private_key_for(const Botan::X509_Certificate&, + const std::string&, + const std::string&) override + { + return key_.get(); + } + + std::vector<Botan::Certificate_Store*> stores_; + std::vector<Botan::X509_Certificate> certs_; + std::shared_ptr<Botan::Certificate_Store> store_; + Botan::X509_Certificate cert_; + std::unique_ptr<Botan::Private_Key> key_; +}; + +using Client_Session_Manager = Botan::TLS::Session_Manager_Noop; + +class Client_Policy : public Botan::TLS::Default_Policy { +public: + virtual ~Client_Policy() + { + } + + std::vector<std::string> allowed_signature_methods() const override + { + return { "RSA", "ECDSA", "IMPLICIT" }; + } + + bool require_cert_revocation_info() const override + { + return false; + } +}; + +class client +{ +public: + client(boost::asio::io_service& io_context, + Botan::TLS::Context& context, + const tcp::endpoint& endpoint) + : socket_(io_context, context) + { + connect(endpoint); + } + +private: + void connect(const tcp::endpoint& endpoint) + { + socket_.lowest_layer().async_connect(endpoint, + [this](const boost::system::error_code& error) + { + if (!error) + { + handshake(); + } + else + { + std::cout << "Connect failed: " << error.message() << "\n"; + } + }); + } + + void handshake() + { + socket_.async_handshake(Botan::TLS::Connection_Side::CLIENT, + [this](const boost::system::error_code& error) + { + if (!error) + { + // Print the certificate's subject name. + const std::vector<Botan::X509_Certificate>& cert_chain = + socket_.native_handle()->peer_cert_chain(); + for (auto const& cert : cert_chain) { + const Botan::X509_DN& subject = cert.subject_dn(); + std::cout << "Verified " << subject.to_string() << "\n"; + } + + send_request(); + } + else + { + std::cout << "Handshake failed: " << error.message() << "\n"; + } + }); + } + + void send_request() + { + std::cout << "Enter message: "; + std::cin.getline(request_, max_length); + size_t request_length = std::strlen(request_); + + boost::asio::async_write(socket_, + boost::asio::buffer(request_, request_length), + [this](const boost::system::error_code& error, std::size_t length) + { + if (!error) + { + receive_response(length); + } + else + { + std::cout << "Write failed: " << error.message() << "\n"; + } + }); + } + + void receive_response(std::size_t length) + { + boost::asio::async_read(socket_, + boost::asio::buffer(reply_, length), + [this](const boost::system::error_code& error, std::size_t length) + { + if (!error) + { + std::cout << "Reply: "; + std::cout.write(reply_, length); + std::cout << "\n"; + } + else + { + std::cout << "Read failed: " << error.message() << "\n"; + } + }); + } + + Botan::TLS::Stream<tcp::socket> socket_; + char request_[max_length]; + char reply_[max_length]; +}; + +int main(int argc, char* argv[]) +{ + try + { + if (argc != 3) + { + std::cerr << "Usage: client <addr> <port>\n"; + return 1; + } + + boost::asio::io_service io_context; + + using namespace std; // For atoi. + tcp::endpoint endpoint( + boost::asio::ip::address::from_string(argv[1]), atoi(argv[2])); + Botan::AutoSeeded_RNG rng; + Client_Credentials_Manager creds_mgr(rng); + Client_Session_Manager sess_mgr; + Client_Policy policy; + Botan::TLS::Context ctx(creds_mgr, rng, sess_mgr, policy); + + client c(io_context, ctx, endpoint); + + io_context.run(); + } + catch (std::exception& e) + { + std::cerr << "Exception: " << e.what() << "\n"; + } + + return 0; +} diff --git a/src/lib/asiolink/testutils/botan_boost_sample_server.cc b/src/lib/asiolink/testutils/botan_boost_sample_server.cc new file mode 100644 index 0000000000..86400ad24f --- /dev/null +++ b/src/lib/asiolink/testutils/botan_boost_sample_server.cc @@ -0,0 +1,220 @@ +// +// server.cpp +// ~~~~~~~~~~ +// +// Copyright (c) 2003-2020 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#include <config.h> + +#include <cstdlib> +#include <functional> +#include <iostream> +#include <boost/asio.hpp> + +#include <asiolink/botan_boost_wrapper.h> +#include <botan/asio_stream.h> +#include <botan/certstor_flatfile.h> +#include <botan/pkcs8.h> +#include <botan/auto_rng.h> + +inline std::string CA_(const std::string& filename) { + return (std::string(TEST_CA_DIR) + "/" + filename); +} + +using boost::asio::ip::tcp; + +using Server_Certificate_Store = Botan::Flatfile_Certificate_Store; + +class Server_Credentials_Manager : public Botan::Credentials_Manager +{ +public: + explicit Server_Credentials_Manager(Botan::RandomNumberGenerator& rng) + : stores_(), certs_(), + store_(new Server_Certificate_Store(CA_("kea-ca.crt"))), + cert_(Botan::X509_Certificate(CA_("kea-server.crt"))), + key_(Botan::PKCS8::load_key(CA_("kea-server.key"), rng)) + { + stores_.push_back(store_.get()); + certs_.push_back(cert_); + } + + virtual ~Server_Credentials_Manager() + { + } + + std::vector<Botan::Certificate_Store*> + trusted_certificate_authorities(const std::string&, + const std::string&) override + { + return stores_; + } + + std::vector<Botan::X509_Certificate> + cert_chain(const std::vector<std::string>&, + const std::string&, + const std::string&) override + { + return certs_; + } + + Botan::Private_Key* + private_key_for(const Botan::X509_Certificate&, + const std::string&, + const std::string&) override + { + return key_.get(); + } + + std::vector<Botan::Certificate_Store*> stores_; + std::vector<Botan::X509_Certificate> certs_; + std::shared_ptr<Botan::Certificate_Store> store_; + Botan::X509_Certificate cert_; + std::unique_ptr<Botan::Private_Key> key_; +}; + +using Server_Session_Manager = Botan::TLS::Session_Manager_Noop; + +class Server_Policy : public Botan::TLS::Default_Policy { +public: + virtual ~Server_Policy() + { + } + + std::vector<std::string> allowed_signature_methods() const override + { + return { "RSA", "ECDSA", "IMPLICIT" }; + } + + bool require_cert_revocation_info() const override + { + return false; + } +}; + +class session : public std::enable_shared_from_this<session> +{ +public: + session(tcp::socket socket, Botan::TLS::Context& ctx) + : socket_(std::move(socket), ctx) + { + } + + void start() + { + do_handshake(); + } + +private: + void do_handshake() + { + auto self(shared_from_this()); + socket_.async_handshake(Botan::TLS::Connection_Side::SERVER, + [this, self](const boost::system::error_code& error) + { + if (!error) + { + do_read(); + } + else + { + std::cerr << "handshake failed with " << error.message() << "\n"; + } + }); + } + + void do_read() + { + auto self(shared_from_this()); + socket_.async_read_some(boost::asio::buffer(data_), + [this, self](const boost::system::error_code& ec, std::size_t length) + { + if (!ec) + { + do_write(length); + } + }); + } + + void do_write(std::size_t length) + { + auto self(shared_from_this()); + boost::asio::async_write(socket_, boost::asio::buffer(data_, length), + [this, self](const boost::system::error_code& ec, + std::size_t /*length*/) + { + if (!ec) + { + do_read(); + } + }); + } + + Botan::TLS::Stream<tcp::socket> socket_; + char data_[1024]; +}; + +class server +{ +public: + server(boost::asio::io_service& io_context, + unsigned short port, + Botan::Credentials_Manager& creds_mgr, + Botan::RandomNumberGenerator& rng, + Botan::TLS::Session_Manager& sess_mgr, + Botan::TLS::Policy& policy) + : acceptor_(io_context, tcp::endpoint(tcp::v4(), port)), + context_(creds_mgr, rng, sess_mgr, policy) + { + do_accept(); + } + +private: + void do_accept() + { + acceptor_.async_accept( + [this](const boost::system::error_code& error, tcp::socket socket) + { + if (!error) + { + std::make_shared<session>(std::move(socket), context_)->start(); + } + + do_accept(); + }); + } + + tcp::acceptor acceptor_; + Botan::TLS::Context context_; +}; + +int main(int argc, char* argv[]) +{ + try + { + if (argc != 2) + { + std::cerr << "Usage: server <port>\n"; + return 1; + } + + boost::asio::io_service io_context; + + Botan::AutoSeeded_RNG rng; + Server_Credentials_Manager creds_mgr(rng); + Server_Session_Manager sess_mgr; + Server_Policy policy; + server s(io_context, std::atoi(argv[1]), creds_mgr, rng, sess_mgr, policy); + + io_context.run(); + } + catch (std::exception& e) + { + std::cerr << "Exception: " << e.what() << "\n"; + } + + return 0; +} diff --git a/src/lib/http/tests/Makefile.am b/src/lib/http/tests/Makefile.am index ab9ccaabbd..5dc64f32e8 100644 --- a/src/lib/http/tests/Makefile.am +++ b/src/lib/http/tests/Makefile.am @@ -42,6 +42,10 @@ if HAVE_OPENSSL libhttp_unittests_SOURCES += tls_server_unittests.cc libhttp_unittests_SOURCES += tls_client_unittests.cc endif +if HAVE_BOTAN_BOOST +libhttp_unittests_SOURCES += tls_server_unittests.cc +libhttp_unittests_SOURCES += tls_client_unittests.cc +endif libhttp_unittests_SOURCES += url_unittests.cc libhttp_unittests_SOURCES += test_http_client.h libhttp_unittests_SOURCES += mt_client_unittests.cc diff --git a/src/lib/process/tests/d_controller_unittests.cc b/src/lib/process/tests/d_controller_unittests.cc index 8e6432e5a4..4d7b73c433 100644 --- a/src/lib/process/tests/d_controller_unittests.cc +++ b/src/lib/process/tests/d_controller_unittests.cc @@ -188,6 +188,23 @@ TEST_F(DStubControllerTest, launchNormalShutdown) { elapsed_time.total_milliseconds() <= 2300); } +/// @brief A variant of the launch and normal shutdown test using a callback. +TEST_F(DStubControllerTest, launchNormalShutdownWithCallback) { + // Write the valid, empty, config and then run launch() for 1000 ms + // Access to the internal state. + auto callback = [&] { EXPECT_FALSE(getProcess()->shouldShutdown()); }; + time_duration elapsed_time; + ASSERT_NO_THROW(runWithConfig("{}", 2000, + static_cast<const TestCallback&>(callback), + elapsed_time)); + + // Verify that duration of the run invocation is the same as the + // timer duration. This demonstrates that the shutdown was driven + // by an io_service event and callback. + EXPECT_TRUE(elapsed_time.total_milliseconds() >= 1900 && + elapsed_time.total_milliseconds() <= 2300); +} + /// @brief Tests launch with an non-existing configuration file. TEST_F(DStubControllerTest, nonExistingConfigFile) { // command line to run standalone diff --git a/src/lib/process/testutils/d_test_stubs.cc b/src/lib/process/testutils/d_test_stubs.cc index 730ac53677..eba659406b 100644 --- a/src/lib/process/testutils/d_test_stubs.cc +++ b/src/lib/process/testutils/d_test_stubs.cc @@ -227,6 +227,36 @@ DControllerTest::runWithConfig(const std::string& config, int run_time_ms, elapsed_time = microsec_clock::universal_time() - start; } +void +DControllerTest::runWithConfig(const std::string& config, int run_time_ms, + const TestCallback& callback, + time_duration& elapsed_time) { + // Create the config file. + writeFile(config); + + // Shutdown (without error) after runtime. + isc::asiolink::IntervalTimer timer(*getIOService()); + timer.setup([&] { callback(); genShutdownCallback(); }, run_time_ms); + + // Record start time, and invoke launch(). + // We catch and rethrow to allow testing error scenarios. + ptime start = microsec_clock::universal_time(); + try { + // Set up valid command line arguments + char* argv[] = { const_cast<char*>("progName"), + const_cast<char*>("-c"), + const_cast<char*>(DControllerTest::CFG_TEST_FILE), + const_cast<char*>("-d") }; + launch(4, argv); + } catch (...) { + // calculate elapsed time, then rethrow it + elapsed_time = microsec_clock::universal_time() - start; + throw; + } + + elapsed_time = microsec_clock::universal_time() - start; +} + DProcessBasePtr DControllerTest:: getProcess() { DProcessBasePtr p; @@ -302,5 +332,5 @@ DStubCfgMgr::parse(isc::data::ConstElementPtr /*config*/, bool /*check_only*/) { return (isc::config::createAnswer(0, "It all went fine. I promise")); } -}; // namespace isc::process -}; // namespace isc +} // namespace isc::process +} // namespace isc diff --git a/src/lib/process/testutils/d_test_stubs.h b/src/lib/process/testutils/d_test_stubs.h index ee3b0c9d21..9881ee4dca 100644 --- a/src/lib/process/testutils/d_test_stubs.h +++ b/src/lib/process/testutils/d_test_stubs.h @@ -529,6 +529,32 @@ public: void runWithConfig(const std::string& config, int run_time_ms, time_duration& elapsed_time); + /// @brief Type of testing callbacks + typedef std::function<void()> TestCallback; + + /// @brief Convenience method for invoking standard, valid launch + /// with a testing callback + /// + /// This method sets up a timed run of the DController::launch. It does + /// the following: + /// - It creates command line argument variables argc/argv + /// - Invokes writeFile to create the config file with the given content + /// - Schedules a shutdown time timer to call DController::executeShutdown + /// after the interval + /// - Records the start time + /// - Invokes DController::launch() with the command line arguments + /// - After launch returns, it calculates the elapsed time and returns it + /// + /// @note the callback is called before the shutdown and MUST NOT throw + /// @param config configuration file content to write before calling launch + /// @param run_time_ms maximum amount of time to allow runProcess() to + /// continue. + /// @param callback testing callback of TestCallback type + /// @param[out] elapsed_time the actual time in ms spent in launch(). + void runWithConfig(const std::string& config, int run_time_ms, + const TestCallback& callback, + time_duration& elapsed_time); + /// @brief Fetches the controller's process /// /// @return A pointer to the process which may be null if it has not yet |