Skip to content

Make CryptoAPI the only chain verifier when Windows verification is on - #2604

Merged
yhirose merged 2 commits into
masterfrom
windows-cryptoapi-sole-verifier
Oct 3, 2026
Merged

yhirose merged 2 commits into
masterfrom
windows-cryptoapi-sole-verifier

Conversation

@yhirose

@yhirose yhirose commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Refs #2596. Stacked on #2602, and replaces #2603.

Problem

Every TLS backend loads its trust store from the Windows ROOT and CA stores. Windows does not keep every root it trusts in those stores. It adds a root only when CryptoAPI needs it to build a chain. Starfield Root Certificate Authority - G2 is one example. On a machine that has not needed it yet, accounts.spotify.com fails like this with OpenSSL:

SSL server verification failed ssl_error=0 ssl_backend_error=20

The backend rejects the chain first, so the CryptoAPI check, which would make Windows fetch the root, never runs. CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE promises this automatic update, but it has not worked since the CryptoAPI check started running only after the backend's own verification succeeded.

Fix

When Windows certificate verification is enabled, CryptoAPI is the only chain verifier for every backend, as in Go, .NET and curl with Schannel:

  • Mbed TLS and wolfSSL no longer verify the chain during the handshake, and the post-handshake verify result is not used. The handshake signature is still checked by every backend, so CryptoAPI judges the certificate whose key signed the handshake.
  • The CryptoAPI check can no longer be skipped. If the leaf cannot be encoded for CryptoAPI, the connection fails.
  • CertGetCertificateChain() now requests szOID_PKIX_KP_SERVER_AUTH. The backends checked the extended key usage, while CERT_CHAIN_POLICY_SSL does not. This also rejects roots that Windows trusts only for other purposes.

Hostname verification is unchanged: the backend's verify_hostname() still runs before the CryptoAPI check.

When a server certificate verifier is set with set_server_certificate_verifier(), the backend still verifies the chain and CryptoAPI adds its own check, as before. The verifier works on the backend's verification result (preverify_ok). With a custom CA, Windows verification stays disabled, as before.

Behavior changes with Windows verification enabled

  • A chain under a root Windows has not fetched yet is accepted, and Windows adds the root to its store.
  • A server that sends an incomplete chain is accepted when CryptoAPI can complete it through AIA (for example incomplete-chain.badssl.com).
  • When the chain is rejected, ssl_backend_error() holds the CryptoAPI status instead of the backend's error. On Mbed TLS and wolfSSL, ssl_error() is now 0 for these failures.
  • When the chain is untrusted and the hostname also does not match, the error is now SSLServerHostnameVerification instead of SSLServerVerification.
  • On Mbed TLS and wolfSSL, a session verifier that returns CertificateAccepted now skips chain verification entirely, as it already did on OpenSSL.

Verification

All checks ran on a windows-latest GitHub Actions runner, comparing #2602 with this PR on OpenSSL 3.6, Mbed TLS 3.6 and wolfSSL 5.8.4.

Case Result
Starfield Root G2 removed, automatic root update enabled Rejected on #2602; accepted with this PR, and Windows fetched the root (OpenSSL and Mbed TLS with accounts.spotify.com, wolfSSL with www.godaddy.com)
untrusted-root, self-signed, expired on badssl.com Rejected on all backends
wrong.host.badssl.com SSLServerHostnameVerification on all backends
incomplete-chain.badssl.com Accepted on all backends
A server certificate verifier that rejects everything Rejected on all backends
Leaf with only the clientAuth EKU, under a root added to the Windows store Rejected with CERT_TRUST_IS_NOT_VALID_FOR_USAGE

Added SSLClientTest.WindowsCertificateVerification_RejectsUntrustedRoot. It runs offline on Windows CI and checks that a self-signed certificate is rejected by CryptoAPI, with CERT_TRUST_IS_UNTRUSTED_ROOT in the error.

Every backend loads the Windows ROOT and CA stores, but Windows adds a
root to them only when CryptoAPI needs it to build a chain. On a machine
that had not needed a root yet, the backend rejected the chain before
the CryptoAPI check ran, so Windows never fetched the root (for example
OpenSSL error 20 for accounts.spotify.com under Starfield Root G2).

With Windows verification enabled, the backend's chain verdict is no
longer used. Mbed TLS and wolfSSL skip chain verification during the
handshake, and the post-handshake verify result is ignored. The
CryptoAPI check becomes mandatory: a leaf that cannot be encoded now
fails the connection instead of skipping the check, and the chain must
allow server authentication, which the backends used to check.

A server certificate verifier works on the backend's chain verification,
so when one is set the backend still decides and CryptoAPI only adds its
own check, as before.

Refs #2596
cert.pem does not match HOST. With CryptoAPI as the chain verifier on
Windows, the hostname check now fails before the chain check, so the
test saw SSLServerHostnameVerification there. Disable hostname
verification so it still fails on the untrusted chain everywhere.
@yhirose
yhirose changed the base branch from windows-verify-intermediates to master October 3, 2026 00:48
@yhirose
yhirose force-pushed the windows-cryptoapi-sole-verifier branch from f08350b to 13e220e Compare October 3, 2026 00:48
@yhirose
yhirose merged commit 43863e1 into master Oct 3, 2026
44 of 45 checks passed
@yhirose
yhirose deleted the windows-cryptoapi-sole-verifier branch October 3, 2026 00:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant