diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index ae2e324d0abaa4..f2a1a190b525c7 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -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`` @@ -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 @@ -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 + `_. + + .. 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 diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 706a816f888b30..f89df8a9d5d814 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -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 ------- diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index c461bc1786ddf4..a88ae1f33bc81b 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1007,6 +1007,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(imag)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(importlib)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(in_fd)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(include_aliases)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(incoming)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(index)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(indexgroup)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index 72c2051bd97660..f9fa5dc479f71d 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -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) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index d378fcae26cf35..646e41f05fcf73 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1005,6 +1005,7 @@ extern "C" { INIT_ID(imag), \ INIT_ID(importlib), \ INIT_ID(in_fd), \ + INIT_ID(include_aliases), \ INIT_ID(incoming), \ INIT_ID(index), \ INIT_ID(indexgroup), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index e516211f6c6cbc..16a6bd5ac3405a 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -1780,6 +1780,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(include_aliases); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(incoming); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Lib/ssl.py b/Lib/ssl.py index 7e3c4cbd6bbf8e..452118822c1277 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -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. @@ -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() diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index f123f6ece40669..790b91b7604ebe 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -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 = {} @@ -960,6 +962,19 @@ 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() + self.assertIsNone(ctx.set_groups('P-256:X25519')) + 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() + # By default, only return official IANA names. + self.assertNotIn('P-256', ctx.get_groups()) + 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) @@ -2701,6 +2716,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 @@ -3851,6 +3868,8 @@ def test_no_shared_ciphers(self): with self.assertRaises(OSError): s.connect((HOST, server.port)) self.assertIn("NO_SHARED_CIPHER", server.conn_errors[0]) + self.assertIsNone(s.cipher()) + self.assertIsNone(s.group()) def test_version_basic(self): """ @@ -4126,6 +4145,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() diff --git a/Misc/NEWS.d/next/Library/2025-07-04-23-45-00.gh-issue-136306.O1YLIU.rst b/Misc/NEWS.d/next/Library/2025-07-04-23-45-00.gh-issue-136306.O1YLIU.rst new file mode 100644 index 00000000000000..5556c512681b78 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-04-23-45-00.gh-issue-136306.O1YLIU.rst @@ -0,0 +1 @@ +:mod:`ssl` can now get and set groups used for key agreement. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 014e624f6c2f00..1229ef9e412b78 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -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 @@ -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 @@ -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; + int 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_DecodeASCII(group, strlen(group), "strict"); + 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, @@ -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 @@ -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 */ diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h index c6e2abd4d93474..a4f540d22abcd2 100644 --- a/Modules/clinic/_ssl.c.h +++ b/Modules/clinic/_ssl.c.h @@ -195,6 +195,29 @@ _ssl__SSLSocket_cipher(PyObject *self, PyObject *Py_UNUSED(ignored)) return return_value; } +PyDoc_STRVAR(_ssl__SSLSocket_group__doc__, +"group($self, /)\n" +"--\n" +"\n"); + +#define _SSL__SSLSOCKET_GROUP_METHODDEF \ + {"group", (PyCFunction)_ssl__SSLSocket_group, METH_NOARGS, _ssl__SSLSocket_group__doc__}, + +static PyObject * +_ssl__SSLSocket_group_impl(PySSLSocket *self); + +static PyObject * +_ssl__SSLSocket_group(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = _ssl__SSLSocket_group_impl((PySSLSocket *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + PyDoc_STRVAR(_ssl__SSLSocket_version__doc__, "version($self, /)\n" "--\n" @@ -859,6 +882,111 @@ _ssl__SSLContext_get_ciphers(PyObject *self, PyObject *Py_UNUSED(ignored)) return return_value; } +PyDoc_STRVAR(_ssl__SSLContext_set_groups__doc__, +"set_groups($self, grouplist, /)\n" +"--\n" +"\n"); + +#define _SSL__SSLCONTEXT_SET_GROUPS_METHODDEF \ + {"set_groups", (PyCFunction)_ssl__SSLContext_set_groups, METH_O, _ssl__SSLContext_set_groups__doc__}, + +static PyObject * +_ssl__SSLContext_set_groups_impl(PySSLContext *self, const char *grouplist); + +static PyObject * +_ssl__SSLContext_set_groups(PyObject *self, PyObject *arg) +{ + PyObject *return_value = NULL; + const char *grouplist; + + if (!PyUnicode_Check(arg)) { + _PyArg_BadArgument("set_groups", "argument", "str", arg); + goto exit; + } + Py_ssize_t grouplist_length; + grouplist = PyUnicode_AsUTF8AndSize(arg, &grouplist_length); + if (grouplist == NULL) { + goto exit; + } + if (strlen(grouplist) != (size_t)grouplist_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } + Py_BEGIN_CRITICAL_SECTION(self); + return_value = _ssl__SSLContext_set_groups_impl((PySSLContext *)self, grouplist); + Py_END_CRITICAL_SECTION(); + +exit: + return return_value; +} + +PyDoc_STRVAR(_ssl__SSLContext_get_groups__doc__, +"get_groups($self, /, *, include_aliases=False)\n" +"--\n" +"\n"); + +#define _SSL__SSLCONTEXT_GET_GROUPS_METHODDEF \ + {"get_groups", _PyCFunction_CAST(_ssl__SSLContext_get_groups), METH_FASTCALL|METH_KEYWORDS, _ssl__SSLContext_get_groups__doc__}, + +static PyObject * +_ssl__SSLContext_get_groups_impl(PySSLContext *self, int include_aliases); + +static PyObject * +_ssl__SSLContext_get_groups(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 1 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(include_aliases), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"include_aliases", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "get_groups", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[1]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0; + int include_aliases = 0; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, + /*minpos*/ 0, /*maxpos*/ 0, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + if (!noptargs) { + goto skip_optional_kwonly; + } + include_aliases = PyObject_IsTrue(args[0]); + if (include_aliases < 0) { + goto exit; + } +skip_optional_kwonly: + Py_BEGIN_CRITICAL_SECTION(self); + return_value = _ssl__SSLContext_get_groups_impl((PySSLContext *)self, include_aliases); + Py_END_CRITICAL_SECTION(); + +exit: + return return_value; +} + PyDoc_STRVAR(_ssl__SSLContext__set_alpn_protocols__doc__, "_set_alpn_protocols($self, protos, /)\n" "--\n" @@ -2900,4 +3028,4 @@ _ssl_enum_crls(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObje #ifndef _SSL_ENUM_CRLS_METHODDEF #define _SSL_ENUM_CRLS_METHODDEF #endif /* !defined(_SSL_ENUM_CRLS_METHODDEF) */ -/*[clinic end generated code: output=748650909fec8906 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=ef7dfc2cc05fd2f4 input=a9049054013a1b77]*/ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy