sslutil: support defining cipher list
authorGregory Szorc <gregory.szorc@gmail.com>
Sun, 17 Jul 2016 10:59:32 -0700
changeset 29577 9654ef41f7cc
parent 29576 d5067913f97b
child 29578 4a4b8d3b4e43
sslutil: support defining cipher list Python 2.7 supports specifying a custom cipher list to TLS sockets. Advanced users may wish to specify a custom cipher list to increase security. Or in some cases they may wish to prefer weaker ciphers in order to increase performance (e.g. when doing stream clones of very large repositories). This patch introduces a [hostsecurity] config option for defining the cipher list. The help documentation states that it is for advanced users only. Honestly, I'm a bit on the fence about providing this because it is a footgun and can be used to decrease security. However, there are legitimate use cases for it, so I think support should be provided.
mercurial/help/config.txt
mercurial/sslutil.py
tests/test-https.t
--- a/mercurial/help/config.txt	Sun Jul 17 10:50:51 2016 -0700
+++ b/mercurial/help/config.txt	Sun Jul 17 10:59:32 2016 -0700
@@ -1005,6 +1005,18 @@
 
 The following options control default behavior for all hosts.
 
+``ciphers``
+    Defines the cryptographic ciphers to use for connections.
+
+    Value must be a valid OpenSSL Cipher List Format as documented at
+    https://www.openssl.org/docs/manmaster/apps/ciphers.html#CIPHER-LIST-FORMAT.
+
+    This setting is for advanced users only. Setting to incorrect values
+    can significantly lower connection security or decrease performance.
+    You have been warned.
+
+    This option requires Python 2.7.
+
 ``minimumprotocol``
     Defines the minimum channel encryption protocol to use.
 
@@ -1027,6 +1039,10 @@
 
 The following per-host settings can be defined.
 
+``ciphers``
+    This behaves like ``ciphers`` as described above except it only applies
+    to the host on which it is defined.
+
 ``fingerprints``
     A list of hashes of the DER encoded peer/remote certificate. Values have
     the form ``algorithm``:``fingerprint``. e.g.
--- a/mercurial/sslutil.py	Sun Jul 17 10:50:51 2016 -0700
+++ b/mercurial/sslutil.py	Sun Jul 17 10:59:32 2016 -0700
@@ -84,7 +84,11 @@
 
         def set_ciphers(self, ciphers):
             if not self._supportsciphers:
-                raise error.Abort(_('setting ciphers not supported'))
+                raise error.Abort(_('setting ciphers in [hostsecurity] is not '
+                                    'supported by this version of Python'),
+                                  hint=_('remove the config option or run '
+                                         'Mercurial with a modern Python '
+                                         'version (preferred)'))
 
             self._ciphers = ciphers
 
@@ -131,6 +135,8 @@
         'verifymode': None,
         # Defines extra ssl.OP* bitwise options to set.
         'ctxoptions': None,
+        # OpenSSL Cipher List to use (instead of default).
+        'ciphers': None,
     }
 
     # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
@@ -183,6 +189,10 @@
 
     s['protocol'], s['ctxoptions'] = protocolsettings(protocol)
 
+    ciphers = ui.config('hostsecurity', 'ciphers')
+    ciphers = ui.config('hostsecurity', '%s:ciphers' % hostname, ciphers)
+    s['ciphers'] = ciphers
+
     # Look for fingerprints in [hostsecurity] section. Value is a list
     # of <alg>:<fingerprint> strings.
     fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname,
@@ -347,6 +357,14 @@
     # This still works on our fake SSLContext.
     sslcontext.verify_mode = settings['verifymode']
 
+    if settings['ciphers']:
+        try:
+            sslcontext.set_ciphers(settings['ciphers'])
+        except ssl.SSLError as e:
+            raise error.Abort(_('could not set ciphers: %s') % e.args[0],
+                              hint=_('change cipher string (%s) in config') %
+                                   settings['ciphers'])
+
     if certfile is not None:
         def password():
             f = keyfile or certfile
--- a/tests/test-https.t	Sun Jul 17 10:50:51 2016 -0700
+++ b/tests/test-https.t	Sun Jul 17 10:59:32 2016 -0700
@@ -326,6 +326,48 @@
   > --config hostsecurity.disabletls10warning=true
   5fed3813f7f5
 
+#if no-sslcontext no-py27+
+Setting ciphers doesn't work in Python 2.6
+  $ P="$CERTSDIR" hg --config hostsecurity.ciphers=HIGH -R copy-pull id https://localhost:$HGPORT/
+  warning: connecting to localhost using legacy security technology (TLS 1.0); see https://mercurial-scm.org/wiki/SecureConnections for more info
+  abort: setting ciphers in [hostsecurity] is not supported by this version of Python
+  (remove the config option or run Mercurial with a modern Python version (preferred))
+  [255]
+#endif
+
+Setting ciphers works in Python 2.7+ but the error message is different on
+legacy ssl. We test legacy once and do more feature checking on modern
+configs.
+
+#if py27+ no-sslcontext
+  $ P="$CERTSDIR" hg --config hostsecurity.ciphers=invalid -R copy-pull id https://localhost:$HGPORT/
+  warning: connecting to localhost using legacy security technology (TLS 1.0); see https://mercurial-scm.org/wiki/SecureConnections for more info
+  abort: *No cipher can be selected. (glob)
+  [255]
+
+  $ P="$CERTSDIR" hg --config hostsecurity.ciphers=HIGH -R copy-pull id https://localhost:$HGPORT/
+  warning: connecting to localhost using legacy security technology (TLS 1.0); see https://mercurial-scm.org/wiki/SecureConnections for more info
+  5fed3813f7f5
+#endif
+
+#if sslcontext
+Setting ciphers to an invalid value aborts
+  $ P="$CERTSDIR" hg --config hostsecurity.ciphers=invalid -R copy-pull id https://localhost:$HGPORT/
+  abort: could not set ciphers: No cipher can be selected.
+  (change cipher string (invalid) in config)
+  [255]
+
+  $ P="$CERTSDIR" hg --config hostsecurity.localhost:ciphers=invalid -R copy-pull id https://localhost:$HGPORT/
+  abort: could not set ciphers: No cipher can be selected.
+  (change cipher string (invalid) in config)
+  [255]
+
+Changing the cipher string works
+
+  $ P="$CERTSDIR" hg --config hostsecurity.ciphers=HIGH -R copy-pull id https://localhost:$HGPORT/
+  5fed3813f7f5
+#endif
+
 Fingerprints
 
 - works without cacerts (hostkeyfingerprints)