mercurial/sslutil.py
changeset 36765 424994a0adfd
parent 35582 72b91f905065
child 36766 25798cf7dc9d
equal deleted inserted replaced
36764:7a25f6cfebe8 36765:424994a0adfd
   111 def _hostsettings(ui, hostname):
   111 def _hostsettings(ui, hostname):
   112     """Obtain security settings for a hostname.
   112     """Obtain security settings for a hostname.
   113 
   113 
   114     Returns a dict of settings relevant to that hostname.
   114     Returns a dict of settings relevant to that hostname.
   115     """
   115     """
       
   116     bhostname = pycompat.bytesurl(hostname)
   116     s = {
   117     s = {
   117         # Whether we should attempt to load default/available CA certs
   118         # Whether we should attempt to load default/available CA certs
   118         # if an explicit ``cafile`` is not defined.
   119         # if an explicit ``cafile`` is not defined.
   119         'allowloaddefaultcerts': True,
   120         'allowloaddefaultcerts': True,
   120         # List of 2-tuple of (hash algorithm, hash).
   121         # List of 2-tuple of (hash algorithm, hash).
   160         # internal config: hostsecurity.disabletls10warning
   161         # internal config: hostsecurity.disabletls10warning
   161         if not ui.configbool('hostsecurity', 'disabletls10warning'):
   162         if not ui.configbool('hostsecurity', 'disabletls10warning'):
   162             ui.warn(_('warning: connecting to %s using legacy security '
   163             ui.warn(_('warning: connecting to %s using legacy security '
   163                       'technology (TLS 1.0); see '
   164                       'technology (TLS 1.0); see '
   164                       'https://mercurial-scm.org/wiki/SecureConnections for '
   165                       'https://mercurial-scm.org/wiki/SecureConnections for '
   165                       'more info\n') % hostname)
   166                       'more info\n') % bhostname)
   166         defaultprotocol = 'tls1.0'
   167         defaultprotocol = 'tls1.0'
   167 
   168 
   168     key = 'minimumprotocol'
   169     key = 'minimumprotocol'
   169     protocol = ui.config('hostsecurity', key, defaultprotocol)
   170     protocol = ui.config('hostsecurity', key, defaultprotocol)
   170     validateprotocol(protocol, key)
   171     validateprotocol(protocol, key)
   171 
   172 
   172     key = '%s:minimumprotocol' % hostname
   173     key = '%s:minimumprotocol' % bhostname
   173     protocol = ui.config('hostsecurity', key, protocol)
   174     protocol = ui.config('hostsecurity', key, protocol)
   174     validateprotocol(protocol, key)
   175     validateprotocol(protocol, key)
   175 
   176 
   176     # If --insecure is used, we allow the use of TLS 1.0 despite config options.
   177     # If --insecure is used, we allow the use of TLS 1.0 despite config options.
   177     # We always print a "connection security to %s is disabled..." message when
   178     # We always print a "connection security to %s is disabled..." message when
   180         protocol = 'tls1.0'
   181         protocol = 'tls1.0'
   181 
   182 
   182     s['protocol'], s['ctxoptions'], s['protocolui'] = protocolsettings(protocol)
   183     s['protocol'], s['ctxoptions'], s['protocolui'] = protocolsettings(protocol)
   183 
   184 
   184     ciphers = ui.config('hostsecurity', 'ciphers')
   185     ciphers = ui.config('hostsecurity', 'ciphers')
   185     ciphers = ui.config('hostsecurity', '%s:ciphers' % hostname, ciphers)
   186     ciphers = ui.config('hostsecurity', '%s:ciphers' % bhostname, ciphers)
   186     s['ciphers'] = ciphers
   187     s['ciphers'] = ciphers
   187 
   188 
   188     # Look for fingerprints in [hostsecurity] section. Value is a list
   189     # Look for fingerprints in [hostsecurity] section. Value is a list
   189     # of <alg>:<fingerprint> strings.
   190     # of <alg>:<fingerprint> strings.
   190     fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname)
   191     fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % bhostname)
   191     for fingerprint in fingerprints:
   192     for fingerprint in fingerprints:
   192         if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
   193         if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
   193             raise error.Abort(_('invalid fingerprint for %s: %s') % (
   194             raise error.Abort(_('invalid fingerprint for %s: %s') % (
   194                                 hostname, fingerprint),
   195                                 bhostname, fingerprint),
   195                               hint=_('must begin with "sha1:", "sha256:", '
   196                               hint=_('must begin with "sha1:", "sha256:", '
   196                                      'or "sha512:"'))
   197                                      'or "sha512:"'))
   197 
   198 
   198         alg, fingerprint = fingerprint.split(':', 1)
   199         alg, fingerprint = fingerprint.split(':', 1)
   199         fingerprint = fingerprint.replace(':', '').lower()
   200         fingerprint = fingerprint.replace(':', '').lower()
   200         s['certfingerprints'].append((alg, fingerprint))
   201         s['certfingerprints'].append((alg, fingerprint))
   201 
   202 
   202     # Fingerprints from [hostfingerprints] are always SHA-1.
   203     # Fingerprints from [hostfingerprints] are always SHA-1.
   203     for fingerprint in ui.configlist('hostfingerprints', hostname):
   204     for fingerprint in ui.configlist('hostfingerprints', bhostname):
   204         fingerprint = fingerprint.replace(':', '').lower()
   205         fingerprint = fingerprint.replace(':', '').lower()
   205         s['certfingerprints'].append(('sha1', fingerprint))
   206         s['certfingerprints'].append(('sha1', fingerprint))
   206         s['legacyfingerprint'] = True
   207         s['legacyfingerprint'] = True
   207 
   208 
   208     # If a host cert fingerprint is defined, it is the only thing that
   209     # If a host cert fingerprint is defined, it is the only thing that
   221         s['allowloaddefaultcerts'] = False
   222         s['allowloaddefaultcerts'] = False
   222 
   223 
   223     # If both fingerprints and a per-host ca file are specified, issue a warning
   224     # If both fingerprints and a per-host ca file are specified, issue a warning
   224     # because users should not be surprised about what security is or isn't
   225     # because users should not be surprised about what security is or isn't
   225     # being performed.
   226     # being performed.
   226     cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname)
   227     cafile = ui.config('hostsecurity', '%s:verifycertsfile' % bhostname)
   227     if s['certfingerprints'] and cafile:
   228     if s['certfingerprints'] and cafile:
   228         ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
   229         ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
   229                   'fingerprints defined; using host fingerprints for '
   230                   'fingerprints defined; using host fingerprints for '
   230                   'verification)\n') % hostname)
   231                   'verification)\n') % bhostname)
   231 
   232 
   232     # Try to hook up CA certificate validation unless something above
   233     # Try to hook up CA certificate validation unless something above
   233     # makes it not necessary.
   234     # makes it not necessary.
   234     if s['verifymode'] is None:
   235     if s['verifymode'] is None:
   235         # Look at per-host ca file first.
   236         # Look at per-host ca file first.
   236         if cafile:
   237         if cafile:
   237             cafile = util.expandpath(cafile)
   238             cafile = util.expandpath(cafile)
   238             if not os.path.exists(cafile):
   239             if not os.path.exists(cafile):
   239                 raise error.Abort(_('path specified by %s does not exist: %s') %
   240                 raise error.Abort(_('path specified by %s does not exist: %s') %
   240                                   ('hostsecurity.%s:verifycertsfile' % hostname,
   241                                   ('hostsecurity.%s:verifycertsfile' % (
   241                                    cafile))
   242                                       bhostname,), cafile))
   242             s['cafile'] = cafile
   243             s['cafile'] = cafile
   243         else:
   244         else:
   244             # Find global certificates file in config.
   245             # Find global certificates file in config.
   245             cafile = ui.config('web', 'cacerts')
   246             cafile = ui.config('web', 'cacerts')
   246 
   247 
   388             if len(e.args) == 1: # pypy has different SSLError args
   389             if len(e.args) == 1: # pypy has different SSLError args
   389                 msg = e.args[0]
   390                 msg = e.args[0]
   390             else:
   391             else:
   391                 msg = e.args[1]
   392                 msg = e.args[1]
   392             raise error.Abort(_('error loading CA file %s: %s') % (
   393             raise error.Abort(_('error loading CA file %s: %s') % (
   393                               settings['cafile'], msg),
   394                               settings['cafile'], util.forcebytestr(msg)),
   394                               hint=_('file is empty or malformed?'))
   395                               hint=_('file is empty or malformed?'))
   395         caloaded = True
   396         caloaded = True
   396     elif settings['allowloaddefaultcerts']:
   397     elif settings['allowloaddefaultcerts']:
   397         # This is a no-op on old Python.
   398         # This is a no-op on old Python.
   398         sslcontext.load_default_certs()
   399         sslcontext.load_default_certs()
   581     the value in ``dn``.
   582     the value in ``dn``.
   582     """
   583     """
   583     pats = []
   584     pats = []
   584     if not dn:
   585     if not dn:
   585         return False
   586         return False
   586 
   587     dn = pycompat.bytesurl(dn)
   587     pieces = dn.split(r'.')
   588     hostname = pycompat.bytesurl(hostname)
       
   589 
       
   590     pieces = dn.split('.')
   588     leftmost = pieces[0]
   591     leftmost = pieces[0]
   589     remainder = pieces[1:]
   592     remainder = pieces[1:]
   590     wildcards = leftmost.count('*')
   593     wildcards = leftmost.count('*')
   591     if wildcards > maxwildcards:
   594     if wildcards > maxwildcards:
   592         raise wildcarderror(
   595         raise wildcarderror(
   635         if key == 'DNS':
   638         if key == 'DNS':
   636             try:
   639             try:
   637                 if _dnsnamematch(value, hostname):
   640                 if _dnsnamematch(value, hostname):
   638                     return
   641                     return
   639             except wildcarderror as e:
   642             except wildcarderror as e:
   640                 return e.args[0]
   643                 return util.forcebytestr(e.args[0])
   641 
   644 
   642             dnsnames.append(value)
   645             dnsnames.append(value)
   643 
   646 
   644     if not dnsnames:
   647     if not dnsnames:
   645         # The subject is only checked when there is no DNS in subjectAltName.
   648         # The subject is only checked when there is no DNS in subjectAltName.
   646         for sub in cert.get('subject', []):
   649         for sub in cert.get(r'subject', []):
   647             for key, value in sub:
   650             for key, value in sub:
   648                 # According to RFC 2818 the most specific Common Name must
   651                 # According to RFC 2818 the most specific Common Name must
   649                 # be used.
   652                 # be used.
   650                 if key == 'commonName':
   653                 if key == r'commonName':
   651                     # 'subject' entries are unicode.
   654                     # 'subject' entries are unicode.
   652                     try:
   655                     try:
   653                         value = value.encode('ascii')
   656                         value = value.encode('ascii')
   654                     except UnicodeEncodeError:
   657                     except UnicodeEncodeError:
   655                         return _('IDN in certificate not supported')
   658                         return _('IDN in certificate not supported')
   656 
   659 
   657                     try:
   660                     try:
   658                         if _dnsnamematch(value, hostname):
   661                         if _dnsnamematch(value, hostname):
   659                             return
   662                             return
   660                     except wildcarderror as e:
   663                     except wildcarderror as e:
   661                         return e.args[0]
   664                         return util.forcebytestr(e.args[0])
   662 
   665 
   663                     dnsnames.append(value)
   666                     dnsnames.append(value)
   664 
   667 
   665     if len(dnsnames) > 1:
   668     if len(dnsnames) > 1:
   666         return _('certificate is for %s') % ', '.join(dnsnames)
   669         return _('certificate is for %s') % ', '.join(dnsnames)
   778 def validatesocket(sock):
   781 def validatesocket(sock):
   779     """Validate a socket meets security requirements.
   782     """Validate a socket meets security requirements.
   780 
   783 
   781     The passed socket must have been created with ``wrapsocket()``.
   784     The passed socket must have been created with ``wrapsocket()``.
   782     """
   785     """
   783     host = sock._hgstate['hostname']
   786     shost = sock._hgstate['hostname']
       
   787     host = pycompat.bytesurl(shost)
   784     ui = sock._hgstate['ui']
   788     ui = sock._hgstate['ui']
   785     settings = sock._hgstate['settings']
   789     settings = sock._hgstate['settings']
   786 
   790 
   787     try:
   791     try:
   788         peercert = sock.getpeercert(True)
   792         peercert = sock.getpeercert(True)
   854             hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
   858             hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
   855                    'how to configure Mercurial to avoid this error or set '
   859                    'how to configure Mercurial to avoid this error or set '
   856                    'hostsecurity.%s:fingerprints=%s to trust this server') %
   860                    'hostsecurity.%s:fingerprints=%s to trust this server') %
   857                    (host, nicefingerprint))
   861                    (host, nicefingerprint))
   858 
   862 
   859     msg = _verifycert(peercert2, host)
   863     msg = _verifycert(peercert2, shost)
   860     if msg:
   864     if msg:
   861         raise error.Abort(_('%s certificate error: %s') % (host, msg),
   865         raise error.Abort(_('%s certificate error: %s') % (host, msg),
   862                          hint=_('set hostsecurity.%s:certfingerprints=%s '
   866                          hint=_('set hostsecurity.%s:certfingerprints=%s '
   863                                 'config setting or use --insecure to connect '
   867                                 'config setting or use --insecure to connect '
   864                                 'insecurely') %
   868                                 'insecurely') %