Commit b5218771 authored by Antoine Pitrou's avatar Antoine Pitrou

Issue #4870: Add an `options` attribute to SSL contexts, as well as

several ``OP_*`` constants to the `ssl` module.  This allows to selectively
disable protocol versions, when used in combination with `PROTOCOL_SSLv23`.
parent 955d1b22
...@@ -257,6 +257,37 @@ Functions, Constants, and Exceptions ...@@ -257,6 +257,37 @@ Functions, Constants, and Exceptions
modern version, and probably the best choice for maximum protection, if both modern version, and probably the best choice for maximum protection, if both
sides can speak it. sides can speak it.
.. data:: OP_ALL
Enables workarounds for various bugs present in other SSL implementations.
This option is set by default.
.. versionadded:: 3.2
.. data:: OP_NO_SSLv2
Prevents an SSLv2 connection. This option is only applicable in
conjunction with :const:`PROTOCOL_SSLv23`. It prevents the peers from
choosing SSLv2 as the protocol version.
.. versionadded:: 3.2
.. data:: OP_NO_SSLv3
Prevents an SSLv3 connection. This option is only applicable in
conjunction with :const:`PROTOCOL_SSLv23`. It prevents the peers from
choosing SSLv3 as the protocol version.
.. versionadded:: 3.2
.. data:: OP_NO_TLSv1
Prevents a TLSv1 connection. This option is only applicable in
conjunction with :const:`PROTOCOL_SSLv23`. It prevents the peers from
choosing TLSv1 as the protocol version.
.. versionadded:: 3.2
.. data:: OPENSSL_VERSION .. data:: OPENSSL_VERSION
The version string of the OpenSSL library loaded by the interpreter:: The version string of the OpenSSL library loaded by the interpreter::
...@@ -440,6 +471,17 @@ SSL Contexts ...@@ -440,6 +471,17 @@ SSL Contexts
and *suppress_ragged_eofs* have the same meaning as in the top-level and *suppress_ragged_eofs* have the same meaning as in the top-level
:func:`wrap_socket` function. :func:`wrap_socket` function.
.. attribute:: SSLContext.options
An integer representing the set of SSL options enabled on this context.
The default value is :data:`OP_ALL`, but you can specify other options
such as :data:`OP_NO_SSLv2` by ORing them together.
.. note::
With versions of OpenSSL older than 0.9.8m, it is only possible
to set options, not to clear them. Attempting to clear an option
(by resetting the corresponding bits) will raise a ``ValueError``.
.. attribute:: SSLContext.protocol .. attribute:: SSLContext.protocol
The protocol version chosen when constructing the context. This attribute The protocol version chosen when constructing the context. This attribute
...@@ -794,6 +836,20 @@ to specify :const:`CERT_REQUIRED` and similarly check the client certificate. ...@@ -794,6 +836,20 @@ to specify :const:`CERT_REQUIRED` and similarly check the client certificate.
equivalent unless anonymous ciphers are enabled (they are disabled equivalent unless anonymous ciphers are enabled (they are disabled
by default). by default).
Protocol versions
^^^^^^^^^^^^^^^^^
SSL version 2 is considered insecure and is therefore dangerous to use. If
you want maximum compatibility between clients and servers, it is recommended
to use :const:`PROTOCOL_SSLv23` as the protocol version and then disable
SSLv2 explicitly using the :data:`SSLContext.options` attribute::
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.options |= ssl.OP_NO_SSLv2
The SSL context created above will allow SSLv3 and TLSv1 connections, but
not SSLv2.
.. seealso:: .. seealso::
......
...@@ -63,6 +63,7 @@ from _ssl import _SSLContext, SSLError ...@@ -63,6 +63,7 @@ from _ssl import _SSLContext, SSLError
from _ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED from _ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED
from _ssl import (PROTOCOL_SSLv2, PROTOCOL_SSLv3, PROTOCOL_SSLv23, from _ssl import (PROTOCOL_SSLv2, PROTOCOL_SSLv3, PROTOCOL_SSLv23,
PROTOCOL_TLSv1) PROTOCOL_TLSv1)
from _ssl import OP_ALL, OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_TLSv1
from _ssl import RAND_status, RAND_egd, RAND_add from _ssl import RAND_status, RAND_egd, RAND_add
from _ssl import ( from _ssl import (
SSL_ERROR_ZERO_RETURN, SSL_ERROR_ZERO_RETURN,
......
...@@ -57,6 +57,14 @@ def handle_error(prefix): ...@@ -57,6 +57,14 @@ def handle_error(prefix):
if support.verbose: if support.verbose:
sys.stdout.write(prefix + exc_format) sys.stdout.write(prefix + exc_format)
def can_clear_options():
# 0.9.8m or higher
return ssl.OPENSSL_VERSION_INFO >= (0, 9, 8, 13, 15)
def no_sslv2_implies_sslv3_hello():
# 0.9.7h or higher
return ssl.OPENSSL_VERSION_INFO >= (0, 9, 7, 8, 15)
class BasicSocketTests(unittest.TestCase): class BasicSocketTests(unittest.TestCase):
...@@ -189,6 +197,26 @@ class ContextTests(unittest.TestCase): ...@@ -189,6 +197,26 @@ class ContextTests(unittest.TestCase):
with self.assertRaisesRegexp(ssl.SSLError, "No cipher can be selected"): with self.assertRaisesRegexp(ssl.SSLError, "No cipher can be selected"):
ctx.set_ciphers("^$:,;?*'dorothyx") ctx.set_ciphers("^$:,;?*'dorothyx")
def test_options(self):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
# OP_ALL is the default value
self.assertEqual(ssl.OP_ALL, ctx.options)
ctx.options |= ssl.OP_NO_SSLv2
self.assertEqual(ssl.OP_ALL | ssl.OP_NO_SSLv2,
ctx.options)
ctx.options |= ssl.OP_NO_SSLv3
self.assertEqual(ssl.OP_ALL | ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3,
ctx.options)
if can_clear_options():
ctx.options = (ctx.options & ~ssl.OP_NO_SSLv2) | ssl.OP_NO_TLSv1
self.assertEqual(ssl.OP_ALL | ssl.OP_NO_TLSv1 | ssl.OP_NO_SSLv3,
ctx.options)
ctx.options = 0
self.assertEqual(0, ctx.options)
else:
with self.assertRaises(ValueError):
ctx.options = 0
def test_verify(self): def test_verify(self):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
# Default value # Default value
...@@ -445,12 +473,8 @@ else: ...@@ -445,12 +473,8 @@ else:
def wrap_conn(self): def wrap_conn(self):
try: try:
self.sslconn = ssl.wrap_socket(self.sock, server_side=True, self.sslconn = self.server.context.wrap_socket(
certfile=self.server.certificate, self.sock, server_side=True)
ssl_version=self.server.protocol,
ca_certs=self.server.cacerts,
cert_reqs=self.server.certreqs,
ciphers=self.server.ciphers)
except ssl.SSLError: except ssl.SSLError:
# XXX Various errors can have happened here, for example # XXX Various errors can have happened here, for example
# a mismatching protocol version, an invalid certificate, # a mismatching protocol version, an invalid certificate,
...@@ -462,7 +486,7 @@ else: ...@@ -462,7 +486,7 @@ else:
self.close() self.close()
return False return False
else: else:
if self.server.certreqs == ssl.CERT_REQUIRED: if self.server.context.verify_mode == ssl.CERT_REQUIRED:
cert = self.sslconn.getpeercert() cert = self.sslconn.getpeercert()
if support.verbose and self.server.chatty: if support.verbose and self.server.chatty:
sys.stdout.write(" client cert is " + pprint.pformat(cert) + "\n") sys.stdout.write(" client cert is " + pprint.pformat(cert) + "\n")
...@@ -542,19 +566,24 @@ else: ...@@ -542,19 +566,24 @@ else:
# harness, we want to stop the server # harness, we want to stop the server
self.server.stop() self.server.stop()
def __init__(self, certificate, ssl_version=None, def __init__(self, certificate=None, ssl_version=None,
certreqs=None, cacerts=None, certreqs=None, cacerts=None,
chatty=True, connectionchatty=False, starttls_server=False, chatty=True, connectionchatty=False, starttls_server=False,
ciphers=None): ciphers=None, context=None):
if ssl_version is None: if context:
ssl_version = ssl.PROTOCOL_TLSv1 self.context = context
if certreqs is None: else:
certreqs = ssl.CERT_NONE self.context = ssl.SSLContext(ssl_version
self.certificate = certificate if ssl_version is not None
self.protocol = ssl_version else ssl.PROTOCOL_TLSv1)
self.certreqs = certreqs self.context.verify_mode = (certreqs if certreqs is not None
self.cacerts = cacerts else ssl.CERT_NONE)
self.ciphers = ciphers if cacerts:
self.context.load_verify_locations(cacerts)
if certificate:
self.context.load_cert_chain(certificate)
if ciphers:
self.context.set_ciphers(ciphers)
self.chatty = chatty self.chatty = chatty
self.connectionchatty = connectionchatty self.connectionchatty = connectionchatty
self.starttls_server = starttls_server self.starttls_server = starttls_server
...@@ -820,18 +849,13 @@ else: ...@@ -820,18 +849,13 @@ else:
server.stop() server.stop()
server.join() server.join()
def server_params_test(certfile, protocol, certreqs, cacertsfile, def server_params_test(client_context, server_context, indata=b"FOO\n",
client_certfile, client_protocol=None, indata=b"FOO\n", chatty=True, connectionchatty=False):
ciphers=None, chatty=True, connectionchatty=False):
""" """
Launch a server, connect a client to it and try various reads Launch a server, connect a client to it and try various reads
and writes. and writes.
""" """
server = ThreadedEchoServer(certfile, server = ThreadedEchoServer(context=server_context,
certreqs=certreqs,
ssl_version=protocol,
cacerts=cacertsfile,
ciphers=ciphers,
chatty=chatty, chatty=chatty,
connectionchatty=False) connectionchatty=False)
flag = threading.Event() flag = threading.Event()
...@@ -839,15 +863,8 @@ else: ...@@ -839,15 +863,8 @@ else:
# wait for it to start # wait for it to start
flag.wait() flag.wait()
# try to connect # try to connect
if client_protocol is None:
client_protocol = protocol
try: try:
s = ssl.wrap_socket(socket.socket(), s = client_context.wrap_socket(socket.socket())
certfile=client_certfile,
ca_certs=cacertsfile,
ciphers=ciphers,
cert_reqs=certreqs,
ssl_version=client_protocol)
s.connect((HOST, server.port)) s.connect((HOST, server.port))
for arg in [indata, bytearray(indata), memoryview(indata)]: for arg in [indata, bytearray(indata), memoryview(indata)]:
if connectionchatty: if connectionchatty:
...@@ -873,10 +890,8 @@ else: ...@@ -873,10 +890,8 @@ else:
server.stop() server.stop()
server.join() server.join()
def try_protocol_combo(server_protocol, def try_protocol_combo(server_protocol, client_protocol, expect_success,
client_protocol, certsreqs=None, server_options=0, client_options=0):
expect_success,
certsreqs=None):
if certsreqs is None: if certsreqs is None:
certsreqs = ssl.CERT_NONE certsreqs = ssl.CERT_NONE
certtype = { certtype = {
...@@ -890,14 +905,21 @@ else: ...@@ -890,14 +905,21 @@ else:
(ssl.get_protocol_name(client_protocol), (ssl.get_protocol_name(client_protocol),
ssl.get_protocol_name(server_protocol), ssl.get_protocol_name(server_protocol),
certtype)) certtype))
try: client_context = ssl.SSLContext(client_protocol)
client_context.options = ssl.OP_ALL | client_options
server_context = ssl.SSLContext(server_protocol)
server_context.options = ssl.OP_ALL | server_options
for ctx in (client_context, server_context):
ctx.verify_mode = certsreqs
# NOTE: we must enable "ALL" ciphers, otherwise an SSLv23 client # NOTE: we must enable "ALL" ciphers, otherwise an SSLv23 client
# will send an SSLv3 hello (rather than SSLv2) starting from # will send an SSLv3 hello (rather than SSLv2) starting from
# OpenSSL 1.0.0 (see issue #8322). # OpenSSL 1.0.0 (see issue #8322).
server_params_test(CERTFILE, server_protocol, certsreqs, ctx.set_ciphers("ALL")
CERTFILE, CERTFILE, client_protocol, ctx.load_cert_chain(CERTFILE)
ciphers="ALL", chatty=False, ctx.load_verify_locations(CERTFILE)
connectionchatty=False) try:
server_params_test(client_context, server_context,
chatty=False, connectionchatty=False)
# Protocol mismatch can result in either an SSLError, or a # Protocol mismatch can result in either an SSLError, or a
# "Connection reset by peer" error. # "Connection reset by peer" error.
except ssl.SSLError: except ssl.SSLError:
...@@ -920,30 +942,27 @@ else: ...@@ -920,30 +942,27 @@ else:
"""Basic test of an SSL client connecting to a server""" """Basic test of an SSL client connecting to a server"""
if support.verbose: if support.verbose:
sys.stdout.write("\n") sys.stdout.write("\n")
server_params_test(CERTFILE, ssl.PROTOCOL_TLSv1, ssl.CERT_NONE, for protocol in PROTOCOLS:
CERTFILE, CERTFILE, ssl.PROTOCOL_TLSv1, context = ssl.SSLContext(protocol)
chatty=True, connectionchatty=True) context.load_cert_chain(CERTFILE)
server_params_test(context, context,
chatty=True, connectionchatty=True)
def test_getpeercert(self): def test_getpeercert(self):
if support.verbose: if support.verbose:
sys.stdout.write("\n") sys.stdout.write("\n")
s2 = socket.socket() context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
server = ThreadedEchoServer(CERTFILE, context.verify_mode = ssl.CERT_REQUIRED
certreqs=ssl.CERT_NONE, context.load_verify_locations(CERTFILE)
ssl_version=ssl.PROTOCOL_SSLv23, context.load_cert_chain(CERTFILE)
cacerts=CERTFILE, server = ThreadedEchoServer(context=context, chatty=False)
chatty=False)
flag = threading.Event() flag = threading.Event()
server.start(flag) server.start(flag)
# wait for it to start # wait for it to start
flag.wait() flag.wait()
# try to connect # try to connect
try: try:
s = ssl.wrap_socket(socket.socket(), s = context.wrap_socket(socket.socket())
certfile=CERTFILE,
ca_certs=CERTFILE,
cert_reqs=ssl.CERT_REQUIRED,
ssl_version=ssl.PROTOCOL_SSLv23)
s.connect((HOST, server.port)) s.connect((HOST, server.port))
cert = s.getpeercert() cert = s.getpeercert()
self.assertTrue(cert, "Can't get peer certificate.") self.assertTrue(cert, "Can't get peer certificate.")
...@@ -1031,6 +1050,15 @@ else: ...@@ -1031,6 +1050,15 @@ else:
try_protocol_combo(ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_SSLv23, True) try_protocol_combo(ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_SSLv23, True)
try_protocol_combo(ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_SSLv3, False) try_protocol_combo(ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_SSLv3, False)
try_protocol_combo(ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_TLSv1, False) try_protocol_combo(ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_TLSv1, False)
# SSLv23 client with specific SSL options
if no_sslv2_implies_sslv3_hello():
# No SSLv2 => client will use an SSLv3 hello on recent OpenSSLs
try_protocol_combo(ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_SSLv23, False,
client_options=ssl.OP_NO_SSLv2)
try_protocol_combo(ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_SSLv23, True,
client_options=ssl.OP_NO_SSLv3)
try_protocol_combo(ssl.PROTOCOL_SSLv2, ssl.PROTOCOL_SSLv23, True,
client_options=ssl.OP_NO_TLSv1)
def test_protocol_sslv23(self): def test_protocol_sslv23(self):
"""Connecting to an SSLv23 server with various client options""" """Connecting to an SSLv23 server with various client options"""
...@@ -1056,6 +1084,16 @@ else: ...@@ -1056,6 +1084,16 @@ else:
try_protocol_combo(ssl.PROTOCOL_SSLv23, ssl.PROTOCOL_SSLv23, True, ssl.CERT_REQUIRED) try_protocol_combo(ssl.PROTOCOL_SSLv23, ssl.PROTOCOL_SSLv23, True, ssl.CERT_REQUIRED)
try_protocol_combo(ssl.PROTOCOL_SSLv23, ssl.PROTOCOL_TLSv1, True, ssl.CERT_REQUIRED) try_protocol_combo(ssl.PROTOCOL_SSLv23, ssl.PROTOCOL_TLSv1, True, ssl.CERT_REQUIRED)
# Server with specific SSL options
try_protocol_combo(ssl.PROTOCOL_SSLv23, ssl.PROTOCOL_SSLv3, False,
server_options=ssl.OP_NO_SSLv3)
# Will choose TLSv1
try_protocol_combo(ssl.PROTOCOL_SSLv23, ssl.PROTOCOL_SSLv23, True,
server_options=ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3)
try_protocol_combo(ssl.PROTOCOL_SSLv23, ssl.PROTOCOL_TLSv1, False,
server_options=ssl.OP_NO_TLSv1)
def test_protocol_sslv3(self): def test_protocol_sslv3(self):
"""Connecting to an SSLv3 server with various client options""" """Connecting to an SSLv3 server with various client options"""
if support.verbose: if support.verbose:
...@@ -1066,6 +1104,10 @@ else: ...@@ -1066,6 +1104,10 @@ else:
try_protocol_combo(ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_SSLv2, False) try_protocol_combo(ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_SSLv2, False)
try_protocol_combo(ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_SSLv23, False) try_protocol_combo(ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_SSLv23, False)
try_protocol_combo(ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_TLSv1, False) try_protocol_combo(ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_TLSv1, False)
if no_sslv2_implies_sslv3_hello():
# No SSLv2 => client will use an SSLv3 hello on recent OpenSSLs
try_protocol_combo(ssl.PROTOCOL_SSLv3, ssl.PROTOCOL_SSLv23, True,
client_options=ssl.OP_NO_SSLv2)
def test_protocol_tlsv1(self): def test_protocol_tlsv1(self):
"""Connecting to a TLSv1 server with various client options""" """Connecting to a TLSv1 server with various client options"""
......
...@@ -375,6 +375,10 @@ C-API ...@@ -375,6 +375,10 @@ C-API
Library Library
------- -------
- Issue #4870: Add an `options` attribute to SSL contexts, as well as
several ``OP_*`` constants to the `ssl` module. This allows to selectively
disable protocol versions, when used in combination with `PROTOCOL_SSLv23`.
- Issue #8759: Fixed user paths in sysconfig for posix and os2 schemes. - Issue #8759: Fixed user paths in sysconfig for posix and os2 schemes.
- Issue #8663: distutils.log emulates backslashreplace error handler. Fix - Issue #8663: distutils.log emulates backslashreplace error handler. Fix
......
...@@ -113,6 +113,13 @@ static unsigned int _ssl_locks_count = 0; ...@@ -113,6 +113,13 @@ static unsigned int _ssl_locks_count = 0;
# undef HAVE_OPENSSL_RAND # undef HAVE_OPENSSL_RAND
#endif #endif
/* SSL_CTX_clear_options() and SSL_clear_options() were first added in OpenSSL 0.9.8m */
#if OPENSSL_VERSION_NUMBER >= 0x009080dfL
# define HAVE_SSL_CTX_CLEAR_OPTIONS
#else
# undef HAVE_SSL_CTX_CLEAR_OPTIONS
#endif
typedef struct { typedef struct {
PyObject_HEAD PyObject_HEAD
SSL_CTX *ctx; SSL_CTX *ctx;
...@@ -1513,6 +1520,35 @@ set_verify_mode(PySSLContext *self, PyObject *arg, void *c) ...@@ -1513,6 +1520,35 @@ set_verify_mode(PySSLContext *self, PyObject *arg, void *c)
return 0; return 0;
} }
static PyObject *
get_options(PySSLContext *self, void *c)
{
return PyLong_FromLong(SSL_CTX_get_options(self->ctx));
}
static int
set_options(PySSLContext *self, PyObject *arg, void *c)
{
long new_opts, opts, set, clear;
if (!PyArg_Parse(arg, "l", &new_opts))
return -1;
opts = SSL_CTX_get_options(self->ctx);
clear = opts & ~new_opts;
set = ~opts & new_opts;
if (clear) {
#ifdef HAVE_SSL_CTX_CLEAR_OPTIONS
SSL_CTX_clear_options(self->ctx, clear);
#else
PyErr_SetString(PyExc_ValueError,
"can't clear options before OpenSSL 0.9.8m");
return -1;
#endif
}
if (set)
SSL_CTX_set_options(self->ctx, set);
return 0;
}
static PyObject * static PyObject *
load_cert_chain(PySSLContext *self, PyObject *args, PyObject *kwds) load_cert_chain(PySSLContext *self, PyObject *args, PyObject *kwds)
{ {
...@@ -1636,6 +1672,8 @@ context_wrap_socket(PySSLContext *self, PyObject *args, PyObject *kwds) ...@@ -1636,6 +1672,8 @@ context_wrap_socket(PySSLContext *self, PyObject *args, PyObject *kwds)
} }
static PyGetSetDef context_getsetlist[] = { static PyGetSetDef context_getsetlist[] = {
{"options", (getter) get_options,
(setter) set_options, NULL},
{"verify_mode", (getter) get_verify_mode, {"verify_mode", (getter) get_verify_mode,
(setter) set_verify_mode, NULL}, (setter) set_verify_mode, NULL},
{NULL}, /* sentinel */ {NULL}, /* sentinel */
...@@ -1953,6 +1991,12 @@ PyInit__ssl(void) ...@@ -1953,6 +1991,12 @@ PyInit__ssl(void)
PyModule_AddIntConstant(m, "PROTOCOL_TLSv1", PyModule_AddIntConstant(m, "PROTOCOL_TLSv1",
PY_SSL_VERSION_TLS1); PY_SSL_VERSION_TLS1);
/* protocol options */
PyModule_AddIntConstant(m, "OP_ALL", SSL_OP_ALL);
PyModule_AddIntConstant(m, "OP_NO_SSLv2", SSL_OP_NO_SSLv2);
PyModule_AddIntConstant(m, "OP_NO_SSLv3", SSL_OP_NO_SSLv3);
PyModule_AddIntConstant(m, "OP_NO_TLSv1", SSL_OP_NO_TLSv1);
/* OpenSSL version */ /* OpenSSL version */
/* SSLeay() gives us the version of the library linked against, /* SSLeay() gives us the version of the library linked against,
which could be different from the headers version. which could be different from the headers version.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment