-
-
Notifications
You must be signed in to change notification settings - Fork 392
Bugfix: Send the TLS alert to the peer upon a ssl.CertificateError.
#3413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sasozivanovic
wants to merge
8
commits into
python-trio:main
Choose a base branch
from
sasozivanovic:ssl_errors
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+162
−18
Open
Changes from 7 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
fd8eebb
Bugfix: Send the TLS alert to the peer upon a `ssl.CertificateError`.
sasozivanovic 34bd8d8
Convert the conditional into the assertion.
sasozivanovic 284ded4
Rename the news fragment.
sasozivanovic 1356ae5
Added a comment on the timeout in `test_client_certificate`.
sasozivanovic 86b33f3
Tests: use `datetime.timezone.utc` instead of `datetime.UTC`.
sasozivanovic dc74283
Tests: Add `| str` to the annotations of `expect_fail`.
sasozivanovic 02aee02
`test_ssl_client_basics`: use `RaisesGroup` instead of `raises`.
sasozivanovic 71fe831
Another attempt to fix type checking.
sasozivanovic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| Upon a ``SSL.CertificateError``, a TLS alert is now sent to the peer before | ||
| raising ``trio.BrokenResourceError`` from the original error, preventing the | ||
| client from hanging. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import datetime | ||
| import os | ||
| import socket as stdlib_socket | ||
| import ssl | ||
|
|
@@ -77,6 +78,13 @@ | |
|
|
||
| TRIO_TEST_1_CERT.configure_cert(SERVER_CTX) | ||
|
|
||
| SERVER_CTX_REQ = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) | ||
| if hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"): | ||
| SERVER_CTX_REQ.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF | ||
| TRIO_TEST_1_CERT.configure_cert(SERVER_CTX_REQ) | ||
| SERVER_CTX_REQ.verify_mode = ssl.CERT_REQUIRED | ||
| TRIO_TEST_CA.configure_trust(SERVER_CTX_REQ) | ||
|
|
||
|
|
||
| # TLS 1.3 has a lot of changes from previous versions. So we want to run tests | ||
| # with both TLS 1.3, and TLS 1.2. | ||
|
|
@@ -105,7 +113,10 @@ def client_ctx(request: pytest.FixtureRequest) -> ssl.SSLContext: | |
| def ssl_echo_serve_sync( | ||
| sock: stdlib_socket.socket, | ||
| *, | ||
| expect_fail: bool = False, | ||
| # expect_fail = "raise" expects to fail but raises nevertheless, i.e. it is | ||
| # like False but does not print; used where the raised exception is checked | ||
| # in the main thread. | ||
| expect_fail: bool | str = False, | ||
| ) -> None: | ||
| try: | ||
| wrapped = SERVER_CTX.wrap_socket( | ||
|
|
@@ -142,7 +153,9 @@ def ssl_echo_serve_sync( | |
| except (ConnectionResetError, ConnectionAbortedError): # pragma: no cover | ||
| return | ||
| except Exception as exc: | ||
| if expect_fail: | ||
| if expect_fail == "raise": | ||
| raise | ||
| elif expect_fail: | ||
| print("ssl_echo_serve_sync got error as expected:", exc) | ||
| else: # pragma: no cover | ||
| print("ssl_echo_serve_sync got unexpected error:", exc) | ||
|
|
@@ -158,7 +171,9 @@ def ssl_echo_serve_sync( | |
| # (running in a thread). Useful for testing making connections with different | ||
| # SSLContexts. | ||
| @asynccontextmanager | ||
| async def ssl_echo_server_raw(expect_fail: bool = False) -> AsyncIterator[SocketStream]: | ||
| async def ssl_echo_server_raw( | ||
| expect_fail: bool | str = False, | ||
| ) -> AsyncIterator[SocketStream]: | ||
| a, b = stdlib_socket.socketpair() | ||
| async with trio.open_nursery() as nursery: | ||
| # Exiting the 'with a, b' context manager closes the sockets, which | ||
|
|
@@ -178,7 +193,7 @@ async def ssl_echo_server_raw(expect_fail: bool = False) -> AsyncIterator[Socket | |
| @asynccontextmanager | ||
| async def ssl_echo_server( | ||
| client_ctx: SSLContext, | ||
| expect_fail: bool = False, | ||
| expect_fail: bool | str = False, | ||
| ) -> AsyncIterator[SSLStream[Stream]]: | ||
| async with ssl_echo_server_raw(expect_fail=expect_fail) as sock: | ||
| yield SSLStream(sock, client_ctx, server_hostname="trio-test-1.example.org") | ||
|
|
@@ -453,21 +468,35 @@ async def test_ssl_client_basics(client_ctx: SSLContext) -> None: | |
| await s.aclose() | ||
|
|
||
| # Didn't configure the CA file, should fail | ||
| async with ssl_echo_server_raw(expect_fail=True) as sock: | ||
| bad_client_ctx = ssl.create_default_context() | ||
| s = SSLStream(sock, bad_client_ctx, server_hostname="trio-test-1.example.org") | ||
| assert not s.server_side | ||
| with pytest.raises(BrokenResourceError) as excinfo: | ||
| await s.send_all(b"x") | ||
| assert isinstance(excinfo.value.__cause__, ssl.SSLError) | ||
| # Check that the server receives TLSV1_ALERT_UNKNOWN_CA | ||
| # rather than choking with UNEXPECTED_EOF_WHILE_READING. | ||
| with pytest.RaisesGroup( | ||
| pytest.RaisesExc(ssl.SSLError, match="TLSV1_ALERT_UNKNOWN_CA") | ||
| ) as excinfo: | ||
| async with ssl_echo_server_raw(expect_fail="raise") as sock: | ||
| bad_client_ctx = ssl.create_default_context() | ||
| s = SSLStream( | ||
| sock, bad_client_ctx, server_hostname="trio-test-1.example.org" | ||
| ) | ||
| assert not s.server_side | ||
| with pytest.RaisesGroup( | ||
| BrokenResourceError, allow_unwrapped=True, flatten_subgroups=True | ||
| ) as excinfo: | ||
| await s.send_all(b"x") | ||
| assert isinstance(excinfo.value.__cause__, ssl.SSLError) | ||
|
A5rocks marked this conversation as resolved.
Outdated
|
||
|
|
||
| # Trusted CA, but wrong host name | ||
| async with ssl_echo_server_raw(expect_fail=True) as sock: | ||
| s = SSLStream(sock, client_ctx, server_hostname="trio-test-2.example.org") | ||
| assert not s.server_side | ||
| with pytest.raises(BrokenResourceError) as excinfo: | ||
| await s.send_all(b"x") | ||
| assert isinstance(excinfo.value.__cause__, ssl.CertificateError) | ||
| # Check that the server receives SSLV3_ALERT_BAD_CERTIFICATE | ||
| # rather than choking with UNEXPECTED_EOF_WHILE_READING. | ||
| with pytest.RaisesGroup( | ||
| pytest.RaisesExc(ssl.SSLError, match="SSLV3_ALERT_BAD_CERTIFICATE") | ||
| ) as excinfo: | ||
|
A5rocks marked this conversation as resolved.
Outdated
|
||
| async with ssl_echo_server_raw(expect_fail="raise") as sock: | ||
| s = SSLStream(sock, client_ctx, server_hostname="trio-test-2.example.org") | ||
| assert not s.server_side | ||
| with pytest.raises(BrokenResourceError) as excinfo: | ||
| await s.send_all(b"x") | ||
| assert isinstance(excinfo.value.__cause__, ssl.CertificateError) | ||
|
A5rocks marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| async def test_ssl_server_basics(client_ctx: SSLContext) -> None: | ||
|
|
@@ -503,6 +532,103 @@ def client() -> None: | |
| t.join() | ||
|
|
||
|
|
||
| async def test_client_certificate(client_ctx: SSLContext) -> None: | ||
|
|
||
| # A valid client certificate | ||
| client_cert = TRIO_TEST_CA.issue_cert("[email protected]") | ||
| client_cert.configure_cert(client_ctx) | ||
|
|
||
| a, b = stdlib_socket.socketpair() | ||
| with a, b: | ||
| server_sock = tsocket.from_stdlib_socket(b) | ||
| server_transport = SSLStream( | ||
| SocketStream(server_sock), | ||
| SERVER_CTX_REQ, | ||
| server_side=True, | ||
| ) | ||
| assert server_transport.server_side | ||
|
|
||
| def client() -> None: | ||
| with client_ctx.wrap_socket( | ||
| a, | ||
| server_hostname="trio-test-1.example.org", | ||
| ) as client_sock: | ||
| client_sock.sendall(b"x") | ||
| assert client_sock.recv(1) == b"y" | ||
| client_sock.sendall(b"z") | ||
| client_sock.unwrap() | ||
|
|
||
| t = threading.Thread(target=client) | ||
| t.start() | ||
|
|
||
| assert await server_transport.receive_some(1) == b"x" | ||
| await server_transport.send_all(b"y") | ||
| assert await server_transport.receive_some(1) == b"z" | ||
| assert await server_transport.receive_some(1) == b"" | ||
| await server_transport.aclose() | ||
|
|
||
| t.join() | ||
|
|
||
| # An expired client certificate: this should fail | ||
| client_cert = TRIO_TEST_CA.issue_cert( | ||
| "[email protected]", not_after=datetime.datetime.now(datetime.timezone.utc) | ||
| ) | ||
| client_cert.configure_cert(client_ctx) | ||
|
|
||
| a, b = stdlib_socket.socketpair() | ||
| with a, b: | ||
| server_sock = tsocket.from_stdlib_socket(b) | ||
| server_transport = SSLStream( | ||
| SocketStream(server_sock), | ||
| SERVER_CTX_REQ, | ||
| server_side=True, | ||
| ) | ||
| assert server_transport.server_side | ||
|
|
||
| # Prior to the changes to _ssl.py made in this commit, this test hangs | ||
| # without the timeout introduced below. With the new _ssl.py, | ||
| # pytest.raises in the client successfully catches the error; the | ||
| # thread therefore finishes, setting client_done. With the old | ||
| # _ssl.py, the thread hangs and will be abandoned after the timeout, | ||
| # leaving client_done unset and thereby triggering the assertion error. | ||
| # (The general timeout imposed on tests is not only too long for this, | ||
| # but it also doesn't work, because the thread is not abandoned.) | ||
| # | ||
| # Potential problem: determinism. It is highly unlikely but I guess it | ||
| # could happen that the client thread doesn't get from .recv to | ||
| # client_done.set in the allotted time, resulting in a false negative. | ||
|
|
||
| client_done = trio.Event() | ||
|
|
||
| def client() -> None: | ||
| # For TLS 1.3, pytest.raises is ok around .sendall below, but it | ||
| # needs to be here for TLS 1.2. (Is this because TLS 1.2 verifies | ||
| # client-side certificates during the initial handshake?) | ||
| with pytest.raises(ssl.SSLError, match="SSLV3_ALERT_CERTIFICATE_EXPIRED"): | ||
| with client_ctx.wrap_socket( | ||
| a, | ||
| server_hostname="trio-test-1.example.org", | ||
| ) as client_sock: | ||
| client_sock.sendall(b"x") | ||
| client_sock.recv(1) | ||
| trio.from_thread.run_sync(client_done.set) | ||
|
|
||
| async with trio.open_nursery() as nursery: | ||
| nursery.start_soon( | ||
| partial(trio.to_thread.run_sync, client, abandon_on_cancel=True) | ||
| ) | ||
| with pytest.raises(BrokenResourceError) as excinfo: | ||
| assert await server_transport.receive_some(1) == b"x" | ||
| assert isinstance(excinfo.value.__cause__, ssl.SSLError) | ||
| assert excinfo.value.__cause__.reason == "CERTIFICATE_VERIFY_FAILED" | ||
| # This timeout shouldn't affect the execution time of the test on | ||
| # healthy code. | ||
| with trio.move_on_after(0.1): | ||
| await client_done.wait() | ||
| nursery.cancel_scope.cancel() | ||
| assert client_done.is_set(), "The client thread is hanging" | ||
|
|
||
|
|
||
| async def test_attributes(client_ctx: SSLContext) -> None: | ||
| async with ssl_echo_server_raw(expect_fail=True) as sock: | ||
| good_ctx = client_ctx | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.