-
-
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?
Changes from all commits
5a1cc22
09534fd
6783fe6
d3375f1
48e5164
11b760e
eaae575
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -1684,19 +1684,30 @@ to speed up repeated connections from the same clients. | |||||
|
||||||
.. method:: SSLContext.set_ciphers(ciphers) | ||||||
|
||||||
Set the available ciphers for sockets created with this context. | ||||||
It should be a string in the `OpenSSL cipher list format | ||||||
Set the allowed ciphers for sockets created with this context when | ||||||
connecting using TLS 1.2 and earlier. The *ciphers* argument should | ||||||
be a string in the `OpenSSL cipher list format | ||||||
<https://docs.openssl.org/master/man1/ciphers/>`_. | ||||||
To set allowed TLS 1.3 ciphers, use :meth:`SSLContext.set_ciphersuites`. | ||||||
If no cipher can be selected (because compile-time options or other | ||||||
configuration forbids use of all the specified ciphers), an | ||||||
:class:`SSLError` will be raised. | ||||||
|
||||||
.. note:: | ||||||
when connected, the :meth:`SSLSocket.cipher` method of SSL sockets will | ||||||
give the currently selected cipher. | ||||||
When connected, the :meth:`SSLSocket.cipher` method of SSL sockets will | ||||||
return details about the negotiated cipher. | ||||||
|
||||||
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 commentThe reason will be displayed to describe this comment to others. Learn more. This needs a |
||||||
|
||||||
Set the allowed ciphers for sockets created with this context when | ||||||
connecting using TLS 1.3. The *ciphersuites* argument should be a | ||||||
colon-separate string of TLS 1.3 cipher names. If no cipher can be | ||||||
selected (because compile-time options or other configuration forbids | ||||||
use of all the specified ciphers), an :class:`SSLError` will be raised. | ||||||
|
||||||
.. note:: | ||||||
When connected, the :meth:`SSLSocket.cipher` method of SSL sockets will | ||||||
return details about the negotiated cipher. | ||||||
|
||||||
.. method:: SSLContext.set_groups(groups) | ||||||
|
||||||
|
@@ -2844,10 +2855,15 @@ TLS 1.3 | |||||
The TLS 1.3 protocol behaves slightly differently than previous version | ||||||
of TLS/SSL. Some new TLS 1.3 features are not yet available. | ||||||
|
||||||
- TLS 1.3 uses a disjunct set of cipher suites. All AES-GCM and | ||||||
ChaCha20 cipher suites are enabled by default. The method | ||||||
: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 commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
are allowed, the method :meth:`SSLContext.set_ciphersuites` should be | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For posterity: I really hate that OpenSSL named it There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||
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 commentThe 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". |
||||||
negotiated cipher for both TLS 1.3 and earlier versions once a connection | ||||||
is established. | ||||||
- Session tickets are no longer sent as part of the initial handshake and | ||||||
are handled differently. :attr:`SSLSocket.session` and :class:`SSLSession` | ||||||
are not compatible with TLS 1.3. | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -332,6 +332,13 @@ ssl | |||||
|
||||||
(Contributed by Ron Frederick in :gh:`136306`) | ||||||
|
||||||
* 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. | ||||||
(Contributed by Ron Frederick in :gh:`137197`) | ||||||
picnixz marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
You don't need to update the entry just above. We'll clean it up in a PR for #133879. |
||||||
|
||||||
|
||||||
tarfile | ||||||
------- | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more. Just for the future, but how about adding There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 commentThe 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 commentThe 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 |
||||||
certfile=None, keyfile=None, | ||||||
**kwargs): | ||||||
if not kwargs.get("server_side"): | ||||||
kwargs["server_hostname"] = SIGNED_CERTFILE_HOSTNAME | ||||||
|
@@ -280,6 +281,10 @@ def test_wrap_socket(sock, *, | |||||
context.load_cert_chain(certfile, keyfile) | ||||||
if ciphers is not None: | ||||||
context.set_ciphers(ciphers) | ||||||
if ciphersuites is not None: | ||||||
context.set_ciphersuites(ciphersuites) | ||||||
if min_version is not None: | ||||||
context.minimum_version = min_version | ||||||
return context.wrap_socket(sock, **kwargs) | ||||||
|
||||||
|
||||||
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
def setUp(self): | ||||||
ciphers = [cipher['name'] for cipher in ctx.get_ciphers() | ||||||
if cipher['protocol'] == 'TLSv1.3'] | ||||||
|
||||||
self.matching_cipher = ciphers[0] | ||||||
self.mismatched_cipher = ciphers[-1] | ||||||
|
||||||
self.server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) | ||||||
self.server_context.set_ciphersuites(self.matching_cipher) | ||||||
self.server_context.load_cert_chain(SIGNED_CERTFILE) | ||||||
server = ThreadedEchoServer(context=self.server_context) | ||||||
self.enterContext(server) | ||||||
self.server_addr = (HOST, server.port) | ||||||
|
||||||
def test_ciphersuites(self): | ||||||
# Test unrecognized TLS 1.3 cipher suite name | ||||||
with self.assertRaisesRegex(ssl.SSLError, | ||||||
"No cipher suite can be selected"): | ||||||
with socket.socket(socket.AF_INET) as sock: | ||||||
s = test_wrap_socket(sock, cert_reqs=ssl.CERT_NONE, | ||||||
ciphersuites="XXX", | ||||||
min_version=ssl.TLSVersion.TLSv1_3) | ||||||
|
||||||
# Test successful TLS 1.3 handshake | ||||||
with test_wrap_socket(socket.socket(socket.AF_INET), | ||||||
cert_reqs=ssl.CERT_NONE, | ||||||
ciphersuites=self.matching_cipher, | ||||||
min_version=ssl.TLSVersion.TLSv1_3) as s: | ||||||
s.connect(self.server_addr) | ||||||
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 commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
with test_wrap_socket(socket.socket(socket.AF_INET), | ||||||
cert_reqs=ssl.CERT_NONE, | ||||||
ciphersuites=self.mismatched_cipher, | ||||||
min_version=ssl.TLSVersion.TLSv1_3) as s: | ||||||
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 commentThe 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 def test_mismatched_ciphersuites(self):
if self.matching_cipher == self.mismatched_cipher:
self.skipTest("only one TLS 1.3 cipher is available")
... |
||||||
|
||||||
|
||||||
@support.requires_resource('network') | ||||||
class NetworkedTests(unittest.TestCase): | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
:mod:`ssl` can now set TLS 1.3 cipher suites. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 commentThe 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 commentThe 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 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 commentThe 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 |
||
otherwise the error will be reported again when another SSL call | ||
is done. */ | ||
ERR_clear_error(); | ||
PyErr_SetString(get_state_ctx(self)->PySSLErrorObject, | ||
"No cipher can be selected."); | ||
_setSSLError(get_state_ctx(self), "No cipher can be selected.", 0, __FILE__, __LINE__); | ||
return NULL; | ||
} | ||
Py_RETURN_NONE; | ||
} | ||
|
||
/*[clinic input] | ||
@critical_section | ||
_ssl._SSLContext.set_ciphersuites | ||
ciphersuites: str | ||
/ | ||
[clinic start generated code]*/ | ||
|
||
static PyObject * | ||
_ssl__SSLContext_set_ciphersuites_impl(PySSLContext *self, | ||
const char *ciphersuites) | ||
/*[clinic end generated code: output=9915bec58e54d76d input=2afcc3693392be41]*/ | ||
{ | ||
int ret = SSL_CTX_set_ciphersuites(self->ctx, ciphersuites); | ||
if (ret == 0) { | ||
_setSSLError(get_state_ctx(self), "No cipher suite can be selected.", 0, __FILE__, __LINE__); | ||
return NULL; | ||
} | ||
Py_RETURN_NONE; | ||
|
@@ -5583,6 +5598,7 @@ static struct PyMethodDef context_methods[] = { | |
_SSL__SSLCONTEXT__WRAP_SOCKET_METHODDEF | ||
_SSL__SSLCONTEXT__WRAP_BIO_METHODDEF | ||
_SSL__SSLCONTEXT_SET_CIPHERS_METHODDEF | ||
_SSL__SSLCONTEXT_SET_CIPHERSUITES_METHODDEF | ||
_SSL__SSLCONTEXT_SET_GROUPS_METHODDEF | ||
_SSL__SSLCONTEXT__SET_ALPN_PROTOCOLS_METHODDEF | ||
_SSL__SSLCONTEXT_LOAD_CERT_CHAIN_METHODDEF | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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.