summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAntoine Pitrou <solipsis@pitrou.net>2010-05-21 09:56:06 (GMT)
committerAntoine Pitrou <solipsis@pitrou.net>2010-05-21 09:56:06 (GMT)
commitb52187710e4b486b33624fbde9ba646bc8e925fc (patch)
tree3f1c6369c64536edc721855273a793339a0fba9f
parent955d1b22e2b7c1e42a23565e29ba150f1fc9a0ef (diff)
downloadcpython-b52187710e4b486b33624fbde9ba646bc8e925fc.zip
cpython-b52187710e4b486b33624fbde9ba646bc8e925fc.tar.gz
cpython-b52187710e4b486b33624fbde9ba646bc8e925fc.tar.bz2
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`.
-rw-r--r--Doc/library/ssl.rst56
-rw-r--r--Lib/ssl.py1
-rw-r--r--Lib/test/test_ssl.py156
-rw-r--r--Misc/NEWS4
-rw-r--r--Modules/_ssl.c44
5 files changed, 204 insertions, 57 deletions
diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst
index 45ffcb0..d2f44a1 100644
--- a/Doc/library/ssl.rst
+++ b/Doc/library/ssl.rst
@@ -257,6 +257,37 @@ Functions, Constants, and Exceptions
modern version, and probably the best choice for maximum protection, if both
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
The version string of the OpenSSL library loaded by the interpreter::
@@ -440,6 +471,17 @@ SSL Contexts
and *suppress_ragged_eofs* have the same meaning as in the top-level
: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
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.
equivalent unless anonymous ciphers are enabled (they are disabled
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::
diff --git a/Lib/ssl.py b/Lib/ssl.py
index 24d3771..585105d 100644
--- a/Lib/ssl.py
+++ b/Lib/ssl.py
@@ -63,6 +63,7 @@ from _ssl import _SSLContext, SSLError
from _ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED
from _ssl import (PROTOCOL_SSLv2, PROTOCOL_SSLv3, PROTOCOL_SSLv23,
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 (
SSL_ERROR_ZERO_RETURN,
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
index c9dc47a..c464440 100644
--- a/Lib/test/test_ssl.py
+++ b/Lib/test/test_ssl.py
@@ -57,6 +57,14 @@ def handle_error(prefix):
if support.verbose:
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):
@@ -189,6 +197,26 @@ class ContextTests(unittest.TestCase):
with self.assertRaisesRegexp(ssl.SSLError, "No cipher can be selected"):
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):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
# Default value
@@ -445,12 +473,8 @@ else:
def wrap_conn(self):
try:
- self.sslconn = ssl.wrap_socket(self.sock, server_side=True,
- certfile=self.server.certificate,
- ssl_version=self.server.protocol,
- ca_certs=self.server.cacerts,
- cert_reqs=self.server.certreqs,
- ciphers=self.server.ciphers)
+ self.sslconn = self.server.context.wrap_socket(
+ self.sock, server_side=True)
except ssl.SSLError:
# XXX Various errors can have happened here, for example
# a mismatching protocol version, an invalid certificate,
@@ -462,7 +486,7 @@ else:
self.close()
return False
else:
- if self.server.certreqs == ssl.CERT_REQUIRED:
+ if self.server.context.verify_mode == ssl.CERT_REQUIRED:
cert = self.sslconn.getpeercert()
if support.verbose and self.server.chatty:
sys.stdout.write(" client cert is " + pprint.pformat(cert) + "\n")
@@ -542,19 +566,24 @@ else:
# harness, we want to stop the server
self.server.stop()
- def __init__(self, certificate, ssl_version=None,
+ def __init__(self, certificate=None, ssl_version=None,
certreqs=None, cacerts=None,
chatty=True, connectionchatty=False, starttls_server=False,
- ciphers=None):
- if ssl_version is None:
- ssl_version = ssl.PROTOCOL_TLSv1
- if certreqs is None:
- certreqs = ssl.CERT_NONE
- self.certificate = certificate
- self.protocol = ssl_version
- self.certreqs = certreqs
- self.cacerts = cacerts
- self.ciphers = ciphers
+ ciphers=None, context=None):
+ if context:
+ self.context = context
+ else:
+ self.context = ssl.SSLContext(ssl_version
+ if ssl_version is not None
+ else ssl.PROTOCOL_TLSv1)
+ self.context.verify_mode = (certreqs if certreqs is not None
+ else ssl.CERT_NONE)
+ 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.connectionchatty = connectionchatty
self.starttls_server = starttls_server
@@ -820,18 +849,13 @@ else:
server.stop()
server.join()
- def server_params_test(certfile, protocol, certreqs, cacertsfile,
- client_certfile, client_protocol=None, indata=b"FOO\n",
- ciphers=None, chatty=True, connectionchatty=False):
+ def server_params_test(client_context, server_context, indata=b"FOO\n",
+ chatty=True, connectionchatty=False):
"""
Launch a server, connect a client to it and try various reads
and writes.
"""
- server = ThreadedEchoServer(certfile,
- certreqs=certreqs,
- ssl_version=protocol,
- cacerts=cacertsfile,
- ciphers=ciphers,
+ server = ThreadedEchoServer(context=server_context,
chatty=chatty,
connectionchatty=False)
flag = threading.Event()
@@ -839,15 +863,8 @@ else:
# wait for it to start
flag.wait()
# try to connect
- if client_protocol is None:
- client_protocol = protocol
try:
- s = ssl.wrap_socket(socket.socket(),
- certfile=client_certfile,
- ca_certs=cacertsfile,
- ciphers=ciphers,
- cert_reqs=certreqs,
- ssl_version=client_protocol)
+ s = client_context.wrap_socket(socket.socket())
s.connect((HOST, server.port))
for arg in [indata, bytearray(indata), memoryview(indata)]:
if connectionchatty:
@@ -873,10 +890,8 @@ else:
server.stop()
server.join()
- def try_protocol_combo(server_protocol,
- client_protocol,
- expect_success,
- certsreqs=None):
+ def try_protocol_combo(server_protocol, client_protocol, expect_success,
+ certsreqs=None, server_options=0, client_options=0):
if certsreqs is None:
certsreqs = ssl.CERT_NONE
certtype = {
@@ -890,14 +905,21 @@ else:
(ssl.get_protocol_name(client_protocol),
ssl.get_protocol_name(server_protocol),
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
# will send an SSLv3 hello (rather than SSLv2) starting from
# OpenSSL 1.0.0 (see issue #8322).
- server_params_test(CERTFILE, server_protocol, certsreqs,
- CERTFILE, CERTFILE, client_protocol,
- ciphers="ALL", chatty=False,
- connectionchatty=False)
+ ctx.set_ciphers("ALL")
+ ctx.load_cert_chain(CERTFILE)
+ ctx.load_verify_locations(CERTFILE)
+ try:
+ server_params_test(client_context, server_context,
+ chatty=False, connectionchatty=False)
# Protocol mismatch can result in either an SSLError, or a
# "Connection reset by peer" error.
except ssl.SSLError:
@@ -920,30 +942,27 @@ else:
"""Basic test of an SSL client connecting to a server"""
if support.verbose:
sys.stdout.write("\n")
- server_params_test(CERTFILE, ssl.PROTOCOL_TLSv1, ssl.CERT_NONE,
- CERTFILE, CERTFILE, ssl.PROTOCOL_TLSv1,
- chatty=True, connectionchatty=True)
+ for protocol in PROTOCOLS:
+ context = ssl.SSLContext(protocol)
+ context.load_cert_chain(CERTFILE)
+ server_params_test(context, context,
+ chatty=True, connectionchatty=True)
def test_getpeercert(self):
if support.verbose:
sys.stdout.write("\n")
- s2 = socket.socket()
- server = ThreadedEchoServer(CERTFILE,
- certreqs=ssl.CERT_NONE,
- ssl_version=ssl.PROTOCOL_SSLv23,
- cacerts=CERTFILE,
- chatty=False)
+ context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
+ context.verify_mode = ssl.CERT_REQUIRED
+ context.load_verify_locations(CERTFILE)
+ context.load_cert_chain(CERTFILE)
+ server = ThreadedEchoServer(context=context, chatty=False)
flag = threading.Event()
server.start(flag)
# wait for it to start
flag.wait()
# try to connect
try:
- s = ssl.wrap_socket(socket.socket(),
- certfile=CERTFILE,
- ca_certs=CERTFILE,
- cert_reqs=ssl.CERT_REQUIRED,
- ssl_version=ssl.PROTOCOL_SSLv23)
+ s = context.wrap_socket(socket.socket())
s.connect((HOST, server.port))
cert = s.getpeercert()
self.assertTrue(cert, "Can't get peer certificate.")
@@ -1031,6 +1050,15 @@ else:
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_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):
"""Connecting to an SSLv23 server with various client options"""
@@ -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_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):
"""Connecting to an SSLv3 server with various client options"""
if support.verbose:
@@ -1066,6 +1104,10 @@ else:
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_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):
"""Connecting to a TLSv1 server with various client options"""
diff --git a/Misc/NEWS b/Misc/NEWS
index e2a7715..fb9b52c 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -375,6 +375,10 @@ C-API
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 #8663: distutils.log emulates backslashreplace error handler. Fix
diff --git a/Modules/_ssl.c b/Modules/_ssl.c
index e4b6fed..a9c772a 100644
--- a/Modules/_ssl.c
+++ b/Modules/_ssl.c
@@ -113,6 +113,13 @@ static unsigned int _ssl_locks_count = 0;
# undef HAVE_OPENSSL_RAND
#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 {
PyObject_HEAD
SSL_CTX *ctx;
@@ -1514,6 +1521,35 @@ set_verify_mode(PySSLContext *self, PyObject *arg, void *c)
}
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 *
load_cert_chain(PySSLContext *self, PyObject *args, PyObject *kwds)
{
char *kwlist[] = {"certfile", "keyfile", NULL};
@@ -1636,6 +1672,8 @@ context_wrap_socket(PySSLContext *self, PyObject *args, PyObject *kwds)
}
static PyGetSetDef context_getsetlist[] = {
+ {"options", (getter) get_options,
+ (setter) set_options, NULL},
{"verify_mode", (getter) get_verify_mode,
(setter) set_verify_mode, NULL},
{NULL}, /* sentinel */
@@ -1953,6 +1991,12 @@ PyInit__ssl(void)
PyModule_AddIntConstant(m, "PROTOCOL_TLSv1",
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 */
/* SSLeay() gives us the version of the library linked against,
which could be different from the headers version.