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 18b27be864..3ab8517373 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); @@ -61,6 +61,7 @@ class CAF_OPENSSL_EXPORT session { std::string openssl_passphrase_; bool connecting_; bool accepting_; + bool hostname_validation_; }; /// @relates session @@ -68,6 +69,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/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/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..9a5f7d6de4 100644 --- a/libcaf_openssl/src/openssl/session.cpp +++ b/libcaf_openssl/src/openssl/session.cpp @@ -4,8 +4,11 @@ #include "caf/openssl/session.hpp" +#include + CAF_PUSH_WARNINGS #include +#include CAF_POP_WARNINGS #include "caf/actor_system_config.hpp" @@ -56,6 +59,58 @@ 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`. +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_string(const std::string& content) { + 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; + ofs.close(); + return filename; +} + +// 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 + // 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_from_envvar(str)) + return tmpfile_from_string(*contents); + return std::nullopt; +} + + } // namespace session::session(actor_system& sys) @@ -154,11 +209,22 @@ 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_); +#if OPENSSL_VERSION_NUMBER >= 0x10100000L + // Enable hostname validation. + 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. + if (SSL_set_tlsext_host_name(ssl_, sni_servername.c_str()) != 1) + return false; auto ret = SSL_connect(ssl_); if (ret == 1) return true; @@ -198,25 +264,49 @@ 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() + 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 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) + 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()) - != 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 (!cfg.openssl_key.empty() - && SSL_CTX_use_PrivateKey_file(ctx, cfg.openssl_key.c_str(), - SSL_FILETYPE_PEM) - != 1) + 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); + if (kbio == nullptr) + CAF_RAISE_ERROR("failed to construct OpenSSL BIO"); + // 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_PrivateKey(ctx, pkey); + } 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); auto capath = (!cfg.openssl_capath.empty() ? cfg.openssl_capath.c_str() : nullptr); + auto tmpfile = pem_file_from_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"); @@ -285,6 +375,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 +384,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;