Patchwork [3,of,3,RFC] sslutil: config option to specify TLS protocol version

login
register
mail settings
Submitter Gregory Szorc
Date July 7, 2016, 6:31 a.m.
Message ID <961c91677f9b2394b3b8.1467873097@ubuntu-vm-main>
Download mbox | patch
Permalink /patch/15766/
State Accepted
Headers show

Comments

Gregory Szorc - July 7, 2016, 6:31 a.m.
# HG changeset patch
# User Gregory Szorc <gregory.szorc@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

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.
* 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?
* Should we stop using TLS 1.0 if TLS 1.1+ is available by default?
* Should we also provide a config option that sets value for
  SSLContext.set_ciphers()?
Augie Fackler - July 12, 2016, 4:42 p.m.
On Wed, Jul 06, 2016 at 11:31:37PM -0700, Gregory Szorc wrote:
> # HG changeset patch
> # User Gregory Szorc <gregory.szorc@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@mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
Gregory Szorc - July 12, 2016, 9:44 p.m.
On Tue, Jul 12, 2016 at 9:42 AM, Augie Fackler <raf@durin42.com> wrote:

> On Wed, Jul 06, 2016 at 11:31:37PM -0700, Gregory Szorc wrote:
> > # HG changeset patch
> > # User Gregory Szorc <gregory.szorc@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.
>

OK. Will change this.


>
> > * 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.
>

I just asked Eric Rescorla "should Mercurial drop TLS 1.0 support" and his
response was "if you can get away with it." Basically a large portion of
the Internet still doesn't run TLS 1.1+ and dropping TLS 1.0 would make
that Internet unavailable. Just how many Mercurial servers don't run TLS
1.1+, I have no idea and there is no easy way to find out.

I'll throw out an idea. We could make TLS 1.1+ the default (on modern
Python versions since legacy Python only supports TLS 1.0) and provide an
option to allow TLS 1.0+. When connecting to a server that doesn't support
TLS 1.1+, we can suggest users try the legacy config. When TLS 1.0 is
insecure, we can drop the config option to allow it. There is also a
related discussion about whether we should print warnings on legacy Pythons
that don't support TLS 1.1+.


>
> > * 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@mercurial-scm.org
> > https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
>
Augie Fackler - July 13, 2016, 1:54 a.m.
> On Jul 12, 2016, at 5:44 PM, Gregory Szorc <gregory.szorc@gmail.com> wrote:
> 
>> > * 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.
> 
> I just asked Eric Rescorla "should Mercurial drop TLS 1.0 support" and his response was "if you can get away with it." Basically a large portion of the Internet still doesn't run TLS 1.1+ and dropping TLS 1.0 would make that Internet unavailable. Just how many Mercurial servers don't run TLS 1.1+, I have no idea and there is no easy way to find out.
> 
> I'll throw out an idea. We could make TLS 1.1+ the default (on modern Python versions since legacy Python only supports TLS 1.0) and provide an option to allow TLS 1.0+. When connecting to a server that doesn't support TLS 1.1+, we can suggest users try the legacy config. When TLS 1.0 is insecure, we can drop the config option to allow it. There is also a related discussion about whether we should print warnings on legacy Pythons that don't support TLS 1.1+.

Works for me. Make it so in the resend?

Patch

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') % (