Skip to content

gh-136306: Add support for SSL groups #136307

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Open
39 changes: 39 additions & 0 deletions Doc/library/ssl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,13 @@ SSL sockets also have the following additional methods and attributes:

.. versionadded:: 3.5

.. method:: SSLSocket.group()

Return the group used for doing key agreement on this connection. If no
connection has been established, returns ``None``.

.. versionadded:: next

.. method:: SSLSocket.compression()

Return the compression algorithm being used as a string, or ``None``
Expand Down Expand Up @@ -1641,6 +1648,25 @@ to speed up repeated connections from the same clients.

.. versionadded:: 3.6

.. method:: SSLContext.get_groups(*, include_aliases=False)

Get a list of groups implemented for key agreement, taking into
account the current TLS :attr:`~SSLContext.minimum_version` and
:attr:`~SSLContext.maximum_version` values. For example::

>>> ctx = ssl.create_default_context()
>>> ctx.minimum_version = ssl.TLSVersion.TLSv1_3
>>> ctx.maximum_version = ssl.TLSVersion.TLSv1_3
>>> ctx.get_groups() # doctest: +SKIP
['secp256r1', 'secp384r1', 'secp521r1', 'x25519', 'x448', ...]

By default, this method returns only the preferred IANA names for the
available groups. However, if the ``include_aliases`` parameter is set to
:const:`True` this method will also return any associated aliases such as
the ECDH curve names supported in older versions of OpenSSL.

.. versionadded:: next

.. method:: SSLContext.set_default_verify_paths()

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

.. method:: SSLContext.set_groups(groups)

Set the groups allowed for key agreement for sockets created with this
context. It should be a string in the `OpenSSL group list format
<https://docs.openssl.org/master/man3/SSL_CTX_set1_groups_list/>`_.

.. note::

When connected, the :meth:`SSLSocket.group` method of SSL sockets will
return the group used for key agreement on that connection.

.. versionadded:: next

.. method:: SSLContext.set_alpn_protocols(protocols)

Specify which protocols the socket should advertise during the SSL/TLS
Expand Down
18 changes: 18 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,24 @@ ssl
supports "External PSKs" in TLSv1.3, as described in RFC 9258.
(Contributed by Will Childs-Klein in :gh:`133624`.)

* Added new methods for managing groups used for SSL key agreement

* :meth:`ssl.SSLContext.set_groups` sets the groups allowed for doing
key agreement, extending the previous
:meth:`ssl.SSLContext.set_ecdh_curve` method.
This new API provides the ability to list multiple groups and
supports fixed-field and post-quantum groups in addition to ECDH
curves. This method can also be used to control what key shares
are sent in the TLS handshake.
* :meth:`ssl.SSLSocket.group` returns the group selected for doing key
agreement on the current connection after the TLS handshake completes.
This call requires OpenSSL 3.2 or later.
* :meth:`ssl.SSLContext.get_groups` returns a list of all available key
agreement groups compatible with the minimum and maximum TLS versions
currently set in the context. This call requires OpenSSL 3.5 or later.

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


tarfile
-------
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(imag)
STRUCT_FOR_ID(importlib)
STRUCT_FOR_ID(in_fd)
STRUCT_FOR_ID(include_aliases)
STRUCT_FOR_ID(incoming)
STRUCT_FOR_ID(index)
STRUCT_FOR_ID(indexgroup)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Lib/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,10 @@ def cipher(self):
ssl_version, secret_bits)``."""
return self._sslobj.cipher()

def group(self):
"""Return the currently selected key agreement group name."""
return self._sslobj.group()

def shared_ciphers(self):
"""Return a list of ciphers shared by the client during the handshake or
None if this is not a valid server connection.
Expand Down Expand Up @@ -1206,6 +1210,14 @@ def cipher(self):
else:
return self._sslobj.cipher()

@_sslcopydoc
def group(self):
self._checkClosed()
if self._sslobj is None:
return None
else:
return self._sslobj.group()

@_sslcopydoc
def shared_ciphers(self):
self._checkClosed()
Expand Down
56 changes: 56 additions & 0 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
PROTOCOLS = sorted(ssl._PROTOCOL_NAMES)
HOST = socket_helper.HOST
IS_OPENSSL_3_0_0 = ssl.OPENSSL_VERSION_INFO >= (3, 0, 0)
CAN_GET_SELECTED_OPENSSL_GROUP = ssl.OPENSSL_VERSION_INFO >= (3, 2)
CAN_GET_AVAILABLE_OPENSSL_GROUPS = ssl.OPENSSL_VERSION_INFO >= (3, 5)
PY_SSL_DEFAULT_CIPHERS = sysconfig.get_config_var('PY_SSL_DEFAULT_CIPHERS')

PROTOCOL_TO_TLS_VERSION = {}
Expand Down Expand Up @@ -960,6 +962,26 @@ def test_get_ciphers(self):
len(intersection), 2, f"\ngot: {sorted(names)}\nexpected: {sorted(expected)}"
)

def test_set_groups(self):
ctx = ssl.create_default_context()

# Test valid group list
self.assertIsNone(ctx.set_groups('P-256:X25519'))

# Test invalid group list
self.assertRaises(ssl.SSLError, ctx.set_groups, 'P-256:xxx')

@unittest.skipUnless(CAN_GET_AVAILABLE_OPENSSL_GROUPS,
"OpenSSL version doesn't support getting groups")
def test_get_groups(self):
ctx = ssl.create_default_context()

# P-256 isn't an IANA name, so it shouldn't be returned by default
self.assertNotIn('P-256', ctx.get_groups())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a default group that is known to always be returned?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The set of returned versions varies depending on the OpenSSL version. However, with the default of include_aliases=False, it should be guaranteed that P-256 will not be included since it is an alias. With include_aliases=True, I believe it should be included in all versions, going all the way back to OpenSSL 1.1.1, and I did some local testing to confirm that.


# Aliases like P-256 sbould be returned when include_aliases is set
self.assertIn('P-256', ctx.get_groups(include_aliases=True))

def test_options(self):
# Test default SSLContext options
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
Expand Down Expand Up @@ -2701,6 +2723,8 @@ def server_params_test(client_context, server_context, indata=b"FOO\n",
'session_reused': s.session_reused,
'session': s.session,
})
if CAN_GET_SELECTED_OPENSSL_GROUP:
stats.update({'group': s.group()})
s.close()
stats['server_alpn_protocols'] = server.selected_alpn_protocols
stats['server_shared_ciphers'] = server.shared_ciphers
Expand Down Expand Up @@ -4126,6 +4150,38 @@ def test_ecdh_curve(self):
chatty=True, connectionchatty=True,
sni_name=hostname)

def test_groups(self):
# server secp384r1, client auto
client_context, server_context, hostname = testing_context()

server_context.set_groups("secp384r1")
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
stats = server_params_test(client_context, server_context,
chatty=True, connectionchatty=True,
sni_name=hostname)
if CAN_GET_SELECTED_OPENSSL_GROUP:
self.assertEqual(stats['group'], "secp384r1")

# server auto, client secp384r1
client_context, server_context, hostname = testing_context()
client_context.set_groups("secp384r1")
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
stats = server_params_test(client_context, server_context,
chatty=True, connectionchatty=True,
sni_name=hostname)
if CAN_GET_SELECTED_OPENSSL_GROUP:
self.assertEqual(stats['group'], "secp384r1")

# server / client curve mismatch
client_context, server_context, hostname = testing_context()
client_context.set_groups("prime256v1")
server_context.set_groups("secp384r1")
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
with self.assertRaises(ssl.SSLError):
server_params_test(client_context, server_context,
chatty=True, connectionchatty=True,
sni_name=hostname)

def test_selected_alpn_protocol(self):
# selected_alpn_protocol() is None unless ALPN is used.
client_context, server_context, hostname = testing_context()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:mod:`ssl` can now get and set groups used for key agreement.
113 changes: 113 additions & 0 deletions Modules/_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -2142,6 +2142,33 @@ _ssl__SSLSocket_cipher_impl(PySSLSocket *self)
return cipher_to_tuple(current);
}

/*[clinic input]
@critical_section
_ssl._SSLSocket.group
[clinic start generated code]*/

static PyObject *
_ssl__SSLSocket_group_impl(PySSLSocket *self)
/*[clinic end generated code: output=9c168ee877017b95 input=5f187d8bf0d433b7]*/
{
#if OPENSSL_VERSION_NUMBER >= 0x30200000L
const char *group_name;

if (self->ssl == NULL) {
Py_RETURN_NONE;
}
group_name = SSL_get0_group_name(self->ssl);
if (group_name == NULL) {
Py_RETURN_NONE;
}
return PyUnicode_DecodeFSDefault(group_name);
#else
PyErr_SetString(PyExc_NotImplementedError,
"Getting selected group requires OpenSSL 3.2 or later.");
return NULL;
#endif
}

/*[clinic input]
@critical_section
_ssl._SSLSocket.version
Expand Down Expand Up @@ -3023,6 +3050,7 @@ static PyMethodDef PySSLMethods[] = {
_SSL__SSLSOCKET_GETPEERCERT_METHODDEF
_SSL__SSLSOCKET_GET_CHANNEL_BINDING_METHODDEF
_SSL__SSLSOCKET_CIPHER_METHODDEF
_SSL__SSLSOCKET_GROUP_METHODDEF
_SSL__SSLSOCKET_SHARED_CIPHERS_METHODDEF
_SSL__SSLSOCKET_VERSION_METHODDEF
_SSL__SSLSOCKET_SELECTED_ALPN_PROTOCOL_METHODDEF
Expand Down Expand Up @@ -3402,6 +3430,89 @@ _ssl__SSLContext_get_ciphers_impl(PySSLContext *self)

}

/*[clinic input]
@critical_section
_ssl._SSLContext.set_groups
grouplist: str
/
[clinic start generated code]*/

static PyObject *
_ssl__SSLContext_set_groups_impl(PySSLContext *self, const char *grouplist)
/*[clinic end generated code: output=0b5d05dfd371ffd0 input=2cc64cef21930741]*/
{
if (!SSL_CTX_set1_groups_list(self->ctx, grouplist)) {
_setSSLError(get_state_ctx(self), "unrecognized group", 0, __FILE__, __LINE__);
return NULL;
}
Py_RETURN_NONE;
}

/*[clinic input]
@critical_section
_ssl._SSLContext.get_groups
*
include_aliases: bool = False
[clinic start generated code]*/

static PyObject *
_ssl__SSLContext_get_groups_impl(PySSLContext *self, int include_aliases)
/*[clinic end generated code: output=6d6209dd1051529b input=3e8ee5deb277dcc5]*/
{
#if OPENSSL_VERSION_NUMBER >= 0x30500000L
STACK_OF(OPENSSL_CSTRING) *groups = NULL;
const char *group;
size_t i, num;
PyObject *item, *result = NULL;

// This "groups" object is dynamically allocated, but the strings inside
// it are internal constants which shouldn't ever be modified or freed.
if ((groups = sk_OPENSSL_CSTRING_new_null()) == NULL) {
_setSSLError(get_state_ctx(self), "Can't allocate stack", 0, __FILE__, __LINE__);
goto error;
}

if (!SSL_CTX_get0_implemented_groups(self->ctx, include_aliases, groups)) {
_setSSLError(get_state_ctx(self), "Can't get groups", 0, __FILE__, __LINE__);
goto error;
}

num = sk_OPENSSL_CSTRING_num(groups);
result = PyList_New(num);
if (result == NULL) {
_setSSLError(get_state_ctx(self), "Can't allocate list", 0, __FILE__, __LINE__);
goto error;
}

for (i = 0; i < num; ++i) {
// There's no allocation here, so group won't ever be NULL.
group = sk_OPENSSL_CSTRING_value(groups, i);
assert(group != NULL);

// Group names are plain ASCII, so there's no chance of a decoding
// error here. However, an allocation failure could occur when
// constructing the Unicode version of the names.
item = PyUnicode_DecodeFSDefault(group);
if (item == NULL) {
_setSSLError(get_state_ctx(self), "Can't allocate group name", 0, __FILE__, __LINE__);
goto error;
}

PyList_SET_ITEM(result, i, item);
}

sk_OPENSSL_CSTRING_free(groups);
return result;
error:
Py_XDECREF(result);
sk_OPENSSL_CSTRING_free(groups);
return NULL;
#else
PyErr_SetString(PyExc_NotImplementedError,
"Getting implemented groups requires OpenSSL 3.5 or later.");
return NULL;
#endif
}

static int
do_protocol_selection(int alpn, unsigned char **out, unsigned char *outlen,
Expand Down Expand Up @@ -5249,6 +5360,7 @@ static struct PyMethodDef context_methods[] = {
_SSL__SSLCONTEXT__WRAP_SOCKET_METHODDEF
_SSL__SSLCONTEXT__WRAP_BIO_METHODDEF
_SSL__SSLCONTEXT_SET_CIPHERS_METHODDEF
_SSL__SSLCONTEXT_SET_GROUPS_METHODDEF
_SSL__SSLCONTEXT__SET_ALPN_PROTOCOLS_METHODDEF
_SSL__SSLCONTEXT_LOAD_CERT_CHAIN_METHODDEF
_SSL__SSLCONTEXT_LOAD_DH_PARAMS_METHODDEF
Expand All @@ -5259,6 +5371,7 @@ static struct PyMethodDef context_methods[] = {
_SSL__SSLCONTEXT_CERT_STORE_STATS_METHODDEF
_SSL__SSLCONTEXT_GET_CA_CERTS_METHODDEF
_SSL__SSLCONTEXT_GET_CIPHERS_METHODDEF
_SSL__SSLCONTEXT_GET_GROUPS_METHODDEF
_SSL__SSLCONTEXT_SET_PSK_CLIENT_CALLBACK_METHODDEF
_SSL__SSLCONTEXT_SET_PSK_SERVER_CALLBACK_METHODDEF
{NULL, NULL} /* sentinel */
Expand Down
Loading
Loading