From 4892b0d44b2eeed0563a823e6697fb10fb4f53d2 Mon Sep 17 00:00:00 2001 From: Benno Evers Date: Fri, 3 Feb 2023 14:10:47 +0100 Subject: [PATCH 1/8] Send SNI information when connecting via TLS --- libcaf_openssl/caf/openssl/session.hpp | 3 ++- libcaf_openssl/src/openssl/middleman_actor.cpp | 4 ++-- libcaf_openssl/src/openssl/session.cpp | 6 ++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/libcaf_openssl/caf/openssl/session.hpp b/libcaf_openssl/caf/openssl/session.hpp index 18b27be864..830afaf9de 100644 --- a/libcaf_openssl/caf/openssl/session.hpp +++ b/libcaf_openssl/caf/openssl/session.hpp @@ -41,7 +41,7 @@ class CAF_OPENSSL_EXPORT session { rw_state read_some(size_t& result, native_socket fd, void* buf, size_t len); rw_state write_some(size_t& result, native_socket fd, const void* buf, size_t len); - bool try_connect(native_socket fd); + bool try_connect(native_socket fd, const std::string& sni_servername); bool try_accept(native_socket fd); bool must_read_more(native_socket, size_t threshold); @@ -68,6 +68,7 @@ using session_ptr = std::unique_ptr; /// @relates session CAF_OPENSSL_EXPORT session_ptr make_session(actor_system& sys, native_socket fd, + const std::string& servername, bool from_accepted_socket); } // namespace caf::openssl diff --git a/libcaf_openssl/src/openssl/middleman_actor.cpp b/libcaf_openssl/src/openssl/middleman_actor.cpp index 732ca6e941..4c6bb4377e 100644 --- a/libcaf_openssl/src/openssl/middleman_actor.cpp +++ b/libcaf_openssl/src/openssl/middleman_actor.cpp @@ -213,7 +213,7 @@ class doorman_impl : public io::network::doorman_impl { auto fd = acceptor_.accepted_socket(); detail::socket_guard sguard{fd}; io::network::nonblocking(fd, true); - auto sssn = make_session(parent()->system(), fd, true); + auto sssn = make_session(parent()->system(), fd, "", true); if (sssn == nullptr) { CAF_LOG_ERROR("Unable to create SSL session for accepted socket"); return false; @@ -245,7 +245,7 @@ class middleman_actor_impl : public io::middleman_actor_impl { if (!fd) return std::move(fd.error()); io::network::nonblocking(*fd, true); - auto sssn = make_session(system(), *fd, false); + auto sssn = make_session(system(), *fd, host, false); if (!sssn) { CAF_LOG_ERROR("Unable to create SSL session for connection"); return sec::cannot_connect_to_node; diff --git a/libcaf_openssl/src/openssl/session.cpp b/libcaf_openssl/src/openssl/session.cpp index 728b584a8f..dcdfb34c13 100644 --- a/libcaf_openssl/src/openssl/session.cpp +++ b/libcaf_openssl/src/openssl/session.cpp @@ -154,11 +154,12 @@ rw_state session::write_some(size_t& result, native_socket, const void* buf, return do_some(wr_fun, result, const_cast(buf), len, "write_some"); } -bool session::try_connect(native_socket fd) { +bool session::try_connect(native_socket fd, const std::string& sni_servername) { CAF_LOG_TRACE(CAF_ARG(fd)); CAF_BLOCK_SIGPIPE(); SSL_set_fd(ssl_, fd); SSL_set_connect_state(ssl_); + SSL_set_tlsext_host_name(ssl_, sni_servername.c_str()); auto ret = SSL_connect(ssl_); if (ret == 1) return true; @@ -285,6 +286,7 @@ bool session::handle_ssl_result(int ret) { } session_ptr make_session(actor_system& sys, native_socket fd, + const std::string& servername, bool from_accepted_socket) { session_ptr ptr{new session(sys)}; if (!ptr->init()) @@ -293,7 +295,7 @@ session_ptr make_session(actor_system& sys, native_socket fd, if (!ptr->try_accept(fd)) return nullptr; } else { - if (!ptr->try_connect(fd)) + if (!ptr->try_connect(fd, servername)) return nullptr; } return ptr; From ed8320c59ed54fa3a29951e6558cbb215d3eacca Mon Sep 17 00:00:00 2001 From: Benno Evers Date: Fri, 3 Feb 2023 14:34:14 +0100 Subject: [PATCH 2/8] Enable hostname validation for server certificates Without this setting, OpenSSL would only validate that the certificate has a valid signature from a trusted CA, but not that it actually matches the host to whom we were trying to connect. --- libcaf_openssl/src/openssl/session.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libcaf_openssl/src/openssl/session.cpp b/libcaf_openssl/src/openssl/session.cpp index dcdfb34c13..93d8ed9519 100644 --- a/libcaf_openssl/src/openssl/session.cpp +++ b/libcaf_openssl/src/openssl/session.cpp @@ -159,6 +159,13 @@ bool session::try_connect(native_socket fd, const std::string& sni_servername) { CAF_BLOCK_SIGPIPE(); SSL_set_fd(ssl_, fd); SSL_set_connect_state(ssl_); +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + // Enable hostname validation. + SSL_set_hostflags(ssl_, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); + if (SSL_set1_host(ssl_, sni_servername.c_str()) != 1) + return false; +#endif + // Send SNI when connecting. SSL_set_tlsext_host_name(ssl_, sni_servername.c_str()); auto ret = SSL_connect(ssl_); if (ret == 1) From cd23cb61b70ec48fbbbcdc78457f69317276a419 Mon Sep 17 00:00:00 2001 From: Benno Evers Date: Fri, 3 Feb 2023 15:54:25 +0100 Subject: [PATCH 3/8] WIP: Temporarily disable hostname validation --- libcaf_openssl/src/openssl/session.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libcaf_openssl/src/openssl/session.cpp b/libcaf_openssl/src/openssl/session.cpp index 93d8ed9519..d325992ac9 100644 --- a/libcaf_openssl/src/openssl/session.cpp +++ b/libcaf_openssl/src/openssl/session.cpp @@ -160,10 +160,11 @@ bool session::try_connect(native_socket fd, const std::string& sni_servername) { SSL_set_fd(ssl_, fd); SSL_set_connect_state(ssl_); #if OPENSSL_VERSION_NUMBER >= 0x10100000L + // FIXME: Re-enable this. // Enable hostname validation. - SSL_set_hostflags(ssl_, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); - if (SSL_set1_host(ssl_, sni_servername.c_str()) != 1) - return false; + // SSL_set_hostflags(ssl_, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); + // if (SSL_set1_host(ssl_, sni_servername.c_str()) != 1) + // return false; #endif // Send SNI when connecting. SSL_set_tlsext_host_name(ssl_, sni_servername.c_str()); From edf4fb78e9c58752b81d22ac3e4e011ab282b8ed Mon Sep 17 00:00:00 2001 From: Benno Evers Date: Wed, 15 Feb 2023 15:06:40 +0100 Subject: [PATCH 4/8] Allow passing OpenSSL options as environment variables CAF currently expects to receive OpenSSL certificates and keys as filenames. This matches the expectations of libopenssl, but is cumbersome to use in a Cloud context. In particular, ECS can only pass secrets as environment variables. So we extend the command-line option to also accept a string like `env:SOME_VARIABLE` and read the contents from the environment variable `SOME_VARIABLE`. --- libcaf_openssl/src/openssl/session.cpp | 68 +++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/libcaf_openssl/src/openssl/session.cpp b/libcaf_openssl/src/openssl/session.cpp index d325992ac9..c96af4c8b2 100644 --- a/libcaf_openssl/src/openssl/session.cpp +++ b/libcaf_openssl/src/openssl/session.cpp @@ -4,6 +4,9 @@ #include "caf/openssl/session.hpp" +#include +#include + CAF_PUSH_WARNINGS #include CAF_POP_WARNINGS @@ -56,6 +59,33 @@ int pem_passwd_cb(char* buf, int size, int, void* ptr) { return static_cast(strlen(buf)); } +// If the input is a string like `env:SOME_VARIABLE` and the environment variable +// `SOME_VARIABLE` exists, returns a string with the value of `SOME_VARIABLE`. +// Otherwise, returns `std::nullopt`. +std::optional contents_from_indirect_envvar(const std::string& str) { + if (str.find("env:") != 0) + return std::nullopt; + auto var = str.substr(4); + auto const* contents = ::getenv(var.c_str()); + if (contents == nullptr) + return std::nullopt; + return std::string{contents}; +} + +// If the input is a string like `env:SOME_VARIABLE` and the environment variable +// `SOME_VARIABLE` exists, returns a string with the value of `SOME_VARIABLE`. +std::optional tmpfile_from_indirect_envvar(const std::string& str) { + auto contents = contents_from_indirect_envvar(str); + if (!contents) + return contents; + auto filename = std::string{"/tmp/caf-openssl.XXXXXX"}; + ::mkstemp(&filename[0]); + std::ofstream ofs(filename); + ofs << *contents; + ofs.close(); + return filename; +} + } // namespace session::session(actor_system& sys) @@ -207,16 +237,47 @@ SSL_CTX* session::create_ssl_context() { if (sys_.openssl_manager().authentication_enabled()) { // Require valid certificates on both sides. auto& cfg = sys_.config(); - if (!cfg.openssl_certificate.empty() + // OpenSSL doesn't expose an API to read a certificate chain PEM from + // memory (as far as I can tell, there might be something in OSSL_DECODER) + // and the implementation of `SSL_CTX_use_certificate_chain_file` + // is huge, so we just write into a temporary file. + if (auto filename = tmpfile_from_indirect_envvar(cfg.openssl_certificate)) { + if (SSL_CTX_use_certificate_chain_file(ctx, filename->c_str()) + != 1) + CAF_RAISE_ERROR("cannot load certificate from environt variable"); + } else if (!cfg.openssl_certificate.empty() && SSL_CTX_use_certificate_chain_file(ctx, cfg.openssl_certificate.c_str()) - != 1) + != 1) { CAF_RAISE_ERROR("cannot load certificate"); + } if (!cfg.openssl_passphrase.empty()) { openssl_passphrase_ = cfg.openssl_passphrase; SSL_CTX_set_default_passwd_cb(ctx, pem_passwd_cb); SSL_CTX_set_default_passwd_cb_userdata(ctx, this); } + if (auto var = contents_from_indirect_envvar(cfg.openssl_key)) { + EVP_PKEY *pkey = nullptr; + const char *format = "PEM"; // NULL for any format + const char *structure = nullptr; // Any structure + const char *keytype = nullptr; // Any key + auto const* datap = reinterpret_cast(var->data()); + auto len = var->size(); + int selection = 0; // Autodetect selection. + // TODO: We might as well read `openssl_passphrase` here and pass it + // to the decoder. + auto* dctx = OSSL_DECODER_CTX_new_for_pkey(&pkey, format, structure, + keytype, + selection, + nullptr, nullptr); + if (dctx == nullptr) + CAF_RAISE_ERROR("couldn't create openssl decoder context"); + if (OSSL_DECODER_from_data(dctx, &datap, &len) != 0) + CAF_RAISE_ERROR("failed to decode private key"); + // Use the private key. + SSL_CTX_use_PrivateKey(ctx, pkey); + OSSL_DECODER_CTX_free(dctx); + } if (!cfg.openssl_key.empty() && SSL_CTX_use_PrivateKey_file(ctx, cfg.openssl_key.c_str(), SSL_FILETYPE_PEM) @@ -226,6 +287,9 @@ SSL_CTX* session::create_ssl_context() { : nullptr); auto capath = (!cfg.openssl_capath.empty() ? cfg.openssl_capath.c_str() : nullptr); + auto tmpfile = tmpfile_from_indirect_envvar(cafile); + if (tmpfile) + cafile = tmpfile->c_str(); if (cafile || capath) { if (SSL_CTX_load_verify_locations(ctx, cafile, capath) != 1) CAF_RAISE_ERROR("cannot load trusted CA certificates"); From 445d3aa366c2767dd8c8edce6a932ebcf6eaec98 Mon Sep 17 00:00:00 2001 From: Benno Evers Date: Fri, 17 Feb 2023 13:10:06 +0100 Subject: [PATCH 5/8] Add support for direct environment variables and OpenSSL 1.x * Support for reading private keys and certificates directly as opposed to specifying the name of a separate environment variable, since the latter is not possible when using a `vast.yaml` config file. Note that this is just a crux, eventually we want to have a cloud endpoint where the node can download the required TLS files on the fly given some auth token. * Support for OpenSSL 1.1.1 series. The OSSL_DECODER API is not available on that version, which is used on Debian Bullseye. --- libcaf_openssl/src/openssl/session.cpp | 61 ++++++++++++++++++++------ 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/libcaf_openssl/src/openssl/session.cpp b/libcaf_openssl/src/openssl/session.cpp index c96af4c8b2..a5512a5264 100644 --- a/libcaf_openssl/src/openssl/session.cpp +++ b/libcaf_openssl/src/openssl/session.cpp @@ -59,6 +59,10 @@ int pem_passwd_cb(char* buf, int size, int, void* ptr) { return static_cast(strlen(buf)); } +std::optional contents_from_direct_envvar(const std::string& str) { + return str; +} + // If the input is a string like `env:SOME_VARIABLE` and the environment variable // `SOME_VARIABLE` exists, returns a string with the value of `SOME_VARIABLE`. // Otherwise, returns `std::nullopt`. @@ -74,18 +78,35 @@ std::optional contents_from_indirect_envvar(const std::string& str) // If the input is a string like `env:SOME_VARIABLE` and the environment variable // `SOME_VARIABLE` exists, returns a string with the value of `SOME_VARIABLE`. -std::optional tmpfile_from_indirect_envvar(const std::string& str) { - auto contents = contents_from_indirect_envvar(str); - if (!contents) - return contents; +std::optional tmpfile_from_string(const std::string& content) { auto filename = std::string{"/tmp/caf-openssl.XXXXXX"}; ::mkstemp(&filename[0]); std::ofstream ofs(filename); - ofs << *contents; + ofs << content; ofs.close(); return filename; } +// Returns +std::optional pem_string_from_envvar(const std::string& str) { + if (str.find("env:") == 0) + return contents_from_indirect_envvar(str); + // The PEM format isn't very strictly defined, some implementations + // write the header as `---- BEGIN`. However, we probably don't want to + // keep supporting this in the long run anyways, so we're fine with just + // detecting exactly those certificates that we create ourselves. + if (str.find("-----BEGIN") == 0) + return contents_from_direct_envvar(str); + return std::nullopt; +} + +std::optional pem_file_from_envvar(const std::string& str) { + if (auto contents = pem_string_from_envvar(str)) + return tmpfile_from_string(*contents); + return std::nullopt; +} + + } // namespace session::session(actor_system& sys) @@ -238,10 +259,9 @@ SSL_CTX* session::create_ssl_context() { // Require valid certificates on both sides. auto& cfg = sys_.config(); // OpenSSL doesn't expose an API to read a certificate chain PEM from - // memory (as far as I can tell, there might be something in OSSL_DECODER) - // and the implementation of `SSL_CTX_use_certificate_chain_file` - // is huge, so we just write into a temporary file. - if (auto filename = tmpfile_from_indirect_envvar(cfg.openssl_certificate)) { + // memory and the implementation of `SSL_CTX_use_certificate_chain_file` + // is huge, so we write into a temporary file. + if (auto filename = pem_file_from_envvar(cfg.openssl_certificate)) { if (SSL_CTX_use_certificate_chain_file(ctx, filename->c_str()) != 1) CAF_RAISE_ERROR("cannot load certificate from environt variable"); @@ -256,7 +276,21 @@ SSL_CTX* session::create_ssl_context() { SSL_CTX_set_default_passwd_cb(ctx, pem_passwd_cb); SSL_CTX_set_default_passwd_cb_userdata(ctx, this); } - if (auto var = contents_from_indirect_envvar(cfg.openssl_key)) { + if (auto var = pem_string_from_envvar(cfg.openssl_key)) { +#if OPENSSL_VERSION_NUMBER < 0x10101000L + // BIO_new_mem_buf just creates a read-only view, so we don't need + // to free it afterwards. + kbio = BIO_new_mem_buf(var->data(), -1); + if (!kbio) + CAF_RAISE_ERROR(""); + // This function was deprecated in favor of the OSSL_DECODER API + // introduced in OpenSSL 3.0, but that is not available on + // Debian Bullseye. + rsa = PEM_read_bio_RSAPrivateKey(kbio, NULL, 0, NULL); + if (rsa != NULL) + CAF_RAISE_CAF_RAISE_ERROR("invalid private key"); + SSL_CTX_use_RSAPrivateKey(ctx, rsa); +#else /* OPENSSL_VERSION_NUMBER < 0x10101000L */ EVP_PKEY *pkey = nullptr; const char *format = "PEM"; // NULL for any format const char *structure = nullptr; // Any structure @@ -272,13 +306,14 @@ SSL_CTX* session::create_ssl_context() { nullptr, nullptr); if (dctx == nullptr) CAF_RAISE_ERROR("couldn't create openssl decoder context"); - if (OSSL_DECODER_from_data(dctx, &datap, &len) != 0) + if (OSSL_DECODER_from_data(dctx, &datap, &len) != 1) CAF_RAISE_ERROR("failed to decode private key"); // Use the private key. SSL_CTX_use_PrivateKey(ctx, pkey); OSSL_DECODER_CTX_free(dctx); +#endif /* OPENSSL_VERSION_NUMBER < 0x10101000L */ } - if (!cfg.openssl_key.empty() + else if (!cfg.openssl_key.empty() && SSL_CTX_use_PrivateKey_file(ctx, cfg.openssl_key.c_str(), SSL_FILETYPE_PEM) != 1) @@ -287,7 +322,7 @@ SSL_CTX* session::create_ssl_context() { : nullptr); auto capath = (!cfg.openssl_capath.empty() ? cfg.openssl_capath.c_str() : nullptr); - auto tmpfile = tmpfile_from_indirect_envvar(cafile); + auto tmpfile = pem_file_from_envvar(cafile); if (tmpfile) cafile = tmpfile->c_str(); if (cafile || capath) { From 18047762815bd89e1ae7190f38d30b785564aa63 Mon Sep 17 00:00:00 2001 From: Benno Evers Date: Fri, 17 Feb 2023 13:46:25 +0100 Subject: [PATCH 6/8] Fix build on Debian Bullseye --- libcaf_openssl/src/openssl/session.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/libcaf_openssl/src/openssl/session.cpp b/libcaf_openssl/src/openssl/session.cpp index a5512a5264..1a2a1622c2 100644 --- a/libcaf_openssl/src/openssl/session.cpp +++ b/libcaf_openssl/src/openssl/session.cpp @@ -5,7 +5,10 @@ #include "caf/openssl/session.hpp" #include + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L #include +#endif CAF_PUSH_WARNINGS #include @@ -79,6 +82,7 @@ std::optional contents_from_indirect_envvar(const std::string& str) // If the input is a string like `env:SOME_VARIABLE` and the environment variable // `SOME_VARIABLE` exists, returns a string with the value of `SOME_VARIABLE`. std::optional tmpfile_from_string(const std::string& content) { + std::cout << "content:\n" << content << std::endl; auto filename = std::string{"/tmp/caf-openssl.XXXXXX"}; ::mkstemp(&filename[0]); std::ofstream ofs(filename); @@ -262,9 +266,10 @@ SSL_CTX* session::create_ssl_context() { // memory and the implementation of `SSL_CTX_use_certificate_chain_file` // is huge, so we write into a temporary file. if (auto filename = pem_file_from_envvar(cfg.openssl_certificate)) { + std::cout << *filename << std::endl; if (SSL_CTX_use_certificate_chain_file(ctx, filename->c_str()) != 1) - CAF_RAISE_ERROR("cannot load certificate from environt variable"); + CAF_RAISE_ERROR("cannot load certificate from environment variable"); } else if (!cfg.openssl_certificate.empty() && SSL_CTX_use_certificate_chain_file(ctx, cfg.openssl_certificate.c_str()) @@ -277,20 +282,20 @@ SSL_CTX* session::create_ssl_context() { SSL_CTX_set_default_passwd_cb_userdata(ctx, this); } if (auto var = pem_string_from_envvar(cfg.openssl_key)) { -#if OPENSSL_VERSION_NUMBER < 0x10101000L +#if OPENSSL_VERSION_NUMBER < 0x30000000L // BIO_new_mem_buf just creates a read-only view, so we don't need // to free it afterwards. - kbio = BIO_new_mem_buf(var->data(), -1); + auto* kbio = BIO_new_mem_buf(var->data(), -1); if (!kbio) - CAF_RAISE_ERROR(""); + CAF_RAISE_ERROR("failed to construct OpenSSL BIO"); // This function was deprecated in favor of the OSSL_DECODER API // introduced in OpenSSL 3.0, but that is not available on // Debian Bullseye. - rsa = PEM_read_bio_RSAPrivateKey(kbio, NULL, 0, NULL); + auto* rsa = PEM_read_bio_RSAPrivateKey(kbio, NULL, 0, NULL); if (rsa != NULL) - CAF_RAISE_CAF_RAISE_ERROR("invalid private key"); + CAF_RAISE_ERROR("invalid private key"); SSL_CTX_use_RSAPrivateKey(ctx, rsa); -#else /* OPENSSL_VERSION_NUMBER < 0x10101000L */ +#else /* OPENSSL_VERSION_NUMBER < 0x30000000L */ EVP_PKEY *pkey = nullptr; const char *format = "PEM"; // NULL for any format const char *structure = nullptr; // Any structure @@ -311,7 +316,7 @@ SSL_CTX* session::create_ssl_context() { // Use the private key. SSL_CTX_use_PrivateKey(ctx, pkey); OSSL_DECODER_CTX_free(dctx); -#endif /* OPENSSL_VERSION_NUMBER < 0x10101000L */ +#endif /* OPENSSL_VERSION_NUMBER < 0x30000000L */ } else if (!cfg.openssl_key.empty() && SSL_CTX_use_PrivateKey_file(ctx, cfg.openssl_key.c_str(), From dfde12a13a6802c4a4d9fc41d07ddaf7b6e0c57d Mon Sep 17 00:00:00 2001 From: Benno Evers Date: Fri, 17 Feb 2023 16:02:54 +0100 Subject: [PATCH 7/8] Unify code for different OpenSSL versions Unlike its lower-level variants, `PEM_read_bio_PrivateKey` is not officially deprecated yet so we can avoid maintaining two versions of the private key loading by relying on that. --- libcaf_openssl/src/openssl/session.cpp | 49 +++++--------------------- 1 file changed, 9 insertions(+), 40 deletions(-) diff --git a/libcaf_openssl/src/openssl/session.cpp b/libcaf_openssl/src/openssl/session.cpp index 1a2a1622c2..d5b891ef55 100644 --- a/libcaf_openssl/src/openssl/session.cpp +++ b/libcaf_openssl/src/openssl/session.cpp @@ -6,10 +6,6 @@ #include -#if OPENSSL_VERSION_NUMBER >= 0x30000000L -#include -#endif - CAF_PUSH_WARNINGS #include CAF_POP_WARNINGS @@ -82,7 +78,6 @@ std::optional contents_from_indirect_envvar(const std::string& str) // If the input is a string like `env:SOME_VARIABLE` and the environment variable // `SOME_VARIABLE` exists, returns a string with the value of `SOME_VARIABLE`. std::optional tmpfile_from_string(const std::string& content) { - std::cout << "content:\n" << content << std::endl; auto filename = std::string{"/tmp/caf-openssl.XXXXXX"}; ::mkstemp(&filename[0]); std::ofstream ofs(filename); @@ -266,7 +261,6 @@ SSL_CTX* session::create_ssl_context() { // memory and the implementation of `SSL_CTX_use_certificate_chain_file` // is huge, so we write into a temporary file. if (auto filename = pem_file_from_envvar(cfg.openssl_certificate)) { - std::cout << *filename << std::endl; if (SSL_CTX_use_certificate_chain_file(ctx, filename->c_str()) != 1) CAF_RAISE_ERROR("cannot load certificate from environment variable"); @@ -282,46 +276,21 @@ SSL_CTX* session::create_ssl_context() { SSL_CTX_set_default_passwd_cb_userdata(ctx, this); } if (auto var = pem_string_from_envvar(cfg.openssl_key)) { -#if OPENSSL_VERSION_NUMBER < 0x30000000L // BIO_new_mem_buf just creates a read-only view, so we don't need // to free it afterwards. auto* kbio = BIO_new_mem_buf(var->data(), -1); - if (!kbio) + if (kbio == nullptr) CAF_RAISE_ERROR("failed to construct OpenSSL BIO"); - // This function was deprecated in favor of the OSSL_DECODER API - // introduced in OpenSSL 3.0, but that is not available on - // Debian Bullseye. - auto* rsa = PEM_read_bio_RSAPrivateKey(kbio, NULL, 0, NULL); - if (rsa != NULL) + // Starting from OpenSSL 3.0, the OSSL_DECODER API is the suggested + // alternative for this. + // TODO: Pass the pem_passwd_cb here if a passphrase was configured. + auto* pkey = PEM_read_bio_PrivateKey(kbio, nullptr, nullptr, nullptr); + if (pkey == nullptr) CAF_RAISE_ERROR("invalid private key"); - SSL_CTX_use_RSAPrivateKey(ctx, rsa); -#else /* OPENSSL_VERSION_NUMBER < 0x30000000L */ - EVP_PKEY *pkey = nullptr; - const char *format = "PEM"; // NULL for any format - const char *structure = nullptr; // Any structure - const char *keytype = nullptr; // Any key - auto const* datap = reinterpret_cast(var->data()); - auto len = var->size(); - int selection = 0; // Autodetect selection. - // TODO: We might as well read `openssl_passphrase` here and pass it - // to the decoder. - auto* dctx = OSSL_DECODER_CTX_new_for_pkey(&pkey, format, structure, - keytype, - selection, - nullptr, nullptr); - if (dctx == nullptr) - CAF_RAISE_ERROR("couldn't create openssl decoder context"); - if (OSSL_DECODER_from_data(dctx, &datap, &len) != 1) - CAF_RAISE_ERROR("failed to decode private key"); - // Use the private key. SSL_CTX_use_PrivateKey(ctx, pkey); - OSSL_DECODER_CTX_free(dctx); -#endif /* OPENSSL_VERSION_NUMBER < 0x30000000L */ - } - else if (!cfg.openssl_key.empty() - && SSL_CTX_use_PrivateKey_file(ctx, cfg.openssl_key.c_str(), - SSL_FILETYPE_PEM) - != 1) + } else if (!cfg.openssl_key.empty() + && SSL_CTX_use_PrivateKey_file(ctx, cfg.openssl_key.c_str(), + SSL_FILETYPE_PEM) != 1) CAF_RAISE_ERROR("cannot load private key"); auto cafile = (!cfg.openssl_cafile.empty() ? cfg.openssl_cafile.c_str() : nullptr); From 23acdb36f1e830ccafc23a7552ff82daf0b3e587 Mon Sep 17 00:00:00 2001 From: Benno Evers Date: Mon, 20 Mar 2023 13:48:30 +0100 Subject: [PATCH 8/8] Add flag to enable or disable hostname validation Co-Authored-By: Tobias Mayer --- libcaf_core/caf/actor_system_config.hpp | 1 + libcaf_core/src/actor_system_config.cpp | 1 + libcaf_openssl/caf/openssl/session.hpp | 1 + libcaf_openssl/src/openssl/manager.cpp | 4 +++- libcaf_openssl/src/openssl/session.cpp | 30 ++++++++++++++++--------- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/libcaf_core/caf/actor_system_config.hpp b/libcaf_core/caf/actor_system_config.hpp index 57c922d8e0..36cb095872 100644 --- a/libcaf_core/caf/actor_system_config.hpp +++ b/libcaf_core/caf/actor_system_config.hpp @@ -211,6 +211,7 @@ class CAF_CORE_EXPORT actor_system_config { std::string openssl_passphrase; std::string openssl_capath; std::string openssl_cafile; + bool hostname_validation; // -- factories -------------------------------------------------------------- diff --git a/libcaf_core/src/actor_system_config.cpp b/libcaf_core/src/actor_system_config.cpp index a3e7590bab..e1177102dd 100644 --- a/libcaf_core/src/actor_system_config.cpp +++ b/libcaf_core/src/actor_system_config.cpp @@ -170,6 +170,7 @@ settings actor_system_config::dump_content() const { put_missing(openssl_group, "passphrase", std::string{}); put_missing(openssl_group, "capath", std::string{}); put_missing(openssl_group, "cafile", std::string{}); + put_missing(openssl_group, "hostname-validation", true); return result; } diff --git a/libcaf_openssl/caf/openssl/session.hpp b/libcaf_openssl/caf/openssl/session.hpp index 830afaf9de..3ab8517373 100644 --- a/libcaf_openssl/caf/openssl/session.hpp +++ b/libcaf_openssl/caf/openssl/session.hpp @@ -61,6 +61,7 @@ class CAF_OPENSSL_EXPORT session { std::string openssl_passphrase_; bool connecting_; bool accepting_; + bool hostname_validation_; }; /// @relates session diff --git a/libcaf_openssl/src/openssl/manager.cpp b/libcaf_openssl/src/openssl/manager.cpp index 0d5761aaa6..f9961add82 100644 --- a/libcaf_openssl/src/openssl/manager.cpp +++ b/libcaf_openssl/src/openssl/manager.cpp @@ -148,7 +148,9 @@ void manager::add_module_options(actor_system_config& cfg) { "path to an OpenSSL-style directory of trusted certificates") .add( cfg.openssl_cafile, "cafile", - "path to a file of concatenated PEM-formatted certificates"); + "path to a file of concatenated PEM-formatted certificates") + .add(cfg.hostname_validation, "enable-hostname-validation", + "explicitly toggle hostname validation on outgoing connections"); } actor_system::module* manager::make(actor_system& sys, detail::type_list<>) { diff --git a/libcaf_openssl/src/openssl/session.cpp b/libcaf_openssl/src/openssl/session.cpp index d5b891ef55..9a5f7d6de4 100644 --- a/libcaf_openssl/src/openssl/session.cpp +++ b/libcaf_openssl/src/openssl/session.cpp @@ -8,6 +8,7 @@ CAF_PUSH_WARNINGS #include +#include CAF_POP_WARNINGS #include "caf/actor_system_config.hpp" @@ -78,7 +79,10 @@ std::optional contents_from_indirect_envvar(const std::string& str) // If the input is a string like `env:SOME_VARIABLE` and the environment variable // `SOME_VARIABLE` exists, returns a string with the value of `SOME_VARIABLE`. std::optional tmpfile_from_string(const std::string& content) { - auto filename = std::string{"/tmp/caf-openssl.XXXXXX"}; + const char* base = ::getenv("TMPDIR"); + if (!base) + base = "/tmp"; + auto filename = base + std::string{"/caf-openssl.XXXXXX"}; ::mkstemp(&filename[0]); std::ofstream ofs(filename); ofs << content; @@ -86,8 +90,9 @@ std::optional tmpfile_from_string(const std::string& content) { return filename; } -// Returns -std::optional pem_string_from_envvar(const std::string& str) { +// Returns a string in PEM format, ie. base64-encoded data surrounded +// by `-----BEGIN XXX` and `-----END XXX` blocks. +std::optional pem_from_envvar(const std::string& str) { if (str.find("env:") == 0) return contents_from_indirect_envvar(str); // The PEM format isn't very strictly defined, some implementations @@ -100,7 +105,7 @@ std::optional pem_string_from_envvar(const std::string& str) { } std::optional pem_file_from_envvar(const std::string& str) { - if (auto contents = pem_string_from_envvar(str)) + if (auto contents = pem_from_envvar(str)) return tmpfile_from_string(*contents); return std::nullopt; } @@ -210,14 +215,16 @@ bool session::try_connect(native_socket fd, const std::string& sni_servername) { SSL_set_fd(ssl_, fd); SSL_set_connect_state(ssl_); #if OPENSSL_VERSION_NUMBER >= 0x10100000L - // FIXME: Re-enable this. // Enable hostname validation. - // SSL_set_hostflags(ssl_, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); - // if (SSL_set1_host(ssl_, sni_servername.c_str()) != 1) - // return false; + if (hostname_validation_) { + SSL_set_hostflags(ssl_, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); + if (SSL_set1_host(ssl_, sni_servername.c_str()) != 1) + return false; + } #endif // Send SNI when connecting. - SSL_set_tlsext_host_name(ssl_, sni_servername.c_str()); + if (SSL_set_tlsext_host_name(ssl_, sni_servername.c_str()) != 1) + return false; auto ret = SSL_connect(ssl_); if (ret == 1) return true; @@ -257,9 +264,10 @@ SSL_CTX* session::create_ssl_context() { if (sys_.openssl_manager().authentication_enabled()) { // Require valid certificates on both sides. auto& cfg = sys_.config(); + enable_hostname_validation_ = cfg.enable_hostname_validation; // OpenSSL doesn't expose an API to read a certificate chain PEM from // memory and the implementation of `SSL_CTX_use_certificate_chain_file` - // is huge, so we write into a temporary file. + // is huge, so instead of reproducing that we write into a temporary file. if (auto filename = pem_file_from_envvar(cfg.openssl_certificate)) { if (SSL_CTX_use_certificate_chain_file(ctx, filename->c_str()) != 1) @@ -275,7 +283,7 @@ SSL_CTX* session::create_ssl_context() { SSL_CTX_set_default_passwd_cb(ctx, pem_passwd_cb); SSL_CTX_set_default_passwd_cb_userdata(ctx, this); } - if (auto var = pem_string_from_envvar(cfg.openssl_key)) { + if (auto var = pem_from_envvar(cfg.openssl_key)) { // BIO_new_mem_buf just creates a read-only view, so we don't need // to free it afterwards. auto* kbio = BIO_new_mem_buf(var->data(), -1);