From d724c26b93daaaa9e08c03a5945885b6f9f9f6eb Mon Sep 17 00:00:00 2001 From: toddabrams Date: Wed, 10 Sep 2025 16:40:45 -0400 Subject: [PATCH 01/11] Add get_group_name to expose SSL_get0_group_name --- CHANGELOG.rst | 1 + src/OpenSSL/SSL.py | 12 ++++++++++++ tests/test_ssl.py | 23 +++++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b24e08aa..10822b07 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,7 @@ Deprecations: Changes: ^^^^^^^^ +- Added ``OpenSSL.SSL.Connection.get_group_name`` to determine which group name was negotiated. - Added ``OpenSSL.SSL.Context.set_tls13_ciphersuites`` that allows the allowed TLS 1.3 ciphers. 25.2.0 (UNRELEASED) diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py index 7760142b..0af4d6be 100644 --- a/src/OpenSSL/SSL.py +++ b/src/OpenSSL/SSL.py @@ -3222,6 +3222,18 @@ def get_selected_srtp_profile(self) -> bytes: return _ffi.string(profile.name) + def get_group_name(self) -> str | None: + """ + Get the name of the negotiated group for the key exchange. + + :return: A string giving the group name or :data:`None`. + """ + group_name = _lib.SSL_get0_group_name(self._ssl) + if group_name == _ffi.NULL: + return None + + return _ffi.string(group_name).decode("utf-8") + def request_ocsp(self) -> None: """ Called to request that the server sends stapled OCSP data, if diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 3cab6f15..a4e25ded 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3434,6 +3434,29 @@ def test_get_protocol_version(self) -> None: assert server_protocol_version == client_protocol_version + def test_get_group_name_before_connect(self) -> None: + """ + `Connection.get_group_name()` returns `None` if no connection + has been established. + """ + ctx = Context(TLS_METHOD) + conn = Connection(ctx, None) + assert conn.get_group_name() is None + + def test_get_group_name(self) -> None: + """ + `Connection.get_group_name()` returns a string giving the + name of the connection's negotiated key exchange group. + """ + server, client = loopback() + client_group_name = client.get_group_name() + server_group_name = server.get_group_name() + + assert isinstance(client_group_name, str) + assert isinstance(server_group_name, str) + + assert client_group_name == server_group_name + def test_wantReadError(self) -> None: """ `Connection.bio_read` raises `OpenSSL.SSL.WantReadError` if there are From 21699de77a5a321d7e9a585e29c7f3903249396a Mon Sep 17 00:00:00 2001 From: toddabrams Date: Sat, 13 Sep 2025 19:48:22 -0400 Subject: [PATCH 02/11] Update test with consistent ordering --- tests/test_ssl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index a4e25ded..9efe264c 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3449,13 +3449,13 @@ def test_get_group_name(self) -> None: name of the connection's negotiated key exchange group. """ server, client = loopback() - client_group_name = client.get_group_name() server_group_name = server.get_group_name() + client_group_name = client.get_group_name() - assert isinstance(client_group_name, str) assert isinstance(server_group_name, str) + assert isinstance(client_group_name, str) - assert client_group_name == server_group_name + assert server_group_name == client_group_name def test_wantReadError(self) -> None: """ From 32d979ac619107b033ccfe46745d122e4d62da48 Mon Sep 17 00:00:00 2001 From: toddabrams Date: Sat, 13 Sep 2025 19:51:26 -0400 Subject: [PATCH 03/11] Add decorator for the get group guard flag --- src/OpenSSL/SSL.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py index 0af4d6be..3b576e0a 100644 --- a/src/OpenSSL/SSL.py +++ b/src/OpenSSL/SSL.py @@ -815,6 +815,10 @@ def explode(*args, **kwargs): # type: ignore[no-untyped-def] getattr(_lib, "Cryptography_HAS_KEYLOG", 0), "Key logging not available" ) +_requires_ssl_get0_group_name = _make_requires( + getattr(_lib, "Cryptography_HAS_SSL_GET0_GROUP_NAME", 0), + "Getting group name is not supported by the linked OpenSSL version" +) class Session: """ @@ -3222,6 +3226,7 @@ def get_selected_srtp_profile(self) -> bytes: return _ffi.string(profile.name) + @_requires_ssl_get0_group_name def get_group_name(self) -> str | None: """ Get the name of the negotiated group for the key exchange. From 877534c225b761f447a88279d3d6a60bdb0b841d Mon Sep 17 00:00:00 2001 From: toddabrams Date: Sat, 13 Sep 2025 20:05:20 -0400 Subject: [PATCH 04/11] ruff format --- src/OpenSSL/SSL.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py index 41ed898c..4b0efe67 100644 --- a/src/OpenSSL/SSL.py +++ b/src/OpenSSL/SSL.py @@ -817,9 +817,10 @@ def explode(*args, **kwargs): # type: ignore[no-untyped-def] _requires_ssl_get0_group_name = _make_requires( getattr(_lib, "Cryptography_HAS_SSL_GET0_GROUP_NAME", 0), - "Getting group name is not supported by the linked OpenSSL version" + "Getting group name is not supported by the linked OpenSSL version", ) + class Session: """ A class representing an SSL session. A session defines certain connection From fb7a1eb10877fce8db2ff7b1eeb9bb589c9ae0ca Mon Sep 17 00:00:00 2001 From: toddabrams Date: Sat, 13 Sep 2025 20:05:34 -0400 Subject: [PATCH 05/11] Skip test if not Cryptography_HAS_SSL_GET0_GROUP_NAME --- tests/test_ssl.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 3d351787..750a5076 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3437,6 +3437,10 @@ def test_get_protocol_version(self) -> None: assert server_protocol_version == client_protocol_version + @pytest.mark.skipif( + not getattr(_lib, "Cryptography_HAS_SSL_GET0_GROUP_NAME", None), + reason="SSL_get0_group_name unavailable", + ) def test_get_group_name_before_connect(self) -> None: """ `Connection.get_group_name()` returns `None` if no connection @@ -3446,6 +3450,10 @@ def test_get_group_name_before_connect(self) -> None: conn = Connection(ctx, None) assert conn.get_group_name() is None + @pytest.mark.skipif( + not getattr(_lib, "Cryptography_HAS_SSL_GET0_GROUP_NAME", None), + reason="SSL_get0_group_name unavailable", + ) def test_get_group_name(self) -> None: """ `Connection.get_group_name()` returns a string giving the From e2f9d22301d03305cb389111eeb3489966b68ad5 Mon Sep 17 00:00:00 2001 From: toddabrams Date: Sun, 14 Sep 2025 18:47:07 -0400 Subject: [PATCH 06/11] Add test case for if group returns NULL --- tests/test_ssl.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_ssl.py b/tests/test_ssl.py index 750a5076..b4506afc 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3450,6 +3450,23 @@ def test_get_group_name_before_connect(self) -> None: conn = Connection(ctx, None) assert conn.get_group_name() is None + @pytest.mark.skipif( + not getattr(_lib, "Cryptography_HAS_SSL_GET0_GROUP_NAME", None), + reason="SSL_get0_group_name unavailable", + ) + def test_group_name_null_case( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """ + `Connection.get_group_name()` returns `None` when SSL_get0_group_name + returns NULL. + """ + monkeypatch.setattr(_lib, "SSL_get0_group_name", lambda ssl: _ffi.NULL) + + server, client = loopback() + assert server.get_group_name() is None + assert client.get_group_name() is None + @pytest.mark.skipif( not getattr(_lib, "Cryptography_HAS_SSL_GET0_GROUP_NAME", None), reason="SSL_get0_group_name unavailable", From d5dc30adcec520f7b13a68c2e0648b16c0d13708 Mon Sep 17 00:00:00 2001 From: toddabrams Date: Sun, 14 Sep 2025 18:48:31 -0400 Subject: [PATCH 07/11] Fix test - add guard to check for session --- src/OpenSSL/SSL.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py index 4b0efe67..89cfbf5b 100644 --- a/src/OpenSSL/SSL.py +++ b/src/OpenSSL/SSL.py @@ -3214,6 +3214,9 @@ def get_group_name(self) -> str | None: :return: A string giving the group name or :data:`None`. """ + session = _lib.SSL_get_session(self._ssl) + if session == _ffi.NULL: + return None group_name = _lib.SSL_get0_group_name(self._ssl) if group_name == _ffi.NULL: return None From 9a06dc81141a8116a94702460f6ed1061bd878ff Mon Sep 17 00:00:00 2001 From: toddabrams Date: Tue, 16 Sep 2025 17:26:34 -0400 Subject: [PATCH 08/11] Set cryptography min to 46.0.0 --- CHANGELOG.rst | 2 ++ noxfile.py | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 53b13b26..6c9a3e1f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,8 @@ The third digit is only for regressions. Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +- The minimum ``cryptography`` version is now 46.0.0. + Deprecations: ^^^^^^^^^^^^^ diff --git a/noxfile.py b/noxfile.py index f46fdf9a..07b767c1 100644 --- a/noxfile.py +++ b/noxfile.py @@ -3,7 +3,7 @@ nox.options.reuse_existing_virtualenvs = True nox.options.default_venv_backend = "uv|virtualenv" -MINIMUM_CRYPTOGRAPHY_VERSION = "45.0.7" +MINIMUM_CRYPTOGRAPHY_VERSION = "46.0.0" @nox.session diff --git a/setup.py b/setup.py index 6823b140..3f80930a 100644 --- a/setup.py +++ b/setup.py @@ -94,7 +94,7 @@ def find_meta(meta): packages=find_packages(where="src"), package_dir={"": "src"}, install_requires=[ - "cryptography>=45.0.7,<46", + "cryptography>=46.0.0,<47", ( "typing-extensions>=4.9; " "python_version < '3.13' and python_version >= '3.8'" From cb720a2e64fdf4a25512fc172437ed0e1c8e8c5d Mon Sep 17 00:00:00 2001 From: toddabrams Date: Wed, 17 Sep 2025 15:03:08 -0400 Subject: [PATCH 09/11] Drop Python 3.7 support --- .github/workflows/ci.yml | 2 -- doc/introduction.rst | 2 +- setup.py | 3 +-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcccdf5d..95697c21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,6 @@ jobs: matrix: PYTHON: # Base builds - - {VERSION: "3.7", NOXSESSION: "tests", OS: "ubuntu-22.04"} - {VERSION: "3.8", NOXSESSION: "tests"} - {VERSION: "3.9", NOXSESSION: "tests"} - {VERSION: "3.10", NOXSESSION: "tests"} @@ -30,7 +29,6 @@ jobs: - {VERSION: "3.13", NOXSESSION: "tests-cryptography-main"} - {VERSION: "pypy-3.11", NOXSESSION: "tests-cryptography-main"} # -cryptography-minimum - - {VERSION: "3.7", NOXSESSION: "tests-cryptography-minimum", OS: "ubuntu-22.04"} - {VERSION: "3.8", NOXSESSION: "tests-cryptography-minimum"} - {VERSION: "3.9", NOXSESSION: "tests-cryptography-minimum"} - {VERSION: "3.10", NOXSESSION: "tests-cryptography-minimum"} diff --git a/doc/introduction.rst b/doc/introduction.rst index 3c95af98..0873ef31 100644 --- a/doc/introduction.rst +++ b/doc/introduction.rst @@ -14,7 +14,7 @@ Other OpenSSL wrappers for Python at the time were also limited, though in diffe Later it was maintained by `Jean-Paul Calderone`_ who among other things managed to make pyOpenSSL a pure Python project which the current maintainers are *very* grateful for. Over the time the standard library's ``ssl`` module improved, never reaching the completeness of pyOpenSSL's API coverage. -pyOpenSSL remains the only choice for full-featured TLS code in Python versions 3.7+ and PyPy_. +pyOpenSSL remains the only choice for full-featured TLS code in Python versions 3.8+ and PyPy_. Development diff --git a/setup.py b/setup.py index 3f80930a..f0d85725 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,6 @@ def find_meta(meta): "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -90,7 +89,7 @@ def find_meta(meta): "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Networking", ], - python_requires=">=3.7", + python_requires=">=3.8", packages=find_packages(where="src"), package_dir={"": "src"}, install_requires=[ From 5ef8f20ec0ea264c9540607857c0caaa343cf334 Mon Sep 17 00:00:00 2001 From: toddabrams Date: Wed, 17 Sep 2025 15:08:13 -0400 Subject: [PATCH 10/11] Reflect in CHANGELOG.rst that Python 3.7 has been dropped --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0722462a..4a3bcd51 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,7 @@ The third digit is only for regressions. Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +- Dropped support for Python 3.7. - The minimum ``cryptography`` version is now 46.0.0. Deprecations: From 2eed0253ba8a153df6ff8799776e9b4c8ea4dcc0 Mon Sep 17 00:00:00 2001 From: toddabrams Date: Wed, 17 Sep 2025 18:39:06 -0400 Subject: [PATCH 11/11] Add comment about guard against segfault --- src/OpenSSL/SSL.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py index 89cfbf5b..17399fe4 100644 --- a/src/OpenSSL/SSL.py +++ b/src/OpenSSL/SSL.py @@ -3214,9 +3214,13 @@ def get_group_name(self) -> str | None: :return: A string giving the group name or :data:`None`. """ + # Do not remove this guard. + # SSL_get0_group_name crashes with a segfault if called without + # an established connection (should return NULL but doesn't). session = _lib.SSL_get_session(self._ssl) if session == _ffi.NULL: return None + group_name = _lib.SSL_get0_group_name(self._ssl) if group_name == _ffi.NULL: return None