diff options
author | Antoine Pitrou <solipsis@pitrou.net> | 2010-05-21 09:56:06 (GMT) |
---|---|---|
committer | Antoine Pitrou <solipsis@pitrou.net> | 2010-05-21 09:56:06 (GMT) |
commit | b52187710e4b486b33624fbde9ba646bc8e925fc (patch) | |
tree | 3f1c6369c64536edc721855273a793339a0fba9f | |
parent | 955d1b22e2b7c1e42a23565e29ba150f1fc9a0ef (diff) | |
download | cpython-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.rst | 56 | ||||
-rw-r--r-- | Lib/ssl.py | 1 | ||||
-rw-r--r-- | Lib/test/test_ssl.py | 156 | ||||
-rw-r--r-- | Misc/NEWS | 4 | ||||
-rw-r--r-- | Modules/_ssl.c | 44 |
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:: @@ -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""" @@ -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. |