Skip to content

Commit

Permalink
[2.7] bpo-28043: improved default settings for SSLContext (GH-10608)
Browse files Browse the repository at this point in the history
The options OP_NO_COMPRESSION, OP_CIPHER_SERVER_PREFERENCE,
OP_SINGLE_DH_USE, OP_SINGLE_ECDH_USE, OP_NO_SSLv2 (except
for PROTOCOL_SSLv2), and OP_NO_SSLv3 (except for PROTOCOL_SSLv3)
are set by default. The initial cipher suite list contains only
HIGH ciphers, no NULL ciphers and MD5 ciphers (except for PROTOCOL_SSLv2).

(cherry picked from commit 358cfd4)
  • Loading branch information
stratakis authored and vstinner committed Feb 15, 2019
1 parent c49f63c commit b8eaec6
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 54 deletions.
11 changes: 11 additions & 0 deletions Doc/library/ssl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,17 @@ to speed up repeated connections from the same clients.
:func:`create_default_context` lets the :mod:`ssl` module choose
security settings for a given purpose.

.. versionchanged:: 2.7.16

The context is created with secure default values. The options
:data:`OP_NO_COMPRESSION`, :data:`OP_CIPHER_SERVER_PREFERENCE`,
:data:`OP_SINGLE_DH_USE`, :data:`OP_SINGLE_ECDH_USE`,
:data:`OP_NO_SSLv2` (except for :data:`PROTOCOL_SSLv2`),
and :data:`OP_NO_SSLv3` (except for :data:`PROTOCOL_SSLv3`) are
set by default. The initial cipher suite list contains only ``HIGH``
ciphers, no ``NULL`` ciphers and no ``MD5`` ciphers (except for
:data:`PROTOCOL_SSLv2`).


:class:`SSLContext` objects have the following methods and attributes:

Expand Down
30 changes: 6 additions & 24 deletions Lib/ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,32 +424,16 @@ def create_default_context(purpose=Purpose.SERVER_AUTH, cafile=None,
if not isinstance(purpose, _ASN1Object):
raise TypeError(purpose)

# SSLContext sets OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION,
# OP_CIPHER_SERVER_PREFERENCE, OP_SINGLE_DH_USE and OP_SINGLE_ECDH_USE
# by default.
context = SSLContext(PROTOCOL_TLS)

# SSLv2 considered harmful.
context.options |= OP_NO_SSLv2

# SSLv3 has problematic security and is only required for really old
# clients such as IE6 on Windows XP
context.options |= OP_NO_SSLv3

# disable compression to prevent CRIME attacks (OpenSSL 1.0+)
context.options |= getattr(_ssl, "OP_NO_COMPRESSION", 0)

if purpose == Purpose.SERVER_AUTH:
# verify certs and host name in client mode
context.verify_mode = CERT_REQUIRED
context.check_hostname = True
elif purpose == Purpose.CLIENT_AUTH:
# Prefer the server's ciphers by default so that we get stronger
# encryption
context.options |= getattr(_ssl, "OP_CIPHER_SERVER_PREFERENCE", 0)

# Use single use keys in order to improve forward secrecy
context.options |= getattr(_ssl, "OP_SINGLE_DH_USE", 0)
context.options |= getattr(_ssl, "OP_SINGLE_ECDH_USE", 0)

# disallow ciphers with known vulnerabilities
context.set_ciphers(_RESTRICTED_SERVER_CIPHERS)

if cafile or capath or cadata:
Expand All @@ -475,12 +459,10 @@ def _create_unverified_context(protocol=PROTOCOL_TLS, cert_reqs=None,
if not isinstance(purpose, _ASN1Object):
raise TypeError(purpose)

# SSLContext sets OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION,
# OP_CIPHER_SERVER_PREFERENCE, OP_SINGLE_DH_USE and OP_SINGLE_ECDH_USE
# by default.
context = SSLContext(protocol)
# SSLv2 considered harmful.
context.options |= OP_NO_SSLv2
# SSLv3 has problematic security and is only required for really old
# clients such as IE6 on Windows XP
context.options |= OP_NO_SSLv3

if cert_reqs is not None:
context.verify_mode = cert_reqs
Expand Down
65 changes: 35 additions & 30 deletions Lib/test/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ def data_file(*name):
DHFILE = data_file("ffdh3072.pem")
BYTES_DHFILE = DHFILE.encode(sys.getfilesystemencoding())

# Not defined in all versions of OpenSSL
OP_NO_COMPRESSION = getattr(ssl, "OP_NO_COMPRESSION", 0)
OP_SINGLE_DH_USE = getattr(ssl, "OP_SINGLE_DH_USE", 0)
OP_SINGLE_ECDH_USE = getattr(ssl, "OP_SINGLE_ECDH_USE", 0)
OP_CIPHER_SERVER_PREFERENCE = getattr(ssl, "OP_CIPHER_SERVER_PREFERENCE", 0)


def handle_error(prefix):
exc_format = ' '.join(traceback.format_exception(*sys.exc_info()))
Expand Down Expand Up @@ -798,8 +804,9 @@ def test_options(self):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
# OP_ALL | OP_NO_SSLv2 | OP_NO_SSLv3 is the default value
default = (ssl.OP_ALL | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3)
if not IS_LIBRESSL and ssl.OPENSSL_VERSION_INFO >= (1, 1, 0):
default |= ssl.OP_NO_COMPRESSION
# SSLContext also enables these by default
default |= (OP_NO_COMPRESSION | OP_CIPHER_SERVER_PREFERENCE |
OP_SINGLE_DH_USE | OP_SINGLE_ECDH_USE)
self.assertEqual(default, ctx.options)
ctx.options |= ssl.OP_NO_TLSv1
self.assertEqual(default | ssl.OP_NO_TLSv1, ctx.options)
Expand Down Expand Up @@ -1178,70 +1185,67 @@ def test_load_default_certs_env_windows(self):
stats["x509"] += 1
self.assertEqual(ctx.cert_store_stats(), stats)

def _assert_context_options(self, ctx):
self.assertEqual(ctx.options & ssl.OP_NO_SSLv2, ssl.OP_NO_SSLv2)
if OP_NO_COMPRESSION != 0:
self.assertEqual(ctx.options & OP_NO_COMPRESSION,
OP_NO_COMPRESSION)
if OP_SINGLE_DH_USE != 0:
self.assertEqual(ctx.options & OP_SINGLE_DH_USE,
OP_SINGLE_DH_USE)
if OP_SINGLE_ECDH_USE != 0:
self.assertEqual(ctx.options & OP_SINGLE_ECDH_USE,
OP_SINGLE_ECDH_USE)
if OP_CIPHER_SERVER_PREFERENCE != 0:
self.assertEqual(ctx.options & OP_CIPHER_SERVER_PREFERENCE,
OP_CIPHER_SERVER_PREFERENCE)

def test_create_default_context(self):
ctx = ssl.create_default_context()

self.assertEqual(ctx.protocol, ssl.PROTOCOL_SSLv23)
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
self.assertTrue(ctx.check_hostname)
self.assertEqual(ctx.options & ssl.OP_NO_SSLv2, ssl.OP_NO_SSLv2)
self.assertEqual(
ctx.options & getattr(ssl, "OP_NO_COMPRESSION", 0),
getattr(ssl, "OP_NO_COMPRESSION", 0),
)
self._assert_context_options(ctx)


with open(SIGNING_CA) as f:
cadata = f.read().decode("ascii")
ctx = ssl.create_default_context(cafile=SIGNING_CA, capath=CAPATH,
cadata=cadata)
self.assertEqual(ctx.protocol, ssl.PROTOCOL_SSLv23)
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
self.assertEqual(ctx.options & ssl.OP_NO_SSLv2, ssl.OP_NO_SSLv2)
self.assertEqual(
ctx.options & getattr(ssl, "OP_NO_COMPRESSION", 0),
getattr(ssl, "OP_NO_COMPRESSION", 0),
)
self._assert_context_options(ctx)

ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
self.assertEqual(ctx.protocol, ssl.PROTOCOL_SSLv23)
self.assertEqual(ctx.verify_mode, ssl.CERT_NONE)
self.assertEqual(ctx.options & ssl.OP_NO_SSLv2, ssl.OP_NO_SSLv2)
self.assertEqual(
ctx.options & getattr(ssl, "OP_NO_COMPRESSION", 0),
getattr(ssl, "OP_NO_COMPRESSION", 0),
)
self.assertEqual(
ctx.options & getattr(ssl, "OP_SINGLE_DH_USE", 0),
getattr(ssl, "OP_SINGLE_DH_USE", 0),
)
self.assertEqual(
ctx.options & getattr(ssl, "OP_SINGLE_ECDH_USE", 0),
getattr(ssl, "OP_SINGLE_ECDH_USE", 0),
)
self._assert_context_options(ctx)

def test__create_stdlib_context(self):
ctx = ssl._create_stdlib_context()
self.assertEqual(ctx.protocol, ssl.PROTOCOL_SSLv23)
self.assertEqual(ctx.verify_mode, ssl.CERT_NONE)
self.assertFalse(ctx.check_hostname)
self.assertEqual(ctx.options & ssl.OP_NO_SSLv2, ssl.OP_NO_SSLv2)
self._assert_context_options(ctx)

ctx = ssl._create_stdlib_context(ssl.PROTOCOL_TLSv1)
self.assertEqual(ctx.protocol, ssl.PROTOCOL_TLSv1)
self.assertEqual(ctx.verify_mode, ssl.CERT_NONE)
self.assertEqual(ctx.options & ssl.OP_NO_SSLv2, ssl.OP_NO_SSLv2)
self._assert_context_options(ctx)

ctx = ssl._create_stdlib_context(ssl.PROTOCOL_TLSv1,
cert_reqs=ssl.CERT_REQUIRED,
check_hostname=True)
self.assertEqual(ctx.protocol, ssl.PROTOCOL_TLSv1)
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
self.assertTrue(ctx.check_hostname)
self.assertEqual(ctx.options & ssl.OP_NO_SSLv2, ssl.OP_NO_SSLv2)
self._assert_context_options(ctx)

ctx = ssl._create_stdlib_context(purpose=ssl.Purpose.CLIENT_AUTH)
self.assertEqual(ctx.protocol, ssl.PROTOCOL_SSLv23)
self.assertEqual(ctx.verify_mode, ssl.CERT_NONE)
self.assertEqual(ctx.options & ssl.OP_NO_SSLv2, ssl.OP_NO_SSLv2)
self._assert_context_options(ctx)

def test__https_verify_certificates(self):
# Unit test to check the contect factory mapping
Expand Down Expand Up @@ -2841,7 +2845,8 @@ def test_tls1_3(self):
ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2
)
with ThreadedEchoServer(context=context) as server:
with context.wrap_socket(socket.socket()) as s:
s = context.wrap_socket(socket.socket())
with closing(s):
s.connect((HOST, server.port))
self.assertIn(s.cipher()[0], [
'TLS_AES_256_GCM_SHA384',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
SSLContext has improved default settings: OP_NO_SSLv2, OP_NO_SSLv3,
OP_NO_COMPRESSION, OP_CIPHER_SERVER_PREFERENCE, OP_SINGLE_DH_USE,
OP_SINGLE_ECDH_USE and HIGH ciphers without MD5.
31 changes: 31 additions & 0 deletions Modules/_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -2181,6 +2181,7 @@ context_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
int proto_version = PY_SSL_VERSION_TLS;
long options;
SSL_CTX *ctx = NULL;
int result;

if (!PyArg_ParseTupleAndKeywords(
args, kwds, "i:_SSLContext", kwlist,
Expand Down Expand Up @@ -2245,8 +2246,38 @@ context_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
options |= SSL_OP_NO_SSLv2;
if (proto_version != PY_SSL_VERSION_SSL3)
options |= SSL_OP_NO_SSLv3;
/* Minimal security flags for server and client side context.
* Client sockets ignore server-side parameters. */
#ifdef SSL_OP_NO_COMPRESSION
options |= SSL_OP_NO_COMPRESSION;
#endif
#ifdef SSL_OP_CIPHER_SERVER_PREFERENCE
options |= SSL_OP_CIPHER_SERVER_PREFERENCE;
#endif
#ifdef SSL_OP_SINGLE_DH_USE
options |= SSL_OP_SINGLE_DH_USE;
#endif
#ifdef SSL_OP_SINGLE_ECDH_USE
options |= SSL_OP_SINGLE_ECDH_USE;
#endif
SSL_CTX_set_options(self->ctx, options);

/* A bare minimum cipher list without completly 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");
} else {
/* SSLv2 needs MD5 */
result = SSL_CTX_set_cipher_list(ctx, "HIGH:!aNULL:!eNULL");
}
if (result == 0) {
Py_DECREF(self);
ERR_clear_error();
PyErr_SetString(PySSLErrorObject,
"No cipher can be selected.");
return NULL;
}

#if !defined(OPENSSL_NO_ECDH) && !defined(OPENSSL_VERSION_1_1)
/* Allow automatic ECDH curve selection (on OpenSSL 1.0.2+), or use
prime256v1 by default. This is Apache mod_ssl's initialization
Expand Down

0 comments on commit b8eaec6

Please sign in to comment.