diff --git a/httplib.h b/httplib.h index e7d2f176a1..eccfb2bcb1 100644 --- a/httplib.h +++ b/httplib.h @@ -10698,10 +10698,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,14 +10718,35 @@ verify_cert_with_windows_schannel(const std::vector &der_cert, auto cert_guard = scope_exit([&] { CertFreeCertificateContext(cert_context); }); - // Setup chain parameters +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // Add the intermediates the server sent. Without them CryptoAPI follows the + // leaf's AIA URL, which may lead to an issuer under an untrusted root. + auto store = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, 0, nullptr); + auto store_guard = scope_exit([&] { CertCloseStore(store, 0); }); + auto sk = SSL_get_peer_cert_chain(static_cast(session)); + for (int i = 1; sk && i < sk_X509_num(sk); i++) { + std::vector der; + tls::get_cert_der(sk_X509_value(sk, i), der); + CertAddEncodedCertificateToStore(store, X509_ASN_ENCODING, der.data(), + static_cast(der.size()), + CERT_STORE_ADD_USE_EXISTING, nullptr); + } +#else + (void)session; + auto store = cert_context->hCertStore; +#endif + + // Setup chain parameters. The SSL policy does not check the key usage. + LPSTR server_auth = const_cast(szOID_PKIX_KP_SERVER_AUTH); CERT_CHAIN_PARA chain_para = {}; chain_para.cbSize = sizeof(chain_para); + chain_para.RequestedUsage.Usage.cUsageIdentifier = 1; + chain_para.RequestedUsage.Usage.rgpszUsageIdentifier = &server_auth; // 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); @@ -10919,6 +10939,15 @@ inline bool setup_client_tls_session( if (verification_status == SSLVerifierResponse::NoDecisionMade && server_certificate_verification) { auto verify_result = get_verify_result(session); +#if defined(CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) && \ + defined(CPPHTTPLIB_OPENSSL_SUPPORT) + // Windows adds a root it trusts to its store only when CryptoAPI needs it, + // so leave a root missing from the store to the CryptoAPI check below + if (options.windows_cert_verification && + verify_result == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY) { + verify_result = X509_V_OK; + } +#endif if (verify_result != 0) { return fail(Error::SSLServerVerification, 0, static_cast(verify_result)); @@ -10949,10 +10978,12 @@ 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); } + } else { + return fail(Error::SSLServerVerification, 0, get_error()); } } #endif diff --git a/test/test.cc b/test/test.cc index f7383094a0..5fa11ca16e 100644 --- a/test/test.cc +++ b/test/test.cc @@ -13958,6 +13958,43 @@ 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(); +} + +// A root missing from the trust store is left to CryptoAPI, which must still +// reject a chain whose root Windows does not trust either. +TEST(SSLClientTest, WindowsCertificateVerification_UnknownIssuerRejected) { + // Issued by the test root CA, which is not in the Windows root store + SSLServer svr(CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); + svr.Get("/", [](const Request &, Response &res) { + res.set_content("ok", "text/plain"); + }); + + thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + SSLClient cli(HOST, PORT); + // Keep the hostname check from failing first + cli.enable_server_hostname_verification(false); + + auto res = cli.Get("/"); + ASSERT_FALSE(res); + EXPECT_EQ(Error::SSLServerVerification, res.error()); +} #endif TEST(SSLClientTest, ServerCertificateVerification1_Online) {