comparison mercurial/sslutil.py @ 36745:424994a0adfd

sslutil: lots of unicode/bytes cleanup In general, we handle hostnames as bytes, except where Python forces them to be unicodes. This fixes all the tracebacks I was seeing in test-https.t, but there's still some ECONNRESET weirdness that I can't hunt down... Differential Revision: https://phab.mercurial-scm.org/D2687
author Augie Fackler <augie@google.com>
date Sun, 04 Mar 2018 18:03:55 -0500
parents 72b91f905065
children 25798cf7dc9d
comparison
equal deleted inserted replaced
36741:7a25f6cfebe8 36745: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') %