Fix test failures (OpenSSL 4 and pyOpenSSL 26.3)

- Fixes: rhbz#2504593

Used LLM to identify and rebase upstream patch #5103

Assisted-By: Claude Opus 4.6
This commit is contained in:
Miro Hrončok 2026-07-27 22:22:53 +02:00
commit d75d492045
4 changed files with 220 additions and 69 deletions

133
5097.patch Normal file
View file

@ -0,0 +1,133 @@
From 8d2659ac68ec8d46dfd8c2fc0ab057b647ac7940 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= <miro@hroncok.cz>
Date: Wed, 1 Jul 2026 15:05:18 +0200
Subject: [PATCH] Deal with ssl.PROTOCOL_TLSv1 removal from Python built with
OpenSSL 4+
https://github.com/python/cpython/commit/3364e7e62fa24d0e19133fb0f90b1c24ef1110c5
removed ssl.PROTOCOL_TLSv1 when python is built with OpenSSL 4+.
We are hitting this error in Fedora (where we already updated OpenSSL):
ImportError while loading conftest '/builddir/build/BUILD/python-urllib3-2.7.0-build/urllib3-2.7.0/test/conftest.py'.
test/__init__.py:42: in <module>
import urllib3.contrib.pyopenssl as pyopenssl
../BUILDROOT/usr/lib/python3.15/site-packages/urllib3/contrib/pyopenssl.py:72: in <module>
ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD,
^^^^^^^^^^^^^^^^^^
E AttributeError: module 'ssl' has no attribute 'PROTOCOL_TLSv1'. Did you mean '.PROTOCOL_TLS' instead of '.PROTOCOL_TLSv1'?
Used LLM to figure out what to use as a replacement protocol in tests.
Assisted-By: Claude Opus 4.6
---
changelog/5097.bugfix.rst | 2 ++
src/urllib3/contrib/pyopenssl.py | 4 +++-
test/test_ssl.py | 14 ++++++++------
test/test_util.py | 24 +++++++++++++++---------
4 files changed, 28 insertions(+), 16 deletions(-)
create mode 100644 changelog/5097.bugfix.rst
diff --git a/changelog/5097.bugfix.rst b/changelog/5097.bugfix.rst
new file mode 100644
index 0000000000..73ed0ddaa1
--- /dev/null
+++ b/changelog/5097.bugfix.rst
@@ -0,0 +1,2 @@
+Fixed an ``AttributeError`` on Python built with OpenSSL 4+, where
+``ssl.PROTOCOL_TLSv1`` no longer exists.
diff --git a/src/urllib3/contrib/pyopenssl.py b/src/urllib3/contrib/pyopenssl.py
index 42781590ee..76a225b7bf 100644
--- a/src/urllib3/contrib/pyopenssl.py
+++ b/src/urllib3/contrib/pyopenssl.py
@@ -68,9 +68,11 @@ class UnsupportedExtension(Exception): # type: ignore[no-redef]
_openssl_versions: dict[int, int] = {
util.ssl_.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined]
util.ssl_.PROTOCOL_TLS_CLIENT: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined]
- ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD,
}
+if hasattr(ssl, "PROTOCOL_TLSv1") and hasattr(OpenSSL.SSL, "TLSv1_METHOD"):
+ _openssl_versions[ssl.PROTOCOL_TLSv1] = OpenSSL.SSL.TLSv1_METHOD
+
if hasattr(ssl, "PROTOCOL_TLSv1_1") and hasattr(OpenSSL.SSL, "TLSv1_1_METHOD"):
_openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD
diff --git a/test/test_ssl.py b/test/test_ssl.py
index 6b1ab8589b..1e4063bf68 100644
--- a/test/test_ssl.py
+++ b/test/test_ssl.py
@@ -176,19 +176,21 @@ def test_create_urllib3_context_default_ciphers(
context.set_ciphers.assert_not_called()
+ # PROTOCOL_TLS_SERVER is used as a stand-in for any non-default ssl_version.
+ # PROTOCOL_TLSv1/TLSv1_2 are unavailable when Python is built with OpenSSL 4+.
@pytest.mark.parametrize(
"kwargs",
[
{
- "ssl_version": ssl.PROTOCOL_TLSv1,
+ "ssl_version": ssl.PROTOCOL_TLS_SERVER,
"ssl_minimum_version": ssl.TLSVersion.MINIMUM_SUPPORTED,
},
{
- "ssl_version": ssl.PROTOCOL_TLSv1,
+ "ssl_version": ssl.PROTOCOL_TLS_SERVER,
"ssl_maximum_version": ssl.TLSVersion.TLSv1,
},
{
- "ssl_version": ssl.PROTOCOL_TLSv1,
+ "ssl_version": ssl.PROTOCOL_TLS_SERVER,
"ssl_minimum_version": ssl.TLSVersion.MINIMUM_SUPPORTED,
"ssl_maximum_version": ssl.TLSVersion.MAXIMUM_SUPPORTED,
},
@@ -229,10 +231,10 @@ def test_create_urllib3_context_ssl_version_and_ssl_min_max_version_no_warning(
@pytest.mark.parametrize(
"kwargs",
[
- {"ssl_version": ssl.PROTOCOL_TLSv1, "ssl_minimum_version": None},
- {"ssl_version": ssl.PROTOCOL_TLSv1, "ssl_maximum_version": None},
+ {"ssl_version": ssl.PROTOCOL_TLS_SERVER, "ssl_minimum_version": None},
+ {"ssl_version": ssl.PROTOCOL_TLS_SERVER, "ssl_maximum_version": None},
{
- "ssl_version": ssl.PROTOCOL_TLSv1,
+ "ssl_version": ssl.PROTOCOL_TLS_SERVER,
"ssl_minimum_version": None,
"ssl_maximum_version": None,
},
diff --git a/test/test_util.py b/test/test_util.py
index 8612ab5eaf..18c65f0312 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -1216,15 +1216,21 @@ def test_resolve_cert_reqs(
) -> None:
assert resolve_cert_reqs(candidate) == requirements
- @pytest.mark.parametrize(
- "candidate, version",
- [
- (ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_TLSv1),
- ("PROTOCOL_TLSv1", ssl.PROTOCOL_TLSv1),
- ("TLSv1", ssl.PROTOCOL_TLSv1),
- (ssl.PROTOCOL_SSLv23, ssl.PROTOCOL_SSLv23),
- ],
- )
+ candidate_version = [
+ (ssl.PROTOCOL_SSLv23, ssl.PROTOCOL_SSLv23),
+ ("PROTOCOL_SSLv23", ssl.PROTOCOL_SSLv23),
+ ("SSLv23", ssl.PROTOCOL_SSLv23),
+ ]
+ if hasattr(ssl, "PROTOCOL_TLSv1"):
+ candidate_version.extend(
+ [
+ (ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_TLSv1),
+ ("PROTOCOL_TLSv1", ssl.PROTOCOL_TLSv1),
+ ("TLSv1", ssl.PROTOCOL_TLSv1),
+ ]
+ )
+
+ @pytest.mark.parametrize("candidate, version", candidate_version)
def test_resolve_ssl_version(self, candidate: int | str, version: int) -> None:
assert resolve_ssl_version(candidate) == version

83
5103.patch Normal file
View file

@ -0,0 +1,83 @@
From 13fa1e033b37405b228599ee590af485dc3e38aa Mon Sep 17 00:00:00 2001
From: Illia Volochii <illia.volochii@gmail.com>
Date: Sat, 18 Jul 2026 13:25:41 +0200
Subject: [PATCH] Replace deprecated pyOpenSSL `X509.get_subject` and
`Context.set_passwd_cb` methods (#5103)
Rebased on top of urllib3-2.7.0.
---
src/urllib3/contrib/pyopenssl.py | 35 ++++++++++++++++++++++++++-------
1 file changed, 28 insertions(+), 7 deletions(-)
diff --git a/src/urllib3/contrib/pyopenssl.py b/src/urllib3/contrib/pyopenssl.py
index 76a225b7bf..a1b2c3d4e5 100644
--- a/src/urllib3/contrib/pyopenssl.py
+++ b/src/urllib3/contrib/pyopenssl.py
@@ -42,6 +42,8 @@
import OpenSSL.SSL # type: ignore[import-not-found]
from cryptography import x509
+from cryptography.hazmat.primitives.serialization import load_pem_private_key
+from cryptography.x509.oid import NameOID
try:
from cryptography.x509 import UnsupportedExtension # type: ignore[attr-defined]
@@ -272,6 +274,15 @@
return names
+def _get_common_name(peer_cert: X509) -> str | None:
+ """
+ Given a pyOpenSSL certificate, return the subject's common name.
+ """
+ cert = peer_cert.to_cryptography()
+ names = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
+ return typing.cast(str, names[0].value) if names else None
+
+
class WrappedSocket:
"""API-compatibility wrapper for Python OpenSSL's Connection-class."""
@@ -396,7 +407,7 @@
return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509) # type: ignore[no-any-return]
return {
- "subject": ((("commonName", x509.get_subject().CN),),), # type: ignore[dict-item]
+ "subject": ((("commonName", _get_common_name(x509)),),), # type: ignore[dict-item]
"subjectAltName": get_subj_alt_name(x509),
}
@@ -482,16 +493,28 @@
self,
certfile: str,
keyfile: str | None = None,
- password: str | None = None,
+ password: str | bytes | None = None,
) -> None:
try:
self._ctx.use_certificate_chain_file(certfile)
if password is not None:
if not isinstance(password, bytes):
- password = password.encode("utf-8") # type: ignore[assignment]
- self._ctx.set_passwd_cb(lambda *_: password)
- self._ctx.use_privatekey_file(keyfile or certfile)
- except OpenSSL.SSL.Error as e:
+ password = password.encode("utf-8")
+ # pyOpenSSL added cryptography-key support in 24.3.0.
+ # Keep using the older password-callback path until 2026's
+ # versions because set_passwd_cb() became deprecated in 26.3.0.
+ if int(OpenSSL.__version__.split(".")[0]) >= 26:
+ with open(keyfile or certfile, "rb") as key_file:
+ private_key = load_pem_private_key(key_file.read(), password)
+ # cryptography's loader returns a wider private-key union
+ # than pyOpenSSL accepts, so we add `type: ignore` here.
+ self._ctx.use_privatekey(private_key) # type: ignore[arg-type]
+ else:
+ self._ctx.set_passwd_cb(lambda *_: password)
+ self._ctx.use_privatekey_file(keyfile or certfile)
+ else:
+ self._ctx.use_privatekey_file(keyfile or certfile)
+ except (OpenSSL.SSL.Error, TypeError, ValueError) as e:
raise ssl.SSLError(f"Unable to load certificate chain: {e!r}") from e
def set_alpn_protocols(self, protocols: list[bytes | str]) -> None:

View file

@ -1,67 +0,0 @@
--- a/src/urllib3/contrib/pyopenssl.py
+++ b/src/urllib3/contrib/pyopenssl.py
@@ -69,9 +69,11 @@
_openssl_versions: dict[int, int] = {
util.ssl_.PROTOCOL_TLS: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined]
util.ssl_.PROTOCOL_TLS_CLIENT: OpenSSL.SSL.SSLv23_METHOD, # type: ignore[attr-defined]
- ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD,
}
+if hasattr(ssl, "PROTOCOL_TLSv1") and hasattr(OpenSSL.SSL, "TLSv1_METHOD"):
+ _openssl_versions[ssl.PROTOCOL_TLSv1] = OpenSSL.SSL.TLSv1_METHOD
+
if hasattr(ssl, "PROTOCOL_TLSv1_1") and hasattr(OpenSSL.SSL, "TLSv1_1_METHOD"):
_openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD
--- a/test/test_ssl.py
+++ b/test/test_ssl.py
@@ -173,15 +173,15 @@
"kwargs",
[
{
- "ssl_version": ssl.PROTOCOL_TLSv1,
+ "ssl_version": getattr(ssl, "PROTOCOL_TLSv1", getattr(ssl, "PROTOCOL_TLSv1_2", 1)),
"ssl_minimum_version": ssl.TLSVersion.MINIMUM_SUPPORTED,
},
{
- "ssl_version": ssl.PROTOCOL_TLSv1,
+ "ssl_version": getattr(ssl, "PROTOCOL_TLSv1", getattr(ssl, "PROTOCOL_TLSv1_2", 1)),
"ssl_maximum_version": ssl.TLSVersion.TLSv1,
},
{
- "ssl_version": ssl.PROTOCOL_TLSv1,
+ "ssl_version": getattr(ssl, "PROTOCOL_TLSv1", getattr(ssl, "PROTOCOL_TLSv1_2", 1)),
"ssl_minimum_version": ssl.TLSVersion.MINIMUM_SUPPORTED,
"ssl_maximum_version": ssl.TLSVersion.MAXIMUM_SUPPORTED,
},
@@ -222,10 +222,10 @@
@pytest.mark.parametrize(
"kwargs",
[
- {"ssl_version": ssl.PROTOCOL_TLSv1, "ssl_minimum_version": None},
- {"ssl_version": ssl.PROTOCOL_TLSv1, "ssl_maximum_version": None},
+ {"ssl_version": getattr(ssl, "PROTOCOL_TLSv1", getattr(ssl, "PROTOCOL_TLSv1_2", 1)), "ssl_minimum_version": None},
+ {"ssl_version": getattr(ssl, "PROTOCOL_TLSv1", getattr(ssl, "PROTOCOL_TLSv1_2", 1)), "ssl_maximum_version": None},
{
- "ssl_version": ssl.PROTOCOL_TLSv1,
+ "ssl_version": getattr(ssl, "PROTOCOL_TLSv1", getattr(ssl, "PROTOCOL_TLSv1_2", 1)),
"ssl_minimum_version": None,
"ssl_maximum_version": None,
},
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -982,9 +982,11 @@
@pytest.mark.parametrize(
"candidate, version",
[
- (ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_TLSv1),
- ("PROTOCOL_TLSv1", ssl.PROTOCOL_TLSv1),
- ("TLSv1", ssl.PROTOCOL_TLSv1),
+ *([
+ (getattr(ssl, "PROTOCOL_TLSv1", None), getattr(ssl, "PROTOCOL_TLSv1", None)),
+ ("PROTOCOL_TLSv1", getattr(ssl, "PROTOCOL_TLSv1", None)),
+ ("TLSv1", getattr(ssl, "PROTOCOL_TLSv1", None)),
+ ] if hasattr(ssl, "PROTOCOL_TLSv1") else []),
(ssl.PROTOCOL_SSLv23, ssl.PROTOCOL_SSLv23),
],
)

View file

@ -32,8 +32,10 @@ Source0: %{url}/archive/%{version}/urllib3-%{version}.tar.gz
Source1: %{hypercorn_url}/archive/%{hypercorn_commit}/hypercorn-%{hypercorn_commit}.tar.gz
# Deal with ssl.PROTOCOL_TLSv1 removal from Python built with OpenSSL 4+
# https://github.com/urllib3/urllib3/pull/5097
Patch: python-urllib3-py315-ssl.patch
Patch: https://github.com/urllib3/urllib3/pull/5097.patch
# Replace deprecated pyOpenSSL X509.get_subject and Context.set_passwd_cb methods
# https://github.com/urllib3/urllib3/pull/5103 rebased
Patch: 5103.patch
BuildArch: noarch