summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/whatsnew/3.7.rst6
-rw-r--r--Lib/ssl.py48
-rw-r--r--Lib/test/test_ssl.py16
-rw-r--r--Misc/NEWS.d/next/Library/2018-01-28-22-40-05.bpo-31429.qNt8rQ.rst4
-rw-r--r--Modules/_ssl.c35
-rwxr-xr-xconfigure48
-rw-r--r--configure.ac37
-rw-r--r--pyconfig.h.in7
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
------
diff --git a/Lib/ssl.py b/Lib/ssl.py
index 5f972e1..b6161d0 100644
--- a/Lib/ssl.py
+++ b/Lib/ssl.py
@@ -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",
diff --git a/configure b/configure
index f94d16b..caa8667 100755
--- a/configure
+++ b/configure
@@ -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