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/CHANGELOG.rst b/CHANGELOG.rst index 355ac68b..4a3bcd51 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,12 +10,17 @@ 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: ^^^^^^^^^^^^^ Changes: ^^^^^^^^ +- Added ``OpenSSL.SSL.Connection.get_group_name`` to determine which group name was negotiated. + 25.3.0 (2025-09-16) ------------------- 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/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 cc322764..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,11 +89,11 @@ 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=[ - "cryptography>=45.0.7,<47", + "cryptography>=46.0.0,<47", ( "typing-extensions>=4.9; " "python_version < '3.13' and python_version >= '3.8'" diff --git a/src/OpenSSL/SSL.py b/src/OpenSSL/SSL.py index 51c60d55..17399fe4 100644 --- a/src/OpenSSL/SSL.py +++ b/src/OpenSSL/SSL.py @@ -815,6 +815,11 @@ 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: """ @@ -3202,6 +3207,26 @@ 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. + + :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 + + 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 080de2e6..b4506afc 100644 --- a/tests/test_ssl.py +++ b/tests/test_ssl.py @@ -3437,6 +3437,54 @@ 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 + has been established. + """ + ctx = Context(TLS_METHOD) + 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", + ) + 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() + server_group_name = server.get_group_name() + client_group_name = client.get_group_name() + + assert isinstance(server_group_name, str) + assert isinstance(client_group_name, str) + + assert server_group_name == client_group_name + def test_wantReadError(self) -> None: """ `Connection.bio_read` raises `OpenSSL.SSL.WantReadError` if there are