_implementation.py revision 420216e5
1"""The match_hostname() function from Python 3.3.3, essential when using SSL."""
2
3# Note: This file is under the PSF license as the code comes from the python
4# stdlib.   http://docs.python.org/3/license.html
5
6import re
7import sys
8
9# ipaddress has been backported to 2.6+ in pypi.  If it is installed on the
10# system, use it to handle IPAddress ServerAltnames (this was added in
11# python-3.5) otherwise only do DNS matching.  This allows
12# backports.ssl_match_hostname to continue to be used all the way back to
13# python-2.4.
14try:
15    import ipaddress
16except ImportError:
17    ipaddress = None
18
19__version__ = '3.5.0.1'
20
21
22class CertificateError(ValueError):
23    pass
24
25
26def _dnsname_match(dn, hostname, max_wildcards=1):
27    """Matching according to RFC 6125, section 6.4.3
28
29    http://tools.ietf.org/html/rfc6125#section-6.4.3
30    """
31    pats = []
32    if not dn:
33        return False
34
35    # Ported from python3-syntax:
36    # leftmost, *remainder = dn.split(r'.')
37    parts = dn.split(r'.')
38    leftmost = parts[0]
39    remainder = parts[1:]
40
41    wildcards = leftmost.count('*')
42    if wildcards > max_wildcards:
43        # Issue #17980: avoid denials of service by refusing more
44        # than one wildcard per fragment.  A survey of established
45        # policy among SSL implementations showed it to be a
46        # reasonable choice.
47        raise CertificateError(
48            "too many wildcards in certificate DNS name: " + repr(dn))
49
50    # speed up common case w/o wildcards
51    if not wildcards:
52        return dn.lower() == hostname.lower()
53
54    # RFC 6125, section 6.4.3, subitem 1.
55    # The client SHOULD NOT attempt to match a presented identifier in which
56    # the wildcard character comprises a label other than the left-most label.
57    if leftmost == '*':
58        # When '*' is a fragment by itself, it matches a non-empty dotless
59        # fragment.
60        pats.append('[^.]+')
61    elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
62        # RFC 6125, section 6.4.3, subitem 3.
63        # The client SHOULD NOT attempt to match a presented identifier
64        # where the wildcard character is embedded within an A-label or
65        # U-label of an internationalized domain name.
66        pats.append(re.escape(leftmost))
67    else:
68        # Otherwise, '*' matches any dotless string, e.g. www*
69        pats.append(re.escape(leftmost).replace(r'\*', '[^.]*'))
70
71    # add the remaining fragments, ignore any wildcards
72    for frag in remainder:
73        pats.append(re.escape(frag))
74
75    pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
76    return pat.match(hostname)
77
78
79def _to_unicode(obj):
80    if isinstance(obj, str) and sys.version_info < (3,):
81        obj = unicode(obj, encoding='ascii', errors='strict')
82    return obj
83
84def _ipaddress_match(ipname, host_ip):
85    """Exact matching of IP addresses.
86
87    RFC 6125 explicitly doesn't define an algorithm for this
88    (section 1.7.2 - "Out of Scope").
89    """
90    # OpenSSL may add a trailing newline to a subjectAltName's IP address
91    # Divergence from upstream: ipaddress can't handle byte str
92    ip = ipaddress.ip_address(_to_unicode(ipname).rstrip())
93    return ip == host_ip
94
95
96def match_hostname(cert, hostname):
97    """Verify that *cert* (in decoded format as returned by
98    SSLSocket.getpeercert()) matches the *hostname*.  RFC 2818 and RFC 6125
99    rules are followed, but IP addresses are not accepted for *hostname*.
100
101    CertificateError is raised on failure. On success, the function
102    returns nothing.
103    """
104    if not cert:
105        raise ValueError("empty or no certificate, match_hostname needs a "
106                         "SSL socket or SSL context with either "
107                         "CERT_OPTIONAL or CERT_REQUIRED")
108    try:
109        # Divergence from upstream: ipaddress can't handle byte str
110        host_ip = ipaddress.ip_address(_to_unicode(hostname))
111    except ValueError:
112        # Not an IP address (common case)
113        host_ip = None
114    except UnicodeError:
115        # Divergence from upstream: Have to deal with ipaddress not taking
116        # byte strings.  addresses should be all ascii, so we consider it not
117        # an ipaddress in this case
118        host_ip = None
119    except AttributeError:
120        # Divergence from upstream: Make ipaddress library optional
121        if ipaddress is None:
122            host_ip = None
123        else:
124            raise
125    dnsnames = []
126    san = cert.get('subjectAltName', ())
127    for key, value in san:
128        if key == 'DNS':
129            if host_ip is None and _dnsname_match(value, hostname):
130                return
131            dnsnames.append(value)
132        elif key == 'IP Address':
133            if host_ip is not None and _ipaddress_match(value, host_ip):
134                return
135            dnsnames.append(value)
136    if not dnsnames:
137        # The subject is only checked when there is no dNSName entry
138        # in subjectAltName
139        for sub in cert.get('subject', ()):
140            for key, value in sub:
141                # XXX according to RFC 2818, the most specific Common Name
142                # must be used.
143                if key == 'commonName':
144                    if _dnsname_match(value, hostname):
145                        return
146                    dnsnames.append(value)
147    if len(dnsnames) > 1:
148        raise CertificateError("hostname %r "
149            "doesn't match either of %s"
150            % (hostname, ', '.join(map(repr, dnsnames))))
151    elif len(dnsnames) == 1:
152        raise CertificateError("hostname %r "
153            "doesn't match %r"
154            % (hostname, dnsnames[0]))
155    else:
156        raise CertificateError("no appropriate commonName or "
157            "subjectAltName fields were found")
158