Skip to content

Commit 377b787

Browse files
authored
gh-136306: Add support for getting and setting SSL groups (#136307)
Add support for getting and setting groups used for key agreement. * `ssl.SSLSocket.group()` returns the name of the group used for the key agreement of the current session establishment. This feature requires Python to be built with OpenSSL 3.2 or later. * `ssl.SSLContext.get_groups()` returns the list of names of groups that are compatible with the TLS version of the current context. This feature requires Python to be built with OpenSSL 3.5 or later. * `ssl.SSLContext.set_groups()` sets the groups allowed for key agreement for sockets created with this context. This feature is always supported.
1 parent 59e2330 commit 377b787

File tree

11 files changed

+370
-1
lines changed

11 files changed

+370
-1
lines changed

Doc/library/ssl.rst

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,13 @@ SSL sockets also have the following additional methods and attributes:
12901290

12911291
.. versionadded:: 3.5
12921292

1293+
.. method:: SSLSocket.group()
1294+
1295+
Return the group used for doing key agreement on this connection. If no
1296+
connection has been established, returns ``None``.
1297+
1298+
.. versionadded:: next
1299+
12931300
.. method:: SSLSocket.compression()
12941301

12951302
Return the compression algorithm being used as a string, or ``None``
@@ -1647,6 +1654,25 @@ to speed up repeated connections from the same clients.
16471654

16481655
.. versionadded:: 3.6
16491656

1657+
.. method:: SSLContext.get_groups(*, include_aliases=False)
1658+
1659+
Get a list of groups implemented for key agreement, taking into
1660+
account the current TLS :attr:`~SSLContext.minimum_version` and
1661+
:attr:`~SSLContext.maximum_version` values. For example::
1662+
1663+
>>> ctx = ssl.create_default_context()
1664+
>>> ctx.minimum_version = ssl.TLSVersion.TLSv1_3
1665+
>>> ctx.maximum_version = ssl.TLSVersion.TLSv1_3
1666+
>>> ctx.get_groups() # doctest: +SKIP
1667+
['secp256r1', 'secp384r1', 'secp521r1', 'x25519', 'x448', ...]
1668+
1669+
By default, this method returns only the preferred IANA names for the
1670+
available groups. However, if the ``include_aliases`` parameter is set to
1671+
:const:`True` this method will also return any associated aliases such as
1672+
the ECDH curve names supported in older versions of OpenSSL.
1673+
1674+
.. versionadded:: next
1675+
16501676
.. method:: SSLContext.set_default_verify_paths()
16511677

16521678
Load a set of default "certification authority" (CA) certificates from
@@ -1672,6 +1698,19 @@ to speed up repeated connections from the same clients.
16721698
TLS 1.3 cipher suites cannot be disabled with
16731699
:meth:`~SSLContext.set_ciphers`.
16741700

1701+
.. method:: SSLContext.set_groups(groups)
1702+
1703+
Set the groups allowed for key agreement for sockets created with this
1704+
context. It should be a string in the `OpenSSL group list format
1705+
<https://docs.openssl.org/master/man3/SSL_CTX_set1_groups_list/>`_.
1706+
1707+
.. note::
1708+
1709+
When connected, the :meth:`SSLSocket.group` method of SSL sockets will
1710+
return the group used for key agreement on that connection.
1711+
1712+
.. versionadded:: next
1713+
16751714
.. method:: SSLContext.set_alpn_protocols(protocols)
16761715

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

Doc/whatsnew/3.15.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,24 @@ ssl
300300
supports "External PSKs" in TLSv1.3, as described in RFC 9258.
301301
(Contributed by Will Childs-Klein in :gh:`133624`.)
302302

303+
* Added new methods for managing groups used for SSL key agreement
304+
305+
* :meth:`ssl.SSLContext.set_groups` sets the groups allowed for doing
306+
key agreement, extending the previous
307+
:meth:`ssl.SSLContext.set_ecdh_curve` method.
308+
This new API provides the ability to list multiple groups and
309+
supports fixed-field and post-quantum groups in addition to ECDH
310+
curves. This method can also be used to control what key shares
311+
are sent in the TLS handshake.
312+
* :meth:`ssl.SSLSocket.group` returns the group selected for doing key
313+
agreement on the current connection after the TLS handshake completes.
314+
This call requires OpenSSL 3.2 or later.
315+
* :meth:`ssl.SSLContext.get_groups` returns a list of all available key
316+
agreement groups compatible with the minimum and maximum TLS versions
317+
currently set in the context. This call requires OpenSSL 3.5 or later.
318+
319+
(Contributed by Ron Frederick in :gh:`136306`)
320+
303321

304322
tarfile
305323
-------

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ struct _Py_global_strings {
496496
STRUCT_FOR_ID(imag)
497497
STRUCT_FOR_ID(importlib)
498498
STRUCT_FOR_ID(in_fd)
499+
STRUCT_FOR_ID(include_aliases)
499500
STRUCT_FOR_ID(incoming)
500501
STRUCT_FOR_ID(index)
501502
STRUCT_FOR_ID(indexgroup)

Include/internal/pycore_runtime_init_generated.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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.
@@ -1210,6 +1214,14 @@ def cipher(self):
12101214
else:
12111215
return self._sslobj.cipher()
12121216

1217+
@_sslcopydoc
1218+
def group(self):
1219+
self._checkClosed()
1220+
if self._sslobj is None:
1221+
return None
1222+
else:
1223+
return self._sslobj.group()
1224+
12131225
@_sslcopydoc
12141226
def shared_ciphers(self):
12151227
self._checkClosed()

Lib/test/test_ssl.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
PROTOCOLS = sorted(ssl._PROTOCOL_NAMES)
4949
HOST = socket_helper.HOST
5050
IS_OPENSSL_3_0_0 = ssl.OPENSSL_VERSION_INFO >= (3, 0, 0)
51+
CAN_GET_SELECTED_OPENSSL_GROUP = ssl.OPENSSL_VERSION_INFO >= (3, 2)
52+
CAN_GET_AVAILABLE_OPENSSL_GROUPS = ssl.OPENSSL_VERSION_INFO >= (3, 5)
5153
PY_SSL_DEFAULT_CIPHERS = sysconfig.get_config_var('PY_SSL_DEFAULT_CIPHERS')
5254

5355
PROTOCOL_TO_TLS_VERSION = {}
@@ -960,6 +962,19 @@ def test_get_ciphers(self):
960962
len(intersection), 2, f"\ngot: {sorted(names)}\nexpected: {sorted(expected)}"
961963
)
962964

965+
def test_set_groups(self):
966+
ctx = ssl.create_default_context()
967+
self.assertIsNone(ctx.set_groups('P-256:X25519'))
968+
self.assertRaises(ssl.SSLError, ctx.set_groups, 'P-256:xxx')
969+
970+
@unittest.skipUnless(CAN_GET_AVAILABLE_OPENSSL_GROUPS,
971+
"OpenSSL version doesn't support getting groups")
972+
def test_get_groups(self):
973+
ctx = ssl.create_default_context()
974+
# By default, only return official IANA names.
975+
self.assertNotIn('P-256', ctx.get_groups())
976+
self.assertIn('P-256', ctx.get_groups(include_aliases=True))
977+
963978
def test_options(self):
964979
# Test default SSLContext options
965980
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
@@ -2720,6 +2735,8 @@ def server_params_test(client_context, server_context, indata=b"FOO\n",
27202735
'session_reused': s.session_reused,
27212736
'session': s.session,
27222737
})
2738+
if CAN_GET_SELECTED_OPENSSL_GROUP:
2739+
stats.update({'group': s.group()})
27232740
s.close()
27242741
stats['server_alpn_protocols'] = server.selected_alpn_protocols
27252742
stats['server_shared_ciphers'] = server.shared_ciphers
@@ -3870,6 +3887,8 @@ def test_no_shared_ciphers(self):
38703887
with self.assertRaises(OSError):
38713888
s.connect((HOST, server.port))
38723889
self.assertIn("NO_SHARED_CIPHER", server.conn_errors[0])
3890+
self.assertIsNone(s.cipher())
3891+
self.assertIsNone(s.group())
38733892

38743893
def test_version_basic(self):
38753894
"""
@@ -4145,6 +4164,38 @@ def test_ecdh_curve(self):
41454164
chatty=True, connectionchatty=True,
41464165
sni_name=hostname)
41474166

4167+
def test_groups(self):
4168+
# server secp384r1, client auto
4169+
client_context, server_context, hostname = testing_context()
4170+
4171+
server_context.set_groups("secp384r1")
4172+
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
4173+
stats = server_params_test(client_context, server_context,
4174+
chatty=True, connectionchatty=True,
4175+
sni_name=hostname)
4176+
if CAN_GET_SELECTED_OPENSSL_GROUP:
4177+
self.assertEqual(stats['group'], "secp384r1")
4178+
4179+
# server auto, client secp384r1
4180+
client_context, server_context, hostname = testing_context()
4181+
client_context.set_groups("secp384r1")
4182+
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
4183+
stats = server_params_test(client_context, server_context,
4184+
chatty=True, connectionchatty=True,
4185+
sni_name=hostname)
4186+
if CAN_GET_SELECTED_OPENSSL_GROUP:
4187+
self.assertEqual(stats['group'], "secp384r1")
4188+
4189+
# server / client curve mismatch
4190+
client_context, server_context, hostname = testing_context()
4191+
client_context.set_groups("prime256v1")
4192+
server_context.set_groups("secp384r1")
4193+
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
4194+
with self.assertRaises(ssl.SSLError):
4195+
server_params_test(client_context, server_context,
4196+
chatty=True, connectionchatty=True,
4197+
sni_name=hostname)
4198+
41484199
def test_selected_alpn_protocol(self):
41494200
# selected_alpn_protocol() is None unless ALPN is used.
41504201
client_context, server_context, hostname = testing_context()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:mod:`ssl` can now get and set groups used for key agreement.

Modules/_ssl.c

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2176,6 +2176,33 @@ _ssl__SSLSocket_cipher_impl(PySSLSocket *self)
21762176
return cipher_to_tuple(current);
21772177
}
21782178

2179+
/*[clinic input]
2180+
@critical_section
2181+
_ssl._SSLSocket.group
2182+
[clinic start generated code]*/
2183+
2184+
static PyObject *
2185+
_ssl__SSLSocket_group_impl(PySSLSocket *self)
2186+
/*[clinic end generated code: output=9c168ee877017b95 input=5f187d8bf0d433b7]*/
2187+
{
2188+
#if OPENSSL_VERSION_NUMBER >= 0x30200000L
2189+
const char *group_name;
2190+
2191+
if (self->ssl == NULL) {
2192+
Py_RETURN_NONE;
2193+
}
2194+
group_name = SSL_get0_group_name(self->ssl);
2195+
if (group_name == NULL) {
2196+
Py_RETURN_NONE;
2197+
}
2198+
return PyUnicode_DecodeFSDefault(group_name);
2199+
#else
2200+
PyErr_SetString(PyExc_NotImplementedError,
2201+
"Getting selected group requires OpenSSL 3.2 or later.");
2202+
return NULL;
2203+
#endif
2204+
}
2205+
21792206
/*[clinic input]
21802207
@critical_section
21812208
_ssl._SSLSocket.version
@@ -3240,6 +3267,7 @@ static PyMethodDef PySSLMethods[] = {
32403267
_SSL__SSLSOCKET_GETPEERCERT_METHODDEF
32413268
_SSL__SSLSOCKET_GET_CHANNEL_BINDING_METHODDEF
32423269
_SSL__SSLSOCKET_CIPHER_METHODDEF
3270+
_SSL__SSLSOCKET_GROUP_METHODDEF
32433271
_SSL__SSLSOCKET_SHARED_CIPHERS_METHODDEF
32443272
_SSL__SSLSOCKET_VERSION_METHODDEF
32453273
_SSL__SSLSOCKET_SELECTED_ALPN_PROTOCOL_METHODDEF
@@ -3622,6 +3650,89 @@ _ssl__SSLContext_get_ciphers_impl(PySSLContext *self)
36223650

36233651
}
36243652

3653+
/*[clinic input]
3654+
@critical_section
3655+
_ssl._SSLContext.set_groups
3656+
grouplist: str
3657+
/
3658+
[clinic start generated code]*/
3659+
3660+
static PyObject *
3661+
_ssl__SSLContext_set_groups_impl(PySSLContext *self, const char *grouplist)
3662+
/*[clinic end generated code: output=0b5d05dfd371ffd0 input=2cc64cef21930741]*/
3663+
{
3664+
if (!SSL_CTX_set1_groups_list(self->ctx, grouplist)) {
3665+
_setSSLError(get_state_ctx(self), "unrecognized group", 0, __FILE__, __LINE__);
3666+
return NULL;
3667+
}
3668+
Py_RETURN_NONE;
3669+
}
3670+
3671+
/*[clinic input]
3672+
@critical_section
3673+
_ssl._SSLContext.get_groups
3674+
*
3675+
include_aliases: bool = False
3676+
[clinic start generated code]*/
3677+
3678+
static PyObject *
3679+
_ssl__SSLContext_get_groups_impl(PySSLContext *self, int include_aliases)
3680+
/*[clinic end generated code: output=6d6209dd1051529b input=3e8ee5deb277dcc5]*/
3681+
{
3682+
#if OPENSSL_VERSION_NUMBER >= 0x30500000L
3683+
STACK_OF(OPENSSL_CSTRING) *groups = NULL;
3684+
const char *group;
3685+
int i, num;
3686+
PyObject *item, *result = NULL;
3687+
3688+
// This "groups" object is dynamically allocated, but the strings inside
3689+
// it are internal constants which shouldn't ever be modified or freed.
3690+
if ((groups = sk_OPENSSL_CSTRING_new_null()) == NULL) {
3691+
_setSSLError(get_state_ctx(self), "Can't allocate stack", 0, __FILE__, __LINE__);
3692+
goto error;
3693+
}
3694+
3695+
if (!SSL_CTX_get0_implemented_groups(self->ctx, include_aliases, groups)) {
3696+
_setSSLError(get_state_ctx(self), "Can't get groups", 0, __FILE__, __LINE__);
3697+
goto error;
3698+
}
3699+
3700+
num = sk_OPENSSL_CSTRING_num(groups);
3701+
result = PyList_New(num);
3702+
if (result == NULL) {
3703+
_setSSLError(get_state_ctx(self), "Can't allocate list", 0, __FILE__, __LINE__);
3704+
goto error;
3705+
}
3706+
3707+
for (i = 0; i < num; ++i) {
3708+
// There's no allocation here, so group won't ever be NULL.
3709+
group = sk_OPENSSL_CSTRING_value(groups, i);
3710+
assert(group != NULL);
3711+
3712+
// Group names are plain ASCII, so there's no chance of a decoding
3713+
// error here. However, an allocation failure could occur when
3714+
// constructing the Unicode version of the names.
3715+
item = PyUnicode_DecodeASCII(group, strlen(group), "strict");
3716+
if (item == NULL) {
3717+
_setSSLError(get_state_ctx(self), "Can't allocate group name", 0, __FILE__, __LINE__);
3718+
goto error;
3719+
}
3720+
3721+
PyList_SET_ITEM(result, i, item);
3722+
}
3723+
3724+
sk_OPENSSL_CSTRING_free(groups);
3725+
return result;
3726+
error:
3727+
Py_XDECREF(result);
3728+
sk_OPENSSL_CSTRING_free(groups);
3729+
return NULL;
3730+
#else
3731+
PyErr_SetString(PyExc_NotImplementedError,
3732+
"Getting implemented groups requires OpenSSL 3.5 or later.");
3733+
return NULL;
3734+
#endif
3735+
}
36253736

36263737
static int
36273738
do_protocol_selection(int alpn, unsigned char **out, unsigned char *outlen,
@@ -5472,6 +5583,7 @@ static struct PyMethodDef context_methods[] = {
54725583
_SSL__SSLCONTEXT__WRAP_SOCKET_METHODDEF
54735584
_SSL__SSLCONTEXT__WRAP_BIO_METHODDEF
54745585
_SSL__SSLCONTEXT_SET_CIPHERS_METHODDEF
5586+
_SSL__SSLCONTEXT_SET_GROUPS_METHODDEF
54755587
_SSL__SSLCONTEXT__SET_ALPN_PROTOCOLS_METHODDEF
54765588
_SSL__SSLCONTEXT_LOAD_CERT_CHAIN_METHODDEF
54775589
_SSL__SSLCONTEXT_LOAD_DH_PARAMS_METHODDEF
@@ -5482,6 +5594,7 @@ static struct PyMethodDef context_methods[] = {
54825594
_SSL__SSLCONTEXT_CERT_STORE_STATS_METHODDEF
54835595
_SSL__SSLCONTEXT_GET_CA_CERTS_METHODDEF
54845596
_SSL__SSLCONTEXT_GET_CIPHERS_METHODDEF
5597+
_SSL__SSLCONTEXT_GET_GROUPS_METHODDEF
54855598
_SSL__SSLCONTEXT_SET_PSK_CLIENT_CALLBACK_METHODDEF
54865599
_SSL__SSLCONTEXT_SET_PSK_SERVER_CALLBACK_METHODDEF
54875600
{NULL, NULL} /* sentinel */

0 commit comments

Comments
 (0)