Skip to content

Commit 7361440

Browse files
committed
Add support in SSL module for getting and setting TLS signature algorithms
1 parent 31d3836 commit 7361440

File tree

6 files changed

+398
-2
lines changed

6 files changed

+398
-2
lines changed

Doc/library/ssl.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,6 +1297,22 @@ SSL sockets also have the following additional methods and attributes:
12971297

12981298
.. versionadded:: next
12991299

1300+
.. method:: SSLSocket.client_sigalg()
1301+
1302+
Return the signature algorithm used for performing certificate-based client
1303+
authentication on this connection. If no connection has been established
1304+
or client authentication didn't occur, this method returns ``None``.
1305+
1306+
.. versionadded:: next
1307+
1308+
.. method:: SSLSocket.server_sigalg()
1309+
1310+
Return the signature algorithm used by the server to complete the TLS
1311+
handshake on this connection. If no connection has been established
1312+
or the cipher suite has no signature, this method returns ``None``.
1313+
1314+
.. versionadded:: next
1315+
13001316
.. method:: SSLSocket.compression()
13011317

13021318
Return the compression algorithm being used as a string, or ``None``
@@ -1725,6 +1741,34 @@ to speed up repeated connections from the same clients.
17251741

17261742
.. versionadded:: next
17271743

1744+
.. method:: SSLContext.set_client_sigalgs(sigalgs)
1745+
1746+
Set the signature algorithms allowed for certificate-based client
1747+
authentication. It should be a string in the `OpenSSL sigalgs list format
1748+
<https://docs.openssl.org/master/man3/SSL_CTX_set1_client_sigalgs_list/>`_.
1749+
1750+
.. note::
1751+
1752+
When connected, the :meth:`SSLSocket.client_sigalg` method of SSL
1753+
sockets will return the signature algorithm used for performing
1754+
certificate-based client authentication on that connection.
1755+
1756+
.. versionadded:: next
1757+
1758+
.. method:: SSLContext.set_server_sigalgs(sigalgs)
1759+
1760+
Set the signature algorithms allowed for the server to complete the TLS
1761+
handshake. It should be a string in the `OpenSSL sigalgs list format
1762+
<https://docs.openssl.org/master/man3/SSL_CTX_set1_sigalgs_list/>`_.
1763+
1764+
.. note::
1765+
1766+
When connected, the :meth:`SSLSocket.server_sigalg` method of SSL
1767+
sockets will return the signature algorithm used by the server to
1768+
complete the TLS handshake on that connection.
1769+
1770+
.. versionadded:: next
1771+
17281772
.. method:: SSLContext.set_alpn_protocols(protocols)
17291773

17301774
Specify which protocols the socket should advertise during the SSL/TLS

Doc/whatsnew/3.15.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,21 @@ ssl
433433
connection is made.
434434
(Contributed by Ron Frederick in :gh:`137197`.)
435435

436+
* Added new methods for managing signature algorithms
437+
438+
* :meth:`ssl.SSLContext.set_client_sigalgs` sets the signature algorithms
439+
allowed for certificate-based client authentication.
440+
* :meth:`ssl.SSLContext.set_server_sigalgs` sets the signature algorithms
441+
allowed for the server to complete the TLS handshake.
442+
* :meth:`ssl.SSLSocket.client_sigalg` returns the signature algorithm
443+
selected for client authentication on the current connection. This call
444+
requires OpenSSL 3.5 or later.
445+
* :meth:`ssl.SSLSocket.server_sigalg` returns the signature algorithm
446+
selected for the server to complete the TLS handshake on the current
447+
connection. This call requires OpenSSL 3.5 or later.
448+
449+
(Contributed by Ron Frederick in :gh:`138252`.)
450+
436451

437452
tarfile
438453
-------

Lib/ssl.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,14 @@ def group(self):
935935
"""Return the currently selected key agreement group name."""
936936
return self._sslobj.group()
937937

938+
def client_sigalg(self):
939+
"""Return the selected client authentication signature algorithm."""
940+
return self._sslobj.client_sigalg()
941+
942+
def server_sigalg(self):
943+
"""Return the selected server handshake signature algorithm."""
944+
return self._sslobj.server_sigalg()
945+
938946
def shared_ciphers(self):
939947
"""Return a list of ciphers shared by the client during the handshake or
940948
None if this is not a valid server connection.
@@ -1222,6 +1230,22 @@ def group(self):
12221230
else:
12231231
return self._sslobj.group()
12241232

1233+
@_sslcopydoc
1234+
def client_sigalg(self):
1235+
self._checkClosed()
1236+
if self._sslobj is None:
1237+
return None
1238+
else:
1239+
return self._sslobj.client_sigalg()
1240+
1241+
@_sslcopydoc
1242+
def server_sigalg(self):
1243+
self._checkClosed()
1244+
if self._sslobj is None:
1245+
return None
1246+
else:
1247+
return self._sslobj.server_sigalg()
1248+
12251249
@_sslcopydoc
12261250
def shared_ciphers(self):
12271251
self._checkClosed()

Lib/test/test_ssl.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
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_IGNORE_UNKNOWN_OPENSSL_SIGALGS = ssl.OPENSSL_VERSION_INFO >= (3, 3)
55+
CAN_GET_SELECTED_OPENSSL_SIGALG = ssl.OPENSSL_VERSION_INFO >= (3, 5)
5456
PY_SSL_DEFAULT_CIPHERS = sysconfig.get_config_var('PY_SSL_DEFAULT_CIPHERS')
5557

5658
PROTOCOL_TO_TLS_VERSION = {}
@@ -294,7 +296,8 @@ def test_wrap_socket(sock, *,
294296
USE_SAME_TEST_CONTEXT = False
295297
_TEST_CONTEXT = None
296298

297-
def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True):
299+
def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True,
300+
client_cert=None):
298301
"""Create context
299302
300303
client_context, server_context, hostname = testing_context()
@@ -321,6 +324,10 @@ def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True):
321324
if server_chain:
322325
server_context.load_verify_locations(SIGNING_CA)
323326

327+
if client_cert:
328+
client_context.load_cert_chain(client_cert)
329+
server_context.verify_mode = ssl.CERT_REQUIRED
330+
324331
if USE_SAME_TEST_CONTEXT:
325332
if _TEST_CONTEXT is not None:
326333
_TEST_CONTEXT = client_context, server_context, hostname
@@ -990,6 +997,22 @@ def test_get_groups(self):
990997
self.assertNotIn('P-256', ctx.get_groups())
991998
self.assertIn('P-256', ctx.get_groups(include_aliases=True))
992999

1000+
def test_set_sigalgs(self):
1001+
ctx = ssl.create_default_context()
1002+
1003+
self.assertIsNone(ctx.set_client_sigalgs('rsa_pss_rsae_sha256'))
1004+
self.assertIsNone(ctx.set_server_sigalgs('rsa_pss_rsae_sha256'))
1005+
1006+
self.assertRaises(ssl.SSLError, ctx.set_client_sigalgs,
1007+
'rsa_pss_rsae_sha256:foo')
1008+
self.assertRaises(ssl.SSLError, ctx.set_server_sigalgs,
1009+
'rsa_pss_rsae_sha256:foo')
1010+
1011+
# Ignoring unknown sigalgs is only supported since OpenSSL 3.3.
1012+
if CAN_IGNORE_UNKNOWN_OPENSSL_SIGALGS:
1013+
self.assertIsNone(ctx.set_client_sigalgs('rsa_pss_rsae_sha256:?foo'))
1014+
self.assertIsNone(ctx.set_server_sigalgs('rsa_pss_rsae_sha256:?foo'))
1015+
9931016
def test_options(self):
9941017
# Test default SSLContext options
9951018
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
@@ -2814,6 +2837,9 @@ def server_params_test(client_context, server_context, indata=b"FOO\n",
28142837
})
28152838
if CAN_GET_SELECTED_OPENSSL_GROUP:
28162839
stats.update({'group': s.group()})
2840+
if CAN_GET_SELECTED_OPENSSL_SIGALG:
2841+
stats.update({'client_sigalg': s.client_sigalg()})
2842+
stats.update({'server_sigalg': s.server_sigalg()})
28172843
s.close()
28182844
stats['server_alpn_protocols'] = server.selected_alpn_protocols
28192845
stats['server_shared_ciphers'] = server.shared_ciphers
@@ -4273,6 +4299,63 @@ def test_groups(self):
42734299
chatty=True, connectionchatty=True,
42744300
sni_name=hostname)
42754301

4302+
def test_client_sigalgs(self):
4303+
# no mutual auth, so cient_sigalg should be None
4304+
client_context, server_context, hostname = testing_context()
4305+
stats = server_params_test(client_context, server_context,
4306+
chatty=True, connectionchatty=True,
4307+
sni_name=hostname)
4308+
if CAN_GET_SELECTED_OPENSSL_SIGALG:
4309+
self.assertIsNone(stats['client_sigalg'])
4310+
4311+
# server auto, client rsa_pss_rsae_sha384
4312+
client_context, server_context, hostname = \
4313+
testing_context(client_cert=SIGNED_CERTFILE)
4314+
client_context.set_client_sigalgs("rsa_pss_rsae_sha384")
4315+
stats = server_params_test(client_context, server_context,
4316+
chatty=True, connectionchatty=True,
4317+
sni_name=hostname)
4318+
if CAN_GET_SELECTED_OPENSSL_SIGALG:
4319+
self.assertEqual(stats['client_sigalg'], "rsa_pss_rsae_sha384")
4320+
4321+
# server / client sigalg mismatch
4322+
client_context, server_context, hostname = \
4323+
testing_context(client_cert=SIGNED_CERTFILE)
4324+
client_context.set_client_sigalgs("rsa_pss_rsae_sha256")
4325+
server_context.set_client_sigalgs("rsa_pss_rsae_sha384")
4326+
with self.assertRaises(ssl.SSLError):
4327+
server_params_test(client_context, server_context,
4328+
chatty=True, connectionchatty=True,
4329+
sni_name=hostname)
4330+
4331+
def test_server_sigalgs(self):
4332+
# server rsa_pss_rsae_sha384, client auto
4333+
client_context, server_context, hostname = testing_context()
4334+
server_context.set_server_sigalgs("rsa_pss_rsae_sha384")
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['server_sigalg'], "rsa_pss_rsae_sha384")
4340+
4341+
# server auto, client rsa_pss_rsae_sha384
4342+
client_context, server_context, hostname = testing_context()
4343+
client_context.set_server_sigalgs("rsa_pss_rsae_sha384")
4344+
stats = server_params_test(client_context, server_context,
4345+
chatty=True, connectionchatty=True,
4346+
sni_name=hostname)
4347+
if CAN_GET_SELECTED_OPENSSL_SIGALG:
4348+
self.assertEqual(stats['server_sigalg'], "rsa_pss_rsae_sha384")
4349+
4350+
# server / client sigalg mismatch
4351+
client_context, server_context, hostname = testing_context()
4352+
client_context.set_server_sigalgs("rsa_pss_rsae_sha256")
4353+
server_context.set_server_sigalgs("rsa_pss_rsae_sha384")
4354+
with self.assertRaises(ssl.SSLError):
4355+
server_params_test(client_context, server_context,
4356+
chatty=True, connectionchatty=True,
4357+
sni_name=hostname)
4358+
42764359
def test_selected_alpn_protocol(self):
42774360
# selected_alpn_protocol() is None unless ALPN is used.
42784361
client_context, server_context, hostname = testing_context()

Modules/_ssl.c

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2200,6 +2200,70 @@ _ssl__SSLSocket_group_impl(PySSLSocket *self)
22002200
#endif
22012201
}
22022202

2203+
/*[clinic input]
2204+
@critical_section
2205+
_ssl._SSLSocket.client_sigalg
2206+
[clinic start generated code]*/
2207+
2208+
static PyObject *
2209+
_ssl__SSLSocket_client_sigalg_impl(PySSLSocket *self)
2210+
/*[clinic end generated code: output=499dd7fbf021a47b input=a0d9696b5414c627]*/
2211+
{
2212+
#if OPENSSL_VERSION_NUMBER >= 0x30500000L
2213+
int ret;
2214+
const char *sigalg;
2215+
2216+
if (self->ssl == NULL) {
2217+
Py_RETURN_NONE;
2218+
}
2219+
if (self->socket_type == PY_SSL_CLIENT) {
2220+
ret = SSL_get0_signature_name(self->ssl, &sigalg);
2221+
} else {
2222+
ret = SSL_get0_peer_signature_name(self->ssl, &sigalg);
2223+
}
2224+
if (ret == 0) {
2225+
Py_RETURN_NONE;
2226+
}
2227+
return PyUnicode_DecodeFSDefault(sigalg);
2228+
#else
2229+
PyErr_SetString(PyExc_NotImplementedError,
2230+
"Getting sig algorithms requires OpenSSL 3.5 or later.");
2231+
return NULL;
2232+
#endif
2233+
}
2234+
2235+
/*[clinic input]
2236+
@critical_section
2237+
_ssl._SSLSocket.server_sigalg
2238+
[clinic start generated code]*/
2239+
2240+
static PyObject *
2241+
_ssl__SSLSocket_server_sigalg_impl(PySSLSocket *self)
2242+
/*[clinic end generated code: output=c508a766a8e275dc input=9063e562a1e6b946]*/
2243+
{
2244+
#if OPENSSL_VERSION_NUMBER >= 0x30500000L
2245+
int ret;
2246+
const char *sigalg;
2247+
2248+
if (self->ssl == NULL) {
2249+
Py_RETURN_NONE;
2250+
}
2251+
if (self->socket_type == PY_SSL_CLIENT) {
2252+
ret = SSL_get0_peer_signature_name(self->ssl, &sigalg);
2253+
} else {
2254+
ret = SSL_get0_signature_name(self->ssl, &sigalg);
2255+
}
2256+
if (ret == 0) {
2257+
Py_RETURN_NONE;
2258+
}
2259+
return PyUnicode_DecodeFSDefault(sigalg);
2260+
#else
2261+
PyErr_SetString(PyExc_NotImplementedError,
2262+
"Getting sig algorithms requires OpenSSL 3.5 or later.");
2263+
return NULL;
2264+
#endif
2265+
}
2266+
22032267
/*[clinic input]
22042268
@critical_section
22052269
_ssl._SSLSocket.version
@@ -3276,6 +3340,8 @@ static PyMethodDef PySSLMethods[] = {
32763340
_SSL__SSLSOCKET_GET_CHANNEL_BINDING_METHODDEF
32773341
_SSL__SSLSOCKET_CIPHER_METHODDEF
32783342
_SSL__SSLSOCKET_GROUP_METHODDEF
3343+
_SSL__SSLSOCKET_CLIENT_SIGALG_METHODDEF
3344+
_SSL__SSLSOCKET_SERVER_SIGALG_METHODDEF
32793345
_SSL__SSLSOCKET_SHARED_CIPHERS_METHODDEF
32803346
_SSL__SSLSOCKET_VERSION_METHODDEF
32813347
_SSL__SSLSOCKET_SELECTED_ALPN_PROTOCOL_METHODDEF
@@ -3761,6 +3827,44 @@ _ssl__SSLContext_get_groups_impl(PySSLContext *self, int include_aliases)
37613827
#endif
37623828
}
37633829

3830+
/*[clinic input]
3831+
@critical_section
3832+
_ssl._SSLContext.set_client_sigalgs
3833+
sigalgslist: str
3834+
/
3835+
[clinic start generated code]*/
3836+
3837+
static PyObject *
3838+
_ssl__SSLContext_set_client_sigalgs_impl(PySSLContext *self,
3839+
const char *sigalgslist)
3840+
/*[clinic end generated code: output=f4f5be160a29c7d6 input=500d853ce9fd94ff]*/
3841+
{
3842+
if (!SSL_CTX_set1_client_sigalgs_list(self->ctx, sigalgslist)) {
3843+
_setSSLError(get_state_ctx(self), "unrecognized signature algorithm", 0, __FILE__, __LINE__);
3844+
return NULL;
3845+
}
3846+
Py_RETURN_NONE;
3847+
}
3848+
3849+
/*[clinic input]
3850+
@critical_section
3851+
_ssl._SSLContext.set_server_sigalgs
3852+
sigalgslist: str
3853+
/
3854+
[clinic start generated code]*/
3855+
3856+
static PyObject *
3857+
_ssl__SSLContext_set_server_sigalgs_impl(PySSLContext *self,
3858+
const char *sigalgslist)
3859+
/*[clinic end generated code: output=31ecb1d310285644 input=653b752e4f8d801b]*/
3860+
{
3861+
if (!SSL_CTX_set1_sigalgs_list(self->ctx, sigalgslist)) {
3862+
_setSSLError(get_state_ctx(self), "unrecognized signature algorithm", 0, __FILE__, __LINE__);
3863+
return NULL;
3864+
}
3865+
Py_RETURN_NONE;
3866+
}
3867+
37643868
static int
37653869
do_protocol_selection(int alpn, unsigned char **out, unsigned char *outlen,
37663870
const unsigned char *server_protocols, unsigned int server_protocols_len,
@@ -5616,6 +5720,8 @@ static struct PyMethodDef context_methods[] = {
56165720
_SSL__SSLCONTEXT_SET_CIPHERS_METHODDEF
56175721
_SSL__SSLCONTEXT_SET_CIPHERSUITES_METHODDEF
56185722
_SSL__SSLCONTEXT_SET_GROUPS_METHODDEF
5723+
_SSL__SSLCONTEXT_SET_CLIENT_SIGALGS_METHODDEF
5724+
_SSL__SSLCONTEXT_SET_SERVER_SIGALGS_METHODDEF
56195725
_SSL__SSLCONTEXT__SET_ALPN_PROTOCOLS_METHODDEF
56205726
_SSL__SSLCONTEXT_LOAD_CERT_CHAIN_METHODDEF
56215727
_SSL__SSLCONTEXT_LOAD_DH_PARAMS_METHODDEF

0 commit comments

Comments
 (0)