Browse Source

Keep the system CAs out when a custom CA comes through empty

Under the default system CA policy, a custom CA given to a client keeps
the system CAs out. SSLClient and WebSocketClient only noted the custom
CA when set_ca_cert_store() got a store or load_ca_cert_store() got some
bytes, so a NULL store, such as the one tls::create_ca_store() returns
for a PEM it cannot parse, or an empty PEM left the client trusting
every system CA instead. Note the custom CA on every call, so that such
a client fails verification. A redirect still passes a CA on only when
the original client had one.
yhirose 22 hours ago
parent
commit
6304fc6abb
2 changed files with 49 additions and 9 deletions
  1. 17 9
      httplib.h
  2. 32 0
      test/test.cc

+ 17 - 9
httplib.h

@@ -3527,9 +3527,13 @@ private:
 
   // Tracks whether a custom CA store was applied via set_ca_cert_store(),
   // since the store handle itself is owned by ctx_ and leaves no other trace.
-  // Used to keep custom CA configuration exclusive with system CA loading.
   bool ca_cert_store_set_ = false;
 
+  // Set by set_ca_cert_store() and load_ca_cert_store() even when they bring
+  // no certificate, so that a CA that failed to come through fails
+  // verification instead of letting the system CAs in.
+  bool custom_ca_loaded_ = false;
+
   std::function<SSLVerifierResponse(tls::session_t)> session_verifier_;
 
   // Like ca_cert_store_set_, keeps what ctx_ cannot report back: the verifier
@@ -4793,6 +4797,9 @@ private:
   tls::session_t tls_session_ = nullptr;
   std::string ca_cert_file_path_;
   std::string ca_cert_dir_path_;
+  // Set by set_ca_cert_store() and load_ca_cert_store() even when they bring
+  // no certificate, so that a CA that failed to come through fails
+  // verification instead of letting the system CAs in.
   bool custom_ca_loaded_ = false;
   bool certs_loaded_ = false;
   SystemCAMode system_ca_mode_ = SystemCAMode::Auto;
@@ -16357,7 +16364,7 @@ inline bool ClientImpl::create_redirect_client(
         }
         redirect_client.load_ca_cert_store(pem.data(), pem.size());
         redirect_client.ca_cert_store_set_ = true;
-      } else {
+      } else if (self.custom_ca_loaded_) {
         redirect_client.load_ca_cert_store(ca_cert_pem_.c_str(),
                                            ca_cert_pem_.size());
       }
@@ -19202,6 +19209,7 @@ inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
   } else if (ca_cert_store) {
     tls::free_ca_store(ca_cert_store);
   }
+  custom_ca_loaded_ = true;
 }
 
 inline void
@@ -19234,6 +19242,7 @@ inline void SSLClient::load_ca_cert_store(const char *ca_cert,
     ca_cert_pem_.append(ca_cert, size);
     tls::load_ca_pem(ctx_, ca_cert, size);
   }
+  custom_ca_loaded_ = true;
 }
 
 inline bool SSLClient::load_certs() {
@@ -19244,10 +19253,9 @@ inline bool SSLClient::load_certs() {
   std::call_once(initialize_cert_, [&]() {
     std::lock_guard<std::mutex> guard(ctx_mutex_);
 
-    ret = detail::load_client_ca_config(
-        ctx_, ca_cert_file_path_, ca_cert_dir_path_,
-        !ca_cert_pem_.empty() || ca_cert_store_set_, system_ca_mode_,
-        last_backend_error_);
+    ret = detail::load_client_ca_config(ctx_, ca_cert_file_path_,
+                                        ca_cert_dir_path_, custom_ca_loaded_,
+                                        system_ca_mode_, last_backend_error_);
   });
 
   return ret;
@@ -19273,7 +19281,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
   options.windows_cert_verification =
       enable_windows_cert_verification_ &&
       system_ca_mode_ != SystemCAMode::Disabled && ca_cert_file_path_.empty() &&
-      ca_cert_dir_path_.empty() && ca_cert_pem_.empty() && !ca_cert_store_set_;
+      ca_cert_dir_path_.empty() && !custom_ca_loaded_;
   // Only a verifier set through set_server_certificate_verifier() is seen
   // here, not one installed with tls::set_verify_callback() directly.
   options.server_certificate_verifier_set =
@@ -23960,18 +23968,18 @@ inline void WebSocketClient::set_ca_cert_store(tls::ca_store_t store) {
   if (store && tls_ctx_) {
     // set_ca_store takes ownership of store
     tls::set_ca_store(tls_ctx_, store);
-    custom_ca_loaded_ = true;
   } else if (store) {
     tls::free_ca_store(store);
   }
+  custom_ca_loaded_ = true;
 }
 
 inline void WebSocketClient::load_ca_cert_store(const char *ca_cert,
                                                 std::size_t size) {
   if (tls_ctx_ && ca_cert && size > 0) {
     tls::load_ca_pem(tls_ctx_, ca_cert, size);
-    custom_ca_loaded_ = true;
   }
+  custom_ca_loaded_ = true;
 }
 
 inline void

+ 32 - 0
test/test.cc

@@ -70,6 +70,7 @@ namespace tls {
 // live below the split border; the definitions are linked from httplib.cc.
 ca_store_t create_ca_store(const char *pem, size_t len);
 bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
+std::vector<std::string> get_ca_names(ctx_t ctx);
 } // namespace tls
 } // namespace httplib
 #endif
@@ -18157,6 +18158,37 @@ TEST(SSLClientTest, IpHostUntrustedChainFails) {
   ASSERT_FALSE(res);
 }
 
+#ifndef CPPHTTPLIB_WOLFSSL_SUPPORT
+// A custom CA that brings no certificate still keeps the system CAs out, so a
+// CA that failed to come through fails verification instead of trusting them.
+// (wolfSSL does not list the system CAs it loaded.)
+TEST(SSLClientTest, EmptyCustomCaKeepsSystemCasOut) {
+  SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
+  ASSERT_TRUE(svr.is_valid());
+  auto port = svr.bind_to_any_port("127.0.0.1");
+  ASSERT_LT(0, port);
+  thread t = thread([&] { svr.listen_after_bind(); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    t.join();
+    ASSERT_FALSE(svr.is_running());
+  });
+  svr.wait_until_ready();
+
+  // The CAs a client trusts once its first request has loaded them
+  auto loaded_cas = [&](const std::function<void(Client &)> &setup) {
+    Client cli("https://127.0.0.1:" + std::to_string(port));
+    setup(cli);
+    cli.Get("/");
+    return tls::get_ca_names(cli.tls_context()).size();
+  };
+
+  EXPECT_EQ(0u, loaded_cas([](Client &cli) { cli.load_ca_cert_store("", 0); }));
+  EXPECT_EQ(0u,
+            loaded_cas([](Client &cli) { cli.set_ca_cert_store(nullptr); }));
+}
+#endif
+
 // enable_system_ca(false) prevents system CA loading entirely
 TEST(SSLClientTest, DisableSystemCa_Online) {
   SSLClient cli("google.com");