diff --git a/mig/install/MiGserver-template.conf b/mig/install/MiGserver-template.conf index 525c9bef5..3aaec9733 100644 --- a/mig/install/MiGserver-template.conf +++ b/mig/install/MiGserver-template.conf @@ -586,24 +586,15 @@ enable_sftp_subsys = __ENABLE_SFTP_SUBSYS__ # Pure Python WsgiDAV-based webdav(s) daemon enable_davs = __ENABLE_DAVS__ # Allow sub-optimal but still relatively strong legacy TLS support in WebDAVS -# NOTE: Python-2.7.x ssl supports TLSv1.2+ with strong ciphers and all popular +# NOTE: Python-2.7+ ssl supports TLSv1.2+ with strong ciphers and all popular # clients (including Windows 10+ native WebDAVS) also work with those. -# NOTE: Apparently Win 7 (+8.1?) native WebDAVS only works with semi-strong -# legacy ciphers and TLSv1.0+v1.1 unless updated and enabled -# NOTE: Win 7 went EoL in January 2020 and should no longer be needed #enable_davs_legacy_tls = False # Pure Python pyftpdlib-based ftp(s) daemon enable_ftps = __ENABLE_FTPS__ # Allow sub-optimal but still relatively strong legacy TLS supports in FTPS -# NOTE: Recent PyOpenSSL supports TLSv1.2+ with strong ciphers and all popular +# NOTE: Modern PyOpenSSL supports TLSv1.2+ with strong ciphers and all popular # clients also work with those. -# NOTE: CentOS 7 native pyOpenSSL 0.13 does NOT support elliptic curve ciphers -# and FileZilla fails on listdir with remaining strong DHE ciphers. -# Installing a recent pyopenssl e.g. from the centos-openstack-X repo -# allows disabling legacy tls support without breaking FileZilla support. -# TODO: disable as soon as a recent pyopenssl version is available - the one -# from pip breaks paramiko so do NOT go there. -enable_ftps_legacy_tls = True +#enable_ftps_legacy_tls = False # Enable WSGI served web pages (faster than CGI) - requires apache wsgi module enable_wsgi = __ENABLE_WSGI__ # Enable system notify helper used e.g. to warn about failed user logins @@ -638,6 +629,10 @@ peers_explicit_fields = __PEERS_EXPLICIT_FIELDS__ peers_contact_hint = __PEERS_CONTACT_HINT__ # Enable OpenID daemon for web access with user/pw from local user DB enable_openid = __ENABLE_OPENID__ +# Allow sub-optimal but still relatively strong legacy TLS support in OpenID 2.0 +# NOTE: Python-2.7+ ssl supports TLSv1.2+ with strong ciphers and all popular +# clients also work with those. +#enable_openid_legacy_tls = False # Enable share links for easy external exchange of data with anyone enable_sharelinks = __ENABLE_SHARELINKS__ # Enable storage quota diff --git a/mig/server/grid_ftps.py b/mig/server/grid_ftps.py index 3c0b6a214..b33482a33 100755 --- a/mig/server/grid_ftps.py +++ b/mig/server/grid_ftps.py @@ -96,6 +96,7 @@ from mig.shared.accountstate import check_account_accessible from mig.shared.base import invisible_path, force_utf8, force_native_str from mig.shared.conf import get_configuration_object +from mig.shared.defaults import STRONG_TLS_CIPHERS, STRONG_TLS_LEGACY_CIPHERS from mig.shared.fileio import user_chroot_exceptions from mig.shared.griddaemons.ftps import default_max_user_hits, \ default_user_abuse_hits, default_proto_abuse_hits, \ @@ -493,14 +494,21 @@ def start_service(conf): handler.tls_data_required = True keyfile = certfile = conf.user_ftps_key handler.certfile = certfile + ciphers = None # Harden TLS/SSL if possible, requires recent pyftpdlib if hasattr(handler, 'ssl_context'): - dhparamsfile = configuration.user_shared_dhparams + dhparams_path = configuration.user_shared_dhparams legacy_tls = configuration.site_enable_ftps_legacy_tls + if ciphers is not None: + use_ciphers = ciphers + elif legacy_tls: + use_ciphers = STRONG_TLS_LEGACY_CIPHERS + else: + use_ciphers = STRONG_TLS_CIPHERS ssl_ctx = hardened_openssl_context(conf, OpenSSL, keyfile, certfile, - dhparamsfile=dhparamsfile, - allow_pre_tlsv12=legacy_tls) + dhparamsfile=dhparams_path, + ciphers=use_ciphers) handler.ssl_context = ssl_ctx else: logger.warning("Unable to enforce explicit strong TLS connections") diff --git a/mig/server/grid_openid.py b/mig/server/grid_openid.py index 76744d03b..a762a42bb 100755 --- a/mig/server/grid_openid.py +++ b/mig/server/grid_openid.py @@ -102,6 +102,8 @@ from mig.shared.base import client_id_dir, cert_field_map, force_utf8, \ force_native_str from mig.shared.conf import get_configuration_object + from mig.shared.defaults import STRONG_TLS_CIPHERS, \ + STRONG_TLS_LEGACY_CIPHERS from mig.shared.griddaemons.openid import default_max_user_hits, \ default_user_abuse_hits, default_proto_abuse_hits, \ default_username_validator, refresh_user_creds, update_login_map, \ @@ -1654,6 +1656,7 @@ def start_service(configuration): data_path = configuration.openid_store daemon_conf = configuration.daemon_conf nossl = daemon_conf['nossl'] + ciphers = None addr = (host, port) # TODO: is this threaded version robust enough (thread safety)? # OpenIDServer = OpenIDHTTPServer @@ -1675,12 +1678,20 @@ def start_service(configuration): # Use best possible SSL/TLS args for this python version key_path = cert_path = configuration.user_openid_key dhparams_path = configuration.user_shared_dhparams + legacy_tls = configuration.site_enable_openid_legacy_tls + if ciphers is not None: + use_ciphers = ciphers + elif legacy_tls: + use_ciphers = STRONG_TLS_LEGACY_CIPHERS + else: + use_ciphers = STRONG_TLS_CIPHERS if not os.path.isfile(cert_path): logger.error('No such server key: %s' % cert_path) sys.exit(1) logger.info('Wrapping connections in SSL') ssl_ctx = hardened_ssl_context(configuration, key_path, cert_path, - dhparams_path) + dhparamsfile=dhparams_path, + ciphers=use_ciphers) httpserver.socket = ssl_ctx.wrap_socket(httpserver.socket, server_side=True) # Override default SSLSocket accept function to inject timeout support diff --git a/mig/server/grid_webdavs.py b/mig/server/grid_webdavs.py index 62d347e22..216b98e4e 100755 --- a/mig/server/grid_webdavs.py +++ b/mig/server/grid_webdavs.py @@ -335,8 +335,7 @@ def __init__(self, certificate, private_key, certificate_chain=None, context to use in all future connections in the wrap method. If the optional legacy_tls arg is set the STRONG_TLS_LEGACY_CIPHERS - are used instead of the STRONG_TLS_CIPHERS, and the limitation to - TLSv1.2+ is left out to allow legacy TLSv1.0 and TLSv1.1 connections. + are used instead of the STRONG_TLS_CIPHERS. This is required to support e.g. native Windows 7 WebDAVS access with the weak ECDHE-RSA-AES128-SHA cipher. """ @@ -345,7 +344,7 @@ def __init__(self, certificate, private_key, certificate_chain=None, certificate_chain, ciphers) # logger.debug("proceed with hardening of ssl contetx") # Set up hardened SSL context once and for all - dhparams = configuration.user_shared_dhparams + dhparams_path = configuration.user_shared_dhparams if ciphers is not None: use_ciphers = ciphers elif legacy_tls: @@ -353,9 +352,9 @@ def __init__(self, certificate, private_key, certificate_chain=None, else: use_ciphers = STRONG_TLS_CIPHERS self.context = hardened_ssl_context(configuration, self.private_key, - self.certificate, dhparams, - ciphers=use_ciphers, - allow_pre_tlsv12=legacy_tls) + self.certificate, + dhparamsfile=dhparams_path, + ciphers=use_ciphers) # logger.debug("established hardened ssl contetx") def __force_close(self, socket_list): @@ -1951,7 +1950,7 @@ def run(configuration): cert = config['ssl_certificate'] = configuration.user_davs_key key = config['ssl_private_key'] = configuration.user_davs_key chain = config['ssl_certificate_chain'] = '' - ciphers = config['ssl_ciphers'] = None + ciphers = None # NOTE: Briefly insert dummy user to avoid bogus warning about anon access # We dynamically add users as they connect so it isn't empty. diff --git a/mig/shared/configuration.py b/mig/shared/configuration.py index 1653dfe3a..4dca877ee 100644 --- a/mig/shared/configuration.py +++ b/mig/shared/configuration.py @@ -1603,6 +1603,11 @@ def reload_config(self, verbose, skip_log=False, disable_auth_log=False, 'SITE', 'enable_openid') else: self.site_enable_openid = False + if config.has_option('SITE', 'enable_openid_legacy_tls'): + self.site_enable_openid_legacy_tls = config.getboolean( + 'SITE', 'enable_openid_legacy_tls') + else: + self.site_enable_openid_legacy_tls = False if config.has_option('GLOBAL', 'user_openid_address'): self.user_openid_address = config.get('GLOBAL', 'user_openid_address') diff --git a/mig/shared/tlsserver.py b/mig/shared/tlsserver.py index 648feb956..812ef1281 100644 --- a/mig/shared/tlsserver.py +++ b/mig/shared/tlsserver.py @@ -20,7 +20,8 @@ # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. # # -- END_HEADER --- # @@ -40,7 +41,6 @@ def hardened_ssl_context(configuration, keyfile, certfile, dhparamsfile=None, ciphers=STRONG_TLS_CIPHERS, curve_priority=STRONG_TLS_CURVES, - allow_pre_tlsv12=False, allow_pre_tlsv13=True, allow_renegotiation=False, ): @@ -48,8 +48,8 @@ def hardened_ssl_context(configuration, keyfile, certfile, dhparamsfile=None, _logger = configuration.logger _logger.info("enforcing strong SSL/TLS connections") _logger.debug("using SSL/TLS ciphers: %s" % ciphers) - ssl_protocol = ssl.PROTOCOL_SSLv23 - ssl_ctx = ssl.SSLContext(ssl_protocol) + ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_ctx.minimum_version = ssl.TLSVersion.TLSv1_2 ssl_ctx.load_cert_chain(certfile, keyfile) ssl_options = 0 # NOTE: Override a number of weak and insecure legacy configurations @@ -58,11 +58,7 @@ def hardened_ssl_context(configuration, keyfile, certfile, dhparamsfile=None, ssl_options |= getattr(ssl, 'OP_NO_SSLv2', 0x1000000) ssl_options |= getattr(ssl, 'OP_NO_SSLv3', 0x2000000) ssl_options |= getattr(ssl, 'OP_NO_TLSv1', 0x4000000) - ssl_ctx.minimum_version = ssl.TLSVersion.TLSv1_1 - # NOTE: refuse weak TLS protocols unless allow_pre_tlsv12 - if not allow_pre_tlsv12: - ssl_options |= getattr(ssl, 'OP_NO_TLSv1_1', 0x10000000) - ssl_ctx.minimum_version = ssl.TLSVersion.TLSv1_2 + ssl_options |= getattr(ssl, 'OP_NO_TLSv1_1', 0x10000000) # NOTE: refuse slightly dated TLS 1.2 protocol unless allow_pre_tlsv13 if not allow_pre_tlsv13: if getattr(ssl, 'HAS_TLSv1_3', False): @@ -78,17 +74,13 @@ def hardened_ssl_context(configuration, keyfile, certfile, dhparamsfile=None, ssl_options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0x400000) ssl_options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0x80000) ssl_options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0x100000) - # Useful for debugging - # ssl_options |= getattr(ssl, 'OP_NO_TICKET', 0x0004000) - # ssl_options |= getattr(ssl, 'OP_NO_TLSv1_1', 0x10000000) - # ssl_options |= getattr(ssl, 'OP_NO_TLSv1_2', 0x8000000) - if sys.version_info[:2] >= (2, 7) and ssl_ctx: + if sys.version_info[:2] >= (3, 7) and ssl_ctx: _logger.info("enforcing strong SSL/TLS options") _logger.debug("SSL/TLS options: %s" % ssl_options) ssl_ctx.options |= ssl_options else: _logger.info("can't enforce strong SSL/TLS options") - _logger.warning("Upgrade to python 2.7.9+ for maximum security") + _logger.warning("Upgrade to python 3.7+ for maximum security") pfs_available = False if dhparamsfile: @@ -134,7 +126,6 @@ def hardened_openssl_context(configuration, OpenSSL, keyfile, certfile, cacertfile=None, dhparamsfile=None, ciphers=STRONG_TLS_CIPHERS, curve_priority=STRONG_TLS_CURVES, - allow_pre_tlsv12=False, allow_pre_tlsv13=True, allow_renegotiation=False, ): @@ -143,8 +134,10 @@ def hardened_openssl_context(configuration, OpenSSL, keyfile, certfile, SSL, crypto = OpenSSL.SSL, OpenSSL.crypto _logger.info("enforcing strong SSL/TLS connections") _logger.debug("using SSL/TLS ciphers: %s" % ciphers) - ssl_protocol = SSL.SSLv23_METHOD - ssl_ctx = SSL.Context(ssl_protocol) + ssl_ctx = SSL.Context(SSL.TLS_SERVER_METHOD) + ssl_ctx.set_min_proto_version(SSL.TLS1_2_VERSION) + # Mimic native ssl exposure of options + ssl_ctx._minimum_version = SSL.TLS1_2_VERSION ssl_ctx.use_certificate_chain_file(certfile) ssl_ctx.use_privatekey_file(keyfile) if cacertfile: @@ -157,15 +150,7 @@ def hardened_openssl_context(configuration, OpenSSL, keyfile, certfile, ssl_options |= getattr(SSL, 'OP_NO_SSLv2', 0x1000000) ssl_options |= getattr(SSL, 'OP_NO_SSLv3', 0x2000000) ssl_options |= getattr(SSL, 'OP_NO_TLSv1', 0x4000000) - ssl_ctx.set_min_proto_version(SSL.TLS1_1_VERSION) - # Mimic native ssl exposure of options - ssl_ctx._minimum_version = SSL.TLS1_1_VERSION - # NOTE: refuse weak TLS protocols unless allow_pre_tlsv12 - if not allow_pre_tlsv12: - ssl_options |= getattr(SSL, 'OP_NO_TLSv1_1', 0x10000000) - ssl_ctx.set_min_proto_version(SSL.TLS1_2_VERSION) - # Mimic native ssl exposure of options - ssl_ctx._minimum_version = SSL.TLS1_2_VERSION + ssl_options |= getattr(SSL, 'OP_NO_TLSv1_1', 0x10000000) # NOTE: refuse slightly dated TLS 1.2 protocol unless allow_pre_tlsv13 if not allow_pre_tlsv13: # IMPORTANT: OpenSSL doesn't have TLSv1.3 support marker at the moment, @@ -186,7 +171,7 @@ def hardened_openssl_context(configuration, OpenSSL, keyfile, certfile, ssl_options |= getattr(SSL, 'OP_CIPHER_SERVER_PREFERENCE', 0x400000) ssl_options |= getattr(SSL, 'OP_SINGLE_ECDH_USE', 0x80000) ssl_options |= getattr(SSL, 'OP_SINGLE_DH_USE', 0x100000) - if sys.version_info[:2] >= (2, 7) and ssl_ctx: + if sys.version_info[:2] >= (3, 7) and ssl_ctx: _logger.info("enforcing strong SSL/TLS options") _logger.debug("SSL/TLS options: %s" % ssl_options) ssl_ctx.set_options(ssl_options) @@ -194,7 +179,7 @@ def hardened_openssl_context(configuration, OpenSSL, keyfile, certfile, ssl_ctx._options = ssl_options else: _logger.info("can't enforce strong SSL/TLS options") - _logger.warning("Upgrade to python 2.7.9+ for maximum security") + _logger.warning("Upgrade to python 3.7+ for maximum security") # Mimic native ssl exposure of options ssl_ctx._options = None diff --git a/tests/fixture/confs-stdlocal/MiGserver.conf b/tests/fixture/confs-stdlocal/MiGserver.conf index c1d41b4ee..c8c0388f4 100644 --- a/tests/fixture/confs-stdlocal/MiGserver.conf +++ b/tests/fixture/confs-stdlocal/MiGserver.conf @@ -586,24 +586,15 @@ enable_sftp_subsys = False # Pure Python WsgiDAV-based webdav(s) daemon enable_davs = False # Allow sub-optimal but still relatively strong legacy TLS support in WebDAVS -# NOTE: Python-2.7.x ssl supports TLSv1.2+ with strong ciphers and all popular +# NOTE: Python-2.7+ ssl supports TLSv1.2+ with strong ciphers and all popular # clients (including Windows 10+ native WebDAVS) also work with those. -# NOTE: Apparently Win 7 (+8.1?) native WebDAVS only works with semi-strong -# legacy ciphers and TLSv1.0+v1.1 unless updated and enabled -# NOTE: Win 7 went EoL in January 2020 and should no longer be needed #enable_davs_legacy_tls = False # Pure Python pyftpdlib-based ftp(s) daemon enable_ftps = False # Allow sub-optimal but still relatively strong legacy TLS supports in FTPS -# NOTE: Recent PyOpenSSL supports TLSv1.2+ with strong ciphers and all popular +# NOTE: Modern PyOpenSSL supports TLSv1.2+ with strong ciphers and all popular # clients also work with those. -# NOTE: CentOS 7 native pyOpenSSL 0.13 does NOT support elliptic curve ciphers -# and FileZilla fails on listdir with remaining strong DHE ciphers. -# Installing a recent pyopenssl e.g. from the centos-openstack-X repo -# allows disabling legacy tls support without breaking FileZilla support. -# TODO: disable as soon as a recent pyopenssl version is available - the one -# from pip breaks paramiko so do NOT go there. -enable_ftps_legacy_tls = True +#enable_ftps_legacy_tls = False # Enable WSGI served web pages (faster than CGI) - requires apache wsgi module enable_wsgi = True # Enable system notify helper used e.g. to warn about failed user logins @@ -638,6 +629,10 @@ peers_explicit_fields = peers_contact_hint = employed here and authorized to invite external users # Enable OpenID daemon for web access with user/pw from local user DB enable_openid = False +# Allow sub-optimal but still relatively strong legacy TLS support in OpenID 2.0 +# NOTE: Python-2.7+ ssl supports TLSv1.2+ with strong ciphers and all popular +# clients also work with those. +#enable_openid_legacy_tls = False # Enable share links for easy external exchange of data with anyone enable_sharelinks = True # Enable storage quota diff --git a/tests/test_mig_shared_tlsserver.py b/tests/test_mig_shared_tlsserver.py index e9ce67f1a..b5bce8d53 100644 --- a/tests/test_mig_shared_tlsserver.py +++ b/tests/test_mig_shared_tlsserver.py @@ -185,12 +185,11 @@ def test_hardened_ssl_context_options_default_without_dhparams(self): config, TEST_KEY_FILE, TEST_CERT_FILE, - None, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - False, - True, - False + dhparamsfile=None, + ciphers=STRONG_TLS_CIPHERS, + curve_priority=STRONG_TLS_CURVES, + allow_pre_tlsv13=True, + allow_renegotiation=False ) # Verify options are set @@ -222,12 +221,11 @@ def test_hardened_ssl_context_options_default(self): config, TEST_KEY_FILE, TEST_CERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - False, - True, - False + dhparamsfile=TEST_DHPARAMS_FILE, + ciphers=STRONG_TLS_CIPHERS, + curve_priority=STRONG_TLS_CURVES, + allow_pre_tlsv13=True, + allow_renegotiation=False ) # Verify options are set @@ -250,42 +248,6 @@ def test_hardened_ssl_context_options_default(self): minimum_version = context.minimum_version self.assertEqual(minimum_version, ssl.TLSVersion.TLSv1_2) - def test_hardened_ssl_context_options_tls1_1(self): - """Test SSL context options are set correctly with TLS 1.1 enabled""" - config = self.configuration - config.logger = self.logger - - context = hardened_ssl_context( - config, - TEST_KEY_FILE, - TEST_CERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - True, - True, - False - ) - - # Verify options are set - expected_options = ( - getattr(ssl, 'OP_NO_SSLv2', 0x1000000) | - getattr(ssl, 'OP_NO_SSLv3', 0x2000000) | - getattr(ssl, 'OP_NO_TLSv1', 0x4000000) | - getattr(ssl, 'OP_NO_COMPRESSION', 0x20000) | - getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0x400000) | - getattr(ssl, 'OP_SINGLE_ECDH_USE', 0x80000) | - getattr(ssl, 'OP_SINGLE_DH_USE', 0x100000) | - getattr(ssl, 'OP_NO_RENEGOTIATION', 0x40000000) - ) - - # Verify the options were OR'd into the context - options = context.options - self.assertEqual(options & expected_options, expected_options) - # Verify that the minimum TLS version is enforced - minimum_version = context.minimum_version - self.assertEqual(minimum_version, ssl.TLSVersion.TLSv1_1) - def test_hardened_ssl_context_options_tls1_3_only(self): """Test SSL context options are set correctly with TLS 1.3 only""" config = self.configuration @@ -295,12 +257,11 @@ def test_hardened_ssl_context_options_tls1_3_only(self): config, TEST_KEY_FILE, TEST_CERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - False, - False, - False + dhparamsfile=TEST_DHPARAMS_FILE, + ciphers=STRONG_TLS_CIPHERS, + curve_priority=STRONG_TLS_CURVES, + allow_pre_tlsv13=False, + allow_renegotiation=False ) # Verify options are set @@ -333,46 +294,11 @@ def test_hardened_ssl_context_options_fail_reneg(self): config, TEST_KEY_FILE, TEST_CERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - False, - True, - True - ) - - # Verify options are set - expected_options = ( - getattr(ssl, 'OP_NO_SSLv2', 0x1000000) | - getattr(ssl, 'OP_NO_SSLv3', 0x2000000) | - getattr(ssl, 'OP_NO_TLSv1', 0x4000000) | - getattr(ssl, 'OP_NO_TLSv1_1', 0x10000000) | - getattr(ssl, 'OP_NO_COMPRESSION', 0x20000) | - getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0x400000) | - getattr(ssl, 'OP_SINGLE_ECDH_USE', 0x80000) | - getattr(ssl, 'OP_SINGLE_DH_USE', 0x100000) | - getattr(ssl, 'OP_NO_RENEGOTIATION', 0x40000000) - ) - - # Verify the options were OR'd into the context - self.assertNotEqual( - context.options & expected_options, expected_options) - - def test_hardened_ssl_context_options_fail_tls1_1(self): - """Test SSL context options fail when different""" - config = self.configuration - config.logger = self.logger - - context = hardened_ssl_context( - config, - TEST_KEY_FILE, - TEST_CERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - True, - True, - False + dhparamsfile=TEST_DHPARAMS_FILE, + ciphers=STRONG_TLS_CIPHERS, + curve_priority=STRONG_TLS_CURVES, + allow_pre_tlsv13=True, + allow_renegotiation=True ) # Verify options are set @@ -389,11 +315,11 @@ def test_hardened_ssl_context_options_fail_tls1_1(self): ) # Verify the options were OR'd into the context - self.assertNotEqual( - context.options & expected_options, expected_options) + options = context.options + self.assertNotEqual(options & expected_options, expected_options) def test_hardened_ssl_context_options_fail_tls1_2(self): - """Test SSL context options fail when different""" + """Test SSL context options fail when conflicting""" config = self.configuration config.logger = self.logger @@ -401,12 +327,11 @@ def test_hardened_ssl_context_options_fail_tls1_2(self): config, TEST_KEY_FILE, TEST_CERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - True, - False, - False + dhparamsfile=TEST_DHPARAMS_FILE, + ciphers=STRONG_TLS_CIPHERS, + curve_priority=STRONG_TLS_CURVES, + allow_pre_tlsv13=True, + allow_renegotiation=False ) # Verify options are set @@ -415,6 +340,7 @@ def test_hardened_ssl_context_options_fail_tls1_2(self): getattr(ssl, 'OP_NO_SSLv3', 0x2000000) | getattr(ssl, 'OP_NO_TLSv1', 0x4000000) | getattr(ssl, 'OP_NO_TLSv1_1', 0x10000000) | + getattr(ssl, 'OP_NO_TLSv1_2', 0x8000000) | getattr(ssl, 'OP_NO_COMPRESSION', 0x20000) | getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0x400000) | getattr(ssl, 'OP_SINGLE_ECDH_USE', 0x80000) | @@ -423,8 +349,11 @@ def test_hardened_ssl_context_options_fail_tls1_2(self): ) # Verify the options were OR'd into the context - self.assertNotEqual( - context.options & expected_options, expected_options) + options = context.options + self.assertNotEqual(options & expected_options, expected_options) + # Verify that the minimum TLS version is still enforced + minimum_version = context.minimum_version + self.assertEqual(minimum_version, ssl.TLSVersion.TLSv1_2) def test_hardened_ssl_context_ciphers(self): """Test SSL context ciphers are set correctly""" @@ -435,12 +364,11 @@ def test_hardened_ssl_context_ciphers(self): config, TEST_KEY_FILE, TEST_CERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - False, - True, - False + dhparamsfile=TEST_DHPARAMS_FILE, + ciphers=STRONG_TLS_CIPHERS, + curve_priority=STRONG_TLS_CURVES, + allow_pre_tlsv13=True, + allow_renegotiation=False ) # NOTE: this may be too platform specific expected_start = "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:" @@ -458,12 +386,11 @@ def test_hardened_ssl_context_legacy_ciphers(self): config, TEST_KEY_FILE, TEST_CERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_LEGACY_CIPHERS, - STRONG_TLS_CURVES, - False, - True, - False + dhparamsfile=TEST_DHPARAMS_FILE, + ciphers=STRONG_TLS_LEGACY_CIPHERS, + curve_priority=STRONG_TLS_CURVES, + allow_pre_tlsv13=True, + allow_renegotiation=False ) # NOTE: this may be too platform specific expected_start = "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:" @@ -484,13 +411,12 @@ def test_hardened_openssl_context_options_default_without_dhparams(self): OpenSSL, TEST_KEY_FILE, TEST_CERT_FILE, - TEST_CACERT_FILE, - None, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - False, - True, - False + cacertfile=TEST_CACERT_FILE, + dhparamsfile=None, + ciphers=STRONG_TLS_CIPHERS, + curve_priority=STRONG_TLS_CURVES, + allow_pre_tlsv13=True, + allow_renegotiation=False ) # Verify options are set @@ -525,13 +451,12 @@ def test_hardened_openssl_context_options_default(self): OpenSSL, TEST_KEY_FILE, TEST_CERT_FILE, - TEST_CACERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - False, - True, - False + cacertfile=TEST_CACERT_FILE, + dhparamsfile=TEST_DHPARAMS_FILE, + ciphers=STRONG_TLS_CIPHERS, + curve_priority=STRONG_TLS_CURVES, + allow_pre_tlsv13=True, + allow_renegotiation=False ) # Verify options are set @@ -554,46 +479,6 @@ def test_hardened_openssl_context_options_default(self): minimum_version = getattr(context, '_minimum_version', None) self.assertEqual(minimum_version, SSL.TLS1_2_VERSION) - @unittest.skipIf(OpenSSL is None, "pyOpenSSL is required for openssl test") - def test_hardened_openssl_context_options_tls1_1(self): - """Test OpenSSL context options are set correctly with TLS 1.1 enabled""" - config = self.configuration - config.logger = self.logger - SSL = OpenSSL.SSL - - context = hardened_openssl_context( - config, - OpenSSL, - TEST_KEY_FILE, - TEST_CERT_FILE, - TEST_CACERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - True, - True, - False - ) - - # Verify options are set - expected_options = ( - getattr(SSL, 'OP_NO_SSLv2', 0x1000000) | - getattr(SSL, 'OP_NO_SSLv3', 0x2000000) | - getattr(SSL, 'OP_NO_TLSv1', 0x4000000) | - getattr(SSL, 'OP_NO_COMPRESSION', 0x20000) | - getattr(SSL, 'OP_CIPHER_SERVER_PREFERENCE', 0x400000) | - getattr(SSL, 'OP_SINGLE_ECDH_USE', 0x80000) | - getattr(SSL, 'OP_SINGLE_DH_USE', 0x100000) | - getattr(SSL, 'OP_NO_RENEGOTIATION', 0x40000000) - ) - - # Verify the options were OR'd into the context - options = getattr(context, '_options', None) - self.assertEqual(options & expected_options, expected_options) - # Verify that the minimum TLS version is enforced - minimum_version = getattr(context, '_minimum_version', None) - self.assertEqual(minimum_version, SSL.TLS1_1_VERSION) - @unittest.skipIf(OpenSSL is None, "pyOpenSSL is required for openssl test") def test_hardened_openssl_context_options_tls1_3_only(self): """Test OpenSSL context options are set correctly with TLS 1.3 only""" @@ -606,13 +491,12 @@ def test_hardened_openssl_context_options_tls1_3_only(self): OpenSSL, TEST_KEY_FILE, TEST_CERT_FILE, - TEST_CACERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - False, - False, - False + cacertfile=TEST_CACERT_FILE, + dhparamsfile=TEST_DHPARAMS_FILE, + ciphers=STRONG_TLS_CIPHERS, + curve_priority=STRONG_TLS_CURVES, + allow_pre_tlsv13=False, + allow_renegotiation=False ) # Verify options are set @@ -648,51 +532,12 @@ def test_hardened_openssl_context_options_fail_reneg(self): OpenSSL, TEST_KEY_FILE, TEST_CERT_FILE, - TEST_CACERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - False, - True, - True - ) - - # Verify options are set - expected_options = ( - getattr(SSL, 'OP_NO_SSLv2', 0x1000000) | - getattr(SSL, 'OP_NO_SSLv3', 0x2000000) | - getattr(SSL, 'OP_NO_TLSv1', 0x4000000) | - getattr(SSL, 'OP_NO_TLSv1_1', 0x10000000) | - getattr(SSL, 'OP_NO_COMPRESSION', 0x20000) | - getattr(SSL, 'OP_CIPHER_SERVER_PREFERENCE', 0x400000) | - getattr(SSL, 'OP_SINGLE_ECDH_USE', 0x80000) | - getattr(SSL, 'OP_SINGLE_DH_USE', 0x100000) | - getattr(SSL, 'OP_NO_RENEGOTIATION', 0x40000000) - ) - - # Verify the options were OR'd into the context - options = getattr(context, '_options', None) - self.assertNotEqual(options & expected_options, expected_options) - - @unittest.skipIf(OpenSSL is None, "pyOpenSSL is required for openssl test") - def test_hardened_openssl_context_options_fail_tls1_1(self): - """Test OpenSSL context options fail when different""" - config = self.configuration - config.logger = self.logger - SSL = OpenSSL.SSL - - context = hardened_openssl_context( - config, - OpenSSL, - TEST_KEY_FILE, - TEST_CERT_FILE, - TEST_CACERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - True, - True, - False + cacertfile=TEST_CACERT_FILE, + dhparamsfile=TEST_DHPARAMS_FILE, + ciphers=STRONG_TLS_CIPHERS, + curve_priority=STRONG_TLS_CURVES, + allow_pre_tlsv13=True, + allow_renegotiation=True ) # Verify options are set @@ -714,7 +559,7 @@ def test_hardened_openssl_context_options_fail_tls1_1(self): @unittest.skipIf(OpenSSL is None, "pyOpenSSL is required for openssl test") def test_hardened_openssl_context_options_fail_tls1_2(self): - """Test OpenSSL context options fail when different""" + """Test OpenSSL context options fail when conflicting""" config = self.configuration config.logger = self.logger SSL = OpenSSL.SSL @@ -724,13 +569,12 @@ def test_hardened_openssl_context_options_fail_tls1_2(self): OpenSSL, TEST_KEY_FILE, TEST_CERT_FILE, - TEST_CACERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - True, - False, - False + cacertfile=TEST_CACERT_FILE, + dhparamsfile=TEST_DHPARAMS_FILE, + ciphers=STRONG_TLS_CIPHERS, + curve_priority=STRONG_TLS_CURVES, + allow_pre_tlsv13=True, + allow_renegotiation=False ) # Verify options are set @@ -739,6 +583,7 @@ def test_hardened_openssl_context_options_fail_tls1_2(self): getattr(SSL, 'OP_NO_SSLv3', 0x2000000) | getattr(SSL, 'OP_NO_TLSv1', 0x4000000) | getattr(SSL, 'OP_NO_TLSv1_1', 0x10000000) | + getattr(SSL, 'OP_NO_TLSv1_2', 0x8000000) | getattr(SSL, 'OP_NO_COMPRESSION', 0x20000) | getattr(SSL, 'OP_CIPHER_SERVER_PREFERENCE', 0x400000) | getattr(SSL, 'OP_SINGLE_ECDH_USE', 0x80000) | @@ -749,6 +594,9 @@ def test_hardened_openssl_context_options_fail_tls1_2(self): # Verify the options were OR'd into the context options = getattr(context, '_options', None) self.assertNotEqual(options & expected_options, expected_options) + # Verify that the minimum TLS version is still enforced + minimum_version = getattr(context, '_minimum_version', None) + self.assertEqual(minimum_version, SSL.TLS1_2_VERSION) @unittest.skipIf(OpenSSL is None, "pyOpenSSL is required for openssl test") def test_hardened_openssl_context_ciphers(self): @@ -762,13 +610,12 @@ def test_hardened_openssl_context_ciphers(self): OpenSSL, TEST_KEY_FILE, TEST_CERT_FILE, - TEST_CACERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_CIPHERS, - STRONG_TLS_CURVES, - False, - True, - False + cacertfile=TEST_CACERT_FILE, + dhparamsfile=TEST_DHPARAMS_FILE, + ciphers=STRONG_TLS_CIPHERS, + curve_priority=STRONG_TLS_CURVES, + allow_pre_tlsv13=True, + allow_renegotiation=False ) # NOTE: this may be too platform specific expected_start = "ECDHE-ECDSA-AES128-GCM-SHA256:" @@ -789,13 +636,12 @@ def test_hardened_openssl_context_legacy_ciphers(self): OpenSSL, TEST_KEY_FILE, TEST_CERT_FILE, - TEST_CACERT_FILE, - TEST_DHPARAMS_FILE, - STRONG_TLS_LEGACY_CIPHERS, - STRONG_TLS_CURVES, - False, - True, - False + cacertfile=TEST_CACERT_FILE, + dhparamsfile=TEST_DHPARAMS_FILE, + ciphers=STRONG_TLS_LEGACY_CIPHERS, + curve_priority=STRONG_TLS_CURVES, + allow_pre_tlsv13=True, + allow_renegotiation=False ) # NOTE: this may be too platform specific expected_start = "ECDHE-RSA-AES128-GCM-SHA256:"