Skip to content
36 changes: 25 additions & 11 deletions Doc/library/ssl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
<https://docs.openssl.org/master/man1/ciphers/>`_.
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 <https://docs.openssl.org/master/man1/ciphers/>`_.
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.

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

Expand Down Expand Up @@ -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
Copy link
Member

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.

Copy link
Contributor Author

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.

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.
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 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
33 changes: 29 additions & 4 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)


Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:mod:`ssl` can now set TLS 1.3 cipher suites.
28 changes: 22 additions & 6 deletions Modules/_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Member

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

Copy link
Contributor Author

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;
}

Copy link
Member

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.

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;
Expand Down Expand Up @@ -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
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