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) {