You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

_common.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. """
  2. Common verification code.
  3. """
  4. from __future__ import absolute_import, division, print_function
  5. import ipaddress
  6. import re
  7. import attr
  8. from ._compat import maketrans, text_type
  9. from .exceptions import (
  10. CertificateError,
  11. DNSMismatch,
  12. IPAddressMismatch,
  13. SRVMismatch,
  14. URIMismatch,
  15. VerificationError,
  16. )
  17. try:
  18. import idna
  19. except ImportError: # pragma: nocover
  20. idna = None
  21. @attr.s(slots=True)
  22. class ServiceMatch(object):
  23. """
  24. A match of a service id and a certificate pattern.
  25. """
  26. service_id = attr.ib()
  27. cert_pattern = attr.ib()
  28. def verify_service_identity(cert_patterns, obligatory_ids, optional_ids):
  29. """
  30. Verify whether *cert_patterns* are valid for *obligatory_ids* and
  31. *optional_ids*.
  32. *obligatory_ids* must be both present and match. *optional_ids* must match
  33. if a pattern of the respective type is present.
  34. """
  35. errors = []
  36. matches = _find_matches(cert_patterns, obligatory_ids) + _find_matches(
  37. cert_patterns, optional_ids
  38. )
  39. matched_ids = [match.service_id for match in matches]
  40. for i in obligatory_ids:
  41. if i not in matched_ids:
  42. errors.append(i.error_on_mismatch(mismatched_id=i))
  43. for i in optional_ids:
  44. # If an optional ID is not matched by a certificate pattern *but* there
  45. # is a pattern of the same type , it is an error and the verification
  46. # fails. Example: the user passes a SRV-ID for "_mail.domain.com" but
  47. # the certificate contains an SRV-Pattern for "_xmpp.domain.com".
  48. if i not in matched_ids and _contains_instance_of(
  49. cert_patterns, i.pattern_class
  50. ):
  51. errors.append(i.error_on_mismatch(mismatched_id=i))
  52. if errors:
  53. raise VerificationError(errors=errors)
  54. return matches
  55. def _find_matches(cert_patterns, service_ids):
  56. """
  57. Search for matching certificate patterns and service_ids.
  58. :param cert_ids: List certificate IDs like DNSPattern.
  59. :type cert_ids: `list`
  60. :param service_ids: List of service IDs like DNS_ID.
  61. :type service_ids: `list`
  62. :rtype: `list` of `ServiceMatch`
  63. """
  64. matches = []
  65. for sid in service_ids:
  66. for cid in cert_patterns:
  67. if sid.verify(cid):
  68. matches.append(ServiceMatch(cert_pattern=cid, service_id=sid))
  69. return matches
  70. def _contains_instance_of(seq, cl):
  71. """
  72. :type seq: iterable
  73. :type cl: type
  74. :rtype: bool
  75. """
  76. for e in seq:
  77. if isinstance(e, cl):
  78. return True
  79. return False
  80. def _is_ip_address(pattern):
  81. """
  82. Check whether *pattern* could be/match an IP address.
  83. :param pattern: A pattern for a host name.
  84. :type pattern: `bytes` or `unicode`
  85. :return: `True` if *pattern* could be an IP address, else `False`.
  86. :rtype: bool
  87. """
  88. if isinstance(pattern, bytes):
  89. try:
  90. pattern = pattern.decode("ascii")
  91. except UnicodeError:
  92. return False
  93. try:
  94. int(pattern)
  95. return True
  96. except ValueError:
  97. pass
  98. try:
  99. ipaddress.ip_address(pattern.replace("*", "1"))
  100. except ValueError:
  101. return False
  102. return True
  103. @attr.s(init=False, slots=True)
  104. class DNSPattern(object):
  105. """
  106. A DNS pattern as extracted from certificates.
  107. """
  108. pattern = attr.ib()
  109. _RE_LEGAL_CHARS = re.compile(br"^[a-z0-9\-_.]+$")
  110. def __init__(self, pattern):
  111. """
  112. :type pattern: `bytes`
  113. """
  114. if not isinstance(pattern, bytes):
  115. raise TypeError("The DNS pattern must be a bytes string.")
  116. pattern = pattern.strip()
  117. if pattern == b"" or _is_ip_address(pattern) or b"\0" in pattern:
  118. raise CertificateError(
  119. "Invalid DNS pattern {0!r}.".format(pattern)
  120. )
  121. self.pattern = pattern.translate(_TRANS_TO_LOWER)
  122. if b"*" in self.pattern:
  123. _validate_pattern(self.pattern)
  124. @attr.s(slots=True)
  125. class IPAddressPattern(object):
  126. """
  127. An IP address pattern as extracted from certificates.
  128. """
  129. pattern = attr.ib()
  130. @classmethod
  131. def from_bytes(cls, bs):
  132. try:
  133. return cls(pattern=ipaddress.ip_address(bs))
  134. except ValueError:
  135. raise CertificateError(
  136. "Invalid IP address pattern {!r}.".format(bs)
  137. )
  138. @attr.s(init=False, slots=True)
  139. class URIPattern(object):
  140. """
  141. An URI pattern as extracted from certificates.
  142. """
  143. protocol_pattern = attr.ib()
  144. dns_pattern = attr.ib()
  145. def __init__(self, pattern):
  146. """
  147. :type pattern: `bytes`
  148. """
  149. if not isinstance(pattern, bytes):
  150. raise TypeError("The URI pattern must be a bytes string.")
  151. pattern = pattern.strip().translate(_TRANS_TO_LOWER)
  152. if b":" not in pattern or b"*" in pattern or _is_ip_address(pattern):
  153. raise CertificateError(
  154. "Invalid URI pattern {0!r}.".format(pattern)
  155. )
  156. self.protocol_pattern, hostname = pattern.split(b":")
  157. self.dns_pattern = DNSPattern(hostname)
  158. @attr.s(init=False, slots=True)
  159. class SRVPattern(object):
  160. """
  161. An SRV pattern as extracted from certificates.
  162. """
  163. name_pattern = attr.ib()
  164. dns_pattern = attr.ib()
  165. def __init__(self, pattern):
  166. """
  167. :type pattern: `bytes`
  168. """
  169. if not isinstance(pattern, bytes):
  170. raise TypeError("The SRV pattern must be a bytes string.")
  171. pattern = pattern.strip().translate(_TRANS_TO_LOWER)
  172. if (
  173. pattern[0] != b"_"[0]
  174. or b"." not in pattern
  175. or b"*" in pattern
  176. or _is_ip_address(pattern)
  177. ):
  178. raise CertificateError(
  179. "Invalid SRV pattern {0!r}.".format(pattern)
  180. )
  181. name, hostname = pattern.split(b".", 1)
  182. self.name_pattern = name[1:]
  183. self.dns_pattern = DNSPattern(hostname)
  184. @attr.s(init=False, slots=True)
  185. class DNS_ID(object):
  186. """
  187. A DNS service ID, aka hostname.
  188. """
  189. hostname = attr.ib()
  190. # characters that are legal in a normalized hostname
  191. _RE_LEGAL_CHARS = re.compile(br"^[a-z0-9\-_.]+$")
  192. pattern_class = DNSPattern
  193. error_on_mismatch = DNSMismatch
  194. def __init__(self, hostname):
  195. """
  196. :type hostname: `unicode`
  197. """
  198. if not isinstance(hostname, text_type):
  199. raise TypeError("DNS-ID must be a unicode string.")
  200. hostname = hostname.strip()
  201. if hostname == u"" or _is_ip_address(hostname):
  202. raise ValueError("Invalid DNS-ID.")
  203. if any(ord(c) > 127 for c in hostname):
  204. if idna:
  205. ascii_id = idna.encode(hostname)
  206. else:
  207. raise ImportError(
  208. "idna library is required for non-ASCII IDs."
  209. )
  210. else:
  211. ascii_id = hostname.encode("ascii")
  212. self.hostname = ascii_id.translate(_TRANS_TO_LOWER)
  213. if self._RE_LEGAL_CHARS.match(self.hostname) is None:
  214. raise ValueError("Invalid DNS-ID.")
  215. def verify(self, pattern):
  216. """
  217. https://tools.ietf.org/search/rfc6125#section-6.4
  218. """
  219. if isinstance(pattern, self.pattern_class):
  220. return _hostname_matches(pattern.pattern, self.hostname)
  221. else:
  222. return False
  223. @attr.s(slots=True)
  224. class IPAddress_ID(object):
  225. """
  226. An IP address service ID.
  227. """
  228. ip = attr.ib(converter=ipaddress.ip_address)
  229. pattern_class = IPAddressPattern
  230. error_on_mismatch = IPAddressMismatch
  231. def verify(self, pattern):
  232. """
  233. https://tools.ietf.org/search/rfc2818#section-3.1
  234. """
  235. return self.ip == pattern.pattern
  236. @attr.s(init=False, slots=True)
  237. class URI_ID(object):
  238. """
  239. An URI service ID.
  240. """
  241. protocol = attr.ib()
  242. dns_id = attr.ib()
  243. pattern_class = URIPattern
  244. error_on_mismatch = URIMismatch
  245. def __init__(self, uri):
  246. """
  247. :type uri: `unicode`
  248. """
  249. if not isinstance(uri, text_type):
  250. raise TypeError("URI-ID must be a unicode string.")
  251. uri = uri.strip()
  252. if u":" not in uri or _is_ip_address(uri):
  253. raise ValueError("Invalid URI-ID.")
  254. prot, hostname = uri.split(u":")
  255. self.protocol = prot.encode("ascii").translate(_TRANS_TO_LOWER)
  256. self.dns_id = DNS_ID(hostname.strip(u"/"))
  257. def verify(self, pattern):
  258. """
  259. https://tools.ietf.org/search/rfc6125#section-6.5.2
  260. """
  261. if isinstance(pattern, self.pattern_class):
  262. return (
  263. pattern.protocol_pattern == self.protocol
  264. and self.dns_id.verify(pattern.dns_pattern)
  265. )
  266. else:
  267. return False
  268. @attr.s(init=False, slots=True)
  269. class SRV_ID(object):
  270. """
  271. An SRV service ID.
  272. """
  273. name = attr.ib()
  274. dns_id = attr.ib()
  275. pattern_class = SRVPattern
  276. error_on_mismatch = SRVMismatch
  277. def __init__(self, srv):
  278. """
  279. :type srv: `unicode`
  280. """
  281. if not isinstance(srv, text_type):
  282. raise TypeError("SRV-ID must be a unicode string.")
  283. srv = srv.strip()
  284. if u"." not in srv or _is_ip_address(srv) or srv[0] != u"_":
  285. raise ValueError("Invalid SRV-ID.")
  286. name, hostname = srv.split(u".", 1)
  287. self.name = name[1:].encode("ascii").translate(_TRANS_TO_LOWER)
  288. self.dns_id = DNS_ID(hostname)
  289. def verify(self, pattern):
  290. """
  291. https://tools.ietf.org/search/rfc6125#section-6.5.1
  292. """
  293. if isinstance(pattern, self.pattern_class):
  294. return self.name == pattern.name_pattern and self.dns_id.verify(
  295. pattern.dns_pattern
  296. )
  297. else:
  298. return False
  299. def _hostname_matches(cert_pattern, actual_hostname):
  300. """
  301. :type cert_pattern: `bytes`
  302. :type actual_hostname: `bytes`
  303. :return: `True` if *cert_pattern* matches *actual_hostname*, else `False`.
  304. :rtype: `bool`
  305. """
  306. if b"*" in cert_pattern:
  307. cert_head, cert_tail = cert_pattern.split(b".", 1)
  308. actual_head, actual_tail = actual_hostname.split(b".", 1)
  309. if cert_tail != actual_tail:
  310. return False
  311. # No patterns for IDNA
  312. if actual_head.startswith(b"xn--"):
  313. return False
  314. return cert_head == b"*" or cert_head == actual_head
  315. else:
  316. return cert_pattern == actual_hostname
  317. def _validate_pattern(cert_pattern):
  318. """
  319. Check whether the usage of wildcards within *cert_pattern* conforms with
  320. our expectations.
  321. :type hostname: `bytes`
  322. :return: None
  323. """
  324. cnt = cert_pattern.count(b"*")
  325. if cnt > 1:
  326. raise CertificateError(
  327. "Certificate's DNS-ID {0!r} contains too many wildcards.".format(
  328. cert_pattern
  329. )
  330. )
  331. parts = cert_pattern.split(b".")
  332. if len(parts) < 3:
  333. raise CertificateError(
  334. "Certificate's DNS-ID {0!r} has too few host components for "
  335. "wildcard usage.".format(cert_pattern)
  336. )
  337. # We assume there will always be only one wildcard allowed.
  338. if b"*" not in parts[0]:
  339. raise CertificateError(
  340. "Certificate's DNS-ID {0!r} has a wildcard outside the left-most "
  341. "part.".format(cert_pattern)
  342. )
  343. if any(not len(p) for p in parts):
  344. raise CertificateError(
  345. "Certificate's DNS-ID {0!r} contains empty parts.".format(
  346. cert_pattern
  347. )
  348. )
  349. # Ensure no locale magic interferes.
  350. _TRANS_TO_LOWER = maketrans(
  351. b"ABCDEFGHIJKLMNOPQRSTUVWXYZ", b"abcdefghijklmnopqrstuvwxyz"
  352. )