Skip to content

Commit 857dd28

Browse files
authored
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.
1 parent db0a9eb commit 857dd28

File tree

30 files changed

+264
-49
lines changed

30 files changed

+264
-49
lines changed

contract-tests/client-contract-tests/src/entity_manager.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ std::optional<std::string> EntityManager::create(ConfigParams const& in) {
134134
if (in.tls->skipVerifyPeer) {
135135
builder.SkipVerifyPeer(*in.tls->skipVerifyPeer);
136136
}
137+
if (in.tls->customCAFile) {
138+
builder.CustomCAFile(*in.tls->customCAFile);
139+
}
137140
config_builder.HttpProperties().Tls(std::move(builder));
138141
}
139142

contract-tests/client-contract-tests/src/main.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ int main(int argc, char* argv[]) {
4545
srv.add_capability("anonymous-redaction");
4646
srv.add_capability("tls:verify-peer");
4747
srv.add_capability("tls:skip-verify-peer");
48-
48+
srv.add_capability("tls:custom-ca");
49+
4950
net::signal_set signals{ioc, SIGINT, SIGTERM};
5051

5152
boost::asio::spawn(ioc.get_executor(), [&](auto yield) mutable {

contract-tests/data-model/include/data_model/data_model.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@ struct adl_serializer<std::optional<T>> {
3131

3232
struct ConfigTLSParams {
3333
std::optional<bool> skipVerifyPeer;
34+
std::optional<std::string> customCAFile;
3435
};
36+
3537
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(ConfigTLSParams,
36-
skipVerifyPeer);
38+
skipVerifyPeer,
39+
customCAFile);
3740

3841
struct ConfigStreamingParams {
3942
std::optional<std::string> baseUri;

contract-tests/server-contract-tests/src/entity_manager.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@ std::optional<std::string> EntityManager::create(ConfigParams const& in) {
125125
if (in.tls->skipVerifyPeer) {
126126
builder.SkipVerifyPeer(*in.tls->skipVerifyPeer);
127127
}
128+
if (in.tls->customCAFile) {
129+
builder.CustomCAFile(*in.tls->customCAFile);
130+
}
128131
config_builder.HttpProperties().Tls(std::move(builder));
129132
}
130133

contract-tests/server-contract-tests/src/main.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ int main(int argc, char* argv[]) {
4444
srv.add_capability("anonymous-redaction");
4545
srv.add_capability("tls:verify-peer");
4646
srv.add_capability("tls:skip-verify-peer");
47+
srv.add_capability("tls:custom-ca");
4748

4849
net::signal_set signals{ioc, SIGINT, SIGTERM};
4950

libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/builder.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,26 @@ LDClientHttpPropertiesTlsBuilder_SkipVerifyPeer(
440440
LDClientHttpPropertiesTlsBuilder b,
441441
bool skip_verify_peer);
442442

443+
/**
444+
* Configures TLS peer certificate verification to use a custom
445+
* CA file.
446+
*
447+
* The parameter is a filepath pointing to a bundle of
448+
* one or more PEM-encoded x509 certificates comprising the root of trust for
449+
* the SDK's outbound connections.
450+
*
451+
* By default, the SDK uses the system's CA bundle. Passing the empty string
452+
* will unset any previously set path and revert to the system's CA bundle.
453+
*
454+
* @param b Client config builder. Must not be NULL.
455+
* @param custom_ca_file Filepath of the custom CA bundle, or empty string. Must
456+
* not be NULL.
457+
*/
458+
LD_EXPORT(void)
459+
LDClientHttpPropertiesTlsBuilder_CustomCAFile(
460+
LDClientHttpPropertiesTlsBuilder b,
461+
char const* custom_ca_file);
462+
443463
/**
444464
* Disables the default SDK logging.
445465
* @param b Client config builder. Must not be NULL.

libs/client-sdk/src/bindings/c/builder.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,16 @@ LDClientHttpPropertiesTlsBuilder_SkipVerifyPeer(
332332
TO_TLS_BUILDER(b)->SkipVerifyPeer(skip_verify_peer);
333333
}
334334

335+
LD_EXPORT(void)
336+
LDClientHttpPropertiesTlsBuilder_CustomCAFile(
337+
LDClientHttpPropertiesTlsBuilder b,
338+
char const* custom_ca_file) {
339+
LD_ASSERT_NOT_NULL(b);
340+
LD_ASSERT_NOT_NULL(custom_ca_file);
341+
342+
TO_TLS_BUILDER(b)->CustomCAFile(custom_ca_file);
343+
}
344+
335345
LD_EXPORT(LDClientHttpPropertiesTlsBuilder)
336346
LDClientHttpPropertiesTlsBuilder_New(void) {
337347
return FROM_TLS_BUILDER(new TlsBuilder());

libs/client-sdk/src/client_impl.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ ClientImpl::ClientImpl(Config in_cfg,
109109
eval_reasons_available_(config_.DataSourceConfig().with_reasons) {
110110
flag_manager_.LoadCache(context_);
111111

112+
if (auto custom_ca = http_properties_.Tls().CustomCAFile()) {
113+
LD_LOG(logger_, LogLevel::kInfo)
114+
<< "TLS peer verification configured with custom CA file: "
115+
<< *custom_ca;
116+
}
117+
if (http_properties_.Tls().PeerVerifyMode() ==
118+
config::shared::built::TlsOptions::VerifyMode::kVerifyNone) {
119+
LD_LOG(logger_, LogLevel::kInfo) << "TLS peer verification disabled";
120+
}
121+
112122
if (config_.Events().Enabled() && !config_.Offline()) {
113123
event_processor_ =
114124
std::make_unique<events::AsioEventProcessor<ClientSDK>>(

libs/client-sdk/src/data_sources/polling_data_source.cpp

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ PollingDataSource::PollingDataSource(
7474
status_manager_(status_manager),
7575
data_source_handler_(
7676
DataSourceEventHandler(context, handler, logger, status_manager_)),
77-
requester_(ioc, http_properties.Tls().PeerVerifyMode()),
77+
requester_(ioc, http_properties.Tls()),
7878
timer_(ioc),
7979
polling_interval_(
8080
std::get<
@@ -88,10 +88,6 @@ PollingDataSource::PollingDataSource(
8888
auto const& polling_config = std::get<
8989
config::shared::built::PollingConfig<config::shared::ClientSDK>>(
9090
data_source_config.method);
91-
if (http_properties.Tls().PeerVerifyMode() ==
92-
config::shared::built::TlsOptions::VerifyMode::kVerifyNone) {
93-
LD_LOG(logger_, LogLevel::kDebug) << "TLS peer verification disabled";
94-
}
9591
if (polling_interval_ < polling_config.min_polling_interval) {
9692
LD_LOG(logger_, LogLevel::kWarn)
9793
<< "Polling interval too frequent, defaulting to "

libs/client-sdk/src/data_sources/streaming_data_source.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ void StreamingDataSource::Start() {
132132
client_builder.skip_verify_peer(true);
133133
}
134134

135+
if (auto ca_file = http_config_.Tls().CustomCAFile()) {
136+
client_builder.custom_ca_file(*ca_file);
137+
}
138+
135139
auto weak_self = weak_from_this();
136140

137141
client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) {

libs/client-sdk/tests/client_config_test.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ TEST(ClientConfigBindings, AllConfigs) {
8282
LDClientHttpPropertiesTlsBuilder tls_builder =
8383
LDClientHttpPropertiesTlsBuilder_New();
8484
LDClientHttpPropertiesTlsBuilder_SkipVerifyPeer(tls_builder, false);
85+
LDClientHttpPropertiesTlsBuilder_CustomCAFile(tls_builder, "ca.pem");
8586
LDClientConfigBuilder_HttpProperties_Tls(builder, tls_builder);
8687

8788
LDClientHttpPropertiesTlsBuilder tls_builder2 =

libs/common/include/launchdarkly/config/shared/builders/http_properties_builder.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ class TlsBuilder {
4040
*/
4141
TlsBuilder& SkipVerifyPeer(bool skip_verify_peer);
4242

43+
/**
44+
* Path to a file containing one or more CAs to verify
45+
* the peer with. The certificate(s) must be PEM-encoded.
46+
*
47+
* By default, the SDK uses the system's root CA bundle.
48+
*
49+
* If the empty string is passed, this function will clear any existing
50+
* CA bundle path previously set, and the system's root CA bundle will be
51+
* used.
52+
*
53+
* @param custom_ca_file File path.
54+
* @return A reference to this builder.
55+
*/
56+
TlsBuilder& CustomCAFile(std::string custom_ca_file);
57+
4358
/**
4459
* Builds the TLS options.
4560
* @return The built options.
@@ -48,6 +63,7 @@ class TlsBuilder {
4863

4964
private:
5065
enum built::TlsOptions::VerifyMode verify_mode_;
66+
std::optional<std::string> custom_ca_file_;
5167
};
5268
/**
5369
* Class used for building a set of HttpProperties.

libs/common/include/launchdarkly/config/shared/built/http_properties.hpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <chrono>
44
#include <map>
5+
#include <optional>
56
#include <string>
67
#include <vector>
78

@@ -10,12 +11,16 @@ namespace launchdarkly::config::shared::built {
1011
class TlsOptions final {
1112
public:
1213
enum class VerifyMode { kVerifyPeer, kVerifyNone };
13-
TlsOptions(VerifyMode verify_mode);
14+
explicit TlsOptions(VerifyMode verify_mode);
15+
TlsOptions(VerifyMode verify_mode,
16+
std::optional<std::string> ca_bundle_path);
1417
TlsOptions();
1518
[[nodiscard]] VerifyMode PeerVerifyMode() const;
19+
[[nodiscard]] std::optional<std::string> const& CustomCAFile() const;
1620

1721
private:
1822
VerifyMode verify_mode_;
23+
std::optional<std::string> ca_bundle_path_;
1924
};
2025

2126
class HttpProperties final {

libs/common/src/config/http_properties.cpp

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,24 @@
44

55
namespace launchdarkly::config::shared::built {
66

7-
TlsOptions::TlsOptions(enum TlsOptions::VerifyMode verify_mode)
8-
: verify_mode_(verify_mode) {}
7+
TlsOptions::TlsOptions(TlsOptions::VerifyMode verify_mode,
8+
std::optional<std::string> ca_bundle_path)
9+
: verify_mode_(verify_mode), ca_bundle_path_(std::move(ca_bundle_path)) {}
910

10-
TlsOptions::TlsOptions() : TlsOptions(TlsOptions::VerifyMode::kVerifyPeer) {}
11+
TlsOptions::TlsOptions(TlsOptions::VerifyMode verify_mode)
12+
: TlsOptions(verify_mode, std::nullopt) {}
13+
14+
TlsOptions::TlsOptions()
15+
: TlsOptions(TlsOptions::VerifyMode::kVerifyPeer, std::nullopt) {}
1116

1217
TlsOptions::VerifyMode TlsOptions::PeerVerifyMode() const {
1318
return verify_mode_;
1419
}
1520

21+
std::optional<std::string> const& TlsOptions::CustomCAFile() const {
22+
return ca_bundle_path_;
23+
}
24+
1625
HttpProperties::HttpProperties(std::chrono::milliseconds connect_timeout,
1726
std::chrono::milliseconds read_timeout,
1827
std::chrono::milliseconds write_timeout,
@@ -58,7 +67,8 @@ bool operator==(HttpProperties const& lhs, HttpProperties const& rhs) {
5867
}
5968

6069
bool operator==(TlsOptions const& lhs, TlsOptions const& rhs) {
61-
return lhs.PeerVerifyMode() == rhs.PeerVerifyMode();
70+
return lhs.PeerVerifyMode() == rhs.PeerVerifyMode() &&
71+
lhs.CustomCAFile() == rhs.CustomCAFile();
6272
}
6373

6474
} // namespace launchdarkly::config::shared::built

libs/common/src/config/http_properties_builder.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ TlsBuilder<SDK>::TlsBuilder() : TlsBuilder(shared::Defaults<SDK>::TLS()) {}
1212
template <typename SDK>
1313
TlsBuilder<SDK>::TlsBuilder(built::TlsOptions const& tls) {
1414
verify_mode_ = tls.PeerVerifyMode();
15+
custom_ca_file_ = tls.CustomCAFile();
1516
}
1617

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

26+
template <typename SDK>
27+
TlsBuilder<SDK>& TlsBuilder<SDK>::CustomCAFile(std::string custom_ca_file) {
28+
if (custom_ca_file.empty()) {
29+
custom_ca_file_ = std::nullopt;
30+
} else {
31+
custom_ca_file_ = std::move(custom_ca_file);
32+
}
33+
return *this;
34+
}
35+
2536
template <typename SDK>
2637
built::TlsOptions TlsBuilder<SDK>::Build() const {
27-
return {verify_mode_};
38+
return {verify_mode_, custom_ca_file_};
2839
}
2940

3041
template <typename SDK>

libs/internal/include/launchdarkly/events/detail/request_worker.hpp

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,12 @@ class RequestWorker {
9999
* @param mode TLS peer verification mode.
100100
* @param logger Logger.
101101
*/
102-
RequestWorker(
103-
boost::asio::any_io_executor io,
104-
std::chrono::milliseconds retry_after,
105-
std::size_t id,
106-
std::optional<std::locale> date_header_locale,
107-
enum config::shared::built::TlsOptions::VerifyMode verify_mode,
108-
Logger& logger);
102+
RequestWorker(boost::asio::any_io_executor io,
103+
std::chrono::milliseconds retry_after,
104+
std::size_t id,
105+
std::optional<std::locale> date_header_locale,
106+
config::shared::built::TlsOptions tls_options,
107+
Logger& logger);
109108

110109
/**
111110
* Returns true if the worker is available for delivery.

libs/internal/include/launchdarkly/events/detail/worker_pool.hpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@ class WorkerPool {
3030
* @param pool_size How many workers to make available.
3131
* @param delivery_retry_delay How long a worker should wait after a failed
3232
* delivery before trying again.
33-
* @param verify_mode The TLS verification mode.
33+
* @param tls_options The TLS options to use for the connection to
34+
* LaunchDarkly event delivery endpoint.
3435
* @param logger Logger.
3536
*/
3637
WorkerPool(boost::asio::any_io_executor io,
3738
std::size_t pool_size,
3839
std::chrono::milliseconds delivery_retry_delay,
39-
enum config::shared::built::TlsOptions::VerifyMode verify_mode,
40+
config::shared::built::TlsOptions const& tls_options,
4041
Logger& logger);
4142

4243
/**

libs/internal/include/launchdarkly/network/asio_requester.hpp

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "http_requester.hpp"
44

55
#include <launchdarkly/config/shared/built/http_properties.hpp>
6+
#include <launchdarkly/detail/c_binding_helpers.hpp>
67
#include <launchdarkly/detail/unreachable.hpp>
78

89
#include <boost/asio.hpp>
@@ -30,7 +31,7 @@ using tcp = boost::asio::ip::tcp;
3031

3132
namespace launchdarkly::network {
3233

33-
using VerifyMode = config::shared::built::TlsOptions::VerifyMode;
34+
using TlsOptions = config::shared::built::TlsOptions;
3435

3536
static unsigned char const kRedirectLimit = 20;
3637

@@ -258,14 +259,26 @@ class AsioRequester {
258259
* must be accounted for.
259260
*/
260261
public:
261-
AsioRequester(net::any_io_executor ctx, VerifyMode verify_mode)
262+
AsioRequester(net::any_io_executor ctx, TlsOptions const& tls_options)
262263
: ctx_(std::move(ctx)),
263264
ssl_ctx_(std::make_shared<net::ssl::context>(
264265
launchdarkly::foxy::make_ssl_ctx(ssl::context::tlsv12_client))) {
266+
ssl_ctx_->set_verify_mode(ssl::verify_peer);
265267
ssl_ctx_->set_default_verify_paths();
266-
ssl_ctx_->set_verify_mode(verify_mode == VerifyMode::kVerifyPeer
267-
? ssl::verify_peer
268-
: ssl::verify_none);
268+
269+
std::optional<std::string> const& custom_ca_file =
270+
tls_options.CustomCAFile();
271+
272+
if (custom_ca_file) {
273+
// The builder should enforce that the path (if set) is not empty.
274+
LD_ASSERT(!custom_ca_file->empty());
275+
ssl_ctx_->load_verify_file(custom_ca_file->c_str());
276+
}
277+
278+
using VerifyMode = config::shared::built::TlsOptions::VerifyMode;
279+
if (tls_options.PeerVerifyMode() == VerifyMode::kVerifyNone) {
280+
ssl_ctx_->set_verify_mode(ssl::verify_none);
281+
}
269282
}
270283

271284
template <typename CompletionToken>

libs/internal/src/events/asio_event_processor.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ AsioEventProcessor<SDK>::AsioEventProcessor(
4747
workers_(io_,
4848
events_config.FlushWorkers(),
4949
events_config.DeliveryRetryDelay(),
50-
http_properties.Tls().PeerVerifyMode(),
50+
http_properties.Tls(),
5151
logger),
5252
inbox_capacity_(events_config.Capacity()),
5353
inbox_size_(0),

libs/internal/src/events/request_worker.cpp

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,16 @@
33

44
namespace launchdarkly::events::detail {
55

6-
RequestWorker::RequestWorker(
7-
boost::asio::any_io_executor io,
8-
std::chrono::milliseconds retry_after,
9-
std::size_t id,
10-
std::optional<std::locale> date_header_locale,
11-
enum config::shared::built::TlsOptions::VerifyMode verify_mode,
12-
Logger& logger)
6+
RequestWorker::RequestWorker(boost::asio::any_io_executor io,
7+
std::chrono::milliseconds retry_after,
8+
std::size_t id,
9+
std::optional<std::locale> date_header_locale,
10+
config::shared::built::TlsOptions tls_options,
11+
Logger& logger)
1312
: timer_(std::move(io)),
1413
retry_delay_(retry_after),
1514
state_(State::Idle),
16-
requester_(timer_.get_executor(), verify_mode),
15+
requester_(timer_.get_executor(), tls_options),
1716
batch_(std::nullopt),
1817
tag_("flush-worker[" + std::to_string(id) + "]: "),
1918
date_header_locale_(std::move(date_header_locale)),

0 commit comments

Comments
 (0)