WinHTTP over an HTTPS forward proxy - CRL problem

Izaf 0 Reputation points
2026-08-30T06:56:01.5466667+00:00

Problem

A native Windows client uses WinHTTP with a secure (HTTPS) forward proxy — i.e. the connection to the proxy is itself TLS, specified as a named proxy with an https:// scheme:

https://mysecureproxy.duckdns.org:3129

The proxy presents a normal, publicly-trusted Let's Encrypt server certificate (valid dates, matching CN/SAN, trusted CA). WinHttpSendRequest nevertheless fails with:

error 12188  (ERROR_WINHTTP_SECURE_FAILURE_PROXY)

The failure happens during the TLS handshake to the proxy, before any request is tunnelled to the origin. SECURITY_FLAG_IGNORE_* (unknown CA / CN / date / usage) does not help.

What we think is happening

Validating the proxy certificate includes a revocation check. Modern Let's Encrypt certificates carry only a CRL distribution point (OCSP was retired), e.g. http://yr2.c.lencr.org/12.crl.

When a system proxy is configured (manual proxy and/or WPAD auto-detect), CryptoAPI's CRL retrieval is routed back through the same secure proxy — but establishing that connection requires validating the proxy certificate, which requires the CRL. A bootstrap loop:

connect to proxy → validate proxy cert → need CRL
    → fetch CRL through proxy → connect to proxy → …  ⇒ revocation "offline"

CryptoAPI reports CRYPT_E_REVOCATION_OFFLINE (CERT_TRUST_IS_OFFLINE_REVOCATION | CERT_TRUST_REVOCATION_STATUS_UNKNOWN), schannel treats offline revocation of the proxy cert as fatal, and WinHTTP surfaces it as 12188.

A direct fetch of the CRL (curl http://yr2.c.lencr.org/12.crl) returns HTTP 200 in ~100 ms — so the endpoint is healthy. The problem is routing of the revocation fetch, not availability.

What crlproof proves

crlproof.cpp isolates revocation as the single variable. It grabs the proxy's leaf certificate (direct TLS connect, cert errors ignored) and builds the certificate chain twice on the same machine:

Build Flags dwErrorStatus (proxy fault present)
without revocation 0 0x00000000 (trusted, name & dates OK)
with revocation CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT 0x01000040 = IS_OFFLINE_REVOCATION | REVOCATION_STATUS_UNKNOWN

Same cert, same machine, same chain engine — only the revocation flag changes. The clean-then-offline result pins the failure to revocation alone, not CA trust, hostname, or expiry. This matches the Windows chain engine (certutil -verify -urlfetch) verdict and the WinHTTP 12188.

main.txtcrlproof.txtCMakeLists.txt

Windows development | Windows API - Win32
0 comments No comments

2 answers

Sort by: Most helpful
  1. Taki Ly (WICLOUD CORPORATION) 4,440 Reputation points Microsoft External Staff Moderator
    2026-08-31T09:22:59.1566667+00:00

    Hello @Izaf ,

    Thank you for sharing such a detailed analysis along with the crlproof and wproxy samples. Based on those, the behavior here appears to relate to the revocation check on the proxy certificate, rather than to CA trust, name, or date.

    When reproducing this with a local secure proxy whose leaf certificate is trusted and valid but whose only CRL distribution point is unreachable, the observed results were 0x00000000 without revocation, 0x01000040 with revocation, and 12188 from WinHttpSendRequest. When the same certificate carries no CDP at all, revocation comes back as status unknown only and the request proceeds, so it appears to be the combination of a present CDP and an unreachable CRL that turns the result into a failure.

    On this point, Windows provides a related option, WINHTTP_OPTION_IGNORE_CERT_REVOCATION_OFFLINE (Windows 10 2004 and later, set on the request handle), described in the WinHTTP option flags as allowing a secure connection to use a certificate whose CRL could not be downloaded. The SECURITY_FLAG_IGNORE_* set does not include a flag for revocation, so this option is the corresponding mechanism for the "revocation could not be performed" case.

    One thing worth noting from the observations is that the per-request relaxations, including SECURITY_FLAG_IGNORE_*, the secure-failure callback, and WINHTTP_OPTION_IGNORE_CERT_REVOCATION_OFFLINE, appear to apply to the origin server certificate rather than to the secure proxy certificate. When the ignore-offline option was set for the proxy leg, the call returned success but the request still stopped at 12188. This is consistent with --insecure and the callback having no effect on the proxy leg, and it suggests there may not be a per-request option to relax revocation for the proxy certificate specifically.

    For mitigation, it may help to adjust things on the environment side: allowing the CRL host to be fetched directly (for Let's Encrypt, that is c.lencr.org) by adding it to the proxy bypass list or otherwise permitting direct outbound HTTP to it, so that CryptoAPI can retrieve the proxy certificate's CRL without going through the proxy. Other options to consider include pre-seeding that CRL into the machine cache, using a proxy certificate whose CRL is reachable without the proxy, or adjusting the revocation policy for that path at the Schannel level. Trying the ignore-offline option on your side may also be worth a look, though in this reproduction it did not change the outcome for the proxy case.

    Hope these information help. If you found my response helpful or informative, I would greatly appreciate it if you could follow this guide for your confirmation.

    Thank you.

    Was this answer helpful?


  2. Taki Ly (WICLOUD CORPORATION) 4,440 Reputation points Microsoft External Staff Moderator
    2026-08-31T02:52:37.6666667+00:00

    Hi @Izaf ,

    I'm working on this issue and will try to get back to you soon. Thank you for your patience.

    Was this answer helpful?


Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.