diff --git a/README.md b/README.md index c7b5e79e..8a76510b 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ cpp-httplib automatically integrates with the OS certificate store on macOS and | Platform | Behavior | Disable (compile time) | | :------- | :------- | :--------------------- | | macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` | -| Windows | Verifies certs via CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) with revocation checking | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` | +| Windows | Verifies the certificate chain with CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) instead of the TLS backend, with revocation checking. Windows fetches missing roots and intermediates on demand. With a custom CA, the TLS backend verifies the chain instead; with `set_server_certificate_verifier()`, both do. | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` | On Windows, verification can also be disabled at runtime: diff --git a/httplib.h b/httplib.h index 03e77ad8..32e7880c 100644 --- a/httplib.h +++ b/httplib.h @@ -3442,6 +3442,9 @@ private: #ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE bool enable_windows_cert_verification_ = true; + // Like ca_cert_store_set_, tracks what ctx_ cannot report back: whether + // set_server_certificate_verifier() installed a verifier. + bool server_certificate_verifier_set_ = false; #endif friend class ClientImpl; @@ -10700,7 +10703,7 @@ inline bool match_hostname(const std::string &pattern, #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). +// integration, independent of the TLS backend. 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) { @@ -10745,6 +10748,13 @@ inline bool verify_cert_with_windows_schannel( CERT_CHAIN_PARA chain_para = {}; chain_para.cbSize = sizeof(chain_para); + // Require the server authentication usage along the chain, which also + // rejects roots that Windows trusts only for other purposes. + LPSTR server_auth = const_cast(szOID_PKIX_KP_SERVER_AUTH); + chain_para.RequestedUsage.dwType = USAGE_MATCH_TYPE_AND; + 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( @@ -10856,6 +10866,9 @@ struct ClientTlsSessionOptions { // The caller decides whether Schannel has anything to say about this // connection; see SSLClient::initialize_ssl(). bool windows_cert_verification = false; + // A server certificate verifier works on the backend's chain verification, + // so the backend keeps deciding and Schannel only adds its own check. + bool server_certificate_verifier_set = false; #endif }; @@ -10890,12 +10903,24 @@ inline bool setup_client_tls_session( return fail(Error::SSLConnection, 0, 0); } + // With Windows verification on and no server certificate verifier set, + // Schannel is the only chain verifier. The backend's trust store is a + // snapshot of the Windows stores that lacks the roots Windows fetches on + // demand, so the backend's verdict is not used. + auto windows_verifies_chain = false; +#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE + windows_verifies_chain = options.windows_cert_verification && + !options.server_certificate_verifier_set; +#endif + #if defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || defined(CPPHTTPLIB_WOLFSSL_SUPPORT) // Mbed TLS and wolfSSL need the verification mode set explicitly; OpenSSL - // uses SSL_VERIFY_NONE and does all verification post-handshake. Chain - // verification happens during the handshake even for IP hosts; the - // certificate identity is verified post-handshake via verify_hostname(). - set_verify_client(ctx, server_certificate_verification); + // uses SSL_VERIFY_NONE and does all verification post-handshake. Unless + // Schannel verifies the chain instead, chain verification happens during + // the handshake even for IP hosts; the certificate identity is verified + // post-handshake via verify_hostname(). + set_verify_client(ctx, + server_certificate_verification && !windows_verifies_chain); #endif { @@ -10940,10 +10965,12 @@ inline bool setup_client_tls_session( if (verification_status == SSLVerifierResponse::NoDecisionMade && server_certificate_verification) { - auto verify_result = get_verify_result(session); - if (verify_result != 0) { - return fail(Error::SSLServerVerification, 0, - static_cast(verify_result)); + if (!windows_verifies_chain) { + auto verify_result = get_verify_result(session); + if (verify_result != 0) { + return fail(Error::SSLServerVerification, 0, + static_cast(verify_result)); + } } auto server_cert = get_peer_cert(session); @@ -10963,18 +10990,17 @@ inline bool setup_client_tls_session( } #ifdef CPPHTTPLIB_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. + // Windows Schannel verification, which lets Windows fetch missing roots + // and intermediates on demand. It must not be skipped: unless a server + // certificate verifier is set, it is the only chain check. if (options.windows_cert_verification) { std::vector der; - 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, - session)) { - return fail(Error::SSLServerVerification, 0, wincrypt_error); - } + uint64_t wincrypt_error = 0; + if (!get_cert_der(server_cert, der) || + !verify_cert_with_windows_schannel( + der, host, options.server_hostname_verification, wincrypt_error, + session)) { + return fail(Error::SSLServerVerification, 0, wincrypt_error); } } #endif @@ -18515,6 +18541,9 @@ inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) { inline void SSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) { if (!ctx_) { return; } +#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE + server_certificate_verifier_set_ = static_cast(verifier); +#endif tls::set_verify_callback(ctx_, verifier); } @@ -18576,6 +18605,9 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { enable_windows_cert_verification_ && system_ca_mode_ != SystemCAMode::Disabled && ca_cert_file_path_.empty() && ca_cert_dir_path_.empty() && ca_cert_pem_.empty() && !ca_cert_store_set_; + // Only a verifier set through set_server_certificate_verifier() is seen + // here, not one installed with tls::set_verify_callback() directly. + options.server_certificate_verifier_set = server_certificate_verifier_set_; #endif tls::session_t session = nullptr; diff --git a/test/test.cc b/test/test.cc index 9679e988..fbba9bd3 100644 --- a/test/test.cc +++ b/test/test.cc @@ -13967,6 +13967,32 @@ TEST(SSLClientTest, WindowsCertificateVerification_ServerIntermediates_Online) { ASSERT_TRUE(res) << "Error: " << to_string(res.error()) << " ssl_backend_error=" << res.ssl_backend_error(); } + +// Windows, not the backend, decides on the chain: the error carries a +// CryptoAPI trust status, which no backend error for a self-signed +// certificate has. +TEST(SSLClientTest, WindowsCertificateVerification_RejectsUntrustedRoot) { + SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); + + thread t = thread([&]() { ASSERT_TRUE(svr.listen("127.0.0.1", PORT)); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + SSLClient cli("127.0.0.1", PORT); + cli.set_connection_timeout(30); + + auto res = cli.Get("/"); + ASSERT_FALSE(res); + EXPECT_EQ(Error::SSLServerVerification, res.error()); + EXPECT_NE(0u, res.ssl_backend_error() & CERT_TRUST_IS_UNTRUSTED_ROOT) + << "ssl_backend_error=" << res.ssl_backend_error(); +} #endif TEST(SSLClientTest, ServerCertificateVerification1_Online) { @@ -14335,6 +14361,9 @@ TEST(SSLClientServerTest, ClientCertMissing) { SSLClient cli(HOST, PORT); cli.set_connection_timeout(30); + // cert.pem does not match HOST. Skip the hostname check, which runs before + // the chain check on Windows, so the result does not depend on the order. + cli.enable_server_hostname_verification(false); auto res = cli.Get("/test"); ASSERT_TRUE(!res);