The Wayback Machine - https://web.archive.org/web/20240217100555/https://github.com/python/cpython/pull/112389
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-107361: strengthen default SSL context flags #112389

Open
wants to merge 17 commits into
base: main
Choose a base branch
from

Conversation

woodruffw
Copy link
Contributor

@woodruffw woodruffw commented Nov 25, 2023

See #107361: this adds VERIFY_X509_STRICT to make the default SSL context perform stricter (per RFC 5280) validation, as well as VERIFY_X509_PARTIAL_CHAIN to enforce more standards-compliant path-building behavior.

As part of this changeset, I had to tweak make_ssl_certs.py slightly to emit 5280-conforming CA certs. This changeset includes the regenerated certificates after that change.

CC @sethmlarson


📚 Documentation preview 📚: https://cpython-previews--112389.org.readthedocs.build/

See python#107361: this adds `VERIFY_X509_STRICT` to make the default
SSL context perform stricter (per RFC 5280) validation, as well
as `VERIFY_X509_PARTIAL_CHAIN` to enforce more standards-compliant
path-building behavior.

As part of this changeset, I had to tweak `make_ssl_certs.py`
slightly to emit 5280-conforming CA certs. This changeset includes
the regenerated certificates after that change.
@sethmlarson sethmlarson added the type-security A security issue label Nov 25, 2023
@gpshead gpshead self-assigned this Nov 29, 2023
@bedevere-app
Copy link

bedevere-app bot commented Nov 29, 2023

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

And if you don't make the requested changes, you will be poked with soft cushions!

@pitrou
Copy link
Member

pitrou commented Nov 29, 2023

Side note: perhaps we mark all PEM files as generated?

@woodruffw
Copy link
Contributor Author

Side note: perhaps we mark all PEM files as generated?

Done: I've marked certdata/*.{pem,0} as generated (the .0 files should also really be .pem I think -- I can do some cleanup there in a follow-up PR, if there's interest).

@@ -125,6 +124,11 @@ Other Language Changes
equivalent of the :option:`-X frozen_modules <-X>` command-line option.
(Contributed by Yilei Yang in :gh:`111374`.)

* The :func:`ssl.create_default_context` API now includes
:data:`ssl.VERIFY_X509_PARTIAL_CHAIN` and :data:`ssl.VERIFY_X509_STRICT`
in its default flags.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please explain (give instructions) how to get the previous behavior?

Also, how can someone write future-proof code by switching to these defaults in Python 3.12 and older? Maybe also provide instructions for that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind to explain which kinds of certificates are no longer accepted? If someone is impacted, how certificates should be generated to fit with stricter security policy?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please explain (give instructions) how to get the previous behavior?

Sure, I can add a note callout similar to the one for SSLv3.

Also, how can someone write future-proof code by switching to these defaults in Python 3.12 and older? Maybe also provide instructions for that?

The simplest way would probably be to either use create_default_context() or SSLContext() and twiddle ctx.verify_flags directly.

Do you think it makes sense to include that in the ssl.rst docs? It looks like there isn't precedent for doing so (unlike the callout for SSLv3). If so, I'm happy to do it 🙂

Would you mind to explain which kinds of certificates are no longer accepted? If someone is impacted, how certificates should be generated to fit with stricter security policy?

This has two parts:

  • VERIFY_X509_PARTIAL_CHAIN makes chain building strictly more permissive, not less. Specifically, it tells OpenSSL to build chains the way RFC 5280 tells it to, not the bespoke way it does by default. So this flag alone shouldn't cause any certificate rejections, only discoveries of more valid chains 🙂 -- no changes should be required on users' parts.
  • VERIFY_X509_STRICT tells OpenSSL to validate X.509 certificates per the RFC 5280 profile, which has more extension/criticality requirements than OpenSSL checks by default. In practice, this should have no effect on >99% of users: most root programs use 5280 as their baseline, and the Web PKI's CABF rules are also built on 5280.

TL;DR: No generation changes need to happen for VERIFY_X509_PARTIAL_CHAIN. For VERIFY_X509_STRICT, the small number of users doing things like openssl req manually should ensure that their generated certs conform to RFC 5280 (which they already need to, if they're being used in browsers or other contexts where 5280 or a superset is assumed).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it makes sense to include that in the ssl.rst docs?

Doing it in the what's new makes sense for sure - It doesn't hurt to also include it in the ssl.rst versionchanged section. People come at docs from multiple angles depending on what they're trying to do or when and how they are debugging things.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was suggesting to add these information in Python doc. Since tuning a SLContext is complex, IMO it's worth it to elaborate that in ssl.rst and point to that from What's New In Python.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either link to ssl doc which explains how to get Python 3.12 behavior, or repeat the instructions here. Something like:

context.verify_flags &= ~(_ssl.VERIFY_X509_PARTIAL_CHAIN | _ssl.VERIFY_X509_STRICT)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a callout similar to the one for SSLv3 here: 4ae44d1

That'll get linked via the ref to :func:ssl.create_default_context -- do you think another link makes sense, or is that sufficient?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to add a link from here to the this new doc? Or copy/paste it here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've copied the same .. note and example here, PTAL 🙂

@vstinner
Copy link
Member

In practice, this should have no effect on >99% of users: most root programs use 5280 as their baseline, and the Web PKI's CABF rules are also built on 5280.

Your PR required to regenerate all PEM test certificates. Does it mean that they belong to the 1% of affected users?

@woodruffw
Copy link
Contributor Author

Your PR required to regenerate all PEM test certificates. Does it mean that they belong to the 1% of affected users?

I made the numbers up for emphasis, but I'll stand by this having no effect on the vast majority of users: the vast majority use case is the Web PKI, which is significantly stricter than even VERIFY_X509_STRICT. The fact that the PEMs had to be regenerated is more a testament to how lax OpenSSL is by default than any actual common practice 🙂

Or put another way: most users are not manually generating certs with openssl req or, if they are, they're massaging them into RFC 5280 compliance because other ecosystems (like Go, Java, and OSes/major software) are much more strict.

Additional datapoints:

(I picked these semi-arbitrarily from https://pkic.org/ltl/)

Copy link
Contributor

@sethmlarson sethmlarson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ++ on the partial chain changes, my comments below apply to VERIFY_X509_STRICT:

Since OpenSSL will likely be the way folks are generating certificates and like you said, OpenSSL allows you to do things that isn't acceptable for Web PKI, is there any value in providing guidance on how one does generate a certificate with OpenSSL that works with VERIFY_X509_STRICT?

Also since we're recommending folks down a new code path to restore old behavior, should we create a new test which exercises that code path and verifies that "bad" certificates would be accepted?

What about non-Web PKI, does this change have any affect for certificates in that domain? I am less versed for this, so am relying a bit on others' experience.

@pitrou
Copy link
Member

pitrou commented Dec 1, 2023

Since OpenSSL will likely be the way folks are generating certificates

Is that still the case? Aren't most users relying on something like Let's Encrypt nowadays (at least for Web certificates, but also perhaps e-mail)?

is there any value in providing guidance on how one does generate a certificate with OpenSSL that works with VERIFY_X509_STRICT?

I don't think writing an OpenSSL command line tutorial is really a responsibility for CPython maintainers.

@woodruffw
Copy link
Contributor Author

Since OpenSSL will likely be the way folks are generating certificates and like you said, OpenSSL allows you to do things that isn't acceptable for Web PKI, is there any value in providing guidance on how one does generate a certificate with OpenSSL that works with VERIFY_X509_STRICT?

To clarify: I think most users won't be using OpenSSL to generate certs -- I think the majority will be getting their certs from a CA service (like Let's Encrypt), with a very small minority doing bespoke things. Putting an OpenSSL example in the docs might cause additional confusion, and IMO won't be too helpful anyways (since the context here is verification, in which case the user might not have any control over the certs they're being presented with).

What about non-Web PKI, does this change have any affect for certificates in that domain? I am less versed for this, so am relying a bit on others' experience

Nope, this should be wholly compatible with those -- "strict" validation for OpenSSL means "approximate RFC 5280," and the Web PKI profiles are a superset of RFC 5280 🙂

In other words: all public web certificates should be compatible with these changes.

@malemburg
Copy link
Member

These are the checks that OpenSSL applies when VERIFY_X509_STRICT is set: https://www.openssl.org/docs/man3.0/man1/openssl-verification-options.html#x509_strict (search for -x509_strict in case the link doesn't take you there directly)

These checks all make sense for certs generated with a trusted CA, e.g. Let's Encrypt.

The other common use case is having self-signed certs (e.g. for intranet/VPC devices). Those will already fail to verify with the current standard settings, since self-signed certs are rejected by OpenSSL's verify. You have to use the ssl._create_unverified_context() or a custom one to the same effect for those.

Note: The POP/FTP/IMAP and SMTP modules use the unverified context, since intranet/VPC mail and FTP servers will often not use certs created by a trusted CA for various reasons or they are referenced using IP addresses, which don't work with certs at all.

Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm in favor of making Python more secure by default, but your PR shows that even Python core devs who maintain PEM files failed to generate certificates properly. So it's likely that at least another user will be affected by stricter default. I'm just asking for clear instructions on how to identify if you are are affected and you're affected, explain how to get the old behavior.

I don't think that Python should explain how to regenerate PEM certificates passing stricter default. I prefer to let users make their own choices.

Doc/library/ssl.rst Show resolved Hide resolved
Doc/library/ssl.rst Outdated Show resolved Hide resolved
@@ -125,6 +124,11 @@ Other Language Changes
equivalent of the :option:`-X frozen_modules <-X>` command-line option.
(Contributed by Yilei Yang in :gh:`111374`.)

* The :func:`ssl.create_default_context` API now includes
:data:`ssl.VERIFY_X509_PARTIAL_CHAIN` and :data:`ssl.VERIFY_X509_STRICT`
in its default flags.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either link to ssl doc which explains how to get Python 3.12 behavior, or repeat the instructions here. Something like:

context.verify_flags &= ~(_ssl.VERIFY_X509_PARTIAL_CHAIN | _ssl.VERIFY_X509_STRICT)

@woodruffw
Copy link
Contributor Author

Makes sense, I'll make those documentation changes today.

@malemburg
Copy link
Member

Just to clarify: The CA certs used for testing had to be updated, since the strict check requires these additional constraints:

  • The basicConstraints of CA certificates must be marked critical.
  • CA certificates must explicitly include the keyUsage extension.

This was not the case for the root CA certs in the test data. After updating the CA certs, all other certs signed with these CA certs had to be resigned.

People will not often create root CA certs by hand (but yes, they might for test cases). So instead of creating uncertainty for regular user certificates, perhaps it's a good idea to highlight these stricter requirements for root CA certs.

@sethmlarson
Copy link
Contributor

sethmlarson commented Dec 5, 2023

@woodruffw I would still like a new test case for ensuring that disabling X509_VERIFY_STRICT would allow a "bad" certificate to be used. After that is added I am satisfied with this PR, thanks much!

Doc/library/ssl.rst Outdated Show resolved Hide resolved
@@ -125,6 +124,11 @@ Other Language Changes
equivalent of the :option:`-X frozen_modules <-X>` command-line option.
(Contributed by Yilei Yang in :gh:`111374`.)

* The :func:`ssl.create_default_context` API now includes
:data:`ssl.VERIFY_X509_PARTIAL_CHAIN` and :data:`ssl.VERIFY_X509_STRICT`
in its default flags.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to add a link from here to the this new doc? Or copy/paste it here?

Lib/ssl.py Show resolved Hide resolved
@woodruffw
Copy link
Contributor Author

@woodruffw I would still like a new test case for ensuring that disabling X509_VERIFY_STRICT would allow a "bad" certificate to be used. After that is added I am satisfied with this PR, thanks much!

Making sure I understand: the idea is a "backstop" test to make sure that users have a downgrade path, right? If so, makes sense, and I'll work on that now.

@@ -2945,6 +2956,36 @@ def test_ecc_cert(self):
cipher = s.cipher()[0].split('-')
self.assertTrue(cipher[:2], ('ECDHE', 'ECDSA'))

def test_verify_strict(self):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sethmlarson This test provides a backstop check on VERIFY_X509_STRICT, PTAL!

@woodruffw
Copy link
Contributor Author

Sorry for the delay here! I've updated this PR to include a testcase for strict verification, including a "backstop" test that ensures that disabling VERIFY_X509_STRICT allows this to verify as it did before the change.

OpenSSL is not consistent about error types between versions.
@woodruffw
Copy link
Contributor Author

woodruffw commented Feb 2, 2024

I'm a little mystified by this error, which only happens on OpenSSL 1.1.1w in CI:

 0:00:10 load avg: 2.63 [18/43/1] test_ssl failed (1 failure)
test test_ssl failed -- Traceback (most recent call last):
  File "/home/runner/work/cpython/cpython/Lib/test/test_ssl.py", line 2971, in test_verify_strict
    with self.assertRaises(ssl.SSLError):
        s.connect((HOST, server.port))
AssertionError: SSLError not raised

AFAICT, the strict flag exists in that version and has the same semantics as in OpenSSL 3.0 and 3.1, which both pass.

Edit: This is probably why: openssl/openssl@1e41dad (and openssl/openssl#10688)

@woodruffw
Copy link
Contributor Author

This should be good to go again: I've added a "backstop" test that confirms that VERIFY_X509_STRICT enables and disables as expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants