Compare commits

...
Author SHA1 Message Date
yhirose 9af2341702 Let CryptoAPI decide on a root missing from the OpenSSL store
Windows adds a root it trusts to its store only when CryptoAPI needs it
to build a chain. The OpenSSL store is loaded from the Windows store, so
OpenSSL fails with "unable to get local issuer certificate" before the
CryptoAPI check runs, and Windows never gets to fetch the root.

When Windows certificate verification is enabled, leave that error to
the CryptoAPI check. Since OpenSSL then skips its purpose check, request
the server authentication usage from CryptoAPI, and fail instead of
skipping the CryptoAPI check when the leaf cannot be encoded.

Refs #2596
2026-10-02 09:30:54 -04:00
yhirose 086a648364 Pass the server's intermediates to Windows certificate verification
CryptoAPI got only the leaf, so it fetched an issuer from the leaf's AIA
URL instead of using the intermediates the server sent. For
accounts.spotify.com that issuer chains to Certainly Root R1, which
Windows does not trust, while the server's own chain ends at Starfield
Root G2. Add the server's intermediates to the store CryptoAPI builds
the chain from. This covers the OpenSSL backend.

Refs #2596
2026-10-02 09:25:55 -04:00
2 changed files with 76 additions and 8 deletions
+39 -8
View File
@@ -10698,10 +10698,9 @@ inline bool match_hostname(const std::string &pattern,
// Verify certificate using Windows CertGetCertificateChain API. // Verify certificate using Windows CertGetCertificateChain API.
// This provides real-time certificate validation with Windows Update // This provides real-time certificate validation with Windows Update
// integration, independent of the TLS backend (OpenSSL or MbedTLS). // integration, independent of the TLS backend (OpenSSL or MbedTLS).
inline bool inline bool verify_cert_with_windows_schannel(
verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert, const std::vector<unsigned char> &der_cert, const std::string &hostname,
const std::string &hostname, bool verify_hostname, uint64_t &out_error, tls::const_session_t session) {
bool verify_hostname, uint64_t &out_error) {
if (der_cert.empty()) { return false; } if (der_cert.empty()) { return false; }
out_error = 0; out_error = 0;
@@ -10719,14 +10718,35 @@ verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert,
auto cert_guard = auto cert_guard =
scope_exit([&] { CertFreeCertificateContext(cert_context); }); 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 = {}; CERT_CHAIN_PARA chain_para = {};
chain_para.cbSize = sizeof(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 // Build certificate chain with revocation checking
PCCERT_CHAIN_CONTEXT chain_context = nullptr; PCCERT_CHAIN_CONTEXT chain_context = nullptr;
auto chain_result = CertGetCertificateChain( 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_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_END_CERT |
CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT, CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT,
nullptr, &chain_context); nullptr, &chain_context);
@@ -10919,6 +10939,15 @@ inline bool setup_client_tls_session(
if (verification_status == SSLVerifierResponse::NoDecisionMade && if (verification_status == SSLVerifierResponse::NoDecisionMade &&
server_certificate_verification) { server_certificate_verification) {
auto verify_result = get_verify_result(session); 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) { if (verify_result != 0) {
return fail(Error::SSLServerVerification, 0, return fail(Error::SSLServerVerification, 0,
static_cast<uint64_t>(verify_result)); static_cast<uint64_t>(verify_result));
@@ -10949,10 +10978,12 @@ inline bool setup_client_tls_session(
if (get_cert_der(server_cert, der)) { if (get_cert_der(server_cert, der)) {
uint64_t wincrypt_error = 0; uint64_t wincrypt_error = 0;
if (!verify_cert_with_windows_schannel( if (!verify_cert_with_windows_schannel(
der, host, options.server_hostname_verification, der, host, options.server_hostname_verification, wincrypt_error,
wincrypt_error)) { session)) {
return fail(Error::SSLServerVerification, 0, wincrypt_error); return fail(Error::SSLServerVerification, 0, wincrypt_error);
} }
} else {
return fail(Error::SSLServerVerification, 0, get_error());
} }
} }
#endif #endif
+37
View File
@@ -13958,6 +13958,43 @@ TEST(SSLClientTest, WindowsCertificateVerification_Disabled) {
auto res = cli.Get("/"); auto res = cli.Get("/");
if (res) { EXPECT_NE(StatusCode::InternalServerError_500, res->status); } 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 #endif
TEST(SSLClientTest, ServerCertificateVerification1_Online) { TEST(SSLClientTest, ServerCertificateVerification1_Online) {