| | """The match_hostname() function from Python 3.5, essential when using SSL.""" |
| |
|
| | |
| | |
| | |
| |
|
| | from __future__ import annotations |
| |
|
| | import ipaddress |
| | import re |
| | import typing |
| | from ipaddress import IPv4Address, IPv6Address |
| |
|
| | if typing.TYPE_CHECKING: |
| | from .ssl_ import _TYPE_PEER_CERT_RET_DICT |
| |
|
| | __version__ = "3.5.0.1" |
| |
|
| |
|
| | class CertificateError(ValueError): |
| | pass |
| |
|
| |
|
| | def _dnsname_match( |
| | dn: typing.Any, hostname: str, max_wildcards: int = 1 |
| | ) -> typing.Match[str] | None | bool: |
| | """Matching according to RFC 6125, section 6.4.3 |
| | |
| | http://tools.ietf.org/html/rfc6125#section-6.4.3 |
| | """ |
| | pats = [] |
| | if not dn: |
| | return False |
| |
|
| | |
| | |
| | parts = dn.split(r".") |
| | leftmost = parts[0] |
| | remainder = parts[1:] |
| |
|
| | wildcards = leftmost.count("*") |
| | if wildcards > max_wildcards: |
| | |
| | |
| | |
| | |
| | raise CertificateError( |
| | "too many wildcards in certificate DNS name: " + repr(dn) |
| | ) |
| |
|
| | |
| | if not wildcards: |
| | return bool(dn.lower() == hostname.lower()) |
| |
|
| | |
| | |
| | |
| | if leftmost == "*": |
| | |
| | |
| | pats.append("[^.]+") |
| | elif leftmost.startswith("xn--") or hostname.startswith("xn--"): |
| | |
| | |
| | |
| | |
| | pats.append(re.escape(leftmost)) |
| | else: |
| | |
| | pats.append(re.escape(leftmost).replace(r"\*", "[^.]*")) |
| |
|
| | |
| | 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) |
| |
|
| |
|
| | def _ipaddress_match(ipname: str, host_ip: IPv4Address | IPv6Address) -> bool: |
| | """Exact matching of IP addresses. |
| | |
| | RFC 9110 section 4.3.5: "A reference identity of IP-ID contains the decoded |
| | bytes of the IP address. An IP version 4 address is 4 octets, and an IP |
| | version 6 address is 16 octets. [...] A reference identity of type IP-ID |
| | matches if the address is identical to an iPAddress value of the |
| | subjectAltName extension of the certificate." |
| | """ |
| | |
| | |
| | ip = ipaddress.ip_address(ipname.rstrip()) |
| | return bool(ip.packed == host_ip.packed) |
| |
|
| |
|
| | def match_hostname( |
| | cert: _TYPE_PEER_CERT_RET_DICT | None, |
| | hostname: str, |
| | hostname_checks_common_name: bool = False, |
| | ) -> None: |
| | """Verify that *cert* (in decoded format as returned by |
| | SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 |
| | rules are followed, but IP addresses are not accepted for *hostname*. |
| | |
| | CertificateError is raised on failure. On success, the function |
| | returns nothing. |
| | """ |
| | if not cert: |
| | raise ValueError( |
| | "empty or no certificate, match_hostname needs a " |
| | "SSL socket or SSL context with either " |
| | "CERT_OPTIONAL or CERT_REQUIRED" |
| | ) |
| | try: |
| | |
| | |
| | |
| | |
| | |
| | if "%" in hostname: |
| | host_ip = ipaddress.ip_address(hostname[: hostname.rfind("%")]) |
| | else: |
| | host_ip = ipaddress.ip_address(hostname) |
| |
|
| | except ValueError: |
| | |
| | host_ip = None |
| | dnsnames = [] |
| | san: tuple[tuple[str, str], ...] = cert.get("subjectAltName", ()) |
| | key: str |
| | value: str |
| | for key, value in san: |
| | if key == "DNS": |
| | if host_ip is None and _dnsname_match(value, hostname): |
| | return |
| | dnsnames.append(value) |
| | elif key == "IP Address": |
| | if host_ip is not None and _ipaddress_match(value, host_ip): |
| | return |
| | dnsnames.append(value) |
| |
|
| | |
| | |
| | if hostname_checks_common_name and host_ip is None and not dnsnames: |
| | for sub in cert.get("subject", ()): |
| | for key, value in sub: |
| | if key == "commonName": |
| | if _dnsname_match(value, hostname): |
| | return |
| | dnsnames.append(value) |
| |
|
| | if len(dnsnames) > 1: |
| | raise CertificateError( |
| | "hostname %r " |
| | "doesn't match either of %s" % (hostname, ", ".join(map(repr, dnsnames))) |
| | ) |
| | elif len(dnsnames) == 1: |
| | raise CertificateError(f"hostname {hostname!r} doesn't match {dnsnames[0]!r}") |
| | else: |
| | raise CertificateError("no appropriate subjectAltName fields were found") |
| |
|