Skip to content

Let CryptoAPI decide on a root missing from the OpenSSL store - #2603

Closed
yhirose wants to merge 2 commits into
windows-verify-intermediatesfrom
windows-defer-missing-root
Closed

yhirose wants to merge 2 commits into
windows-verify-intermediatesfrom
windows-defer-missing-root

Conversation

@yhirose

@yhirose yhirose commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Refs #2596. Stacked on #2602. Only the last commit belongs to this PR.

Problem

On Windows, the OpenSSL trust store is loaded 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:

SSL server verification failed ssl_error=0 ssl_backend_error=20

OpenSSL fails first with X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY. The CryptoAPI check, which would make Windows fetch the root, never runs.

Fix

When Windows certificate verification is enabled, the OpenSSL backend no longer fails on X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY. It leaves the decision to the CryptoAPI check that already runs afterwards. Two small changes keep that check complete:

  • OpenSSL stops at the missing issuer, so its purpose check never runs. CERT_CHAIN_POLICY_SSL does not check the extended key usage either. So CertGetCertificateChain() now requests szOID_PKIX_KP_SERVER_AUTH.
  • If the leaf cannot be encoded for CryptoAPI, the connection now fails. Before, the CryptoAPI check was skipped.

Behavior changes with Windows verification enabled

  • A server that sends an incomplete chain is accepted when CryptoAPI can complete it through AIA, as Schannel does (for example incomplete-chain.badssl.com).
  • When CryptoAPI rejects a chain on this path, ssl_backend_error() holds the CryptoAPI status instead of 20.
  • A chain whose certificates Windows does not allow for server authentication is rejected.

Verification

All checks ran on a windows-latest GitHub Actions runner with OpenSSL 3.x.

Case Result
accounts.spotify.com, Starfield Root G2 removed, automatic root update enabled OK on the first request; Windows fetched the root
Same, with automatic root update disabled by policy Rejected
untrusted-root, self-signed, expired, wrong.host on badssl.com Rejected as before
Leaf with only the clientAuth EKU, under a root Windows trusts but OpenSSL does not have Rejected with CERT_TRUST_IS_NOT_VALID_FOR_USAGE

Added SSLClientTest.WindowsCertificateVerification_UnknownIssuerRejected. It runs offline on Windows CI and checks that a chain under a root Windows does not trust is still rejected after the error is left to CryptoAPI.

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
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
@yhirose
yhirose force-pushed the windows-verify-intermediates branch from 1cdd4ed to 086a648 Compare October 2, 2026 13:30
@yhirose
yhirose force-pushed the windows-defer-missing-root branch from e137c37 to 9af2341 Compare October 2, 2026 13:30
@yhirose yhirose changed the title Let CryptoAPI decide when OpenSSL cannot find a Windows root Let CryptoAPI decide on a root missing from the OpenSSL store Oct 2, 2026
@yhirose
yhirose force-pushed the windows-verify-intermediates branch from 086a648 to 75cc18b Compare October 2, 2026 19:29
@yhirose

yhirose commented Oct 2, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #2604, which makes CryptoAPI the only chain verifier for every backend instead of deferring only this OpenSSL error.

@yhirose yhirose closed this Oct 2, 2026
@yhirose
yhirose deleted the windows-defer-missing-root 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