1"""The match_hostname() function from Python 3.5, 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# It is modified to remove commonName support. 
    6 
    7from __future__ import annotations 
    8 
    9import ipaddress 
    10import re 
    11import typing 
    12from ipaddress import IPv4Address, IPv6Address 
    13 
    14if typing.TYPE_CHECKING: 
    15    from .ssl_ import _TYPE_PEER_CERT_RET_DICT 
    16 
    17__version__ = "3.5.0.1" 
    18 
    19 
    20class CertificateError(ValueError): 
    21    pass 
    22 
    23 
    24def _dnsname_match( 
    25    dn: typing.Any, hostname: str, max_wildcards: int = 1 
    26) -> typing.Match[str] | None | bool: 
    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 
    51    # speed up common case w/o wildcards 
    52    if not wildcards: 
    53        return bool(dn.lower() == hostname.lower()) 
    54 
    55    # RFC 6125, section 6.4.3, subitem 1. 
    56    # The client SHOULD NOT attempt to match a presented identifier in which 
    57    # the wildcard character comprises a label other than the left-most label. 
    58    if leftmost == "*": 
    59        # When '*' is a fragment by itself, it matches a non-empty dotless 
    60        # fragment. 
    61        pats.append("[^.]+") 
    62    elif leftmost.startswith("xn--") or hostname.startswith("xn--"): 
    63        # RFC 6125, section 6.4.3, subitem 3. 
    64        # The client SHOULD NOT attempt to match a presented identifier 
    65        # where the wildcard character is embedded within an A-label or 
    66        # U-label of an internationalized domain name. 
    67        pats.append(re.escape(leftmost)) 
    68    else: 
    69        # Otherwise, '*' matches any dotless string, e.g. www* 
    70        pats.append(re.escape(leftmost).replace(r"\*", "[^.]*")) 
    71 
    72    # add the remaining fragments, ignore any wildcards 
    73    for frag in remainder: 
    74        pats.append(re.escape(frag)) 
    75 
    76    pat = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE) 
    77    return pat.match(hostname) 
    78 
    79 
    80def _ipaddress_match(ipname: str, host_ip: IPv4Address | IPv6Address) -> bool: 
    81    """Exact matching of IP addresses. 
    82 
    83    RFC 9110 section 4.3.5: "A reference identity of IP-ID contains the decoded 
    84    bytes of the IP address. An IP version 4 address is 4 octets, and an IP 
    85    version 6 address is 16 octets. [...] A reference identity of type IP-ID 
    86    matches if the address is identical to an iPAddress value of the 
    87    subjectAltName extension of the certificate." 
    88    """ 
    89    # OpenSSL may add a trailing newline to a subjectAltName's IP address 
    90    # Divergence from upstream: ipaddress can't handle byte str 
    91    ip = ipaddress.ip_address(ipname.rstrip()) 
    92    return bool(ip.packed == host_ip.packed) 
    93 
    94 
    95def match_hostname( 
    96    cert: _TYPE_PEER_CERT_RET_DICT | None, 
    97    hostname: str, 
    98    hostname_checks_common_name: bool = False, 
    99) -> None: 
    100    """Verify that *cert* (in decoded format as returned by 
    101    SSLSocket.getpeercert()) matches the *hostname*.  RFC 2818 and RFC 6125 
    102    rules are followed, but IP addresses are not accepted for *hostname*. 
    103 
    104    CertificateError is raised on failure. On success, the function 
    105    returns nothing. 
    106    """ 
    107    if not cert: 
    108        raise ValueError( 
    109            "empty or no certificate, match_hostname needs a " 
    110            "SSL socket or SSL context with either " 
    111            "CERT_OPTIONAL or CERT_REQUIRED" 
    112        ) 
    113    try: 
    114        # Divergence from upstream: ipaddress can't handle byte str 
    115        # 
    116        # The ipaddress module shipped with Python < 3.9 does not support 
    117        # scoped IPv6 addresses so we unconditionally strip the Zone IDs for 
    118        # now. Once we drop support for Python 3.9 we can remove this branch. 
    119        if "%" in hostname: 
    120            host_ip = ipaddress.ip_address(hostname[: hostname.rfind("%")]) 
    121        else: 
    122            host_ip = ipaddress.ip_address(hostname) 
    123 
    124    except ValueError: 
    125        # Not an IP address (common case) 
    126        host_ip = None 
    127    dnsnames = [] 
    128    san: tuple[tuple[str, str], ...] = cert.get("subjectAltName", ()) 
    129    key: str 
    130    value: str 
    131    for key, value in san: 
    132        if key == "DNS": 
    133            if host_ip is None and _dnsname_match(value, hostname): 
    134                return 
    135            dnsnames.append(value) 
    136        elif key == "IP Address": 
    137            if host_ip is not None and _ipaddress_match(value, host_ip): 
    138                return 
    139            dnsnames.append(value) 
    140 
    141    # We only check 'commonName' if it's enabled and we're not verifying 
    142    # an IP address. IP addresses aren't valid within 'commonName'. 
    143    if hostname_checks_common_name and host_ip is None and not dnsnames: 
    144        for sub in cert.get("subject", ()): 
    145            for key, value in sub: 
    146                if key == "commonName": 
    147                    if _dnsname_match(value, hostname): 
    148                        return 
    149                    dnsnames.append(value)  # Defensive: for Python < 3.9.3 
    150 
    151    if len(dnsnames) > 1: 
    152        raise CertificateError( 
    153            "hostname %r " 
    154            "doesn't match either of %s" % (hostname, ", ".join(map(repr, dnsnames))) 
    155        ) 
    156    elif len(dnsnames) == 1: 
    157        raise CertificateError(f"hostname {hostname!r} doesn't match {dnsnames[0]!r}") 
    158    else: 
    159        raise CertificateError("no appropriate subjectAltName fields were found")