Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand All @@ -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"}
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
-------------------

Expand Down
2 changes: 1 addition & 1 deletion doc/introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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'"
Expand Down
25 changes: 25 additions & 0 deletions src/OpenSSL/SSL.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions tests/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading