[PATCH 5 of 5 V3] sslutil: support defining cipher list

Gregory Szorc gregory.szorc at gmail.com
Thu Jul 14 00:50:16 EDT 2016


# HG changeset patch
# User Gregory Szorc <gregory.szorc at gmail.com>
# Date 1468470132 25200
#      Wed Jul 13 21:22:12 2016 -0700
# Node ID a79b1c2387e42762b3862738ba0321ea8190b999
# Parent  5bcd23efc23d6f2110c398565e3ecb9cad3ed866
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.

diff --git a/mercurial/help/config.txt b/mercurial/help/config.txt
--- a/mercurial/help/config.txt
+++ b/mercurial/help/config.txt
@@ -1000,16 +1000,28 @@ For example::
 ``hostsecurity``
 ----------------
 
 Used to specify global and per-host security settings for connecting to
 other machines.
 
 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.
 
     By default, the highest version of TLS supported by both client and server
     is used.
 
     Allowed values are: ``tls1.0``, ``tls1.1``, ``tls1.2``.
 
@@ -1022,16 +1034,20 @@ The following options control default be
     a server does not support TLS 1.1+.
 
 Options in the ``[hostsecurity]`` section can have the form
 ``hostname``:``setting``. This allows multiple settings to be defined on a
 per-host basis.
 
 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.
     ``sha256:c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2``.
 
     The following algorithms/prefixes are supported: ``sha1``, ``sha256``,
     ``sha512``.
 
diff --git a/mercurial/sslutil.py b/mercurial/sslutil.py
--- a/mercurial/sslutil.py
+++ b/mercurial/sslutil.py
@@ -79,17 +79,21 @@ except AttributeError:
                 raise error.Abort(_('capath not supported'))
             if cadata:
                 raise error.Abort(_('cadata not supported'))
 
             self._cacerts = cafile
 
         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
 
         def wrap_socket(self, socket, server_hostname=None, server_side=False):
             # server_hostname is unique to SSLContext.wrap_socket and is used
             # for SNI in that context. So there's nothing for us to do with it
             # in this legacy code since we don't support SNI.
 
@@ -126,16 +130,18 @@ def _hostsettings(ui, hostname):
         # Whether the legacy [hostfingerprints] section has data for this host.
         'legacyfingerprint': False,
         # PROTOCOL_* constant to use for SSLContext.__init__.
         'protocol': None,
         # ssl.CERT_* constant used by SSLContext.verify_mode.
         '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
     # that both ends support, including TLS protocols. On legacy stacks,
     # the highest it likely goes is TLS 1.0. On modern stacks, it can
     # support TLS 1.2.
     #
     # The PROTOCOL_TLSv* constants select a specific TLS version
@@ -178,16 +184,20 @@ def _hostsettings(ui, hostname):
     validateprotocol(protocol, key)
 
     key = '%s:minimumprotocol' % hostname
     protocol = ui.config('hostsecurity', key, protocol)
     validateprotocol(protocol, key)
 
     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,
                                  [])
     for fingerprint in fingerprints:
         if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
             raise error.Abort(_('invalid fingerprint for %s: %s') % (
                                 hostname, fingerprint),
@@ -338,16 +348,24 @@ def wrapsocket(sock, keyfile, certfile, 
         sslcontext = SSLContext(settings['protocol'])
 
     # This is a no-op unless using modern ssl.
     sslcontext.options |= settings['ctxoptions']
 
     # 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
             return ui.getpass(_('passphrase for %s: ') % f, '')
         sslcontext.load_cert_chain(certfile, keyfile, password)
 
     if settings['cafile'] is not None:
         try:
diff --git a/tests/test-https.t b/tests/test-https.t
--- a/tests/test-https.t
+++ b/tests/test-https.t
@@ -321,16 +321,43 @@ Test server cert which no longer is vali
   [255]
 
 Disabling the TLS 1.0 warning works
   $ hg -R copy-pull id https://localhost:$HGPORT/ \
   > --config hostsecurity.localhost:fingerprints=sha1:ecd87cd6b386d04fc1b8b41c9d8f5e168eef1c03 \
   > --config hostsecurity.disabletls10warning=true
   5fed3813f7f5
 
+#if no-sslcontext
+Setting ciphers requires Python 2.7 (any version - it doesn't require SSLContext)
+  $ 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
+
+#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)
   $ hg -R copy-pull id https://localhost:$HGPORT/ --insecure --config hostfingerprints.localhost=ec:d8:7c:d6:b3:86:d0:4f:c1:b8:b4:1c:9d:8f:5e:16:8e:ef:1c:03
   warning: connecting to localhost using legacy security technology (TLS 1.0); see https://mercurial-scm.org/wiki/SecureConnections for more info (?)
   5fed3813f7f5
 
 - works without cacerts (hostsecurity)


More information about the Mercurial-devel mailing list