From 857dd2824f725ee837737130321121595d95d67c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 May 2024 14:13:27 -0700 Subject: [PATCH] feat: specify a custom CA file for TLS peer verification (#409) This adds a new config builder option, `CustomCAFile` and associated C binding to the server and client SDKs. When specified, the SDK's streaming, polling, and event connections will verify its TLS peer based on the CAs found in this file. The custom file may be un-set by passing an empty string. --- .../src/entity_manager.cpp | 3 ++ .../client-contract-tests/src/main.cpp | 3 +- .../include/data_model/data_model.hpp | 5 ++- .../src/entity_manager.cpp | 3 ++ .../server-contract-tests/src/main.cpp | 1 + .../client_side/bindings/c/config/builder.h | 20 ++++++++++ libs/client-sdk/src/bindings/c/builder.cpp | 10 +++++ libs/client-sdk/src/client_impl.cpp | 10 +++++ .../src/data_sources/polling_data_source.cpp | 6 +-- .../data_sources/streaming_data_source.cpp | 4 ++ libs/client-sdk/tests/client_config_test.cpp | 1 + .../builders/http_properties_builder.hpp | 16 ++++++++ .../config/shared/built/http_properties.hpp | 7 +++- libs/common/src/config/http_properties.cpp | 18 +++++++-- .../src/config/http_properties_builder.cpp | 13 ++++++- .../events/detail/request_worker.hpp | 13 +++---- .../events/detail/worker_pool.hpp | 5 ++- .../launchdarkly/network/asio_requester.hpp | 23 ++++++++--- .../src/events/asio_event_processor.cpp | 2 +- libs/internal/src/events/request_worker.cpp | 15 ++++---- libs/internal/src/events/worker_pool.cpp | 4 +- libs/internal/tests/event_processor_test.cpp | 5 ++- .../server_side/bindings/c/config/builder.h | 20 ++++++++++ libs/server-sdk/src/bindings/c/builder.cpp | 10 +++++ libs/server-sdk/src/client_impl.cpp | 12 ++++++ .../sources/polling/polling_data_source.cpp | 7 +--- .../streaming/streaming_data_source.cpp | 4 ++ .../tests/server_c_bindings_test.cpp | 38 ++++++++++++++++++- .../include/launchdarkly/sse/client.hpp | 14 +++++++ libs/server-sent-events/src/client.cpp | 21 +++++++++- 30 files changed, 264 insertions(+), 49 deletions(-) diff --git a/contract-tests/client-contract-tests/src/entity_manager.cpp b/contract-tests/client-contract-tests/src/entity_manager.cpp index f094af4f5..58eef6cfd 100644 --- a/contract-tests/client-contract-tests/src/entity_manager.cpp +++ b/contract-tests/client-contract-tests/src/entity_manager.cpp @@ -134,6 +134,9 @@ std::optional EntityManager::create(ConfigParams const& in) { if (in.tls->skipVerifyPeer) { builder.SkipVerifyPeer(*in.tls->skipVerifyPeer); } + if (in.tls->customCAFile) { + builder.CustomCAFile(*in.tls->customCAFile); + } config_builder.HttpProperties().Tls(std::move(builder)); } diff --git a/contract-tests/client-contract-tests/src/main.cpp b/contract-tests/client-contract-tests/src/main.cpp index b85f35cea..3c4fc4883 100644 --- a/contract-tests/client-contract-tests/src/main.cpp +++ b/contract-tests/client-contract-tests/src/main.cpp @@ -45,7 +45,8 @@ int main(int argc, char* argv[]) { srv.add_capability("anonymous-redaction"); srv.add_capability("tls:verify-peer"); srv.add_capability("tls:skip-verify-peer"); - + srv.add_capability("tls:custom-ca"); + net::signal_set signals{ioc, SIGINT, SIGTERM}; boost::asio::spawn(ioc.get_executor(), [&](auto yield) mutable { diff --git a/contract-tests/data-model/include/data_model/data_model.hpp b/contract-tests/data-model/include/data_model/data_model.hpp index b89346bd8..51ee897ca 100644 --- a/contract-tests/data-model/include/data_model/data_model.hpp +++ b/contract-tests/data-model/include/data_model/data_model.hpp @@ -31,9 +31,12 @@ struct adl_serializer> { struct ConfigTLSParams { std::optional skipVerifyPeer; + std::optional customCAFile; }; + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigTLSParams, - skipVerifyPeer); + skipVerifyPeer, + customCAFile); struct ConfigStreamingParams { std::optional baseUri; diff --git a/contract-tests/server-contract-tests/src/entity_manager.cpp b/contract-tests/server-contract-tests/src/entity_manager.cpp index cd3a5c60d..68d75d642 100644 --- a/contract-tests/server-contract-tests/src/entity_manager.cpp +++ b/contract-tests/server-contract-tests/src/entity_manager.cpp @@ -125,6 +125,9 @@ std::optional EntityManager::create(ConfigParams const& in) { if (in.tls->skipVerifyPeer) { builder.SkipVerifyPeer(*in.tls->skipVerifyPeer); } + if (in.tls->customCAFile) { + builder.CustomCAFile(*in.tls->customCAFile); + } config_builder.HttpProperties().Tls(std::move(builder)); } diff --git a/contract-tests/server-contract-tests/src/main.cpp b/contract-tests/server-contract-tests/src/main.cpp index 8fe0da565..e492b4ab6 100644 --- a/contract-tests/server-contract-tests/src/main.cpp +++ b/contract-tests/server-contract-tests/src/main.cpp @@ -44,6 +44,7 @@ int main(int argc, char* argv[]) { srv.add_capability("anonymous-redaction"); srv.add_capability("tls:verify-peer"); srv.add_capability("tls:skip-verify-peer"); + srv.add_capability("tls:custom-ca"); net::signal_set signals{ioc, SIGINT, SIGTERM}; diff --git a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h index fef7e51ec..7e9b4fe1a 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h +++ b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h @@ -440,6 +440,26 @@ LDClientHttpPropertiesTlsBuilder_SkipVerifyPeer( LDClientHttpPropertiesTlsBuilder b, bool skip_verify_peer); +/** + * Configures TLS peer certificate verification to use a custom + * CA file. + * + * The parameter is a filepath pointing to a bundle of + * one or more PEM-encoded x509 certificates comprising the root of trust for + * the SDK's outbound connections. + * + * By default, the SDK uses the system's CA bundle. Passing the empty string + * will unset any previously set path and revert to the system's CA bundle. + * + * @param b Client config builder. Must not be NULL. + * @param custom_ca_file Filepath of the custom CA bundle, or empty string. Must + * not be NULL. + */ +LD_EXPORT(void) +LDClientHttpPropertiesTlsBuilder_CustomCAFile( + LDClientHttpPropertiesTlsBuilder b, + char const* custom_ca_file); + /** * Disables the default SDK logging. * @param b Client config builder. Must not be NULL. diff --git a/libs/client-sdk/src/bindings/c/builder.cpp b/libs/client-sdk/src/bindings/c/builder.cpp index f1071a73d..7994207e4 100644 --- a/libs/client-sdk/src/bindings/c/builder.cpp +++ b/libs/client-sdk/src/bindings/c/builder.cpp @@ -332,6 +332,16 @@ LDClientHttpPropertiesTlsBuilder_SkipVerifyPeer( TO_TLS_BUILDER(b)->SkipVerifyPeer(skip_verify_peer); } +LD_EXPORT(void) +LDClientHttpPropertiesTlsBuilder_CustomCAFile( + LDClientHttpPropertiesTlsBuilder b, + char const* custom_ca_file) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(custom_ca_file); + + TO_TLS_BUILDER(b)->CustomCAFile(custom_ca_file); +} + LD_EXPORT(LDClientHttpPropertiesTlsBuilder) LDClientHttpPropertiesTlsBuilder_New(void) { return FROM_TLS_BUILDER(new TlsBuilder()); diff --git a/libs/client-sdk/src/client_impl.cpp b/libs/client-sdk/src/client_impl.cpp index 79ab4d63e..9e2f66075 100644 --- a/libs/client-sdk/src/client_impl.cpp +++ b/libs/client-sdk/src/client_impl.cpp @@ -109,6 +109,16 @@ ClientImpl::ClientImpl(Config in_cfg, eval_reasons_available_(config_.DataSourceConfig().with_reasons) { flag_manager_.LoadCache(context_); + if (auto custom_ca = http_properties_.Tls().CustomCAFile()) { + LD_LOG(logger_, LogLevel::kInfo) + << "TLS peer verification configured with custom CA file: " + << *custom_ca; + } + if (http_properties_.Tls().PeerVerifyMode() == + config::shared::built::TlsOptions::VerifyMode::kVerifyNone) { + LD_LOG(logger_, LogLevel::kInfo) << "TLS peer verification disabled"; + } + if (config_.Events().Enabled() && !config_.Offline()) { event_processor_ = std::make_unique>( diff --git a/libs/client-sdk/src/data_sources/polling_data_source.cpp b/libs/client-sdk/src/data_sources/polling_data_source.cpp index 5f0481a47..762e7dce0 100644 --- a/libs/client-sdk/src/data_sources/polling_data_source.cpp +++ b/libs/client-sdk/src/data_sources/polling_data_source.cpp @@ -74,7 +74,7 @@ PollingDataSource::PollingDataSource( status_manager_(status_manager), data_source_handler_( DataSourceEventHandler(context, handler, logger, status_manager_)), - requester_(ioc, http_properties.Tls().PeerVerifyMode()), + requester_(ioc, http_properties.Tls()), timer_(ioc), polling_interval_( std::get< @@ -88,10 +88,6 @@ PollingDataSource::PollingDataSource( auto const& polling_config = std::get< config::shared::built::PollingConfig>( data_source_config.method); - if (http_properties.Tls().PeerVerifyMode() == - config::shared::built::TlsOptions::VerifyMode::kVerifyNone) { - LD_LOG(logger_, LogLevel::kDebug) << "TLS peer verification disabled"; - } if (polling_interval_ < polling_config.min_polling_interval) { LD_LOG(logger_, LogLevel::kWarn) << "Polling interval too frequent, defaulting to " diff --git a/libs/client-sdk/src/data_sources/streaming_data_source.cpp b/libs/client-sdk/src/data_sources/streaming_data_source.cpp index 9b2e169d2..39c559252 100644 --- a/libs/client-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/client-sdk/src/data_sources/streaming_data_source.cpp @@ -132,6 +132,10 @@ void StreamingDataSource::Start() { client_builder.skip_verify_peer(true); } + if (auto ca_file = http_config_.Tls().CustomCAFile()) { + client_builder.custom_ca_file(*ca_file); + } + auto weak_self = weak_from_this(); client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) { diff --git a/libs/client-sdk/tests/client_config_test.cpp b/libs/client-sdk/tests/client_config_test.cpp index c62357541..df94f48d8 100644 --- a/libs/client-sdk/tests/client_config_test.cpp +++ b/libs/client-sdk/tests/client_config_test.cpp @@ -82,6 +82,7 @@ TEST(ClientConfigBindings, AllConfigs) { LDClientHttpPropertiesTlsBuilder tls_builder = LDClientHttpPropertiesTlsBuilder_New(); LDClientHttpPropertiesTlsBuilder_SkipVerifyPeer(tls_builder, false); + LDClientHttpPropertiesTlsBuilder_CustomCAFile(tls_builder, "ca.pem"); LDClientConfigBuilder_HttpProperties_Tls(builder, tls_builder); LDClientHttpPropertiesTlsBuilder tls_builder2 = diff --git a/libs/common/include/launchdarkly/config/shared/builders/http_properties_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/http_properties_builder.hpp index 6c7334ec9..dbe312df8 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/http_properties_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/http_properties_builder.hpp @@ -40,6 +40,21 @@ class TlsBuilder { */ TlsBuilder& SkipVerifyPeer(bool skip_verify_peer); + /** + * Path to a file containing one or more CAs to verify + * the peer with. The certificate(s) must be PEM-encoded. + * + * By default, the SDK uses the system's root CA bundle. + * + * If the empty string is passed, this function will clear any existing + * CA bundle path previously set, and the system's root CA bundle will be + * used. + * + * @param custom_ca_file File path. + * @return A reference to this builder. + */ + TlsBuilder& CustomCAFile(std::string custom_ca_file); + /** * Builds the TLS options. * @return The built options. @@ -48,6 +63,7 @@ class TlsBuilder { private: enum built::TlsOptions::VerifyMode verify_mode_; + std::optional custom_ca_file_; }; /** * Class used for building a set of HttpProperties. diff --git a/libs/common/include/launchdarkly/config/shared/built/http_properties.hpp b/libs/common/include/launchdarkly/config/shared/built/http_properties.hpp index ac24267c1..a21e7143f 100644 --- a/libs/common/include/launchdarkly/config/shared/built/http_properties.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/http_properties.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -10,12 +11,16 @@ namespace launchdarkly::config::shared::built { class TlsOptions final { public: enum class VerifyMode { kVerifyPeer, kVerifyNone }; - TlsOptions(VerifyMode verify_mode); + explicit TlsOptions(VerifyMode verify_mode); + TlsOptions(VerifyMode verify_mode, + std::optional ca_bundle_path); TlsOptions(); [[nodiscard]] VerifyMode PeerVerifyMode() const; + [[nodiscard]] std::optional const& CustomCAFile() const; private: VerifyMode verify_mode_; + std::optional ca_bundle_path_; }; class HttpProperties final { diff --git a/libs/common/src/config/http_properties.cpp b/libs/common/src/config/http_properties.cpp index ffdc77d63..07c4213ec 100644 --- a/libs/common/src/config/http_properties.cpp +++ b/libs/common/src/config/http_properties.cpp @@ -4,15 +4,24 @@ namespace launchdarkly::config::shared::built { -TlsOptions::TlsOptions(enum TlsOptions::VerifyMode verify_mode) - : verify_mode_(verify_mode) {} +TlsOptions::TlsOptions(TlsOptions::VerifyMode verify_mode, + std::optional ca_bundle_path) + : verify_mode_(verify_mode), ca_bundle_path_(std::move(ca_bundle_path)) {} -TlsOptions::TlsOptions() : TlsOptions(TlsOptions::VerifyMode::kVerifyPeer) {} +TlsOptions::TlsOptions(TlsOptions::VerifyMode verify_mode) + : TlsOptions(verify_mode, std::nullopt) {} + +TlsOptions::TlsOptions() + : TlsOptions(TlsOptions::VerifyMode::kVerifyPeer, std::nullopt) {} TlsOptions::VerifyMode TlsOptions::PeerVerifyMode() const { return verify_mode_; } +std::optional const& TlsOptions::CustomCAFile() const { + return ca_bundle_path_; +} + HttpProperties::HttpProperties(std::chrono::milliseconds connect_timeout, std::chrono::milliseconds read_timeout, std::chrono::milliseconds write_timeout, @@ -58,7 +67,8 @@ bool operator==(HttpProperties const& lhs, HttpProperties const& rhs) { } bool operator==(TlsOptions const& lhs, TlsOptions const& rhs) { - return lhs.PeerVerifyMode() == rhs.PeerVerifyMode(); + return lhs.PeerVerifyMode() == rhs.PeerVerifyMode() && + lhs.CustomCAFile() == rhs.CustomCAFile(); } } // namespace launchdarkly::config::shared::built diff --git a/libs/common/src/config/http_properties_builder.cpp b/libs/common/src/config/http_properties_builder.cpp index 96767ff9c..836eeb397 100644 --- a/libs/common/src/config/http_properties_builder.cpp +++ b/libs/common/src/config/http_properties_builder.cpp @@ -12,6 +12,7 @@ TlsBuilder::TlsBuilder() : TlsBuilder(shared::Defaults::TLS()) {} template TlsBuilder::TlsBuilder(built::TlsOptions const& tls) { verify_mode_ = tls.PeerVerifyMode(); + custom_ca_file_ = tls.CustomCAFile(); } template @@ -22,9 +23,19 @@ TlsBuilder& TlsBuilder::SkipVerifyPeer(bool skip_verify_peer) { return *this; } +template +TlsBuilder& TlsBuilder::CustomCAFile(std::string custom_ca_file) { + if (custom_ca_file.empty()) { + custom_ca_file_ = std::nullopt; + } else { + custom_ca_file_ = std::move(custom_ca_file); + } + return *this; +} + template built::TlsOptions TlsBuilder::Build() const { - return {verify_mode_}; + return {verify_mode_, custom_ca_file_}; } template diff --git a/libs/internal/include/launchdarkly/events/detail/request_worker.hpp b/libs/internal/include/launchdarkly/events/detail/request_worker.hpp index 232163edb..f2d01da24 100644 --- a/libs/internal/include/launchdarkly/events/detail/request_worker.hpp +++ b/libs/internal/include/launchdarkly/events/detail/request_worker.hpp @@ -99,13 +99,12 @@ class RequestWorker { * @param mode TLS peer verification mode. * @param logger Logger. */ - RequestWorker( - boost::asio::any_io_executor io, - std::chrono::milliseconds retry_after, - std::size_t id, - std::optional date_header_locale, - enum config::shared::built::TlsOptions::VerifyMode verify_mode, - Logger& logger); + RequestWorker(boost::asio::any_io_executor io, + std::chrono::milliseconds retry_after, + std::size_t id, + std::optional date_header_locale, + config::shared::built::TlsOptions tls_options, + Logger& logger); /** * Returns true if the worker is available for delivery. diff --git a/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp b/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp index f83145c66..f88f52e97 100644 --- a/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp +++ b/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp @@ -30,13 +30,14 @@ class WorkerPool { * @param pool_size How many workers to make available. * @param delivery_retry_delay How long a worker should wait after a failed * delivery before trying again. - * @param verify_mode The TLS verification mode. + * @param tls_options The TLS options to use for the connection to + * LaunchDarkly event delivery endpoint. * @param logger Logger. */ WorkerPool(boost::asio::any_io_executor io, std::size_t pool_size, std::chrono::milliseconds delivery_retry_delay, - enum config::shared::built::TlsOptions::VerifyMode verify_mode, + config::shared::built::TlsOptions const& tls_options, Logger& logger); /** diff --git a/libs/internal/include/launchdarkly/network/asio_requester.hpp b/libs/internal/include/launchdarkly/network/asio_requester.hpp index 0f8b23ac8..a4078953c 100644 --- a/libs/internal/include/launchdarkly/network/asio_requester.hpp +++ b/libs/internal/include/launchdarkly/network/asio_requester.hpp @@ -3,6 +3,7 @@ #include "http_requester.hpp" #include +#include #include #include @@ -30,7 +31,7 @@ using tcp = boost::asio::ip::tcp; namespace launchdarkly::network { -using VerifyMode = config::shared::built::TlsOptions::VerifyMode; +using TlsOptions = config::shared::built::TlsOptions; static unsigned char const kRedirectLimit = 20; @@ -258,14 +259,26 @@ class AsioRequester { * must be accounted for. */ public: - AsioRequester(net::any_io_executor ctx, VerifyMode verify_mode) + AsioRequester(net::any_io_executor ctx, TlsOptions const& tls_options) : ctx_(std::move(ctx)), ssl_ctx_(std::make_shared( launchdarkly::foxy::make_ssl_ctx(ssl::context::tlsv12_client))) { + ssl_ctx_->set_verify_mode(ssl::verify_peer); ssl_ctx_->set_default_verify_paths(); - ssl_ctx_->set_verify_mode(verify_mode == VerifyMode::kVerifyPeer - ? ssl::verify_peer - : ssl::verify_none); + + std::optional const& custom_ca_file = + tls_options.CustomCAFile(); + + if (custom_ca_file) { + // The builder should enforce that the path (if set) is not empty. + LD_ASSERT(!custom_ca_file->empty()); + ssl_ctx_->load_verify_file(custom_ca_file->c_str()); + } + + using VerifyMode = config::shared::built::TlsOptions::VerifyMode; + if (tls_options.PeerVerifyMode() == VerifyMode::kVerifyNone) { + ssl_ctx_->set_verify_mode(ssl::verify_none); + } } template diff --git a/libs/internal/src/events/asio_event_processor.cpp b/libs/internal/src/events/asio_event_processor.cpp index e1f988529..6f56b7dcf 100644 --- a/libs/internal/src/events/asio_event_processor.cpp +++ b/libs/internal/src/events/asio_event_processor.cpp @@ -47,7 +47,7 @@ AsioEventProcessor::AsioEventProcessor( workers_(io_, events_config.FlushWorkers(), events_config.DeliveryRetryDelay(), - http_properties.Tls().PeerVerifyMode(), + http_properties.Tls(), logger), inbox_capacity_(events_config.Capacity()), inbox_size_(0), diff --git a/libs/internal/src/events/request_worker.cpp b/libs/internal/src/events/request_worker.cpp index e5437777c..36847ad81 100644 --- a/libs/internal/src/events/request_worker.cpp +++ b/libs/internal/src/events/request_worker.cpp @@ -3,17 +3,16 @@ namespace launchdarkly::events::detail { -RequestWorker::RequestWorker( - boost::asio::any_io_executor io, - std::chrono::milliseconds retry_after, - std::size_t id, - std::optional date_header_locale, - enum config::shared::built::TlsOptions::VerifyMode verify_mode, - Logger& logger) +RequestWorker::RequestWorker(boost::asio::any_io_executor io, + std::chrono::milliseconds retry_after, + std::size_t id, + std::optional date_header_locale, + config::shared::built::TlsOptions tls_options, + Logger& logger) : timer_(std::move(io)), retry_delay_(retry_after), state_(State::Idle), - requester_(timer_.get_executor(), verify_mode), + requester_(timer_.get_executor(), tls_options), batch_(std::nullopt), tag_("flush-worker[" + std::to_string(id) + "]: "), date_header_locale_(std::move(date_header_locale)), diff --git a/libs/internal/src/events/worker_pool.cpp b/libs/internal/src/events/worker_pool.cpp index 07a5df6a1..629ae7e3e 100644 --- a/libs/internal/src/events/worker_pool.cpp +++ b/libs/internal/src/events/worker_pool.cpp @@ -24,7 +24,7 @@ std::optional GetLocale(std::string const& locale, WorkerPool::WorkerPool(boost::asio::any_io_executor io, std::size_t pool_size, std::chrono::milliseconds delivery_retry_delay, - enum TlsOptions::VerifyMode verify_mode, + TlsOptions const& tls_options, Logger& logger) : io_(io), workers_() { // The en_US.utf-8 locale is used whenever a date is parsed from the HTTP @@ -38,7 +38,7 @@ WorkerPool::WorkerPool(boost::asio::any_io_executor io, for (std::size_t i = 0; i < pool_size; i++) { workers_.emplace_back(std::make_unique( - io_, delivery_retry_delay, i, date_header_locale, verify_mode, + io_, delivery_retry_delay, i, date_header_locale, tls_options, logger)); } } diff --git a/libs/internal/tests/event_processor_test.cpp b/libs/internal/tests/event_processor_test.cpp index 2c718db1a..4e735bed9 100644 --- a/libs/internal/tests/event_processor_test.cpp +++ b/libs/internal/tests/event_processor_test.cpp @@ -13,6 +13,7 @@ using namespace launchdarkly::events; using namespace launchdarkly::events::detail; using namespace launchdarkly::network; +using namespace launchdarkly::config::shared; static std::chrono::system_clock::time_point TimeZero() { return std::chrono::system_clock::time_point{}; @@ -32,7 +33,7 @@ TEST(WorkerPool, PoolReturnsAvailableWorker) { std::thread ioc_thread([&]() { ioc.run(); }); WorkerPool pool(ioc.get_executor(), 1, std::chrono::seconds(1), - VerifyMode::kVerifyPeer, logger); + built::TlsOptions{}, logger); RequestWorker* worker = pool.Get(boost::asio::use_future).get(); ASSERT_TRUE(worker); @@ -51,7 +52,7 @@ TEST(WorkerPool, PoolReturnsNullptrWhenNoWorkerAvaialable) { std::thread ioc_thread([&]() { ioc.run(); }); WorkerPool pool(ioc.get_executor(), 0, std::chrono::seconds(1), - VerifyMode::kVerifyPeer, logger); + built::TlsOptions{}, logger); RequestWorker* worker = pool.Get(boost::asio::use_future).get(); ASSERT_FALSE(worker); diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h index 35ba0327f..11ecfb786 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h @@ -397,6 +397,26 @@ LDServerHttpPropertiesTlsBuilder_SkipVerifyPeer( LDServerHttpPropertiesTlsBuilder b, bool skip_verify_peer); +/** + * Configures TLS peer certificate verification to use a custom + * CA file. + * + * The parameter is a filepath pointing to a bundle of + * one or more PEM-encoded x509 certificates comprising the root of trust for + * the SDK's outbound connections. + * + * By default, the SDK uses the system's CA bundle. Passing the empty string + * will unset any previously set path and revert to the system's CA bundle. + * + * @param b Server config builder. Must not be NULL. + * @param custom_ca_file Filepath of the custom CA bundle, or empty string. + * Must not be NULL. + */ +LD_EXPORT(void) +LDServerHttpPropertiesTlsBuilder_CustomCAFile( + LDServerHttpPropertiesTlsBuilder b, + char const* custom_ca_file); + /** * Disables the default SDK logging. * @param b Server config builder. Must not be NULL. diff --git a/libs/server-sdk/src/bindings/c/builder.cpp b/libs/server-sdk/src/bindings/c/builder.cpp index eb9a0db75..8bdc43b15 100644 --- a/libs/server-sdk/src/bindings/c/builder.cpp +++ b/libs/server-sdk/src/bindings/c/builder.cpp @@ -358,6 +358,16 @@ LDServerHttpPropertiesTlsBuilder_SkipVerifyPeer( TO_TLS_BUILDER(b)->SkipVerifyPeer(skip_verify_peer); } +LD_EXPORT(void) +LDServerHttpPropertiesTlsBuilder_CustomCAFile( + LDServerHttpPropertiesTlsBuilder b, + char const* custom_ca_file) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(custom_ca_file); + + TO_TLS_BUILDER(b)->CustomCAFile(custom_ca_file); +} + LD_EXPORT(LDServerHttpPropertiesTlsBuilder) LDServerHttpPropertiesTlsBuilder_New(void) { return FROM_TLS_BUILDER(new TlsBuilder()); diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index f176f4dc4..37eaf1b18 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -8,6 +8,7 @@ #include "data_interfaces/system/idata_system.hpp" +#include #include #include #include @@ -124,6 +125,17 @@ ClientImpl::ClientImpl(Config config, std::string const& version) EventFactory::WithReasons()) { LD_LOG(logger_, LogLevel::kDebug) << "data system: " << data_system_->Identity(); + if (auto custom_ca = http_properties_.Tls().CustomCAFile()) { + LD_LOG(logger_, LogLevel::kInfo) + << "TLS peer verification configured with custom CA file: " + << *custom_ca; + } + if (http_properties_.Tls().PeerVerifyMode() == + launchdarkly::config::shared::built::TlsOptions::VerifyMode:: + kVerifyNone) { + LD_LOG(logger_, LogLevel::kInfo) << "TLS peer verification disabled"; + } + run_thread_ = std::move(std::thread([&]() { ioc_.run(); })); } diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp index 694bad87a..a0d830843 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp @@ -52,16 +52,11 @@ PollingDataSource::PollingDataSource( config::built::HttpProperties const& http_properties) : logger_(logger), status_manager_(status_manager), - requester_(ioc, http_properties.Tls().PeerVerifyMode()), + requester_(ioc, http_properties.Tls()), polling_interval_(data_source_config.poll_interval), request_(MakeRequest(data_source_config, endpoints, http_properties)), timer_(ioc), sink_(nullptr) { - if (http_properties.Tls().PeerVerifyMode() == - launchdarkly::config::shared::built::TlsOptions::VerifyMode:: - kVerifyNone) { - LD_LOG(logger_, LogLevel::kDebug) << "TLS peer verification disabled"; - } if (polling_interval_ < data_source_config.min_polling_interval) { LD_LOG(logger_, LogLevel::kWarn) << "Polling interval too frequent, defaulting to " diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp index d58d7662f..2e7de1a4f 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp @@ -110,6 +110,10 @@ void StreamingDataSource::StartAsync( client_builder.skip_verify_peer(true); } + if (auto ca_file = http_config_.Tls().CustomCAFile()) { + client_builder.custom_ca_file(*ca_file); + } + auto weak_self = weak_from_this(); client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) { diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp index 3c6bac2ea..dc68fa8f6 100644 --- a/libs/server-sdk/tests/server_c_bindings_test.cpp +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -251,7 +251,7 @@ TEST(ClientBindings, LazyLoadDataSource) { LDStatus_Free(status); } -TEST(ClientBindings, TlsConfiguration) { +TEST(ClientBindings, TlsConfigurationSkipVerifyPeer) { LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); LDServerHttpPropertiesTlsBuilder tls = @@ -262,6 +262,42 @@ TEST(ClientBindings, TlsConfiguration) { LDServerConfig config; LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + ASSERT_TRUE(LDStatus_Ok(status)); + + LDServerConfig_Free(config); +} + +TEST(ClientBindings, TlsConfigurationCustomCAFile) { + LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + + LDServerHttpPropertiesTlsBuilder tls = + LDServerHttpPropertiesTlsBuilder_New(); + LDServerHttpPropertiesTlsBuilder_CustomCAFile(tls, "/path/to/file.pem"); + + LDServerConfigBuilder_HttpProperties_Tls(cfg_builder, tls); + + LDServerConfig config; + LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + ASSERT_TRUE(LDStatus_Ok(status)); + + LDServerConfig_Free(config); +} + +TEST(ClientBindings, TlsConfigurationSystemCAFile) { + LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + + LDServerHttpPropertiesTlsBuilder tls = + LDServerHttpPropertiesTlsBuilder_New(); + + // Set, then unset the file. This should not cause a configuration error. + LDServerHttpPropertiesTlsBuilder_CustomCAFile(tls, "/path/to/file.pem"); + LDServerHttpPropertiesTlsBuilder_CustomCAFile(tls, ""); + + LDServerConfigBuilder_HttpProperties_Tls(cfg_builder, tls); + + LDServerConfig config; + LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + ASSERT_TRUE(LDStatus_Ok(status)); LDServerConfig_Free(config); } diff --git a/libs/server-sent-events/include/launchdarkly/sse/client.hpp b/libs/server-sent-events/include/launchdarkly/sse/client.hpp index 1143f13b5..2bf93989c 100644 --- a/libs/server-sent-events/include/launchdarkly/sse/client.hpp +++ b/libs/server-sent-events/include/launchdarkly/sse/client.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include namespace launchdarkly::sse { @@ -140,6 +141,18 @@ class Builder { */ Builder& skip_verify_peer(bool skip_verify_peer); + /** + * Specify the path to a CA bundle file for verifying the peer's + * certificate. + * + * By default, the system's CA bundle is used. Passing an empty string will + * unset any previously set path and revert to the system's CA bundle. + * + * @param path The filepath. + * @return Reference to this builder. + */ + Builder& custom_ca_file(std::string path); + /** * Builds a Client. The shared pointer is necessary to extend the lifetime * of the Client to encompass each asynchronous operation that it performs. @@ -160,6 +173,7 @@ class Builder { EventReceiver receiver_; ErrorCallback error_cb_; bool skip_verify_peer_; + std::optional custom_ca_file_; }; /** diff --git a/libs/server-sent-events/src/client.cpp b/libs/server-sent-events/src/client.cpp index d9dd11afc..4f104c264 100644 --- a/libs/server-sent-events/src/client.cpp +++ b/libs/server-sent-events/src/client.cpp @@ -504,7 +504,8 @@ Builder::Builder(net::any_io_executor ctx, std::string url) logging_cb_([](auto msg) {}), receiver_([](launchdarkly::sse::Event const&) {}), error_cb_([](auto err) {}), - skip_verify_peer_(false) { + skip_verify_peer_(false), + custom_ca_file_(std::nullopt) { request_.version(11); request_.set(http::field::user_agent, kDefaultUserAgent); request_.method(http::verb::get); @@ -567,6 +568,15 @@ Builder& Builder::skip_verify_peer(bool skip_verify_peer) { return *this; } +Builder& Builder::custom_ca_file(std::string path) { + if (path.empty()) { + custom_ca_file_ = std::nullopt; + return *this; + } + custom_ca_file_ = std::move(path); + return *this; +} + std::shared_ptr Builder::build() { auto uri_components = boost::urls::parse_uri(url_); if (!uri_components) { @@ -612,10 +622,17 @@ std::shared_ptr Builder::build() { std::optional ssl; if (uri_components->scheme_id() == boost::urls::scheme::https) { ssl = launchdarkly::foxy::make_ssl_ctx(ssl::context::tlsv12_client); + ssl->set_default_verify_paths(); + ssl->set_verify_mode(ssl::context::verify_peer); + + if (custom_ca_file_) { + assert(!custom_ca_file_->empty()); + ssl->load_verify_file(*custom_ca_file_); + } + if (skip_verify_peer_) { ssl->set_verify_mode(ssl::context::verify_none); - logging_cb_("TLS peer verification disabled"); } }