Skip to content
Closed
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
47 changes: 39 additions & 8 deletions httplib.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<unsigned char> &der_cert,
const std::string &hostname,
bool verify_hostname, uint64_t &out_error) {
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) {
if (der_cert.empty()) { return false; }

out_error = 0;
Expand All @@ -10719,14 +10718,35 @@ verify_cert_with_windows_schannel(const std::vector<unsigned char> &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<const SSL *>(session));
for (int i = 1; sk && i < sk_X509_num(sk); i++) {
std::vector<unsigned char> der;
tls::get_cert_der(sk_X509_value(sk, i), der);
CertAddEncodedCertificateToStore(store, X509_ASN_ENCODING, der.data(),
static_cast<DWORD>(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<LPSTR>(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);
Expand Down Expand Up @@ -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<uint64_t>(verify_result));
Expand Down Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions test/test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading