Skip to content

gh-137197: Add SSLContext.set_ciphersuites to set TLS 1.3 ciphers #137198

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

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

Conversation

ronf
Copy link
Contributor

@ronf ronf commented Jul 29, 2025

This commit adds a new method SSLContext.set_ciphersuites which can be used to set TLS 1.3 cipher suites. It also updates the documentation, unit tests, and "what's new" text. A NEWS blurb will be added shortly.


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

@@ -3595,12 +3595,27 @@ _ssl__SSLContext_set_ciphers_impl(PySSLContext *self, const char *cipherlist)
{
int ret = SSL_CTX_set_cipher_list(self->ctx, cipherlist);
if (ret == 0) {
/* Clearing the error queue is necessary on some OpenSSL versions,
Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't remove this without understanding which OpenSSL versions it was referring to and whether they are still in use by CPython builds (1.1.x-ish API'd AWS-LC at a minimum, otherwise OpenSSL 3.0+).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The clearing of the error queue is still happening here. I just took advantage of an existing helper function setSSLError to take care of this:

static PyObject *
_setSSLError (_sslmodulestate *state, const char *errstr, int errcode, const char *filename, int lineno)
{
    if (errstr == NULL)
        errcode = ERR_peek_last_error();
    else
        errcode = 0;
    fill_and_set_sslerror(state, NULL, state->PySSLErrorObject, errcode, errstr, lineno, errcode);
    ERR_clear_error();
    return NULL;
}

Comment on lines 321 to 324
* Added new method :meth:`ssl.SSLContext.set_ciphersuites` for setting TLS 1.3
ciphers and updated the documentation on :meth:`ssl.SSLContext.set_ciphers`
to mention that it only applies to TLS 1.2 and earlier and that this new
method must be used to set TLS 1.3 cipher suites.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Added new method :meth:`ssl.SSLContext.set_ciphersuites` for setting TLS 1.3
ciphers and updated the documentation on :meth:`ssl.SSLContext.set_ciphers`
to mention that it only applies to TLS 1.2 and earlier and that this new
method must be used to set TLS 1.3 cipher suites.
* Added new method :meth:`ssl.SSLContext.set_ciphersuites` for setting TLS 1.3
ciphers. For TLS 1.2 or earlier, use :meth:`ssl.SSLContext.set_ciphers` instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you think about changing the text here to something like:

* Added new method :meth:`ssl.SSLContext.set_ciphersuites` for setting TLS 1.3
  ciphers. For TLS 1.2 or earlier, :meth:`ssl.SSLContext.set_ciphers` should
  continue to be used. Both calls can be made on the same context and the
  selected cipher suite will depend on the TLS version negotiated when a
  connection is made.

I wanted to somehow capture that these calls aren't mutually exclusive if you don't know in advance what version of TLS will end up being used. The existing call only affects cipher suites chosen when TLS 1.2 or earlier is negotiated, and the new call only affects cipher suites chosen when TLS 1.3 is negotiated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@picnixz, are you ok with this new wording?

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 checked in the new wording I proposed. Let me know if you want any changes here.

I think this change may be ready to go if there are no other review comments.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I forgot about this PR and I missed the notification I think. Let me have a look again.

@ronf ronf requested a review from AA-Turner as a code owner August 9, 2025 00:23
ronf and others added 4 commits August 8, 2025 18:00
Clarify when to use the original set_ciphers (TLS 1.2 and earlier)
vs. the new set_ciphersuites (TLS 1.3) methods and that both can
be used at once.
@ronf ronf force-pushed the ssl-ciphersuite-support branch from f2cda88 to d3375f1 Compare August 9, 2025 01:00
@ronf
Copy link
Contributor Author

ronf commented Aug 9, 2025

I think all the remaining review comments should be addressed in 48e5164. Let me know if you'd like any other changes.

Comment on lines +1708 to +1710
.. note::
when connected, the :meth:`SSLSocket.cipher` method of SSL sockets will
return details about the negotiated cipher.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
.. note::
when connected, the :meth:`SSLSocket.cipher` method of SSL sockets will
return details about the negotiated cipher.
.. note::
When connected, the :meth:`SSLSocket.cipher` method of SSL sockets will
return details about the negotiated cipher.

You can also update the other note. I never saw that we had a missing capital letter.

continue to be used. Both calls can be made on the same context and the
selected cipher suite will depend on the TLS version negotiated when a
connection is made.
(Contributed by Ron Frederick in :gh:`137197`)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
(Contributed by Ron Frederick in :gh:`137197`)
(Contributed by Ron Frederick in :gh:`137197`.)

You don't need to update the entry just above. We'll clean it up in a PR for #133879.

@@ -263,7 +263,8 @@ def utc_offset(): #NOTE: ignore issues like #1647654

def test_wrap_socket(sock, *,
cert_reqs=ssl.CERT_NONE, ca_certs=None,
ciphers=None, certfile=None, keyfile=None,
ciphers=None, ciphersuites=None, min_version=None,
Copy link
Member

Choose a reason for hiding this comment

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

Just for the future, but how about adding max_version as well? and put min_version and max_version on their own lines? even if we don't use it, I think it's fine to add it just in case.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The current version of test_wrap_socket() only lets you specify min_version for now. It would be easy to add max, but it's called from 35 places. Would you change all of these to set both min & max?

The defaults for min/max version are special symbols MININUM_SUPPORTED and MAXIMUM_SUPPORTED, so there's no need to set the maximum unless you want to forcibly restrict it (in the future) to not using a TLS version greater than 1.3. I'm not sure I see a good reason to do that here, at least not yet. It would really come down to whether TLS 1.4 did something to yet again change the method used to set cipher suites, which I'm hoping won't be the case.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, it was more to ease checks in internal code paths.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I'm not sure what you mean.

@@ -1863,6 +1868,10 @@ class SimpleBackgroundTests(unittest.TestCase):

def setUp(self):
self.server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)

if has_tls_version('TLSv1_3'):
self.server_context.set_ciphersuites('TLS_AES_256_GCM_SHA384')
Copy link
Member

Choose a reason for hiding this comment

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

This could raise so we should be careful (for weird build options, I think it's possible to disable TLS_AES_256_GCM_SHA384). I would suggest only calling set_ciphersuites in tests that need it (that is those decorated by requires_tls_version. Otherwise the server_context would unconditoinally have this ciphersuite even if a test doesn't need it.

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 must admit I didn't love that change, but the problem here is in testing the mismatched ciphers case. I we leave the server accepting all ciphers, then there's no way to trigger a mismatched cipher, and I think the server is set up only once and then re-used for multiple test methods.

This mismatch test could be broken into its own test class so that we only set client and server cipher suites for that one specific test, but it would mean adding more boiler plate to set up the server in two different classes.

I felt pretty comfortable letting it be shared as this change doesn't impact the set of TLS 1.2 cipher suites used by any of these tests (either client or server) and any existing code would not be attempting to set any TLS 1.3 ciphers as that functionality didn't exist. Some of those tests might use TLS 1.3 today based on the defaults set but changing the server side to a specific cipher will still allow TLS 1.3 to be used, since the client-side would not be restricting TLS 1.3 ciphers.

That said, if you'd prefer I replicate the server setup to restrict this, I'm happy to do so.

As for the specific cipher I chose, I tried to pick the most "vanilla" one I could. For a test like this to exist, we're going to have to pick SOMETHING on the client and something else on the server to create the mismatch. I could try to do something like a get_ciphers(), filtering on TLS 1.3, and then pick two different values from that list, but it would complicate the test. For the unit testing, do you expect any of the test environments to disable these cipher suites? I see other test code hardcoding ciphers like these.

Copy link
Member

Choose a reason for hiding this comment

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

and I think the server is set up only once and then re-used for multiple test methods.

The server context is new for every test though? setUp() is called before every test method. If you want to ease your life, just have different test methods, each of them will have their own server_context that is independent.

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 was thinking of setUpClass() instead of setUp(), which only runs once for all tests in a class. However, even with setUp() running before each test, I'd still need to somehow pass into setUp() whether to restrict the ciphers on a test-by-test basis, and I'm not sure how to do that.

Since there's not a lot of setup code, I'm going to see what it looks like if I add a new class, with its own separate setUp() function, used only for this mismatch test. That'll minimize changes made to the existing test code.

Copy link
Member

Choose a reason for hiding this comment

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

Ah yes, I see the issue. It's because we create the server before and changing its context afterwards wouldn't help right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's right.

Copy link
Contributor Author

@ronf ronf Aug 9, 2025

Choose a reason for hiding this comment

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

Ok - I've checked in a new version of the test cases which uses its own class, reverting the changes to the previously modified class. This version also attempts to dynamically select ciphers from the list of available ciphers on the system. It assumes at least one cipher is available when the TLS 1.3 enabled check passes, but it will skip the mismatched cipher test if there's only one available TLS 1.3 cipher.

This commit reworks the set_ciphersuites() test cases, moving them into
their own class to avoid any changes to existing tests. It also makes
the cipher selection dynamic to avoid potentially trying to use a cipher
not available in some environments.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy