Skip to content

Commit bacb777

Browse files
ronfpicnixz
andauthored
gh-137197: Add SSLContext.set_ciphersuites to set TLSv1.3 ciphers (#137198)
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
1 parent 11217a8 commit bacb777

File tree

6 files changed

+169
-12
lines changed

6 files changed

+169
-12
lines changed

Doc/library/ssl.rst

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,19 +1684,33 @@ to speed up repeated connections from the same clients.
16841684

16851685
.. method:: SSLContext.set_ciphers(ciphers)
16861686

1687-
Set the available ciphers for sockets created with this context.
1688-
It should be a string in the `OpenSSL cipher list format
1687+
Set the allowed ciphers for sockets created with this context when
1688+
connecting using TLS 1.2 and earlier. The *ciphers* argument should
1689+
be a string in the `OpenSSL cipher list format
16891690
<https://docs.openssl.org/master/man1/ciphers/>`_.
1691+
To set allowed TLS 1.3 ciphers, use :meth:`SSLContext.set_ciphersuites`.
1692+
16901693
If no cipher can be selected (because compile-time options or other
16911694
configuration forbids use of all the specified ciphers), an
16921695
:class:`SSLError` will be raised.
16931696

16941697
.. note::
1695-
when connected, the :meth:`SSLSocket.cipher` method of SSL sockets will
1696-
give the currently selected cipher.
1698+
When connected, the :meth:`SSLSocket.cipher` method of SSL sockets will
1699+
return details about the negotiated cipher.
1700+
1701+
.. method:: SSLContext.set_ciphersuites(ciphersuites)
1702+
1703+
Set the allowed ciphers for sockets created with this context when
1704+
connecting using TLS 1.3. The *ciphersuites* argument should be a
1705+
colon-separate string of TLS 1.3 cipher names. If no cipher can be
1706+
selected (because compile-time options or other configuration forbids
1707+
use of all the specified ciphers), an :class:`SSLError` will be raised.
1708+
1709+
.. note::
1710+
When connected, the :meth:`SSLSocket.cipher` method of SSL sockets will
1711+
return details about the negotiated cipher.
16971712

1698-
TLS 1.3 cipher suites cannot be disabled with
1699-
:meth:`~SSLContext.set_ciphers`.
1713+
.. versionadded:: next
17001714

17011715
.. method:: SSLContext.set_groups(groups)
17021716

@@ -2845,10 +2859,15 @@ TLS 1.3
28452859
The TLS 1.3 protocol behaves slightly differently than previous version
28462860
of TLS/SSL. Some new TLS 1.3 features are not yet available.
28472861

2848-
- TLS 1.3 uses a disjunct set of cipher suites. All AES-GCM and
2849-
ChaCha20 cipher suites are enabled by default. The method
2850-
:meth:`SSLContext.set_ciphers` cannot enable or disable any TLS 1.3
2851-
ciphers yet, but :meth:`SSLContext.get_ciphers` returns them.
2862+
- TLS 1.3 uses a disjunct set of cipher suites. All AES-GCM and ChaCha20
2863+
cipher suites are enabled by default. To restrict which TLS 1.3 ciphers
2864+
are allowed, the :meth:`SSLContext.set_ciphersuites` method should be
2865+
called instead of :meth:`SSLContext.set_ciphers`, which only affects
2866+
ciphers in older TLS versions. The :meth:`SSLContext.get_ciphers` method
2867+
returns information about ciphers for both TLS 1.3 and earlier versions
2868+
and the method :meth:`SSLSocket.cipher` returns information about the
2869+
negotiated cipher for both TLS 1.3 and earlier versions once a connection
2870+
is established.
28522871
- Session tickets are no longer sent as part of the initial handshake and
28532872
are handled differently. :attr:`SSLSocket.session` and :class:`SSLSession`
28542873
are not compatible with TLS 1.3.

Doc/whatsnew/3.15.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,13 @@ ssl
426426

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

429+
* Added a new method :meth:`ssl.SSLContext.set_ciphersuites` for setting TLS 1.3
430+
ciphers. For TLS 1.2 or earlier, :meth:`ssl.SSLContext.set_ciphers` should
431+
continue to be used. Both calls can be made on the same context and the
432+
selected cipher suite will depend on the TLS version negotiated when a
433+
connection is made.
434+
(Contributed by Ron Frederick in :gh:`137197`.)
435+
429436

430437
tarfile
431438
-------

Lib/test/test_ssl.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,9 @@ def utc_offset(): #NOTE: ignore issues like #1647654
263263

264264
def test_wrap_socket(sock, *,
265265
cert_reqs=ssl.CERT_NONE, ca_certs=None,
266-
ciphers=None, certfile=None, keyfile=None,
266+
ciphers=None, ciphersuites=None,
267+
min_version=None, max_version=None,
268+
certfile=None, keyfile=None,
267269
**kwargs):
268270
if not kwargs.get("server_side"):
269271
kwargs["server_hostname"] = SIGNED_CERTFILE_HOSTNAME
@@ -280,6 +282,12 @@ def test_wrap_socket(sock, *,
280282
context.load_cert_chain(certfile, keyfile)
281283
if ciphers is not None:
282284
context.set_ciphers(ciphers)
285+
if ciphersuites is not None:
286+
context.set_ciphersuites(ciphersuites)
287+
if min_version is not None:
288+
context.minimum_version = min_version
289+
if max_version is not None:
290+
context.maximum_version = max_version
283291
return context.wrap_socket(sock, **kwargs)
284292

285293

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

22402248

2249+
@unittest.skipUnless(has_tls_version('TLSv1_3'), "TLS 1.3 is not available")
2250+
class SimpleBackgroundTestsTLS_1_3(unittest.TestCase):
2251+
"""Tests that connect to a simple server running in the background."""
2252+
2253+
def setUp(self):
2254+
server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
2255+
ciphers = [cipher['name'] for cipher in server_ctx.get_ciphers()
2256+
if cipher['protocol'] == 'TLSv1.3']
2257+
2258+
if not ciphers:
2259+
self.skipTest("No cipher supports TLSv1.3")
2260+
2261+
self.matching_cipher = ciphers[0]
2262+
# Some tests need at least two ciphers, and are responsible
2263+
# to skip themselves if matching_cipher == mismatched_cipher.
2264+
self.mismatched_cipher = ciphers[-1]
2265+
2266+
server_ctx.set_ciphersuites(self.matching_cipher)
2267+
server_ctx.load_cert_chain(SIGNED_CERTFILE)
2268+
server = ThreadedEchoServer(context=server_ctx)
2269+
self.enterContext(server)
2270+
self.server_addr = (HOST, server.port)
2271+
2272+
def test_ciphersuites(self):
2273+
# Test unrecognized TLS 1.3 cipher suite name
2274+
with (
2275+
socket.socket(socket.AF_INET) as sock,
2276+
self.assertRaisesRegex(ssl.SSLError,
2277+
"No cipher suite can be selected")
2278+
):
2279+
test_wrap_socket(sock, cert_reqs=ssl.CERT_NONE,
2280+
ciphersuites="XXX",
2281+
min_version=ssl.TLSVersion.TLSv1_3)
2282+
2283+
# Test successful TLS 1.3 handshake
2284+
with test_wrap_socket(socket.socket(socket.AF_INET),
2285+
cert_reqs=ssl.CERT_NONE,
2286+
ciphersuites=self.matching_cipher,
2287+
min_version=ssl.TLSVersion.TLSv1_3) as s:
2288+
s.connect(self.server_addr)
2289+
self.assertEqual(s.cipher()[0], self.matching_cipher)
2290+
2291+
def test_ciphersuite_downgrade(self):
2292+
with test_wrap_socket(socket.socket(socket.AF_INET),
2293+
cert_reqs=ssl.CERT_NONE,
2294+
ciphersuites=self.matching_cipher,
2295+
min_version=ssl.TLSVersion.TLSv1_2,
2296+
max_version=ssl.TLSVersion.TLSv1_2) as s:
2297+
s.connect(self.server_addr)
2298+
self.assertEqual(s.cipher()[1], 'TLSv1.2')
2299+
2300+
def test_ciphersuite_mismatch(self):
2301+
if self.matching_cipher == self.mismatched_cipher:
2302+
self.skipTest("Multiple TLS 1.3 ciphers are not available")
2303+
2304+
with test_wrap_socket(socket.socket(socket.AF_INET),
2305+
cert_reqs=ssl.CERT_NONE,
2306+
ciphersuites=self.mismatched_cipher,
2307+
min_version=ssl.TLSVersion.TLSv1_3) as s:
2308+
self.assertRaises(ssl.SSLError, s.connect, self.server_addr)
2309+
2310+
22412311
@support.requires_resource('network')
22422312
class NetworkedTests(unittest.TestCase):
22432313

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:class:`~ssl.SSLContext` objects can now set TLS 1.3 cipher suites
2+
via :meth:`~ssl.SSLContext.set_ciphersuites`.

Modules/_ssl.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3614,6 +3614,25 @@ _ssl__SSLContext_set_ciphers_impl(PySSLContext *self, const char *cipherlist)
36143614
Py_RETURN_NONE;
36153615
}
36163616

3617+
/*[clinic input]
3618+
@critical_section
3619+
_ssl._SSLContext.set_ciphersuites
3620+
ciphersuites: str
3621+
/
3622+
[clinic start generated code]*/
3623+
3624+
static PyObject *
3625+
_ssl__SSLContext_set_ciphersuites_impl(PySSLContext *self,
3626+
const char *ciphersuites)
3627+
/*[clinic end generated code: output=9915bec58e54d76d input=2afcc3693392be41]*/
3628+
{
3629+
if (!SSL_CTX_set_ciphersuites(self->ctx, ciphersuites)) {
3630+
_setSSLError(get_state_ctx(self), "No cipher suite can be selected.", 0, __FILE__, __LINE__);
3631+
return NULL;
3632+
}
3633+
Py_RETURN_NONE;
3634+
}
3635+
36173636
/*[clinic input]
36183637
@critical_section
36193638
_ssl._SSLContext.get_ciphers
@@ -5595,6 +5614,7 @@ static struct PyMethodDef context_methods[] = {
55955614
_SSL__SSLCONTEXT__WRAP_SOCKET_METHODDEF
55965615
_SSL__SSLCONTEXT__WRAP_BIO_METHODDEF
55975616
_SSL__SSLCONTEXT_SET_CIPHERS_METHODDEF
5617+
_SSL__SSLCONTEXT_SET_CIPHERSUITES_METHODDEF
55985618
_SSL__SSLCONTEXT_SET_GROUPS_METHODDEF
55995619
_SSL__SSLCONTEXT__SET_ALPN_PROTOCOLS_METHODDEF
56005620
_SSL__SSLCONTEXT_LOAD_CERT_CHAIN_METHODDEF

Modules/clinic/_ssl.c.h

Lines changed: 40 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)