From cc6870f24d39c0a397d2da94872aace4230fb24a Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Mon, 6 May 2019 17:54:06 -0400 Subject: [PATCH 1/3] [3.6] bpo-35925: Skip SSL tests that fail due to weak external certs. (GH-13124) Modern Linux distros such as Debian Buster have default OpenSSL system configurations that reject connections to servers with weak certificates by default. This causes our test suite run with external networking resources enabled to skip these tests when they encounter such a failure. Fixing the network servers is a separate issue.. (cherry picked from commit 2cc0223f43a1ffd59c887a73e2b0ce5202f3be90) Co-authored-by: Gregory P. Smith --- Lib/test/test_httplib.py | 27 +++++++++++-- Lib/test/test_nntplib.py | 38 +++++++++++++++---- .../2019-05-06-18-29-54.bpo-35925.gwQPuC.rst | 1 + 3 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 Misc/NEWS.d/next/Tests/2019-05-06-18-29-54.bpo-35925.gwQPuC.rst diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 714d521cf0e504..49550467958ec1 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -4,6 +4,7 @@ import itertools import os import array +import re import socket import unittest @@ -1597,13 +1598,31 @@ def test_networked_good_cert(self): # We feed the server's cert as a validating cert import ssl support.requires('network') - with support.transient_internet('self-signed.pythontest.net'): + selfsigned_pythontestdotnet = 'self-signed.pythontest.net' + with support.transient_internet(selfsigned_pythontestdotnet): context = ssl.SSLContext(ssl.PROTOCOL_TLS) context.verify_mode = ssl.CERT_REQUIRED + self.assertEqual(context.verify_mode, ssl.CERT_REQUIRED) + self.assertEqual(context.check_hostname, True) context.load_verify_locations(CERT_selfsigned_pythontestdotnet) - h = client.HTTPSConnection('self-signed.pythontest.net', 443, context=context) - h.request('GET', '/') - resp = h.getresponse() + try: + h = client.HTTPSConnection(selfsigned_pythontestdotnet, 443, + context=context) + h.request('GET', '/') + resp = h.getresponse() + except ssl.SSLError as ssl_err: + ssl_err_str = str(ssl_err) + # In the error message of [SSL: CERTIFICATE_VERIFY_FAILED] on + # modern Linux distros (Debian Buster, etc) default OpenSSL + # configurations it'll fail saying "key too weak" until we + # address https://bugs.python.org/issue36816 to use a proper + # key size on self-signed.pythontest.net. + if re.search(r'(?i)key.too.weak', ssl_err_str): + raise unittest.SkipTest( + f'Got {ssl_err_str} trying to connect ' + f'to {selfsigned_pythontestdotnet}. ' + 'See https://bugs.python.org/issue36816.') + raise server_string = resp.getheader('server') resp.close() h.close() diff --git a/Lib/test/test_nntplib.py b/Lib/test/test_nntplib.py index d7642bc66ab346..1d1750a5be22e5 100644 --- a/Lib/test/test_nntplib.py +++ b/Lib/test/test_nntplib.py @@ -6,6 +6,8 @@ import functools import contextlib import os.path +import re + from test import support from nntplib import NNTP, GroupInfo import nntplib @@ -22,6 +24,13 @@ TIMEOUT = 30 certfile = os.path.join(os.path.dirname(__file__), 'keycert3.pem') +if ssl is not None: + SSLError = ssl.SSLError +else: + class SSLError(Exception): + """Non-existent exception class when we lack SSL support.""" + reason = "This will never be raised." + # TODO: # - test the `file` arg to more commands # - test error conditions @@ -262,14 +271,21 @@ def is_connected(): return False return True - with self.NNTP_CLASS(self.NNTP_HOST, timeout=TIMEOUT, usenetrc=False) as server: - self.assertTrue(is_connected()) - self.assertTrue(server.help()) - self.assertFalse(is_connected()) - - with self.NNTP_CLASS(self.NNTP_HOST, timeout=TIMEOUT, usenetrc=False) as server: - server.quit() - self.assertFalse(is_connected()) + try: + with self.NNTP_CLASS(self.NNTP_HOST, timeout=TIMEOUT, usenetrc=False) as server: + self.assertTrue(is_connected()) + self.assertTrue(server.help()) + self.assertFalse(is_connected()) + + with self.NNTP_CLASS(self.NNTP_HOST, timeout=TIMEOUT, usenetrc=False) as server: + server.quit() + self.assertFalse(is_connected()) + except SSLError as ssl_err: + # matches "[SSL: DH_KEY_TOO_SMALL] dh key too small" + if re.search(r'(?i)KEY.TOO.SMALL', ssl_err.reason): + raise unittest.SkipTest(f"Got {ssl_err} connecting " + f"to {self.NNTP_HOST!r}") + raise NetworkedNNTPTestsMixin.wrap_methods() @@ -290,6 +306,12 @@ def setUpClass(cls): try: cls.server = cls.NNTP_CLASS(cls.NNTP_HOST, timeout=TIMEOUT, usenetrc=False) + except SSLError as ssl_err: + # matches "[SSL: DH_KEY_TOO_SMALL] dh key too small" + if re.search(r'(?i)KEY.TOO.SMALL', ssl_err.reason): + raise unittest.SkipTest(f"{cls} got {ssl_err} connecting " + f"to {cls.NNTP_HOST!r}") + raise except EOFError: raise unittest.SkipTest(f"{cls} got EOF error on connecting " f"to {cls.NNTP_HOST!r}") diff --git a/Misc/NEWS.d/next/Tests/2019-05-06-18-29-54.bpo-35925.gwQPuC.rst b/Misc/NEWS.d/next/Tests/2019-05-06-18-29-54.bpo-35925.gwQPuC.rst new file mode 100644 index 00000000000000..ad8cc8fc61a07f --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2019-05-06-18-29-54.bpo-35925.gwQPuC.rst @@ -0,0 +1 @@ +Skip httplib and nntplib networking tests when they would otherwise fail due to a modern OS or distro with a default OpenSSL policy of rejecting connections to servers with weak certificates. From 8766941753e2a6bf9373198e94b38f6a39f0c1d0 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Sat, 11 May 2019 11:39:26 -0700 Subject: [PATCH 2/3] Also skip ssl tests that fail when the system rejects TLSv1. --- Lib/test/test_ssl.py | 35 +++++++++++++++++++ .../2019-05-06-18-29-54.bpo-35925.gwQPuC.rst | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 0aeabc10f2a97f..e47edd22401d87 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -17,6 +17,7 @@ import asyncore import weakref import platform +import re import functools try: import ctypes @@ -145,6 +146,38 @@ def f(*args, **kwargs): else: return func +def skip_if_openssl_cnf_minprotocol_gt_tls1(func): + """Skip a test if the OpenSSL config MinProtocol is > TLSv1. + + OS distros with an /etc/ssl/openssl.cnf and MinProtocol set often do so to + require TLSv1.2 or higher (Debian Buster). Some of our tests for older + protocol versions will fail under such a config. + + Alternative workaround: Run this test in a process with + OPENSSL_CONF=/dev/null in the environment. + """ + @functools.wraps(func) + def f(*args, **kwargs): + openssl_cnf = os.environ.get("OPENSSL_CONF", "/etc/ssl/openssl.cnf") + try: + with open(openssl_cnf, "r") as config: + for line in config: + match = re.match(r"MinProtocol\s*=\s*(TLSv\d+\S*)", line) + if match: + tls_ver = match.group(1) + if tls_ver > "TLSv1": + raise unittest.SkipTest( + "%s has MinProtocol = %s which is > TLSv1." % + (openssl_cnf, tls_ver)) + except (EnvironmentError, UnicodeDecodeError) as err: + # no config file found, etc. + if support.verbose: + sys.stdout.write("\n Could not scan %s for MinProtocol: %s\n" + % (openssl_cnf, err)) + return func(*args, **kwargs) + return f + + needs_sni = unittest.skipUnless(ssl.HAS_SNI, "SNI support needed for this test") @@ -2614,6 +2647,7 @@ def test_protocol_sslv2(self): client_options=ssl.OP_NO_TLSv1) @skip_if_broken_ubuntu_ssl + @skip_if_openssl_cnf_minprotocol_gt_tls1 def test_protocol_sslv23(self): """Connecting to an SSLv23 server with various client options""" if support.verbose: @@ -2691,6 +2725,7 @@ def test_protocol_tlsv1(self): @skip_if_broken_ubuntu_ssl @unittest.skipUnless(hasattr(ssl, "PROTOCOL_TLSv1_1"), "TLS version 1.1 not supported.") + @skip_if_openssl_cnf_minprotocol_gt_tls1 def test_protocol_tlsv1_1(self): """Connecting to a TLSv1.1 server with various client options. Testing against older TLS versions.""" diff --git a/Misc/NEWS.d/next/Tests/2019-05-06-18-29-54.bpo-35925.gwQPuC.rst b/Misc/NEWS.d/next/Tests/2019-05-06-18-29-54.bpo-35925.gwQPuC.rst index ad8cc8fc61a07f..1881b8d93913f3 100644 --- a/Misc/NEWS.d/next/Tests/2019-05-06-18-29-54.bpo-35925.gwQPuC.rst +++ b/Misc/NEWS.d/next/Tests/2019-05-06-18-29-54.bpo-35925.gwQPuC.rst @@ -1 +1 @@ -Skip httplib and nntplib networking tests when they would otherwise fail due to a modern OS or distro with a default OpenSSL policy of rejecting connections to servers with weak certificates. +Skip specific httplib, nntplib, and ssl networking tests when they would otherwise fail due to a modern OS or distro with a default OpenSSL policy of rejecting connections to servers with weak certificates or disabling TLS below TLSv1.2. From 60d4fb90b3ace2b5b19066b3edaafe8be9895e0d Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Sat, 11 May 2019 12:00:59 -0700 Subject: [PATCH 3/3] Remove the test_httplib change; server was updated. self-signed.pythontest.net was updated so the test_httplib change is no longer necessary. --- Lib/test/test_httplib.py | 27 +++---------------- .../2019-05-06-18-29-54.bpo-35925.gwQPuC.rst | 2 +- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 49550467958ec1..714d521cf0e504 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -4,7 +4,6 @@ import itertools import os import array -import re import socket import unittest @@ -1598,31 +1597,13 @@ def test_networked_good_cert(self): # We feed the server's cert as a validating cert import ssl support.requires('network') - selfsigned_pythontestdotnet = 'self-signed.pythontest.net' - with support.transient_internet(selfsigned_pythontestdotnet): + with support.transient_internet('self-signed.pythontest.net'): context = ssl.SSLContext(ssl.PROTOCOL_TLS) context.verify_mode = ssl.CERT_REQUIRED - self.assertEqual(context.verify_mode, ssl.CERT_REQUIRED) - self.assertEqual(context.check_hostname, True) context.load_verify_locations(CERT_selfsigned_pythontestdotnet) - try: - h = client.HTTPSConnection(selfsigned_pythontestdotnet, 443, - context=context) - h.request('GET', '/') - resp = h.getresponse() - except ssl.SSLError as ssl_err: - ssl_err_str = str(ssl_err) - # In the error message of [SSL: CERTIFICATE_VERIFY_FAILED] on - # modern Linux distros (Debian Buster, etc) default OpenSSL - # configurations it'll fail saying "key too weak" until we - # address https://bugs.python.org/issue36816 to use a proper - # key size on self-signed.pythontest.net. - if re.search(r'(?i)key.too.weak', ssl_err_str): - raise unittest.SkipTest( - f'Got {ssl_err_str} trying to connect ' - f'to {selfsigned_pythontestdotnet}. ' - 'See https://bugs.python.org/issue36816.') - raise + h = client.HTTPSConnection('self-signed.pythontest.net', 443, context=context) + h.request('GET', '/') + resp = h.getresponse() server_string = resp.getheader('server') resp.close() h.close() diff --git a/Misc/NEWS.d/next/Tests/2019-05-06-18-29-54.bpo-35925.gwQPuC.rst b/Misc/NEWS.d/next/Tests/2019-05-06-18-29-54.bpo-35925.gwQPuC.rst index 1881b8d93913f3..428326cdfe5bba 100644 --- a/Misc/NEWS.d/next/Tests/2019-05-06-18-29-54.bpo-35925.gwQPuC.rst +++ b/Misc/NEWS.d/next/Tests/2019-05-06-18-29-54.bpo-35925.gwQPuC.rst @@ -1 +1 @@ -Skip specific httplib, nntplib, and ssl networking tests when they would otherwise fail due to a modern OS or distro with a default OpenSSL policy of rejecting connections to servers with weak certificates or disabling TLS below TLSv1.2. +Skip specific nntplib and ssl networking tests when they would otherwise fail due to a modern OS or distro with a default OpenSSL policy of rejecting connections to servers with weak certificates or disabling TLS below TLSv1.2.