From 75cc18b9666aeefe1901f8571e2a7537d840a2e5 Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 2 Oct 2026 15:29:37 -0400 Subject: [PATCH] Pass the server's intermediates to Windows certificate verification CryptoAPI got only the leaf, so it fetched an issuer from the leaf's AIA URL instead of using the intermediates the server sent. For accounts.spotify.com that issuer chains to Certainly Root R1, which Windows does not trust, while the server's own chain ends at Starfield Root G2. Add tls::get_peer_certs(), which returns the certificates the peer sent in the same way get_ca_certs() returns the CA certificates, for every backend. The CryptoAPI check puts them into a memory store that it passes to CertGetCertificateChain(). wolfSSL keeps the received chain only when built with SESSION_CERTS; without it, CryptoAPI still gets the leaf alone. Refs #2596 --- httplib.h | 82 +++++++++++++++++++++++++++++++++++++++++++++++----- test/test.cc | 9 ++++++ 2 files changed, 84 insertions(+), 7 deletions(-) diff --git a/httplib.h b/httplib.h index e7d2f176..e3777c88 100644 --- a/httplib.h +++ b/httplib.h @@ -5120,6 +5120,8 @@ bool is_peer_closed(session_t session, socket_t sock); // Certificate verification cert_t get_peer_cert(const_session_t session); +// The certificates the peer sent, leaf first. Free each with free_cert(). +size_t get_peer_certs(const_session_t session, std::vector &certs); void free_cert(cert_t cert); bool verify_hostname(cert_t cert, const char *hostname); uint64_t hostname_mismatch_code(); @@ -10698,10 +10700,9 @@ 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 &der_cert, - const std::string &hostname, - bool verify_hostname, uint64_t &out_error) { +inline bool verify_cert_with_windows_schannel( + const std::vector &der_cert, const std::string &hostname, + bool verify_hostname, uint64_t &out_error, tls::const_session_t session) { if (der_cert.empty()) { return false; } out_error = 0; @@ -10719,6 +10720,25 @@ verify_cert_with_windows_schannel(const std::vector &der_cert, auto cert_guard = scope_exit([&] { CertFreeCertificateContext(cert_context); }); + // Give CryptoAPI the certificates the server sent. Without them it follows + // the leaf's AIA URL, which may lead to an issuer under an untrusted root. + std::vector peer_certs; + tls::get_peer_certs(session, peer_certs); + auto store = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, 0, nullptr); + auto store_guard = scope_exit([&] { + for (auto cert : peer_certs) { + tls::free_cert(cert); + } + if (store) { CertCloseStore(store, 0); } + }); + for (auto cert : peer_certs) { + std::vector der; + tls::get_cert_der(cert, der); + CertAddEncodedCertificateToStore(store, X509_ASN_ENCODING, der.data(), + static_cast(der.size()), + CERT_STORE_ADD_USE_EXISTING, nullptr); + } + // Setup chain parameters CERT_CHAIN_PARA chain_para = {}; chain_para.cbSize = sizeof(chain_para); @@ -10726,7 +10746,7 @@ verify_cert_with_windows_schannel(const std::vector &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); @@ -10949,8 +10969,8 @@ inline bool setup_client_tls_session( if (get_cert_der(server_cert, der)) { uint64_t wincrypt_error = 0; if (!verify_cert_with_windows_schannel( - der, host, options.server_hostname_verification, - wincrypt_error)) { + der, host, options.server_hostname_verification, wincrypt_error, + session)) { return fail(Error::SSLServerVerification, 0, wincrypt_error); } } @@ -19523,6 +19543,24 @@ inline cert_t get_peer_cert(const_session_t session) { static_cast(const_cast(session)))); } +inline size_t get_peer_certs(const_session_t session, + std::vector &certs) { + certs.clear(); + if (!session) { return 0; } + auto ssl = static_cast(session); + // On the server side, the chain leaves out the peer's own certificate + if (SSL_is_server(ssl)) { + if (auto leaf = get_peer_cert(session)) { certs.push_back(leaf); } + } + auto sk = SSL_get_peer_cert_chain(ssl); + for (int i = 0; sk && i < sk_X509_num(sk); i++) { + auto x509 = sk_X509_value(sk, i); + X509_up_ref(x509); + certs.push_back(static_cast(x509)); + } + return certs.size(); +} + inline void free_cert(cert_t cert) { if (cert) { X509_free(static_cast(cert)); } } @@ -20867,6 +20905,18 @@ inline cert_t get_peer_cert(const_session_t session) { return const_cast(cert); } +inline size_t get_peer_certs(const_session_t session, + std::vector &certs) { + certs.clear(); + // Mbed TLS parses the whole received chain into a list headed by the peer + // certificate, owned by the session like get_peer_cert()'s result + for (auto crt = static_cast(get_peer_cert(session)); crt; + crt = crt->next) { + certs.push_back(static_cast(crt)); + } + return certs.size(); +} + inline void free_cert(cert_t cert) { // Mbed TLS: peer certificate is owned by the SSL context. // No-op here, but callers should still call this for cross-backend @@ -22024,6 +22074,24 @@ inline cert_t get_peer_cert(const_session_t session) { return static_cast(cert); } +inline size_t get_peer_certs(const_session_t session, + std::vector &certs) { + certs.clear(); + if (!session) { return 0; } + // wolfSSL keeps the received chain only when built with SESSION_CERTS +#ifdef SESSION_CERTS + auto wsession = + static_cast(const_cast(session)); + auto chain = wolfSSL_get_peer_chain(wsession->ssl); + auto count = chain ? wolfSSL_get_chain_count(chain) : 0; + for (int i = 0; i < count; i++) { + auto x509 = wolfSSL_get_chain_X509(chain, i); + if (x509) { certs.push_back(static_cast(x509)); } + } +#endif + return certs.size(); +} + inline void free_cert(cert_t cert) { if (cert) { wolfSSL_X509_free(static_cast(cert)); } } diff --git a/test/test.cc b/test/test.cc index f7383094..9679e988 100644 --- a/test/test.cc +++ b/test/test.cc @@ -13958,6 +13958,15 @@ TEST(SSLClientTest, WindowsCertificateVerification_Disabled) { auto res = cli.Get("/"); if (res) { EXPECT_NE(StatusCode::InternalServerError_500, res->status); } } + +// The server sends an intermediate cross-signed by a root Windows trusts, +// while the leaf's AIA URL leads to one under a root Windows does not trust. +TEST(SSLClientTest, WindowsCertificateVerification_ServerIntermediates_Online) { + SSLClient cli("accounts.spotify.com", 443); + auto res = cli.Get("/"); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()) + << " ssl_backend_error=" << res.ssl_backend_error(); +} #endif TEST(SSLClientTest, ServerCertificateVerification1_Online) {