-
-
Notifications
You must be signed in to change notification settings - Fork 32.7k
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
base: main
Are you sure you want to change the base?
Conversation
@@ -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, |
There was a problem hiding this comment.
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+).
There was a problem hiding this comment.
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;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep it that way and change it in a follow-up PR instead. Actually, the errcode
parameter for _setSSLError
is redundant as it's always overwritten.
Doc/whatsnew/3.15.rst
Outdated
* 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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.
f2cda88
to
d3375f1
Compare
I think all the remaining review comments should be addressed in 48e5164. Let me know if you'd like any other changes. |
Doc/library/ssl.rst
Outdated
.. note:: | ||
when connected, the :meth:`SSLSocket.cipher` method of SSL sockets will | ||
return details about the negotiated cipher. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.. 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed both instances.
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`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My suggestion was:
def test_wrap_socket(sock, *,
cert_reqs=ssl.CERT_NONE, ca_certs=None,
ciphers=None, ciphersuites=None,
min_version=None, max_version=None,
...):
...
if min_version is not None:
context.minimum_version = min_version
if max_version is not None:
context.maximum_version = max_version
...
That way, we can set maximum_version
in tests if needed. For now, we don't but in the future we could. For instance, a test where we call set_ciphersuites
but with context.maximum_version = ssl.TLSVersion.TLSv1_2
. Or is the maximum version actually ignored?
Lib/test/test_ssl.py
Outdated
@@ -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') |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's right.
There was a problem hiding this comment.
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.
@picnixz, any further changes required? The build failure appears to be something in the test runner, as the only thing changed in the last checkin was capitalization in the docs. Prior to that, all tests succeeded. |
I'm not available before next week but I'll have a look. |
That'd be great - thanks! I spent some time today working on adding methods to manage signature algorithms and will create a new issue & PR for those as soon as these changes are merged. |
|
||
TLS 1.3 cipher suites cannot be disabled with | ||
:meth:`~SSLContext.set_ciphers`. | ||
.. method:: SSLContext.set_ciphersuites(ciphersuites) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs a .. versionadded:: next
(sorry I forgot about it).
<https://docs.openssl.org/master/man1/ciphers/>`_. | ||
To set allowed TLS 1.3 ciphers, use :meth:`SSLContext.set_ciphersuites`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to myself: I would have added a linebreak after this sentence to separate from "If no cipher..." but I think the entire page needs some edits, so let's not spend too much time on wording for now.
called instead of :meth:`SSLContext.set_ciphers`, which only affects | ||
ciphers in older TLS versions. The method :meth:`SSLContext.get_ciphers` | ||
returns information about ciphers for both TLS 1.3 and earlier versions | ||
and the method :meth:`SSLSocket.cipher` returns information about the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this paragraph, I would say "The XXX method" instead of "The method XXX".
:meth:`SSLContext.set_ciphers` cannot enable or disable any TLS 1.3 | ||
ciphers yet, but :meth:`SSLContext.get_ciphers` returns them. | ||
- TLS 1.3 uses a disjunct set of cipher suites. All AES-GCM and ChaCha20 | ||
cipher suites are enabled by default. To restrict which TLS1.3 ciphers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cipher suites are enabled by default. To restrict which TLS1.3 ciphers | |
cipher suites are enabled by default. To restrict which TLS 1.3 ciphers |
ciphers yet, but :meth:`SSLContext.get_ciphers` returns them. | ||
- TLS 1.3 uses a disjunct set of cipher suites. All AES-GCM and ChaCha20 | ||
cipher suites are enabled by default. To restrict which TLS1.3 ciphers | ||
are allowed, the method :meth:`SSLContext.set_ciphersuites` should be |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For posterity: I really hate that OpenSSL named it SSL_CTX_set_ciphersuites
for TLS 1.3 and later, and SSL_CTX_set_cipher_list
for TLS 1.2 and below. I hope that this won't be annoying to users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was surprised what they did here at first and I'm not a big fan about the naming chosen, but I can see why they didn't want to try and extend the existing command. It allows all kinds of partial matches on the five individual elements that make up a cipher suite in TLS 1.2 and earlier, plus other special keywords like LOW/MEDIUM/HIGH. All of that is gone with TLS 1.3, and the new function is much simpler and only supports an exact match against the very small number of cipher suites defined in TLS 1.3.
I don't really know why they didn't include "_list" in the name of the OpenSSL function, given that this seems to be the convention for other such calls that take a string argument.
The good news here is that things like post-quantum crypto are going to drive migration to TLS 1.3, and at some point setting TLS 1.2 cipher suites will no longer matter. At that point, the previous set_ciphers()
can be deprecated and eventually removed, with only set_ciphersuites()
remaining.
@@ -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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My suggestion was:
def test_wrap_socket(sock, *,
cert_reqs=ssl.CERT_NONE, ca_certs=None,
ciphers=None, ciphersuites=None,
min_version=None, max_version=None,
...):
...
if min_version is not None:
context.minimum_version = min_version
if max_version is not None:
context.maximum_version = max_version
...
That way, we can set maximum_version
in tests if needed. For now, we don't but in the future we could. For instance, a test where we call set_ciphersuites
but with context.maximum_version = ssl.TLSVersion.TLSv1_2
. Or is the maximum version actually ignored?
@@ -2238,6 +2243,53 @@ def test_transport_eof(self): | |||
self.assertRaises(ssl.SSLEOFError, sslobj.read) | |||
|
|||
|
|||
@requires_tls_version('TLSv1_3') | |||
class SimpleBackgroundTestsTLS_1_3(unittest.TestCase): | |||
"""Tests that connect to a simple server running in the background""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"""Tests that connect to a simple server running in the background""" | |
"""Tests that connect to a simple server running in the background.""" |
with self.assertRaises(ssl.SSLError): | ||
s.connect(self.server_addr) | ||
else: | ||
self.skipTest("Multiple TLS 1.3 ciphers are not available") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should make a separate test method for this one, otherwise the entire test is marked as skipped (but the two first checks must unconditionally pass). You can also swap the if
condition:
def test_mismatched_ciphersuites(self):
if self.matching_cipher == self.mismatched_cipher:
self.skipTest("only one TLS 1.3 cipher is available")
...
self.assertEqual(s.cipher()[0], self.matching_cipher) | ||
|
||
# Test mismatched TLS 1.3 cipher suites | ||
if self.matching_client != self.mismatched_cipher: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
matching_client
should be matching_cipher
right?
@@ -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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep it that way and change it in a follow-up PR instead. Actually, the errcode
parameter for _setSSLError
is redundant as it's always overwritten.
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/