[PATCH 3 of 3] [RFC] sslutil: config option to specify TLS protocol version
Augie Fackler
raf at durin42.com
Tue Jul 12 16:42:26 UTC 2016
On Wed, Jul 06, 2016 at 11:31:37PM -0700, Gregory Szorc wrote:
> # HG changeset patch
> # User Gregory Szorc <gregory.szorc at gmail.com>
> # Date 1467873039 25200
> # Wed Jul 06 23:30:39 2016 -0700
> # Node ID 961c91677f9b2394b3b8140c28059d5fc5eee00f
> # Parent 66a5adf42ac7bc0031e772b407403e809e67fb6e
> [RFC] sslutil: config option to specify TLS protocol version
queued patches 1 and 2, thanks. Comments inline below for some questions.
>
> Currently, Mercurial will use TLS 1.0 or newer when connecting to
> remote servers, selecting the highest TLS version supported by both
> peers. On older Pythons, only TLS 1.0 is available. On newer Pythons,
> TLS 1.1 and 1.2 should be available.
>
> Security professionals recommend avoiding TLS 1.0 if possible.
> PCI DSS 3.1 "strongly encourages" the use of TLS 1.2.
>
> Known attacks like BEAST and POODLE exist against TLS 1.0 (although
> mitigations are available and properly configured servers aren't
> vulnerable).
>
> Web browsers and many other applications continue to support TLS
> 1.0 for compatibility reasons.
>
> Security-minded people may want to not take any risks running
> TLS 1.0 (or even TLS 1.1). This patch gives those people a config
> option to explicitly control which TLS version Mercurial should use.
> By providing this option, people can require newer TLS versions
> before they are formally deprecated by Mercurial/Python/OpenSSL/etc
> and lower their security exposure. This option also provides an
> easy mechanism to change protocol policies in Mercurial. If there
> is a 0-day and TLS 1.0 is completely broken, we can act quickly
> without changing much code.
>
> Because setting the minimum TLS protocol is something you'll likely
> want to do globally, this patch introduces the special
> "hostsecurity._default_" config option to hold settings that apply
> to all hosts. Only the "protocol" sub-option currently works.
>
> Open Issues:
>
> * Need to write tests
> * Is "hostsecurity._default_" really the best way to do that?
> * Do we want to support both the explicit and "+" values. I could be
> convinced that only the e.g. "tls1.1+" values are needed. Why would
> you only choose TLS 1.1 when TLS 1.2 is available? That feels like
> a footgun that could accidentally lock you into old security when
> TLS 1.3 is available.
I think that it should really just be a "minimum version" of TLS, so
tls1.1 means 1.1 or newer. The footgun aspect is important.
> * Support "hostsecurity._default_:verifycertsfile"? (would effectively
> be web.cacerts for clients)
> * Should we issue a warning if TLS 1.0 is used? What if TLS 1.0 is all
> that is available?
I'm not sure. Browsers are still allowing TLS1, so we probably should
just silently allow it by default too.
> * Should we stop using TLS 1.0 if TLS 1.1+ is available by default?
I don't know that I'd go this far yet.
> * Should we also provide a config option that sets value for
> SSLContext.set_ciphers()?
Probably.
>
> diff --git a/mercurial/help/config.txt b/mercurial/help/config.txt
> --- a/mercurial/help/config.txt
> +++ b/mercurial/help/config.txt
> @@ -1021,16 +1021,24 @@ The following per-host settings can be d
> host and Mercurial will require the remote certificate to match one
> of the fingerprints specified. This means if the server updates its
> certificate, Mercurial will abort until a new fingerprint is defined.
> This can provide stronger security than traditional CA-based validation
> at the expense of convenience.
>
> This option takes precedence over ``verifycertsfile``.
>
> +``protocol``
> + Defines the channel encryption protocol to use.
> +
> + By default, the highest TLS version 1.0 or greater protocol supported by
> + both client and server is used. This option can be used to explicitly use
> + a specific protocol version or to bump the minimum version that would
> + otherwise be used.
> +
> ``verifycertsfile``
> Path to file a containing a list of PEM encoded certificates used to
> verify the server certificate. Environment variables and ``~user``
> constructs are expanded in the filename.
>
> The server certificate or the certificate's certificate authority (CA)
> must match a certificate from this file or certificate verification
> will fail and connections to the server will be refused.
> diff --git a/mercurial/sslutil.py b/mercurial/sslutil.py
> --- a/mercurial/sslutil.py
> +++ b/mercurial/sslutil.py
> @@ -24,24 +24,26 @@ from . import (
> # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
> # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
> # all exposed via the "ssl" module.
> #
> # Depending on the version of Python being used, SSL/TLS support is either
> # modern/secure or legacy/insecure. Many operations in this module have
> # separate code paths depending on support in Python.
>
> -hassni = getattr(ssl, 'HAS_SNI', False)
> +configprotocols = set([
> + 'tls1.0',
> + 'tls1.0+',
> + 'tls1.1',
> + 'tls1.1+',
> + 'tls1.2',
> + 'tls1.2+',
> +])
>
> -try:
> - OP_NO_SSLv2 = ssl.OP_NO_SSLv2
> - OP_NO_SSLv3 = ssl.OP_NO_SSLv3
> -except AttributeError:
> - OP_NO_SSLv2 = 0x1000000
> - OP_NO_SSLv3 = 0x2000000
> +hassni = getattr(ssl, 'HAS_SNI', False)
>
> try:
> # ssl.SSLContext was added in 2.7.9 and presence indicates modern
> # SSL/TLS features are available.
> SSLContext = ssl.SSLContext
> modernssl = True
> _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
> except AttributeError:
> @@ -140,25 +142,65 @@ def _hostsettings(ui, hostname):
> # 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.
> - if modernssl:
> - s['protocol'] = ssl.PROTOCOL_SSLv23
> +
> + # Allow TLS protocol to be specified in the config.
> + def validateprotocol(protocol, key):
> + if protocol not in configprotocols:
> + raise error.Abort(
> + _('unsupported protocol from hostsecurity.%s: %s') %
> + (key, protocol),
> + hint=_('valid protocols: %s') %
> + ' '.join(sorted(configprotocols)))
> +
> + protocol = ui.config('hostsecurity', '_default_:protocol', 'tls1.0+')
> + validateprotocol(protocol, '_default_:protocol')
> +
> + protocol = ui.config('hostsecurity', '%s:protocol' % hostname, protocol)
> + validateprotocol(protocol, '%s.protocol' % hostname)
> +
> + # Legacy ssl module only supports TLS 1.0.
> + if not modernssl:
> + if protocol in ('tls1.0', 'tls1.0+'):
> + s['protocol'] = ssl.PROTOCOL_TLSv1
> + s['ctxoptions'] = 0
> + else:
> + raise error.Abort(_('current Python does not support protocol '
> + 'setting %s') % protocol,
> + hint=_('upgrade Python or disable setting since '
> + 'only TLS 1.0 is supported'))
> else:
> - s['protocol'] = ssl.PROTOCOL_TLSv1
> -
> - # SSLv2 and SSLv3 are broken. We ban them outright.
> - # WARNING: ctxoptions doesn't have an effect unless the modern ssl module
> - # is available. Be careful when adding flags!
> - s['ctxoptions'] = OP_NO_SSLv2 | OP_NO_SSLv3
> + if protocol == 'tls1.0':
> + s['protocol'] = ssl.PROTOCOL_TLSv1
> + s['ctxoptions'] = 0
> + elif protocol == 'tls1.0+':
> + s['protocol'] = ssl.PROTOCOL_SSLv23
> + s['ctxoptions'] = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
> + elif protocol == 'tls1.1':
> + s['protocol'] = ssl.PROTOCOL_TLSv1_1
> + s['ctxoptions'] = 0
> + elif protocol == 'tls1.1+':
> + s['protocol'] = ssl.PROTOCOL_SSLv23
> + s['ctxoptions'] = (ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 |
> + ssl.OP_NO_TLSv1_1)
> + elif protocol == 'tls1.2':
> + s['protocol'] = ssl.PROTOCOL_TLSv1_2
> + s['ctxoptions'] = 0
> + elif protocol == 'tls1.2+':
> + s['protocol'] = ssl.PROTOCOL_SSLv23
> + s['ctxoptions'] = (ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 |
> + ssl.OP_NO_TLSv1_1 | ssl.OP_NO_TLSv1_2)
> + else:
> + raise error.Abort(_('this should not happen'))
>
> # 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') % (
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel at mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
More information about the Mercurial-devel
mailing list