Skip to content
39 changes: 29 additions & 10 deletions Doc/library/ssl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1684,19 +1684,33 @@ 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.

.. 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.

TLS 1.3 cipher suites cannot be disabled with
:meth:`~SSLContext.set_ciphers`.
.. versionadded:: next

.. method:: SSLContext.set_groups(groups)

Expand Down Expand Up @@ -2844,10 +2858,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 TLS 1.3 ciphers
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
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.
Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,13 @@ ssl

(Contributed by Ron Frederick in :gh:`136306`)

* 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
connection is made.
(Contributed by Ron Frederick in :gh:`137197`.)


tarfile
-------
Expand Down
72 changes: 71 additions & 1 deletion Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,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,
ciphers=None, ciphersuites=None,
min_version=None, max_version=None,
certfile=None, keyfile=None,
**kwargs):
if not kwargs.get("server_side"):
kwargs["server_hostname"] = SIGNED_CERTFILE_HOSTNAME
Expand All @@ -280,6 +282,12 @@ 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
if max_version is not None:
context.maximum_version = max_version
return context.wrap_socket(sock, **kwargs)


Expand Down Expand Up @@ -2238,6 +2246,68 @@ def test_transport_eof(self):
self.assertRaises(ssl.SSLEOFError, sslobj.read)


@unittest.skipUnless(has_tls_version('TLSv1_3'), "TLS 1.3 is not available")
Copy link
Member

Choose a reason for hiding this comment

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

Note to myself: change the requires_tls_version decorator so that it works.

class SimpleBackgroundTestsTLS_1_3(unittest.TestCase):
"""Tests that connect to a simple server running in the background."""

def setUp(self):
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, and are responsible
# to skip themselves if matching_cipher == mismatched_cipher.
self.mismatched_cipher = ciphers[-1]

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 (
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),
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)

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')
Copy link
Member

Choose a reason for hiding this comment

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

This test is asserting that the default TLSv1_2 cipher suite is actually being used right? I'm fine with the current test but I'd like to fortify it (which I'll do) in the future where we have a ciphersuite for TLSv1.3 and a ciphersuite for TLSv1.2 and if we use min/max versions, then we're going to be using the TLSv1.2 ciphersuite we specified.


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:
self.assertRaises(ssl.SSLError, s.connect, self.server_addr)


@support.requires_resource('network')
class NetworkedTests(unittest.TestCase):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:class:`~ssl.SSLContext` objects can now set TLS 1.3 cipher suites
via :meth:`~ssl.SSLContext.set_ciphersuites`.
20 changes: 20 additions & 0 deletions Modules/_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -3606,6 +3606,25 @@ _ssl__SSLContext_set_ciphers_impl(PySSLContext *self, const char *cipherlist)
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]*/
{
if (!SSL_CTX_set_ciphersuites(self->ctx, ciphersuites)) {
_setSSLError(get_state_ctx(self), "No cipher suite can be selected.", 0, __FILE__, __LINE__);
return NULL;
}
Py_RETURN_NONE;
}

/*[clinic input]
@critical_section
_ssl._SSLContext.get_ciphers
Expand Down Expand Up @@ -5583,6 +5602,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
Expand Down
41 changes: 40 additions & 1 deletion Modules/clinic/_ssl.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading