Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
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.

hazmat.py 12KB

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