From 5a1cc22eb6a3c4fef84c76330542316accf03e3d Mon Sep 17 00:00:00 2001 From: Ron Frederick Date: Mon, 28 Jul 2025 21:58:42 -0700 Subject: [PATCH 01/11] gh-137197: Add SSLContext.set_ciphersuites to set TLS 1.3 ciphers --- Doc/library/ssl.rst | 36 +++++++++++++++++++++++++----------- Doc/whatsnew/3.15.rst | 5 +++++ Lib/test/test_ssl.py | 33 +++++++++++++++++++++++++++++---- Modules/_ssl.c | 28 ++++++++++++++++++++++------ Modules/clinic/_ssl.c.h | 41 ++++++++++++++++++++++++++++++++++++++++- 5 files changed, 121 insertions(+), 22 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index ff6053cb7e94d9..58eb9ac64c5aa8 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1684,19 +1684,29 @@ 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 - `_. - If no cipher can be selected (because compile-time options or other + Set the allowed ciphers for sockets created with this context when + connecting using TLS 1.2 and earlier. It should be a string in the `OpenSSL + cipher list format `_. + To set allowed TLS 1.3 ciphers, use :meth:`SSHContext.set_ciphersuites`. + below. 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. + return the negotiated cipher and associated TLS version. - TLS 1.3 cipher suites cannot be disabled with - :meth:`~SSLContext.set_ciphers`. +.. method:: SSLContext.set_ciphersuites(ciphersuites) + + Set the allowed ciphers for sockets created with this context when + connecting using TLS 1.3. It 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 the negotiated cipher and associated TLS version. .. method:: SSLContext.set_groups(groups) @@ -2844,10 +2854,14 @@ 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 + are allowed, the method :meth:`SSLContext.set_ciphersuites` should be + 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 the negotiated cipher and + the associated TLS version 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. diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 9f01b52f1aff3b..0aec043364942a 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -332,6 +332,11 @@ ssl (Contributed by Ron Frederick in :gh:`136306`) +* 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. + (Contributed by Ron Frederick in :gh:`137197`) tarfile ------- diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index b5263129baed3f..c8457e8f75a876 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -261,10 +261,9 @@ 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, - **kwargs): +def test_wrap_socket(sock, *, cert_reqs=ssl.CERT_NONE, ca_certs=None, + ciphers=None, ciphersuites=None, min_version=None, + certfile=None, keyfile=None, **kwargs): if not kwargs.get("server_side"): kwargs["server_hostname"] = SIGNED_CERTFILE_HOSTNAME context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) @@ -280,6 +279,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) @@ -2109,6 +2112,28 @@ def test_ciphers(self): cert_reqs=ssl.CERT_NONE, ciphers="^$:,;?*'dorothyx") s.connect(self.server_addr) + def test_ciphersuites(self): + with test_wrap_socket(socket.socket(socket.AF_INET), + cert_reqs=ssl.CERT_NONE, + min_version=ssl.TLSVersion.TLSv1_3) as s: + s.connect(self.server_addr) + self.assertEqual(s.cipher()[1], "TLSv1.3") + with test_wrap_socket(socket.socket(socket.AF_INET), + cert_reqs=ssl.CERT_NONE, + ciphersuites="TLS_AES_256_GCM_SHA384", + min_version=ssl.TLSVersion.TLSv1_3) as s: + s.connect(self.server_addr) + self.assertEqual(s.cipher(), + ("TLS_AES_256_GCM_SHA384", "TLSv1.3", 256)) + # Error checking can happen at instantiation or when connecting + 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) + s.connect(self.server_addr) + def test_get_ca_certs_capath(self): # capath certs are loaded on request ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) diff --git a/Modules/_ssl.c b/Modules/_ssl.c index ab30258faf3f62..348fa3e054eabf 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -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, - 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 diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h index 5b80fab0abb45e..e8b51c1f1e326d 100644 --- a/Modules/clinic/_ssl.c.h +++ b/Modules/clinic/_ssl.c.h @@ -969,6 +969,45 @@ _ssl__SSLContext_set_ciphers(PyObject *self, PyObject *arg) return return_value; } +PyDoc_STRVAR(_ssl__SSLContext_set_ciphersuites__doc__, +"set_ciphersuites($self, ciphersuites, /)\n" +"--\n" +"\n"); + +#define _SSL__SSLCONTEXT_SET_CIPHERSUITES_METHODDEF \ + {"set_ciphersuites", (PyCFunction)_ssl__SSLContext_set_ciphersuites, METH_O, _ssl__SSLContext_set_ciphersuites__doc__}, + +static PyObject * +_ssl__SSLContext_set_ciphersuites_impl(PySSLContext *self, + const char *ciphersuites); + +static PyObject * +_ssl__SSLContext_set_ciphersuites(PyObject *self, PyObject *arg) +{ + PyObject *return_value = NULL; + const char *ciphersuites; + + if (!PyUnicode_Check(arg)) { + _PyArg_BadArgument("set_ciphersuites", "argument", "str", arg); + goto exit; + } + Py_ssize_t ciphersuites_length; + ciphersuites = PyUnicode_AsUTF8AndSize(arg, &ciphersuites_length); + if (ciphersuites == NULL) { + goto exit; + } + if (strlen(ciphersuites) != (size_t)ciphersuites_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } + Py_BEGIN_CRITICAL_SECTION(self); + return_value = _ssl__SSLContext_set_ciphersuites_impl((PySSLContext *)self, ciphersuites); + Py_END_CRITICAL_SECTION(); + +exit: + return return_value; +} + PyDoc_STRVAR(_ssl__SSLContext_get_ciphers__doc__, "get_ciphers($self, /)\n" "--\n" @@ -3142,4 +3181,4 @@ _ssl_enum_crls(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObje #ifndef _SSL_ENUM_CRLS_METHODDEF #define _SSL_ENUM_CRLS_METHODDEF #endif /* !defined(_SSL_ENUM_CRLS_METHODDEF) */ -/*[clinic end generated code: output=c409bdf3c123b28b input=a9049054013a1b77]*/ +/*[clinic end generated code: output=4e35d2ea2fc46023 input=a9049054013a1b77]*/ From 09534fdb552fe8f6641607903578a417d030c3f1 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 05:12:54 +0000 Subject: [PATCH 02/11] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-07-29-05-12-50.gh-issue-137197.bMK3sO.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-07-29-05-12-50.gh-issue-137197.bMK3sO.rst diff --git a/Misc/NEWS.d/next/Library/2025-07-29-05-12-50.gh-issue-137197.bMK3sO.rst b/Misc/NEWS.d/next/Library/2025-07-29-05-12-50.gh-issue-137197.bMK3sO.rst new file mode 100644 index 00000000000000..592a1f6914fc82 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-29-05-12-50.gh-issue-137197.bMK3sO.rst @@ -0,0 +1 @@ +:mod:`ssl` can now set TLS 1.3 cipher suites. From 6783fe6ce3e35c533cbee9e25d39d9535ff675b7 Mon Sep 17 00:00:00 2001 From: Ron Frederick Date: Mon, 28 Jul 2025 22:57:20 -0700 Subject: [PATCH 03/11] gh-137197: Fix typo in documentation --- Doc/library/ssl.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 58eb9ac64c5aa8..7a21f52ecdbc08 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1687,7 +1687,7 @@ to speed up repeated connections from the same clients. Set the allowed ciphers for sockets created with this context when connecting using TLS 1.2 and earlier. It should be a string in the `OpenSSL cipher list format `_. - To set allowed TLS 1.3 ciphers, use :meth:`SSHContext.set_ciphersuites`. + To set allowed TLS 1.3 ciphers, use :meth:`SSLContext.set_ciphersuites`. below. 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. From d3375f1b541054531d491ca27d208413427c5742 Mon Sep 17 00:00:00 2001 From: Ron Frederick Date: Fri, 8 Aug 2025 17:20:47 -0700 Subject: [PATCH 04/11] gh-137197: Update what's new text 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. --- Doc/whatsnew/3.15.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 0aec043364942a..d0ffbe090da88d 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -333,11 +333,13 @@ ssl (Contributed by Ron Frederick in :gh:`136306`) * 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. + 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`) + tarfile ------- From 48e5164e8487c42edb4f770c536d2efd1cb97a34 Mon Sep 17 00:00:00 2001 From: Ron Frederick Date: Sat, 9 Aug 2025 09:16:49 -0700 Subject: [PATCH 05/11] gh-137197: Address review comments --- Doc/library/ssl.rst | 24 +++++++++++++----------- Lib/test/test_ssl.py | 29 ++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 7a21f52ecdbc08..057108e6778201 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1685,28 +1685,29 @@ to speed up repeated connections from the same clients. .. method:: SSLContext.set_ciphers(ciphers) Set the allowed ciphers for sockets created with this context when - connecting using TLS 1.2 and earlier. It should be a string in the `OpenSSL - cipher list format `_. + connecting using TLS 1.2 and earlier. The *ciphers* argument should + be a string in the `OpenSSL cipher list format + `_. To set allowed TLS 1.3 ciphers, use :meth:`SSLContext.set_ciphersuites`. - below. If no cipher can be selected (because compile-time options or other + 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 the negotiated cipher and associated TLS version. + return details about the negotiated cipher. .. method:: SSLContext.set_ciphersuites(ciphersuites) Set the allowed ciphers for sockets created with this context when - connecting using TLS 1.3. It 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. + 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 the negotiated cipher and associated TLS version. + return details about the negotiated cipher. .. method:: SSLContext.set_groups(groups) @@ -2860,8 +2861,9 @@ of TLS/SSL. Some new TLS 1.3 features are not yet available. 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 the negotiated cipher and - the associated TLS version once a connection is established. + and the method :meth:`SSLSocket.cipher` returns information about the + 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. diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index c8457e8f75a876..f969e5c2e039e3 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -261,9 +261,11 @@ def utc_offset(): #NOTE: ignore issues like #1647654 ) -def test_wrap_socket(sock, *, cert_reqs=ssl.CERT_NONE, ca_certs=None, +def test_wrap_socket(sock, *, + cert_reqs=ssl.CERT_NONE, ca_certs=None, ciphers=None, ciphersuites=None, min_version=None, - certfile=None, keyfile=None, **kwargs): + certfile=None, keyfile=None, + **kwargs): if not kwargs.get("server_side"): kwargs["server_hostname"] = SIGNED_CERTFILE_HOSTNAME context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) @@ -1866,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') + self.server_context.load_cert_chain(SIGNED_CERTFILE) server = ThreadedEchoServer(context=self.server_context) self.enterContext(server) @@ -2112,12 +2118,9 @@ def test_ciphers(self): cert_reqs=ssl.CERT_NONE, ciphers="^$:,;?*'dorothyx") s.connect(self.server_addr) + @requires_tls_version('TLSv1_3') def test_ciphersuites(self): - with test_wrap_socket(socket.socket(socket.AF_INET), - cert_reqs=ssl.CERT_NONE, - min_version=ssl.TLSVersion.TLSv1_3) as s: - s.connect(self.server_addr) - self.assertEqual(s.cipher()[1], "TLSv1.3") + # Test successful TLS 1.3 handshake with test_wrap_socket(socket.socket(socket.AF_INET), cert_reqs=ssl.CERT_NONE, ciphersuites="TLS_AES_256_GCM_SHA384", @@ -2125,14 +2128,22 @@ def test_ciphersuites(self): s.connect(self.server_addr) self.assertEqual(s.cipher(), ("TLS_AES_256_GCM_SHA384", "TLSv1.3", 256)) - # Error checking can happen at instantiation or when connecting + + # Test mismatched TLS 1.3 cipher suites + with test_wrap_socket(socket.socket(socket.AF_INET), + cert_reqs=ssl.CERT_NONE, + ciphersuites="TLS_AES_128_GCM_SHA256", + min_version=ssl.TLSVersion.TLSv1_3) as s: + with self.assertRaises(ssl.SSLError): + s.connect(self.server_addr) + + # 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) - s.connect(self.server_addr) def test_get_ca_certs_capath(self): # capath certs are loaded on request From 11b760e0a93229943ad717597502e4af6cead918 Mon Sep 17 00:00:00 2001 From: Ron Frederick Date: Sat, 9 Aug 2025 13:23:56 -0700 Subject: [PATCH 06/11] gh-137197: Rework test cases 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. --- Lib/test/test_ssl.py | 78 ++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index f969e5c2e039e3..ea45afc8eff8a0 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -1868,10 +1868,6 @@ 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') - self.server_context.load_cert_chain(SIGNED_CERTFILE) server = ThreadedEchoServer(context=self.server_context) self.enterContext(server) @@ -2118,33 +2114,6 @@ def test_ciphers(self): cert_reqs=ssl.CERT_NONE, ciphers="^$:,;?*'dorothyx") s.connect(self.server_addr) - @requires_tls_version('TLSv1_3') - def test_ciphersuites(self): - # Test successful TLS 1.3 handshake - with test_wrap_socket(socket.socket(socket.AF_INET), - cert_reqs=ssl.CERT_NONE, - ciphersuites="TLS_AES_256_GCM_SHA384", - min_version=ssl.TLSVersion.TLSv1_3) as s: - s.connect(self.server_addr) - self.assertEqual(s.cipher(), - ("TLS_AES_256_GCM_SHA384", "TLSv1.3", 256)) - - # Test mismatched TLS 1.3 cipher suites - with test_wrap_socket(socket.socket(socket.AF_INET), - cert_reqs=ssl.CERT_NONE, - ciphersuites="TLS_AES_128_GCM_SHA256", - min_version=ssl.TLSVersion.TLSv1_3) as s: - with self.assertRaises(ssl.SSLError): - s.connect(self.server_addr) - - # 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) - def test_get_ca_certs_capath(self): # capath certs are loaded on request ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) @@ -2274,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""" + + 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: + 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") + + @support.requires_resource('network') class NetworkedTests(unittest.TestCase): From eaae5758a659f91499f613faad5f13348403ee09 Mon Sep 17 00:00:00 2001 From: Ron Frederick Date: Wed, 13 Aug 2025 17:08:11 -0700 Subject: [PATCH 07/11] gh-137197: Fix capitalization in docs --- Doc/library/ssl.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 057108e6778201..806db601cc3cc6 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1694,7 +1694,7 @@ to speed up repeated connections from the same clients. :class:`SSLError` will be raised. .. note:: - when connected, the :meth:`SSLSocket.cipher` method of SSL sockets will + When connected, the :meth:`SSLSocket.cipher` method of SSL sockets will return details about the negotiated cipher. .. method:: SSLContext.set_ciphersuites(ciphersuites) @@ -1706,7 +1706,7 @@ to speed up repeated connections from the same clients. use of all the specified ciphers), an :class:`SSLError` will be raised. .. note:: - when connected, the :meth:`SSLSocket.cipher` method of SSL sockets will + When connected, the :meth:`SSLSocket.cipher` method of SSL sockets will return details about the negotiated cipher. .. method:: SSLContext.set_groups(groups) From e09017f5407198619b815155e0bb0eb52f434555 Mon Sep 17 00:00:00 2001 From: Ron Frederick Date: Wed, 20 Aug 2025 19:56:54 -0700 Subject: [PATCH 08/11] gh-137197: Address review comments --- Doc/library/ssl.rst | 7 +++++-- Lib/test/test_ssl.py | 25 ++++++++++++++----------- Modules/_ssl.c | 7 ++++++- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 806db601cc3cc6..4321b95e0711d2 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1689,6 +1689,7 @@ to speed up repeated connections from the same clients. be a string in the `OpenSSL cipher list format `_. 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. @@ -1709,6 +1710,8 @@ to speed up repeated connections from the same clients. When connected, the :meth:`SSLSocket.cipher` method of SSL sockets will return details about the negotiated cipher. + .. versionadded:: next + .. method:: SSLContext.set_groups(groups) Set the groups allowed for key agreement for sockets created with this @@ -2856,10 +2859,10 @@ 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. To restrict which TLS1.3 ciphers + cipher suites are enabled by default. To restrict which TLS 1.3 ciphers are allowed, the method :meth:`SSLContext.set_ciphersuites` should be called instead of :meth:`SSLContext.set_ciphers`, which only affects - ciphers in older TLS versions. The method :meth:`SSLContext.get_ciphers` + ciphers in older TLS versions. The :meth:`SSLContext.get_ciphers` method returns information about ciphers for both TLS 1.3 and earlier versions and the method :meth:`SSLSocket.cipher` returns information about the negotiated cipher for both TLS 1.3 and earlier versions once a connection diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index ea45afc8eff8a0..4adc69204d9618 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -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, ciphersuites=None, min_version=None, + ciphers=None, ciphersuites=None, + min_version=None, max_version=None, certfile=None, keyfile=None, **kwargs): if not kwargs.get("server_side"): @@ -285,6 +286,8 @@ def test_wrap_socket(sock, *, context.set_ciphersuites(ciphersuites) if min_version is not None: context.minimum_version = min_version + if max_version is not None: + context.maximum_version = max_version return context.wrap_socket(sock, **kwargs) @@ -2245,7 +2248,7 @@ def test_transport_eof(self): @requires_tls_version('TLSv1_3') class SimpleBackgroundTestsTLS_1_3(unittest.TestCase): - """Tests that connect to a simple server running in the background""" + """Tests that connect to a simple server running in the background.""" def setUp(self): ciphers = [cipher['name'] for cipher in ctx.get_ciphers() @@ -2278,17 +2281,17 @@ def test_ciphersuites(self): 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: - 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: + def test_ciphersuite_mismatch(self): + if self.matching_cipher == self.mismatched_cipher: self.skipTest("Multiple TLS 1.3 ciphers are not available") + 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) + @support.requires_resource('network') class NetworkedTests(unittest.TestCase): diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 348fa3e054eabf..7adc75f1d6d7cd 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -3595,7 +3595,12 @@ _ssl__SSLContext_set_ciphers_impl(PySSLContext *self, const char *cipherlist) { int ret = SSL_CTX_set_cipher_list(self->ctx, cipherlist); if (ret == 0) { - _setSSLError(get_state_ctx(self), "No cipher can be selected.", 0, __FILE__, __LINE__); + /* Clearing the error queue is necessary on some OpenSSL versions, + 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."); return NULL; } Py_RETURN_NONE; From caf6675845c0b17c4f2bbad788ecd307de64925a Mon Sep 17 00:00:00 2001 From: Ron Frederick Date: Fri, 29 Aug 2025 10:04:13 -0700 Subject: [PATCH 09/11] gh-137197: Address review comments --- Doc/library/ssl.rst | 2 +- Doc/whatsnew/3.15.rst | 2 +- Lib/test/test_ssl.py | 42 ++++++++++++------- ...-07-29-05-12-50.gh-issue-137197.bMK3sO.rst | 3 +- Modules/_ssl.c | 3 +- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 4321b95e0711d2..333099fac05d57 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -2860,7 +2860,7 @@ 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. To restrict which TLS 1.3 ciphers - are allowed, the method :meth:`SSLContext.set_ciphersuites` should be + are allowed, the :meth:`SSLContext.set_ciphersuites` method should be called instead of :meth:`SSLContext.set_ciphers`, which only affects ciphers in older TLS versions. The :meth:`SSLContext.get_ciphers` method returns information about ciphers for both TLS 1.3 and earlier versions diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index d0ffbe090da88d..7af93661e6e609 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -337,7 +337,7 @@ ssl 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`) + (Contributed by Ron Frederick in :gh:`137197`.) tarfile diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 4adc69204d9618..7d79a505031d1f 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -2246,32 +2246,38 @@ 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.""" + @requires_tls_version('TLSv1_3') def setUp(self): - ciphers = [cipher['name'] for cipher in ctx.get_ciphers() + server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ciphers = [cipher['name'] for cipher in server_ctx.get_ciphers() if cipher['protocol'] == 'TLSv1.3'] + if not ciphers: + self.skipTest("No cipher supports TLSv1.3") + self.matching_cipher = ciphers[0] + # Some tests need at least two ciphers. 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) + server_ctx.set_ciphersuites(self.matching_cipher) + server_ctx.load_cert_chain(SIGNED_CERTFILE) + server = ThreadedEchoServer(context=server_ctx) 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) + with ( + socket.socket(socket.AF_INET) as sock, + self.assertRaisesRegex(ssl.SSLError, + "No cipher suite can be selected") + ): + 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), @@ -2281,6 +2287,15 @@ def test_ciphersuites(self): s.connect(self.server_addr) self.assertEqual(s.cipher()[0], self.matching_cipher) + def test_ciphersuite_downgrade(self): + with test_wrap_socket(socket.socket(socket.AF_INET), + cert_reqs=ssl.CERT_NONE, + ciphersuites=self.matching_cipher, + min_version=ssl.TLSVersion.TLSv1_2, + max_version=ssl.TLSVersion.TLSv1_2) as s: + s.connect(self.server_addr) + self.assertEqual(s.cipher()[1], 'TLSv1.2') + def test_ciphersuite_mismatch(self): if self.matching_cipher == self.mismatched_cipher: self.skipTest("Multiple TLS 1.3 ciphers are not available") @@ -2289,8 +2304,7 @@ def test_ciphersuite_mismatch(self): 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) + self.assertRaises(ssl.SSLError, s.connect, self.server_addr) @support.requires_resource('network') diff --git a/Misc/NEWS.d/next/Library/2025-07-29-05-12-50.gh-issue-137197.bMK3sO.rst b/Misc/NEWS.d/next/Library/2025-07-29-05-12-50.gh-issue-137197.bMK3sO.rst index 592a1f6914fc82..70275041897313 100644 --- a/Misc/NEWS.d/next/Library/2025-07-29-05-12-50.gh-issue-137197.bMK3sO.rst +++ b/Misc/NEWS.d/next/Library/2025-07-29-05-12-50.gh-issue-137197.bMK3sO.rst @@ -1 +1,2 @@ -:mod:`ssl` can now set TLS 1.3 cipher suites. +:class:`~ssl.SSLContext` objects can now set TLS 1.3 cipher suites +via :meth:`~ssl.SSLContext.set_ciphersuites`. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 7adc75f1d6d7cd..d54785d7e93947 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -3618,8 +3618,7 @@ _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) { + if (!SSL_CTX_set_ciphersuites(self->ctx, ciphersuites)) { _setSSLError(get_state_ctx(self), "No cipher suite can be selected.", 0, __FILE__, __LINE__); return NULL; } From fe8791dc6e14613bc9e28a1cd018994264188eea Mon Sep 17 00:00:00 2001 From: Ron Frederick Date: Fri, 29 Aug 2025 10:47:46 -0700 Subject: [PATCH 10/11] gh-137197: Address last review comment --- Lib/test/test_ssl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 7d79a505031d1f..2f338509d157ba 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -2246,10 +2246,10 @@ def test_transport_eof(self): self.assertRaises(ssl.SSLEOFError, sslobj.read) +@unittest.skipUnless(has_tls_version('TLSv1_3'), "TLS 1.3 is not available") class SimpleBackgroundTestsTLS_1_3(unittest.TestCase): """Tests that connect to a simple server running in the background.""" - @requires_tls_version('TLSv1_3') def setUp(self): server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ciphers = [cipher['name'] for cipher in server_ctx.get_ciphers() From 45664787931bffb8ba0903cc43edcb0c8a2a15af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 30 Aug 2025 11:07:00 +0200 Subject: [PATCH 11/11] Apply suggestions from code review --- Doc/whatsnew/3.15.rst | 2 +- Lib/test/test_ssl.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 7af93661e6e609..327cc79f37672a 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -332,7 +332,7 @@ ssl (Contributed by Ron Frederick in :gh:`136306`) -* Added new method :meth:`ssl.SSLContext.set_ciphersuites` for setting TLS 1.3 +* Added a 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 diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 2f338509d157ba..e14f19671d4ac4 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -2259,7 +2259,8 @@ def setUp(self): self.skipTest("No cipher supports TLSv1.3") self.matching_cipher = ciphers[0] - # Some tests need at least two ciphers. + # Some tests need at least two ciphers, and are responsible + # to skip themselves if matching_cipher == mismatched_cipher. self.mismatched_cipher = ciphers[-1] server_ctx.set_ciphersuites(self.matching_cipher)