From 66ba8442dcaba24de0322793876bd241858b49a3 Mon Sep 17 00:00:00 2001 From: Guillaume Chauvel Date: Wed, 15 Jul 2020 19:34:49 +0200 Subject: [PATCH] Create SSL context using PROTOCOL_TLS, fallback to highest supported version Zuul test: tests.unit.test_scheduler.TestSchedulerSSL.test_jobs_executed fails on ubuntu focal with the following exception: Traceback (most recent call last): File "/home/gchauvel/zuul/zuul/.tox/py38/lib/python3.8/site-packages/gear/__init__.py", line 2835, in _doConnectLoop self.connectLoop() File "/home/gchauvel/zuul/zuul/.tox/py38/lib/python3.8/site-packages/gear/__init__.py", line 2865, in connectLoop c = context.wrap_socket(c, server_side=True) File "/usr/lib/python3.8/ssl.py", line 500, in wrap_socket return self.sslsocket_class._create( File "/usr/lib/python3.8/ssl.py", line 1040, in _create self.do_handshake() File "/usr/lib/python3.8/ssl.py", line 1309, in do_handshake self._sslobj.do_handshake() ssl.SSLError: [SSL] internal error (_ssl.c:1108) This is due to libssl1.1 being compiled with "-DOPENSSL_TLS_SECURITY_LEVEL=2" and gear forcing TLSv1.0 Extracted from ubuntu source package: The default security level for TLS connections was increased from level 1 to level 2. This moves from the 80 bit security level to the 112 bit security level and will require 2048 bit or larger RSA and DHE keys, 224 bit or larger ECC keys, SHA-2, TLSv1.2 or DTLSv1.2. Allowing to negotiate TLS to the highest available version between server and client solves the issue, provided that TLSv1.2 is useable. The option is supported by in the latest version of all pythons >=3.5 [1][2][3]. Unfortunately Xenial doesn't have latest 3.5 and lacks the ssl.PROTOCOL_TLS definition. We provide a fallback to select the highest version of TLS supported in that case. There is some risk using the fallback beacuse both the client and server need to agree on the version supported in this case. Xenial python 3.5 does support TLSv1_2 which means that for all practical purposes TLS v1.2 should be available on all platforms that gear runs avoiding this problem. Disable TLSv1.3: According to https://bugs.python.org/issue43622#msg389497, an event on ssl socket can happen without data being available at application level. As gear is using a polling loop with multiple file descriptors and ssl socket used as a blocking one, a blocked state could happen. This is highlighted by Zuul SSL test: TestSchedulerSSL, where such blocked state appears consistently. note: gear tests and zuul tests are ok when using TLSv1.2 but the previous behavior could also happen [1] https://docs.python.org/2.7/library/ssl.html?highlight=protocol_tls#ssl.PROTOCOL_TLS [2] https://docs.python.org/3.5/library/ssl.html?highlight=protocol_tls#ssl.PROTOCOL_TLS [3] https://docs.python.org/3/library/ssl.html?highlight=protocol_tls#ssl.PROTOCOL_TLS Change-Id: I5efb6c0576987815c5b93f8bc4020cdee2898d04 --- gear/__init__.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/gear/__init__.py b/gear/__init__.py index 6287281..6e0f1c8 100644 --- a/gear/__init__.py +++ b/gear/__init__.py @@ -91,6 +91,44 @@ def convert_to_bytes(data): return data +def best_tls_version(): + if hasattr(ssl, 'PROTOCOL_TLS'): + return ssl.PROTOCOL_TLS + # Note there is some risk in selecting tls 1.2 if available + # as both the client and server may not support it and need 1.1 + # or 1.0. However, a xenial installation with python 3.5 does + # support 1.2 which is probably as old a setup as we need to worry + # about. + elif hasattr(ssl, 'PROTOCOL_TLSv1_2'): + return ssl.PROTOCOL_TLSv1_2 + elif hasattr(ssl, 'PROTOCOL_TLSv1_1'): + return ssl.PROTOCOL_TLSv1_1 + elif hasattr(ssl, 'PROTOCOL_TLSv1'): + return ssl.PROTOCOL_TLSv1 + else: + raise ConnectionError('No supported TLS version available.') + + +def create_ssl_context(): + tls_version = best_tls_version() + context = ssl.SSLContext(tls_version) + + # Disable TLSv1.3 + # According to https://bugs.python.org/issue43622#msg389497, an event on + # ssl socket can happen without data being available at application level. + # As gear is using a polling loop with multiple file descriptors and ssl + # socket used as a blocking one, a blocked state could happen. + # This is highlighted by Zuul SSL test: TestSchedulerSSL, where such + # blocked state appears consistently. + # note: gear tests and zuul tests are ok for TLSv1.2 but this behavior + # could also happen + if (hasattr(ssl, 'PROTOCOL_TLS') and + tls_version == ssl.PROTOCOL_TLS): + context.options |= ssl.OP_NO_TLSv1_3 + + return context + + class Task(object): def __init__(self): self._wait_event = threading.Event() @@ -209,7 +247,7 @@ class Connection(object): if self.use_ssl: self.log.debug("Using SSL") - context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + context = create_ssl_context() context.verify_mode = ssl.CERT_REQUIRED context.check_hostname = False context.load_cert_chain(self.ssl_cert, self.ssl_key) @@ -2862,7 +2900,7 @@ class Server(BaseClientServer): self.log.debug("Accepting new connection") c, addr = self.socket.accept() if self.use_ssl: - context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + context = create_ssl_context() context.verify_mode = ssl.CERT_REQUIRED context.load_cert_chain(self.ssl_cert, self.ssl_key) context.load_verify_locations(self.ssl_ca)