Skip to content

Commit 9dd660f

Browse files
fix asyncio SSL transports not sending the fatal TLS alert to the
peer when the TLS handshake fails
1 parent dd2faeb commit 9dd660f

3 files changed

Lines changed: 70 additions & 2 deletions

File tree

Lib/asyncio/sslproto.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,12 @@ def _on_handshake_complete(self, handshake_exc):
588588
msg = 'SSL handshake failed on verifying the certificate'
589589
else:
590590
msg = 'SSL handshake failed'
591+
# gh-98078: When the handshake fails, OpenSSL leaves the fatal
592+
# TLS alert (for example "bad certificate" or "protocol
593+
# version") in the outgoing BIO. Send it to the peer before
594+
# closing the transport so that it knows why the handshake
595+
# failed.
596+
self._process_outgoing()
591597
self._fatal_error(exc, msg)
592598
self._wakeup_waiter(exc)
593599
return

Lib/test/test_asyncio/test_sslproto.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,14 +753,16 @@ def test_create_connection_ssl_failed_certificate(self):
753753
sslctx = test_utils.simple_server_sslcontext()
754754
client_sslctx = test_utils.simple_client_sslcontext(
755755
disable_verify=False)
756+
server_err = None
756757

757758
def server(sock):
759+
nonlocal server_err
758760
try:
759761
sock.start_tls(
760762
sslctx,
761763
server_side=True)
762-
except ssl.SSLError:
763-
pass
764+
except ssl.SSLError as exc:
765+
server_err = exc
764766
except OSError:
765767
pass
766768
finally:
@@ -780,6 +782,62 @@ async def client(addr):
780782
with self.assertRaises(ssl.SSLCertVerificationError):
781783
self.loop.run_until_complete(client(srv.addr))
782784

785+
# gh-98078: the client must send a fatal TLS alert to the
786+
# server instead of just closing the connection, so that the
787+
# server knows why the handshake failed.
788+
self.assertIsInstance(server_err, ssl.SSLError)
789+
self.assertIn('ALERT_UNKNOWN_CA', server_err.reason or '')
790+
791+
def test_create_server_ssl_failed_handshake_sends_alert(self):
792+
# gh-98078: when the handshake fails, the server must send the
793+
# fatal TLS alert generated by OpenSSL to the client before
794+
# closing the connection, so that the client knows why the
795+
# handshake failed (here: no TLS version in common).
796+
if not ssl.HAS_TLSv1_3 or not ssl.HAS_TLSv1_2:
797+
self.skipTest('needs TLSv1.2 and TLSv1.3 support')
798+
799+
self.loop.set_exception_handler(lambda loop, ctx: None)
800+
801+
server_context = test_utils.simple_server_sslcontext()
802+
server_context.minimum_version = ssl.TLSVersion.TLSv1_3
803+
client_context = test_utils.simple_client_sslcontext()
804+
client_context.maximum_version = ssl.TLSVersion.TLSv1_2
805+
806+
client_done = self.loop.create_future()
807+
client_err = None
808+
809+
def client(sock, addr):
810+
nonlocal client_err
811+
try:
812+
sock.settimeout(self.TIMEOUT)
813+
sock.connect(addr)
814+
try:
815+
sock.start_tls(client_context)
816+
except OSError as exc:
817+
client_err = exc
818+
finally:
819+
sock.close()
820+
finally:
821+
self.loop.call_soon_threadsafe(
822+
client_done.set_result, None)
823+
824+
async def run_main():
825+
server = await self.loop.create_server(
826+
asyncio.Protocol, '127.0.0.1', 0, ssl=server_context)
827+
addr = server.sockets[0].getsockname()
828+
try:
829+
with self.tcp_client(lambda sock: client(sock, addr),
830+
timeout=self.TIMEOUT):
831+
await asyncio.wait_for(client_done, self.TIMEOUT)
832+
finally:
833+
server.close()
834+
await server.wait_closed()
835+
836+
self.loop.run_until_complete(run_main())
837+
838+
self.assertIsInstance(client_err, ssl.SSLError)
839+
self.assertIn('ALERT_PROTOCOL_VERSION', client_err.reason or '')
840+
783841
def test_start_tls_client_corrupted_ssl(self):
784842
self.loop.set_exception_handler(lambda loop, ctx: None)
785843

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :mod:`asyncio` SSL transports not sending the fatal TLS alert to the
2+
peer when the TLS handshake fails. The peer can now tell why the handshake
3+
failed (for example, certificate verification failure or no TLS version in
4+
common) instead of seeing the connection abruptly closed.

0 commit comments

Comments
 (0)