diff -r a935cd7d51a6 -r 7dec5e441bf7 mercurial/sslutil.py --- a/mercurial/sslutil.py Thu Jul 14 20:07:10 2016 -0700 +++ b/mercurial/sslutil.py Thu Jul 14 20:47:22 2016 -0700 @@ -29,14 +29,13 @@ # 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.1', + '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 @@ -136,7 +135,7 @@ # 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 + # 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 @@ -145,19 +144,26 @@ # 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 - else: - s['protocol'] = ssl.PROTOCOL_TLSv1 + + # Allow minimum 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))) - # 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 + key = 'minimumprotocol' + # Default to TLS 1.0+ as that is what browsers are currently doing. + protocol = ui.config('hostsecurity', key, 'tls1.0') + validateprotocol(protocol, key) - # Prevent CRIME. - # There is no guarantee this attribute is defined on the module. - s['ctxoptions'] |= getattr(ssl, 'OP_NO_COMPRESSION', 0) + key = '%s:minimumprotocol' % hostname + protocol = ui.config('hostsecurity', key, protocol) + validateprotocol(protocol, key) + + s['protocol'], s['ctxoptions'] = protocolsettings(protocol) # Look for fingerprints in [hostsecurity] section. Value is a list # of : strings. @@ -250,6 +256,46 @@ return s +def protocolsettings(protocol): + """Resolve the protocol and context options for a config value.""" + if protocol not in configprotocols: + raise ValueError('protocol value not supported: %s' % protocol) + + # Legacy ssl module only supports up to TLS 1.0. Ideally we'd use + # PROTOCOL_SSLv23 and options to disable SSLv2 and SSLv3. However, + # SSLContext.options doesn't work in our implementation since we use + # a fake SSLContext on these Python versions. + if not modernssl: + if protocol != 'tls1.0': + 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')) + + return ssl.PROTOCOL_TLSv1, 0 + + # WARNING: returned options don't work unless the modern ssl module + # is available. Be careful when adding options here. + + # SSLv2 and SSLv3 are broken. We ban them outright. + options = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3 + + if protocol == 'tls1.0': + # Defaults above are to use TLS 1.0+ + pass + elif protocol == 'tls1.1': + options |= ssl.OP_NO_TLSv1 + elif protocol == 'tls1.2': + options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + else: + raise error.Abort(_('this should not happen')) + + # Prevent CRIME. + # There is no guarantee this attribute is defined on the module. + options |= getattr(ssl, 'OP_NO_COMPRESSION', 0) + + return ssl.PROTOCOL_SSLv23, options + def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None): """Add SSL/TLS to a socket. @@ -306,7 +352,7 @@ try: sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname) - except ssl.SSLError: + except ssl.SSLError as e: # If we're doing certificate verification and no CA certs are loaded, # that is almost certainly the reason why verification failed. Provide # a hint to the user. @@ -318,6 +364,13 @@ 'were loaded; see ' 'https://mercurial-scm.org/wiki/SecureConnections for ' 'how to configure Mercurial to avoid this error)\n')) + # Try to print more helpful error messages for known failures. + if util.safehasattr(e, 'reason'): + if e.reason == 'UNSUPPORTED_PROTOCOL': + ui.warn(_('(could not negotiate a common protocol; see ' + 'https://mercurial-scm.org/wiki/SecureConnections ' + 'for how to configure Mercurial to avoid this ' + 'error)\n')) raise # check if wrap_socket failed silently because socket had been @@ -349,14 +402,28 @@ Typically ``cafile`` is only defined if ``requireclientcert`` is true. """ + protocol, options = protocolsettings('tls1.0') + + # This config option is intended for use in tests only. It is a giant + # footgun to kill security. Don't define it. + exactprotocol = ui.config('devel', 'serverexactprotocol') + if exactprotocol == 'tls1.0': + protocol = ssl.PROTOCOL_TLSv1 + elif exactprotocol == 'tls1.1': + protocol = ssl.PROTOCOL_TLSv1_1 + elif exactprotocol == 'tls1.2': + protocol = ssl.PROTOCOL_TLSv1_2 + elif exactprotocol: + raise error.Abort(_('invalid value for serverexactprotocol: %s') % + exactprotocol) + if modernssl: # We /could/ use create_default_context() here since it doesn't load - # CAs when configured for client auth. - sslcontext = SSLContext(ssl.PROTOCOL_SSLv23) - # SSLv2 and SSLv3 are broken. Ban them outright. - sslcontext.options |= OP_NO_SSLv2 | OP_NO_SSLv3 - # Prevent CRIME - sslcontext.options |= getattr(ssl, 'OP_NO_COMPRESSION', 0) + # CAs when configured for client auth. However, it is hard-coded to + # use ssl.PROTOCOL_SSLv23 which may not be appropriate here. + sslcontext = SSLContext(protocol) + sslcontext.options |= options + # Improve forward secrecy. sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0) sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)