Skip to content

Commit 39ec7d0

Browse files
committed
Add enable_system_ca() and unify WebSocketClient CA handling (#2471)
Add an explicit opt-in for loading system CA certs alongside a custom CA, addressing the request in #2471. The default behavior is unchanged: a custom CA remains exclusive. - Add Client/SSLClient/WebSocketClient::enable_system_ca(bool); the policy carries over to redirect clients - Extract the CA loading policy into detail::load_client_ca_config() shared by SSLClient and WebSocketClient, making WebSocketClient exclusive by default (it previously always merged system certs) - Make the WebSocketClient TLS context live as long as the client, fixing a use-after-free when reconnecting after set_ca_cert_store() - Free the source store in the Mbed TLS and wolfSSL set_ca_store() backends, honoring the take-ownership contract (memory leak) - Verify IP hosts against IP SANs in the OpenSSL set_hostname() backend so WebSocket connections to IP hosts can use full verification
1 parent e7e7bf7 commit 39ec7d0

2 files changed

Lines changed: 326 additions & 48 deletions

File tree

httplib.h

Lines changed: 136 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,11 @@ enum class SSLVerifierResponse {
810810
CertificateRejected
811811
};
812812

813+
// System CA loading policy for SSL clients. Auto (the default) loads system
814+
// CA certs only when no custom CA is configured; enable_system_ca() switches
815+
// to an explicit policy.
816+
enum class SystemCAMode { Auto, Enabled, Disabled };
817+
813818
enum StatusCode {
814819
// Information responses
815820
Continue_100 = 100,
@@ -2451,6 +2456,7 @@ class ClientImpl {
24512456
const std::string &ca_cert_dir_path = std::string());
24522457
void enable_server_certificate_verification(bool enabled);
24532458
void enable_server_hostname_verification(bool enabled);
2459+
void enable_system_ca(bool enabled);
24542460

24552461
protected:
24562462
std::string digest_auth_username_;
@@ -2461,6 +2467,7 @@ class ClientImpl {
24612467
std::string ca_cert_dir_path_;
24622468
bool server_certificate_verification_ = true;
24632469
bool server_hostname_verification_ = true;
2470+
SystemCAMode system_ca_mode_ = SystemCAMode::Auto;
24642471
std::string ca_cert_pem_; // Store CA cert PEM for redirect transfer
24652472
int last_ssl_error_ = 0;
24662473
uint64_t last_backend_error_ = 0;
@@ -2667,6 +2674,7 @@ class Client {
26672674
const std::string &password);
26682675
void enable_server_certificate_verification(bool enabled);
26692676
void enable_server_hostname_verification(bool enabled);
2677+
void enable_system_ca(bool enabled);
26702678
void set_ca_cert_path(const std::string &ca_cert_file_path,
26712679
const std::string &ca_cert_dir_path = std::string());
26722680

@@ -3858,7 +3866,9 @@ class WebSocketClient {
38583866
#ifdef CPPHTTPLIB_SSL_ENABLED
38593867
void set_ca_cert_path(const std::string &path);
38603868
void set_ca_cert_store(tls::ca_store_t store);
3869+
void load_ca_cert_store(const char *ca_cert, std::size_t size);
38613870
void enable_server_certificate_verification(bool enabled);
3871+
void enable_system_ca(bool enabled);
38623872
#endif
38633873

38643874
private:
@@ -3896,7 +3906,9 @@ class WebSocketClient {
38963906
tls::ctx_t tls_ctx_ = nullptr;
38973907
tls::session_t tls_session_ = nullptr;
38983908
std::string ca_cert_file_path_;
3899-
tls::ca_store_t ca_cert_store_ = nullptr;
3909+
bool custom_ca_loaded_ = false;
3910+
bool certs_loaded_ = false;
3911+
SystemCAMode system_ca_mode_ = SystemCAMode::Auto;
39003912
bool server_certificate_verification_ = true;
39013913
#endif
39023914
};
@@ -9228,25 +9240,47 @@ verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert,
92289240
}
92299241
#endif // _WIN32
92309242

9231-
inline bool setup_client_tls_session(const std::string &host, tls::ctx_t &ctx,
9243+
// Loads CA file/dir configuration and applies the system CA policy to a
9244+
// client TLS context. PEM data and native stores are applied to the context
9245+
// directly at set time; has_custom_store reflects them for the Auto policy
9246+
// decision.
9247+
inline bool load_client_ca_config(tls::ctx_t ctx,
9248+
const std::string &ca_cert_file_path,
9249+
const std::string &ca_cert_dir_path,
9250+
bool has_custom_store, SystemCAMode mode,
9251+
uint64_t &backend_error) {
9252+
auto ret = true;
9253+
9254+
if (!ca_cert_file_path.empty()) {
9255+
if (!tls::load_ca_file(ctx, ca_cert_file_path.c_str())) {
9256+
backend_error = tls::get_error();
9257+
ret = false;
9258+
}
9259+
} else if (!ca_cert_dir_path.empty()) {
9260+
if (!tls::load_ca_dir(ctx, ca_cert_dir_path.c_str())) {
9261+
backend_error = tls::get_error();
9262+
ret = false;
9263+
}
9264+
}
9265+
9266+
auto has_custom_ca = !ca_cert_file_path.empty() ||
9267+
!ca_cert_dir_path.empty() || has_custom_store;
9268+
if (mode == SystemCAMode::Enabled ||
9269+
(mode == SystemCAMode::Auto && !has_custom_ca)) {
9270+
if (!tls::load_system_certs(ctx)) { backend_error = tls::get_error(); }
9271+
}
9272+
9273+
return ret;
9274+
}
9275+
9276+
inline bool setup_client_tls_session(const std::string &host, tls::ctx_t ctx,
92329277
tls::session_t &session, socket_t sock,
92339278
bool server_certificate_verification,
9234-
const std::string &ca_cert_file_path,
9235-
tls::ca_store_t ca_cert_store,
92369279
time_t timeout_sec, time_t timeout_usec) {
92379280
using namespace tls;
92389281

9239-
ctx = create_client_context();
92409282
if (!ctx) { return false; }
92419283

9242-
if (server_certificate_verification) {
9243-
if (!ca_cert_file_path.empty()) {
9244-
load_ca_file(ctx, ca_cert_file_path.c_str());
9245-
}
9246-
if (ca_cert_store) { set_ca_store(ctx, ca_cert_store); }
9247-
load_system_certs(ctx);
9248-
}
9249-
92509284
bool is_ip = is_ip_address(host);
92519285

92529286
#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT
@@ -12579,6 +12613,7 @@ inline void ClientImpl::copy_settings(const ClientImpl &rhs) {
1257912613
ca_cert_dir_path_ = rhs.ca_cert_dir_path_;
1258012614
server_certificate_verification_ = rhs.server_certificate_verification_;
1258112615
server_hostname_verification_ = rhs.server_hostname_verification_;
12616+
system_ca_mode_ = rhs.system_ca_mode_;
1258212617
#endif
1258312618
}
1258412619

@@ -13361,6 +13396,7 @@ inline bool ClientImpl::create_redirect_client(
1336113396
server_certificate_verification_);
1336213397
redirect_client.enable_server_hostname_verification(
1336313398
server_hostname_verification_);
13399+
redirect_client.system_ca_mode_ = system_ca_mode_;
1336413400

1336513401
// Transfer CA certificate to redirect client
1336613402
if (!ca_cert_pem_.empty()) {
@@ -15012,6 +15048,10 @@ inline void ClientImpl::enable_server_certificate_verification(bool enabled) {
1501215048
inline void ClientImpl::enable_server_hostname_verification(bool enabled) {
1501315049
server_hostname_verification_ = enabled;
1501415050
}
15051+
15052+
inline void ClientImpl::enable_system_ca(bool enabled) {
15053+
system_ca_mode_ = enabled ? SystemCAMode::Enabled : SystemCAMode::Disabled;
15054+
}
1501515055
#endif
1501615056

1501715057
inline void ClientImpl::set_logger(Logger logger) {
@@ -16134,21 +16174,10 @@ inline bool SSLClient::load_certs() {
1613416174
std::call_once(initialize_cert_, [&]() {
1613516175
std::lock_guard<std::mutex> guard(ctx_mutex_);
1613616176

16137-
if (!ca_cert_file_path_.empty()) {
16138-
if (!tls::load_ca_file(ctx_, ca_cert_file_path_.c_str())) {
16139-
last_backend_error_ = tls::get_error();
16140-
ret = false;
16141-
}
16142-
} else if (!ca_cert_dir_path_.empty()) {
16143-
if (!tls::load_ca_dir(ctx_, ca_cert_dir_path_.c_str())) {
16144-
last_backend_error_ = tls::get_error();
16145-
ret = false;
16146-
}
16147-
} else if (ca_cert_pem_.empty() && !ca_cert_store_set_) {
16148-
if (!tls::load_system_certs(ctx_)) {
16149-
last_backend_error_ = tls::get_error();
16150-
}
16151-
}
16177+
ret = detail::load_client_ca_config(
16178+
ctx_, ca_cert_file_path_, ca_cert_dir_path_,
16179+
!ca_cert_pem_.empty() || ca_cert_store_set_, system_ca_mode_,
16180+
last_backend_error_);
1615216181
});
1615316182

1615416183
return ret;
@@ -16277,10 +16306,12 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
1627716306
// This provides real-time certificate validation with Windows Update
1627816307
// integration, working with both OpenSSL and MbedTLS backends.
1627916308
// Skip when a custom CA cert is specified, as the Windows certificate
16280-
// store would not know about user-provided CA certificates.
16281-
if (enable_windows_cert_verification_ && ca_cert_file_path_.empty() &&
16282-
ca_cert_dir_path_.empty() && ca_cert_pem_.empty() &&
16283-
!ca_cert_store_set_) {
16309+
// store would not know about user-provided CA certificates. Also skip
16310+
// when system CA trust is explicitly disabled.
16311+
if (enable_windows_cert_verification_ &&
16312+
system_ca_mode_ != SystemCAMode::Disabled &&
16313+
ca_cert_file_path_.empty() && ca_cert_dir_path_.empty() &&
16314+
ca_cert_pem_.empty() && !ca_cert_store_set_) {
1628416315
std::vector<unsigned char> der;
1628516316
if (get_cert_der(server_cert, der)) {
1628616317
uint64_t wincrypt_error = 0;
@@ -16319,6 +16350,10 @@ inline void Client::enable_server_hostname_verification(bool enabled) {
1631916350
cli_->enable_server_hostname_verification(enabled);
1632016351
}
1632116352

16353+
inline void Client::enable_system_ca(bool enabled) {
16354+
cli_->enable_system_ca(enabled);
16355+
}
16356+
1632216357
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
1632316358
inline void Client::enable_windows_certificate_verification(bool enabled) {
1632416359
if (is_ssl_) {
@@ -16987,15 +17022,22 @@ inline bool set_hostname(session_t session, const char *hostname) {
1698717022

1698817023
auto ssl = static_cast<SSL *>(session);
1698917024

16990-
// Set SNI (Server Name Indication)
16991-
if (!set_sni(session, hostname)) { return false; }
16992-
1699317025
// Enable hostname verification
1699417026
auto param = SSL_get0_param(ssl);
1699517027
if (!param) return false;
1699617028

16997-
X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
16998-
if (X509_VERIFY_PARAM_set1_host(param, hostname, 0) != 1) { return false; }
17029+
if (detail::is_ip_address(hostname)) {
17030+
// RFC 6066: SNI must not be set for IP addresses; verify against the
17031+
// certificate's IP SANs instead of its DNS names
17032+
if (X509_VERIFY_PARAM_set1_ip_asc(param, hostname) != 1) { return false; }
17033+
} else {
17034+
// Set SNI (Server Name Indication)
17035+
if (!set_sni(session, hostname)) { return false; }
17036+
17037+
X509_VERIFY_PARAM_set_hostflags(param,
17038+
X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
17039+
if (X509_VERIFY_PARAM_set1_host(param, hostname, 0) != 1) { return false; }
17040+
}
1699917041

1700017042
SSL_set_verify(ssl, SSL_VERIFY_PEER, nullptr);
1700117043
return true;
@@ -18784,10 +18826,17 @@ inline bool set_ca_store(ctx_t ctx, ca_store_t store) {
1878418826
while (src != nullptr) {
1878518827
int ret = mbedtls_x509_crt_parse_der(&mbed_ctx->ca_chain, src->raw.p,
1878618828
src->raw.len);
18787-
if (ret != 0) { return false; }
18829+
if (ret != 0) {
18830+
free_ca_store(store);
18831+
return false;
18832+
}
1878818833
src = src->next;
1878918834
}
1879018835

18836+
// This function takes ownership of the store; the chain was deep-copied
18837+
// above, so release the source
18838+
free_ca_store(store);
18839+
1879118840
// Update the SSL config to use the new CA chain
1879218841
mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->ca_chain, nullptr);
1879318842
return true;
@@ -19931,6 +19980,9 @@ inline bool set_ca_store(ctx_t ctx, ca_store_t store) {
1993119980
wctx->ctx, reinterpret_cast<const unsigned char *>(ca->pem_data.data()),
1993219981
static_cast<long>(ca->pem_data.size()), SSL_FILETYPE_PEM);
1993319982
if (ret == SSL_SUCCESS) { wctx->ca_pem_data_ += ca->pem_data; }
19983+
// This function takes ownership of the store; the PEM data was copied into
19984+
// the context, so release the source
19985+
free_ca_store(store);
1993419986
return ret == SSL_SUCCESS;
1993519987
}
1993619988

@@ -20293,6 +20345,12 @@ inline WebSocketClient::WebSocketClient(
2029320345

2029420346
#ifdef CPPHTTPLIB_SSL_ENABLED
2029520347
is_ssl_ = is_ssl;
20348+
if (is_ssl_) {
20349+
// The context lives as long as the client so that CA configuration
20350+
// survives reconnects; sessions are created per connection.
20351+
tls_ctx_ = tls::create_client_context();
20352+
if (!tls_ctx_) { return; }
20353+
}
2029620354
#else
2029720355
if (is_ssl) { return; }
2029820356
#endif
@@ -20301,7 +20359,15 @@ inline WebSocketClient::WebSocketClient(
2030120359
}
2030220360
}
2030320361

20304-
inline WebSocketClient::~WebSocketClient() { shutdown_and_close(); }
20362+
inline WebSocketClient::~WebSocketClient() {
20363+
shutdown_and_close();
20364+
#ifdef CPPHTTPLIB_SSL_ENABLED
20365+
if (tls_ctx_) {
20366+
tls::free_context(tls_ctx_);
20367+
tls_ctx_ = nullptr;
20368+
}
20369+
#endif
20370+
}
2030520371

2030620372
inline bool WebSocketClient::is_valid() const { return is_valid_; }
2030720373

@@ -20313,10 +20379,6 @@ inline void WebSocketClient::shutdown_and_close() {
2031320379
tls::free_session(tls_session_);
2031420380
tls_session_ = nullptr;
2031520381
}
20316-
if (tls_ctx_) {
20317-
tls::free_context(tls_ctx_);
20318-
tls_ctx_ = nullptr;
20319-
}
2032020382
}
2032120383
#endif
2032220384
if (ws_ && ws_->is_open()) { ws_->close(); }
@@ -20331,10 +20393,18 @@ inline void WebSocketClient::shutdown_and_close() {
2033120393
inline bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm) {
2033220394
#ifdef CPPHTTPLIB_SSL_ENABLED
2033320395
if (is_ssl_) {
20334-
if (!detail::setup_client_tls_session(
20335-
host_, tls_ctx_, tls_session_, sock_,
20336-
server_certificate_verification_, ca_cert_file_path_,
20337-
ca_cert_store_, read_timeout_sec_, read_timeout_usec_)) {
20396+
if (server_certificate_verification_ && !certs_loaded_) {
20397+
uint64_t backend_error = 0;
20398+
detail::load_client_ca_config(tls_ctx_, ca_cert_file_path_, std::string(),
20399+
custom_ca_loaded_, system_ca_mode_,
20400+
backend_error);
20401+
certs_loaded_ = true;
20402+
}
20403+
20404+
if (!detail::setup_client_tls_session(host_, tls_ctx_, tls_session_, sock_,
20405+
server_certificate_verification_,
20406+
read_timeout_sec_,
20407+
read_timeout_usec_)) {
2033820408
return false;
2033920409
}
2034020410

@@ -20468,14 +20538,32 @@ inline void WebSocketClient::set_ca_cert_path(const std::string &path) {
2046820538
}
2046920539

2047020540
inline void WebSocketClient::set_ca_cert_store(tls::ca_store_t store) {
20471-
ca_cert_store_ = store;
20541+
if (store && tls_ctx_) {
20542+
// set_ca_store takes ownership of store
20543+
tls::set_ca_store(tls_ctx_, store);
20544+
custom_ca_loaded_ = true;
20545+
} else if (store) {
20546+
tls::free_ca_store(store);
20547+
}
20548+
}
20549+
20550+
inline void WebSocketClient::load_ca_cert_store(const char *ca_cert,
20551+
std::size_t size) {
20552+
if (tls_ctx_ && ca_cert && size > 0) {
20553+
tls::load_ca_pem(tls_ctx_, ca_cert, size);
20554+
custom_ca_loaded_ = true;
20555+
}
2047220556
}
2047320557

2047420558
inline void
2047520559
WebSocketClient::enable_server_certificate_verification(bool enabled) {
2047620560
server_certificate_verification_ = enabled;
2047720561
}
2047820562

20563+
inline void WebSocketClient::enable_system_ca(bool enabled) {
20564+
system_ca_mode_ = enabled ? SystemCAMode::Enabled : SystemCAMode::Disabled;
20565+
}
20566+
2047920567
#endif // CPPHTTPLIB_SSL_ENABLED
2048020568

2048120569
} // namespace ws

0 commit comments

Comments
 (0)