Skip to content

Commit 698dde1

Browse files
authored
bpo-31453: Add setter for min/max protocol version (#5259)
OpenSSL 1.1 has introduced a new API to set the minimum and maximum supported protocol version. The API is easier to use than the old OP_NO_TLS1 option flags, too. Since OpenSSL has no call to set minimum version to highest supported, the implementation emulate maximum_version = MINIMUM_SUPPORTED and minimum_version = MAXIMUM_SUPPORTED by figuring out the minumum and maximum supported version at compile time. Signed-off-by: Christian Heimes <[email protected]>
1 parent 9d50ab5 commit 698dde1

File tree

6 files changed

+471
-27
lines changed

6 files changed

+471
-27
lines changed

Doc/library/ssl.rst

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,11 @@ Constants
762762

763763
.. versionadded:: 3.2
764764

765+
.. deprecated:: 3.7
766+
The option is deprecated since OpenSSL 1.1.0, use the new
767+
:attr:`SSLContext.minimum_version` and
768+
:attr:`SSLContext.maximum_version` instead.
769+
765770
.. data:: OP_NO_TLSv1_1
766771

767772
Prevents a TLSv1.1 connection. This option is only applicable in conjunction
@@ -770,6 +775,9 @@ Constants
770775

771776
.. versionadded:: 3.4
772777

778+
.. deprecated:: 3.7
779+
The option is deprecated since OpenSSL 1.1.0.
780+
773781
.. data:: OP_NO_TLSv1_2
774782

775783
Prevents a TLSv1.2 connection. This option is only applicable in conjunction
@@ -778,6 +786,9 @@ Constants
778786

779787
.. versionadded:: 3.4
780788

789+
.. deprecated:: 3.7
790+
The option is deprecated since OpenSSL 1.1.0.
791+
781792
.. data:: OP_NO_TLSv1_3
782793

783794
Prevents a TLSv1.3 connection. This option is only applicable in conjunction
@@ -788,6 +799,10 @@ Constants
788799

789800
.. versionadded:: 3.7
790801

802+
.. deprecated:: 3.7
803+
The option is deprecated since OpenSSL 1.1.0. It was added to 2.7.15,
804+
3.6.3 and 3.7.0 for backwards compatibility with OpenSSL 1.0.2.
805+
791806
.. data:: OP_CIPHER_SERVER_PREFERENCE
792807

793808
Use the server's cipher ordering preference, rather than the client's.
@@ -856,7 +871,7 @@ Constants
856871

857872
.. data:: HAS_ECDH
858873

859-
Whether the OpenSSL library has built-in support for Elliptic Curve-based
874+
Whether the OpenSSL library has built-in support for the Elliptic Curve-based
860875
Diffie-Hellman key exchange. This should be true unless the feature was
861876
explicitly disabled by the distributor.
862877

@@ -871,14 +886,44 @@ Constants
871886

872887
.. data:: HAS_NPN
873888

874-
Whether the OpenSSL library has built-in support for *Next Protocol
889+
Whether the OpenSSL library has built-in support for the *Next Protocol
875890
Negotiation* as described in the `Application Layer Protocol
876891
Negotiation <https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation>`_.
877892
When true, you can use the :meth:`SSLContext.set_npn_protocols` method to advertise
878893
which protocols you want to support.
879894

880895
.. versionadded:: 3.3
881896

897+
.. data:: HAS_SSLv2
898+
899+
Whether the OpenSSL library has built-in support for the SSL 2.0 protocol.
900+
901+
.. versionadded:: 3.7
902+
903+
.. data:: HAS_SSLv3
904+
905+
Whether the OpenSSL library has built-in support for the SSL 3.0 protocol.
906+
907+
.. versionadded:: 3.7
908+
909+
.. data:: HAS_TLSv1
910+
911+
Whether the OpenSSL library has built-in support for the TLS 1.0 protocol.
912+
913+
.. versionadded:: 3.7
914+
915+
.. data:: HAS_TLSv1_1
916+
917+
Whether the OpenSSL library has built-in support for the TLS 1.1 protocol.
918+
919+
.. versionadded:: 3.7
920+
921+
.. data:: HAS_TLSv1_2
922+
923+
Whether the OpenSSL library has built-in support for the TLS 1.2 protocol.
924+
925+
.. versionadded:: 3.7
926+
882927
.. data:: HAS_TLSv1_3
883928

884929
Whether the OpenSSL library has built-in support for the TLS 1.3 protocol.
@@ -965,6 +1010,27 @@ Constants
9651010

9661011
.. versionadded:: 3.6
9671012

1013+
.. class:: TLSVersion
1014+
1015+
:class:`enum.IntEnum` collection of SSL and TLS versions for
1016+
:attr:`SSLContext.maximum_version` and :attr:`SSLContext.minimum_version`.
1017+
1018+
.. versionadded:: 3.7
1019+
1020+
.. attribute:: TLSVersion.MINIMUM_SUPPORTED
1021+
.. attribute:: TLSVersion.MAXIMUM_SUPPORTED
1022+
1023+
The minimum or maximum supported SSL or TLS version. These are magic
1024+
constants. Their values don't reflect the lowest and highest available
1025+
TLS/SSL versions.
1026+
1027+
.. attribute:: TLSVersion.SSLv3
1028+
.. attribute:: TLSVersion.TLSv1
1029+
.. attribute:: TLSVersion.TLSv1_1
1030+
.. attribute:: TLSVersion.TLSv1_2
1031+
.. attribute:: TLSVersion.TLSv1_3
1032+
1033+
SSL 3.0 to TLS 1.3.
9681034

9691035
SSL Sockets
9701036
-----------
@@ -1788,6 +1854,37 @@ to speed up repeated connections from the same clients.
17881854

17891855
This features requires OpenSSL 0.9.8f or newer.
17901856

1857+
.. attribute:: SSLContext.maximum_version
1858+
1859+
A :class:`TLSVersion` enum member representing the highest supported
1860+
TLS version. The value defaults to :attr:`TLSVersion.MAXIMUM_SUPPORTED`.
1861+
The attribute is read-only for protocols other than :attr:`PROTOCOL_TLS`,
1862+
:attr:`PROTOCOL_TLS_CLIENT`, and :attr:`PROTOCOL_TLS_SERVER`.
1863+
1864+
The attributes :attr:`~SSLContext.maximum_version`,
1865+
:attr:`~SSLContext.minimum_version` and
1866+
:attr:`SSLContext.options` all affect the supported SSL
1867+
and TLS versions of the context. The implementation does not prevent
1868+
invalid combination. For example a context with
1869+
:attr:`OP_NO_TLSv1_2` in :attr:`~SSLContext.options` and
1870+
:attr:`~SSLContext.maximum_version` set to :attr:`TLSVersion.TLSv1_2`
1871+
will not be able to establish a TLS 1.2 connection.
1872+
1873+
.. note::
1874+
1875+
This attribute is not available unless the ssl module is compiled
1876+
with OpenSSL 1.1.0g or newer.
1877+
1878+
.. attribute:: SSLContext.minimum_version
1879+
1880+
Like :attr:`SSLContext.maximum_version` except it is the lowest
1881+
supported version or :attr:`TLSVersion.MINIMUM_SUPPORTED`.
1882+
1883+
.. note::
1884+
1885+
This attribute is not available unless the ssl module is compiled
1886+
with OpenSSL 1.1.0g or newer.
1887+
17911888
.. attribute:: SSLContext.options
17921889

17931890
An integer representing the set of SSL options enabled on this context.

Doc/whatsnew/3.7.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,11 @@ feature. Instances must be created with :class:`~ssl.SSLContext` methods
683683
:meth:`~ssl.SSLContext.wrap_socket` and :meth:`~ssl.SSLContext.wrap_bio`.
684684
(Contributed by Christian Heimes in :issue:`32951`)
685685

686+
OpenSSL 1.1 APIs for setting the minimum and maximum TLS protocol version are
687+
available as as :attr:`~ssl.SSLContext.minimum_version` and
688+
:attr:`~ssl.SSLContext.maximum_version`. Supported protocols are indicated
689+
by new flags like :data:`~ssl.HAS_TLSv1_1`.
690+
(Contributed by Christian Heimes in :issue:`32609`.)
686691

687692
string
688693
------

Lib/ssl.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,11 @@
112112
pass
113113

114114

115-
from _ssl import HAS_SNI, HAS_ECDH, HAS_NPN, HAS_ALPN, HAS_TLSv1_3
116-
from _ssl import _DEFAULT_CIPHERS
117-
from _ssl import _OPENSSL_API_VERSION
115+
from _ssl import (
116+
HAS_SNI, HAS_ECDH, HAS_NPN, HAS_ALPN, HAS_SSLv2, HAS_SSLv3, HAS_TLSv1,
117+
HAS_TLSv1_1, HAS_TLSv1_2, HAS_TLSv1_3
118+
)
119+
from _ssl import _DEFAULT_CIPHERS, _OPENSSL_API_VERSION
118120

119121

120122
_IntEnum._convert(
@@ -153,6 +155,16 @@
153155
_SSLv2_IF_EXISTS = getattr(_SSLMethod, 'PROTOCOL_SSLv2', None)
154156

155157

158+
class TLSVersion(_IntEnum):
159+
MINIMUM_SUPPORTED = _ssl.PROTO_MINIMUM_SUPPORTED
160+
SSLv3 = _ssl.PROTO_SSLv3
161+
TLSv1 = _ssl.PROTO_TLSv1
162+
TLSv1_1 = _ssl.PROTO_TLSv1_1
163+
TLSv1_2 = _ssl.PROTO_TLSv1_2
164+
TLSv1_3 = _ssl.PROTO_TLSv1_3
165+
MAXIMUM_SUPPORTED = _ssl.PROTO_MAXIMUM_SUPPORTED
166+
167+
156168
if sys.platform == "win32":
157169
from _ssl import enum_certificates, enum_crls
158170

@@ -467,6 +479,25 @@ def load_default_certs(self, purpose=Purpose.SERVER_AUTH):
467479
self._load_windows_store_certs(storename, purpose)
468480
self.set_default_verify_paths()
469481

482+
if hasattr(_SSLContext, 'minimum_version'):
483+
@property
484+
def minimum_version(self):
485+
return TLSVersion(super().minimum_version)
486+
487+
@minimum_version.setter
488+
def minimum_version(self, value):
489+
if value == TLSVersion.SSLv3:
490+
self.options &= ~Options.OP_NO_SSLv3
491+
super(SSLContext, SSLContext).minimum_version.__set__(self, value)
492+
493+
@property
494+
def maximum_version(self):
495+
return TLSVersion(super().maximum_version)
496+
497+
@maximum_version.setter
498+
def maximum_version(self, value):
499+
super(SSLContext, SSLContext).maximum_version.__set__(self, value)
500+
470501
@property
471502
def options(self):
472503
return Options(super().options)

Lib/test/test_ssl.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,69 @@ def test_hostname_checks_common_name(self):
10771077
with self.assertRaises(AttributeError):
10781078
ctx.hostname_checks_common_name = True
10791079

1080+
@unittest.skipUnless(hasattr(ssl.SSLContext, 'minimum_version'),
1081+
"required OpenSSL 1.1.0g")
1082+
def test_min_max_version(self):
1083+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
1084+
self.assertEqual(
1085+
ctx.minimum_version, ssl.TLSVersion.MINIMUM_SUPPORTED
1086+
)
1087+
self.assertEqual(
1088+
ctx.maximum_version, ssl.TLSVersion.MAXIMUM_SUPPORTED
1089+
)
1090+
1091+
ctx.minimum_version = ssl.TLSVersion.TLSv1_1
1092+
ctx.maximum_version = ssl.TLSVersion.TLSv1_2
1093+
self.assertEqual(
1094+
ctx.minimum_version, ssl.TLSVersion.TLSv1_1
1095+
)
1096+
self.assertEqual(
1097+
ctx.maximum_version, ssl.TLSVersion.TLSv1_2
1098+
)
1099+
1100+
ctx.minimum_version = ssl.TLSVersion.MINIMUM_SUPPORTED
1101+
ctx.maximum_version = ssl.TLSVersion.TLSv1
1102+
self.assertEqual(
1103+
ctx.minimum_version, ssl.TLSVersion.MINIMUM_SUPPORTED
1104+
)
1105+
self.assertEqual(
1106+
ctx.maximum_version, ssl.TLSVersion.TLSv1
1107+
)
1108+
1109+
ctx.maximum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED
1110+
self.assertEqual(
1111+
ctx.maximum_version, ssl.TLSVersion.MAXIMUM_SUPPORTED
1112+
)
1113+
1114+
ctx.maximum_version = ssl.TLSVersion.MINIMUM_SUPPORTED
1115+
self.assertIn(
1116+
ctx.maximum_version,
1117+
{ssl.TLSVersion.TLSv1, ssl.TLSVersion.SSLv3}
1118+
)
1119+
1120+
ctx.minimum_version = ssl.TLSVersion.MAXIMUM_SUPPORTED
1121+
self.assertIn(
1122+
ctx.minimum_version,
1123+
{ssl.TLSVersion.TLSv1_2, ssl.TLSVersion.TLSv1_3}
1124+
)
1125+
1126+
with self.assertRaises(ValueError):
1127+
ctx.minimum_version = 42
1128+
1129+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_1)
1130+
1131+
self.assertEqual(
1132+
ctx.minimum_version, ssl.TLSVersion.MINIMUM_SUPPORTED
1133+
)
1134+
self.assertEqual(
1135+
ctx.maximum_version, ssl.TLSVersion.MAXIMUM_SUPPORTED
1136+
)
1137+
with self.assertRaises(ValueError):
1138+
ctx.minimum_version = ssl.TLSVersion.MINIMUM_SUPPORTED
1139+
with self.assertRaises(ValueError):
1140+
ctx.maximum_version = ssl.TLSVersion.TLSv1
1141+
1142+
10801143
@unittest.skipUnless(have_verify_flags(),
10811144
"verify_flags need OpenSSL > 0.9.8")
10821145
def test_verify_flags(self):
@@ -3457,6 +3520,60 @@ def test_tls1_3(self):
34573520
})
34583521
self.assertEqual(s.version(), 'TLSv1.3')
34593522

3523+
@unittest.skipUnless(hasattr(ssl.SSLContext, 'minimum_version'),
3524+
"required OpenSSL 1.1.0g")
3525+
def test_min_max_version(self):
3526+
client_context, server_context, hostname = testing_context()
3527+
# client TLSv1.0 to 1.2
3528+
client_context.minimum_version = ssl.TLSVersion.TLSv1
3529+
client_context.maximum_version = ssl.TLSVersion.TLSv1_2
3530+
# server only TLSv1.2
3531+
server_context.minimum_version = ssl.TLSVersion.TLSv1_2
3532+
server_context.maximum_version = ssl.TLSVersion.TLSv1_2
3533+
3534+
with ThreadedEchoServer(context=server_context) as server:
3535+
with client_context.wrap_socket(socket.socket(),
3536+
server_hostname=hostname) as s:
3537+
s.connect((HOST, server.port))
3538+
self.assertEqual(s.version(), 'TLSv1.2')
3539+
3540+
# client 1.0 to 1.2, server 1.0 to 1.1
3541+
server_context.minimum_version = ssl.TLSVersion.TLSv1
3542+
server_context.maximum_version = ssl.TLSVersion.TLSv1_1
3543+
3544+
with ThreadedEchoServer(context=server_context) as server:
3545+
with client_context.wrap_socket(socket.socket(),
3546+
server_hostname=hostname) as s:
3547+
s.connect((HOST, server.port))
3548+
self.assertEqual(s.version(), 'TLSv1.1')
3549+
3550+
# client 1.0, server 1.2 (mismatch)
3551+
server_context.minimum_version = ssl.TLSVersion.TLSv1_2
3552+
server_context.maximum_version = ssl.TLSVersion.TLSv1_2
3553+
client_context.minimum_version = ssl.TLSVersion.TLSv1
3554+
client_context.maximum_version = ssl.TLSVersion.TLSv1
3555+
with ThreadedEchoServer(context=server_context) as server:
3556+
with client_context.wrap_socket(socket.socket(),
3557+
server_hostname=hostname) as s:
3558+
with self.assertRaises(ssl.SSLError) as e:
3559+
s.connect((HOST, server.port))
3560+
self.assertIn("alert", str(e.exception))
3561+
3562+
3563+
@unittest.skipUnless(hasattr(ssl.SSLContext, 'minimum_version'),
3564+
"required OpenSSL 1.1.0g")
3565+
@unittest.skipUnless(ssl.HAS_SSLv3, "requires SSLv3 support")
3566+
def test_min_max_version_sslv3(self):
3567+
client_context, server_context, hostname = testing_context()
3568+
server_context.minimum_version = ssl.TLSVersion.SSLv3
3569+
client_context.minimum_version = ssl.TLSVersion.SSLv3
3570+
client_context.maximum_version = ssl.TLSVersion.SSLv3
3571+
with ThreadedEchoServer(context=server_context) as server:
3572+
with client_context.wrap_socket(socket.socket(),
3573+
server_hostname=hostname) as s:
3574+
s.connect((HOST, server.port))
3575+
self.assertEqual(s.version(), 'SSLv3')
3576+
34603577
@unittest.skipUnless(ssl.HAS_ECDH, "test requires ECDH-enabled OpenSSL")
34613578
def test_default_ecdh_curve(self):
34623579
# Issue #21015: elliptic curve-based Diffie Hellman key exchange
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add TLSVersion constants and SSLContext.maximum_version / minimum_version
2+
attributes. The new API wraps OpenSSL 1.1
3+
https://www.openssl.org/docs/man1.1.0/ssl/SSL_CTX_set_min_proto_version.html
4+
feature.

0 commit comments

Comments
 (0)