diff options
-rw-r--r-- | Doc/whatsnew/3.7.rst | 6 | ||||
-rw-r--r-- | Lib/ssl.py | 48 | ||||
-rw-r--r-- | Lib/test/test_ssl.py | 16 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2018-01-28-22-40-05.bpo-31429.qNt8rQ.rst | 4 | ||||
-rw-r--r-- | Modules/_ssl.c | 35 | ||||
-rwxr-xr-x | configure | 48 | ||||
-rw-r--r-- | configure.ac | 37 | ||||
-rw-r--r-- | pyconfig.h.in | 7 |
8 files changed, 153 insertions, 48 deletions
diff --git a/Doc/whatsnew/3.7.rst b/Doc/whatsnew/3.7.rst index b70945f..e36e505 100644 --- a/Doc/whatsnew/3.7.rst +++ b/Doc/whatsnew/3.7.rst @@ -623,6 +623,12 @@ wildcard matching disabled by default. (Contributed by Mandeep Singh in :issue:`23033` and Christian Heimes in :issue:`31399`.) +The default cipher suite selection of the ssl module now uses a blacklist +approach rather than a hard-coded whitelist. Python no longer re-enables +ciphers that have been blocked by OpenSSL security update. Default cipher +suite selection can be configured on compile time. +(Contributed by Christian Heimes in :issue:`31429`.) + string ------ @@ -115,6 +115,7 @@ except ImportError: from _ssl import HAS_SNI, HAS_ECDH, HAS_NPN, HAS_ALPN, HAS_TLSv1_3 +from _ssl import _DEFAULT_CIPHERS from _ssl import _OPENSSL_API_VERSION @@ -174,48 +175,7 @@ else: HAS_NEVER_CHECK_COMMON_NAME = hasattr(_ssl, 'HOSTFLAG_NEVER_CHECK_SUBJECT') -# Disable weak or insecure ciphers by default -# (OpenSSL's default setting is 'DEFAULT:!aNULL:!eNULL') -# Enable a better set of ciphers by default -# This list has been explicitly chosen to: -# * TLS 1.3 ChaCha20 and AES-GCM cipher suites -# * Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE) -# * Prefer ECDHE over DHE for better performance -# * Prefer AEAD over CBC for better performance and security -# * Prefer AES-GCM over ChaCha20 because most platforms have AES-NI -# (ChaCha20 needs OpenSSL 1.1.0 or patched 1.0.2) -# * Prefer any AES-GCM and ChaCha20 over any AES-CBC for better -# performance and security -# * Then Use HIGH cipher suites as a fallback -# * Disable NULL authentication, NULL encryption, 3DES and MD5 MACs -# for security reasons -_DEFAULT_CIPHERS = ( - 'TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:' - 'TLS13-AES-128-GCM-SHA256:' - 'ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+CHACHA20:ECDH+AES256:DH+AES256:' - 'ECDH+AES128:DH+AES:ECDH+HIGH:DH+HIGH:RSA+AESGCM:RSA+AES:RSA+HIGH:' - '!aNULL:!eNULL:!MD5:!3DES' - ) - -# Restricted and more secure ciphers for the server side -# This list has been explicitly chosen to: -# * TLS 1.3 ChaCha20 and AES-GCM cipher suites -# * Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE) -# * Prefer ECDHE over DHE for better performance -# * Prefer AEAD over CBC for better performance and security -# * Prefer AES-GCM over ChaCha20 because most platforms have AES-NI -# * Prefer any AES-GCM and ChaCha20 over any AES-CBC for better -# performance and security -# * Then Use HIGH cipher suites as a fallback -# * Disable NULL authentication, NULL encryption, MD5 MACs, DSS, RC4, and -# 3DES for security reasons -_RESTRICTED_SERVER_CIPHERS = ( - 'TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:' - 'TLS13-AES-128-GCM-SHA256:' - 'ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+CHACHA20:ECDH+AES256:DH+AES256:' - 'ECDH+AES128:DH+AES:ECDH+HIGH:DH+HIGH:RSA+AESGCM:RSA+AES:RSA+HIGH:' - '!aNULL:!eNULL:!MD5:!DSS:!RC4:!3DES' -) +_RESTRICTED_SERVER_CIPHERS = _DEFAULT_CIPHERS CertificateError = SSLCertVerificationError @@ -393,8 +353,6 @@ class SSLContext(_SSLContext): def __new__(cls, protocol=PROTOCOL_TLS, *args, **kwargs): self = _SSLContext.__new__(cls, protocol) - if protocol != _SSLv2_IF_EXISTS: - self.set_ciphers(_DEFAULT_CIPHERS) return self def __init__(self, protocol=PROTOCOL_TLS): @@ -530,8 +488,6 @@ def create_default_context(purpose=Purpose.SERVER_AUTH, *, cafile=None, # verify certs and host name in client mode context.verify_mode = CERT_REQUIRED context.check_hostname = True - elif purpose == Purpose.CLIENT_AUTH: - context.set_ciphers(_RESTRICTED_SERVER_CIPHERS) if cafile or capath or cadata: context.load_verify_locations(cafile, capath, cadata) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index fdf727f..6a8bf0e 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -18,6 +18,7 @@ import asyncore import weakref import platform import functools +import sysconfig try: import ctypes except ImportError: @@ -30,7 +31,7 @@ PROTOCOLS = sorted(ssl._PROTOCOL_NAMES) HOST = support.HOST IS_LIBRESSL = ssl.OPENSSL_VERSION.startswith('LibreSSL') IS_OPENSSL_1_1 = not IS_LIBRESSL and ssl.OPENSSL_VERSION_INFO >= (1, 1, 0) - +PY_SSL_DEFAULT_CIPHERS = sysconfig.get_config_var('PY_SSL_DEFAULT_CIPHERS') def data_file(*name): return os.path.join(os.path.dirname(__file__), *name) @@ -936,6 +937,19 @@ class ContextTests(unittest.TestCase): with self.assertRaisesRegex(ssl.SSLError, "No cipher can be selected"): ctx.set_ciphers("^$:,;?*'dorothyx") + @unittest.skipUnless(PY_SSL_DEFAULT_CIPHERS == 1, + "Test applies only to Python default ciphers") + def test_python_ciphers(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ciphers = ctx.get_ciphers() + for suite in ciphers: + name = suite['name'] + self.assertNotIn("PSK", name) + self.assertNotIn("SRP", name) + self.assertNotIn("MD5", name) + self.assertNotIn("RC4", name) + self.assertNotIn("3DES", name) + @unittest.skipIf(ssl.OPENSSL_VERSION_INFO < (1, 0, 2, 0, 0), 'OpenSSL too old') def test_get_ciphers(self): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) diff --git a/Misc/NEWS.d/next/Library/2018-01-28-22-40-05.bpo-31429.qNt8rQ.rst b/Misc/NEWS.d/next/Library/2018-01-28-22-40-05.bpo-31429.qNt8rQ.rst new file mode 100644 index 0000000..1ac537b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-01-28-22-40-05.bpo-31429.qNt8rQ.rst @@ -0,0 +1,4 @@ +The default cipher suite selection of the ssl module now uses a blacklist +approach rather than a hard-coded whitelist. Python no longer re-enables +ciphers that have been blocked by OpenSSL security update. Default cipher +suite selection can be configured on compile time. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index ec8c8af..7545e91 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -234,6 +234,31 @@ SSL_SESSION_get_ticket_lifetime_hint(const SSL_SESSION *s) #endif /* OpenSSL < 1.1.0 or LibreSSL */ +/* Default cipher suites */ +#ifndef PY_SSL_DEFAULT_CIPHERS +#define PY_SSL_DEFAULT_CIPHERS 1 +#endif + +#if PY_SSL_DEFAULT_CIPHERS == 0 + #ifndef PY_SSL_DEFAULT_CIPHER_STRING + #error "Py_SSL_DEFAULT_CIPHERS 0 needs Py_SSL_DEFAULT_CIPHER_STRING" + #endif +#elif PY_SSL_DEFAULT_CIPHERS == 1 +/* Python custom selection of sensible ciper suites + * DEFAULT: OpenSSL's default cipher list. Since 1.0.2 the list is in sensible order. + * !aNULL:!eNULL: really no NULL ciphers + * !MD5:!3DES:!DES:!RC4:!IDEA:!SEED: no weak or broken algorithms on old OpenSSL versions. + * !aDSS: no authentication with discrete logarithm DSA algorithm + * !SRP:!PSK: no secure remote password or pre-shared key authentication + */ + #define PY_SSL_DEFAULT_CIPHER_STRING "DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK" +#elif PY_SSL_DEFAULT_CIPHERS == 2 +/* Ignored in SSLContext constructor, only used to as _ssl.DEFAULT_CIPHER_STRING */ + #define PY_SSL_DEFAULT_CIPHER_STRING SSL_DEFAULT_CIPHER_LIST +#else + #error "Unsupported PY_SSL_DEFAULT_CIPHERS" +#endif + enum py_ssl_error { /* these mirror ssl.h */ @@ -2873,7 +2898,12 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version) /* A bare minimum cipher list without completely broken cipher suites. * It's far from perfect but gives users a better head start. */ if (proto_version != PY_SSL_VERSION_SSL2) { - result = SSL_CTX_set_cipher_list(ctx, "HIGH:!aNULL:!eNULL:!MD5"); +#if PY_SSL_DEFAULT_CIPHERS == 2 + /* stick to OpenSSL's default settings */ + result = 1; +#else + result = SSL_CTX_set_cipher_list(ctx, PY_SSL_DEFAULT_CIPHER_STRING); +#endif } else { /* SSLv2 needs MD5 */ result = SSL_CTX_set_cipher_list(ctx, "HIGH:!aNULL:!eNULL"); @@ -5430,6 +5460,9 @@ PyInit__ssl(void) (PyObject *)&PySSLSession_Type) != 0) return NULL; + PyModule_AddStringConstant(m, "_DEFAULT_CIPHERS", + PY_SSL_DEFAULT_CIPHER_STRING); + PyModule_AddIntConstant(m, "SSL_ERROR_ZERO_RETURN", PY_SSL_ERROR_ZERO_RETURN); PyModule_AddIntConstant(m, "SSL_ERROR_WANT_READ", @@ -840,6 +840,7 @@ enable_big_digits with_computed_gotos with_ensurepip with_openssl +with_ssl_default_suites ' ac_precious_vars='build_alias host_alias @@ -1538,6 +1539,11 @@ Optional Packages: --with(out)-ensurepip=[=upgrade] "install" or "upgrade" using bundled pip --with-openssl=DIR root of the OpenSSL directory + --with-ssl-default-suites=[python|openssl|STRING] + Override default cipher suites string, python: use + Python's preferred selection (default), openssl: + leave OpenSSL's defaults untouched, STRING: use a + custom string, PROTOCOL_SSLv2 ignores the setting Some influential environment variables: MACHDEP name for machine-dependent library files @@ -16931,6 +16937,48 @@ $as_echo "#define HAVE_X509_VERIFY_PARAM_SET1_HOST 1" >>confdefs.h LIBS="$save_LIBS" fi +# ssl module default cipher suite string + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for --with-ssl-default-suites" >&5 +$as_echo_n "checking for --with-ssl-default-suites... " >&6; } + +# Check whether --with-ssl-default-suites was given. +if test "${with_ssl_default_suites+set}" = set; then : + withval=$with_ssl_default_suites; +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $withval" >&5 +$as_echo "$withval" >&6; } +case "$withval" in + python) + $as_echo "#define PY_SSL_DEFAULT_CIPHERS 1" >>confdefs.h + + ;; + openssl) + $as_echo "#define PY_SSL_DEFAULT_CIPHERS 2" >>confdefs.h + + ;; + *) + $as_echo "#define PY_SSL_DEFAULT_CIPHERS 0" >>confdefs.h + + cat >>confdefs.h <<_ACEOF +#define PY_SSL_DEFAULT_CIPHER_STRING "$withval" +_ACEOF + + ;; +esac + +else + +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: python" >&5 +$as_echo "python" >&6; } +$as_echo "#define PY_SSL_DEFAULT_CIPHERS 1" >>confdefs.h + + +fi + + + # generate output files ac_config_files="$ac_config_files Makefile.pre Misc/python.pc Misc/python-config.sh" diff --git a/configure.ac b/configure.ac index 6524863..af10a6d 100644 --- a/configure.ac +++ b/configure.ac @@ -5497,6 +5497,43 @@ if test "$have_openssl" = yes; then LIBS="$save_LIBS" fi +# ssl module default cipher suite string +AH_TEMPLATE(PY_SSL_DEFAULT_CIPHERS, + [Default cipher suites list for ssl module. + 1: Python's preferred selection, 2: leave OpenSSL defaults untouched, 0: custom string]) +AH_TEMPLATE(PY_SSL_DEFAULT_CIPHER_STRING, + [Cipher suite string for PY_SSL_DEFAULT_CIPHERS=0] +) + +AC_MSG_CHECKING(for --with-ssl-default-suites) +AC_ARG_WITH(ssl-default-suites, + AS_HELP_STRING([--with-ssl-default-suites=@<:@python|openssl|STRING@:>@], + [Override default cipher suites string, + python: use Python's preferred selection (default), + openssl: leave OpenSSL's defaults untouched, + STRING: use a custom string, + PROTOCOL_SSLv2 ignores the setting]), +[ +AC_MSG_RESULT($withval) +case "$withval" in + python) + AC_DEFINE(PY_SSL_DEFAULT_CIPHERS, 1) + ;; + openssl) + AC_DEFINE(PY_SSL_DEFAULT_CIPHERS, 2) + ;; + *) + AC_DEFINE(PY_SSL_DEFAULT_CIPHERS, 0) + AC_DEFINE_UNQUOTED(PY_SSL_DEFAULT_CIPHER_STRING, "$withval") + ;; +esac +], +[ +AC_MSG_RESULT(python) +AC_DEFINE(PY_SSL_DEFAULT_CIPHERS, 1) +]) + + # generate output files AC_CONFIG_FILES(Makefile.pre Misc/python.pc Misc/python-config.sh) AC_CONFIG_FILES([Modules/ld_so_aix], [chmod +x Modules/ld_so_aix]) diff --git a/pyconfig.h.in b/pyconfig.h.in index a18e3ca..a0efff9 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -1314,6 +1314,13 @@ /* Define to printf format modifier for Py_ssize_t */ #undef PY_FORMAT_SIZE_T +/* Default cipher suites list for ssl module. 1: Python's preferred selection, + 2: leave OpenSSL defaults untouched, 0: custom string */ +#undef PY_SSL_DEFAULT_CIPHERS + +/* Cipher suite string for PY_SSL_DEFAULT_CIPHERS=0 */ +#undef PY_SSL_DEFAULT_CIPHER_STRING + /* Define to emit a locale compatibility warning in the C locale */ #undef PY_WARN_ON_C_LOCALE |