comparison mercurial/sslutil.py @ 29459:fd93b15b5c30

merge with stable
author Matt Mackall <mpm@selenic.com>
date Fri, 01 Jul 2016 16:02:56 -0500
parents 5b71a8d7f7ff 26a5d605b868
children 4e72995f6c9c
comparison
equal deleted inserted replaced
29458:59058549a611 29459:fd93b15b5c30
9 9
10 from __future__ import absolute_import 10 from __future__ import absolute_import
11 11
12 import hashlib 12 import hashlib
13 import os 13 import os
14 import re
14 import ssl 15 import ssl
15 import sys 16 import sys
16 17
17 from .i18n import _ 18 from .i18n import _
18 from . import ( 19 from . import (
313 'ui': ui, 314 'ui': ui,
314 } 315 }
315 316
316 return sslsocket 317 return sslsocket
317 318
319 class wildcarderror(Exception):
320 """Represents an error parsing wildcards in DNS name."""
321
322 def _dnsnamematch(dn, hostname, maxwildcards=1):
323 """Match DNS names according RFC 6125 section 6.4.3.
324
325 This code is effectively copied from CPython's ssl._dnsname_match.
326
327 Returns a bool indicating whether the expected hostname matches
328 the value in ``dn``.
329 """
330 pats = []
331 if not dn:
332 return False
333
334 pieces = dn.split(r'.')
335 leftmost = pieces[0]
336 remainder = pieces[1:]
337 wildcards = leftmost.count('*')
338 if wildcards > maxwildcards:
339 raise wildcarderror(
340 _('too many wildcards in certificate DNS name: %s') % dn)
341
342 # speed up common case w/o wildcards
343 if not wildcards:
344 return dn.lower() == hostname.lower()
345
346 # RFC 6125, section 6.4.3, subitem 1.
347 # The client SHOULD NOT attempt to match a presented identifier in which
348 # the wildcard character comprises a label other than the left-most label.
349 if leftmost == '*':
350 # When '*' is a fragment by itself, it matches a non-empty dotless
351 # fragment.
352 pats.append('[^.]+')
353 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
354 # RFC 6125, section 6.4.3, subitem 3.
355 # The client SHOULD NOT attempt to match a presented identifier
356 # where the wildcard character is embedded within an A-label or
357 # U-label of an internationalized domain name.
358 pats.append(re.escape(leftmost))
359 else:
360 # Otherwise, '*' matches any dotless string, e.g. www*
361 pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
362
363 # add the remaining fragments, ignore any wildcards
364 for frag in remainder:
365 pats.append(re.escape(frag))
366
367 pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
368 return pat.match(hostname) is not None
369
318 def _verifycert(cert, hostname): 370 def _verifycert(cert, hostname):
319 '''Verify that cert (in socket.getpeercert() format) matches hostname. 371 '''Verify that cert (in socket.getpeercert() format) matches hostname.
320 CRLs is not handled. 372 CRLs is not handled.
321 373
322 Returns error message if any problems are found and None on success. 374 Returns error message if any problems are found and None on success.
323 ''' 375 '''
324 if not cert: 376 if not cert:
325 return _('no certificate received') 377 return _('no certificate received')
326 dnsname = hostname.lower() 378
327 def matchdnsname(certname): 379 dnsnames = []
328 return (certname == dnsname or
329 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
330
331 san = cert.get('subjectAltName', []) 380 san = cert.get('subjectAltName', [])
332 if san: 381 for key, value in san:
333 certnames = [value.lower() for key, value in san if key == 'DNS'] 382 if key == 'DNS':
334 for name in certnames:
335 if matchdnsname(name):
336 return None
337 if certnames:
338 return _('certificate is for %s') % ', '.join(certnames)
339
340 # subject is only checked when subjectAltName is empty
341 for s in cert.get('subject', []):
342 key, value = s[0]
343 if key == 'commonName':
344 try: 383 try:
345 # 'subject' entries are unicode 384 if _dnsnamematch(value, hostname):
346 certname = value.lower().encode('ascii') 385 return
347 except UnicodeEncodeError: 386 except wildcarderror as e:
348 return _('IDN in certificate not supported') 387 return e.message
349 if matchdnsname(certname): 388
350 return None 389 dnsnames.append(value)
351 return _('certificate is for %s') % certname 390
352 return _('no commonName or subjectAltName found in certificate') 391 if not dnsnames:
392 # The subject is only checked when there is no DNS in subjectAltName.
393 for sub in cert.get('subject', []):
394 for key, value in sub:
395 # According to RFC 2818 the most specific Common Name must
396 # be used.
397 if key == 'commonName':
398 # 'subject' entries are unicide.
399 try:
400 value = value.encode('ascii')
401 except UnicodeEncodeError:
402 return _('IDN in certificate not supported')
403
404 try:
405 if _dnsnamematch(value, hostname):
406 return
407 except wildcarderror as e:
408 return e.message
409
410 dnsnames.append(value)
411
412 if len(dnsnames) > 1:
413 return _('certificate is for %s') % ', '.join(dnsnames)
414 elif len(dnsnames) == 1:
415 return _('certificate is for %s') % dnsnames[0]
416 else:
417 return _('no commonName or subjectAltName found in certificate')
353 418
354 def _plainapplepython(): 419 def _plainapplepython():
355 """return true if this seems to be a pure Apple Python that 420 """return true if this seems to be a pure Apple Python that
356 * is unfrozen and presumably has the whole mercurial module in the file 421 * is unfrozen and presumably has the whole mercurial module in the file
357 system 422 system