diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index ff6053cb7e94d9..806db601cc3cc6 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -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 `_. + 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) + + 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 + 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 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/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 9f01b52f1aff3b..d0ffbe090da88d 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -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`) + tarfile ------- diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index b5263129baed3f..ea45afc8eff8a0 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, certfile=None, keyfile=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 @@ -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""" + + 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): 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. 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]*/