Skip to content

Commit 662d66e

Browse files
committed
gh-136306: Initial cut at SSL groups support
This is an initial implementation of the feature proposed in issue #136306.
1 parent b499105 commit 662d66e

5 files changed

Lines changed: 310 additions & 1 deletion

File tree

Doc/library/ssl.rst

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,6 +1641,21 @@ to speed up repeated connections from the same clients.
16411641

16421642
.. versionadded:: 3.6
16431643

1644+
.. method:: SSLContext.get_groups()
1645+
1646+
Get a list of groups implemented for key agreement, taking into account
1647+
the SSLContext's current TLS `minimum_version` and `maximum_version` values.
1648+
1649+
Example::
1650+
1651+
>>> ctx = ssl.create_default_context()
1652+
>>> ctx.minimum_version=ssl.TLSVersion.TLSv1_3
1653+
>>> ctx.maximum_version=ssl.TLSVersion.TLSv1_3
1654+
>>> ctx.get_groups()
1655+
['secp256r1', 'secp384r1', 'secp521r1', 'x25519', 'x448', 'brainpoolP256r1tls13', 'brainpoolP384r1tls13', 'brainpoolP512r1tls13', 'ffdhe2048', 'ffdhe3072', 'ffdhe4096', 'ffdhe6144', 'ffdhe8192', 'MLKEM512', 'MLKEM768', 'MLKEM1024', 'SecP256r1MLKEM768', 'X25519MLKEM768', 'SecP384r1MLKEM1024'
1656+
1657+
.. versionadded:: 3.15
1658+
16441659
.. method:: SSLContext.set_default_verify_paths()
16451660

16461661
Load a set of default "certification authority" (CA) certificates from
@@ -1666,6 +1681,18 @@ to speed up repeated connections from the same clients.
16661681
TLS 1.3 cipher suites cannot be disabled with
16671682
:meth:`~SSLContext.set_ciphers`.
16681683

1684+
.. method:: SSLContext.set_groups(groups)
1685+
1686+
Set the groups allowed for key agreement for sockets created with this
1687+
context. It should be a string in the `OpenSSL group list format
1688+
<https://docs.openssl.org/master/man3/SSL_CTX_set1_groups_list/>`_.
1689+
1690+
.. note::
1691+
when connected, the :meth:`SSLSocket.group` method of SSL sockets will
1692+
return the group used for key agreement on that connection.
1693+
1694+
.. versionadded:: 3.15
1695+
16691696
.. method:: SSLContext.set_alpn_protocols(protocols)
16701697

16711698
Specify which protocols the socket should advertise during the SSL/TLS
@@ -1789,6 +1816,10 @@ to speed up repeated connections from the same clients.
17891816

17901817
.. versionadded:: 3.3
17911818

1819+
.. deprecated:: 3.15
1820+
1821+
This method has been replaced by :math:`set_groups`.
1822+
17921823
.. seealso::
17931824
`SSL/TLS & Perfect Forward Secrecy <https://vincent.bernat.ch/en/blog/2011-ssl-perfect-forward-secrecy>`_
17941825
Vincent Bernat.

Lib/ssl.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,10 @@ def cipher(self):
931931
ssl_version, secret_bits)``."""
932932
return self._sslobj.cipher()
933933

934+
def group(self):
935+
"""Return the currently selected key agreement group name."""
936+
return self._sslobj.group()
937+
934938
def shared_ciphers(self):
935939
"""Return a list of ciphers shared by the client during the handshake or
936940
None if this is not a valid server connection.
@@ -1206,6 +1210,14 @@ def cipher(self):
12061210
else:
12071211
return self._sslobj.cipher()
12081212

1213+
@_sslcopydoc
1214+
def group(self):
1215+
self._checkClosed()
1216+
if self._sslobj is None:
1217+
return None
1218+
else:
1219+
return self._sslobj.group()
1220+
12091221
@_sslcopydoc
12101222
def shared_ciphers(self):
12111223
self._checkClosed()

Lib/test/test_ssl.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,15 @@ def test_get_ciphers(self):
960960
len(intersection), 2, f"\ngot: {sorted(names)}\nexpected: {sorted(expected)}"
961961
)
962962

963+
def test_groups(self):
964+
ctx = ssl.create_default_context()
965+
self.assertIsNone(ctx.set_groups('P-256'))
966+
self.assertIsNone(ctx.set_groups('P-256:X25519'))
967+
968+
if ssl.OPENSSL_VERSION_INFO >= (3, 5):
969+
self.assertNotIn('P-256', ctx.get_groups())
970+
self.assertIn('P-256', ctx.get_groups(include_aliases=True))
971+
963972
def test_options(self):
964973
# Test default SSLContext options
965974
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
@@ -2701,6 +2710,8 @@ def server_params_test(client_context, server_context, indata=b"FOO\n",
27012710
'session_reused': s.session_reused,
27022711
'session': s.session,
27032712
})
2713+
if ssl.OPENSSL_VERSION_INFO >= (3, 2):
2714+
stats.update({'group': s.group()})
27042715
s.close()
27052716
stats['server_alpn_protocols'] = server.selected_alpn_protocols
27062717
stats['server_shared_ciphers'] = server.shared_ciphers
@@ -4126,6 +4137,38 @@ def test_ecdh_curve(self):
41264137
chatty=True, connectionchatty=True,
41274138
sni_name=hostname)
41284139

4140+
def test_groups(self):
4141+
# server secp384r1, client auto
4142+
client_context, server_context, hostname = testing_context()
4143+
4144+
server_context.set_groups("secp384r1")
4145+
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
4146+
stats = server_params_test(client_context, server_context,
4147+
chatty=True, connectionchatty=True,
4148+
sni_name=hostname)
4149+
if ssl.OPENSSL_VERSION_INFO >= (3, 2):
4150+
self.assertEqual(stats['group'], "secp384r1")
4151+
4152+
# server auto, client secp384r1
4153+
client_context, server_context, hostname = testing_context()
4154+
client_context.set_groups("secp384r1")
4155+
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
4156+
stats = server_params_test(client_context, server_context,
4157+
chatty=True, connectionchatty=True,
4158+
sni_name=hostname)
4159+
if ssl.OPENSSL_VERSION_INFO >= (3, 2):
4160+
self.assertEqual(stats['group'], "secp384r1")
4161+
4162+
# server / client curve mismatch
4163+
client_context, server_context, hostname = testing_context()
4164+
client_context.set_groups("prime256v1")
4165+
server_context.set_groups("secp384r1")
4166+
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
4167+
with self.assertRaises(ssl.SSLError):
4168+
server_params_test(client_context, server_context,
4169+
chatty=True, connectionchatty=True,
4170+
sni_name=hostname)
4171+
41294172
def test_selected_alpn_protocol(self):
41304173
# selected_alpn_protocol() is None unless ALPN is used.
41314174
client_context, server_context, hostname = testing_context()

Modules/_ssl.c

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2142,6 +2142,31 @@ _ssl__SSLSocket_cipher_impl(PySSLSocket *self)
21422142
return cipher_to_tuple(current);
21432143
}
21442144

2145+
/*[clinic input]
2146+
@critical_section
2147+
_ssl._SSLSocket.group
2148+
[clinic start generated code]*/
2149+
2150+
static PyObject *
2151+
_ssl__SSLSocket_group_impl(PySSLSocket *self)
2152+
/*[clinic end generated code: output=9c168ee877017b95 input=5f187d8bf0d433b7]*/
2153+
{
2154+
#if OPENSSL_VERSION_NUMBER >= 0x30200000L
2155+
const char *group_name;
2156+
2157+
if (self->ssl == NULL)
2158+
Py_RETURN_NONE;
2159+
group_name = SSL_get0_group_name(self->ssl);
2160+
if (group_name == NULL)
2161+
Py_RETURN_NONE;
2162+
return PyUnicode_DecodeFSDefault(group_name);
2163+
#else
2164+
PyErr_SetString(PyExc_NotImplementedError,
2165+
"Getting selected group requires OpenSSL 3.2 or later.");
2166+
return NULL;
2167+
#endif
2168+
}
2169+
21452170
/*[clinic input]
21462171
@critical_section
21472172
_ssl._SSLSocket.version
@@ -3023,6 +3048,7 @@ static PyMethodDef PySSLMethods[] = {
30233048
_SSL__SSLSOCKET_GETPEERCERT_METHODDEF
30243049
_SSL__SSLSOCKET_GET_CHANNEL_BINDING_METHODDEF
30253050
_SSL__SSLSOCKET_CIPHER_METHODDEF
3051+
_SSL__SSLSOCKET_GROUP_METHODDEF
30263052
_SSL__SSLSOCKET_SHARED_CIPHERS_METHODDEF
30273053
_SSL__SSLSOCKET_VERSION_METHODDEF
30283054
_SSL__SSLSOCKET_SELECTED_ALPN_PROTOCOL_METHODDEF
@@ -3402,6 +3428,73 @@ _ssl__SSLContext_get_ciphers_impl(PySSLContext *self)
34023428

34033429
}
34043430

3431+
/*[clinic input]
3432+
@critical_section
3433+
_ssl._SSLContext.set_groups
3434+
grouplist: str
3435+
/
3436+
[clinic start generated code]*/
3437+
3438+
static PyObject *
3439+
_ssl__SSLContext_set_groups_impl(PySSLContext *self, const char *grouplist)
3440+
/*[clinic end generated code: output=0b5d05dfd371ffd0 input=2cc64cef21930741]*/
3441+
{
3442+
if (!SSL_CTX_set1_groups_list(self->ctx, grouplist)) {
3443+
_setSSLError(get_state_ctx(self), "unrecognized group", 0, __FILE__, __LINE__);
3444+
return NULL;
3445+
}
3446+
Py_RETURN_NONE;
3447+
}
3448+
3449+
/*[clinic input]
3450+
@critical_section
3451+
_ssl._SSLContext.get_groups
3452+
*
3453+
include_aliases: bool = False
3454+
[clinic start generated code]*/
3455+
3456+
static PyObject *
3457+
_ssl__SSLContext_get_groups_impl(PySSLContext *self, int include_aliases)
3458+
/*[clinic end generated code: output=6d6209dd1051529b input=3e8ee5deb277dcc5]*/
3459+
{
3460+
#if OPENSSL_VERSION_NUMBER >= 0x30500000L
3461+
STACK_OF(OPENSSL_CSTRING) *groups;
3462+
const char *group;
3463+
size_t i, num;
3464+
PyObject *result = NULL;
3465+
3466+
if ((groups = sk_OPENSSL_CSTRING_new_null()) == NULL) {
3467+
_setSSLError(get_state_ctx(self), "Can't allocate stack", 0, __FILE__, __LINE__);
3468+
return NULL;
3469+
}
3470+
3471+
if (!SSL_CTX_get0_implemented_groups(self->ctx, include_aliases, groups)) {
3472+
_setSSLError(get_state_ctx(self), "Can't get groups", 0, __FILE__, __LINE__);
3473+
sk_OPENSSL_CSTRING_free(groups);
3474+
return NULL;
3475+
}
3476+
3477+
num = sk_OPENSSL_CSTRING_num(groups);
3478+
result = PyList_New(num);
3479+
if (result == NULL) {
3480+
_setSSLError(get_state_ctx(self), "Can't allocate list", 0, __FILE__, __LINE__);
3481+
sk_OPENSSL_CSTRING_free(groups);
3482+
return NULL;
3483+
}
3484+
3485+
for (i = 0; i < num; ++i) {
3486+
group = sk_OPENSSL_CSTRING_value(groups, i);
3487+
PyList_SET_ITEM(result, i, PyUnicode_DecodeFSDefault(group));
3488+
}
3489+
3490+
sk_OPENSSL_CSTRING_free(groups);
3491+
return result;
3492+
#else
3493+
PyErr_SetString(PyExc_NotImplementedError,
3494+
"Getting implemented groups requires OpenSSL 3.5 or later.");
3495+
return NULL;
3496+
#endif
3497+
}
34053498

34063499
static int
34073500
do_protocol_selection(int alpn, unsigned char **out, unsigned char *outlen,
@@ -5249,6 +5342,7 @@ static struct PyMethodDef context_methods[] = {
52495342
_SSL__SSLCONTEXT__WRAP_SOCKET_METHODDEF
52505343
_SSL__SSLCONTEXT__WRAP_BIO_METHODDEF
52515344
_SSL__SSLCONTEXT_SET_CIPHERS_METHODDEF
5345+
_SSL__SSLCONTEXT_SET_GROUPS_METHODDEF
52525346
_SSL__SSLCONTEXT__SET_ALPN_PROTOCOLS_METHODDEF
52535347
_SSL__SSLCONTEXT_LOAD_CERT_CHAIN_METHODDEF
52545348
_SSL__SSLCONTEXT_LOAD_DH_PARAMS_METHODDEF
@@ -5259,6 +5353,7 @@ static struct PyMethodDef context_methods[] = {
52595353
_SSL__SSLCONTEXT_CERT_STORE_STATS_METHODDEF
52605354
_SSL__SSLCONTEXT_GET_CA_CERTS_METHODDEF
52615355
_SSL__SSLCONTEXT_GET_CIPHERS_METHODDEF
5356+
_SSL__SSLCONTEXT_GET_GROUPS_METHODDEF
52625357
_SSL__SSLCONTEXT_SET_PSK_CLIENT_CALLBACK_METHODDEF
52635358
_SSL__SSLCONTEXT_SET_PSK_SERVER_CALLBACK_METHODDEF
52645359
{NULL, NULL} /* sentinel */

0 commit comments

Comments
 (0)