summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/bin/agent/ca_process.cc13
-rw-r--r--src/bin/agent/ca_process.h7
-rw-r--r--src/bin/agent/tests/ca_controller_unittests.cc85
-rw-r--r--src/lib/asiolink/Makefile.am7
-rw-r--r--src/lib/asiolink/asiolink.dox9
-rw-r--r--src/lib/asiolink/botan_boost_tls.cc339
-rw-r--r--src/lib/asiolink/botan_boost_tls.h207
-rw-r--r--src/lib/asiolink/botan_boost_wrapper.h32
-rw-r--r--src/lib/asiolink/botan_tls.cc4
-rw-r--r--src/lib/asiolink/botan_tls.h4
-rw-r--r--src/lib/asiolink/crypto_tls.h1
-rw-r--r--src/lib/asiolink/tests/Makefile.am5
-rw-r--r--src/lib/asiolink/tests/tls_unittest.cc1014
-rw-r--r--src/lib/asiolink/testutils/.gitignore2
-rw-r--r--src/lib/asiolink/testutils/Makefile.am16
-rw-r--r--src/lib/asiolink/testutils/botan_boost_sample_client.cc229
-rw-r--r--src/lib/asiolink/testutils/botan_boost_sample_server.cc220
-rw-r--r--src/lib/http/tests/Makefile.am4
-rw-r--r--src/lib/process/tests/d_controller_unittests.cc17
-rw-r--r--src/lib/process/testutils/d_test_stubs.cc34
-rw-r--r--src/lib/process/testutils/d_test_stubs.h26
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