Skip to content

Commit df6ac7e

Browse files
tiranmiss-islington
authored andcommitted
bpo-38275: Skip ssl tests for disabled versions (GH-16386)
test_ssl now handles disabled TLS/SSL versions better. OpenSSL's crypto policy and run-time settings are recognized and tests for disabled versions are skipped. Signed-off-by: Christian Heimes <[email protected]> https://bugs.python.org/issue38275
1 parent 64b4a3a commit df6ac7e

File tree

2 files changed

+147
-51
lines changed

2 files changed

+147
-51
lines changed

Lib/test/test_ssl.py

Lines changed: 143 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import weakref
2020
import platform
2121
import sysconfig
22+
import functools
2223
try:
2324
import ctypes
2425
except ImportError:
@@ -146,6 +147,85 @@ def data_file(*name):
146147
OP_ENABLE_MIDDLEBOX_COMPAT = getattr(ssl, "OP_ENABLE_MIDDLEBOX_COMPAT", 0)
147148

148149

150+
def has_tls_protocol(protocol):
151+
"""Check if a TLS protocol is available and enabled
152+
153+
:param protocol: enum ssl._SSLMethod member or name
154+
:return: bool
155+
"""
156+
if isinstance(protocol, str):
157+
assert protocol.startswith('PROTOCOL_')
158+
protocol = getattr(ssl, protocol, None)
159+
if protocol is None:
160+
return False
161+
if protocol in {
162+
ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLS_SERVER,
163+
ssl.PROTOCOL_TLS_CLIENT
164+
}:
165+
# auto-negotiate protocols are always available
166+
return True
167+
name = protocol.name
168+
return has_tls_version(name[len('PROTOCOL_'):])
169+
170+
171+
@functools.lru_cache
172+
def has_tls_version(version):
173+
"""Check if a TLS/SSL version is enabled
174+
175+
:param version: TLS version name or ssl.TLSVersion member
176+
:return: bool
177+
"""
178+
if version == "SSLv2":
179+
# never supported and not even in TLSVersion enum
180+
return False
181+
182+
if isinstance(version, str):
183+
version = ssl.TLSVersion.__members__[version]
184+
185+
# check compile time flags like ssl.HAS_TLSv1_2
186+
if not getattr(ssl, f'HAS_{version.name}'):
187+
return False
188+
189+
# check runtime and dynamic crypto policy settings. A TLS version may
190+
# be compiled in but disabled by a policy or config option.
191+
ctx = ssl.SSLContext()
192+
if (
193+
ctx.minimum_version != ssl.TLSVersion.MINIMUM_SUPPORTED and
194+
version < ctx.minimum_version
195+
):
196+
return False
197+
if (
198+
ctx.maximum_version != ssl.TLSVersion.MAXIMUM_SUPPORTED and
199+
version > ctx.maximum_version
200+
):
201+
return False
202+
203+
return True
204+
205+
206+
def requires_tls_version(version):
207+
"""Decorator to skip tests when a required TLS version is not available
208+
209+
:param version: TLS version name or ssl.TLSVersion member
210+
:return:
211+
"""
212+
def decorator(func):
213+
@functools.wraps(func)
214+
def wrapper(*args, **kw):
215+
if not has_tls_version(version):
216+
raise unittest.SkipTest(f"{version} is not available.")
217+
else:
218+
return func(*args, **kw)
219+
return wrapper
220+
return decorator
221+
222+
223+
requires_minimum_version = unittest.skipUnless(
224+
hasattr(ssl.SSLContext, 'minimum_version'),
225+
"required OpenSSL >= 1.1.0g"
226+
)
227+
228+
149229
def handle_error(prefix):
150230
exc_format = ' '.join(traceback.format_exception(*sys.exc_info()))
151231
if support.verbose:
@@ -1107,20 +1187,23 @@ def test_hostname_checks_common_name(self):
11071187
with self.assertRaises(AttributeError):
11081188
ctx.hostname_checks_common_name = True
11091189

1110-
@unittest.skipUnless(hasattr(ssl.SSLContext, 'minimum_version'),
1111-
"required OpenSSL 1.1.0g")
1190+
@requires_minimum_version
11121191
@unittest.skipIf(IS_LIBRESSL, "see bpo-34001")
11131192
def test_min_max_version(self):
11141193
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
11151194
# OpenSSL default is MINIMUM_SUPPORTED, however some vendors like
11161195
# Fedora override the setting to TLS 1.0.
1196+
minimum_range = {
1197+
# stock OpenSSL
1198+
ssl.TLSVersion.MINIMUM_SUPPORTED,
1199+
# Fedora 29 uses TLS 1.0 by default
1200+
ssl.TLSVersion.TLSv1,
1201+
# RHEL 8 uses TLS 1.2 by default
1202+
ssl.TLSVersion.TLSv1_2
1203+
}
1204+
11171205
self.assertIn(
1118-
ctx.minimum_version,
1119-
{ssl.TLSVersion.MINIMUM_SUPPORTED,
1120-
# Fedora 29 uses TLS 1.0 by default
1121-
ssl.TLSVersion.TLSv1,
1122-
# RHEL 8 uses TLS 1.2 by default
1123-
ssl.TLSVersion.TLSv1_2}
1206+
ctx.minimum_version, minimum_range
11241207
)
11251208
self.assertEqual(
11261209
ctx.maximum_version, ssl.TLSVersion.MAXIMUM_SUPPORTED
@@ -1166,8 +1249,8 @@ def test_min_max_version(self):
11661249

11671250
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_1)
11681251

1169-
self.assertEqual(
1170-
ctx.minimum_version, ssl.TLSVersion.MINIMUM_SUPPORTED
1252+
self.assertIn(
1253+
ctx.minimum_version, minimum_range
11711254
)
11721255
self.assertEqual(
11731256
ctx.maximum_version, ssl.TLSVersion.MAXIMUM_SUPPORTED
@@ -2722,6 +2805,8 @@ def test_echo(self):
27222805
for protocol in PROTOCOLS:
27232806
if protocol in {ssl.PROTOCOL_TLS_CLIENT, ssl.PROTOCOL_TLS_SERVER}:
27242807
continue
2808+
if not has_tls_protocol(protocol):
2809+
continue
27252810
with self.subTest(protocol=ssl._PROTOCOL_NAMES[protocol]):
27262811
context = ssl.SSLContext(protocol)
27272812
context.load_cert_chain(CERTFILE)
@@ -3013,7 +3098,7 @@ def test_wrong_cert_tls12(self):
30133098
else:
30143099
self.fail("Use of invalid cert should have failed!")
30153100

3016-
@unittest.skipUnless(ssl.HAS_TLSv1_3, "Test needs TLS 1.3")
3101+
@requires_tls_version('TLSv1_3')
30173102
def test_wrong_cert_tls13(self):
30183103
client_context, server_context, hostname = testing_context()
30193104
# load client cert that is not signed by trusted CA
@@ -3108,8 +3193,7 @@ def test_ssl_cert_verify_error(self):
31083193
self.assertIn(msg, repr(e))
31093194
self.assertIn('certificate verify failed', repr(e))
31103195

3111-
@unittest.skipUnless(hasattr(ssl, 'PROTOCOL_SSLv2'),
3112-
"OpenSSL is compiled without SSLv2 support")
3196+
@requires_tls_version('SSLv2')
31133197
def test_protocol_sslv2(self):
31143198
"""Connecting to an SSLv2 server with various client options"""
31153199
if support.verbose:
@@ -3118,7 +3202,7 @@ def test_protocol_sslv2(self):
31183202
try_protocol_combo(ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_SSLv2, True, ssl.CERT_OPTIONAL)
31193203
try_protocol_combo(ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_SSLv2, True, ssl.CERT_REQUIRED)
31203204
try_protocol_combo(ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_TLS, False)
3121-
if hasattr(ssl, 'PROTOCOL_SSLv3'):
3205+
if has_tls_version('SSLv3'):
31223206
try_protocol_combo(ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_SSLv3, False)
31233207
try_protocol_combo(ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_TLSv1, False)
31243208
# SSLv23 client with specific SSL options
@@ -3135,7 +3219,7 @@ def test_PROTOCOL_TLS(self):
31353219
"""Connecting to an SSLv23 server with various client options"""
31363220
if support.verbose:
31373221
sys.stdout.write("\n")
3138-
if hasattr(ssl, 'PROTOCOL_SSLv2'):
3222+
if has_tls_version('SSLv2'):
31393223
try:
31403224
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_SSLv2, True)
31413225
except OSError as x:
@@ -3144,42 +3228,44 @@ def test_PROTOCOL_TLS(self):
31443228
sys.stdout.write(
31453229
" SSL2 client to SSL23 server test unexpectedly failed:\n %s\n"
31463230
% str(x))
3147-
if hasattr(ssl, 'PROTOCOL_SSLv3'):
3231+
if has_tls_version('SSLv3'):
31483232
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_SSLv3, False)
31493233
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLS, True)
3150-
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLSv1, 'TLSv1')
3234+
if has_tls_version('TLSv1'):
3235+
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLSv1, 'TLSv1')
31513236

3152-
if hasattr(ssl, 'PROTOCOL_SSLv3'):
3237+
if has_tls_version('SSLv3'):
31533238
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_SSLv3, False, ssl.CERT_OPTIONAL)
31543239
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLS, True, ssl.CERT_OPTIONAL)
3155-
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLSv1, 'TLSv1', ssl.CERT_OPTIONAL)
3240+
if has_tls_version('TLSv1'):
3241+
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLSv1, 'TLSv1', ssl.CERT_OPTIONAL)
31563242

3157-
if hasattr(ssl, 'PROTOCOL_SSLv3'):
3243+
if has_tls_version('SSLv3'):
31583244
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_SSLv3, False, ssl.CERT_REQUIRED)
31593245
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLS, True, ssl.CERT_REQUIRED)
3160-
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLSv1, 'TLSv1', ssl.CERT_REQUIRED)
3246+
if has_tls_version('TLSv1'):
3247+
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLSv1, 'TLSv1', ssl.CERT_REQUIRED)
31613248

31623249
# Server with specific SSL options
3163-
if hasattr(ssl, 'PROTOCOL_SSLv3'):
3250+
if has_tls_version('SSLv3'):
31643251
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_SSLv3, False,
31653252
server_options=ssl.OP_NO_SSLv3)
31663253
# Will choose TLSv1
31673254
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLS, True,
31683255
server_options=ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3)
3169-
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLSv1, False,
3170-
server_options=ssl.OP_NO_TLSv1)
3171-
3256+
if has_tls_version('TLSv1'):
3257+
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLSv1, False,
3258+
server_options=ssl.OP_NO_TLSv1)
31723259

3173-
@unittest.skipUnless(hasattr(ssl, 'PROTOCOL_SSLv3'),
3174-
"OpenSSL is compiled without SSLv3 support")
3260+
@requires_tls_version('SSLv3')
31753261
def test_protocol_sslv3(self):
31763262
"""Connecting to an SSLv3 server with various client options"""
31773263
if support.verbose:
31783264
sys.stdout.write("\n")
31793265
try_protocol_combo(ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_SSLv3, 'SSLv3')
31803266
try_protocol_combo(ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_SSLv3, 'SSLv3', ssl.CERT_OPTIONAL)
31813267
try_protocol_combo(ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_SSLv3, 'SSLv3', ssl.CERT_REQUIRED)
3182-
if hasattr(ssl, 'PROTOCOL_SSLv2'):
3268+
if has_tls_version('SSLv2'):
31833269
try_protocol_combo(ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_SSLv2, False)
31843270
try_protocol_combo(ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_TLS, False,
31853271
client_options=ssl.OP_NO_SSLv3)
@@ -3189,41 +3275,40 @@ def test_protocol_sslv3(self):
31893275
try_protocol_combo(ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_TLS,
31903276
False, client_options=ssl.OP_NO_SSLv2)
31913277

3278+
@requires_tls_version('TLSv1')
31923279
def test_protocol_tlsv1(self):
31933280
"""Connecting to a TLSv1 server with various client options"""
31943281
if support.verbose:
31953282
sys.stdout.write("\n")
31963283
try_protocol_combo(ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_TLSv1, 'TLSv1')
31973284
try_protocol_combo(ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_TLSv1, 'TLSv1', ssl.CERT_OPTIONAL)
31983285
try_protocol_combo(ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_TLSv1, 'TLSv1', ssl.CERT_REQUIRED)
3199-
if hasattr(ssl, 'PROTOCOL_SSLv2'):
3286+
if has_tls_version('SSLv2'):
32003287
try_protocol_combo(ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_SSLv2, False)
3201-
if hasattr(ssl, 'PROTOCOL_SSLv3'):
3288+
if has_tls_version('SSLv3'):
32023289
try_protocol_combo(ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_SSLv3, False)
32033290
try_protocol_combo(ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_TLS, False,
32043291
client_options=ssl.OP_NO_TLSv1)
32053292

3206-
@unittest.skipUnless(hasattr(ssl, "PROTOCOL_TLSv1_1"),
3207-
"TLS version 1.1 not supported.")
3293+
@requires_tls_version('TLSv1_1')
32083294
def test_protocol_tlsv1_1(self):
32093295
"""Connecting to a TLSv1.1 server with various client options.
32103296
Testing against older TLS versions."""
32113297
if support.verbose:
32123298
sys.stdout.write("\n")
32133299
try_protocol_combo(ssl.PROTOCOL_TLSv1_1, ssl.PROTOCOL_TLSv1_1, 'TLSv1.1')
3214-
if hasattr(ssl, 'PROTOCOL_SSLv2'):
3300+
if has_tls_version('SSLv2'):
32153301
try_protocol_combo(ssl.PROTOCOL_TLSv1_1, ssl.PROTOCOL_SSLv2, False)
3216-
if hasattr(ssl, 'PROTOCOL_SSLv3'):
3302+
if has_tls_version('SSLv3'):
32173303
try_protocol_combo(ssl.PROTOCOL_TLSv1_1, ssl.PROTOCOL_SSLv3, False)
32183304
try_protocol_combo(ssl.PROTOCOL_TLSv1_1, ssl.PROTOCOL_TLS, False,
32193305
client_options=ssl.OP_NO_TLSv1_1)
32203306

32213307
try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLSv1_1, 'TLSv1.1')
3222-
try_protocol_combo(ssl.PROTOCOL_TLSv1_1, ssl.PROTOCOL_TLSv1, False)
3223-
try_protocol_combo(ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_TLSv1_1, False)
3308+
try_protocol_combo(ssl.PROTOCOL_TLSv1_1, ssl.PROTOCOL_TLSv1_2, False)
3309+
try_protocol_combo(ssl.PROTOCOL_TLSv1_2, ssl.PROTOCOL_TLSv1_1, False)
32243310

3225-
@unittest.skipUnless(hasattr(ssl, "PROTOCOL_TLSv1_2"),
3226-
"TLS version 1.2 not supported.")
3311+
@requires_tls_version('TLSv1_2')
32273312
def test_protocol_tlsv1_2(self):
32283313
"""Connecting to a TLSv1.2 server with various client options.
32293314
Testing against older TLS versions."""
@@ -3232,9 +3317,9 @@ def test_protocol_tlsv1_2(self):
32323317
try_protocol_combo(ssl.PROTOCOL_TLSv1_2, ssl.PROTOCOL_TLSv1_2, 'TLSv1.2',
32333318
server_options=ssl.OP_NO_SSLv3|ssl.OP_NO_SSLv2,
32343319
client_options=ssl.OP_NO_SSLv3|ssl.OP_NO_SSLv2,)
3235-
if hasattr(ssl, 'PROTOCOL_SSLv2'):
3320+
if has_tls_version('SSLv2'):
32363321
try_protocol_combo(ssl.PROTOCOL_TLSv1_2, ssl.PROTOCOL_SSLv2, False)
3237-
if hasattr(ssl, 'PROTOCOL_SSLv3'):
3322+
if has_tls_version('SSLv3'):
32383323
try_protocol_combo(ssl.PROTOCOL_TLSv1_2, ssl.PROTOCOL_SSLv3, False)
32393324
try_protocol_combo(ssl.PROTOCOL_TLSv1_2, ssl.PROTOCOL_TLS, False,
32403325
client_options=ssl.OP_NO_TLSv1_2)
@@ -3677,7 +3762,7 @@ def test_version_basic(self):
36773762
self.assertIs(s.version(), None)
36783763
self.assertIs(s._sslobj, None)
36793764
s.connect((HOST, server.port))
3680-
if IS_OPENSSL_1_1_1 and ssl.HAS_TLSv1_3:
3765+
if IS_OPENSSL_1_1_1 and has_tls_version('TLSv1_3'):
36813766
self.assertEqual(s.version(), 'TLSv1.3')
36823767
elif ssl.OPENSSL_VERSION_INFO >= (1, 0, 2):
36833768
self.assertEqual(s.version(), 'TLSv1.2')
@@ -3686,8 +3771,7 @@ def test_version_basic(self):
36863771
self.assertIs(s._sslobj, None)
36873772
self.assertIs(s.version(), None)
36883773

3689-
@unittest.skipUnless(ssl.HAS_TLSv1_3,
3690-
"test requires TLSv1.3 enabled OpenSSL")
3774+
@requires_tls_version('TLSv1_3')
36913775
def test_tls1_3(self):
36923776
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
36933777
context.load_cert_chain(CERTFILE)
@@ -3704,9 +3788,9 @@ def test_tls1_3(self):
37043788
})
37053789
self.assertEqual(s.version(), 'TLSv1.3')
37063790

3707-
@unittest.skipUnless(hasattr(ssl.SSLContext, 'minimum_version'),
3708-
"required OpenSSL 1.1.0g")
3709-
def test_min_max_version(self):
3791+
@requires_minimum_version
3792+
@requires_tls_version('TLSv1_2')
3793+
def test_min_max_version_tlsv1_2(self):
37103794
client_context, server_context, hostname = testing_context()
37113795
# client TLSv1.0 to 1.2
37123796
client_context.minimum_version = ssl.TLSVersion.TLSv1
@@ -3721,7 +3805,13 @@ def test_min_max_version(self):
37213805
s.connect((HOST, server.port))
37223806
self.assertEqual(s.version(), 'TLSv1.2')
37233807

3808+
@requires_minimum_version
3809+
@requires_tls_version('TLSv1_1')
3810+
def test_min_max_version_tlsv1_1(self):
3811+
client_context, server_context, hostname = testing_context()
37243812
# client 1.0 to 1.2, server 1.0 to 1.1
3813+
client_context.minimum_version = ssl.TLSVersion.TLSv1
3814+
client_context.maximum_version = ssl.TLSVersion.TLSv1_2
37253815
server_context.minimum_version = ssl.TLSVersion.TLSv1
37263816
server_context.maximum_version = ssl.TLSVersion.TLSv1_1
37273817

@@ -3731,6 +3821,10 @@ def test_min_max_version(self):
37313821
s.connect((HOST, server.port))
37323822
self.assertEqual(s.version(), 'TLSv1.1')
37333823

3824+
@requires_minimum_version
3825+
@requires_tls_version('TLSv1_2')
3826+
def test_min_max_version_mismatch(self):
3827+
client_context, server_context, hostname = testing_context()
37343828
# client 1.0, server 1.2 (mismatch)
37353829
server_context.maximum_version = ssl.TLSVersion.TLSv1_2
37363830
server_context.minimum_version = ssl.TLSVersion.TLSv1_2
@@ -3743,10 +3837,8 @@ def test_min_max_version(self):
37433837
s.connect((HOST, server.port))
37443838
self.assertIn("alert", str(e.exception))
37453839

3746-
3747-
@unittest.skipUnless(hasattr(ssl.SSLContext, 'minimum_version'),
3748-
"required OpenSSL 1.1.0g")
3749-
@unittest.skipUnless(ssl.HAS_SSLv3, "requires SSLv3 support")
3840+
@requires_minimum_version
3841+
@requires_tls_version('SSLv3')
37503842
def test_min_max_version_sslv3(self):
37513843
client_context, server_context, hostname = testing_context()
37523844
server_context.minimum_version = ssl.TLSVersion.SSLv3
@@ -4270,7 +4362,7 @@ def test_session_handling(self):
42704362
'Session refers to a different SSLContext.')
42714363

42724364

4273-
@unittest.skipUnless(ssl.HAS_TLSv1_3, "Test needs TLS 1.3")
4365+
@unittest.skipUnless(has_tls_version('TLSv1_3'), "Test needs TLS 1.3")
42744366
class TestPostHandshakeAuth(unittest.TestCase):
42754367
def test_pha_setter(self):
42764368
protocols = [
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
test_ssl now handles disabled TLS/SSL versions better. OpenSSL's crypto
2+
policy and run-time settings are recognized and tests for disabled versions
3+
are skipped. Tests also accept more TLS minimum_versions for platforms that
4+
override OpenSSL's default with strict settings.

0 commit comments

Comments
 (0)