--- a/mercurial/sslutil.py Sun Jun 26 19:16:54 2016 -0700
+++ b/mercurial/sslutil.py Sun Jun 26 19:34:48 2016 -0700
@@ -10,6 +10,7 @@
from __future__ import absolute_import
import os
+import re
import ssl
import sys
@@ -167,6 +168,57 @@
raise error.Abort(_('ssl connection failed'))
return sslsocket
+class wildcarderror(Exception):
+ """Represents an error parsing wildcards in DNS name."""
+
+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
+
+ pieces = dn.split(r'.')
+ leftmost = pieces[0]
+ remainder = pieces[1:]
+ wildcards = leftmost.count('*')
+ if wildcards > maxwildcards:
+ raise wildcarderror(
+ _('too many wildcards in certificate DNS name: %s') % dn)
+
+ # 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.
+ if leftmost == '*':
+ # When '*' is a fragment by itself, it matches a non-empty dotless
+ # fragment.
+ pats.append('[^.]+')
+ elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
+ # 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.
+ pats.append(re.escape(leftmost))
+ else:
+ # Otherwise, '*' matches any dotless string, e.g. www*
+ pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
+
+ # add the remaining fragments, ignore any wildcards
+ for frag in remainder:
+ pats.append(re.escape(frag))
+
+ pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
+ return pat.match(hostname) is not None
+
def _verifycert(cert, hostname):
'''Verify that cert (in socket.getpeercert() format) matches hostname.
CRLs is not handled.
@@ -175,33 +227,46 @@
'''
if not cert:
return _('no certificate received')
- dnsname = hostname.lower()
- def matchdnsname(certname):
- return (certname == dnsname or
- '.' in dnsname and certname == '*.' + dnsname.split('.', 1)[1])
+ dnsnames = []
san = cert.get('subjectAltName', [])
- if san:
- certnames = [value.lower() for key, value in san if key == 'DNS']
- for name in certnames:
- if matchdnsname(name):
- return None
- if certnames:
- return _('certificate is for %s') % ', '.join(certnames)
+ for key, value in san:
+ if key == 'DNS':
+ try:
+ if _dnsnamematch(value, hostname):
+ return
+ except wildcarderror as e:
+ return e.message
+
+ dnsnames.append(value)
- # subject is only checked when subjectAltName is empty
- for s in cert.get('subject', []):
- key, value = s[0]
- if key == 'commonName':
- try:
- # 'subject' entries are unicode
- certname = value.lower().encode('ascii')
- except UnicodeEncodeError:
- return _('IDN in certificate not supported')
- if matchdnsname(certname):
- return None
- return _('certificate is for %s') % certname
- return _('no commonName or subjectAltName found in certificate')
+ if not dnsnames:
+ # The subject is only checked when there is no DNS in subjectAltName.
+ for sub in cert.get('subject', []):
+ for key, value in sub:
+ # According to RFC 2818 the most specific Common Name must
+ # be used.
+ if key == 'commonName':
+ # 'subject' entries are unicide.
+ try:
+ value = value.encode('ascii')
+ except UnicodeEncodeError:
+ return _('IDN in certificate not supported')
+
+ try:
+ if _dnsnamematch(value, hostname):
+ return
+ except wildcarderror as e:
+ return e.message
+
+ dnsnames.append(value)
+
+ if len(dnsnames) > 1:
+ return _('certificate is for %s') % ', '.join(dnsnames)
+ elif len(dnsnames) == 1:
+ return _('certificate is for %s') % dnsnames[0]
+ else:
+ return _('no commonName or subjectAltName found in certificate')
# CERT_REQUIRED means fetch the cert from the server all the time AND