_common.py 11 KB

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