Skip to content

extmod/asyncio: Add ssl support with SSLContext. #11897

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

Merged
merged 1 commit into from
Dec 14, 2023

Conversation

Carglglz
Copy link
Contributor

@Carglglz Carglglz commented Jun 28, 2023

This adds asyncio ssl support and fix async stream read methods in unix port.
The reason is stream ssl sockets seem to behave different from plain stream sockets. The main difference
is doing partial reads on ssl sockets will clear any write/read/close "event" flag even if there is still
pending data to be read. So registering the ssl socket again in the poller will wait there until a new event
or timeout.
The fix is check first if the ssl socket has pending data and read/return it instead of registering in the poller to wait
for an event.

Follow-up to:

This may close #8647, #9071, #8177, #5840, #7315, #9015

@github-actions
Copy link

github-actions bot commented Jun 28, 2023

Code size report:

   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64:  +616 +0.076% standard
      stm32:  +264 +0.067% PYBV10
     mimxrt:  +264 +0.073% TEENSY40
        rp2:  +272 +0.083% RPI_PICO
       samd:  +272 +0.104% ADAFRUIT_ITSYBITSY_M4_EXPRESS

@codecov
Copy link

codecov bot commented Jun 29, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (f33dfb9) 98.39% compared to head (4848bc3) 98.40%.

❗ Current head 4848bc3 differs from pull request most recent head bfd6ad9. Consider uploading reports for the commit bfd6ad9 to get more accurate results

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #11897   +/-   ##
=======================================
  Coverage   98.39%   98.40%           
=======================================
  Files         159      159           
  Lines       21055    21076   +21     
=======================================
+ Hits        20718    20739   +21     
  Misses        337      337           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@Carglglz Carglglz force-pushed the asyncio-tls branch 4 times, most recently from 3936b2e to b83af3b Compare July 4, 2023 22:27
@Carglglz Carglglz force-pushed the asyncio-tls branch 2 times, most recently from 2175158 to ab3a8eb Compare July 7, 2023 15:08
@dpgeorge dpgeorge added the extmod Relates to extmod/ directory in source label Jul 11, 2023
@Carglglz
Copy link
Contributor Author

Note that the "fix" is more a workaround than a real fix, I'm not sure where in the codebase the "read/write/close" events for stream ssl sockets are handled other than in modssl_mbedtls.c and why the bug affects the unix port but not the esp32 port...

@@ -30,6 +36,15 @@ async def wait_closed(self):
def read(self, n=-1):
r = b""
while True:
if hasattr(self.s, "ssl_pending"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this ssl_pending only needed for the unix port to work correctly? If so I would rather fix unix by making it work correctly when polling an ssl socket.

Copy link
Contributor Author

@Carglglz Carglglz Jul 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not tested any other port besides esp32 and unix, but I think that is the case. It would be nice if someone could test with rp2/stm32 ports to confirm. Revelant test is tests/multi_net/asyncio_tls_server_client_readline.py where the unix port will get stuck without the fix due to this:

The main difference is doing partial reads on ssl sockets will clear any write/read/close "event" flag even if there is
still pending data to be read. So registering the ssl socket again in the poller will wait there until a new event or timeout.

I did try to fix it but to no avail. I only came to the conclusion that stream ssl sockets seem to behave different from plain stream sockets.

If so I would rather fix unix by making it work correctly when polling an ssl socket.

Yes that would be better and I'm curious to see where exactly is the offending line of code 👀

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can confirm that the tests/multi_net/asyncio_tls_server_client_readline.py test works with the ssl_pending code there and fails when it is removed.

But, by enabling MICROPY_PY_SELECT, it will work without the ssl_pending code.

So I will fix the unix port so that it can work without needing to enable that option.

BTW, to run this test I actually had to regenerate the certificate and key, because otherwise it gave a "certificate has expired" error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, by enabling MICROPY_PY_SELECT, it will work without the ssl_pending code.

well that seems like an easy fix 😅

BTW, to run this test I actually had to regenerate the certificate and key, because otherwise it gave a "certificate has expired" error.

I will update the tests too with a longer expiration date then 👍🏼

Copy link
Contributor Author

@Carglglz Carglglz Jul 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, by enabling MICROPY_PY_SELECT, it will work without the ssl_pending code.
So I will fix the unix port so that it can work without needing to enable that option

I will wait for the fix then... 👀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I did try to solve it fixing the poll_set_all_are_fds logic checking the poll_set length but that turned out to be a slippery slope... 😕 so this was the only reliable method I could think of instead...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #12224 for a fix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've just tested #12224 and it seems to be working as expected this time 👍🏼
I've pulled the changes here and CI tests should pass now again.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That PR is now merged to master.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased and all CI tests passing, I guess this is finally ready!

@dpgeorge
Copy link
Member

@Carglglz can you please rebase this on the latest master? Then it's hopefully ready to be merged.

@Carglglz
Copy link
Contributor Author

@dpgeorge I've update the tests with the os.stat logic too 👍🏼


client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client_ctx.verify_mode = ssl.CERT_REQUIRED
client_ctx.load_verify_locations(cafile=ca_cert_chain)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test (and the other 3 added to the net_inet directory) don't run on CPython (at least not 3.11.6) because CPython's load_verify_locations() requires the file to be in PEM format, not DER.

The fix for that is to load the data from the DER here and then pass it in as cadata.

But, that still doesn't seem to have the correct behaviour because I would think that you don't need the line below (the call to load_default_certs()) because it should be enough to load just the DER certificate from mpycert.der in order to access micropython.org. But if I remove the load_default_certs() then CPython cannot connect to micropython.org. If I leave load_default_certs() in place then I can just remove this load_verify_locations() call and CPython works.

So it seems this test is not working as expected on CPython.

Copy link
Contributor Author

@Carglglz Carglglz Dec 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix for that is to load the data from the DER here and then pass it in as cadata.

I agree

But, that still doesn't seem to have the correct behaviour because I would think that you don't need the line below (the call to load_default_certs()) because it should be enough to load just the DER certificate from mpycert.der in order to access micropython.org. But if I remove the load_default_certs() then CPython cannot connect to micropython.org. If I leave load_default_certs() in place then I can just remove this load_verify_locations() call and CPython works.

I could reproduce this, and it seems mbedtls is a bit more "permissive" than OpenSSL when it comes to certificate validation, i.e. mbedtls is OK using "intermediate" CA certificates, while OpenSSL wants the "ultimate" root CA certificate.

The fix is using this root CA certificate that both MicroPython and CPython accept 👍🏼

FWIW I got this root CA certificate from micropython/micropython-lib#633 so if you have time you may want to have a look.

from README.md

5. Default ca-bundle.

certifi package will include CA Root certs ISRG Root X1 which is used to validate MicroPython mip server and DigiCert Global Root CA, which is used to validate github.com. This way mip can validate by default that the package being installed comes in fact from MicroPython package index server, or from the github repo of a 3rd party package.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, using the ISRG certificate is a good idea. Except it doesn't seem to work for me, running that test I get:

CPython:

ssl.SSLError: not enough data: cadata does not contain a certificate (_ssl.c:4020)

MicroPython:

ValueError: invalid cert

Copy link
Contributor Author

@Carglglz Carglglz Dec 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was suspecting something may go wrong after this message, but I wasn't sure...

$ git add net_inet/isrg.der
warning: in the working copy of 'tests/net_inet/isrg.der', CRLF will be replaced by LF the next time Git touches it

I guess I will need to add this in the test itself, the good thing is that it should be valid for a while (notAfter=Jun 4 11:04:38 2035 GMT)

OK: adding *.der binary to .gitattributes does fix it. 😅

@dpgeorge dpgeorge force-pushed the asyncio-tls branch 2 times, most recently from aa7a74d to 4848bc3 Compare December 14, 2023 00:57
This adds asyncio ssl support with SSLContext and the corresponding
tests in `tests/net_inet` and `tests/multi_net`.

Note that not doing the handshake on connect will delegate the handshake to
the following `mbedtls_ssl_read/write` calls.  However if the handshake
fails when a client certificate is required and not presented by the peer,
it needs to be notified of this handshake error (otherwise it will hang
until timeout if any).  Finally at MicroPython side raise the proper
mbedtls error code and message.

Signed-off-by: Carlos Gil <carlosgilglez@gmail.com>
@dpgeorge dpgeorge merged commit bfd6ad9 into micropython:master Dec 14, 2023
@dpgeorge
Copy link
Member

Merged! Thanks again @Carglglz for sticking with this and getting it through.

@Carglglz
Copy link
Contributor Author

@dpgeorge Thank you for the support and for helping to get this through! 👍🏼

@Carglglz Carglglz deleted the asyncio-tls branch December 14, 2023 02:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
extmod Relates to extmod/ directory in source
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Need solution for asyncio SSL/TLS connections
3 participants