tls.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. from __future__ import annotations
  2. __all__ = (
  3. "TLSAttribute",
  4. "TLSConnectable",
  5. "TLSListener",
  6. "TLSStream",
  7. )
  8. import logging
  9. import re
  10. import ssl
  11. import sys
  12. from collections.abc import Callable, Mapping
  13. from dataclasses import dataclass
  14. from functools import wraps
  15. from ssl import SSLContext
  16. from typing import Any, TypeAlias, TypeVar
  17. from .. import (
  18. BrokenResourceError,
  19. EndOfStream,
  20. aclose_forcefully,
  21. get_cancelled_exc_class,
  22. to_thread,
  23. )
  24. from .._core._typedattr import TypedAttributeSet, typed_attribute
  25. from ..abc import (
  26. AnyByteStream,
  27. AnyByteStreamConnectable,
  28. ByteStream,
  29. ByteStreamConnectable,
  30. Listener,
  31. TaskGroup,
  32. )
  33. if sys.version_info >= (3, 11):
  34. from typing import TypeVarTuple, Unpack
  35. else:
  36. from typing_extensions import TypeVarTuple, Unpack
  37. if sys.version_info >= (3, 12):
  38. from typing import override
  39. else:
  40. from typing_extensions import override
  41. T_Retval = TypeVar("T_Retval")
  42. PosArgsT = TypeVarTuple("PosArgsT")
  43. _PCTRTT: TypeAlias = tuple[tuple[str, str], ...]
  44. _PCTRTTT: TypeAlias = tuple[_PCTRTT, ...]
  45. class TLSAttribute(TypedAttributeSet):
  46. """Contains Transport Layer Security related attributes."""
  47. #: the selected ALPN protocol
  48. alpn_protocol: str | None = typed_attribute()
  49. #: the channel binding for type ``tls-unique``
  50. channel_binding_tls_unique: bytes = typed_attribute()
  51. #: the selected cipher
  52. cipher: tuple[str, str, int] = typed_attribute()
  53. #: the peer certificate in dictionary form (see :meth:`ssl.SSLSocket.getpeercert`
  54. # for more information)
  55. peer_certificate: None | (dict[str, str | _PCTRTTT | _PCTRTT]) = typed_attribute()
  56. #: the peer certificate in binary form
  57. peer_certificate_binary: bytes | None = typed_attribute()
  58. #: ``True`` if this is the server side of the connection
  59. server_side: bool = typed_attribute()
  60. #: ciphers shared by the client during the TLS handshake (``None`` if this is the
  61. #: client side)
  62. shared_ciphers: list[tuple[str, str, int]] | None = typed_attribute()
  63. #: the :class:`~ssl.SSLObject` used for encryption
  64. ssl_object: ssl.SSLObject = typed_attribute()
  65. #: ``True`` if this stream does (and expects) a closing TLS handshake when the
  66. #: stream is being closed
  67. standard_compatible: bool = typed_attribute()
  68. #: the TLS protocol version (e.g. ``TLSv1.2``)
  69. tls_version: str = typed_attribute()
  70. @dataclass(eq=False)
  71. class TLSStream(ByteStream):
  72. """
  73. A stream wrapper that encrypts all sent data and decrypts received data.
  74. This class has no public initializer; use :meth:`wrap` instead.
  75. All extra attributes from :class:`~TLSAttribute` are supported.
  76. :var AnyByteStream transport_stream: the wrapped stream
  77. """
  78. transport_stream: AnyByteStream
  79. standard_compatible: bool
  80. _ssl_object: ssl.SSLObject
  81. _read_bio: ssl.MemoryBIO
  82. _write_bio: ssl.MemoryBIO
  83. @classmethod
  84. async def wrap(
  85. cls,
  86. transport_stream: AnyByteStream,
  87. *,
  88. server_side: bool | None = None,
  89. hostname: str | None = None,
  90. ssl_context: ssl.SSLContext | None = None,
  91. standard_compatible: bool = True,
  92. ) -> TLSStream:
  93. """
  94. Wrap an existing stream with Transport Layer Security.
  95. This performs a TLS handshake with the peer.
  96. :param transport_stream: a bytes-transporting stream to wrap
  97. :param server_side: ``True`` if this is the server side of the connection,
  98. ``False`` if this is the client side (if omitted, will be set to ``False``
  99. if ``hostname`` has been provided, ``False`` otherwise). Used only to create
  100. a default context when an explicit context has not been provided.
  101. :param hostname: host name of the peer (if host name checking is desired)
  102. :param ssl_context: the SSLContext object to use (if not provided, a secure
  103. default will be created)
  104. :param standard_compatible: if ``False``, skip the closing handshake when
  105. closing the connection, and don't raise an exception if the peer does the
  106. same
  107. :raises ~ssl.SSLError: if the TLS handshake fails
  108. """
  109. if server_side is None:
  110. server_side = not hostname
  111. if not ssl_context:
  112. purpose = (
  113. ssl.Purpose.CLIENT_AUTH if server_side else ssl.Purpose.SERVER_AUTH
  114. )
  115. ssl_context = ssl.create_default_context(purpose)
  116. # Re-enable detection of unexpected EOFs if it was disabled by Python
  117. if hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"):
  118. ssl_context.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF
  119. bio_in = ssl.MemoryBIO()
  120. bio_out = ssl.MemoryBIO()
  121. # Resolve international host names using IDNA 2008.
  122. # Otherwise wrap_bio() would resolve them with IDNA 2003.
  123. if hostname is not None:
  124. from .._core._sockets import idna2008_resolve
  125. server_hostname: bytes | None = idna2008_resolve(hostname)
  126. else:
  127. server_hostname = None
  128. # External SSLContext implementations may do blocking I/O in wrap_bio(),
  129. # but the standard library implementation won't
  130. if type(ssl_context) is ssl.SSLContext:
  131. ssl_object = ssl_context.wrap_bio(
  132. bio_in,
  133. bio_out,
  134. server_side=server_side,
  135. server_hostname=server_hostname,
  136. )
  137. else:
  138. ssl_object = await to_thread.run_sync(
  139. ssl_context.wrap_bio,
  140. bio_in,
  141. bio_out,
  142. server_side,
  143. server_hostname,
  144. None,
  145. )
  146. wrapper = cls(
  147. transport_stream=transport_stream,
  148. standard_compatible=standard_compatible,
  149. _ssl_object=ssl_object,
  150. _read_bio=bio_in,
  151. _write_bio=bio_out,
  152. )
  153. await wrapper._call_sslobject_method(ssl_object.do_handshake)
  154. return wrapper
  155. async def _call_sslobject_method(
  156. self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT]
  157. ) -> T_Retval:
  158. while True:
  159. try:
  160. result = func(*args)
  161. except ssl.SSLWantReadError:
  162. try:
  163. # Flush any pending writes first
  164. if self._write_bio.pending:
  165. await self.transport_stream.send(self._write_bio.read())
  166. data = await self.transport_stream.receive()
  167. except EndOfStream:
  168. self._read_bio.write_eof()
  169. except OSError as exc:
  170. self._read_bio.write_eof()
  171. self._write_bio.write_eof()
  172. raise BrokenResourceError from exc
  173. else:
  174. self._read_bio.write(data)
  175. except ssl.SSLWantWriteError:
  176. await self.transport_stream.send(self._write_bio.read())
  177. except ssl.SSLSyscallError as exc:
  178. self._read_bio.write_eof()
  179. self._write_bio.write_eof()
  180. raise BrokenResourceError from exc
  181. except ssl.SSLError as exc:
  182. self._read_bio.write_eof()
  183. self._write_bio.write_eof()
  184. if isinstance(exc, ssl.SSLEOFError) or (
  185. exc.strerror and "UNEXPECTED_EOF_WHILE_READING" in exc.strerror
  186. ):
  187. if self.standard_compatible:
  188. raise BrokenResourceError from exc
  189. else:
  190. raise EndOfStream from None
  191. raise
  192. else:
  193. # Flush any pending writes first
  194. if self._write_bio.pending:
  195. await self.transport_stream.send(self._write_bio.read())
  196. return result
  197. async def unwrap(self) -> tuple[AnyByteStream, bytes]:
  198. """
  199. Does the TLS closing handshake.
  200. :return: a tuple of (wrapped byte stream, bytes left in the read buffer)
  201. """
  202. await self._call_sslobject_method(self._ssl_object.unwrap)
  203. self._read_bio.write_eof()
  204. self._write_bio.write_eof()
  205. return self.transport_stream, self._read_bio.read()
  206. async def aclose(self) -> None:
  207. if self.standard_compatible:
  208. try:
  209. await self.unwrap()
  210. except BaseException:
  211. await aclose_forcefully(self.transport_stream)
  212. raise
  213. await self.transport_stream.aclose()
  214. async def receive(self, max_bytes: int = 65536) -> bytes:
  215. if max_bytes < 1:
  216. raise ValueError("max_bytes must be a positive integer")
  217. data = await self._call_sslobject_method(self._ssl_object.read, max_bytes)
  218. if not data:
  219. raise EndOfStream
  220. return data
  221. async def send(self, item: bytes) -> None:
  222. await self._call_sslobject_method(self._ssl_object.write, item)
  223. async def send_eof(self) -> None:
  224. tls_version = self.extra(TLSAttribute.tls_version)
  225. match = re.match(r"TLSv(\d+)(?:\.(\d+))?", tls_version)
  226. if match:
  227. major, minor = int(match.group(1)), int(match.group(2) or 0)
  228. if (major, minor) < (1, 3):
  229. raise NotImplementedError(
  230. f"send_eof() requires at least TLSv1.3; current "
  231. f"session uses {tls_version}"
  232. )
  233. raise NotImplementedError(
  234. "send_eof() has not yet been implemented for TLS streams"
  235. )
  236. @property
  237. def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
  238. return {
  239. **self.transport_stream.extra_attributes,
  240. TLSAttribute.alpn_protocol: self._ssl_object.selected_alpn_protocol,
  241. TLSAttribute.channel_binding_tls_unique: (
  242. self._ssl_object.get_channel_binding
  243. ),
  244. TLSAttribute.cipher: self._ssl_object.cipher,
  245. TLSAttribute.peer_certificate: lambda: self._ssl_object.getpeercert(False),
  246. TLSAttribute.peer_certificate_binary: lambda: self._ssl_object.getpeercert(
  247. True
  248. ),
  249. TLSAttribute.server_side: lambda: self._ssl_object.server_side,
  250. TLSAttribute.shared_ciphers: lambda: (
  251. self._ssl_object.shared_ciphers()
  252. if self._ssl_object.server_side
  253. else None
  254. ),
  255. TLSAttribute.standard_compatible: lambda: self.standard_compatible,
  256. TLSAttribute.ssl_object: lambda: self._ssl_object,
  257. TLSAttribute.tls_version: self._ssl_object.version,
  258. }
  259. @dataclass(eq=False)
  260. class TLSListener(Listener[TLSStream]):
  261. """
  262. A convenience listener that wraps another listener and auto-negotiates a TLS session
  263. on every accepted connection.
  264. If the TLS handshake times out or raises an exception,
  265. :meth:`handle_handshake_error` is called to do whatever post-mortem processing is
  266. deemed necessary.
  267. Supports only the :attr:`~TLSAttribute.standard_compatible` extra attribute.
  268. :param Listener listener: the listener to wrap
  269. :param ssl_context: the SSL context object
  270. :param standard_compatible: a flag passed through to :meth:`TLSStream.wrap`
  271. :param handshake_timeout: time limit for the TLS handshake
  272. (passed to :func:`~anyio.fail_after`)
  273. """
  274. listener: Listener[Any]
  275. ssl_context: ssl.SSLContext
  276. standard_compatible: bool = True
  277. handshake_timeout: float = 30
  278. @staticmethod
  279. async def handle_handshake_error(exc: BaseException, stream: AnyByteStream) -> None:
  280. """
  281. Handle an exception raised during the TLS handshake.
  282. This method does 3 things:
  283. #. Forcefully closes the original stream
  284. #. Logs the exception (unless it was a cancellation exception) using the
  285. ``anyio.streams.tls`` logger
  286. #. Reraises the exception if it was a base exception or a cancellation exception
  287. :param exc: the exception
  288. :param stream: the original stream
  289. """
  290. await aclose_forcefully(stream)
  291. # Log all except cancellation exceptions
  292. if not isinstance(exc, get_cancelled_exc_class()):
  293. # CPython (as of 3.11.5) returns incorrect `sys.exc_info()` here when using
  294. # any asyncio implementation, so we explicitly pass the exception to log
  295. # (https://github.com/python/cpython/issues/108668). Trio does not have this
  296. # issue because it works around the CPython bug.
  297. logging.getLogger(__name__).exception(
  298. "Error during TLS handshake", exc_info=exc
  299. )
  300. # Only reraise base exceptions and cancellation exceptions
  301. if not isinstance(exc, Exception) or isinstance(exc, get_cancelled_exc_class()):
  302. raise
  303. async def serve(
  304. self,
  305. handler: Callable[[TLSStream], Any],
  306. task_group: TaskGroup | None = None,
  307. ) -> None:
  308. @wraps(handler)
  309. async def handler_wrapper(stream: AnyByteStream) -> None:
  310. from .. import fail_after
  311. try:
  312. with fail_after(self.handshake_timeout):
  313. wrapped_stream = await TLSStream.wrap(
  314. stream,
  315. ssl_context=self.ssl_context,
  316. standard_compatible=self.standard_compatible,
  317. )
  318. except BaseException as exc:
  319. await self.handle_handshake_error(exc, stream)
  320. else:
  321. await handler(wrapped_stream)
  322. await self.listener.serve(handler_wrapper, task_group)
  323. async def aclose(self) -> None:
  324. await self.listener.aclose()
  325. @property
  326. def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]:
  327. return {
  328. TLSAttribute.standard_compatible: lambda: self.standard_compatible,
  329. }
  330. class TLSConnectable(ByteStreamConnectable):
  331. """
  332. Wraps another connectable and does TLS negotiation after a successful connection.
  333. :param connectable: the connectable to wrap
  334. :param hostname: host name of the server (if host name checking is desired)
  335. :param ssl_context: the SSLContext object to use (if not provided, a secure default
  336. will be created)
  337. :param standard_compatible: if ``False``, skip the closing handshake when closing
  338. the connection, and don't raise an exception if the server does the same
  339. """
  340. def __init__(
  341. self,
  342. connectable: AnyByteStreamConnectable,
  343. *,
  344. hostname: str | None = None,
  345. ssl_context: ssl.SSLContext | None = None,
  346. standard_compatible: bool = True,
  347. ) -> None:
  348. self.connectable = connectable
  349. self.ssl_context: SSLContext = ssl_context or ssl.create_default_context(
  350. ssl.Purpose.SERVER_AUTH
  351. )
  352. if not isinstance(self.ssl_context, ssl.SSLContext):
  353. raise TypeError(
  354. "ssl_context must be an instance of ssl.SSLContext, not "
  355. f"{type(self.ssl_context).__name__}"
  356. )
  357. self.hostname = hostname
  358. self.standard_compatible = standard_compatible
  359. @override
  360. async def connect(self) -> TLSStream:
  361. stream = await self.connectable.connect()
  362. try:
  363. return await TLSStream.wrap(
  364. stream,
  365. hostname=self.hostname,
  366. ssl_context=self.ssl_context,
  367. standard_compatible=self.standard_compatible,
  368. )
  369. except BaseException:
  370. await aclose_forcefully(stream)
  371. raise