Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
70 changes: 51 additions & 19 deletions httplib.h
Original file line number Diff line number Diff line change
Expand Up @@ -3442,6 +3442,9 @@ class SSLClient final : public ClientImpl {

#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;
Expand Down Expand Up @@ -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<unsigned char> &der_cert, const std::string &hostname,
bool verify_hostname, uint64_t &out_error, tls::const_session_t session) {
Expand Down Expand Up @@ -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<LPSTR>(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(
Expand Down Expand Up @@ -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
};

Expand Down Expand Up @@ -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

{
Expand Down Expand Up @@ -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<uint64_t>(verify_result));
if (!windows_verifies_chain) {
auto verify_result = get_verify_result(session);
if (verify_result != 0) {
return fail(Error::SSLServerVerification, 0,
static_cast<uint64_t>(verify_result));
}
}

auto server_cert = get_peer_cert(session);
Expand All @@ -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<unsigned char> 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
Expand Down Expand Up @@ -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<bool>(verifier);
#endif
tls::set_verify_callback(ctx_, verifier);
}

Expand Down Expand Up @@ -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;
Expand Down
29 changes: 29 additions & 0 deletions test/test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
Loading