Skip to content

ssl.create_default_context(): add VERIFY_X509_STRICT and VERIFY_X509_PARTIAL_CHAIN to the default verify_flags #107361

Description

@woodruffw

Feature or enhancement

My proposal is to add two new flags to the SSLContext.verify_flags created within ssl.create_default_context():

  1. VERIFY_X509_STRICT: This will enable stricter RFC 5280 compliance checks within OpenSSL's X.509 path validation implementation. This will have no effect on the overwhelming majority of users (since the overwhelming majority of TLS connections are likely going through the Web PKI, which is already much stricter than RFC 5280).
  2. VERIFY_X509_PARTIAL_CHAIN: This will allow OpenSSL's path validation to terminate at the first certificate listed in the user's root of trust, even if that certificate is not self-signed. Despite the confusing name and the fact that this isn't the default, this is the correct behavior per the various X.509 RFCs (3280, 5280): a trust anchor is defined to be an a priori trust relationship with a subject and its public key, regardless of whether the anchor comes in certificate form or is signed by another member of the trusted store. This should have no breaking effect on any users, but may cause some validations to produce shorter chains than the current SSLContext enables.

My proposal is consistent with the stability policy for create_default_context, which says that CPython may introduce changes to the default context without a prior deprecation period:

The protocol, options, cipher and other settings may change to more restrictive values anytime without prior deprecation. The values represent a fair balance between compatibility and security.

As with previous changes to the default context (such as the removal of 3DES support with 3.6), these proposed changes will not prevent people from constructing their own SSLContext without these new flags. In other words: users who do experience breakage or other behavioral changes will have a well-trodden alternative available to them, one that is documented as a matter of policy.

Pitch

To summarize from DPO:

  • Enabling VERIFY_X509_STRICT is a net security win: it reduces the amount of flexibility in the X.509 certificates that the ssl module accepts by default, which means less attacker controlled flexibility. It also makes CPython itself more compatible with the X.509 profile defined in RFC 5280, meaning that end-users can more confidently expect interoperation with PKIs that use the 5280 profile.
  • Enabling VERIFY_X509_PARTIAL_CHAIN makes ssl comply more closely with other path validation/building implementations (like Go's), and eliminates an error mode that's only possible because of OpenSSL's non-standard default behavior (a chain is built to the root of trust successfully, but can still fail because OpenSSL can't find a self-signed root also within the root of trust). It's also consistent with what curl and other downstream consumers of OpenSSL do.

Previous discussion

See DPO discussion here: https://discuss.python.org/t/ssl-changing-the-default-sslcontext-verify-flags/30230

Linked PRs

Activity

  1. alex commented on Jul 27, 2023

    @alex
    Member

    @python/crypto-team

  2. added a commit that references this issue on Nov 25, 2023
  3. woodruffw commented on Nov 25, 2023

    @woodruffw
    ContributorAuthor

    Opened #112389 for this.

  4. added a commit that references this issue on Mar 6, 2024
  5. gpshead commented on Mar 6, 2024

    @gpshead
    Member

    Thanks for taking the time to do this!

  6. added a commit that references this issue on Mar 25, 2024
  7. added a commit that references this issue on Apr 17, 2024
  8. 7 remaining items

  9. Tasssadar commented on May 22, 2025

    @Tasssadar
    Contributor

    Hello, the VERIFY_X509_STRICT is breaking connections on some SSL inspection proxies, which replace the original SSL cert, with Missing Authority Key Identifier. I've tried to check some docs, and I'm not even sure they can generate a "fake" certificate with valid AKI actually.

    No browsers seem to require this either, so openssl seems stricter here.

    I'm not trying to say this is a wrong change or that you should not do it, just trying to provide some feedback, not sure if you get much of it after changes like these.

    Also, looking at the discussion, there were some examples mentioned of other projects making SSL stricter, and introducing env variables to disable this stricter mode. AFAIK golang project also does this, and I'd like to propose you to adopt the same approach with future changes like this - setting env variable is significantly easier than hunting down every place where ssl context is created, and setting some flag to it.

  10. woodruffw commented on May 22, 2025

    @woodruffw
    ContributorAuthor

    I've tried to check some docs, and I'm not even sure they can generate a "fake" certificate with valid AKI actually.

    This is possible: the AKI is tied to whatever the SSL inspection proxy's root cert is. For context, I believe another large SSL proxy vendor has already fixed this in their products based on the Python change here. I'll ping some people and see if they're willing to share more information about that.

    No browsers seem to require this either, so openssl seems stricter here.

    There's some nuance here: modern browsers are aware of inspection proxies, and apply looser validation rules to them because they're persistently misconfigured (e.g. violating RFC 5280 in this case). Chrome and Firefox should both refuse a similarly mis-issued certificate on the Web PKI, although this is difficult to provide a public example of since Web PKI CAs will also refuse to issue an invalid certificate here.

    AFAIK golang project also does this, and I'd like to propose you to adopt the same approach with future changes like this - setting env variable is significantly easier than hunting down every place where ssl context is created, and setting some flag to it.

    This is ultimately the maintainers' call, but I would be a -1 on it as an outsider: IMO environment variables are too easy to "set and forget," leaving deployments in a permanently degraded security posture. This is the point I made in this DPO comment.

    (I believe the official advice with Python and the ssl module is that create_default_context can change at any time for security reasons. If you're in an environment where stability is more important than a default security posture, the docs recommend creating your own SSLContext. However, per above, this is something that your SSL proxy vendor should fix directly -- they're currently violating Web PKI requirements that the entire public Internet has adopted 🙂)

  11. Tasssadar commented on May 22, 2025

    @Tasssadar
    Contributor

    Thank you for this information, it is very hard to come by, so this is much appreciated!

    This is possible: the AKI is tied to whatever the SSL inspection proxy's root cert is. For context, I believe another large SSL proxy vendor has already fixed this in their products based on the Python change here. I'll ping some people and see if they're willing to share more information about that.

    Thank you, we'll try to talk to our vendor - if you'd be able to get any more information, I'd be very grateful.

    This is ultimately the maintainers' call, but I would be a -1 on it as an outsider: IMO environment variables are too easy to "set and forget," leaving deployments in a permanently degraded security posture.

    I understand your reasoning, and I'd love this to have the wanted effect, but the reality is the SSL proxy will not be fixed anytime soon even if it is entirely at fault, so I now have two options - either not use Python 3.13, or disable the extra validation. And without env var, the "easy to set and forget" thing is now everywhere an ssl context is created instead of, say, one base Docker image.

  12. klausenbusk commented on May 28, 2025

    @klausenbusk
    1. VERIFY_X509_STRICT: This will enable stricter RFC 5280 compliance checks within OpenSSL's X.509 path validation implementation. This will have no effect on the overwhelming majority of users (since the overwhelming majority of TLS connections are likely going through the Web PKI, which is already much stricter than RFC 5280).

    VERIFY_X509_STRICT apparently requires the optional (from a CA baseline perspective) "Subject Key Identifier" to be set. That is not the case for all CAs included in Mozilla Root Program, in particular it is missing for "TWCA Global Root CA".

    So all leaf certificates with trust to said CA now result in a ssl.SSLCertVerificationError (for example the CA own website: https://www.twca.com.tw). I don't use said CA myself and only noticed due to a user report.

    Is this expected?

    $ python3
    Python 3.13.3 (main, Apr  9 2025, 07:44:25) [GCC 14.2.1 20250207] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import urllib.request
    >>> urllib.request.urlopen('https://www.twca.com.tw/')
    Traceback (most recent call last):
      File "/usr/lib/python3.13/urllib/request.py", line 1319, in do_open
        h.request(req.get_method(), req.selector, req.data, headers,
        ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                  encode_chunked=req.has_header('Transfer-encoding'))
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/usr/lib/python3.13/http/client.py", line 1338, in request
        self._send_request(method, url, body, headers, encode_chunked)
        ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/usr/lib/python3.13/http/client.py", line 1384, in _send_request
        self.endheaders(body, encode_chunked=encode_chunked)
        ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/usr/lib/python3.13/http/client.py", line 1333, in endheaders
        self._send_output(message_body, encode_chunked=encode_chunked)
        ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/usr/lib/python3.13/http/client.py", line 1093, in _send_output
        self.send(msg)
        ~~~~~~~~~^^^^^
      File "/usr/lib/python3.13/http/client.py", line 1037, in send
        self.connect()
        ~~~~~~~~~~~~^^
      File "/usr/lib/python3.13/http/client.py", line 1479, in connect
        self.sock = self._context.wrap_socket(self.sock,
                    ~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
                                              server_hostname=server_hostname)
                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/usr/lib/python3.13/ssl.py", line 455, in wrap_socket
        return self.sslsocket_class._create(
               ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
            sock=sock,
            ^^^^^^^^^^
        ...<5 lines>...
            session=session
            ^^^^^^^^^^^^^^^
        )
        ^
      File "/usr/lib/python3.13/ssl.py", line 1076, in _create
        self.do_handshake()
        ~~~~~~~~~~~~~~~~~^^
      File "/usr/lib/python3.13/ssl.py", line 1372, in do_handshake
        self._sslobj.do_handshake()
        ~~~~~~~~~~~~~~~~~~~~~~~~~^^
    ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Missing Subject Key Identifier (_ssl.c:1028)
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "<python-input-1>", line 1, in <module>
        urllib.request.urlopen('https://www.twca.com.tw/')
        ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "/usr/lib/python3.13/urllib/request.py", line 189, in urlopen
        return opener.open(url, data, timeout)
               ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
      File "/usr/lib/python3.13/urllib/request.py", line 489, in open
        response = self._open(req, data)
      File "/usr/lib/python3.13/urllib/request.py", line 506, in _open
        result = self._call_chain(self.handle_open, protocol, protocol +
                                  '_open', req)
      File "/usr/lib/python3.13/urllib/request.py", line 466, in _call_chain
        result = func(*args)
      File "/usr/lib/python3.13/urllib/request.py", line 1367, in https_open
        return self.do_open(http.client.HTTPSConnection, req,
               ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                            context=self._context)
                            ^^^^^^^^^^^^^^^^^^^^^^
      File "/usr/lib/python3.13/urllib/request.py", line 1322, in do_open
        raise URLError(err)
    urllib.error.URLError: <urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Missing Subject Key Identifier (_ssl.c:1028)>
    >>> 
  13. woodruffw commented on May 29, 2025

    @woodruffw
    ContributorAuthor

    VERIFY_X509_STRICT apparently requires the optional (from a CA baseline perspective) "Subject Key Identifier" to be set. That is not the case for all CAs included in Mozilla Root Program, in particular it is missing for "TWCA Global Root CA".

    The nuance here is that CABF 7.1.2.11.4 points to RFC 5280 4.2.1.2, which says:

    To facilitate certification path construction, this extension MUST
    appear in all conforming CA certificates, that is, all certificates
    including the basic constraints extension (Section 4.2.1.9) where the
    value of cA is TRUE.

    Given that, my interpretation (which could be wrong) of CABF's language here is that "If present" is intended to provide leeway for existing root program members, not act as normative guidance for new CA issuances. In other words, a CA issued in 2025 would be considered mis-issued if it was missing an SKI, but older CAs that don't conform to RFC 5280 are seemingly permitted in this instance by CABF.

    Is this expected?

    Yes and no 😅 -- "yes" in the sense that this change intentionally enforces strict RFC 5280 conformance, and "no" in the sense that the intention wasn't to break chains actually present in the Web PKI (versus ratcheting down on chains in private PKIs).

    However, I'm actually a bit confused as to how this is breaking: my connection to www.twca.com.tw shows TWCA Global EVSSL Certification Authority as the ICA, which in turn has at least one issued CA certificate with a conforming SKI extension as well as an issued root CA certificate with a conforming SKI. So I suspect that what's happening here is that the CA has issued conforming certs, but that not all root programs have adopted them. I'll need to look into that more.

    Edit: for context:

    Edit 2: the plot thickens, as my macOS machine's root program does contain the expected certificate for TWCA Root Certification Authority:

    Image

    ...and so does /private/etc/ssl/cert.pem for TWCA Root Certification Authority but not TWCA Global Root CA:

    Image Image

    That version of TWCA Global Root CA appears to be https://crt.sh/?id=8559119, which PKI linters do indeed complain about: https://crt.sh/?id=8559119&opt=pkimetal

    Edit 3:

    Looks like the keychain/PEM bundle thing was a red herring, since the keychain version of TWCA Global Root CA is also missing an SKI:

    Image

    So, recap:

    1. The ultimate problematic cert here appears to be from TWCA Global Root CA, and is https://crt.sh/?id=8559119.
    2. If the root program contained https://crt.sh/?id=5537412 instead (same CA and key, just a different issuance), then there would be no validation problem here.
    3. In principle none of this should matter, since https://crt.sh/?id=8559119 is part of the root program and therefore should be considered a "trust anchor" rather than having its X.509 encapsulation evaluated for policy purposes. This is presumably happening because OpenSSL insists on evaluating trust anchors as if they were untrusted intermediates.
  14. added a commit that references this issue on Jun 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions