|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- """
- Common verification code.
- """
-
- from __future__ import absolute_import, division, print_function
-
- import ipaddress
- import re
-
- import attr
-
- from ._compat import maketrans, text_type
- from .exceptions import (
- CertificateError,
- DNSMismatch,
- IPAddressMismatch,
- SRVMismatch,
- URIMismatch,
- VerificationError,
- )
-
-
- try:
- import idna
- except ImportError: # pragma: nocover
- idna = None
-
-
- @attr.s(slots=True)
- class ServiceMatch(object):
- """
- A match of a service id and a certificate pattern.
- """
-
- service_id = attr.ib()
- cert_pattern = attr.ib()
-
-
- def verify_service_identity(cert_patterns, obligatory_ids, optional_ids):
- """
- Verify whether *cert_patterns* are valid for *obligatory_ids* and
- *optional_ids*.
-
- *obligatory_ids* must be both present and match. *optional_ids* must match
- if a pattern of the respective type is present.
- """
- errors = []
- matches = _find_matches(cert_patterns, obligatory_ids) + _find_matches(
- cert_patterns, optional_ids
- )
-
- matched_ids = [match.service_id for match in matches]
- for i in obligatory_ids:
- if i not in matched_ids:
- errors.append(i.error_on_mismatch(mismatched_id=i))
-
- for i in optional_ids:
- # If an optional ID is not matched by a certificate pattern *but* there
- # is a pattern of the same type , it is an error and the verification
- # fails. Example: the user passes a SRV-ID for "_mail.domain.com" but
- # the certificate contains an SRV-Pattern for "_xmpp.domain.com".
- if i not in matched_ids and _contains_instance_of(
- cert_patterns, i.pattern_class
- ):
- errors.append(i.error_on_mismatch(mismatched_id=i))
-
- if errors:
- raise VerificationError(errors=errors)
-
- return matches
-
-
- def _find_matches(cert_patterns, service_ids):
- """
- Search for matching certificate patterns and service_ids.
-
- :param cert_ids: List certificate IDs like DNSPattern.
- :type cert_ids: `list`
-
- :param service_ids: List of service IDs like DNS_ID.
- :type service_ids: `list`
-
- :rtype: `list` of `ServiceMatch`
- """
- matches = []
- for sid in service_ids:
- for cid in cert_patterns:
- if sid.verify(cid):
- matches.append(ServiceMatch(cert_pattern=cid, service_id=sid))
- return matches
-
-
- def _contains_instance_of(seq, cl):
- """
- :type seq: iterable
- :type cl: type
-
- :rtype: bool
- """
- for e in seq:
- if isinstance(e, cl):
- return True
- return False
-
-
- def _is_ip_address(pattern):
- """
- Check whether *pattern* could be/match an IP address.
-
- :param pattern: A pattern for a host name.
- :type pattern: `bytes` or `unicode`
-
- :return: `True` if *pattern* could be an IP address, else `False`.
- :rtype: bool
- """
- if isinstance(pattern, bytes):
- try:
- pattern = pattern.decode("ascii")
- except UnicodeError:
- return False
-
- try:
- int(pattern)
- return True
- except ValueError:
- pass
-
- try:
- ipaddress.ip_address(pattern.replace("*", "1"))
- except ValueError:
- return False
-
- return True
-
-
- @attr.s(init=False, slots=True)
- class DNSPattern(object):
- """
- A DNS pattern as extracted from certificates.
- """
-
- pattern = attr.ib()
-
- _RE_LEGAL_CHARS = re.compile(br"^[a-z0-9\-_.]+$")
-
- def __init__(self, pattern):
- """
- :type pattern: `bytes`
- """
- if not isinstance(pattern, bytes):
- raise TypeError("The DNS pattern must be a bytes string.")
-
- pattern = pattern.strip()
-
- if pattern == b"" or _is_ip_address(pattern) or b"\0" in pattern:
- raise CertificateError(
- "Invalid DNS pattern {0!r}.".format(pattern)
- )
-
- self.pattern = pattern.translate(_TRANS_TO_LOWER)
- if b"*" in self.pattern:
- _validate_pattern(self.pattern)
-
-
- @attr.s(slots=True)
- class IPAddressPattern(object):
- """
- An IP address pattern as extracted from certificates.
- """
-
- pattern = attr.ib()
-
- @classmethod
- def from_bytes(cls, bs):
- try:
- return cls(pattern=ipaddress.ip_address(bs))
- except ValueError:
- raise CertificateError(
- "Invalid IP address pattern {!r}.".format(bs)
- )
-
-
- @attr.s(init=False, slots=True)
- class URIPattern(object):
- """
- An URI pattern as extracted from certificates.
- """
-
- protocol_pattern = attr.ib()
- dns_pattern = attr.ib()
-
- def __init__(self, pattern):
- """
- :type pattern: `bytes`
- """
- if not isinstance(pattern, bytes):
- raise TypeError("The URI pattern must be a bytes string.")
-
- pattern = pattern.strip().translate(_TRANS_TO_LOWER)
-
- if b":" not in pattern or b"*" in pattern or _is_ip_address(pattern):
- raise CertificateError(
- "Invalid URI pattern {0!r}.".format(pattern)
- )
- self.protocol_pattern, hostname = pattern.split(b":")
- self.dns_pattern = DNSPattern(hostname)
-
-
- @attr.s(init=False, slots=True)
- class SRVPattern(object):
- """
- An SRV pattern as extracted from certificates.
- """
-
- name_pattern = attr.ib()
- dns_pattern = attr.ib()
-
- def __init__(self, pattern):
- """
- :type pattern: `bytes`
- """
- if not isinstance(pattern, bytes):
- raise TypeError("The SRV pattern must be a bytes string.")
-
- pattern = pattern.strip().translate(_TRANS_TO_LOWER)
-
- if (
- pattern[0] != b"_"[0]
- or b"." not in pattern
- or b"*" in pattern
- or _is_ip_address(pattern)
- ):
- raise CertificateError(
- "Invalid SRV pattern {0!r}.".format(pattern)
- )
- name, hostname = pattern.split(b".", 1)
- self.name_pattern = name[1:]
- self.dns_pattern = DNSPattern(hostname)
-
-
- @attr.s(init=False, slots=True)
- class DNS_ID(object):
- """
- A DNS service ID, aka hostname.
- """
-
- hostname = attr.ib()
-
- # characters that are legal in a normalized hostname
- _RE_LEGAL_CHARS = re.compile(br"^[a-z0-9\-_.]+$")
- pattern_class = DNSPattern
- error_on_mismatch = DNSMismatch
-
- def __init__(self, hostname):
- """
- :type hostname: `unicode`
- """
- if not isinstance(hostname, text_type):
- raise TypeError("DNS-ID must be a unicode string.")
-
- hostname = hostname.strip()
- if hostname == u"" or _is_ip_address(hostname):
- raise ValueError("Invalid DNS-ID.")
-
- if any(ord(c) > 127 for c in hostname):
- if idna:
- ascii_id = idna.encode(hostname)
- else:
- raise ImportError(
- "idna library is required for non-ASCII IDs."
- )
- else:
- ascii_id = hostname.encode("ascii")
-
- self.hostname = ascii_id.translate(_TRANS_TO_LOWER)
- if self._RE_LEGAL_CHARS.match(self.hostname) is None:
- raise ValueError("Invalid DNS-ID.")
-
- def verify(self, pattern):
- """
- https://tools.ietf.org/search/rfc6125#section-6.4
- """
- if isinstance(pattern, self.pattern_class):
- return _hostname_matches(pattern.pattern, self.hostname)
- else:
- return False
-
-
- @attr.s(slots=True)
- class IPAddress_ID(object):
- """
- An IP address service ID.
- """
-
- ip = attr.ib(converter=ipaddress.ip_address)
-
- pattern_class = IPAddressPattern
- error_on_mismatch = IPAddressMismatch
-
- def verify(self, pattern):
- """
- https://tools.ietf.org/search/rfc2818#section-3.1
- """
- return self.ip == pattern.pattern
-
-
- @attr.s(init=False, slots=True)
- class URI_ID(object):
- """
- An URI service ID.
- """
-
- protocol = attr.ib()
- dns_id = attr.ib()
-
- pattern_class = URIPattern
- error_on_mismatch = URIMismatch
-
- def __init__(self, uri):
- """
- :type uri: `unicode`
- """
- if not isinstance(uri, text_type):
- raise TypeError("URI-ID must be a unicode string.")
-
- uri = uri.strip()
- if u":" not in uri or _is_ip_address(uri):
- raise ValueError("Invalid URI-ID.")
-
- prot, hostname = uri.split(u":")
-
- self.protocol = prot.encode("ascii").translate(_TRANS_TO_LOWER)
- self.dns_id = DNS_ID(hostname.strip(u"/"))
-
- def verify(self, pattern):
- """
- https://tools.ietf.org/search/rfc6125#section-6.5.2
- """
- if isinstance(pattern, self.pattern_class):
- return (
- pattern.protocol_pattern == self.protocol
- and self.dns_id.verify(pattern.dns_pattern)
- )
- else:
- return False
-
-
- @attr.s(init=False, slots=True)
- class SRV_ID(object):
- """
- An SRV service ID.
- """
-
- name = attr.ib()
- dns_id = attr.ib()
-
- pattern_class = SRVPattern
- error_on_mismatch = SRVMismatch
-
- def __init__(self, srv):
- """
- :type srv: `unicode`
- """
- if not isinstance(srv, text_type):
- raise TypeError("SRV-ID must be a unicode string.")
-
- srv = srv.strip()
- if u"." not in srv or _is_ip_address(srv) or srv[0] != u"_":
- raise ValueError("Invalid SRV-ID.")
-
- name, hostname = srv.split(u".", 1)
-
- self.name = name[1:].encode("ascii").translate(_TRANS_TO_LOWER)
- self.dns_id = DNS_ID(hostname)
-
- def verify(self, pattern):
- """
- https://tools.ietf.org/search/rfc6125#section-6.5.1
- """
- if isinstance(pattern, self.pattern_class):
- return self.name == pattern.name_pattern and self.dns_id.verify(
- pattern.dns_pattern
- )
- else:
- return False
-
-
- def _hostname_matches(cert_pattern, actual_hostname):
- """
- :type cert_pattern: `bytes`
- :type actual_hostname: `bytes`
-
- :return: `True` if *cert_pattern* matches *actual_hostname*, else `False`.
- :rtype: `bool`
- """
- if b"*" in cert_pattern:
- cert_head, cert_tail = cert_pattern.split(b".", 1)
- actual_head, actual_tail = actual_hostname.split(b".", 1)
- if cert_tail != actual_tail:
- return False
- # No patterns for IDNA
- if actual_head.startswith(b"xn--"):
- return False
-
- return cert_head == b"*" or cert_head == actual_head
- else:
- return cert_pattern == actual_hostname
-
-
- def _validate_pattern(cert_pattern):
- """
- Check whether the usage of wildcards within *cert_pattern* conforms with
- our expectations.
-
- :type hostname: `bytes`
-
- :return: None
- """
- cnt = cert_pattern.count(b"*")
- if cnt > 1:
- raise CertificateError(
- "Certificate's DNS-ID {0!r} contains too many wildcards.".format(
- cert_pattern
- )
- )
- parts = cert_pattern.split(b".")
- if len(parts) < 3:
- raise CertificateError(
- "Certificate's DNS-ID {0!r} has too few host components for "
- "wildcard usage.".format(cert_pattern)
- )
- # We assume there will always be only one wildcard allowed.
- if b"*" not in parts[0]:
- raise CertificateError(
- "Certificate's DNS-ID {0!r} has a wildcard outside the left-most "
- "part.".format(cert_pattern)
- )
- if any(not len(p) for p in parts):
- raise CertificateError(
- "Certificate's DNS-ID {0!r} contains empty parts.".format(
- cert_pattern
- )
- )
-
-
- # Ensure no locale magic interferes.
- _TRANS_TO_LOWER = maketrans(
- b"ABCDEFGHIJKLMNOPQRSTUVWXYZ", b"abcdefghijklmnopqrstuvwxyz"
- )
|