Jelajahi Sumber

Fix #2116 (#2346)

* Fix #2116

* Fix problem
yhirose 1 bulan lalu
induk
melakukan
8b4146324f
3 mengubah file dengan 247 tambahan dan 25 penghapusan
  1. 21 0
      README.md
  2. 194 23
      httplib.h
  3. 32 2
      test/test.cc

+ 21 - 0
README.md

@@ -92,6 +92,27 @@ cli.enable_server_certificate_verification(false);
 cli.enable_server_hostname_verification(false);
 ```
 
+### Windows Certificate Verification
+
+On Windows, cpp-httplib automatically performs additional certificate verification using the Windows certificate store via CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`). This works with both OpenSSL and Mbed TLS backends, providing:
+
+- Real-time certificate validation integrated with Windows Update
+- Certificate revocation checking
+- SSL/TLS policy verification using the system certificate store (ROOT and CA)
+
+This feature is enabled by default and can be controlled at runtime:
+
+```c++
+// Disable Windows certificate verification (use only OpenSSL/Mbed TLS verification)
+cli.enable_windows_certificate_verification(false);
+```
+
+To disable this feature at compile time, define:
+
+```c++
+#define CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
+```
+
 > [!NOTE]
 > When using SSL, it seems impossible to avoid SIGPIPE in all cases, since on some operating systems, SIGPIPE can only be suppressed on a per-message basis, but there is no way to make the OpenSSL library do so for its internal communications. If your program needs to avoid being terminated on SIGPIPE, the only fully general way might be to set up a signal handler for SIGPIPE to handle or ignore it yourself.
 

+ 194 - 23
httplib.h

@@ -2267,6 +2267,11 @@ public:
 
   tls::ctx_t tls_context() const;
 
+#if defined(_WIN32) &&                                                         \
+    !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
+  void enable_windows_certificate_verification(bool enabled);
+#endif
+
 private:
   bool is_ssl_ = false;
 #endif
@@ -2384,6 +2389,11 @@ public:
 
   tls::ctx_t tls_context() const { return ctx_; }
 
+#if defined(_WIN32) &&                                                         \
+    !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
+  void enable_windows_certificate_verification(bool enabled);
+#endif
+
 private:
   bool create_and_connect_socket(Socket &socket, Error &error) override;
   bool ensure_socket_connection(Socket &socket, Error &error) override;
@@ -2412,6 +2422,11 @@ private:
 
   std::function<SSLVerifierResponse(tls::session_t)> session_verifier_;
 
+#if defined(_WIN32) &&                                                         \
+    !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
+  bool enable_windows_cert_verification_ = true;
+#endif
+
   friend class ClientImpl;
 
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
@@ -3680,6 +3695,7 @@ std::string get_cert_issuer_name(cert_t cert);
 bool get_cert_sans(cert_t cert, std::vector<SanEntry> &sans);
 bool get_cert_validity(cert_t cert, time_t &not_before, time_t &not_after);
 std::string get_cert_serial(cert_t cert);
+bool get_cert_der(cert_t cert, std::vector<unsigned char> &der);
 const char *get_sni(const_session_t session);
 
 // CA store management
@@ -7736,6 +7752,96 @@ inline bool match_hostname(const std::string &pattern,
   return true;
 }
 
+#ifdef _WIN32
+// Verify certificate using Windows CertGetCertificateChain API.
+// This provides real-time certificate validation with Windows Update
+// integration, independent of the TLS backend (OpenSSL or MbedTLS).
+inline bool verify_cert_with_windows_schannel(
+    const std::vector<unsigned char> &der_cert, const std::string &hostname,
+    bool verify_hostname, unsigned long &out_error) {
+  if (der_cert.empty()) { return false; }
+
+  out_error = 0;
+
+  // Create Windows certificate context from DER data
+  auto cert_context = CertCreateCertificateContext(
+      X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, der_cert.data(),
+      static_cast<DWORD>(der_cert.size()));
+
+  if (!cert_context) {
+    out_error = GetLastError();
+    return false;
+  }
+
+  auto cert_guard =
+      scope_exit([&] { CertFreeCertificateContext(cert_context); });
+
+  // Setup chain parameters
+  CERT_CHAIN_PARA chain_para = {};
+  chain_para.cbSize = sizeof(chain_para);
+
+  // Build certificate chain with revocation checking
+  PCCERT_CHAIN_CONTEXT chain_context = nullptr;
+  auto chain_result = CertGetCertificateChain(
+      nullptr, cert_context, nullptr, cert_context->hCertStore, &chain_para,
+      CERT_CHAIN_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_END_CERT |
+          CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT,
+      nullptr, &chain_context);
+
+  if (!chain_result || !chain_context) {
+    out_error = GetLastError();
+    return false;
+  }
+
+  auto chain_guard =
+      scope_exit([&] { CertFreeCertificateChain(chain_context); });
+
+  // Check if chain has errors
+  if (chain_context->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) {
+    out_error = chain_context->TrustStatus.dwErrorStatus;
+    return false;
+  }
+
+  // Verify SSL policy
+  SSL_EXTRA_CERT_CHAIN_POLICY_PARA extra_policy_para = {};
+  extra_policy_para.cbSize = sizeof(extra_policy_para);
+#ifdef AUTHTYPE_SERVER
+  extra_policy_para.dwAuthType = AUTHTYPE_SERVER;
+#endif
+
+  std::wstring whost;
+  if (verify_hostname) {
+    whost = u8string_to_wstring(hostname.c_str());
+    extra_policy_para.pwszServerName = const_cast<wchar_t *>(whost.c_str());
+  }
+
+  CERT_CHAIN_POLICY_PARA policy_para = {};
+  policy_para.cbSize = sizeof(policy_para);
+#ifdef CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS
+  policy_para.dwFlags = CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS;
+#else
+  policy_para.dwFlags = 0;
+#endif
+  policy_para.pvExtraPolicyPara = &extra_policy_para;
+
+  CERT_CHAIN_POLICY_STATUS policy_status = {};
+  policy_status.cbSize = sizeof(policy_status);
+
+  if (!CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, chain_context,
+                                        &policy_para, &policy_status)) {
+    out_error = GetLastError();
+    return false;
+  }
+
+  if (policy_status.dwError != 0) {
+    out_error = policy_status.dwError;
+    return false;
+  }
+
+  return true;
+}
+#endif // _WIN32
+
 } // namespace detail
 #endif // CPPHTTPLIB_SSL_ENABLED
 
@@ -14100,6 +14206,13 @@ inline void SSLClient::set_session_verifier(
   session_verifier_ = std::move(verifier);
 }
 
+#if defined(_WIN32) &&                                                         \
+    !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
+inline void SSLClient::enable_windows_certificate_verification(bool enabled) {
+  enable_windows_cert_verification_ = enabled;
+}
+#endif
+
 inline void SSLClient::load_ca_cert_store(const char *ca_cert,
                                           std::size_t size) {
   if (ctx_ && ca_cert && size > 0) {
@@ -14251,6 +14364,29 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
         return false;
       }
     }
+
+#if defined(_WIN32) &&                                                         \
+    !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
+    // Additional Windows Schannel verification.
+    // This provides real-time certificate validation with Windows Update
+    // integration, working with both OpenSSL and MbedTLS backends.
+    // 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()) {
+      std::vector<unsigned char> der;
+      if (get_cert_der(server_cert, der)) {
+        unsigned long wincrypt_error = 0;
+        if (!detail::verify_cert_with_windows_schannel(
+                der, host_, server_hostname_verification_, wincrypt_error)) {
+          last_backend_error_ = wincrypt_error;
+          error = Error::SSLServerVerification;
+          output_error_log(error, nullptr);
+          return false;
+        }
+      }
+    }
+#endif
   }
 
   success = true;
@@ -14276,6 +14412,16 @@ inline void Client::enable_server_hostname_verification(bool enabled) {
   cli_->enable_server_hostname_verification(enabled);
 }
 
+#if defined(_WIN32) &&                                                         \
+    !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
+inline void Client::enable_windows_certificate_verification(bool enabled) {
+  if (is_ssl_) {
+    static_cast<SSLClient &>(*cli_).enable_windows_certificate_verification(
+        enabled);
+  }
+}
+#endif
+
 inline void Client::set_ca_cert_path(const std::string &ca_cert_file_path,
                                      const std::string &ca_cert_dir_path) {
   cli_->set_ca_cert_path(ca_cert_file_path, ca_cert_dir_path);
@@ -14729,25 +14875,28 @@ inline bool load_system_certs(ctx_t ctx) {
   auto ssl_ctx = static_cast<SSL_CTX *>(ctx);
 
 #ifdef _WIN32
-  // Windows: Load from system certificate store
+  // Windows: Load from system certificate store (ROOT and CA)
   auto store = SSL_CTX_get_cert_store(ssl_ctx);
   if (!store) return false;
 
-  auto hStore = CertOpenSystemStoreW(NULL, L"ROOT");
-  if (!hStore) return false;
-
   bool loaded_any = false;
-  PCCERT_CONTEXT pContext = nullptr;
-  while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) !=
-         nullptr) {
-    const unsigned char *data = pContext->pbCertEncoded;
-    auto x509 = d2i_X509(nullptr, &data, pContext->cbCertEncoded);
-    if (x509) {
-      if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; }
-      X509_free(x509);
+  static const wchar_t *store_names[] = {L"ROOT", L"CA"};
+  for (auto store_name : store_names) {
+    auto hStore = CertOpenSystemStoreW(NULL, store_name);
+    if (!hStore) continue;
+
+    PCCERT_CONTEXT pContext = nullptr;
+    while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) !=
+           nullptr) {
+      const unsigned char *data = pContext->pbCertEncoded;
+      auto x509 = d2i_X509(nullptr, &data, pContext->cbCertEncoded);
+      if (x509) {
+        if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; }
+        X509_free(x509);
+      }
     }
+    CertCloseStore(hStore, 0);
   }
-  CertCloseStore(hStore, 0);
   return loaded_any;
 
 #elif defined(__APPLE__)
@@ -15288,6 +15437,17 @@ inline std::string get_cert_serial(cert_t cert) {
   return result;
 }
 
+inline bool get_cert_der(cert_t cert, std::vector<unsigned char> &der) {
+  if (!cert) return false;
+  auto x509 = static_cast<X509 *>(cert);
+  auto len = i2d_X509(x509, nullptr);
+  if (len < 0) return false;
+  der.resize(static_cast<size_t>(len));
+  auto p = der.data();
+  i2d_X509(x509, &p);
+  return true;
+}
+
 inline const char *get_sni(const_session_t session) {
   if (!session) return nullptr;
   auto ssl = static_cast<SSL *>(const_cast<void *>(session));
@@ -16136,17 +16296,20 @@ inline bool load_system_certs(ctx_t ctx) {
   bool loaded = false;
 
 #ifdef _WIN32
-  // Load from Windows certificate store
-  HCERTSTORE hStore = CertOpenSystemStoreW(0, L"ROOT");
-  if (hStore) {
-    PCCERT_CONTEXT pContext = nullptr;
-    while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) !=
-           nullptr) {
-      int ret = mbedtls_x509_crt_parse_der(
-          &mctx->ca_chain, pContext->pbCertEncoded, pContext->cbCertEncoded);
-      if (ret == 0) { loaded = true; }
+  // Load from Windows certificate store (ROOT and CA)
+  static const wchar_t *store_names[] = {L"ROOT", L"CA"};
+  for (auto store_name : store_names) {
+    HCERTSTORE hStore = CertOpenSystemStoreW(0, store_name);
+    if (hStore) {
+      PCCERT_CONTEXT pContext = nullptr;
+      while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) !=
+             nullptr) {
+        int ret = mbedtls_x509_crt_parse_der(
+            &mctx->ca_chain, pContext->pbCertEncoded, pContext->cbCertEncoded);
+        if (ret == 0) { loaded = true; }
+      }
+      CertCloseStore(hStore, 0);
     }
-    CertCloseStore(hStore, 0);
   }
 #elif defined(__APPLE__) && defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)
   // Load from macOS Keychain
@@ -16786,6 +16949,14 @@ inline std::string get_cert_serial(cert_t cert) {
   return result;
 }
 
+inline bool get_cert_der(cert_t cert, std::vector<unsigned char> &der) {
+  if (!cert) return false;
+  auto crt = static_cast<mbedtls_x509_crt *>(cert);
+  if (!crt->raw.p || crt->raw.len == 0) return false;
+  der.assign(crt->raw.p, crt->raw.p + crt->raw.len);
+  return true;
+}
+
 inline const char *get_sni(const_session_t session) {
   if (!session) return nullptr;
   auto msession = static_cast<const impl::MbedTlsSession *>(session);

+ 32 - 2
test/test.cc

@@ -8645,13 +8645,13 @@ TEST(ClientVulnerabilityTest, UnboundedReadWithoutContentLength) {
 // Verify that set_payload_max_length(0) means "no limit" and allows reading
 // the entire response body without truncation.
 TEST(ClientVulnerabilityTest, PayloadMaxLengthZeroMeansNoLimit) {
-  constexpr size_t DATA_SIZE = 4 * 1024 * 1024; // 4MB from server
+  static constexpr size_t DATA_SIZE = 4 * 1024 * 1024; // 4MB from server
 
 #ifndef _WIN32
   signal(SIGPIPE, SIG_IGN);
 #endif
 
-  auto server_thread = std::thread([DATA_SIZE] {
+  auto server_thread = std::thread([] {
     auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
     default_socket_options(srv);
     detail::set_socket_opt_time(srv, SOL_SOCKET, SO_RCVTIMEO, 5, 0);
@@ -8956,9 +8956,14 @@ TEST(SSLClientTest, ServerCertificateVerificationError_Online) {
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
   // For OpenSSL, ssl_error is 0 for verification errors
   EXPECT_EQ(0, res.ssl_error());
+#if !defined(_WIN32) ||                                                        \
+    defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
+  // On non-Windows or when Windows Schannel is disabled, the error comes
+  // from OpenSSL's verification
   EXPECT_EQ(static_cast<unsigned long>(X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT),
             res.ssl_backend_error());
 #endif
+#endif
 }
 
 TEST(SSLClientTest, ServerHostnameVerificationError_Online) {
@@ -8982,11 +8987,36 @@ TEST(SSLClientTest, ServerHostnameVerificationError_Online) {
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
   // For OpenSSL, ssl_error is 0 for verification errors
   EXPECT_EQ(0, res.ssl_error());
+#if !defined(_WIN32) ||                                                        \
+    defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
+  // On non-Windows or when Windows Schannel is disabled, the error comes
+  // from OpenSSL's hostname verification
   EXPECT_EQ(static_cast<unsigned long>(X509_V_ERR_HOSTNAME_MISMATCH),
             res.ssl_backend_error());
 #endif
+#endif
+}
+
+#if defined(_WIN32) && defined(CPPHTTPLIB_SSL_ENABLED) &&                      \
+    !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE)
+TEST(SSLClientTest, WindowsCertificateVerification_DefaultEnabled) {
+  SSLClient cli("www.google.com", 443);
+  cli.enable_server_certificate_verification(true);
+
+  auto res = cli.Get("/");
+  if (res) { EXPECT_NE(StatusCode::InternalServerError_500, res->status); }
 }
 
+TEST(SSLClientTest, WindowsCertificateVerification_Disabled) {
+  SSLClient cli("www.google.com", 443);
+  cli.enable_server_certificate_verification(true);
+  cli.enable_windows_certificate_verification(false);
+
+  auto res = cli.Get("/");
+  if (res) { EXPECT_NE(StatusCode::InternalServerError_500, res->status); }
+}
+#endif
+
 TEST(SSLClientTest, ServerCertificateVerification1_Online) {
   Client cli("https://google.com");
   auto res = cli.Get("/");