Przeglądaj źródła

Pass the server's intermediates to Windows certificate verification

verify_cert_with_windows_schannel() built the chain from the leaf alone,
so CryptoAPI fetched an issuer from the leaf's AIA URL instead of using
the intermediates the server sent. When that issuer chains to a root
Windows does not trust, verification failed with CERT_TRUST_IS_UNTRUSTED_ROOT
even though the chain the server sent ends at a trusted root.
accounts.spotify.com is such a site: its leaf's AIA leads to Certainly
Root R1, while the server sends an intermediate cross-signed by Starfield
Root G2.

Add tls::get_peer_cert_chain_der() for all backends and hand the whole
chain to CertGetCertificateChain() through an in-memory store, falling
back to the leaf alone when the chain is unavailable.

Refs #2596
yhirose 6 dni temu
rodzic
commit
413c7d42f6
2 zmienionych plików z 121 dodań i 14 usunięć
  1. 108 14
      httplib.h
  2. 13 0
      test/test.cc

+ 108 - 14
httplib.h

@@ -5132,6 +5132,9 @@ 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);
+// The certificates the peer sent, leaf first, in DER form
+bool get_peer_cert_chain_der(const_session_t session,
+                             std::vector<std::vector<unsigned char>> &chain);
 const char *get_sni(const_session_t session);
 
 // CA store management
@@ -10698,20 +10701,31 @@ inline bool match_hostname(const std::string &pattern,
 // 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, uint64_t &out_error) {
-  if (der_cert.empty()) { return false; }
+// `chain` holds the certificates the server sent, leaf first.
+inline bool verify_cert_with_windows_schannel(
+    const std::vector<std::vector<unsigned char>> &chain,
+    const std::string &hostname, bool verify_hostname, uint64_t &out_error) {
+  if (chain.empty() || chain[0].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()));
+  // Give CryptoAPI the intermediates the server sent, not just the leaf.
+  // Without them it fetches an issuer from the leaf's AIA URL instead, and
+  // that one may chain to a root Windows does not trust even when the chain
+  // the server sent ends at a trusted root.
+  auto store = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, 0, nullptr);
+  if (!store) {
+    out_error = GetLastError();
+    return false;
+  }
+
+  auto store_guard = scope_exit([&] { CertCloseStore(store, 0); });
 
-  if (!cert_context) {
+  PCCERT_CONTEXT cert_context = nullptr;
+  if (!CertAddEncodedCertificateToStore(
+          store, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, chain[0].data(),
+          static_cast<DWORD>(chain[0].size()), CERT_STORE_ADD_ALWAYS,
+          &cert_context)) {
     out_error = GetLastError();
     return false;
   }
@@ -10719,6 +10733,14 @@ verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert,
   auto cert_guard =
       scope_exit([&] { CertFreeCertificateContext(cert_context); });
 
+  // An intermediate CryptoAPI cannot parse is just one less hint for chain
+  // building, so a failure here is not an error by itself.
+  for (size_t i = 1; i < chain.size(); i++) {
+    CertAddEncodedCertificateToStore(
+        store, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, chain[i].data(),
+        static_cast<DWORD>(chain[i].size()), CERT_STORE_ADD_ALWAYS, nullptr);
+  }
+
   // Setup chain parameters
   CERT_CHAIN_PARA chain_para = {};
   chain_para.cbSize = sizeof(chain_para);
@@ -10726,7 +10748,7 @@ verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert,
   // 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,
+      nullptr, cert_context, nullptr, store, &chain_para,
       CERT_CHAIN_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_END_CERT |
           CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT,
       nullptr, &chain_context);
@@ -10945,11 +10967,17 @@ inline bool setup_client_tls_session(
     // This provides real-time certificate validation with Windows Update
     // integration, working with both OpenSSL and MbedTLS backends.
     if (options.windows_cert_verification) {
-      std::vector<unsigned char> der;
-      if (get_cert_der(server_cert, der)) {
+      std::vector<std::vector<unsigned char>> chain;
+      if (!get_peer_cert_chain_der(session, chain)) {
+        // Fall back to the leaf alone and let CryptoAPI find the issuers
+        chain.clear();
+        std::vector<unsigned char> der;
+        if (get_cert_der(server_cert, der)) { chain.push_back(std::move(der)); }
+      }
+      if (!chain.empty()) {
         uint64_t wincrypt_error = 0;
         if (!verify_cert_with_windows_schannel(
-                der, host, options.server_hostname_verification,
+                chain, host, options.server_hostname_verification,
                 wincrypt_error)) {
           return fail(Error::SSLServerVerification, 0, wincrypt_error);
         }
@@ -19708,6 +19736,34 @@ inline bool get_cert_der(cert_t cert, std::vector<unsigned char> &der) {
   return true;
 }
 
+inline bool
+get_peer_cert_chain_der(const_session_t session,
+                        std::vector<std::vector<unsigned char>> &chain) {
+  chain.clear();
+  if (!session) return false;
+  auto ssl = static_cast<SSL *>(const_cast<void *>(session));
+
+  // On the server side, SSL_get_peer_cert_chain() leaves out the peer's own
+  // certificate.
+  if (SSL_is_server(ssl)) {
+    auto leaf = get_peer_cert(session);
+    if (!leaf) return false;
+    std::vector<unsigned char> der;
+    auto ok = get_cert_der(leaf, der);
+    free_cert(leaf);
+    if (!ok) return false;
+    chain.push_back(std::move(der));
+  }
+
+  auto sk = SSL_get_peer_cert_chain(ssl);
+  for (int i = 0; sk && i < sk_X509_num(sk); i++) {
+    std::vector<unsigned char> der;
+    if (!get_cert_der(sk_X509_value(sk, i), der)) return false;
+    chain.push_back(std::move(der));
+  }
+  return !chain.empty();
+}
+
 inline const char *get_sni(const_session_t session) {
   if (!session) return nullptr;
   auto ssl = static_cast<SSL *>(const_cast<void *>(session));
@@ -21099,6 +21155,20 @@ inline bool get_cert_der(cert_t cert, std::vector<unsigned char> &der) {
   return true;
 }
 
+inline bool
+get_peer_cert_chain_der(const_session_t session,
+                        std::vector<std::vector<unsigned char>> &chain) {
+  chain.clear();
+  // The peer certificate is the head of the list Mbed TLS parsed the whole
+  // received chain into.
+  for (auto crt = static_cast<const mbedtls_x509_crt *>(get_peer_cert(session));
+       crt; crt = crt->next) {
+    if (!crt->raw.p || crt->raw.len == 0) { break; }
+    chain.emplace_back(crt->raw.p, crt->raw.p + crt->raw.len);
+  }
+  return !chain.empty();
+}
+
 inline const char *get_sni(const_session_t session) {
   if (!session) return nullptr;
   auto msession = static_cast<const impl::MbedTlsSession *>(session);
@@ -22278,6 +22348,30 @@ inline bool get_cert_der(cert_t cert, std::vector<unsigned char> &der) {
   return true;
 }
 
+inline bool
+get_peer_cert_chain_der(const_session_t session,
+                        std::vector<std::vector<unsigned char>> &chain) {
+  chain.clear();
+  if (!session) return false;
+#ifdef SESSION_CERTS
+  auto wsession =
+      static_cast<impl::WolfSSLSession *>(const_cast<void *>(session));
+  auto peer_chain = wolfSSL_get_peer_chain(wsession->ssl);
+  if (!peer_chain) return false;
+  auto count = wolfSSL_get_chain_count(peer_chain);
+  for (int i = 0; i < count; i++) {
+    auto data = wolfSSL_get_chain_cert(peer_chain, i);
+    auto len = wolfSSL_get_chain_length(peer_chain, i);
+    if (!data || len <= 0) return false;
+    chain.emplace_back(data, data + len);
+  }
+  return !chain.empty();
+#else
+  // wolfSSL keeps the received chain only when built with SESSION_CERTS
+  return false;
+#endif
+}
+
 inline const char *get_sni(const_session_t session) {
   if (!session) return nullptr;
   auto wsession = static_cast<const impl::WolfSSLSession *>(session);

+ 13 - 0
test/test.cc

@@ -13958,6 +13958,19 @@ TEST(SSLClientTest, WindowsCertificateVerification_Disabled) {
   auto res = cli.Get("/");
   if (res) { EXPECT_NE(StatusCode::InternalServerError_500, res->status); }
 }
+
+// accounts.spotify.com sends an intermediate cross-signed by Starfield Root
+// G2, while its leaf's AIA URL leads to a version issued by Certainly Root R1,
+// which Windows does not trust. CryptoAPI only builds the trusted chain when
+// it is given the intermediates the server sent.
+TEST(SSLClientTest, WindowsCertificateVerification_ServerIntermediates_Online) {
+  SSLClient cli("accounts.spotify.com", 443);
+  cli.enable_server_certificate_verification(true);
+
+  auto res = cli.Get("/");
+  ASSERT_TRUE(res) << "Error: " << to_string(res.error())
+                   << " ssl_backend_error=" << res.ssl_backend_error();
+}
 #endif
 
 TEST(SSLClientTest, ServerCertificateVerification1_Online) {