changeset 28651:4827d07073e6

sslutil: always use SSLContext Now that we have a fake SSLContext instance, we can unify the code paths for wrapping sockets to always use the SSLContext APIs. Because this is security code, I've retained the try..except to make the diff easier to read. It will be removed in the next patch. I took the liberty of updating the inline docs about supported protocols and how the constants work because this stuff is important and needs to be explicitly documented.
author Gregory Szorc <gregory.szorc@gmail.com>
date Sun, 27 Mar 2016 14:18:32 -0700
parents 737863b01d9f
children c617614aefd2
files mercurial/sslutil.py
diffstat 1 files changed, 26 insertions(+), 28 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/sslutil.py	Sun Mar 27 14:08:52 2016 -0700
+++ b/mercurial/sslutil.py	Sun Mar 27 14:18:32 2016 -0700
@@ -107,23 +107,32 @@
             return ssl.wrap_socket(socket, **args)
 
 try:
-    # ssl.SSLContext was added in 2.7.9 and presence indicates modern
-    # SSL/TLS features are available.
-    ssl_context = ssl.SSLContext
-
     def wrapsocket(sock, keyfile, certfile, ui, cert_reqs=ssl.CERT_NONE,
                    ca_certs=None, serverhostname=None):
-        # Allow any version of SSL starting with TLSv1 and
-        # up. Note that specifying TLSv1 here prohibits use of
-        # newer standards (like TLSv1_2), so this is the right way
-        # to do this. Note that in the future it'd be better to
-        # support using ssl.create_default_context(), which sets
-        # up a bunch of things in smart ways (strong ciphers,
-        # protocol versions, etc) and is upgraded by Python
-        # maintainers for us, but that breaks too many things to
-        # do it in a hurry.
-        sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
+        # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
+        # that both ends support, including TLS protocols. On legacy stacks,
+        # the highest it likely goes in TLS 1.0. On modern stacks, it can
+        # support TLS 1.2.
+        #
+        # The PROTOCOL_TLSv* constants select a specific TLS version
+        # only (as opposed to multiple versions). So the method for
+        # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
+        # disable protocols via SSLContext.options and OP_NO_* constants.
+        # However, SSLContext.options doesn't work unless we have the
+        # full/real SSLContext available to us.
+        #
+        # SSLv2 and SSLv3 are broken. We ban them outright.
+        if modernssl:
+            protocol = ssl.PROTOCOL_SSLv23
+        else:
+            protocol = ssl.PROTOCOL_TLSv1
+
+        # TODO use ssl.create_default_context() on modernssl.
+        sslcontext = SSLContext(protocol)
+
+        # This is a no-op on old Python.
         sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3
+
         if certfile is not None:
             def password():
                 f = keyfile or certfile
@@ -132,7 +141,8 @@
         sslcontext.verify_mode = cert_reqs
         if ca_certs is not None:
             sslcontext.load_verify_locations(cafile=ca_certs)
-        elif _canloaddefaultcerts:
+        else:
+            # This is a no-op on old Python.
             sslcontext.load_default_certs()
 
         sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
@@ -143,19 +153,7 @@
             raise error.Abort(_('ssl connection failed'))
         return sslsocket
 except AttributeError:
-    # We don't have a modern version of the "ssl" module and are running
-    # Python <2.7.9.
-    def wrapsocket(sock, keyfile, certfile, ui, cert_reqs=ssl.CERT_NONE,
-                   ca_certs=None, serverhostname=None):
-        sslsocket = ssl.wrap_socket(sock, keyfile, certfile,
-                                    cert_reqs=cert_reqs, ca_certs=ca_certs,
-                                    ssl_version=ssl.PROTOCOL_TLSv1)
-        # check if wrap_socket failed silently because socket had been
-        # closed
-        # - see http://bugs.python.org/issue13721
-        if not sslsocket.cipher():
-            raise error.Abort(_('ssl connection failed'))
-        return sslsocket
+    raise util.Abort('this should not happen')
 
 def _verifycert(cert, hostname):
     '''Verify that cert (in socket.getpeercert() format) matches hostname.