Skip to content

Urllib default SSL context erratic in Windows #101738

@bitlogik

Description

@bitlogik

On Windows 10. I mostly expect that Windows 8.1 and 11 are performing the same.

Python 3.9.11 and 3.10.9 are affected for sure. I think all recent Python versions are affected.

How to Reproduce ?

Delete all the following certificates from the Computer certificates manager (and the user certificate manager)

  • DST Root CA X
  • ISRG Root X
  • LetEncrypt X
  • IdenTrust
  • R3 intermediate

I think we can also start from a brand new Windows machine with the bare minimal dozen roots.

At the end (starting point to reproduce) is not an exotic Windows configuration, but a pretty standard one. We initially investigate on this matter as a customer faces this issue on one of our commercial Python bundled application.

Open in Edge :
https://valid-isrgrootx1.letsencrypt.org/

Now runs this snippet

from urllib import request

domain = "https://valid-isrgrootx1.letsencrypt.org"
req = request.Request(domain)
resp = request.urlopen(req)

Sometimes its OK
Sometimes it throws
urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1129)>

That's the issue. The behavior is not even the same between Python sessions.

Investigation

The "divergence" seems to be during loading. Like the certificates store is loaded once when Python starts, probably during import urllib. I saw that because on an affected app which communicates with various domains, it is "all or nothing". Sometimes the app starts and all HTTPS calls are OK. And some other start, all the calls to HTTPS to some web domains are failing (cert expired).

To see more information I did :

from urllib import request
import ssl

domain = "https://valid-isrgrootx1.letsencrypt.org"
req = request.Request(domain)
ctx = ssl.SSLContext()
ctx.load_default_certs()
print(ctx.cert_store_stats())
resp = request.urlopen(req, context=ctx)

But it doesn't bring the issue.
That means this is a different behaviour from when the context is loaded "automatically by default" (no context provided). At least that provide a workaround of this issue. But many developer just use plain urlopen. I mean it complexifies all the queries as one needs to import ssl and load a default context.

So the "automatic load by default" (context is None) code path in Windows is kind of faulty, as it seems it can't see the same certificates at launch time.

My investigations suggests it happens when a website TLS proxy handle the new ISRG Root X1 and also the old DST Root CA X3. The problem is that there is a lot of such web sites or APIs.

Interesting point with "valid-isrgrootx1.letsencrypt.org" is that the expired chain it refers to, is a chain for a different domains. That means it check for certificate validity before checking for domain validity (if it does so).

Additionally if one deletes the DST Root CA X3 certificates, the error is similar it just tell about "certificate verify failed: unable to get issuer certificate" which is a variant of this issue. During some start, it can't see the valid full cert path with the new ISRG X1.

What is the more mysterious in this issue is that during some Python program start, it behaves well. And during some others session, it doesn't (like ignoring the IRSG1 new certificate). It feels like Python openssl doesn't always load the certificate store the same way.

Is there a way to reach the context of the request?
Something like resp._sock._ctx ?

This issue is very close to #89535
but different: the certificate store is not changing between run sessions, it is only how Python openssl loads the certificates. And the behavior is erratic : sometimes the store loading is OK, sometimes partial and provides a bad default ssl context.
One expects the default SSLcontext calls load_default_certs() and behaves same, but this is not the case. It is different, and from one start to an other, it doesn't behaves the same.

I feel the issue lies in openssl. As Python chose to use and integrates openssl, this is an isue in Python. Also this affects developers (Python users), as one default loading is not working properly.

We'll continue to investigate, focusing only on openssl.

Metadata

Metadata

Assignees

No one assigned

    Labels

    OS-windowspendingThe issue will be closed if no feedback is providedtopic-SSLtype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions