mercurial/sslutil.py
branchstable
changeset 29452 26a5d605b868
parent 29042 693b856a4d45
child 29459 fd93b15b5c30
child 29460 a7d1532b26a1
equal deleted inserted replaced
29451:676f4d0e3a7b 29452:26a5d605b868
     8 # GNU General Public License version 2 or any later version.
     8 # GNU General Public License version 2 or any later version.
     9 
     9 
    10 from __future__ import absolute_import
    10 from __future__ import absolute_import
    11 
    11 
    12 import os
    12 import os
       
    13 import re
    13 import ssl
    14 import ssl
    14 import sys
    15 import sys
    15 
    16 
    16 from .i18n import _
    17 from .i18n import _
    17 from . import (
    18 from . import (
   165     # - see http://bugs.python.org/issue13721
   166     # - see http://bugs.python.org/issue13721
   166     if not sslsocket.cipher():
   167     if not sslsocket.cipher():
   167         raise error.Abort(_('ssl connection failed'))
   168         raise error.Abort(_('ssl connection failed'))
   168     return sslsocket
   169     return sslsocket
   169 
   170 
       
   171 class wildcarderror(Exception):
       
   172     """Represents an error parsing wildcards in DNS name."""
       
   173 
       
   174 def _dnsnamematch(dn, hostname, maxwildcards=1):
       
   175     """Match DNS names according RFC 6125 section 6.4.3.
       
   176 
       
   177     This code is effectively copied from CPython's ssl._dnsname_match.
       
   178 
       
   179     Returns a bool indicating whether the expected hostname matches
       
   180     the value in ``dn``.
       
   181     """
       
   182     pats = []
       
   183     if not dn:
       
   184         return False
       
   185 
       
   186     pieces = dn.split(r'.')
       
   187     leftmost = pieces[0]
       
   188     remainder = pieces[1:]
       
   189     wildcards = leftmost.count('*')
       
   190     if wildcards > maxwildcards:
       
   191         raise wildcarderror(
       
   192             _('too many wildcards in certificate DNS name: %s') % dn)
       
   193 
       
   194     # speed up common case w/o wildcards
       
   195     if not wildcards:
       
   196         return dn.lower() == hostname.lower()
       
   197 
       
   198     # RFC 6125, section 6.4.3, subitem 1.
       
   199     # The client SHOULD NOT attempt to match a presented identifier in which
       
   200     # the wildcard character comprises a label other than the left-most label.
       
   201     if leftmost == '*':
       
   202         # When '*' is a fragment by itself, it matches a non-empty dotless
       
   203         # fragment.
       
   204         pats.append('[^.]+')
       
   205     elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
       
   206         # RFC 6125, section 6.4.3, subitem 3.
       
   207         # The client SHOULD NOT attempt to match a presented identifier
       
   208         # where the wildcard character is embedded within an A-label or
       
   209         # U-label of an internationalized domain name.
       
   210         pats.append(re.escape(leftmost))
       
   211     else:
       
   212         # Otherwise, '*' matches any dotless string, e.g. www*
       
   213         pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
       
   214 
       
   215     # add the remaining fragments, ignore any wildcards
       
   216     for frag in remainder:
       
   217         pats.append(re.escape(frag))
       
   218 
       
   219     pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
       
   220     return pat.match(hostname) is not None
       
   221 
   170 def _verifycert(cert, hostname):
   222 def _verifycert(cert, hostname):
   171     '''Verify that cert (in socket.getpeercert() format) matches hostname.
   223     '''Verify that cert (in socket.getpeercert() format) matches hostname.
   172     CRLs is not handled.
   224     CRLs is not handled.
   173 
   225 
   174     Returns error message if any problems are found and None on success.
   226     Returns error message if any problems are found and None on success.
   175     '''
   227     '''
   176     if not cert:
   228     if not cert:
   177         return _('no certificate received')
   229         return _('no certificate received')
   178     dnsname = hostname.lower()
   230 
   179     def matchdnsname(certname):
   231     dnsnames = []
   180         return (certname == dnsname or
       
   181                 '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
       
   182 
       
   183     san = cert.get('subjectAltName', [])
   232     san = cert.get('subjectAltName', [])
   184     if san:
   233     for key, value in san:
   185         certnames = [value.lower() for key, value in san if key == 'DNS']
   234         if key == 'DNS':
   186         for name in certnames:
       
   187             if matchdnsname(name):
       
   188                 return None
       
   189         if certnames:
       
   190             return _('certificate is for %s') % ', '.join(certnames)
       
   191 
       
   192     # subject is only checked when subjectAltName is empty
       
   193     for s in cert.get('subject', []):
       
   194         key, value = s[0]
       
   195         if key == 'commonName':
       
   196             try:
   235             try:
   197                 # 'subject' entries are unicode
   236                 if _dnsnamematch(value, hostname):
   198                 certname = value.lower().encode('ascii')
   237                     return
   199             except UnicodeEncodeError:
   238             except wildcarderror as e:
   200                 return _('IDN in certificate not supported')
   239                 return e.message
   201             if matchdnsname(certname):
   240 
   202                 return None
   241             dnsnames.append(value)
   203             return _('certificate is for %s') % certname
   242 
   204     return _('no commonName or subjectAltName found in certificate')
   243     if not dnsnames:
       
   244         # The subject is only checked when there is no DNS in subjectAltName.
       
   245         for sub in cert.get('subject', []):
       
   246             for key, value in sub:
       
   247                 # According to RFC 2818 the most specific Common Name must
       
   248                 # be used.
       
   249                 if key == 'commonName':
       
   250                     # 'subject' entries are unicide.
       
   251                     try:
       
   252                         value = value.encode('ascii')
       
   253                     except UnicodeEncodeError:
       
   254                         return _('IDN in certificate not supported')
       
   255 
       
   256                     try:
       
   257                         if _dnsnamematch(value, hostname):
       
   258                             return
       
   259                     except wildcarderror as e:
       
   260                         return e.message
       
   261 
       
   262                     dnsnames.append(value)
       
   263 
       
   264     if len(dnsnames) > 1:
       
   265         return _('certificate is for %s') % ', '.join(dnsnames)
       
   266     elif len(dnsnames) == 1:
       
   267         return _('certificate is for %s') % dnsnames[0]
       
   268     else:
       
   269         return _('no commonName or subjectAltName found in certificate')
   205 
   270 
   206 
   271 
   207 # CERT_REQUIRED means fetch the cert from the server all the time AND
   272 # CERT_REQUIRED means fetch the cert from the server all the time AND
   208 # validate it against the CA store provided in web.cacerts.
   273 # validate it against the CA store provided in web.cacerts.
   209 
   274