Skip to content

Commit

Permalink
feat: specify a custom CA file for TLS peer verification (#409)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cwaldren-ld authored May 30, 2024
1 parent db0a9eb commit 857dd28
Show file tree
Hide file tree
Showing 30 changed files with 264 additions and 49 deletions.
3 changes: 3 additions & 0 deletions contract-tests/client-contract-tests/src/entity_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ std::optional<std::string> 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));
}

Expand Down
3 changes: 2 additions & 1 deletion contract-tests/client-contract-tests/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 4 additions & 1 deletion contract-tests/data-model/include/data_model/data_model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ struct adl_serializer<std::optional<T>> {

struct ConfigTLSParams {
std::optional<bool> skipVerifyPeer;
std::optional<std::string> customCAFile;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigTLSParams,
skipVerifyPeer);
skipVerifyPeer,
customCAFile);

struct ConfigStreamingParams {
std::optional<std::string> baseUri;
Expand Down
3 changes: 3 additions & 0 deletions contract-tests/server-contract-tests/src/entity_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ std::optional<std::string> 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));
}

Expand Down
1 change: 1 addition & 0 deletions contract-tests/server-contract-tests/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions libs/client-sdk/src/bindings/c/builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
10 changes: 10 additions & 0 deletions libs/client-sdk/src/client_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<events::AsioEventProcessor<ClientSDK>>(
Expand Down
6 changes: 1 addition & 5 deletions libs/client-sdk/src/data_sources/polling_data_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand All @@ -88,10 +88,6 @@ PollingDataSource::PollingDataSource(
auto const& polling_config = std::get<
config::shared::built::PollingConfig<config::shared::ClientSDK>>(
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 "
Expand Down
4 changes: 4 additions & 0 deletions libs/client-sdk/src/data_sources/streaming_data_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions libs/client-sdk/tests/client_config_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -48,6 +63,7 @@ class TlsBuilder {

private:
enum built::TlsOptions::VerifyMode verify_mode_;
std::optional<std::string> custom_ca_file_;
};
/**
* Class used for building a set of HttpProperties.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include <chrono>
#include <map>
#include <optional>
#include <string>
#include <vector>

Expand All @@ -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<std::string> ca_bundle_path);
TlsOptions();
[[nodiscard]] VerifyMode PeerVerifyMode() const;
[[nodiscard]] std::optional<std::string> const& CustomCAFile() const;

private:
VerifyMode verify_mode_;
std::optional<std::string> ca_bundle_path_;
};

class HttpProperties final {
Expand Down
18 changes: 14 additions & 4 deletions libs/common/src/config/http_properties.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> 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<std::string> 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,
Expand Down Expand Up @@ -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
13 changes: 12 additions & 1 deletion libs/common/src/config/http_properties_builder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ TlsBuilder<SDK>::TlsBuilder() : TlsBuilder(shared::Defaults<SDK>::TLS()) {}
template <typename SDK>
TlsBuilder<SDK>::TlsBuilder(built::TlsOptions const& tls) {
verify_mode_ = tls.PeerVerifyMode();
custom_ca_file_ = tls.CustomCAFile();
}

template <typename SDK>
Expand All @@ -22,9 +23,19 @@ TlsBuilder<SDK>& TlsBuilder<SDK>::SkipVerifyPeer(bool skip_verify_peer) {
return *this;
}

template <typename SDK>
TlsBuilder<SDK>& TlsBuilder<SDK>::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 <typename SDK>
built::TlsOptions TlsBuilder<SDK>::Build() const {
return {verify_mode_};
return {verify_mode_, custom_ca_file_};
}

template <typename SDK>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::locale> 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<std::locale> date_header_locale,
config::shared::built::TlsOptions tls_options,
Logger& logger);

/**
* Returns true if the worker is available for delivery.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/**
Expand Down
23 changes: 18 additions & 5 deletions libs/internal/include/launchdarkly/network/asio_requester.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "http_requester.hpp"

#include <launchdarkly/config/shared/built/http_properties.hpp>
#include <launchdarkly/detail/c_binding_helpers.hpp>
#include <launchdarkly/detail/unreachable.hpp>

#include <boost/asio.hpp>
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<net::ssl::context>(
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<std::string> 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 <typename CompletionToken>
Expand Down
2 changes: 1 addition & 1 deletion libs/internal/src/events/asio_event_processor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ AsioEventProcessor<SDK>::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),
Expand Down
15 changes: 7 additions & 8 deletions libs/internal/src/events/request_worker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::locale> 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<std::locale> 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)),
Expand Down
Loading

0 comments on commit 857dd28

Please sign in to comment.