Use 'SSLContext.minimum|maximum_version' when passed 'ssl_version'
ADD
changelog/2110.feature.rst
+Added ``ssl_minimum_version`` and ``ssl_maximum_version`` options which set
+``SSLContext.minimum_version`` and ``SSLContext.maximum_version``.
+
+Changed ``ssl_version`` to instead set the corresponding ``SSLContext.minimum_version``
+and ``SSLContext.maximum_version`` values. Regardless of ``ssl_version`` passed
+``SSLContext`` objects are now constructed using ``ssl.PROTOCOL_TLS_CLIENT``.
+
+Deprecated ``ssl_version`` option in favor of ``ssl_minimum_version``. ``ssl_version``
+will be removed in a future 2.x release of urllib3.

MODIFY
src/urllib3/connection.py
src/urllib3/connection.py
@@ -349,6 +349,8 @@ class HTTPSConnection(HTTPConnection):
ca_cert_dir: Optional[str] = None
ca_cert_data: Union[None, str, bytes] = None
ssl_version: Optional[Union[int, str]] = None
+ ssl_minimum_version: Optional[int] = None
+ ssl_maximum_version: Optional[int] = None
assert_fingerprint: Optional[str] = None
tls_in_tls_required: bool = False
@@ -387,6 +389,9 @@ class HTTPSConnection(HTTPConnection):
self.key_password = key_password
self.ssl_context = ssl_context
self.server_hostname = server_hostname
+ self.ssl_version = None
+ self.ssl_minimum_version = None
+ self.ssl_maximum_version = None
def set_cert(
self,
@@ -466,6 +471,8 @@ class HTTPSConnection(HTTPConnection):
default_ssl_context = True
self.ssl_context = create_urllib3_context(
ssl_version=resolve_ssl_version(self.ssl_version),
+ ssl_minimum_version=self.ssl_minimum_version,
+ ssl_maximum_version=self.ssl_maximum_version,
cert_reqs=resolve_cert_reqs(self.cert_reqs),
)
# In some cases, we want to verify hostnames ourselves
@@ -513,18 +520,26 @@ class HTTPSConnection(HTTPConnection):
# If we're using all defaults and the connection
# is TLSv1 or TLSv1.1 we throw a DeprecationWarning
# for the host.
+ if hasattr(self.sock, "version"):
+ tls_version = self.sock.version()
+ else:
+ tls_version = None
+
if (
default_ssl_context
- and self.ssl_version is None
- and hasattr(self.sock, "version")
- and self.sock.version() in {"TLSv1", "TLSv1.1"}
+ and self.ssl_minimum_version is None
+ and self.ssl_maximum_version is None
+ and tls_version is not None
+ and tls_version in {"TLSv1", "TLSv1.1"}
):
warnings.warn(
"Negotiating TLSv1/TLSv1.1 by default is deprecated "
"and will be disabled in urllib3 v2.0.0. Connecting to "
- f"'{self.host}' with '{self.sock.version()}' can be "
- "enabled by explicitly opting-in with 'ssl_version'",
+ f"'{self.host}' with '{tls_version}' can be "
+ f"enabled by explicitly opting-in with "
+ f"'ssl_minimum_version=ssl.TLSVersion.{tls_version.replace('.', '_')}'",
DeprecationWarning,
+ stacklevel=2,
)
if self.assert_fingerprint:

MODIFY
src/urllib3/connectionpool.py
src/urllib3/connectionpool.py
@@ -53,6 +53,8 @@ from .util.url import parse_url
from .util.util import to_str
if TYPE_CHECKING:
+ import ssl
+
from typing_extensions import Literal
log = logging.getLogger(__name__)
@@ -902,6 +904,8 @@ class HTTPSConnectionPool(HTTPConnectionPool):
key_password: Optional[str] = None,
ca_certs: Optional[str] = None,
ssl_version: Optional[Union[int, str]] = None,
+ ssl_minimum_version: Optional["ssl.TLSVersion"] = None,
+ ssl_maximum_version: Optional["ssl.TLSVersion"] = None,
assert_hostname: Optional[Union[str, "Literal[False]"]] = None,
assert_fingerprint: Optional[str] = None,
ca_cert_dir: Optional[str] = None,
@@ -928,6 +932,8 @@ class HTTPSConnectionPool(HTTPConnectionPool):
self.ca_certs = ca_certs
self.ca_cert_dir = ca_cert_dir
self.ssl_version = ssl_version
+ self.ssl_minimum_version = ssl_minimum_version
+ self.ssl_maximum_version = ssl_maximum_version
self.assert_hostname = assert_hostname
self.assert_fingerprint = assert_fingerprint
@@ -949,6 +955,9 @@ class HTTPSConnectionPool(HTTPConnectionPool):
assert_fingerprint=self.assert_fingerprint,
)
conn.ssl_version = self.ssl_version
+ conn.ssl_minimum_version = self.ssl_minimum_version
+ conn.ssl_maximum_version = self.ssl_maximum_version
+
return conn
def _prepare_proxy(self, conn: HTTPSConnection) -> None: # type: ignore[override]

MODIFY
src/urllib3/contrib/pyopenssl.py
src/urllib3/contrib/pyopenssl.py
@@ -107,6 +107,44 @@ _stdlib_to_openssl_verify = {
}
_openssl_to_stdlib_verify = {v: k for k, v in _stdlib_to_openssl_verify.items()}
+# The SSLvX values are the most likely to be missing in the future
+# but we check them all just to be sure.
+_OP_NO_SSLv2: int = getattr(OpenSSL.SSL, "OP_NO_SSLv2", 0)
+_OP_NO_SSLv3: int = getattr(OpenSSL.SSL, "OP_NO_SSLv3", 0)
+_OP_NO_TLSv1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1", 0)
+_OP_NO_TLSv1_1: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_1", 0)
+_OP_NO_TLSv1_2: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_2", 0)
+_OP_NO_TLSv1_3: int = getattr(OpenSSL.SSL, "OP_NO_TLSv1_3", 0)
+
+_openssl_to_ssl_minimum_version: Dict[int, int] = {
+ ssl.TLSVersion.MINIMUM_SUPPORTED: _OP_NO_SSLv2,
+ ssl.TLSVersion.SSLv3: _OP_NO_SSLv2,
+ ssl.TLSVersion.TLSv1: _OP_NO_SSLv2 | _OP_NO_SSLv3,
+ ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2 | _OP_NO_SSLv3 | _OP_NO_TLSv1,
+ ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2 | _OP_NO_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1,
+ ssl.TLSVersion.TLSv1_3: (
+ _OP_NO_SSLv2 | _OP_NO_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2
+ ),
+ ssl.TLSVersion.MAXIMUM_SUPPORTED: (
+ _OP_NO_SSLv2 | _OP_NO_SSLv3 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2
+ ),
+}
+_openssl_to_ssl_maximum_version: Dict[int, int] = {
+ ssl.TLSVersion.MINIMUM_SUPPORTED: (
+ _OP_NO_SSLv2 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3
+ ),
+ ssl.TLSVersion.SSLv3: (
+ _OP_NO_SSLv2 | _OP_NO_TLSv1 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3
+ ),
+ ssl.TLSVersion.TLSv1: (
+ _OP_NO_SSLv2 | _OP_NO_TLSv1_1 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3
+ ),
+ ssl.TLSVersion.TLSv1_1: _OP_NO_SSLv2 | _OP_NO_TLSv1_2 | _OP_NO_TLSv1_3,
+ ssl.TLSVersion.TLSv1_2: _OP_NO_SSLv2 | _OP_NO_TLSv1_3,
+ ssl.TLSVersion.TLSv1_3: _OP_NO_SSLv2,
+ ssl.TLSVersion.MAXIMUM_SUPPORTED: _OP_NO_SSLv2,
+}
+
# OpenSSL will only write 16K at a time
SSL_WRITE_BLOCKSIZE = 16384
@@ -407,6 +445,8 @@ class PyOpenSSLContext:
self._ctx = OpenSSL.SSL.Context(self.protocol)
self._options = 0
self.check_hostname = False
+ self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED
+ self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED
@property
def options(self) -> int:
@@ -415,7 +455,7 @@ class PyOpenSSLContext:
@options.setter
def options(self, value: int) -> None:
self._options = value
- self._ctx.set_options(value)
+ self._set_ctx_options()
@property
def verify_mode(self) -> int:
@@ -498,6 +538,31 @@ class PyOpenSSLContext:
return WrappedSocket(cnx, sock)
+ def _set_ctx_options(self) -> None:
+ self._ctx.set_options(
+ self._options
+ | _openssl_to_ssl_minimum_version[self._minimum_version]
+ | _openssl_to_ssl_maximum_version[self._maximum_version]
+ )
+
+ @property
+ def minimum_version(self) -> int:
+ return self._minimum_version
+
+ @minimum_version.setter
+ def minimum_version(self, minimum_version: int) -> None:
+ self._minimum_version = minimum_version
+ self._set_ctx_options()
+
+ @property
+ def maximum_version(self) -> int:
+ return self._maximum_version
+
+ @maximum_version.setter
+ def maximum_version(self, maximum_version: int) -> None:
+ self._maximum_version = maximum_version
+ self._set_ctx_options()
+
def _verify_callback(
cnx: OpenSSL.SSL.Connection,

MODIFY
src/urllib3/contrib/securetransport.py
src/urllib3/contrib/securetransport.py
@@ -67,6 +67,7 @@ from typing import (
TYPE_CHECKING,
Any,
BinaryIO,
+ Dict,
Generator,
List,
Optional,
@@ -165,6 +166,15 @@ if hasattr(ssl, "PROTOCOL_TLSv1_2"):
)
+_tls_version_to_st: Dict[int, int] = {
+ ssl.TLSVersion.MINIMUM_SUPPORTED: SecurityConst.kTLSProtocol1,
+ ssl.TLSVersion.TLSv1: SecurityConst.kTLSProtocol1,
+ ssl.TLSVersion.TLSv1_1: SecurityConst.kTLSProtocol11,
+ ssl.TLSVersion.TLSv1_2: SecurityConst.kTLSProtocol12,
+ ssl.TLSVersion.MAXIMUM_SUPPORTED: SecurityConst.kTLSProtocol12,
+}
+
+
def inject_into_urllib3() -> None:
"""
Monkey-patch urllib3 with SecureTransport-backed SSL-support.
@@ -753,7 +763,11 @@ class SecureTransportContext:
"""
def __init__(self, protocol: int) -> None:
+ self._minimum_version: int = ssl.TLSVersion.MINIMUM_SUPPORTED
+ self._maximum_version: int = ssl.TLSVersion.MAXIMUM_SUPPORTED
+ if protocol not in (None, ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLS_CLIENT):
self._min_version, self._max_version = _protocol_to_min_max[protocol]
+
self._options = 0
self._verify = False
self._trust_bundle: Optional[bytes] = None
@@ -882,11 +896,27 @@ class SecureTransportContext:
server_hostname,
self._verify,
self._trust_bundle,
- self._min_version,
- self._max_version,
+ _tls_version_to_st[self._minimum_version],
+ _tls_version_to_st[self._maximum_version],
self._client_cert,
self._client_key,
self._client_key_passphrase,
self._alpn_protocols,
)
return wrapped_socket
+
+ @property
+ def minimum_version(self) -> int:
+ return self._minimum_version
+
+ @minimum_version.setter
+ def minimum_version(self, minimum_version: int) -> None:
+ self._minimum_version = minimum_version
+
+ @property
+ def maximum_version(self) -> int:
+ return self._maximum_version
+
+ @maximum_version.setter
+ def maximum_version(self, maximum_version: int) -> None:
+ self._maximum_version = maximum_version

MODIFY
src/urllib3/poolmanager.py
src/urllib3/poolmanager.py
@@ -50,6 +50,8 @@ SSL_KEYWORDS = (
"cert_reqs",
"ca_certs",
"ssl_version",
+ "ssl_minimum_version",
+ "ssl_maximum_version",
"ca_cert_dir",
"ssl_context",
"key_password",
@@ -79,6 +81,8 @@ class PoolKey(NamedTuple):
key_cert_reqs: Optional[str]
key_ca_certs: Optional[str]
key_ssl_version: Optional[Union[int, str]]
+ key_ssl_minimum_version: Optional["ssl.TLSVersion"]
+ key_ssl_maximum_version: Optional["ssl.TLSVersion"]
key_ca_cert_dir: Optional[str]
key_ssl_context: Optional["ssl.SSLContext"]
key_maxsize: Optional[int]

MODIFY
src/urllib3/util/ssl_.py
src/urllib3/util/ssl_.py
@@ -41,6 +41,9 @@ if TYPE_CHECKING:
from .ssltransport import SSLTransport as SSLTransportType
+# Mapping from 'ssl.PROTOCOL_TLSX' to 'TLSVersion.X'
+_SSL_VERSION_TO_TLS_VERSION: Dict[int, int] = {}
+
try: # Do we have ssl at all?
import ssl
from ssl import ( # type: ignore[misc]
@@ -56,12 +59,24 @@ try: # Do we have ssl at all?
OP_NO_SSLv2,
OP_NO_SSLv3,
SSLContext,
+ TLSVersion,
)
USE_DEFAULT_SSLCONTEXT_CIPHERS = _is_ge_openssl_v1_1_1(
OPENSSL_VERSION, OPENSSL_VERSION_NUMBER
)
PROTOCOL_SSLv23 = PROTOCOL_TLS
+
+ # Need to be careful here in case old TLS versions get
+ # removed in future 'ssl' module implementations.
+ for attr in ("TLSv1", "TLSv1_1", "TLSv1_2"):
+ try:
+ _SSL_VERSION_TO_TLS_VERSION[getattr(ssl, f"PROTOCOL_{attr}")] = getattr(
+ TLSVersion, attr
+ )
+ except AttributeError: # Defensive:
+ continue
+
from .ssltransport import SSLTransport # type: ignore[misc]
except ImportError:
OP_NO_COMPRESSION = 0x20000 # type: ignore[assignment]
@@ -77,7 +92,6 @@ _PCTRTTT = Tuple[_PCTRTT, ...]
_TYPE_PEER_CERT_RET_DICT = Dict[str, Union[str, _PCTRTTT, _PCTRTT]]
_TYPE_PEER_CERT_RET = Union[_TYPE_PEER_CERT_RET_DICT, bytes, None]
-
# A secure default.
# Sources for more information on TLS ciphers:
#
@@ -190,6 +204,8 @@ def create_urllib3_context(
cert_reqs: Optional[int] = None,
options: Optional[int] = None,
ciphers: Optional[str] = None,
+ ssl_minimum_version: Optional[int] = None,
+ ssl_maximum_version: Optional[int] = None,
) -> "ssl.SSLContext":
"""All arguments have the same meaning as ``ssl_wrap_socket``.
@@ -212,6 +228,14 @@ def create_urllib3_context(
The desired protocol version to use. This will default to
PROTOCOL_SSLv23 which will negotiate the highest protocol that both
the server and your installation of OpenSSL support.
+
+ This parameter is deprecated instead use 'ssl_minimum_version'.
+ :param ssl_minimum_version:
+ The minimum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value.
+ :param ssl_maximum_version:
+ The maximum version of TLS to be used. Use the 'ssl.TLSVersion' enum for specifying the value.
+ Not recommended to set to anything other than 'ssl.TLSVersion.MAXIMUM_SUPPORTED' which is the
+ default value.
:param cert_reqs:
Whether to require the certificate verification. This defaults to
``ssl.CERT_REQUIRED``.
@@ -228,11 +252,44 @@ def create_urllib3_context(
if SSLContext is None:
raise TypeError("Can't create an SSLContext object without an ssl module")
- # PROTOCOL_TLS is deprecated in Python 3.10 so we pass PROTOCOL_TLS_CLIENT instead.
- if ssl_version in (None, PROTOCOL_TLS):
- ssl_version = PROTOCOL_TLS_CLIENT
+ # This means 'ssl_version' was specified as an exact value.
+ if ssl_version not in (None, PROTOCOL_TLS, PROTOCOL_TLS_CLIENT):
+ # Disallow setting 'ssl_version' and 'ssl_minimum|maximum_version'
+ # to avoid conflicts.
+ if ssl_minimum_version is not None or ssl_maximum_version is not None:
+ raise ValueError(
+ "Can't specify both 'ssl_version' and either "
+ "'ssl_minimum_version' or 'ssl_maximum_version'"
+ )
+
+ # 'ssl_version' is deprecated and will be removed in the future.
+ else:
+ # Use 'ssl_minimum_version' and 'ssl_maximum_version' instead.
+ ssl_minimum_version = _SSL_VERSION_TO_TLS_VERSION.get(
+ ssl_version, TLSVersion.MINIMUM_SUPPORTED
+ )
+ ssl_maximum_version = _SSL_VERSION_TO_TLS_VERSION.get(
+ ssl_version, TLSVersion.MAXIMUM_SUPPORTED
+ )
+
+ # This warning message is pushing users to use 'ssl_minimum_version'
+ # instead of both min/max. Best practice is to only set the minimum version and
+ # keep the maximum version to be it's default value: 'TLSVersion.MAXIMUM_SUPPORTED'
+ warnings.warn(
+ "'ssl_version' option is deprecated and will be "
+ "removed in a future release of urllib3 2.x. Instead "
+ "use 'ssl_minimum_version'",
+ category=DeprecationWarning,
+ stacklevel=2,
+ )
+
+ # PROTOCOL_TLS is deprecated in Python 3.10 so we always use PROTOCOL_TLS_CLIENT
+ context = SSLContext(PROTOCOL_TLS_CLIENT)
- context = SSLContext(ssl_version)
+ if ssl_minimum_version is not None:
+ context.minimum_version = ssl_minimum_version
+ if ssl_maximum_version is not None:
+ context.maximum_version = ssl_maximum_version
# Unless we're given ciphers defer to either system ciphers in
# the case of OpenSSL 1.1.1+ or use our own secure default ciphers.

MODIFY
test/test_ssl.py
test/test_ssl.py
+import ssl
from unittest import mock
import pytest
@@ -173,6 +174,63 @@ class TestSSL:
else:
context.set_ciphers.assert_called_with(ssl_.DEFAULT_CIPHERS)
+ @pytest.mark.parametrize(
+ "kwargs",
+ [
+ {
+ "ssl_version": ssl.PROTOCOL_TLSv1,
+ "ssl_minimum_version": ssl.TLSVersion.MINIMUM_SUPPORTED,
+ },
+ {
+ "ssl_version": ssl.PROTOCOL_TLSv1,
+ "ssl_maximum_version": ssl.TLSVersion.TLSv1,
+ },
+ {
+ "ssl_version": ssl.PROTOCOL_TLSv1,
+ "ssl_minimum_version": ssl.TLSVersion.MINIMUM_SUPPORTED,
+ "ssl_maximum_version": ssl.TLSVersion.MAXIMUM_SUPPORTED,
+ },
+ ],
+ )
+ def test_create_urllib3_context_ssl_version_and_ssl_min_max_version_errors(
+ self, kwargs
+ ):
+ with pytest.raises(ValueError) as e:
+ ssl_.create_urllib3_context(**kwargs)
+
+ assert str(e.value) == (
+ "Can't specify both 'ssl_version' and either 'ssl_minimum_version' or 'ssl_maximum_version'"
+ )
+
+ @pytest.mark.parametrize(
+ "kwargs",
+ [
+ {
+ "ssl_version": ssl.PROTOCOL_TLS,
+ "ssl_minimum_version": ssl.TLSVersion.MINIMUM_SUPPORTED,
+ },
+ {
+ "ssl_version": ssl.PROTOCOL_TLS_CLIENT,
+ "ssl_minimum_version": ssl.TLSVersion.MINIMUM_SUPPORTED,
+ },
+ {
+ "ssl_version": None,
+ "ssl_minimum_version": ssl.TLSVersion.MINIMUM_SUPPORTED,
+ },
+ {"ssl_version": ssl.PROTOCOL_TLSv1, "ssl_minimum_version": None},
+ {"ssl_version": ssl.PROTOCOL_TLSv1, "ssl_maximum_version": None},
+ {
+ "ssl_version": ssl.PROTOCOL_TLSv1,
+ "ssl_minimum_version": None,
+ "ssl_maximum_version": None,
+ },
+ ],
+ )
+ def test_create_urllib3_context_ssl_version_and_ssl_min_max_version_no_error(
+ self, kwargs
+ ):
+ ssl_.create_urllib3_context(**kwargs)
+
def test_assert_fingerprint_raises_exception_on_none_cert(self):
with pytest.raises(SSLError):
ssl_.assert_fingerprint(

MODIFY
test/with_dummyserver/test_https.py
test/with_dummyserver/test_https.py
@@ -22,6 +22,7 @@ import pytest
import trustme
import urllib3.util as util
+import urllib3.util.ssl_
from dummyserver.server import (
DEFAULT_CA,
DEFAULT_CA_KEY,
@@ -78,6 +79,22 @@ class TestHTTPS(HTTPSDummyServerTestCase):
def tls_protocol_deprecated(self):
return self.tls_protocol_name in {"TLSv1", "TLSv1.1"}
+ def tls_version(self):
+ try:
+ from ssl import TLSVersion
+ except ImportError:
+ return pytest.skip("ssl.TLSVersion isn't available")
+ return getattr(TLSVersion, self.tls_protocol_name.replace(".", "_"))
+
+ def ssl_version(self):
+ if self.tls_protocol_name is None:
+ return pytest.skip("Skipping base test class")
+ attribute = f"PROTOCOL_{self.tls_protocol_name.replace('.', '_')}"
+ ssl_version = getattr(ssl, attribute, None)
+ if ssl_version is None:
+ return pytest.skip(f"ssl.{attribute} isn't available")
+ return ssl_version
+
@classmethod
def setup_class(cls):
super().setup_class()
@@ -679,16 +696,47 @@ class TestHTTPS(HTTPSDummyServerTestCase):
if self.tls_protocol_deprecated():
assert len(w) == 1
assert str(w[0].message) == (
- "Negotiating TLSv1/TLSv1.1 by default is deprecated "
- "and will be disabled in urllib3 v2.0.0. Connecting to "
- "'%s' with '%s' can be enabled by explicitly opting-in "
- "with 'ssl_version'" % (self.host, self.tls_protocol_name)
+ "Negotiating TLSv1/TLSv1.1 by default is deprecated and will be disabled in "
+ "urllib3 v2.0.0. Connecting to '%s' with '%s' can be enabled by "
+ "explicitly opting-in with 'ssl_minimum_version=ssl.TLSVersion.%s'"
+ % (
+ self.host,
+ self.tls_protocol_name,
+ self.tls_protocol_name.replace(".", "_"),
+ )
)
else:
assert w == []
- @pytest.mark.parametrize("ssl_version", [ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLS_CLIENT])
- def test_no_tls_version_deprecation_with_ssl_version(self, ssl_version):
+ def test_ssl_version_is_deprecated(self):
+ if self.tls_protocol_name is None:
+ pytest.skip("Skipping base test class")
+
+ with HTTPSConnectionPool(
+ self.host, self.port, ca_certs=DEFAULT_CA, ssl_version=self.ssl_version()
+ ) as https_pool:
+ conn = https_pool._get_conn()
+ try:
+ with warnings.catch_warnings(record=True) as w:
+ conn.connect()
+ finally:
+ conn.close()
+
+ assert len(w) >= 1
+ assert any(x.category == DeprecationWarning for x in w)
+ assert any(
+ str(x.message)
+ == (
+ "'ssl_version' option is deprecated and will be removed in "
+ "a future release of urllib3 2.x. Instead use 'ssl_minimum_version'"
+ )
+ for x in w
+ )
+
+ @pytest.mark.parametrize(
+ "ssl_version", [None, ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLS_CLIENT]
+ )
+ def test_ssl_version_with_protocol_tls_or_client_not_deprecated(self, ssl_version):
if self.tls_protocol_name is None:
pytest.skip("Skipping base test class")
@@ -702,6 +750,19 @@ class TestHTTPS(HTTPSDummyServerTestCase):
finally:
conn.close()
+ if self.tls_protocol_deprecated():
+ assert len(w) == 1
+ assert str(w[0].message) == (
+ "Negotiating TLSv1/TLSv1.1 by default is deprecated and will be disabled in "
+ "urllib3 v2.0.0. Connecting to '%s' with '%s' can be enabled by "
+ "explicitly opting-in with 'ssl_minimum_version=ssl.TLSVersion.%s'"
+ % (
+ self.host,
+ self.tls_protocol_name,
+ self.tls_protocol_name.replace(".", "_"),
+ )
+ )
+ else:
assert w == []
def test_no_tls_version_deprecation_with_ssl_context(self):
@@ -723,6 +784,33 @@ class TestHTTPS(HTTPSDummyServerTestCase):
assert w == []
+ def test_tls_version_maximum_and_minimum(self):
+ if self.tls_protocol_name is None:
+ pytest.skip("Skipping base test class")
+
+ from ssl import TLSVersion
+
+ min_max_versions = [
+ (self.tls_version(), self.tls_version()),
+ (TLSVersion.MINIMUM_SUPPORTED, self.tls_version()),
+ (TLSVersion.MINIMUM_SUPPORTED, TLSVersion.MAXIMUM_SUPPORTED),
+ ]
+
+ for minimum_version, maximum_version in min_max_versions:
+ with HTTPSConnectionPool(
+ self.host,
+ self.port,
+ ca_certs=DEFAULT_CA,
+ ssl_minimum_version=minimum_version,
+ ssl_maximum_version=maximum_version,
+ ) as https_pool:
+ conn = https_pool._get_conn()
+ try:
+ conn.connect()
+ assert conn.sock.version() == self.tls_protocol_name
+ finally:
+ conn.close()
+
@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python 3.8+")
def test_sslkeylogfile(self, tmpdir, monkeypatch):
if not hasattr(util.SSLContext, "keylog_filename"):
@@ -764,6 +852,16 @@ class TestHTTPS(HTTPSDummyServerTestCase):
assert r.status == 200
assert r.data.decode("utf-8") == util.ALPN_PROTOCOLS[0]
+ def test_default_ssl_context_ssl_min_max_versions(self):
+ ctx = urllib3.util.ssl_.create_urllib3_context()
+ assert ctx.minimum_version == ssl.TLSVersion.MINIMUM_SUPPORTED
+ assert ctx.maximum_version == ssl.TLSVersion.MAXIMUM_SUPPORTED
+
+ def test_ssl_context_ssl_version_uses_ssl_min_max_versions(self):
+ ctx = urllib3.util.ssl_.create_urllib3_context(ssl_version=self.ssl_version())
+ assert ctx.minimum_version == self.tls_version()
+ assert ctx.maximum_version == self.tls_version()
+
@pytest.mark.usefixtures("requires_tlsv1")
class TestHTTPS_TLSv1(TestHTTPS):

Use 'SSLContext.minimum|maximum_version' when passed 'ssl_version'