Просмотр исходного кода

Fix set_ca_cert_store() breaking CA exclusivity and redirect CA transfer

Since the TLS abstraction layer was introduced, SSLClient::set_ca_cert_store()
handed the store to the TLS context without leaving any trace on the client.
As a result:

- load_certs() merged system CA certs into the user-provided store,
  silently broadening the trust set (a custom store used to suppress
  system CA loading)
- Client::load_ca_cert_store() went through the native store path,
  bypassing the PEM retention used for redirect transfer, so CA certs
  were not carried over to clients created for HTTPS redirects
- The Windows Schannel verification skip for custom CA certs did not
  trigger

Track custom store assignment with a flag checked by load_certs() and
the Schannel path, and route Client::load_ca_cert_store() through the
PEM-based SSLClient path so the CA data survives redirects.
yhirose 2 месяцев назад
Родитель
Сommit
e7e7bf7b44
2 измененных файлов с 116 добавлено и 3 удалено
  1. 13 3
      httplib.h
  2. 103 0
      test/test.cc

+ 13 - 3
httplib.h

@@ -2804,6 +2804,11 @@ private:
   std::mutex ctx_mutex_;
   std::once_flag initialize_cert_;
 
+  // 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;
+
   long verify_result_ = 0;
 
   std::function<SSLVerifierResponse(tls::session_t)> session_verifier_;
@@ -16092,6 +16097,7 @@ inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
   if (ca_cert_store && ctx_) {
     // set_ca_store takes ownership of ca_cert_store
     tls::set_ca_store(ctx_, ca_cert_store);
+    ca_cert_store_set_ = true;
   } else if (ca_cert_store) {
     tls::free_ca_store(ca_cert_store);
   }
@@ -16138,7 +16144,7 @@ inline bool SSLClient::load_certs() {
         last_backend_error_ = tls::get_error();
         ret = false;
       }
-    } else if (ca_cert_pem_.empty()) {
+    } else if (ca_cert_pem_.empty() && !ca_cert_store_set_) {
       if (!tls::load_system_certs(ctx_)) {
         last_backend_error_ = tls::get_error();
       }
@@ -16273,7 +16279,8 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
     // Skip when a custom CA cert is specified, as the Windows certificate
     // store would not know about user-provided CA certificates.
     if (enable_windows_cert_verification_ && ca_cert_file_path_.empty() &&
-        ca_cert_dir_path_.empty() && ca_cert_pem_.empty()) {
+        ca_cert_dir_path_.empty() && ca_cert_pem_.empty() &&
+        !ca_cert_store_set_) {
       std::vector<unsigned char> der;
       if (get_cert_der(server_cert, der)) {
         uint64_t wincrypt_error = 0;
@@ -16335,7 +16342,10 @@ inline void Client::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
 }
 
 inline void Client::load_ca_cert_store(const char *ca_cert, std::size_t size) {
-  set_ca_cert_store(tls::create_ca_store(ca_cert, size));
+  if (is_ssl_) {
+    // Use the PEM-based path so the CA data is retained for redirect transfer
+    static_cast<SSLClient &>(*cli_).load_ca_cert_store(ca_cert, size);
+  }
 }
 
 inline void

+ 103 - 0
test/test.cc

@@ -51,6 +51,16 @@ inline std::string u8_to_string(const char8_t *s) {
 #define SERVER_ENCRYPTED_PRIVATE_KEY_FILE "./key_encrypted.pem"
 #define SERVER_ENCRYPTED_PRIVATE_KEY_PASS "test123!"
 
+#ifdef CPPHTTPLIB_SSL_ENABLED
+namespace httplib {
+namespace tls {
+// Declared here for the split build, where the TLS abstraction declarations
+// live below the split border; the definition is linked from httplib.cc.
+ca_store_t create_ca_store(const char *pem, size_t len);
+} // namespace tls
+} // namespace httplib
+#endif
+
 using namespace std;
 using namespace httplib;
 
@@ -11625,6 +11635,99 @@ TEST(SSLClientTest, SetCaCertStoreSkipsSystemCerts_Online) {
   EXPECT_EQ(Error::SSLServerVerification, res.error());
 }
 
+// Same as above, but through the native store handle path. Regression test:
+// set_ca_cert_store() used to leave no trace on the client, so load_certs()
+// merged system certs into the user-provided store.
+TEST(SSLClientTest, SetCaCertStoreNativeSkipsSystemCerts_Online) {
+  std::string cert;
+  read_file(SERVER_CERT2_FILE, cert);
+
+  SSLClient cli("google.com");
+  cli.set_ca_cert_store(tls::create_ca_store(cert.c_str(), cert.size()));
+  cli.enable_server_certificate_verification(true);
+
+  auto res = cli.Get("/");
+  ASSERT_FALSE(res);
+  EXPECT_EQ(Error::SSLServerVerification, res.error());
+}
+
+// A custom store set via the native handle must still verify a server whose
+// cert it does contain.
+TEST(SSLClientTest, SetCaCertStoreNativeVerifiesLocalServer) {
+  SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
+  ASSERT_TRUE(svr.is_valid());
+  svr.Get("/test", [&](const Request &, Response &res) {
+    res.set_content("test", "text/plain");
+  });
+
+  thread t = thread([&]() { ASSERT_TRUE(svr.listen("127.0.0.1", PORT)); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    t.join();
+    ASSERT_FALSE(svr.is_running());
+  });
+
+  svr.wait_until_ready();
+
+  SSLClient cli("127.0.0.1", PORT);
+  std::string cert;
+  read_file(SERVER_CERT2_FILE, cert);
+  cli.set_ca_cert_store(tls::create_ca_store(cert.c_str(), cert.size()));
+  cli.enable_server_certificate_verification(true);
+  cli.set_connection_timeout(30);
+
+  auto res = cli.Get("/test");
+  ASSERT_TRUE(res);
+  ASSERT_EQ(StatusCode::OK_200, res->status);
+}
+
+// CA certs loaded through the universal Client must be transferred to the
+// client created internally for following an HTTPS redirect. Regression test:
+// Client::load_ca_cert_store() used to bypass the PEM-based path that stores
+// the CA data for redirect transfer.
+TEST(UniversalClientRedirectTest, LoadCaCertStore) {
+  auto ssl_port = PORT + 1;
+
+  SSLServer ssl_svr1(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
+  ASSERT_TRUE(ssl_svr1.is_valid());
+  ssl_svr1.Get("/index", [&](const Request &, Response &res) {
+    res.set_redirect("https://127.0.0.1:" + std::to_string(ssl_port) +
+                     "/index");
+  });
+
+  SSLServer ssl_svr2(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
+  ASSERT_TRUE(ssl_svr2.is_valid());
+  ssl_svr2.Get("/index", [&](const Request &, Response &res) {
+    res.set_content("test", "text/plain");
+  });
+
+  thread t = thread([&]() { ASSERT_TRUE(ssl_svr1.listen("127.0.0.1", PORT)); });
+  thread t2 =
+      thread([&]() { ASSERT_TRUE(ssl_svr2.listen("127.0.0.1", ssl_port)); });
+  auto se = detail::scope_exit([&] {
+    ssl_svr2.stop();
+    ssl_svr1.stop();
+    t2.join();
+    t.join();
+    ASSERT_FALSE(ssl_svr1.is_running());
+  });
+
+  ssl_svr1.wait_until_ready();
+  ssl_svr2.wait_until_ready();
+
+  Client cli("https://127.0.0.1:" + std::to_string(PORT));
+  std::string cert;
+  read_file(SERVER_CERT2_FILE, cert);
+  cli.load_ca_cert_store(cert.c_str(), cert.size());
+  cli.enable_server_certificate_verification(true);
+  cli.set_follow_location(true);
+  cli.set_connection_timeout(30);
+
+  auto res = cli.Get("/index");
+  ASSERT_TRUE(res);
+  ASSERT_EQ(StatusCode::OK_200, res->status);
+}
+
 TEST(MultipartFormDataTest, LargeData) {
   SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);