sslutil.py
1024 lines
| 38.7 KiB
| text/x-python
|
PythonLexer
/ mercurial / sslutil.py
Augie Fackler
|
r14204 | # sslutil.py - SSL handling for mercurial | ||
# | ||||
# Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com> | ||||
# Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br> | ||||
# Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com> | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
Gregory Szorc
|
r25977 | |||
from __future__ import absolute_import | ||||
Augie Fackler
|
r14204 | |||
Augie Fackler
|
r29341 | import hashlib | ||
Gregory Szorc
|
r25977 | import os | ||
Gregory Szorc
|
r29452 | import re | ||
Gregory Szorc
|
r25977 | import ssl | ||
from .i18n import _ | ||||
Gregory Szorc
|
r43359 | from .pycompat import getattr | ||
Gregory Szorc
|
r28577 | from . import ( | ||
Augie Fackler
|
r42455 | encoding, | ||
Gregory Szorc
|
r28577 | error, | ||
Pulkit Goyal
|
r35600 | node, | ||
Pulkit Goyal
|
r30639 | pycompat, | ||
Gregory Szorc
|
r28577 | util, | ||
) | ||||
Yuya Nishihara
|
r37102 | from .utils import ( | ||
Augie Fackler
|
r44518 | hashutil, | ||
Martin von Zweigbergk
|
r44067 | resourceutil, | ||
Yuya Nishihara
|
r37102 | stringutil, | ||
) | ||||
Yuya Nishihara
|
r24291 | |||
Gregory Szorc
|
r28647 | # 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. | ||||
Martin von Zweigbergk
|
r32291 | configprotocols = { | ||
Augie Fackler
|
r43347 | b'tls1.0', | ||
b'tls1.1', | ||||
b'tls1.2', | ||||
Martin von Zweigbergk
|
r32291 | } | ||
Gregory Szorc
|
r26622 | |||
Gregory Szorc
|
r29559 | hassni = getattr(ssl, 'HAS_SNI', False) | ||
Gregory Szorc
|
r28648 | |||
Gregory Szorc
|
r29601 | # TLS 1.1 and 1.2 may not be supported if the OpenSSL Python is compiled | ||
# against doesn't support them. | ||||
Augie Fackler
|
r43347 | supportedprotocols = {b'tls1.0'} | ||
if util.safehasattr(ssl, b'PROTOCOL_TLSv1_1'): | ||||
supportedprotocols.add(b'tls1.1') | ||||
if util.safehasattr(ssl, b'PROTOCOL_TLSv1_2'): | ||||
supportedprotocols.add(b'tls1.2') | ||||
Gregory Szorc
|
r29601 | |||
Gregory Szorc
|
r28649 | try: | ||
# ssl.SSLContext was added in 2.7.9 and presence indicates modern | ||||
# SSL/TLS features are available. | ||||
SSLContext = ssl.SSLContext | ||||
modernssl = True | ||||
Augie Fackler
|
r43347 | _canloaddefaultcerts = util.safehasattr(SSLContext, b'load_default_certs') | ||
Gregory Szorc
|
r28649 | except AttributeError: | ||
modernssl = False | ||||
Gregory Szorc
|
r28650 | _canloaddefaultcerts = False | ||
Gregory Szorc
|
r28649 | |||
# We implement SSLContext using the interface from the standard library. | ||||
class SSLContext(object): | ||||
def __init__(self, protocol): | ||||
# From the public interface of SSLContext | ||||
self.protocol = protocol | ||||
self.check_hostname = False | ||||
self.options = 0 | ||||
self.verify_mode = ssl.CERT_NONE | ||||
# Used by our implementation. | ||||
self._certfile = None | ||||
self._keyfile = None | ||||
self._certpassword = None | ||||
self._cacerts = None | ||||
self._ciphers = None | ||||
def load_cert_chain(self, certfile, keyfile=None, password=None): | ||||
self._certfile = certfile | ||||
self._keyfile = keyfile | ||||
self._certpassword = password | ||||
def load_default_certs(self, purpose=None): | ||||
pass | ||||
def load_verify_locations(self, cafile=None, capath=None, cadata=None): | ||||
if capath: | ||||
Augie Fackler
|
r43347 | raise error.Abort(_(b'capath not supported')) | ||
Gregory Szorc
|
r28649 | if cadata: | ||
Augie Fackler
|
r43347 | raise error.Abort(_(b'cadata not supported')) | ||
Gregory Szorc
|
r28649 | |||
self._cacerts = cafile | ||||
def set_ciphers(self, ciphers): | ||||
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. | ||||
args = { | ||||
Augie Fackler
|
r43906 | 'keyfile': self._keyfile, | ||
'certfile': self._certfile, | ||||
'server_side': server_side, | ||||
'cert_reqs': self.verify_mode, | ||||
'ssl_version': self.protocol, | ||||
'ca_certs': self._cacerts, | ||||
'ciphers': self._ciphers, | ||||
Gregory Szorc
|
r28649 | } | ||
return ssl.wrap_socket(socket, **args) | ||||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r29258 | def _hostsettings(ui, hostname): | ||
"""Obtain security settings for a hostname. | ||||
Returns a dict of settings relevant to that hostname. | ||||
""" | ||||
Augie Fackler
|
r36760 | bhostname = pycompat.bytesurl(hostname) | ||
Gregory Szorc
|
r29258 | s = { | ||
Gregory Szorc
|
r29288 | # Whether we should attempt to load default/available CA certs | ||
# if an explicit ``cafile`` is not defined. | ||||
Augie Fackler
|
r43347 | b'allowloaddefaultcerts': True, | ||
Gregory Szorc
|
r29258 | # List of 2-tuple of (hash algorithm, hash). | ||
Augie Fackler
|
r43347 | b'certfingerprints': [], | ||
Gregory Szorc
|
r29260 | # Path to file containing concatenated CA certs. Used by | ||
# SSLContext.load_verify_locations(). | ||||
Augie Fackler
|
r43347 | b'cafile': None, | ||
Gregory Szorc
|
r29287 | # Whether certificate verification should be disabled. | ||
Augie Fackler
|
r43347 | b'disablecertverification': False, | ||
Gregory Szorc
|
r29268 | # Whether the legacy [hostfingerprints] section has data for this host. | ||
Augie Fackler
|
r43347 | b'legacyfingerprint': False, | ||
Gregory Szorc
|
r29507 | # PROTOCOL_* constant to use for SSLContext.__init__. | ||
Augie Fackler
|
r43347 | b'protocol': None, | ||
Gregory Szorc
|
r29618 | # String representation of minimum protocol to be used for UI | ||
# presentation. | ||||
Augie Fackler
|
r43347 | b'protocolui': None, | ||
Gregory Szorc
|
r29259 | # ssl.CERT_* constant used by SSLContext.verify_mode. | ||
Augie Fackler
|
r43347 | b'verifymode': None, | ||
Gregory Szorc
|
r29508 | # Defines extra ssl.OP* bitwise options to set. | ||
Augie Fackler
|
r43347 | b'ctxoptions': None, | ||
Gregory Szorc
|
r29577 | # OpenSSL Cipher List to use (instead of default). | ||
Augie Fackler
|
r43347 | b'ciphers': None, | ||
Gregory Szorc
|
r29258 | } | ||
Gregory Szorc
|
r29559 | # Allow minimum TLS protocol to be specified in the config. | ||
def validateprotocol(protocol, key): | ||||
if protocol not in configprotocols: | ||||
raise error.Abort( | ||||
Augie Fackler
|
r43347 | _(b'unsupported protocol from hostsecurity.%s: %s') | ||
Augie Fackler
|
r43346 | % (key, protocol), | ||
Augie Fackler
|
r43347 | hint=_(b'valid protocols: %s') | ||
% b' '.join(sorted(configprotocols)), | ||||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r29507 | |||
Gregory Szorc
|
r29601 | # We default to TLS 1.1+ where we can because TLS 1.0 has known | ||
# vulnerabilities (like BEAST and POODLE). We allow users to downgrade to | ||||
# TLS 1.0+ via config options in case a legacy server is encountered. | ||||
Augie Fackler
|
r43347 | if b'tls1.1' in supportedprotocols: | ||
defaultprotocol = b'tls1.1' | ||||
Gregory Szorc
|
r29560 | else: | ||
Gregory Szorc
|
r29601 | # Let people know they are borderline secure. | ||
Gregory Szorc
|
r29561 | # We don't document this config option because we want people to see | ||
# the bold warnings on the web site. | ||||
# internal config: hostsecurity.disabletls10warning | ||||
Augie Fackler
|
r43347 | if not ui.configbool(b'hostsecurity', b'disabletls10warning'): | ||
Augie Fackler
|
r43346 | ui.warn( | ||
_( | ||||
Augie Fackler
|
r43347 | b'warning: connecting to %s using legacy security ' | ||
b'technology (TLS 1.0); see ' | ||||
b'https://mercurial-scm.org/wiki/SecureConnections for ' | ||||
b'more info\n' | ||||
Augie Fackler
|
r43346 | ) | ||
% bhostname | ||||
) | ||||
Augie Fackler
|
r43347 | defaultprotocol = b'tls1.0' | ||
Gregory Szorc
|
r29560 | |||
Augie Fackler
|
r43347 | key = b'minimumprotocol' | ||
protocol = ui.config(b'hostsecurity', key, defaultprotocol) | ||||
Gregory Szorc
|
r29559 | validateprotocol(protocol, key) | ||
Gregory Szorc
|
r29508 | |||
Augie Fackler
|
r43347 | key = b'%s:minimumprotocol' % bhostname | ||
protocol = ui.config(b'hostsecurity', key, protocol) | ||||
Gregory Szorc
|
r29559 | validateprotocol(protocol, key) | ||
Gregory Szorc
|
r29617 | # If --insecure is used, we allow the use of TLS 1.0 despite config options. | ||
# We always print a "connection security to %s is disabled..." message when | ||||
# --insecure is used. So no need to print anything more here. | ||||
if ui.insecureconnections: | ||||
Augie Fackler
|
r43347 | protocol = b'tls1.0' | ||
Gregory Szorc
|
r29558 | |||
Augie Fackler
|
r43347 | s[b'protocol'], s[b'ctxoptions'], s[b'protocolui'] = protocolsettings( | ||
protocol | ||||
) | ||||
ciphers = ui.config(b'hostsecurity', b'ciphers') | ||||
ciphers = ui.config(b'hostsecurity', b'%s:ciphers' % bhostname, ciphers) | ||||
s[b'ciphers'] = ciphers | ||||
Gregory Szorc
|
r29577 | |||
Gregory Szorc
|
r29267 | # Look for fingerprints in [hostsecurity] section. Value is a list | ||
# of <alg>:<fingerprint> strings. | ||||
Augie Fackler
|
r43347 | fingerprints = ui.configlist( | ||
b'hostsecurity', b'%s:fingerprints' % bhostname | ||||
) | ||||
Gregory Szorc
|
r29267 | for fingerprint in fingerprints: | ||
Augie Fackler
|
r43347 | if not (fingerprint.startswith((b'sha1:', b'sha256:', b'sha512:'))): | ||
Augie Fackler
|
r43346 | raise error.Abort( | ||
Augie Fackler
|
r43347 | _(b'invalid fingerprint for %s: %s') % (bhostname, fingerprint), | ||
Martin von Zweigbergk
|
r43387 | hint=_(b'must begin with "sha1:", "sha256:", or "sha512:"'), | ||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r29267 | |||
Augie Fackler
|
r43347 | alg, fingerprint = fingerprint.split(b':', 1) | ||
fingerprint = fingerprint.replace(b':', b'').lower() | ||||
s[b'certfingerprints'].append((alg, fingerprint)) | ||||
Gregory Szorc
|
r29267 | |||
Gregory Szorc
|
r29258 | # Fingerprints from [hostfingerprints] are always SHA-1. | ||
Augie Fackler
|
r43347 | for fingerprint in ui.configlist(b'hostfingerprints', bhostname): | ||
fingerprint = fingerprint.replace(b':', b'').lower() | ||||
s[b'certfingerprints'].append((b'sha1', fingerprint)) | ||||
s[b'legacyfingerprint'] = True | ||||
Gregory Szorc
|
r29258 | |||
Gregory Szorc
|
r29259 | # If a host cert fingerprint is defined, it is the only thing that | ||
# matters. No need to validate CA certs. | ||||
Augie Fackler
|
r43347 | if s[b'certfingerprints']: | ||
s[b'verifymode'] = ssl.CERT_NONE | ||||
s[b'allowloaddefaultcerts'] = False | ||||
Gregory Szorc
|
r29259 | |||
# If --insecure is used, don't take CAs into consideration. | ||||
elif ui.insecureconnections: | ||||
Augie Fackler
|
r43347 | s[b'disablecertverification'] = True | ||
s[b'verifymode'] = ssl.CERT_NONE | ||||
s[b'allowloaddefaultcerts'] = False | ||||
Gregory Szorc
|
r29259 | |||
Augie Fackler
|
r43347 | if ui.configbool(b'devel', b'disableloaddefaultcerts'): | ||
s[b'allowloaddefaultcerts'] = False | ||||
Gregory Szorc
|
r29288 | |||
Gregory Szorc
|
r29334 | # If both fingerprints and a per-host ca file are specified, issue a warning | ||
# because users should not be surprised about what security is or isn't | ||||
# being performed. | ||||
Augie Fackler
|
r43347 | cafile = ui.config(b'hostsecurity', b'%s:verifycertsfile' % bhostname) | ||
if s[b'certfingerprints'] and cafile: | ||||
Augie Fackler
|
r43346 | ui.warn( | ||
_( | ||||
Augie Fackler
|
r43347 | b'(hostsecurity.%s:verifycertsfile ignored when host ' | ||
b'fingerprints defined; using host fingerprints for ' | ||||
b'verification)\n' | ||||
Augie Fackler
|
r43346 | ) | ||
% bhostname | ||||
) | ||||
Gregory Szorc
|
r29334 | |||
Gregory Szorc
|
r29260 | # Try to hook up CA certificate validation unless something above | ||
# makes it not necessary. | ||||
Augie Fackler
|
r43347 | if s[b'verifymode'] is None: | ||
Gregory Szorc
|
r29334 | # Look at per-host ca file first. | ||
Gregory Szorc
|
r29260 | if cafile: | ||
cafile = util.expandpath(cafile) | ||||
if not os.path.exists(cafile): | ||||
Augie Fackler
|
r43346 | raise error.Abort( | ||
Augie Fackler
|
r43347 | _(b'path specified by %s does not exist: %s') | ||
% ( | ||||
b'hostsecurity.%s:verifycertsfile' % (bhostname,), | ||||
cafile, | ||||
) | ||||
Augie Fackler
|
r43346 | ) | ||
Augie Fackler
|
r43347 | s[b'cafile'] = cafile | ||
Gregory Szorc
|
r29260 | else: | ||
Gregory Szorc
|
r29334 | # Find global certificates file in config. | ||
Augie Fackler
|
r43347 | cafile = ui.config(b'web', b'cacerts') | ||
Gregory Szorc
|
r29334 | |||
Gregory Szorc
|
r29260 | if cafile: | ||
Gregory Szorc
|
r29334 | cafile = util.expandpath(cafile) | ||
if not os.path.exists(cafile): | ||||
Augie Fackler
|
r43346 | raise error.Abort( | ||
Augie Fackler
|
r43347 | _(b'could not find web.cacerts: %s') % cafile | ||
Augie Fackler
|
r43346 | ) | ||
Augie Fackler
|
r43347 | elif s[b'allowloaddefaultcerts']: | ||
Gregory Szorc
|
r29482 | # CAs not defined in config. Try to find system bundles. | ||
Gregory Szorc
|
r29483 | cafile = _defaultcacerts(ui) | ||
Gregory Szorc
|
r29334 | if cafile: | ||
Augie Fackler
|
r43347 | ui.debug(b'using %s for CA file\n' % cafile) | ||
Gregory Szorc
|
r29260 | |||
Augie Fackler
|
r43347 | s[b'cafile'] = cafile | ||
Gregory Szorc
|
r29260 | |||
# Require certificate validation if CA certs are being loaded and | ||||
# verification hasn't been disabled above. | ||||
Augie Fackler
|
r43347 | if cafile or (_canloaddefaultcerts and s[b'allowloaddefaultcerts']): | ||
s[b'verifymode'] = ssl.CERT_REQUIRED | ||||
Gregory Szorc
|
r29260 | else: | ||
# At this point we don't have a fingerprint, aren't being | ||||
# explicitly insecure, and can't load CA certs. Connecting | ||||
Gregory Szorc
|
r29411 | # is insecure. We allow the connection and abort during | ||
# validation (once we have the fingerprint to print to the | ||||
# user). | ||||
Augie Fackler
|
r43347 | s[b'verifymode'] = ssl.CERT_NONE | ||
Gregory Szorc
|
r29260 | |||
Augie Fackler
|
r43347 | assert s[b'protocol'] is not None | ||
assert s[b'ctxoptions'] is not None | ||||
assert s[b'verifymode'] is not None | ||||
Gregory Szorc
|
r29259 | |||
Gregory Szorc
|
r29258 | return s | ||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r29559 | def protocolsettings(protocol): | ||
Gregory Szorc
|
r29618 | """Resolve the protocol for a config value. | ||
Returns a 3-tuple of (protocol, options, ui value) where the first | ||||
2 items are values used by SSLContext and the last is a string value | ||||
of the ``minimumprotocol`` config option equivalent. | ||||
""" | ||||
Gregory Szorc
|
r29559 | if protocol not in configprotocols: | ||
Augie Fackler
|
r43347 | raise ValueError(b'protocol value not supported: %s' % protocol) | ||
Gregory Szorc
|
r29559 | |||
Gregory Szorc
|
r29578 | # 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 | ||||
# 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. | ||||
Augie Fackler
|
r43347 | if supportedprotocols == {b'tls1.0'}: | ||
if protocol != b'tls1.0': | ||||
Augie Fackler
|
r43346 | raise error.Abort( | ||
Martin von Zweigbergk
|
r43387 | _(b'current Python does not support protocol setting %s') | ||
Augie Fackler
|
r43346 | % protocol, | ||
hint=_( | ||||
Augie Fackler
|
r43347 | b'upgrade Python or disable setting since ' | ||
b'only TLS 1.0 is supported' | ||||
Augie Fackler
|
r43346 | ), | ||
) | ||||
Gregory Szorc
|
r29559 | |||
Augie Fackler
|
r43347 | return ssl.PROTOCOL_TLSv1, 0, b'tls1.0' | ||
Gregory Szorc
|
r29559 | |||
# 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 | ||||
Augie Fackler
|
r43347 | if protocol == b'tls1.0': | ||
Gregory Szorc
|
r29559 | # Defaults above are to use TLS 1.0+ | ||
pass | ||||
Augie Fackler
|
r43347 | elif protocol == b'tls1.1': | ||
Gregory Szorc
|
r29559 | options |= ssl.OP_NO_TLSv1 | ||
Augie Fackler
|
r43347 | elif protocol == b'tls1.2': | ||
Gregory Szorc
|
r29559 | options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 | ||
else: | ||||
Augie Fackler
|
r43347 | raise error.Abort(_(b'this should not happen')) | ||
Gregory Szorc
|
r29559 | |||
# Prevent CRIME. | ||||
# There is no guarantee this attribute is defined on the module. | ||||
options |= getattr(ssl, 'OP_NO_COMPRESSION', 0) | ||||
Gregory Szorc
|
r29618 | return ssl.PROTOCOL_SSLv23, options, protocol | ||
Gregory Szorc
|
r29559 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r29249 | def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None): | ||
Gregory Szorc
|
r28653 | """Add SSL/TLS to a socket. | ||
This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane | ||||
choices based on what security options are available. | ||||
In addition to the arguments supported by ``ssl.wrap_socket``, we allow | ||||
the following additional arguments: | ||||
* serverhostname - The expected hostname of the remote server. If the | ||||
server (and client) support SNI, this tells the server which certificate | ||||
to use. | ||||
""" | ||||
Gregory Szorc
|
r29224 | if not serverhostname: | ||
Augie Fackler
|
r43347 | raise error.Abort(_(b'serverhostname argument is required')) | ||
Gregory Szorc
|
r29224 | |||
Augie Fackler
|
r42455 | if b'SSLKEYLOGFILE' in encoding.environ: | ||
try: | ||||
import sslkeylog | ||||
Augie Fackler
|
r43346 | |||
sslkeylog.set_keylog( | ||||
pycompat.fsdecode(encoding.environ[b'SSLKEYLOGFILE']) | ||||
) | ||||
Augie Fackler
|
r43350 | ui.warnnoi18n( | ||
Augie Fackler
|
r43346 | b'sslkeylog enabled by SSLKEYLOGFILE environment variable\n' | ||
) | ||||
Augie Fackler
|
r42455 | except ImportError: | ||
Augie Fackler
|
r43350 | ui.warnnoi18n( | ||
Augie Fackler
|
r43346 | b'sslkeylog module missing, ' | ||
b'but SSLKEYLOGFILE set in environment\n' | ||||
) | ||||
Augie Fackler
|
r42455 | |||
Gregory Szorc
|
r33381 | for f in (keyfile, certfile): | ||
if f and not os.path.exists(f): | ||||
Augie Fackler
|
r36762 | raise error.Abort( | ||
Augie Fackler
|
r43347 | _(b'certificate file (%s) does not exist; cannot connect to %s') | ||
Augie Fackler
|
r36762 | % (f, pycompat.bytesurl(serverhostname)), | ||
Augie Fackler
|
r43346 | hint=_( | ||
Augie Fackler
|
r43347 | b'restore missing file or fix references ' | ||
b'in Mercurial config' | ||||
Augie Fackler
|
r43346 | ), | ||
) | ||||
Gregory Szorc
|
r33381 | |||
Gregory Szorc
|
r29259 | settings = _hostsettings(ui, serverhostname) | ||
Gregory Szorc
|
r29249 | |||
Gregory Szorc
|
r29557 | # We can't use ssl.create_default_context() because it calls | ||
# load_default_certs() unless CA arguments are passed to it. We want to | ||||
# have explicit control over CA loading because implicitly loading | ||||
# CAs may undermine the user's intent. For example, a user may define a CA | ||||
# bundle with a specific CA cert removed. If the system/default CA bundle | ||||
# is loaded and contains that removed CA, you've just undone the user's | ||||
# choice. | ||||
Augie Fackler
|
r43347 | sslcontext = SSLContext(settings[b'protocol']) | ||
Gregory Szorc
|
r29507 | |||
Gregory Szorc
|
r29508 | # This is a no-op unless using modern ssl. | ||
Augie Fackler
|
r43347 | sslcontext.options |= settings[b'ctxoptions'] | ||
Gregory Szorc
|
r28651 | |||
Gregory Szorc
|
r28848 | # This still works on our fake SSLContext. | ||
Augie Fackler
|
r43347 | sslcontext.verify_mode = settings[b'verifymode'] | ||
Gregory Szorc
|
r28848 | |||
Augie Fackler
|
r43347 | if settings[b'ciphers']: | ||
Gregory Szorc
|
r29577 | try: | ||
Augie Fackler
|
r43347 | sslcontext.set_ciphers(pycompat.sysstr(settings[b'ciphers'])) | ||
Gregory Szorc
|
r29577 | except ssl.SSLError as e: | ||
Augie Fackler
|
r36762 | raise error.Abort( | ||
Augie Fackler
|
r43347 | _(b'could not set ciphers: %s') | ||
Yuya Nishihara
|
r37102 | % stringutil.forcebytestr(e.args[0]), | ||
Augie Fackler
|
r43347 | hint=_(b'change cipher string (%s) in config') | ||
% settings[b'ciphers'], | ||||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r29577 | |||
Gregory Szorc
|
r28652 | if certfile is not None: | ||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r28652 | def password(): | ||
f = keyfile or certfile | ||||
Augie Fackler
|
r43347 | return ui.getpass(_(b'passphrase for %s: ') % f, b'') | ||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r28652 | sslcontext.load_cert_chain(certfile, keyfile, password) | ||
Gregory Szorc
|
r28848 | |||
Augie Fackler
|
r43347 | if settings[b'cafile'] is not None: | ||
Gregory Szorc
|
r29446 | try: | ||
Augie Fackler
|
r43347 | sslcontext.load_verify_locations(cafile=settings[b'cafile']) | ||
Gregory Szorc
|
r29446 | except ssl.SSLError as e: | ||
Augie Fackler
|
r43346 | if len(e.args) == 1: # pypy has different SSLError args | ||
Pierre-Yves David
|
r29927 | msg = e.args[0] | ||
else: | ||||
msg = e.args[1] | ||||
Augie Fackler
|
r43346 | raise error.Abort( | ||
Augie Fackler
|
r43347 | _(b'error loading CA file %s: %s') | ||
% (settings[b'cafile'], stringutil.forcebytestr(msg)), | ||||
hint=_(b'file is empty or malformed?'), | ||||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r29113 | caloaded = True | ||
Augie Fackler
|
r43347 | elif settings[b'allowloaddefaultcerts']: | ||
Gregory Szorc
|
r28652 | # This is a no-op on old Python. | ||
sslcontext.load_default_certs() | ||||
Gregory Szorc
|
r29288 | caloaded = True | ||
else: | ||||
caloaded = False | ||||
Alex Orange
|
r23834 | |||
Gregory Szorc
|
r29449 | try: | ||
sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname) | ||||
Gregory Szorc
|
r29559 | except ssl.SSLError as e: | ||
Gregory Szorc
|
r29449 | # 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. | ||||
# Only modern ssl module exposes SSLContext.get_ca_certs() so we can | ||||
# only show this warning if modern ssl is available. | ||||
Matt Harbison
|
r31725 | # The exception handler is here to handle bugs around cert attributes: | ||
# https://bugs.python.org/issue20916#msg213479. (See issues5313.) | ||||
# When the main 20916 bug occurs, 'sslcontext.get_ca_certs()' is a | ||||
# non-empty list, but the following conditional is otherwise True. | ||||
Gregory Szorc
|
r29631 | try: | ||
Augie Fackler
|
r43346 | if ( | ||
caloaded | ||||
Augie Fackler
|
r43347 | and settings[b'verifymode'] == ssl.CERT_REQUIRED | ||
Augie Fackler
|
r43346 | and modernssl | ||
and not sslcontext.get_ca_certs() | ||||
): | ||||
ui.warn( | ||||
_( | ||||
Augie Fackler
|
r43347 | b'(an attempt was made to load CA certificates but ' | ||
b'none were loaded; see ' | ||||
b'https://mercurial-scm.org/wiki/SecureConnections ' | ||||
b'for how to configure Mercurial to avoid this ' | ||||
b'error)\n' | ||||
Augie Fackler
|
r43346 | ) | ||
) | ||||
Gregory Szorc
|
r29631 | except ssl.SSLError: | ||
pass | ||||
Gregory Szorc
|
r41455 | |||
Gregory Szorc
|
r29559 | # Try to print more helpful error messages for known failures. | ||
Augie Fackler
|
r43347 | if util.safehasattr(e, b'reason'): | ||
Gregory Szorc
|
r29619 | # This error occurs when the client and server don't share a | ||
# common/supported SSL/TLS protocol. We've disabled SSLv2 and SSLv3 | ||||
# outright. Hopefully the reason for this error is that we require | ||||
# TLS 1.1+ and the server only supports TLS 1.0. Whatever the | ||||
# reason, try to emit an actionable warning. | ||||
Augie Fackler
|
r43906 | if e.reason == 'UNSUPPORTED_PROTOCOL': | ||
Gregory Szorc
|
r29619 | # We attempted TLS 1.0+. | ||
Augie Fackler
|
r43347 | if settings[b'protocolui'] == b'tls1.0': | ||
Gregory Szorc
|
r29619 | # We support more than just TLS 1.0+. If this happens, | ||
# the likely scenario is either the client or the server | ||||
# is really old. (e.g. server doesn't support TLS 1.0+ or | ||||
# client doesn't support modern TLS versions introduced | ||||
# several years from when this comment was written). | ||||
Augie Fackler
|
r43347 | if supportedprotocols != {b'tls1.0'}: | ||
Augie Fackler
|
r43346 | ui.warn( | ||
_( | ||||
Augie Fackler
|
r43347 | b'(could not communicate with %s using security ' | ||
b'protocols %s; if you are using a modern Mercurial ' | ||||
b'version, consider contacting the operator of this ' | ||||
b'server; see ' | ||||
b'https://mercurial-scm.org/wiki/SecureConnections ' | ||||
b'for more info)\n' | ||||
Augie Fackler
|
r43346 | ) | ||
% ( | ||||
Gregory Szorc
|
r41456 | pycompat.bytesurl(serverhostname), | ||
Augie Fackler
|
r43347 | b', '.join(sorted(supportedprotocols)), | ||
Augie Fackler
|
r43346 | ) | ||
) | ||||
Gregory Szorc
|
r29619 | else: | ||
Augie Fackler
|
r43346 | ui.warn( | ||
_( | ||||
Augie Fackler
|
r43347 | b'(could not communicate with %s using TLS 1.0; the ' | ||
b'likely cause of this is the server no longer ' | ||||
b'supports TLS 1.0 because it has known security ' | ||||
b'vulnerabilities; see ' | ||||
b'https://mercurial-scm.org/wiki/SecureConnections ' | ||||
b'for more info)\n' | ||||
Augie Fackler
|
r43346 | ) | ||
% pycompat.bytesurl(serverhostname) | ||||
) | ||||
Gregory Szorc
|
r29619 | else: | ||
# We attempted TLS 1.1+. We can only get here if the client | ||||
# supports the configured protocol. So the likely reason is | ||||
# the client wants better security than the server can | ||||
# offer. | ||||
Augie Fackler
|
r43346 | ui.warn( | ||
_( | ||||
Augie Fackler
|
r43347 | b'(could not negotiate a common security protocol (%s+) ' | ||
b'with %s; the likely cause is Mercurial is configured ' | ||||
b'to be more secure than the server can support)\n' | ||||
Augie Fackler
|
r43346 | ) | ||
% ( | ||||
Augie Fackler
|
r43347 | settings[b'protocolui'], | ||
Augie Fackler
|
r43346 | pycompat.bytesurl(serverhostname), | ||
) | ||||
) | ||||
ui.warn( | ||||
_( | ||||
Augie Fackler
|
r43347 | b'(consider contacting the operator of this ' | ||
b'server and ask them to support modern TLS ' | ||||
b'protocol versions; or, set ' | ||||
b'hostsecurity.%s:minimumprotocol=tls1.0 to allow ' | ||||
b'use of legacy, less secure protocols when ' | ||||
b'communicating with this server)\n' | ||||
Augie Fackler
|
r43346 | ) | ||
% pycompat.bytesurl(serverhostname) | ||||
) | ||||
ui.warn( | ||||
_( | ||||
Augie Fackler
|
r43347 | b'(see https://mercurial-scm.org/wiki/SecureConnections ' | ||
b'for more info)\n' | ||||
Augie Fackler
|
r43346 | ) | ||
) | ||||
Matt Harbison
|
r33494 | |||
Augie Fackler
|
r43906 | elif e.reason == 'CERTIFICATE_VERIFY_FAILED' and pycompat.iswindows: | ||
Matt Harbison
|
r33494 | |||
Augie Fackler
|
r43346 | ui.warn( | ||
_( | ||||
Augie Fackler
|
r43347 | b'(the full certificate chain may not be available ' | ||
b'locally; see "hg help debugssl")\n' | ||||
Augie Fackler
|
r43346 | ) | ||
) | ||||
Gregory Szorc
|
r29449 | raise | ||
Gregory Szorc
|
r28652 | # check if wrap_socket failed silently because socket had been | ||
# closed | ||||
# - see http://bugs.python.org/issue13721 | ||||
if not sslsocket.cipher(): | ||||
Augie Fackler
|
r43347 | raise error.Abort(_(b'ssl connection failed')) | ||
Gregory Szorc
|
r29113 | |||
Gregory Szorc
|
r29225 | sslsocket._hgstate = { | ||
Augie Fackler
|
r43347 | b'caloaded': caloaded, | ||
b'hostname': serverhostname, | ||||
b'settings': settings, | ||||
b'ui': ui, | ||||
Gregory Szorc
|
r29225 | } | ||
Gregory Szorc
|
r29113 | |||
Gregory Szorc
|
r28652 | return sslsocket | ||
Augie Fackler
|
r14204 | |||
Augie Fackler
|
r43346 | |||
def wrapserversocket( | ||||
sock, ui, certfile=None, keyfile=None, cafile=None, requireclientcert=False | ||||
): | ||||
Gregory Szorc
|
r29554 | """Wrap a socket for use by servers. | ||
``certfile`` and ``keyfile`` specify the files containing the certificate's | ||||
public and private keys, respectively. Both keys can be defined in the same | ||||
file via ``certfile`` (the private key must come first in the file). | ||||
``cafile`` defines the path to certificate authorities. | ||||
``requireclientcert`` specifies whether to require client certificates. | ||||
Typically ``cafile`` is only defined if ``requireclientcert`` is true. | ||||
""" | ||||
Gregory Szorc
|
r33381 | # This function is not used much by core Mercurial, so the error messaging | ||
# doesn't have to be as detailed as for wrapsocket(). | ||||
for f in (certfile, keyfile, cafile): | ||||
if f and not os.path.exists(f): | ||||
Augie Fackler
|
r43346 | raise error.Abort( | ||
Martin von Zweigbergk
|
r43387 | _(b'referenced certificate file (%s) does not exist') % f | ||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r33381 | |||
Augie Fackler
|
r43347 | protocol, options, _protocolui = protocolsettings(b'tls1.0') | ||
Gregory Szorc
|
r29559 | |||
# This config option is intended for use in tests only. It is a giant | ||||
# footgun to kill security. Don't define it. | ||||
Augie Fackler
|
r43347 | exactprotocol = ui.config(b'devel', b'serverexactprotocol') | ||
if exactprotocol == b'tls1.0': | ||||
Gregory Szorc
|
r29559 | protocol = ssl.PROTOCOL_TLSv1 | ||
Augie Fackler
|
r43347 | elif exactprotocol == b'tls1.1': | ||
if b'tls1.1' not in supportedprotocols: | ||||
raise error.Abort(_(b'TLS 1.1 not supported by this Python')) | ||||
Gregory Szorc
|
r29559 | protocol = ssl.PROTOCOL_TLSv1_1 | ||
Augie Fackler
|
r43347 | elif exactprotocol == b'tls1.2': | ||
if b'tls1.2' not in supportedprotocols: | ||||
raise error.Abort(_(b'TLS 1.2 not supported by this Python')) | ||||
Gregory Szorc
|
r29559 | protocol = ssl.PROTOCOL_TLSv1_2 | ||
elif exactprotocol: | ||||
Augie Fackler
|
r43346 | raise error.Abort( | ||
Augie Fackler
|
r43347 | _(b'invalid value for serverexactprotocol: %s') % exactprotocol | ||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r29559 | |||
Gregory Szorc
|
r29554 | if modernssl: | ||
# We /could/ use create_default_context() here since it doesn't load | ||||
Gregory Szorc
|
r29559 | # 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 | ||||
Gregory Szorc
|
r29554 | # Improve forward secrecy. | ||
sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0) | ||||
sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0) | ||||
# Use the list of more secure ciphers if found in the ssl module. | ||||
Augie Fackler
|
r43347 | if util.safehasattr(ssl, b'_RESTRICTED_SERVER_CIPHERS'): | ||
Gregory Szorc
|
r29554 | sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0) | ||
sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS) | ||||
else: | ||||
sslcontext = SSLContext(ssl.PROTOCOL_TLSv1) | ||||
if requireclientcert: | ||||
sslcontext.verify_mode = ssl.CERT_REQUIRED | ||||
else: | ||||
sslcontext.verify_mode = ssl.CERT_NONE | ||||
if certfile or keyfile: | ||||
sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile) | ||||
if cafile: | ||||
sslcontext.load_verify_locations(cafile=cafile) | ||||
return sslcontext.wrap_socket(sock, server_side=True) | ||||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r29452 | class wildcarderror(Exception): | ||
"""Represents an error parsing wildcards in DNS name.""" | ||||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r29452 | def _dnsnamematch(dn, hostname, maxwildcards=1): | ||
"""Match DNS names according RFC 6125 section 6.4.3. | ||||
This code is effectively copied from CPython's ssl._dnsname_match. | ||||
Returns a bool indicating whether the expected hostname matches | ||||
the value in ``dn``. | ||||
""" | ||||
pats = [] | ||||
if not dn: | ||||
return False | ||||
Augie Fackler
|
r36760 | dn = pycompat.bytesurl(dn) | ||
hostname = pycompat.bytesurl(hostname) | ||||
Gregory Szorc
|
r29452 | |||
Augie Fackler
|
r43347 | pieces = dn.split(b'.') | ||
Gregory Szorc
|
r29452 | leftmost = pieces[0] | ||
remainder = pieces[1:] | ||||
Augie Fackler
|
r43347 | wildcards = leftmost.count(b'*') | ||
Gregory Szorc
|
r29452 | if wildcards > maxwildcards: | ||
raise wildcarderror( | ||||
Augie Fackler
|
r43347 | _(b'too many wildcards in certificate DNS name: %s') % dn | ||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r29452 | |||
# speed up common case w/o wildcards | ||||
if not wildcards: | ||||
return dn.lower() == hostname.lower() | ||||
# RFC 6125, section 6.4.3, subitem 1. | ||||
# The client SHOULD NOT attempt to match a presented identifier in which | ||||
# the wildcard character comprises a label other than the left-most label. | ||||
Augie Fackler
|
r43347 | if leftmost == b'*': | ||
Gregory Szorc
|
r29452 | # When '*' is a fragment by itself, it matches a non-empty dotless | ||
# fragment. | ||||
Augie Fackler
|
r43347 | pats.append(b'[^.]+') | ||
elif leftmost.startswith(b'xn--') or hostname.startswith(b'xn--'): | ||||
Gregory Szorc
|
r29452 | # RFC 6125, section 6.4.3, subitem 3. | ||
# The client SHOULD NOT attempt to match a presented identifier | ||||
# where the wildcard character is embedded within an A-label or | ||||
# U-label of an internationalized domain name. | ||||
Augie Fackler
|
r38494 | pats.append(stringutil.reescape(leftmost)) | ||
Gregory Szorc
|
r29452 | else: | ||
# Otherwise, '*' matches any dotless string, e.g. www* | ||||
Augie Fackler
|
r43347 | pats.append(stringutil.reescape(leftmost).replace(br'\*', b'[^.]*')) | ||
Gregory Szorc
|
r29452 | |||
# add the remaining fragments, ignore any wildcards | ||||
for frag in remainder: | ||||
Augie Fackler
|
r38494 | pats.append(stringutil.reescape(frag)) | ||
Gregory Szorc
|
r29452 | |||
Pulkit Goyal
|
r37684 | pat = re.compile(br'\A' + br'\.'.join(pats) + br'\Z', re.IGNORECASE) | ||
Gregory Szorc
|
r29452 | return pat.match(hostname) is not None | ||
Augie Fackler
|
r43346 | |||
Augie Fackler
|
r14204 | def _verifycert(cert, hostname): | ||
'''Verify that cert (in socket.getpeercert() format) matches hostname. | ||||
CRLs is not handled. | ||||
Returns error message if any problems are found and None on success. | ||||
''' | ||||
if not cert: | ||||
Augie Fackler
|
r43347 | return _(b'no certificate received') | ||
Augie Fackler
|
r14204 | |||
Gregory Szorc
|
r29452 | dnsnames = [] | ||
Augie Fackler
|
r43906 | san = cert.get('subjectAltName', []) | ||
Gregory Szorc
|
r29452 | for key, value in san: | ||
Augie Fackler
|
r43906 | if key == 'DNS': | ||
Gregory Szorc
|
r29452 | try: | ||
if _dnsnamematch(value, hostname): | ||||
return | ||||
except wildcarderror as e: | ||||
Yuya Nishihara
|
r37102 | return stringutil.forcebytestr(e.args[0]) | ||
Gregory Szorc
|
r29452 | |||
dnsnames.append(value) | ||||
Augie Fackler
|
r14204 | |||
Gregory Szorc
|
r29452 | if not dnsnames: | ||
# The subject is only checked when there is no DNS in subjectAltName. | ||||
Augie Fackler
|
r43906 | for sub in cert.get('subject', []): | ||
Gregory Szorc
|
r29452 | for key, value in sub: | ||
# According to RFC 2818 the most specific Common Name must | ||||
# be used. | ||||
Augie Fackler
|
r43906 | if key == 'commonName': | ||
Mads Kiilerich
|
r30332 | # 'subject' entries are unicode. | ||
Gregory Szorc
|
r29452 | try: | ||
value = value.encode('ascii') | ||||
except UnicodeEncodeError: | ||||
Augie Fackler
|
r43347 | return _(b'IDN in certificate not supported') | ||
Gregory Szorc
|
r29452 | |||
try: | ||||
if _dnsnamematch(value, hostname): | ||||
return | ||||
except wildcarderror as e: | ||||
Yuya Nishihara
|
r37102 | return stringutil.forcebytestr(e.args[0]) | ||
Gregory Szorc
|
r29452 | |||
dnsnames.append(value) | ||||
Augie Fackler
|
r37889 | dnsnames = [pycompat.bytesurl(d) for d in dnsnames] | ||
Gregory Szorc
|
r29452 | if len(dnsnames) > 1: | ||
Augie Fackler
|
r43347 | return _(b'certificate is for %s') % b', '.join(dnsnames) | ||
Gregory Szorc
|
r29452 | elif len(dnsnames) == 1: | ||
Augie Fackler
|
r43347 | return _(b'certificate is for %s') % dnsnames[0] | ||
Gregory Szorc
|
r29452 | else: | ||
Augie Fackler
|
r43347 | return _(b'no commonName or subjectAltName found in certificate') | ||
Augie Fackler
|
r14204 | |||
Augie Fackler
|
r43346 | |||
Mads Kiilerich
|
r23042 | def _plainapplepython(): | ||
"""return true if this seems to be a pure Apple Python that | ||||
* is unfrozen and presumably has the whole mercurial module in the file | ||||
system | ||||
* presumably is an Apple Python that uses Apple OpenSSL which has patches | ||||
for using system certificate store CAs in addition to the provided | ||||
cacerts file | ||||
""" | ||||
Augie Fackler
|
r43346 | if ( | ||
not pycompat.isdarwin | ||||
Martin von Zweigbergk
|
r44067 | or resourceutil.mainfrozen() | ||
Augie Fackler
|
r43346 | or not pycompat.sysexecutable | ||
): | ||||
Mads Kiilerich
|
r23042 | return False | ||
Pulkit Goyal
|
r30669 | exe = os.path.realpath(pycompat.sysexecutable).lower() | ||
Augie Fackler
|
r43347 | return exe.startswith(b'/usr/bin/python') or exe.startswith( | ||
b'/system/library/frameworks/python.framework/' | ||||
Augie Fackler
|
r43346 | ) | ||
Mads Kiilerich
|
r23042 | |||
Gregory Szorc
|
r29500 | _systemcacertpaths = [ | ||
# RHEL, CentOS, and Fedora | ||||
Augie Fackler
|
r43347 | b'/etc/pki/tls/certs/ca-bundle.trust.crt', | ||
Gregory Szorc
|
r29500 | # Debian, Ubuntu, Gentoo | ||
Augie Fackler
|
r43347 | b'/etc/ssl/certs/ca-certificates.crt', | ||
Gregory Szorc
|
r29500 | ] | ||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r29483 | def _defaultcacerts(ui): | ||
Gregory Szorc
|
r29488 | """return path to default CA certificates or None. | ||
It is assumed this function is called when the returned certificates | ||||
file will actually be used to validate connections. Therefore this | ||||
function may print warnings or debug messages assuming this usage. | ||||
Gregory Szorc
|
r29500 | |||
We don't print a message when the Python is able to load default | ||||
CA certs because this scenario is detected at socket connect time. | ||||
Gregory Szorc
|
r29488 | """ | ||
Gábor Stefanik
|
r30228 | # The "certifi" Python package provides certificates. If it is installed | ||
# and usable, assume the user intends it to be used and use it. | ||||
Gregory Szorc
|
r29486 | try: | ||
import certifi | ||||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r29486 | certs = certifi.where() | ||
Gábor Stefanik
|
r30228 | if os.path.exists(certs): | ||
Augie Fackler
|
r43347 | ui.debug(b'using ca certificates from certifi\n') | ||
Augie Fackler
|
r42450 | return pycompat.fsencode(certs) | ||
Gábor Stefanik
|
r30228 | except (ImportError, AttributeError): | ||
Gregory Szorc
|
r29486 | pass | ||
Gregory Szorc
|
r29489 | # On Windows, only the modern ssl module is capable of loading the system | ||
# CA certificates. If we're not capable of doing that, emit a warning | ||||
# because we'll get a certificate verification error later and the lack | ||||
# of loaded CA certificates will be the reason why. | ||||
# Assertion: this code is only called if certificates are being verified. | ||||
Jun Wu
|
r34646 | if pycompat.iswindows: | ||
Gregory Szorc
|
r29489 | if not _canloaddefaultcerts: | ||
Augie Fackler
|
r43346 | ui.warn( | ||
_( | ||||
Augie Fackler
|
r43347 | b'(unable to load Windows CA certificates; see ' | ||
b'https://mercurial-scm.org/wiki/SecureConnections for ' | ||||
b'how to configure Mercurial to avoid this message)\n' | ||||
Augie Fackler
|
r43346 | ) | ||
) | ||||
Gregory Szorc
|
r29489 | |||
return None | ||||
Gregory Szorc
|
r29487 | # Apple's OpenSSL has patches that allow a specially constructed certificate | ||
# to load the system CA store. If we're running on Apple Python, use this | ||||
# trick. | ||||
Yuya Nishihara
|
r24288 | if _plainapplepython(): | ||
Pulkit Goyal
|
r31074 | dummycert = os.path.join( | ||
Augie Fackler
|
r43347 | os.path.dirname(pycompat.fsencode(__file__)), b'dummycert.pem' | ||
Augie Fackler
|
r43346 | ) | ||
Yuya Nishihara
|
r24288 | if os.path.exists(dummycert): | ||
return dummycert | ||||
Gregory Szorc
|
r29107 | |||
Gregory Szorc
|
r29499 | # The Apple OpenSSL trick isn't available to us. If Python isn't able to | ||
# load system certs, we're out of luck. | ||||
Jun Wu
|
r34648 | if pycompat.isdarwin: | ||
Gregory Szorc
|
r29499 | # FUTURE Consider looking for Homebrew or MacPorts installed certs | ||
# files. Also consider exporting the keychain certs to a file during | ||||
# Mercurial install. | ||||
if not _canloaddefaultcerts: | ||||
Augie Fackler
|
r43346 | ui.warn( | ||
_( | ||||
Augie Fackler
|
r43347 | b'(unable to load CA certificates; see ' | ||
b'https://mercurial-scm.org/wiki/SecureConnections for ' | ||||
b'how to configure Mercurial to avoid this message)\n' | ||||
Augie Fackler
|
r43346 | ) | ||
) | ||||
Yuya Nishihara
|
r24291 | return None | ||
Yuya Nishihara
|
r24288 | |||
Gregory Szorc
|
r29537 | # / is writable on Windows. Out of an abundance of caution make sure | ||
# we're not on Windows because paths from _systemcacerts could be installed | ||||
# by non-admin users. | ||||
Jun Wu
|
r34646 | assert not pycompat.iswindows | ||
Gregory Szorc
|
r29537 | |||
Gregory Szorc
|
r29500 | # Try to find CA certificates in well-known locations. We print a warning | ||
# when using a found file because we don't want too much silent magic | ||||
# for security settings. The expectation is that proper Mercurial | ||||
# installs will have the CA certs path defined at install time and the | ||||
# installer/packager will make an appropriate decision on the user's | ||||
# behalf. We only get here and perform this setting as a feature of | ||||
# last resort. | ||||
if not _canloaddefaultcerts: | ||||
for path in _systemcacertpaths: | ||||
if os.path.isfile(path): | ||||
Augie Fackler
|
r43346 | ui.warn( | ||
_( | ||||
Augie Fackler
|
r43347 | b'(using CA certificates from %s; if you see this ' | ||
b'message, your Mercurial install is not properly ' | ||||
b'configured; see ' | ||||
b'https://mercurial-scm.org/wiki/SecureConnections ' | ||||
b'for how to configure Mercurial to avoid this ' | ||||
b'message)\n' | ||||
Augie Fackler
|
r43346 | ) | ||
% path | ||||
) | ||||
Gregory Szorc
|
r29500 | return path | ||
Augie Fackler
|
r14204 | |||
Augie Fackler
|
r43346 | ui.warn( | ||
_( | ||||
Augie Fackler
|
r43347 | b'(unable to load CA certificates; see ' | ||
b'https://mercurial-scm.org/wiki/SecureConnections for ' | ||||
b'how to configure Mercurial to avoid this message)\n' | ||||
Augie Fackler
|
r43346 | ) | ||
) | ||||
Augie Fackler
|
r14204 | |||
Gregory Szorc
|
r29107 | return None | ||
Yuya Nishihara
|
r24288 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r29286 | def validatesocket(sock): | ||
Mads Kiilerich
|
r30332 | """Validate a socket meets security requirements. | ||
Matt Mackall
|
r18879 | |||
Gregory Szorc
|
r29227 | The passed socket must have been created with ``wrapsocket()``. | ||
""" | ||||
Augie Fackler
|
r43347 | shost = sock._hgstate[b'hostname'] | ||
Augie Fackler
|
r36760 | host = pycompat.bytesurl(shost) | ||
Augie Fackler
|
r43347 | ui = sock._hgstate[b'ui'] | ||
settings = sock._hgstate[b'settings'] | ||||
Matt Mackall
|
r18879 | |||
Gregory Szorc
|
r29227 | try: | ||
peercert = sock.getpeercert(True) | ||||
peercert2 = sock.getpeercert() | ||||
except AttributeError: | ||||
Augie Fackler
|
r43347 | raise error.Abort(_(b'%s ssl connection error') % host) | ||
Yuya Nishihara
|
r24288 | |||
Gregory Szorc
|
r29227 | if not peercert: | ||
Augie Fackler
|
r43346 | raise error.Abort( | ||
Martin von Zweigbergk
|
r43387 | _(b'%s certificate error: no certificate received') % host | ||
Augie Fackler
|
r43346 | ) | ||
Matt Mackall
|
r18879 | |||
Augie Fackler
|
r43347 | if settings[b'disablecertverification']: | ||
Gregory Szorc
|
r29289 | # We don't print the certificate fingerprint because it shouldn't | ||
# be necessary: if the user requested certificate verification be | ||||
# disabled, they presumably already saw a message about the inability | ||||
# to verify the certificate and this message would have printed the | ||||
# fingerprint. So printing the fingerprint here adds little to no | ||||
# value. | ||||
Augie Fackler
|
r43346 | ui.warn( | ||
_( | ||||
Augie Fackler
|
r43347 | b'warning: connection security to %s is disabled per current ' | ||
b'settings; communication is susceptible to eavesdropping ' | ||||
b'and tampering\n' | ||||
Augie Fackler
|
r43346 | ) | ||
% host | ||||
) | ||||
Gregory Szorc
|
r29289 | return | ||
Matt Mackall
|
r18879 | |||
Gregory Szorc
|
r29227 | # If a certificate fingerprint is pinned, use it and only it to | ||
# validate the remote cert. | ||||
Gregory Szorc
|
r29262 | peerfingerprints = { | ||
Augie Fackler
|
r44518 | b'sha1': node.hex(hashutil.sha1(peercert).digest()), | ||
Augie Fackler
|
r43347 | b'sha256': node.hex(hashlib.sha256(peercert).digest()), | ||
b'sha512': node.hex(hashlib.sha512(peercert).digest()), | ||||
Gregory Szorc
|
r29262 | } | ||
Matt Mackall
|
r18879 | |||
Gregory Szorc
|
r29290 | def fmtfingerprint(s): | ||
Augie Fackler
|
r43347 | return b':'.join([s[x : x + 2] for x in range(0, len(s), 2)]) | ||
Gregory Szorc
|
r29290 | |||
Augie Fackler
|
r43347 | nicefingerprint = b'sha256:%s' % fmtfingerprint(peerfingerprints[b'sha256']) | ||
Gregory Szorc
|
r28850 | |||
Augie Fackler
|
r43347 | if settings[b'certfingerprints']: | ||
for hash, fingerprint in settings[b'certfingerprints']: | ||||
Gregory Szorc
|
r29262 | if peerfingerprints[hash].lower() == fingerprint: | ||
Augie Fackler
|
r43346 | ui.debug( | ||
Augie Fackler
|
r43347 | b'%s certificate matched fingerprint %s:%s\n' | ||
Augie Fackler
|
r43346 | % (host, hash, fmtfingerprint(fingerprint)) | ||
) | ||||
Augie Fackler
|
r43347 | if settings[b'legacyfingerprint']: | ||
Augie Fackler
|
r43346 | ui.warn( | ||
_( | ||||
Augie Fackler
|
r43347 | b'(SHA-1 fingerprint for %s found in legacy ' | ||
b'[hostfingerprints] section; ' | ||||
b'if you trust this fingerprint, remove the old ' | ||||
b'SHA-1 fingerprint from [hostfingerprints] and ' | ||||
b'add the following entry to the new ' | ||||
b'[hostsecurity] section: %s:fingerprints=%s)\n' | ||||
Augie Fackler
|
r43346 | ) | ||
% (host, host, nicefingerprint) | ||||
) | ||||
Gregory Szorc
|
r29291 | return | ||
Gregory Szorc
|
r28850 | |||
Gregory Szorc
|
r29293 | # Pinned fingerprint didn't match. This is a fatal error. | ||
Augie Fackler
|
r43347 | if settings[b'legacyfingerprint']: | ||
section = b'hostfingerprint' | ||||
nice = fmtfingerprint(peerfingerprints[b'sha1']) | ||||
Gregory Szorc
|
r29293 | else: | ||
Augie Fackler
|
r43347 | section = b'hostsecurity' | ||
nice = b'%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash])) | ||||
Augie Fackler
|
r43346 | raise error.Abort( | ||
Martin von Zweigbergk
|
r43387 | _(b'certificate for %s has unexpected fingerprint %s') | ||
Augie Fackler
|
r43346 | % (host, nice), | ||
Augie Fackler
|
r43347 | hint=_(b'check %s configuration') % section, | ||
Augie Fackler
|
r43346 | ) | ||
Gregory Szorc
|
r28850 | |||
Gregory Szorc
|
r29411 | # Security is enabled but no CAs are loaded. We can't establish trust | ||
# for the cert so abort. | ||||
Augie Fackler
|
r43347 | if not sock._hgstate[b'caloaded']: | ||
Gregory Szorc
|
r29411 | raise error.Abort( | ||
Augie Fackler
|
r43346 | _( | ||
Augie Fackler
|
r43347 | b'unable to verify security of %s (no loaded CA certificates); ' | ||
b'refusing to connect' | ||||
Augie Fackler
|
r43346 | ) | ||
% host, | ||||
hint=_( | ||||
Augie Fackler
|
r43347 | b'see https://mercurial-scm.org/wiki/SecureConnections for ' | ||
b'how to configure Mercurial to avoid this error or set ' | ||||
b'hostsecurity.%s:fingerprints=%s to trust this server' | ||||
Augie Fackler
|
r43346 | ) | ||
% (host, nicefingerprint), | ||||
) | ||||
Gregory Szorc
|
r29113 | |||
Augie Fackler
|
r36760 | msg = _verifycert(peercert2, shost) | ||
Gregory Szorc
|
r29227 | if msg: | ||
Augie Fackler
|
r43346 | raise error.Abort( | ||
Augie Fackler
|
r43347 | _(b'%s certificate error: %s') % (host, msg), | ||
Augie Fackler
|
r43346 | hint=_( | ||
Augie Fackler
|
r43347 | b'set hostsecurity.%s:certfingerprints=%s ' | ||
b'config setting or use --insecure to connect ' | ||||
b'insecurely' | ||||
Augie Fackler
|
r43346 | ) | ||
% (host, nicefingerprint), | ||||
) | ||||