Skip to content

Commit 44ca856

Browse files
committed
Expose set_groups functions
This allows pyOpenSSL to restrict the groups allowed to be used. E.g. for restricting the groups to post-quantum hybrid groups (e.g. X25519MLKEM768) to always ensure that post-quantum cryptography is used. This commit uses the set_groups as public API since that is the preferred naming. Internally for the API we use the set_curves API since this name is available on all OpenSSL implementations and OpenSSL forks. Signed-off-by: Arne Schwabe <arne@rfc2549.org>
1 parent f72218e commit 44ca856

File tree

4 files changed

+128
-1
lines changed

4 files changed

+128
-1
lines changed

CHANGELOG.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@ Changelog
44
Versions are year-based with a strict backward-compatibility policy.
55
The third digit is only for regressions.
66

7+
26.3.0 (UNRELEASED)
8+
-------------------
9+
10+
Backward-incompatible changes:
11+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
13+
Deprecations:
14+
^^^^^^^^^^^^^
15+
16+
Changes:
17+
^^^^^^^^
18+
19+
- Added ``OpenSSL.SSL.Context.set_groups`` and ``OpenSSL.SSL.Connection.set_groups`` to set allowed groups/curves.
20+
- The minimum ``cryptography`` version is now 47.0.0.
21+
722
26.0.0 (2026-03-15)
823
-------------------
924

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def find_meta(meta):
9393
packages=find_packages(where="src"),
9494
package_dir={"": "src"},
9595
install_requires=[
96-
"cryptography>=46.0.0,<47",
96+
"cryptography>=47.0.0,<48",
9797
(
9898
"typing-extensions>=4.9; "
9999
"python_version < '3.13' and python_version >= '3.8'"

src/OpenSSL/SSL.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,6 +1531,19 @@ def set_tls13_ciphersuites(self, ciphersuites: bytes) -> None:
15311531
_lib.SSL_CTX_set_ciphersuites(self._context, ciphersuites) == 1
15321532
)
15331533

1534+
@_require_not_used
1535+
def set_groups(self, groups: bytes) -> None:
1536+
"""
1537+
Set the supported groups/curves in this SSL Session.
1538+
"""
1539+
if not isinstance(groups, bytes):
1540+
raise TypeError("groups must be a byte string.")
1541+
1542+
# We use the newer name (groups) in our public API and
1543+
# use the legacy/more compatible name in the internal API
1544+
rc = _lib.SSL_CTX_set1_curves_list(self._context, groups)
1545+
_openssl_assert(rc == 1)
1546+
15341547
@_require_not_used
15351548
def set_client_ca_list(
15361549
self, certificate_authorities: Sequence[X509Name]
@@ -3249,6 +3262,18 @@ def get_group_name(self) -> str | None:
32493262

32503263
return _ffi.string(group_name).decode("utf-8")
32513264

3265+
def set_groups(self, groups: bytes) -> None:
3266+
"""
3267+
Set the supported groups/curves in this SSL Session.
3268+
"""
3269+
if not isinstance(groups, bytes):
3270+
raise TypeError("groups must be a byte string.")
3271+
3272+
# We use the newer name (groups) in our public API and
3273+
# use the legacy/more compatible name in the internal API
3274+
rc = _lib.SSL_set1_curves_list(self._ssl, groups)
3275+
_openssl_assert(rc == 1)
3276+
32523277
def request_ocsp(self) -> None:
32533278
"""
32543279
Called to request that the server sends stapled OCSP data, if

tests/test_ssl.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3581,6 +3581,93 @@ def test_get_group_name(self) -> None:
35813581

35823582
assert server_group_name == client_group_name
35833583

3584+
@pytest.mark.skipif(
3585+
not getattr(_lib, "Cryptography_HAS_SSL_GET0_GROUP_NAME", None),
3586+
reason="SSL_get0_group_name unavailable",
3587+
)
3588+
def test_set_groups_context(self) -> None:
3589+
"""
3590+
`Context.set_groups` forces the use of a specific curve/groups list.
3591+
"""
3592+
3593+
def loopback_x448_client_factory(
3594+
socket: socket, version: int = SSLv23_METHOD
3595+
) -> Connection:
3596+
context = Context(version)
3597+
context.set_groups(b"X448")
3598+
client = Connection(context, socket)
3599+
client.set_connect_state()
3600+
return client
3601+
3602+
server, client = loopback(client_factory=loopback_x448_client_factory)
3603+
server_group_name = server.get_group_name()
3604+
client_group_name = client.get_group_name()
3605+
3606+
assert isinstance(server_group_name, str)
3607+
assert isinstance(client_group_name, str)
3608+
3609+
assert server_group_name.lower() == "x448"
3610+
assert client_group_name.lower() == "x448"
3611+
3612+
@pytest.mark.skipif(
3613+
not getattr(_lib, "Cryptography_HAS_SSL_GET0_GROUP_NAME", None),
3614+
reason="SSL_get0_group_name unavailable",
3615+
)
3616+
def test_set_groups_session(self) -> None:
3617+
"""
3618+
`Connection.set_groups` forces the use of a specific curve/groups list.
3619+
"""
3620+
3621+
def loopback_x448_server_factory(
3622+
socket: socket, version: int = SSLv23_METHOD
3623+
) -> Connection:
3624+
connection = loopback_server_factory(socket, version)
3625+
connection.set_groups(b"X448")
3626+
return connection
3627+
3628+
server, client = loopback(server_factory=loopback_x448_server_factory)
3629+
server_group_name = server.get_group_name()
3630+
client_group_name = client.get_group_name()
3631+
3632+
assert isinstance(server_group_name, str)
3633+
assert isinstance(client_group_name, str)
3634+
3635+
assert server_group_name.lower() == "x448"
3636+
assert client_group_name.lower() == "x448"
3637+
3638+
def test_set_groups_mismatch(self) -> None:
3639+
"""
3640+
Forces different group lists on client and server so that a connection
3641+
should not be possible.
3642+
"""
3643+
3644+
def loopback_x25519_client_factory(
3645+
socket: socket, version: int = SSLv23_METHOD
3646+
) -> Connection:
3647+
connection = loopback_client_factory(socket, version)
3648+
connection.set_groups(b"X25519")
3649+
return connection
3650+
3651+
def loopback_x448_server_factory(
3652+
socket: socket, version: int = SSLv23_METHOD
3653+
) -> Connection:
3654+
ctx = Context(version)
3655+
ctx.set_groups(b"X448")
3656+
3657+
ctx.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem))
3658+
ctx.use_certificate(
3659+
load_certificate(FILETYPE_PEM, server_cert_pem)
3660+
)
3661+
server = Connection(ctx, socket)
3662+
server.set_accept_state()
3663+
return server
3664+
3665+
with pytest.raises(SSL.Error):
3666+
loopback(
3667+
client_factory=loopback_x25519_client_factory,
3668+
server_factory=loopback_x448_server_factory,
3669+
)
3670+
35843671
def test_wantReadError(self) -> None:
35853672
"""
35863673
`Connection.bio_read` raises `OpenSSL.SSL.WantReadError` if there are

0 commit comments

Comments
 (0)