[Python-checkins] r81297 - in python/branches/sslopts-4870: Doc/library/ssl.rst Lib/ssl.py Lib/test/test_ssl.py Modules/_ssl.c

antoine.pitrou python-checkins at python.org
Tue May 18 22:09:08 CEST 2010


Author: antoine.pitrou
Date: Tue May 18 22:09:07 2010
New Revision: 81297

Log:
Checkin the current patch for SSL options



Modified:
   python/branches/sslopts-4870/Doc/library/ssl.rst
   python/branches/sslopts-4870/Lib/ssl.py
   python/branches/sslopts-4870/Lib/test/test_ssl.py
   python/branches/sslopts-4870/Modules/_ssl.c

Modified: python/branches/sslopts-4870/Doc/library/ssl.rst
==============================================================================
--- python/branches/sslopts-4870/Doc/library/ssl.rst	(original)
+++ python/branches/sslopts-4870/Doc/library/ssl.rst	Tue May 18 22:09:07 2010
@@ -257,6 +257,37 @@
    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 @@
    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 @@
       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::
 

Modified: python/branches/sslopts-4870/Lib/ssl.py
==============================================================================
--- python/branches/sslopts-4870/Lib/ssl.py	(original)
+++ python/branches/sslopts-4870/Lib/ssl.py	Tue May 18 22:09:07 2010
@@ -63,6 +63,7 @@
 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,

Modified: python/branches/sslopts-4870/Lib/test/test_ssl.py
==============================================================================
--- python/branches/sslopts-4870/Lib/test/test_ssl.py	(original)
+++ python/branches/sslopts-4870/Lib/test/test_ssl.py	Tue May 18 22:09:07 2010
@@ -57,6 +57,10 @@
     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)
+
 
 class BasicSocketTests(unittest.TestCase):
 
@@ -189,6 +193,26 @@
         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 +469,8 @@
 
             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 +482,7 @@
                     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 +562,24 @@
                         # 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 +845,13 @@
             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 +859,8 @@
         # 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 +886,8 @@
             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 +901,21 @@
                              (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 +938,27 @@
             """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 +1046,13 @@
             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
+            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 +1078,16 @@
             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 +1098,9 @@
             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)
+            # No SSLv2 => client will use an SSLv3 hello
+            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"""

Modified: python/branches/sslopts-4870/Modules/_ssl.c
==============================================================================
--- python/branches/sslopts-4870/Modules/_ssl.c	(original)
+++ python/branches/sslopts-4870/Modules/_ssl.c	Tue May 18 22:09:07 2010
@@ -113,6 +113,13 @@
 # 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 @@
 }
 
 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 @@
 }
 
 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 @@
     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.


More information about the Python-checkins mailing list