bpo-31429: Define TLS cipher suite on build time (#3532)
Until now Python used a hard coded white list of default TLS cipher suites. The old approach has multiple downsides. OpenSSL's default selection was completely overruled. Python did neither benefit from new cipher suites (ChaCha20, TLS 1.3 suites) nor blacklisted cipher suites. For example we used to re-enable 3DES. Python now defaults to OpenSSL DEFAULT cipher suite selection and black lists all unwanted ciphers. Downstream vendors can override the default cipher list with --with-ssl-default-suites. Signed-off-by: Christian Heimes <christian@python.org>
This commit is contained in:
parent
d951157268
commit
892d66e422
|
@ -623,6 +623,12 @@ wildcard matching disabled by default.
|
||||||
(Contributed by Mandeep Singh in :issue:`23033` and Christian Heimes in
|
(Contributed by Mandeep Singh in :issue:`23033` and Christian Heimes in
|
||||||
:issue:`31399`.)
|
: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
|
string
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|
48
Lib/ssl.py
48
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 HAS_SNI, HAS_ECDH, HAS_NPN, HAS_ALPN, HAS_TLSv1_3
|
||||||
|
from _ssl import _DEFAULT_CIPHERS
|
||||||
from _ssl import _OPENSSL_API_VERSION
|
from _ssl import _OPENSSL_API_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@ -174,48 +175,7 @@ else:
|
||||||
HAS_NEVER_CHECK_COMMON_NAME = hasattr(_ssl, 'HOSTFLAG_NEVER_CHECK_SUBJECT')
|
HAS_NEVER_CHECK_COMMON_NAME = hasattr(_ssl, 'HOSTFLAG_NEVER_CHECK_SUBJECT')
|
||||||
|
|
||||||
|
|
||||||
# Disable weak or insecure ciphers by default
|
_RESTRICTED_SERVER_CIPHERS = _DEFAULT_CIPHERS
|
||||||
# (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'
|
|
||||||
)
|
|
||||||
|
|
||||||
CertificateError = SSLCertVerificationError
|
CertificateError = SSLCertVerificationError
|
||||||
|
|
||||||
|
@ -393,8 +353,6 @@ class SSLContext(_SSLContext):
|
||||||
|
|
||||||
def __new__(cls, protocol=PROTOCOL_TLS, *args, **kwargs):
|
def __new__(cls, protocol=PROTOCOL_TLS, *args, **kwargs):
|
||||||
self = _SSLContext.__new__(cls, protocol)
|
self = _SSLContext.__new__(cls, protocol)
|
||||||
if protocol != _SSLv2_IF_EXISTS:
|
|
||||||
self.set_ciphers(_DEFAULT_CIPHERS)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __init__(self, protocol=PROTOCOL_TLS):
|
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
|
# verify certs and host name in client mode
|
||||||
context.verify_mode = CERT_REQUIRED
|
context.verify_mode = CERT_REQUIRED
|
||||||
context.check_hostname = True
|
context.check_hostname = True
|
||||||
elif purpose == Purpose.CLIENT_AUTH:
|
|
||||||
context.set_ciphers(_RESTRICTED_SERVER_CIPHERS)
|
|
||||||
|
|
||||||
if cafile or capath or cadata:
|
if cafile or capath or cadata:
|
||||||
context.load_verify_locations(cafile, capath, cadata)
|
context.load_verify_locations(cafile, capath, cadata)
|
||||||
|
|
|
@ -18,6 +18,7 @@ import asyncore
|
||||||
import weakref
|
import weakref
|
||||||
import platform
|
import platform
|
||||||
import functools
|
import functools
|
||||||
|
import sysconfig
|
||||||
try:
|
try:
|
||||||
import ctypes
|
import ctypes
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
@ -30,7 +31,7 @@ PROTOCOLS = sorted(ssl._PROTOCOL_NAMES)
|
||||||
HOST = support.HOST
|
HOST = support.HOST
|
||||||
IS_LIBRESSL = ssl.OPENSSL_VERSION.startswith('LibreSSL')
|
IS_LIBRESSL = ssl.OPENSSL_VERSION.startswith('LibreSSL')
|
||||||
IS_OPENSSL_1_1 = not IS_LIBRESSL and ssl.OPENSSL_VERSION_INFO >= (1, 1, 0)
|
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):
|
def data_file(*name):
|
||||||
return os.path.join(os.path.dirname(__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"):
|
with self.assertRaisesRegex(ssl.SSLError, "No cipher can be selected"):
|
||||||
ctx.set_ciphers("^$:,;?*'dorothyx")
|
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')
|
@unittest.skipIf(ssl.OPENSSL_VERSION_INFO < (1, 0, 2, 0, 0), 'OpenSSL too old')
|
||||||
def test_get_ciphers(self):
|
def test_get_ciphers(self):
|
||||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||||
|
|
|
@ -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.
|
|
@ -234,6 +234,31 @@ SSL_SESSION_get_ticket_lifetime_hint(const SSL_SESSION *s)
|
||||||
|
|
||||||
#endif /* OpenSSL < 1.1.0 or LibreSSL */
|
#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 {
|
enum py_ssl_error {
|
||||||
/* these mirror ssl.h */
|
/* 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.
|
/* A bare minimum cipher list without completely broken cipher suites.
|
||||||
* It's far from perfect but gives users a better head start. */
|
* It's far from perfect but gives users a better head start. */
|
||||||
if (proto_version != PY_SSL_VERSION_SSL2) {
|
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 {
|
} else {
|
||||||
/* SSLv2 needs MD5 */
|
/* SSLv2 needs MD5 */
|
||||||
result = SSL_CTX_set_cipher_list(ctx, "HIGH:!aNULL:!eNULL");
|
result = SSL_CTX_set_cipher_list(ctx, "HIGH:!aNULL:!eNULL");
|
||||||
|
@ -5430,6 +5460,9 @@ PyInit__ssl(void)
|
||||||
(PyObject *)&PySSLSession_Type) != 0)
|
(PyObject *)&PySSLSession_Type) != 0)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
|
PyModule_AddStringConstant(m, "_DEFAULT_CIPHERS",
|
||||||
|
PY_SSL_DEFAULT_CIPHER_STRING);
|
||||||
|
|
||||||
PyModule_AddIntConstant(m, "SSL_ERROR_ZERO_RETURN",
|
PyModule_AddIntConstant(m, "SSL_ERROR_ZERO_RETURN",
|
||||||
PY_SSL_ERROR_ZERO_RETURN);
|
PY_SSL_ERROR_ZERO_RETURN);
|
||||||
PyModule_AddIntConstant(m, "SSL_ERROR_WANT_READ",
|
PyModule_AddIntConstant(m, "SSL_ERROR_WANT_READ",
|
||||||
|
|
|
@ -840,6 +840,7 @@ enable_big_digits
|
||||||
with_computed_gotos
|
with_computed_gotos
|
||||||
with_ensurepip
|
with_ensurepip
|
||||||
with_openssl
|
with_openssl
|
||||||
|
with_ssl_default_suites
|
||||||
'
|
'
|
||||||
ac_precious_vars='build_alias
|
ac_precious_vars='build_alias
|
||||||
host_alias
|
host_alias
|
||||||
|
@ -1538,6 +1539,11 @@ Optional Packages:
|
||||||
--with(out)-ensurepip=[=upgrade]
|
--with(out)-ensurepip=[=upgrade]
|
||||||
"install" or "upgrade" using bundled pip
|
"install" or "upgrade" using bundled pip
|
||||||
--with-openssl=DIR root of the OpenSSL directory
|
--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:
|
Some influential environment variables:
|
||||||
MACHDEP name for machine-dependent library files
|
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"
|
LIBS="$save_LIBS"
|
||||||
fi
|
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
|
# generate output files
|
||||||
ac_config_files="$ac_config_files Makefile.pre Misc/python.pc Misc/python-config.sh"
|
ac_config_files="$ac_config_files Makefile.pre Misc/python.pc Misc/python-config.sh"
|
||||||
|
|
||||||
|
|
37
configure.ac
37
configure.ac
|
@ -5497,6 +5497,43 @@ if test "$have_openssl" = yes; then
|
||||||
LIBS="$save_LIBS"
|
LIBS="$save_LIBS"
|
||||||
fi
|
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
|
# generate output files
|
||||||
AC_CONFIG_FILES(Makefile.pre Misc/python.pc Misc/python-config.sh)
|
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])
|
AC_CONFIG_FILES([Modules/ld_so_aix], [chmod +x Modules/ld_so_aix])
|
||||||
|
|
|
@ -1314,6 +1314,13 @@
|
||||||
/* Define to printf format modifier for Py_ssize_t */
|
/* Define to printf format modifier for Py_ssize_t */
|
||||||
#undef PY_FORMAT_SIZE_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 */
|
/* Define to emit a locale compatibility warning in the C locale */
|
||||||
#undef PY_WARN_ON_C_LOCALE
|
#undef PY_WARN_ON_C_LOCALE
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue