Skip to content

Commit 6401823

Browse files
ronfpicnixz
andauthored
gh-138252: Add support in SSL module for getting and setting TLS signature algorithms (#138269)
The signature algorithms allowed for certificate-based client authentication or for the server to complete the TLS handshake can be defined on a SSL context via `ctx.set_client_sigalgs()` and `ctx.set_server_sigalgs()`. With OpenSSL 3.4 or later, the list of available TLS algorithms can be retrieved by `ssl.get_sigalgs()`. With OpenSSL 3.5 or later, the selected signature algorithms can be retrieved from SSL sockets via `socket.client_sigalg()` and `socket.server_sigalg()`. This commit also partially amends 377b787 by using `PyUnicode_DecodeFSDefault` instead of `PyUnicode_DecodeASCII` in `_ssl._SSLContext.get_groups`, so that functions consistently decode strings obtained from OpenSSL. --------- Co-authored-by: Bénédikt Tran <[email protected]>
1 parent 919c7e8 commit 6401823

File tree

7 files changed

+497
-7
lines changed

7 files changed

+497
-7
lines changed

Doc/library/ssl.rst

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,25 @@ purposes.
215215
:data:`VERIFY_X509_STRICT` in its default verify flags.
216216

217217

218+
Signature algorithms
219+
^^^^^^^^^^^^^^^^^^^^
220+
221+
.. function:: get_sigalgs()
222+
223+
Return a list of available TLS signature algorithm names used
224+
by servers to complete the TLS handshake or clients requesting
225+
certificate-based authentication. For example::
226+
227+
>>> ssl.get_sigalgs() # doctest: +SKIP
228+
['ecdsa_secp256r1_sha256', 'ecdsa_secp384r1_sha384', ...]
229+
230+
These names can be used when building string values to pass to the
231+
:meth:`SSLContext.set_client_sigalgs` and
232+
:meth:`SSLContext.set_server_sigalgs` methods.
233+
234+
.. versionadded:: next
235+
236+
218237
Exceptions
219238
^^^^^^^^^^
220239

@@ -1297,6 +1316,22 @@ SSL sockets also have the following additional methods and attributes:
12971316

12981317
.. versionadded:: next
12991318

1319+
.. method:: SSLSocket.client_sigalg()
1320+
1321+
Return the signature algorithm used for performing certificate-based client
1322+
authentication on this connection, or ``None`` if no connection has been
1323+
established or client authentication didn't occur.
1324+
1325+
.. versionadded:: next
1326+
1327+
.. method:: SSLSocket.server_sigalg()
1328+
1329+
Return the signature algorithm used by the server to complete the TLS
1330+
handshake on this connection, or ``None`` if no connection has been
1331+
established or the cipher suite has no signature.
1332+
1333+
.. versionadded:: next
1334+
13001335
.. method:: SSLSocket.compression()
13011336

13021337
Return the compression algorithm being used as a string, or ``None``
@@ -1725,6 +1760,35 @@ to speed up repeated connections from the same clients.
17251760

17261761
.. versionadded:: next
17271762

1763+
.. method:: SSLContext.set_client_sigalgs(sigalgs)
1764+
1765+
Set the signature algorithms allowed for certificate-based client
1766+
authentication. It should be a string in the `OpenSSL client sigalgs
1767+
list format
1768+
<https://docs.openssl.org/master/man3/SSL_CTX_set1_client_sigalgs_list/>`_.
1769+
1770+
.. note::
1771+
1772+
When connected, the :meth:`SSLSocket.client_sigalg` method of SSL
1773+
sockets will return the signature algorithm used for performing
1774+
certificate-based client authentication on that connection.
1775+
1776+
.. versionadded:: next
1777+
1778+
.. method:: SSLContext.set_server_sigalgs(sigalgs)
1779+
1780+
Set the signature algorithms allowed for the server to complete the TLS
1781+
handshake. It should be a string in the `OpenSSL sigalgs list format
1782+
<https://docs.openssl.org/master/man3/SSL_CTX_set1_sigalgs_list/>`_.
1783+
1784+
.. note::
1785+
1786+
When connected, the :meth:`SSLSocket.server_sigalg` method of SSL
1787+
sockets will return the signature algorithm used by the server to
1788+
complete the TLS handshake on that connection.
1789+
1790+
.. versionadded:: next
1791+
17281792
.. method:: SSLContext.set_alpn_protocols(protocols)
17291793

17301794
Specify which protocols the socket should advertise during the SSL/TLS
@@ -2876,7 +2940,7 @@ of TLS/SSL. Some new TLS 1.3 features are not yet available.
28762940
process certificate requests while they send or receive application data
28772941
from the server.
28782942
- TLS 1.3 features like early data, deferred TLS client cert request,
2879-
signature algorithm configuration, and rekeying are not supported yet.
2943+
and rekeying are not supported yet.
28802944

28812945

28822946
.. seealso::

Doc/whatsnew/3.15.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,23 @@ ssl
442442
connection is made.
443443
(Contributed by Ron Frederick in :gh:`137197`.)
444444

445+
* Added new methods for managing signature algorithms:
446+
447+
* :func:`ssl.get_sigalgs` returns a list of all available TLS signature
448+
algorithms. This call requires OpenSSL 3.4 or later.
449+
* :meth:`ssl.SSLContext.set_client_sigalgs` sets the signature algorithms
450+
allowed for certificate-based client authentication.
451+
* :meth:`ssl.SSLContext.set_server_sigalgs` sets the signature algorithms
452+
allowed for the server to complete the TLS handshake.
453+
* :meth:`ssl.SSLSocket.client_sigalg` returns the signature algorithm
454+
selected for client authentication on the current connection. This call
455+
requires OpenSSL 3.5 or later.
456+
* :meth:`ssl.SSLSocket.server_sigalg` returns the signature algorithm
457+
selected for the server to complete the TLS handshake on the current
458+
connection. This call requires OpenSSL 3.5 or later.
459+
460+
(Contributed by Ron Frederick in :gh:`138252`.)
461+
445462

446463
tarfile
447464
-------

Lib/ssl.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
1414
Functions:
1515
16+
get_sigalgs -- return a list of all available TLS signature
17+
algorithms (requires OpenSSL 3.4 or later)
18+
1619
cert_time_to_seconds -- convert time string used for certificate
1720
notBefore and notAfter functions to integer
1821
seconds past the Epoch (the time values
@@ -112,6 +115,7 @@
112115
except ImportError:
113116
# RAND_egd is not supported on some platforms
114117
pass
118+
from _ssl import get_sigalgs
115119

116120

117121
from _ssl import (
@@ -935,6 +939,14 @@ def group(self):
935939
"""Return the currently selected key agreement group name."""
936940
return self._sslobj.group()
937941

942+
def client_sigalg(self):
943+
"""Return the selected client authentication signature algorithm."""
944+
return self._sslobj.client_sigalg()
945+
946+
def server_sigalg(self):
947+
"""Return the selected server handshake signature algorithm."""
948+
return self._sslobj.server_sigalg()
949+
938950
def shared_ciphers(self):
939951
"""Return a list of ciphers shared by the client during the handshake or
940952
None if this is not a valid server connection.
@@ -1222,6 +1234,22 @@ def group(self):
12221234
else:
12231235
return self._sslobj.group()
12241236

1237+
@_sslcopydoc
1238+
def client_sigalg(self):
1239+
self._checkClosed()
1240+
if self._sslobj is None:
1241+
return None
1242+
else:
1243+
return self._sslobj.client_sigalg()
1244+
1245+
@_sslcopydoc
1246+
def server_sigalg(self):
1247+
self._checkClosed()
1248+
if self._sslobj is None:
1249+
return None
1250+
else:
1251+
return self._sslobj.server_sigalg()
1252+
12251253
@_sslcopydoc
12261254
def shared_ciphers(self):
12271255
self._checkClosed()

Lib/test/test_ssl.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@
5151
CAN_GET_SELECTED_OPENSSL_GROUP = ssl.OPENSSL_VERSION_INFO >= (3, 2)
5252
CAN_IGNORE_UNKNOWN_OPENSSL_GROUPS = ssl.OPENSSL_VERSION_INFO >= (3, 3)
5353
CAN_GET_AVAILABLE_OPENSSL_GROUPS = ssl.OPENSSL_VERSION_INFO >= (3, 5)
54+
CAN_GET_AVAILABLE_OPENSSL_SIGALGS = ssl.OPENSSL_VERSION_INFO >= (3, 4)
55+
CAN_SET_CLIENT_SIGALGS = "AWS-LC" not in ssl.OPENSSL_VERSION
56+
CAN_IGNORE_UNKNOWN_OPENSSL_SIGALGS = ssl.OPENSSL_VERSION_INFO >= (3, 3)
57+
CAN_GET_SELECTED_OPENSSL_SIGALG = ssl.OPENSSL_VERSION_INFO >= (3, 5)
5458
PY_SSL_DEFAULT_CIPHERS = sysconfig.get_config_var('PY_SSL_DEFAULT_CIPHERS')
5559

5660
PROTOCOL_TO_TLS_VERSION = {}
@@ -294,7 +298,8 @@ def test_wrap_socket(sock, *,
294298
USE_SAME_TEST_CONTEXT = False
295299
_TEST_CONTEXT = None
296300

297-
def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True):
301+
def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True,
302+
client_cert=None):
298303
"""Create context
299304
300305
client_context, server_context, hostname = testing_context()
@@ -321,6 +326,10 @@ def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True):
321326
if server_chain:
322327
server_context.load_verify_locations(SIGNING_CA)
323328

329+
if client_cert:
330+
client_context.load_cert_chain(client_cert)
331+
server_context.verify_mode = ssl.CERT_REQUIRED
332+
324333
if USE_SAME_TEST_CONTEXT:
325334
if _TEST_CONTEXT is not None:
326335
_TEST_CONTEXT = client_context, server_context, hostname
@@ -990,6 +999,37 @@ def test_get_groups(self):
990999
self.assertNotIn('P-256', ctx.get_groups())
9911000
self.assertIn('P-256', ctx.get_groups(include_aliases=True))
9921001

1002+
@unittest.skipUnless(CAN_GET_AVAILABLE_OPENSSL_SIGALGS,
1003+
"SSL library doesn't support getting sigalgs")
1004+
def test_get_sigalgs(self):
1005+
self.assertIn('rsa_pss_rsae_sha256', ssl.get_sigalgs())
1006+
1007+
@unittest.skipUnless(CAN_SET_CLIENT_SIGALGS,
1008+
"SSL library doesn't support setting client sigalgs")
1009+
def test_set_client_sigalgs(self):
1010+
ctx = ssl.create_default_context()
1011+
1012+
self.assertIsNone(ctx.set_client_sigalgs('rsa_pss_rsae_sha256'))
1013+
1014+
self.assertRaises(ssl.SSLError, ctx.set_client_sigalgs,
1015+
'rsa_pss_rsae_sha256:foo')
1016+
1017+
# Ignoring unknown sigalgs is only supported since OpenSSL 3.3.
1018+
if CAN_IGNORE_UNKNOWN_OPENSSL_SIGALGS:
1019+
self.assertIsNone(ctx.set_client_sigalgs('rsa_pss_rsae_sha256:?foo'))
1020+
1021+
def test_set_server_sigalgs(self):
1022+
ctx = ssl.create_default_context()
1023+
1024+
self.assertIsNone(ctx.set_server_sigalgs('rsa_pss_rsae_sha256'))
1025+
1026+
self.assertRaises(ssl.SSLError, ctx.set_server_sigalgs,
1027+
'rsa_pss_rsae_sha256:foo')
1028+
1029+
# Ignoring unknown sigalgs is only supported since OpenSSL 3.3.
1030+
if CAN_IGNORE_UNKNOWN_OPENSSL_SIGALGS:
1031+
self.assertIsNone(ctx.set_server_sigalgs('rsa_pss_rsae_sha256:?foo'))
1032+
9931033
def test_options(self):
9941034
# Test default SSLContext options
9951035
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
@@ -2814,6 +2854,9 @@ def server_params_test(client_context, server_context, indata=b"FOO\n",
28142854
})
28152855
if CAN_GET_SELECTED_OPENSSL_GROUP:
28162856
stats.update({'group': s.group()})
2857+
if CAN_GET_SELECTED_OPENSSL_SIGALG:
2858+
stats.update({'client_sigalg': s.client_sigalg()})
2859+
stats.update({'server_sigalg': s.server_sigalg()})
28172860
s.close()
28182861
stats['server_alpn_protocols'] = server.selected_alpn_protocols
28192862
stats['server_shared_ciphers'] = server.shared_ciphers
@@ -4273,6 +4316,71 @@ def test_groups(self):
42734316
chatty=True, connectionchatty=True,
42744317
sni_name=hostname)
42754318

4319+
@unittest.skipUnless(CAN_SET_CLIENT_SIGALGS,
4320+
"SSL library doesn't support setting client sigalgs")
4321+
def test_client_sigalgs(self):
4322+
# no mutual auth, so cient_sigalg should be None
4323+
client_context, server_context, hostname = testing_context()
4324+
stats = server_params_test(client_context, server_context,
4325+
chatty=True, connectionchatty=True,
4326+
sni_name=hostname)
4327+
if CAN_GET_SELECTED_OPENSSL_SIGALG:
4328+
self.assertIsNone(stats['client_sigalg'])
4329+
4330+
# server auto, client rsa_pss_rsae_sha384
4331+
sigalg = "rsa_pss_rsae_sha384"
4332+
client_context, server_context, hostname = \
4333+
testing_context(client_cert=SIGNED_CERTFILE)
4334+
client_context.set_client_sigalgs(sigalg)
4335+
stats = server_params_test(client_context, server_context,
4336+
chatty=True, connectionchatty=True,
4337+
sni_name=hostname)
4338+
if CAN_GET_SELECTED_OPENSSL_SIGALG:
4339+
self.assertEqual(stats['client_sigalg'], sigalg)
4340+
4341+
@unittest.skipUnless(CAN_SET_CLIENT_SIGALGS,
4342+
"SSL library doesn't support setting client sigalgs")
4343+
def test_client_sigalgs_mismatch(self):
4344+
client_context, server_context, hostname = \
4345+
testing_context(client_cert=SIGNED_CERTFILE)
4346+
client_context.set_client_sigalgs("rsa_pss_rsae_sha256")
4347+
server_context.set_client_sigalgs("rsa_pss_rsae_sha384")
4348+
4349+
# Some systems return ConnectionResetError on handshake failures
4350+
with self.assertRaises((ssl.SSLError, ConnectionResetError)):
4351+
server_params_test(client_context, server_context,
4352+
chatty=True, connectionchatty=True,
4353+
sni_name=hostname)
4354+
4355+
def test_server_sigalgs(self):
4356+
# server rsa_pss_rsae_sha384, client auto
4357+
sigalg = "rsa_pss_rsae_sha384"
4358+
client_context, server_context, hostname = testing_context()
4359+
server_context.set_server_sigalgs(sigalg)
4360+
stats = server_params_test(client_context, server_context,
4361+
chatty=True, connectionchatty=True,
4362+
sni_name=hostname)
4363+
if CAN_GET_SELECTED_OPENSSL_SIGALG:
4364+
self.assertEqual(stats['server_sigalg'], sigalg)
4365+
4366+
# server auto, client rsa_pss_rsae_sha384
4367+
client_context, server_context, hostname = testing_context()
4368+
client_context.set_server_sigalgs(sigalg)
4369+
stats = server_params_test(client_context, server_context,
4370+
chatty=True, connectionchatty=True,
4371+
sni_name=hostname)
4372+
if CAN_GET_SELECTED_OPENSSL_SIGALG:
4373+
self.assertEqual(stats['server_sigalg'], sigalg)
4374+
4375+
def test_server_sigalgs_mismatch(self):
4376+
client_context, server_context, hostname = testing_context()
4377+
client_context.set_server_sigalgs("rsa_pss_rsae_sha256")
4378+
server_context.set_server_sigalgs("rsa_pss_rsae_sha384")
4379+
with self.assertRaises(ssl.SSLError):
4380+
server_params_test(client_context, server_context,
4381+
chatty=True, connectionchatty=True,
4382+
sni_name=hostname)
4383+
42764384
def test_selected_alpn_protocol(self):
42774385
# selected_alpn_protocol() is None unless ALPN is used.
42784386
client_context, server_context, hostname = testing_context()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:mod:`ssl`: :class:`~ssl.SSLContext` objects can now set client and server
2+
TLS signature algorithms. If Python has been built with OpenSSL 3.5 or later,
3+
:class:`~ssl.SSLSocket` objects can return the signature algorithms selected
4+
on a connection.

0 commit comments

Comments
 (0)