Skip to content

bpo-31429: Define TLS cipher suite on build time #3532

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Doc/whatsnew/3.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------

Expand Down
48 changes: 2 additions & 46 deletions Lib/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@


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


Expand Down Expand Up @@ -174,48 +175,7 @@
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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 15 additions & 1 deletion Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import weakref
import platform
import functools
import sysconfig
try:
import ctypes
except ImportError:
Expand All @@ -30,7 +31,7 @@
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)
Expand Down Expand Up @@ -936,6 +937,19 @@ def test_ciphers(self):
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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
35 changes: 34 additions & 1 deletion Modules/_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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",
Expand Down
48 changes: 48 additions & 0 deletions configure
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"

Expand Down
37 changes: 37 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
7 changes: 7 additions & 0 deletions pyconfig.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,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

Expand Down