agronholm / anyio

High level asynchronous concurrency and networking framework that works on top of either trio or asyncio
MIT License
1.82k stars 139 forks source link

When using a truststore SSLContext, certificate verification fails. #795

Closed LTsCreed closed 1 month ago

LTsCreed commented 1 month ago

Things to check first

AnyIO version

4.6.0

Python version

3.11

What happened?

I encountered an issue while using httpx with truststore SSLContext. Specifically, when the TLS handshake fails, I receive the following error:

  File "C:\Users\zz\AppData\Local\pypoetry\Cache\virtualenvs\test-j-cxRcCV-py3.11\Lib\site-packages\httpcore\_backends\anyio.py", line 71, in start_tls

    ssl_stream = await anyio.streams.tls.TLSStream.wrap(

                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

  File "C:\Users\zz\AppData\Local\pypoetry\Cache\virtualenvs\test-j-cxRcCV-py3.11\Lib\site-packages\anyio\streams\tls.py", line 125, in

wrap

    await wrapper._call_sslobject_method(ssl_object.do_handshake)

  File "C:\Users\zz\AppData\Local\pypoetry\Cache\virtualenvs\test-j-cxRcCV-py3.11\Lib\site-packages\anyio\streams\tls.py", line 163, in

_call_sslobject_method

    or "UNEXPECTED_EOF_WHILE_READING" in exc.strerror

       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

TypeError: argument of type 'NoneType' is not iterable

The root cause seems to be that ssl.SSLCertVerificationError exception is a subclass of ssl.SSLError, but it does not have a strerror attribute

How can we reproduce the bug?


context = truststore.SSLContect(ssl.PROTOCOL_TLS_CLIENT)
client = httpx.AsyncClient(verify=context)

await client.get('__url_with_self_signed_crt_')
agronholm commented 1 month ago

Seems like checking for exc.strerror is None first is the straightforward fix, but how to test for this properly?

agronholm commented 1 month ago

Could you try with the linked PR? And it would also help to have a way to reproduce this.

LTsCreed commented 1 month ago

Yes, it fixes the problem, thanks.

I have looked at other packages, and they don't perform additional checks for UNEXPECTED_EOF_WHILE_READING. Why is it required here?

To reproduce the issue:

OS - Windows truststore==0.9.1 anyio==4.6.0 httpx==0.27.0

import asyncio
import ssl

import truststore
from httpx import AsyncClient

ts = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

client = AsyncClient(verify=ts)

async def main():
    res = await client.get("https://wrong.host.badssl.com")
    print(res.status_code)

asyncio.run(main())
agronholm commented 1 month ago

Because, as I found, OpenSSL is weird and in some cases it didn't return the proper error code when an EOF occurs. I'd wager that most libraries don't even care about EOF in TLS streams. I'm hesitant to remove this check as it's not clear if this is still happening or not.