From 4ed46805fe1613aa2611a235fd9f7d2e96820f00 Mon Sep 17 00:00:00 2001 From: Denis Razinkin Date: Mon, 21 Jul 2025 17:43:22 +0300 Subject: [PATCH 1/6] Reuse SSL context for server --- .../include/userver/engine/io/tls_wrapper.hpp | 8 +- core/src/clients/http/client_crl_test.cpp | 10 +- core/src/engine/io/tls_wrapper.cpp | 136 +------------ core/src/engine/io/tls_wrapper_benchmark.cpp | 14 +- core/src/engine/io/tls_wrapper_test.cpp | 102 ++++++---- core/src/server/net/listener_config.cpp | 15 +- core/src/server/net/listener_config.hpp | 6 + core/src/server/net/listener_impl.cpp | 6 +- universal/include/userver/crypto/ssl_ctx.hpp | 61 ++++++ universal/src/crypto/ssl_ctx.cpp | 188 ++++++++++++++++++ 10 files changed, 362 insertions(+), 184 deletions(-) create mode 100644 universal/include/userver/crypto/ssl_ctx.hpp create mode 100644 universal/src/crypto/ssl_ctx.cpp diff --git a/core/include/userver/engine/io/tls_wrapper.hpp b/core/include/userver/engine/io/tls_wrapper.hpp index aaecf95080c1..3efc0e147912 100644 --- a/core/include/userver/engine/io/tls_wrapper.hpp +++ b/core/include/userver/engine/io/tls_wrapper.hpp @@ -6,6 +6,8 @@ #include #include +#include +//#include #include #include #include @@ -42,10 +44,8 @@ class [[nodiscard]] TlsWrapper final : public RwBase { /// Starts a TLS server on an opened socket static TlsWrapper StartTlsServer( Socket&& socket, - const crypto::CertificatesChain& cert_chain, - const crypto::PrivateKey& key, - Deadline deadline, - const std::vector& extra_cert_authorities = {} + const crypto::SslCtx& ctx, + Deadline deadline ); ~TlsWrapper() override; diff --git a/core/src/clients/http/client_crl_test.cpp b/core/src/clients/http/client_crl_test.cpp index 2efaecbdc819..926830c6260b 100644 --- a/core/src/clients/http/client_crl_test.cpp +++ b/core/src/clients/http/client_crl_test.cpp @@ -315,12 +315,14 @@ struct TlsServer { auto deadline = engine::Deadline::FromDuration(utest::kMaxTestWaitTime); auto socket = tcp_listener_.socket.Accept(deadline); - auto tls_server = engine::io::TlsWrapper::StartTlsServer( - std::move(socket), + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext( crypto::LoadCertificatesChainFromString(kServerCertificate), crypto::PrivateKey::LoadFromString(kRevokedServerPrivateKey), - deadline, - cas + cas); + auto tls_server = engine::io::TlsWrapper::StartTlsServer( + std::move(socket), + ssl_ctx, + deadline ); std::array data{}; diff --git a/core/src/engine/io/tls_wrapper.cpp b/core/src/engine/io/tls_wrapper.cpp index ccd4890aef31..9c453e783bfc 100644 --- a/core/src/engine/io/tls_wrapper.cpp +++ b/core/src/engine/io/tls_wrapper.cpp @@ -21,11 +21,6 @@ USERVER_NAMESPACE_BEGIN namespace engine::io { namespace { -struct SslCtxDeleter { - void operator()(SSL_CTX* ctx) const noexcept { SSL_CTX_free(ctx); } -}; -using SslCtx = std::unique_ptr; - struct SslDeleter { void operator()(SSL* ssl) const noexcept { SSL_free(ssl); } }; @@ -190,65 +185,11 @@ int SSL_write_ex(SSL* ssl, const void* data, size_t len, size_t* bytes_written) } #endif -SslCtx MakeSslCtx() { - crypto::Openssl::Init(); - - SslCtx ssl_ctx{SSL_CTX_new(SSLv23_method())}; - if (!ssl_ctx) { - throw TlsException(crypto::FormatSslError("Failed create an SSL context: SSL_CTX_new")); - } -#if OPENSSL_VERSION_NUMBER >= 0x010100000L - if (1 != SSL_CTX_set_min_proto_version(ssl_ctx.get(), TLS1_VERSION)) { - throw TlsException(crypto::FormatSslError("Failed create an SSL context: SSL_CTX_set_min_proto_version")); - } -#endif - - constexpr auto options = SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION -#if OPENSSL_VERSION_NUMBER >= 0x010100000L - | SSL_OP_NO_RENEGOTIATION -#endif - ; - SSL_CTX_set_options(ssl_ctx.get(), options); - SSL_CTX_set_mode(ssl_ctx.get(), SSL_MODE_ENABLE_PARTIAL_WRITE); - SSL_CTX_clear_mode(ssl_ctx.get(), SSL_MODE_AUTO_RETRY); - if (1 != SSL_CTX_set_default_verify_paths(ssl_ctx.get())) { - LOG_LIMITED_WARNING() << crypto::FormatSslError("Failed create an SSL context: SSL_CTX_set_default_verify_paths" - ); - } - return ssl_ctx; -} - enum InterruptAction { kPass, kFail, }; -void SetServerName(SslCtx& ctx, std::string_view server_name) { - if (server_name.empty()) { - return; - } - - X509_VERIFY_PARAM* verify_param = SSL_CTX_get0_param(ctx.get()); - if (!verify_param) { - throw TlsException("Failed to set up client TLS wrapper: SSL_CTX_get0_param"); - } - if (1 != X509_VERIFY_PARAM_set1_host(verify_param, server_name.data(), server_name.size())) { - throw TlsException(crypto::FormatSslError("Failed to set up client TLS wrapper: X509_VERIFY_PARAM_set1_host")); - } - SSL_CTX_set_verify(ctx.get(), SSL_VERIFY_PEER, nullptr); -} - -void AddCertAuthorities(SslCtx& ctx, const std::vector& cert_authorities) { - UASSERT(!cert_authorities.empty()); - auto* store = SSL_CTX_get_cert_store(ctx.get()); - UASSERT(store); - for (const auto& ca : cert_authorities) { - if (1 != X509_STORE_add_cert(store, ca.GetNative())) { - throw TlsException(crypto::FormatSslError("Failed to set up client TLS wrapper: X509_STORE_add_cert")); - } - } -} - } // namespace class TlsWrapper::ReadContextAccessor final : public engine::impl::ContextAccessor { @@ -284,7 +225,7 @@ class TlsWrapper::Impl { SyncBioData(SSL_get_rbio(ssl.get()), &other.bio_data); } - void SetUp(SslCtx&& ssl_ctx) { + void SetUp(const crypto::SslCtx& ssl_ctx) { Bio socket_bio{BIO_new(GetSocketBioMethod())}; if (!socket_bio) { throw TlsException(crypto::FormatSslError("Failed to set up TLS wrapper: BIO_new")); @@ -293,7 +234,7 @@ class TlsWrapper::Impl { SyncBioData(socket_bio.get(), nullptr); BIO_set_init(socket_bio.get(), 1); - ssl.reset(SSL_new(ssl_ctx.get())); + ssl.reset(SSL_new(static_cast(ssl_ctx.GetRawSslCtx()))); if (!ssl) { throw TlsException(crypto::FormatSslError("Failed to set up TLS wrapper: SSL_new")); } @@ -454,11 +395,8 @@ engine::impl::ContextAccessor& TlsWrapper::ReadContextAccessor::GetSocketContext TlsWrapper::TlsWrapper(Socket&& socket) : impl_(std::move(socket)) { SetupContextAccessors(); } TlsWrapper TlsWrapper::StartTlsClient(Socket&& socket, const std::string& server_name, Deadline deadline) { - auto ssl_ctx = MakeSslCtx(); - SetServerName(ssl_ctx, server_name); - TlsWrapper wrapper{std::move(socket)}; - wrapper.impl_->SetUp(std::move(ssl_ctx)); + wrapper.impl_->SetUp(crypto::SslCtx::CreateClientTlsContext(server_name)); wrapper.impl_->ClientConnect(server_name, deadline); return wrapper; } @@ -471,79 +409,19 @@ TlsWrapper TlsWrapper::StartTlsClient( Deadline deadline, const std::vector& extra_cert_authorities ) { - auto ssl_ctx = MakeSslCtx(); - SetServerName(ssl_ctx, server_name); - - if (!extra_cert_authorities.empty()) { - AddCertAuthorities(ssl_ctx, extra_cert_authorities); - } - - if (cert) { - if (1 != SSL_CTX_use_certificate(ssl_ctx.get(), cert.GetNative())) { - throw TlsException(crypto::FormatSslError("Failed to set up client TLS wrapper: SSL_CTX_use_certificate")); - } - } - - if (key) { - if (1 != SSL_CTX_use_PrivateKey(ssl_ctx.get(), key.GetNative())) { - throw TlsException(crypto::FormatSslError("Failed to set up client TLS wrapper: SSL_CTX_use_PrivateKey")); - } - } - TlsWrapper wrapper{std::move(socket)}; - wrapper.impl_->SetUp(std::move(ssl_ctx)); + wrapper.impl_->SetUp(crypto::SslCtx::CreateClientTlsContext(server_name, cert, key, extra_cert_authorities)); wrapper.impl_->ClientConnect(server_name, deadline); return wrapper; } TlsWrapper TlsWrapper::StartTlsServer( Socket&& socket, - const crypto::CertificatesChain& cert_chain, - const crypto::PrivateKey& key, - Deadline deadline, - const std::vector& extra_cert_authorities + const crypto::SslCtx& ctx, + Deadline deadline ) { - auto ssl_ctx = MakeSslCtx(); - - if (!extra_cert_authorities.empty()) { - AddCertAuthorities(ssl_ctx, extra_cert_authorities); - SSL_CTX_set_verify(ssl_ctx.get(), SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr); - LOG_INFO() << "Client SSL cert will be verified"; - } else { - LOG_INFO() << "Client SSL cert will not be verified"; - } - - if (cert_chain.empty()) { - throw TlsException(crypto::FormatSslError("Empty certificate chain provided")); - } - - if (1 != SSL_CTX_use_certificate(ssl_ctx.get(), cert_chain.begin()->GetNative())) { - throw TlsException(crypto::FormatSslError("Failed to set up server TLS wrapper: SSL_CTX_use_certificate")); - } - - if (cert_chain.size() > 1) { - auto cert_it = std::next(cert_chain.begin()); - for (; cert_it != cert_chain.end(); ++cert_it) { - // cast in openssl1.0 macro expansion - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) - if (SSL_CTX_add_extra_chain_cert(ssl_ctx.get(), cert_it->GetNative()) <= 0) { - throw TlsException( - crypto::FormatSslError("Failed to set up server TLS wrapper: SSL_CTX_add_extra_chain_cert") - ); - } - - // After SSL_CTX_add_extra_chain_cert we should not free the cert - const auto ret = X509_up_ref(cert_it->GetNative()); - UASSERT(ret == 1); - } - } - - if (1 != SSL_CTX_use_PrivateKey(ssl_ctx.get(), key.GetNative())) { - throw TlsException(crypto::FormatSslError("Failed to set up server TLS wrapper: SSL_CTX_use_PrivateKey")); - } - TlsWrapper wrapper{std::move(socket)}; - wrapper.impl_->SetUp(std::move(ssl_ctx)); + wrapper.impl_->SetUp(ctx); wrapper.impl_->bio_data.current_deadline = deadline; auto ret = SSL_accept(wrapper.impl_->ssl.get()); diff --git a/core/src/engine/io/tls_wrapper_benchmark.cpp b/core/src/engine/io/tls_wrapper_benchmark.cpp index e617bf19cc34..33379b6a0184 100644 --- a/core/src/engine/io/tls_wrapper_benchmark.cpp +++ b/core/src/engine/io/tls_wrapper_benchmark.cpp @@ -93,12 +93,13 @@ constexpr auto kDeadlineMaxTime = std::chrono::seconds{60}; auto [server, client] = tcp_listener.MakeSocketPair(deadline); std::atomic reading{true}; + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext(crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(key)); auto server_task = engine::AsyncNoSpan( - [&reading, deadline](auto&& server) { + [&reading, deadline, &ssl_ctx](auto&& server) { auto tls_server = io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(key), + ssl_ctx, deadline ); @@ -137,12 +138,13 @@ BENCHMARK(tls_write_all_buffered)->RangeMultiplier(2)->Range(1 << 6, 1 << 12)->U auto [server, client] = tcp_listener.MakeSocketPair(deadline); std::atomic reading{true}; + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext(crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(key)); auto server_task = engine::AsyncNoSpan( - [&reading, deadline](auto&& server) { + [&reading, deadline, &ssl_ctx](auto&& server) { auto tls_server = io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(key), + ssl_ctx, deadline ); diff --git a/core/src/engine/io/tls_wrapper_test.cpp b/core/src/engine/io/tls_wrapper_test.cpp index 68cfa30eb1a9..023ef52bf137 100644 --- a/core/src/engine/io/tls_wrapper_test.cpp +++ b/core/src/engine/io/tls_wrapper_test.cpp @@ -232,10 +232,11 @@ UTEST(TlsWrapper, InitListSmall) { auto server_task = utils::Async( "tls-server", [deadline, kDataA, kDataB, kDataC, kDataD](auto&& server) { + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext(crypto::LoadCertificatesChainFromString(cert_chain), + crypto::PrivateKey::LoadFromString(chain_private_key)); auto tls_server = io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert_chain), - crypto::PrivateKey::LoadFromString(chain_private_key), + ssl_ctx, deadline ); if (tls_server.WriteAll({kDataA, kDataB, kDataC, kDataD}, deadline) != @@ -272,10 +273,12 @@ UTEST(TlsWrapper, InitListLarge) { auto server_task = utils::Async( "tls-server", [deadline, kDataA, kDataB, kDataC, kDataD](auto&& server) { + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext( + crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(key)); auto tls_server = io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(key), + ssl_ctx, deadline ); if (tls_server.WriteAll({kDataA, kDataB, kDataC, kDataD}, deadline) != @@ -308,10 +311,12 @@ UTEST(TlsWrapper, InitListSmallThenLarge) { auto server_task = utils::Async( "tls-server", [deadline, kDataSmall, kDataLarge](auto&& server) { + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext( + crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(key)); auto tls_server = io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(key), + ssl_ctx, deadline ); if (tls_server.WriteAll({kDataSmall, kDataSmall, kDataSmall, kDataSmall, kDataLarge}, deadline) != @@ -340,10 +345,12 @@ UTEST_MT(TlsWrapper, Smoke, 2) { auto server_task = engine::AsyncNoSpan( [test_deadline](auto&& server) { try { + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext( + crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(key)); auto tls_server = io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(key), + ssl_ctx, test_deadline ); EXPECT_EQ(1, tls_server.SendAll("1", 1, test_deadline)); @@ -387,10 +394,12 @@ UTEST_MT(TlsWrapper, DocTest, 2) { auto server_task = utils::Async( "tls-server", [deadline](auto&& server) { + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext( + crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(key)); auto tls_server = io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(key), + ssl_ctx, deadline ); if (tls_server.SendAll(kData.data(), kData.size(), deadline) != kData.size()) { @@ -419,10 +428,12 @@ UTEST(TlsWrapper, Move) { auto server_task = engine::AsyncNoSpan( [test_deadline](auto&& server) { try { + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext( + crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(key)); auto tls_server = io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(key), + ssl_ctx, test_deadline ); engine::AsyncNoSpan( @@ -468,11 +479,13 @@ UTEST(TlsWrapper, ConnectTimeout) { static_cast(io::TlsWrapper::StartTlsClient(std::move(client), {}, Deadline::FromDuration(kShortTimeout))), io::IoTimeout ); + + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext(crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(key)); EXPECT_THROW( static_cast(io::TlsWrapper::StartTlsServer( std::move(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(key), + ssl_ctx, Deadline::FromDuration(kShortTimeout) )), io::IoException @@ -488,10 +501,12 @@ UTEST_MT(TlsWrapper, IoTimeout, 2) { engine::SingleConsumerEvent timeout_happened; auto server_task = engine::AsyncNoSpan( [test_deadline, &timeout_happened](auto&& server) { + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext( + crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(key)); auto tls_server = io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(key), + ssl_ctx, test_deadline ); char c = 0; @@ -525,10 +540,12 @@ UTEST(TlsWrapper, Cancel) { auto server_task = engine::AsyncNoSpan( [test_deadline](auto&& server) { + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext( + crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(key)); auto tls_server = io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(key), + ssl_ctx, test_deadline ); char c = 0; @@ -552,20 +569,23 @@ UTEST_MT(TlsWrapper, CertKeyMismatch, 2) { auto server_task = engine::AsyncNoSpan( [test_deadline](auto&& server) { UEXPECT_THROW( + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext( + crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(other_key)); static_cast(io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(other_key), + ssl_ctx, test_deadline )), - io::TlsException + crypto::CryptoException ); }, std::move(server) ); EXPECT_THROW( - static_cast(io::TlsWrapper::StartTlsClient(std::move(client), {}, test_deadline)), io::IoException + static_cast(io::TlsWrapper::StartTlsClient(std::move(client), {}, test_deadline)), + userver::engine::io::IoSystemError ); server_task.Get(); } @@ -579,13 +599,15 @@ UTEST_MT(TlsWrapper, NonTlsClient, 2) { auto server_task = engine::AsyncNoSpan( [test_deadline](auto&& server) { UEXPECT_THROW( + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext( + crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(other_key)); static_cast(io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(other_key), + ssl_ctx, test_deadline )), - io::TlsException + crypto::CryptoException ); }, std::move(server) @@ -620,10 +642,12 @@ UTEST_MT(TlsWrapper, DoubleSmoke, 4) { auto server_task = engine::AsyncNoSpan( [test_deadline](auto&& server) { + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext( + crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(key)); auto tls_server = io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(key), + ssl_ctx, test_deadline ); EXPECT_EQ(1, tls_server.SendAll("1", 1, test_deadline)); @@ -641,10 +665,12 @@ UTEST_MT(TlsWrapper, DoubleSmoke, 4) { auto other_server_task = engine::AsyncNoSpan( [test_deadline](auto&& server) { + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext( + crypto::LoadCertificatesChainFromString(other_cert), + crypto::PrivateKey::LoadFromString(other_key)); auto tls_server = io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(other_cert), - crypto::PrivateKey::LoadFromString(other_key), + ssl_ctx, test_deadline ); EXPECT_EQ(1, tls_server.SendAll("5", 1, test_deadline)); @@ -694,10 +720,10 @@ UTEST(TlsWrapper, InvalidSocket) { const auto test_deadline = Deadline::FromDuration(utest::kMaxTestWaitTime); UEXPECT_THROW(static_cast(io::TlsWrapper::StartTlsClient({}, {}, test_deadline)), io::TlsException); + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext(crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(key)); UEXPECT_THROW( - static_cast(io::TlsWrapper::StartTlsServer( - {}, crypto::LoadCertificatesChainFromString(cert), crypto::PrivateKey::LoadFromString(key), test_deadline - )), + static_cast(io::TlsWrapper::StartTlsServer({}, ssl_ctx, test_deadline)), io::TlsException ); } @@ -711,10 +737,12 @@ UTEST(TlsWrapper, PeerShutdown) { auto server_task = engine::AsyncNoSpan( [test_deadline](auto&& server) { try { + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext( + crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(key)); auto tls_server = io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(key), + ssl_ctx, test_deadline ); char c = 0; @@ -753,10 +781,12 @@ UTEST(TlsWrapper, PeerDisconnect) { auto server_task = engine::AsyncNoSpan( [test_deadline](auto&& server) { try { + crypto::SslCtx ssl_ctx = crypto::SslCtx::CreateServerTlsContext( + crypto::LoadCertificatesChainFromString(cert), + crypto::PrivateKey::LoadFromString(key)); auto tls_server = io::TlsWrapper::StartTlsServer( std::forward(server), - crypto::LoadCertificatesChainFromString(cert), - crypto::PrivateKey::LoadFromString(key), + ssl_ctx, test_deadline ); char c = 0; diff --git a/core/src/server/net/listener_config.cpp b/core/src/server/net/listener_config.cpp index 8ba15e978d17..c51533ae3e27 100644 --- a/core/src/server/net/listener_config.cpp +++ b/core/src/server/net/listener_config.cpp @@ -9,8 +9,9 @@ #include -USERVER_NAMESPACE_BEGIN +#include "userver/engine/io.hpp" +USERVER_NAMESPACE_BEGIN namespace server::net { namespace { @@ -62,6 +63,11 @@ PortConfig Parse(const yaml_config::YamlConfig& value, formats::parse::To #include #include +#include #include #include #include +#include + #include "connection_config.hpp" USERVER_NAMESPACE_BEGIN @@ -29,7 +32,10 @@ struct PortConfig { crypto::PrivateKey tls_private_key; std::vector tls_certificate_authorities; + std::optional ssl_ctx; + void ReadTlsSettings(const storages::secdist::SecdistConfig& secdist); + void InitSslCtx(); }; struct ListenerConfig { diff --git a/core/src/server/net/listener_impl.cpp b/core/src/server/net/listener_impl.cpp index f52f08d97863..2f27e888908f 100644 --- a/core/src/server/net/listener_impl.cpp +++ b/core/src/server/net/listener_impl.cpp @@ -104,12 +104,10 @@ void ListenerImpl::ProcessConnection(engine::io::Socket peer_socket, const PortC std::unique_ptr socket; auto remote_address = peer_socket.Getpeername(); if (port_config.tls) { + UASSERT(port_config.ssl_ctx.has_value()); socket = std::make_unique(engine::io::TlsWrapper::StartTlsServer( std::move(peer_socket), - port_config.tls_cert_chain, - port_config.tls_private_key, - {}, - port_config.tls_certificate_authorities + port_config.ssl_ctx.value(), {} )); } else { socket = std::make_unique(std::move(peer_socket)); diff --git a/universal/include/userver/crypto/ssl_ctx.hpp b/universal/include/userver/crypto/ssl_ctx.hpp new file mode 100644 index 000000000000..901b37f64678 --- /dev/null +++ b/universal/include/userver/crypto/ssl_ctx.hpp @@ -0,0 +1,61 @@ +#pragma once + +/// @file userver/crypto/ssl_ctx.hpp +/// @brief @copybrief crypto::SslCtx + +#include +#include + +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace crypto { + +/// @ingroup userver_universal userver_containers +/// +/// SSL context +class SslCtx { +public: + static SslCtx CreateServerTlsContext( + const crypto::CertificatesChain& cert_chain, + const crypto::PrivateKey& key, + const std::vector& extra_cert_authorities = {} + ); + + static SslCtx CreateClientTlsContext(const std::string& server_name); + + static SslCtx CreateClientTlsContext( + const std::string& server_name, + const crypto::Certificate& cert, + const crypto::PrivateKey& key, + const std::vector& extra_cert_authorities = {} + ); + + SslCtx(SslCtx&&) noexcept; + SslCtx& operator=(SslCtx&&) noexcept; + ~SslCtx(); + + SslCtx(const SslCtx&) = delete; + SslCtx& operator=(const SslCtx&) = delete; + + void* GetRawSslCtx() const noexcept; + +private: + void AddCertAuthorities(const std::vector& cert_authorities); + void EnableVerifyClientCertificate(); + void SetServerName(std::string_view server_name); + void SetCertificate(const crypto::Certificate& cert); + void SetCertificates(const crypto::CertificatesChain& cert_chain); + void SetPrivateKey(const crypto::PrivateKey& key); + + class Impl; + std::unique_ptr impl_{}; + + explicit SslCtx(std::unique_ptr&& impl); +}; + +} // namespace crypto + +USERVER_NAMESPACE_END \ No newline at end of file diff --git a/universal/src/crypto/ssl_ctx.cpp b/universal/src/crypto/ssl_ctx.cpp new file mode 100644 index 000000000000..8b8462e2ba6c --- /dev/null +++ b/universal/src/crypto/ssl_ctx.cpp @@ -0,0 +1,188 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include + +USERVER_NAMESPACE_BEGIN +namespace crypto { + +class SslCtx::Impl { +public: + static std::unique_ptr MakeSslCtx(); + + explicit Impl(SSL_CTX* ctx) : ctx_{ctx} { + } + + ~Impl() { + if (ctx_) SSL_CTX_free(ctx_); + } + + SSL_CTX* Get() noexcept { return ctx_; } + +private: + SSL_CTX* ctx_; +}; + +std::unique_ptr SslCtx::Impl::MakeSslCtx() { + + crypto::Openssl::Init(); + + SSL_CTX* ssl_ctx = SSL_CTX_new(SSLv23_method()); + if (!ssl_ctx) { + throw CryptoException(crypto::FormatSslError("Failed create an SSL context: SSL_CTX_new")); + } +#if OPENSSL_VERSION_NUMBER >= 0x010100000L + if (1 != SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_VERSION)) { + throw CryptoException(crypto::FormatSslError("Failed create an SSL context: SSL_CTX_set_min_proto_version")); + } +#endif + + constexpr auto options = SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION +#if OPENSSL_VERSION_NUMBER >= 0x010100000L + | SSL_OP_NO_RENEGOTIATION +#endif + ; + SSL_CTX_set_options(ssl_ctx, options); + SSL_CTX_set_mode(ssl_ctx, SSL_MODE_ENABLE_PARTIAL_WRITE); + SSL_CTX_clear_mode(ssl_ctx, SSL_MODE_AUTO_RETRY); + if (1 != SSL_CTX_set_default_verify_paths(ssl_ctx)) { + LOG_LIMITED_WARNING() << crypto::FormatSslError("Failed create an SSL context: SSL_CTX_set_default_verify_paths" + ); + } + + return std::make_unique(ssl_ctx); +} + +void* SslCtx::GetRawSslCtx() const noexcept { + return static_cast(impl_->Get()); +} + +SslCtx::SslCtx(std::unique_ptr&& impl) : impl_(std::move(impl)) {} + +SslCtx::SslCtx(SslCtx&& other) noexcept = default; +SslCtx& SslCtx::operator=(SslCtx&& other) noexcept = default; +SslCtx::~SslCtx() = default; + + +void SslCtx::AddCertAuthorities(const std::vector& cert_authorities) { + UASSERT(!cert_authorities.empty()); + auto* store = SSL_CTX_get_cert_store(impl_->Get()); + UASSERT(store); + for (const auto& ca : cert_authorities) { + if (1 != X509_STORE_add_cert(store, ca.GetNative())) { + throw CryptoException(crypto::FormatSslError("X509_STORE_add_cert failed")); + } + } +} + +void SslCtx::EnableVerifyClientCertificate() { + SSL_CTX_set_verify(impl_->Get(), SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr); +} + +void SslCtx::SetServerName(std::string_view server_name) { + if (server_name.empty()) { + return; + } + + X509_VERIFY_PARAM* verify_param = SSL_CTX_get0_param(impl_->Get()); + if (!verify_param) { + throw CryptoException("SSL_CTX_get0_param failed"); + } + if (1 != X509_VERIFY_PARAM_set1_host(verify_param, server_name.data(), server_name.size())) { + throw CryptoException(crypto::FormatSslError("X509_VERIFY_PARAM_set1_host failed")); + } + SSL_CTX_set_verify(impl_->Get(), SSL_VERIFY_PEER, nullptr); +} + +void SslCtx::SetCertificate(const crypto::Certificate& cert) { + if (cert && 1 != SSL_CTX_use_certificate(impl_->Get(), cert.GetNative())) { + throw CryptoException(crypto::FormatSslError("SSL_CTX_use_certificate failed")); + } +} + +void SslCtx::SetCertificates(const crypto::CertificatesChain& cert_chain) { + + if (cert_chain.empty()) { + throw CryptoException(crypto::FormatSslError("Empty certificate chain provided")); + } + + SetCertificate(*cert_chain.begin()); + + if (cert_chain.size() > 1) { + auto it = std::next(cert_chain.begin()); + for (; it != cert_chain.end(); ++it) { + // cast in openssl1.0 macro expansion + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast) + if (SSL_CTX_add_extra_chain_cert(impl_->Get(), it->GetNative()) <= 0) { + throw CryptoException(crypto::FormatSslError("SSL_CTX_add_extra_chain_cert failed")); + } + + // After SSL_CTX_add_extra_chain_cert we should not free the cert + const auto ret = X509_up_ref(it->GetNative()); + UASSERT(ret == 1); + } + } +} + +void SslCtx::SetPrivateKey(const crypto::PrivateKey& key) { + if (key) { + if (1 != SSL_CTX_use_PrivateKey(impl_->Get(), key.GetNative())) { + throw CryptoException(crypto::FormatSslError("SSL_CTX_use_PrivateKey failed")); + } + } +} + +SslCtx SslCtx::CreateClientTlsContext(const std::string& server_name) { + SslCtx ssl_ctx(Impl::MakeSslCtx()); + ssl_ctx.SetServerName(server_name); + return ssl_ctx; +} + +SslCtx SslCtx::CreateClientTlsContext( + const std::string& server_name, + const crypto::Certificate& cert, + const crypto::PrivateKey& key, + const std::vector& extra_cert_authorities +) { + + SslCtx ssl_ctx(Impl::MakeSslCtx()); + + ssl_ctx.SetServerName(server_name); + + if (!extra_cert_authorities.empty()) { + ssl_ctx.AddCertAuthorities(extra_cert_authorities); + } + + ssl_ctx.SetCertificate(cert); + ssl_ctx.SetPrivateKey(key); + + return ssl_ctx; +} + +SslCtx SslCtx::CreateServerTlsContext( + const crypto::CertificatesChain& cert_chain, + const crypto::PrivateKey& key, + const std::vector& extra_cert_authorities) +{ + SslCtx ssl_ctx(Impl::MakeSslCtx()); + + if (!extra_cert_authorities.empty()) { + ssl_ctx.AddCertAuthorities(extra_cert_authorities); + ssl_ctx.EnableVerifyClientCertificate(); + } + + ssl_ctx.SetCertificates(cert_chain); + ssl_ctx.SetPrivateKey(key); + + return ssl_ctx; +} + +} // namespace crypto + +USERVER_NAMESPACE_END \ No newline at end of file From a881d307bd86696ad16c5df543be713d45e1e7fa Mon Sep 17 00:00:00 2001 From: Denis Razinkin Date: Tue, 22 Jul 2025 13:31:59 +0300 Subject: [PATCH 2/6] Reuse SSL context for server --- conanfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conanfile.py b/conanfile.py index 049147397b24..2d2ec0316019 100644 --- a/conanfile.py +++ b/conanfile.py @@ -106,8 +106,9 @@ def set_version(self): ) major_version = re.search(r'set\(USERVER_MAJOR_VERSION (.*)\)', content).group(1).strip() minor_version = re.search(r'set\(USERVER_MINOR_VERSION (.*)\)', content).group(1).strip() + hotfix_version = 1 - self.version = f'{major_version}.{minor_version}' # pylint: disable=attribute-defined-outside-init + self.version = f'{major_version}.{minor_version}.{hotfix_version}' # pylint: disable=attribute-defined-outside-init def layout(self): cmake_layout(self) From bc2c4e5f44a2a3cb1de659a4e181e02409398cb3 Mon Sep 17 00:00:00 2001 From: Denis Razinkin Date: Tue, 22 Jul 2025 18:49:32 +0300 Subject: [PATCH 3/6] logs --- universal/src/crypto/ssl_ctx.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/universal/src/crypto/ssl_ctx.cpp b/universal/src/crypto/ssl_ctx.cpp index 8b8462e2ba6c..6f5ebe576fd0 100644 --- a/universal/src/crypto/ssl_ctx.cpp +++ b/universal/src/crypto/ssl_ctx.cpp @@ -175,6 +175,9 @@ SslCtx SslCtx::CreateServerTlsContext( if (!extra_cert_authorities.empty()) { ssl_ctx.AddCertAuthorities(extra_cert_authorities); ssl_ctx.EnableVerifyClientCertificate(); + LOG_INFO() << "Client SSL cert will be verified"; + } else { + LOG_INFO() << "Client SSL cert will not be verified"; } ssl_ctx.SetCertificates(cert_chain); From 05b331910a763e61153a065db7482ac0a8d3b794 Mon Sep 17 00:00:00 2001 From: Denis Razinkin Date: Wed, 23 Jul 2025 12:25:49 +0300 Subject: [PATCH 4/6] logs --- conanfile.py | 2 +- .../include/userver/crypto/certificate.hpp | 3 ++ universal/src/crypto/certificate.cpp | 32 +++++++++++++++++++ universal/src/crypto/ssl_ctx.cpp | 19 ++++++++--- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/conanfile.py b/conanfile.py index 2d2ec0316019..a1fdfc54ecf0 100644 --- a/conanfile.py +++ b/conanfile.py @@ -108,7 +108,7 @@ def set_version(self): minor_version = re.search(r'set\(USERVER_MINOR_VERSION (.*)\)', content).group(1).strip() hotfix_version = 1 - self.version = f'{major_version}.{minor_version}.{hotfix_version}' # pylint: disable=attribute-defined-outside-init + self.version = f'{major_version}.{minor_version}.{hotfix_version}.1' # pylint: disable=attribute-defined-outside-init def layout(self): cmake_layout(self) diff --git a/universal/include/userver/crypto/certificate.hpp b/universal/include/userver/crypto/certificate.hpp index d50e6c64818c..42f59393007f 100644 --- a/universal/include/userver/crypto/certificate.hpp +++ b/universal/include/userver/crypto/certificate.hpp @@ -42,6 +42,9 @@ class Certificate { /// @throw crypto::KeyParseError if failed to load the certificate. static Certificate LoadFromStringSkippingAttributes(std::string_view certificate); + /// Returns Subject + std::string GetSubject() const; + private: explicit Certificate(std::shared_ptr cert) : cert_(std::move(cert)) {} diff --git a/universal/src/crypto/certificate.cpp b/universal/src/crypto/certificate.cpp index 9f8ac5abf942..44463030e638 100644 --- a/universal/src/crypto/certificate.cpp +++ b/universal/src/crypto/certificate.cpp @@ -86,6 +86,38 @@ CertificatesChain LoadCertificatesChainFromString(std::string_view chain) { return certificates; } +std::string Certificate::GetSubject() const { + X509* x509 = GetNative(); + if (!x509) { + throw std::runtime_error("Invalid certificate"); + } + + X509_NAME* subject_name = X509_get_subject_name(x509); + if (!subject_name) { + throw std::runtime_error("Failed to get subject name from certificate"); + } + + // Используем BIO для более гибкого форматирования + BIO* bio = BIO_new(BIO_s_mem()); + if (!bio) { + throw std::runtime_error("Failed to create BIO"); + } + + // Флаги форматирования можно настроить по вкусу + const int flags = XN_FLAG_RFC2253; // компактная строка, как в DN + if (X509_NAME_print_ex(bio, subject_name, 0, flags) < 0) { + BIO_free(bio); + throw std::runtime_error("Failed to print subject name"); + } + + char* data = nullptr; + long len = BIO_get_mem_data(bio, &data); + std::string result(data, len); + + BIO_free(bio); + return result; +} + } // namespace crypto USERVER_NAMESPACE_END diff --git a/universal/src/crypto/ssl_ctx.cpp b/universal/src/crypto/ssl_ctx.cpp index 6f5ebe576fd0..8ea09a8c45d2 100644 --- a/universal/src/crypto/ssl_ctx.cpp +++ b/universal/src/crypto/ssl_ctx.cpp @@ -101,9 +101,12 @@ void SslCtx::SetServerName(std::string_view server_name) { } void SslCtx::SetCertificate(const crypto::Certificate& cert) { - if (cert && 1 != SSL_CTX_use_certificate(impl_->Get(), cert.GetNative())) { + if (1 != SSL_CTX_use_certificate(impl_->Get(), cert.GetNative())) + { throw CryptoException(crypto::FormatSslError("SSL_CTX_use_certificate failed")); } + + LOG_INFO() << "Loaded server cert: subject=" << cert.GetSubject(); } void SslCtx::SetCertificates(const crypto::CertificatesChain& cert_chain) { @@ -123,6 +126,8 @@ void SslCtx::SetCertificates(const crypto::CertificatesChain& cert_chain) { throw CryptoException(crypto::FormatSslError("SSL_CTX_add_extra_chain_cert failed")); } + LOG_INFO() << "Loaded extra chain cert: subject=" << it->GetSubject(); + // After SSL_CTX_add_extra_chain_cert we should not free the cert const auto ret = X509_up_ref(it->GetNative()); UASSERT(ret == 1); @@ -131,10 +136,14 @@ void SslCtx::SetCertificates(const crypto::CertificatesChain& cert_chain) { } void SslCtx::SetPrivateKey(const crypto::PrivateKey& key) { - if (key) { - if (1 != SSL_CTX_use_PrivateKey(impl_->Get(), key.GetNative())) { - throw CryptoException(crypto::FormatSslError("SSL_CTX_use_PrivateKey failed")); - } + if (1 != SSL_CTX_use_PrivateKey(impl_->Get(), key.GetNative())) { + throw CryptoException(crypto::FormatSslError("SSL_CTX_use_PrivateKey failed")); + } + + LOG_INFO() << "Loaded server private key"; + + if (1 != SSL_CTX_check_private_key(impl_->Get())) { + throw CryptoException("Private key does not match the certificate public key"); } } From e9c8a55dfb00f851ec38080301964900b175e988 Mon Sep 17 00:00:00 2001 From: Denis Razinkin Date: Wed, 23 Jul 2025 13:35:37 +0300 Subject: [PATCH 5/6] fix init ssl ctx --- core/src/server/net/listener_config.cpp | 5 ----- core/src/server/server.cpp | 5 ++++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/core/src/server/net/listener_config.cpp b/core/src/server/net/listener_config.cpp index c51533ae3e27..bac0b5dd5e1e 100644 --- a/core/src/server/net/listener_config.cpp +++ b/core/src/server/net/listener_config.cpp @@ -63,11 +63,6 @@ PortConfig Parse(const yaml_config::YamlConfig& value, formats::parse::To Date: Fri, 25 Jul 2025 13:28:24 +0300 Subject: [PATCH 6/6] KeyParseError for Certificate::GetSubject --- core/src/engine/io/tls_wrapper_test.cpp | 3 ++- universal/src/crypto/certificate.cpp | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/core/src/engine/io/tls_wrapper_test.cpp b/core/src/engine/io/tls_wrapper_test.cpp index 023ef52bf137..ca1591062297 100644 --- a/core/src/engine/io/tls_wrapper_test.cpp +++ b/core/src/engine/io/tls_wrapper_test.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -585,7 +586,7 @@ UTEST_MT(TlsWrapper, CertKeyMismatch, 2) { EXPECT_THROW( static_cast(io::TlsWrapper::StartTlsClient(std::move(client), {}, test_deadline)), - userver::engine::io::IoSystemError + engine::io::IoSystemError ); server_task.Get(); } diff --git a/universal/src/crypto/certificate.cpp b/universal/src/crypto/certificate.cpp index 44463030e638..2acc5bf271fc 100644 --- a/universal/src/crypto/certificate.cpp +++ b/universal/src/crypto/certificate.cpp @@ -89,25 +89,25 @@ CertificatesChain LoadCertificatesChainFromString(std::string_view chain) { std::string Certificate::GetSubject() const { X509* x509 = GetNative(); if (!x509) { - throw std::runtime_error("Invalid certificate"); + throw KeyParseError(FormatSslError("Invalid certificate")); } X509_NAME* subject_name = X509_get_subject_name(x509); if (!subject_name) { - throw std::runtime_error("Failed to get subject name from certificate"); + throw KeyParseError(FormatSslError("Failed to get subject name from certificate")); } // Используем BIO для более гибкого форматирования BIO* bio = BIO_new(BIO_s_mem()); if (!bio) { - throw std::runtime_error("Failed to create BIO"); + throw KeyParseError(FormatSslError("Failed to create BIO")); } // Флаги форматирования можно настроить по вкусу const int flags = XN_FLAG_RFC2253; // компактная строка, как в DN if (X509_NAME_print_ex(bio, subject_name, 0, flags) < 0) { BIO_free(bio); - throw std::runtime_error("Failed to print subject name"); + throw KeyParseError(FormatSslError("Failed to print subject name")); } char* data = nullptr;