Skip to content

Commit 892d66e

Browse files
authored
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 <[email protected]>
1 parent d951157 commit 892d66e

File tree

8 files changed

+153
-48
lines changed

8 files changed

+153
-48
lines changed

Doc/whatsnew/3.7.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,12 @@ wildcard matching disabled by default.
623623
(Contributed by Mandeep Singh in :issue:`23033` and Christian Heimes in
624624
:issue:`31399`.)
625625

626+
The default cipher suite selection of the ssl module now uses a blacklist
627+
approach rather than a hard-coded whitelist. Python no longer re-enables
628+
ciphers that have been blocked by OpenSSL security update. Default cipher
629+
suite selection can be configured on compile time.
630+
(Contributed by Christian Heimes in :issue:`31429`.)
631+
626632
string
627633
------
628634

Lib/ssl.py

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115

116116

117117
from _ssl import HAS_SNI, HAS_ECDH, HAS_NPN, HAS_ALPN, HAS_TLSv1_3
118+
from _ssl import _DEFAULT_CIPHERS
118119
from _ssl import _OPENSSL_API_VERSION
119120

120121

@@ -174,48 +175,7 @@
174175
HAS_NEVER_CHECK_COMMON_NAME = hasattr(_ssl, 'HOSTFLAG_NEVER_CHECK_SUBJECT')
175176

176177

177-
# Disable weak or insecure ciphers by default
178-
# (OpenSSL's default setting is 'DEFAULT:!aNULL:!eNULL')
179-
# Enable a better set of ciphers by default
180-
# This list has been explicitly chosen to:
181-
# * TLS 1.3 ChaCha20 and AES-GCM cipher suites
182-
# * Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE)
183-
# * Prefer ECDHE over DHE for better performance
184-
# * Prefer AEAD over CBC for better performance and security
185-
# * Prefer AES-GCM over ChaCha20 because most platforms have AES-NI
186-
# (ChaCha20 needs OpenSSL 1.1.0 or patched 1.0.2)
187-
# * Prefer any AES-GCM and ChaCha20 over any AES-CBC for better
188-
# performance and security
189-
# * Then Use HIGH cipher suites as a fallback
190-
# * Disable NULL authentication, NULL encryption, 3DES and MD5 MACs
191-
# for security reasons
192-
_DEFAULT_CIPHERS = (
193-
'TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:'
194-
'TLS13-AES-128-GCM-SHA256:'
195-
'ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+CHACHA20:ECDH+AES256:DH+AES256:'
196-
'ECDH+AES128:DH+AES:ECDH+HIGH:DH+HIGH:RSA+AESGCM:RSA+AES:RSA+HIGH:'
197-
'!aNULL:!eNULL:!MD5:!3DES'
198-
)
199-
200-
# Restricted and more secure ciphers for the server side
201-
# This list has been explicitly chosen to:
202-
# * TLS 1.3 ChaCha20 and AES-GCM cipher suites
203-
# * Prefer cipher suites that offer perfect forward secrecy (DHE/ECDHE)
204-
# * Prefer ECDHE over DHE for better performance
205-
# * Prefer AEAD over CBC for better performance and security
206-
# * Prefer AES-GCM over ChaCha20 because most platforms have AES-NI
207-
# * Prefer any AES-GCM and ChaCha20 over any AES-CBC for better
208-
# performance and security
209-
# * Then Use HIGH cipher suites as a fallback
210-
# * Disable NULL authentication, NULL encryption, MD5 MACs, DSS, RC4, and
211-
# 3DES for security reasons
212-
_RESTRICTED_SERVER_CIPHERS = (
213-
'TLS13-AES-256-GCM-SHA384:TLS13-CHACHA20-POLY1305-SHA256:'
214-
'TLS13-AES-128-GCM-SHA256:'
215-
'ECDH+AESGCM:ECDH+CHACHA20:DH+AESGCM:DH+CHACHA20:ECDH+AES256:DH+AES256:'
216-
'ECDH+AES128:DH+AES:ECDH+HIGH:DH+HIGH:RSA+AESGCM:RSA+AES:RSA+HIGH:'
217-
'!aNULL:!eNULL:!MD5:!DSS:!RC4:!3DES'
218-
)
178+
_RESTRICTED_SERVER_CIPHERS = _DEFAULT_CIPHERS
219179

220180
CertificateError = SSLCertVerificationError
221181

@@ -393,8 +353,6 @@ class SSLContext(_SSLContext):
393353

394354
def __new__(cls, protocol=PROTOCOL_TLS, *args, **kwargs):
395355
self = _SSLContext.__new__(cls, protocol)
396-
if protocol != _SSLv2_IF_EXISTS:
397-
self.set_ciphers(_DEFAULT_CIPHERS)
398356
return self
399357

400358
def __init__(self, protocol=PROTOCOL_TLS):
@@ -530,8 +488,6 @@ def create_default_context(purpose=Purpose.SERVER_AUTH, *, cafile=None,
530488
# verify certs and host name in client mode
531489
context.verify_mode = CERT_REQUIRED
532490
context.check_hostname = True
533-
elif purpose == Purpose.CLIENT_AUTH:
534-
context.set_ciphers(_RESTRICTED_SERVER_CIPHERS)
535491

536492
if cafile or capath or cadata:
537493
context.load_verify_locations(cafile, capath, cadata)

Lib/test/test_ssl.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import weakref
1919
import platform
2020
import functools
21+
import sysconfig
2122
try:
2223
import ctypes
2324
except ImportError:
@@ -30,7 +31,7 @@
3031
HOST = support.HOST
3132
IS_LIBRESSL = ssl.OPENSSL_VERSION.startswith('LibreSSL')
3233
IS_OPENSSL_1_1 = not IS_LIBRESSL and ssl.OPENSSL_VERSION_INFO >= (1, 1, 0)
33-
34+
PY_SSL_DEFAULT_CIPHERS = sysconfig.get_config_var('PY_SSL_DEFAULT_CIPHERS')
3435

3536
def data_file(*name):
3637
return os.path.join(os.path.dirname(__file__), *name)
@@ -936,6 +937,19 @@ def test_ciphers(self):
936937
with self.assertRaisesRegex(ssl.SSLError, "No cipher can be selected"):
937938
ctx.set_ciphers("^$:,;?*'dorothyx")
938939

940+
@unittest.skipUnless(PY_SSL_DEFAULT_CIPHERS == 1,
941+
"Test applies only to Python default ciphers")
942+
def test_python_ciphers(self):
943+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
944+
ciphers = ctx.get_ciphers()
945+
for suite in ciphers:
946+
name = suite['name']
947+
self.assertNotIn("PSK", name)
948+
self.assertNotIn("SRP", name)
949+
self.assertNotIn("MD5", name)
950+
self.assertNotIn("RC4", name)
951+
self.assertNotIn("3DES", name)
952+
939953
@unittest.skipIf(ssl.OPENSSL_VERSION_INFO < (1, 0, 2, 0, 0), 'OpenSSL too old')
940954
def test_get_ciphers(self):
941955
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The default cipher suite selection of the ssl module now uses a blacklist
2+
approach rather than a hard-coded whitelist. Python no longer re-enables
3+
ciphers that have been blocked by OpenSSL security update. Default cipher
4+
suite selection can be configured on compile time.

Modules/_ssl.c

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,31 @@ SSL_SESSION_get_ticket_lifetime_hint(const SSL_SESSION *s)
234234

235235
#endif /* OpenSSL < 1.1.0 or LibreSSL */
236236

237+
/* Default cipher suites */
238+
#ifndef PY_SSL_DEFAULT_CIPHERS
239+
#define PY_SSL_DEFAULT_CIPHERS 1
240+
#endif
241+
242+
#if PY_SSL_DEFAULT_CIPHERS == 0
243+
#ifndef PY_SSL_DEFAULT_CIPHER_STRING
244+
#error "Py_SSL_DEFAULT_CIPHERS 0 needs Py_SSL_DEFAULT_CIPHER_STRING"
245+
#endif
246+
#elif PY_SSL_DEFAULT_CIPHERS == 1
247+
/* Python custom selection of sensible ciper suites
248+
* DEFAULT: OpenSSL's default cipher list. Since 1.0.2 the list is in sensible order.
249+
* !aNULL:!eNULL: really no NULL ciphers
250+
* !MD5:!3DES:!DES:!RC4:!IDEA:!SEED: no weak or broken algorithms on old OpenSSL versions.
251+
* !aDSS: no authentication with discrete logarithm DSA algorithm
252+
* !SRP:!PSK: no secure remote password or pre-shared key authentication
253+
*/
254+
#define PY_SSL_DEFAULT_CIPHER_STRING "DEFAULT:!aNULL:!eNULL:!MD5:!3DES:!DES:!RC4:!IDEA:!SEED:!aDSS:!SRP:!PSK"
255+
#elif PY_SSL_DEFAULT_CIPHERS == 2
256+
/* Ignored in SSLContext constructor, only used to as _ssl.DEFAULT_CIPHER_STRING */
257+
#define PY_SSL_DEFAULT_CIPHER_STRING SSL_DEFAULT_CIPHER_LIST
258+
#else
259+
#error "Unsupported PY_SSL_DEFAULT_CIPHERS"
260+
#endif
261+
237262

238263
enum py_ssl_error {
239264
/* these mirror ssl.h */
@@ -2873,7 +2898,12 @@ _ssl__SSLContext_impl(PyTypeObject *type, int proto_version)
28732898
/* A bare minimum cipher list without completely broken cipher suites.
28742899
* It's far from perfect but gives users a better head start. */
28752900
if (proto_version != PY_SSL_VERSION_SSL2) {
2876-
result = SSL_CTX_set_cipher_list(ctx, "HIGH:!aNULL:!eNULL:!MD5");
2901+
#if PY_SSL_DEFAULT_CIPHERS == 2
2902+
/* stick to OpenSSL's default settings */
2903+
result = 1;
2904+
#else
2905+
result = SSL_CTX_set_cipher_list(ctx, PY_SSL_DEFAULT_CIPHER_STRING);
2906+
#endif
28772907
} else {
28782908
/* SSLv2 needs MD5 */
28792909
result = SSL_CTX_set_cipher_list(ctx, "HIGH:!aNULL:!eNULL");
@@ -5430,6 +5460,9 @@ PyInit__ssl(void)
54305460
(PyObject *)&PySSLSession_Type) != 0)
54315461
return NULL;
54325462

5463+
PyModule_AddStringConstant(m, "_DEFAULT_CIPHERS",
5464+
PY_SSL_DEFAULT_CIPHER_STRING);
5465+
54335466
PyModule_AddIntConstant(m, "SSL_ERROR_ZERO_RETURN",
54345467
PY_SSL_ERROR_ZERO_RETURN);
54355468
PyModule_AddIntConstant(m, "SSL_ERROR_WANT_READ",

configure

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,7 @@ enable_big_digits
840840
with_computed_gotos
841841
with_ensurepip
842842
with_openssl
843+
with_ssl_default_suites
843844
'
844845
ac_precious_vars='build_alias
845846
host_alias
@@ -1538,6 +1539,11 @@ Optional Packages:
15381539
--with(out)-ensurepip=[=upgrade]
15391540
"install" or "upgrade" using bundled pip
15401541
--with-openssl=DIR root of the OpenSSL directory
1542+
--with-ssl-default-suites=[python|openssl|STRING]
1543+
Override default cipher suites string, python: use
1544+
Python's preferred selection (default), openssl:
1545+
leave OpenSSL's defaults untouched, STRING: use a
1546+
custom string, PROTOCOL_SSLv2 ignores the setting
15411547
15421548
Some influential environment variables:
15431549
MACHDEP name for machine-dependent library files
@@ -16931,6 +16937,48 @@ $as_echo "#define HAVE_X509_VERIFY_PARAM_SET1_HOST 1" >>confdefs.h
1693116937
LIBS="$save_LIBS"
1693216938
fi
1693316939

16940+
# ssl module default cipher suite string
16941+
16942+
16943+
16944+
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for --with-ssl-default-suites" >&5
16945+
$as_echo_n "checking for --with-ssl-default-suites... " >&6; }
16946+
16947+
# Check whether --with-ssl-default-suites was given.
16948+
if test "${with_ssl_default_suites+set}" = set; then :
16949+
withval=$with_ssl_default_suites;
16950+
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $withval" >&5
16951+
$as_echo "$withval" >&6; }
16952+
case "$withval" in
16953+
python)
16954+
$as_echo "#define PY_SSL_DEFAULT_CIPHERS 1" >>confdefs.h
16955+
16956+
;;
16957+
openssl)
16958+
$as_echo "#define PY_SSL_DEFAULT_CIPHERS 2" >>confdefs.h
16959+
16960+
;;
16961+
*)
16962+
$as_echo "#define PY_SSL_DEFAULT_CIPHERS 0" >>confdefs.h
16963+
16964+
cat >>confdefs.h <<_ACEOF
16965+
#define PY_SSL_DEFAULT_CIPHER_STRING "$withval"
16966+
_ACEOF
16967+
16968+
;;
16969+
esac
16970+
16971+
else
16972+
16973+
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: python" >&5
16974+
$as_echo "python" >&6; }
16975+
$as_echo "#define PY_SSL_DEFAULT_CIPHERS 1" >>confdefs.h
16976+
16977+
16978+
fi
16979+
16980+
16981+
1693416982
# generate output files
1693516983
ac_config_files="$ac_config_files Makefile.pre Misc/python.pc Misc/python-config.sh"
1693616984

configure.ac

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5497,6 +5497,43 @@ if test "$have_openssl" = yes; then
54975497
LIBS="$save_LIBS"
54985498
fi
54995499

5500+
# ssl module default cipher suite string
5501+
AH_TEMPLATE(PY_SSL_DEFAULT_CIPHERS,
5502+
[Default cipher suites list for ssl module.
5503+
1: Python's preferred selection, 2: leave OpenSSL defaults untouched, 0: custom string])
5504+
AH_TEMPLATE(PY_SSL_DEFAULT_CIPHER_STRING,
5505+
[Cipher suite string for PY_SSL_DEFAULT_CIPHERS=0]
5506+
)
5507+
5508+
AC_MSG_CHECKING(for --with-ssl-default-suites)
5509+
AC_ARG_WITH(ssl-default-suites,
5510+
AS_HELP_STRING([--with-ssl-default-suites=@<:@python|openssl|STRING@:>@],
5511+
[Override default cipher suites string,
5512+
python: use Python's preferred selection (default),
5513+
openssl: leave OpenSSL's defaults untouched,
5514+
STRING: use a custom string,
5515+
PROTOCOL_SSLv2 ignores the setting]),
5516+
[
5517+
AC_MSG_RESULT($withval)
5518+
case "$withval" in
5519+
python)
5520+
AC_DEFINE(PY_SSL_DEFAULT_CIPHERS, 1)
5521+
;;
5522+
openssl)
5523+
AC_DEFINE(PY_SSL_DEFAULT_CIPHERS, 2)
5524+
;;
5525+
*)
5526+
AC_DEFINE(PY_SSL_DEFAULT_CIPHERS, 0)
5527+
AC_DEFINE_UNQUOTED(PY_SSL_DEFAULT_CIPHER_STRING, "$withval")
5528+
;;
5529+
esac
5530+
],
5531+
[
5532+
AC_MSG_RESULT(python)
5533+
AC_DEFINE(PY_SSL_DEFAULT_CIPHERS, 1)
5534+
])
5535+
5536+
55005537
# generate output files
55015538
AC_CONFIG_FILES(Makefile.pre Misc/python.pc Misc/python-config.sh)
55025539
AC_CONFIG_FILES([Modules/ld_so_aix], [chmod +x Modules/ld_so_aix])

pyconfig.h.in

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1314,6 +1314,13 @@
13141314
/* Define to printf format modifier for Py_ssize_t */
13151315
#undef PY_FORMAT_SIZE_T
13161316

1317+
/* Default cipher suites list for ssl module. 1: Python's preferred selection,
1318+
2: leave OpenSSL defaults untouched, 0: custom string */
1319+
#undef PY_SSL_DEFAULT_CIPHERS
1320+
1321+
/* Cipher suite string for PY_SSL_DEFAULT_CIPHERS=0 */
1322+
#undef PY_SSL_DEFAULT_CIPHER_STRING
1323+
13171324
/* Define to emit a locale compatibility warning in the C locale */
13181325
#undef PY_WARN_ON_C_LOCALE
13191326

0 commit comments

Comments
 (0)