diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index a9930183f9a400..ff6053cb7e94d9 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -1290,6 +1290,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`` @@ -1647,6 +1654,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 @@ -1672,6 +1698,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 0f65317633ba70..07fe90d64e4e4d 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -279,6 +279,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 493377b4c25040..5e7dda3a3715a1 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1005,6 +1005,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 5dfea2f479d5fe..6908cbf78f349e 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -496,6 +496,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 85ced09d29d338..da2ed7422c9deb 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1003,6 +1003,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 6018d98d156a65..b1f411945e7856 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -1772,6 +1772,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 86fb8990636692..5b8762bcdc25d1 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. @@ -1210,6 +1214,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 9e519537ca5ed3..354f5846eb38bd 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 24c243e330d4bf..008d91d36ff250 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2169,6 +2169,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 @@ -3231,6 +3258,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 @@ -3610,6 +3638,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, @@ -5457,6 +5568,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 @@ -5467,6 +5579,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 7027d87379283d..5b80fab0abb45e 100644 --- a/Modules/clinic/_ssl.c.h +++ b/Modules/clinic/_ssl.c.h @@ -196,6 +196,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" @@ -969,6 +992,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" @@ -3014,4 +3142,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=1adc3780d8ca682a input=a9049054013a1b77]*/ +/*[clinic end generated code: output=c409bdf3c123b28b 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