-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
Conversation
Code size report:
|
Codecov ReportAll modified and coverable lines are covered by tests ✅
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. |
3936b2e
to
b83af3b
Compare
2175158
to
ab3a8eb
Compare
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 |
extmod/asyncio/stream.py
Outdated
@@ -30,6 +36,15 @@ async def wait_closed(self): | |||
def read(self, n=-1): | |||
r = b"" | |||
while True: | |||
if hasattr(self.s, "ssl_pending"): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 👀
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 👍🏼
There was a problem hiding this comment.
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... 👀
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
a68b999
to
6ac75ad
Compare
6833d43
to
8d8ea5a
Compare
d283028
to
b20f564
Compare
b20f564
to
824bc21
Compare
@Carglglz can you please rebase this on the latest master? Then it's hopefully ready to be merged. |
53c7abe
to
1cf31f6
Compare
@dpgeorge I've update the tests with the |
|
||
client_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) | ||
client_ctx.verify_mode = ssl.CERT_REQUIRED | ||
client_ctx.load_verify_locations(cafile=ca_cert_chain) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 certsISRG Root X1
which is used to validate MicroPythonmip
server andDigiCert Global Root CA
, which is used to validategithub.com
. This waymip
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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. 😅
1cf31f6
to
8e5201f
Compare
8e5201f
to
e3e838f
Compare
aa7a74d
to
4848bc3
Compare
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>
4848bc3
to
bfd6ad9
Compare
Merged! Thanks again @Carglglz for sticking with this and getting it through. |
@dpgeorge Thank you for the support and for helping to get this through! 👍🏼 |
This adds
asyncio
ssl supportand 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:
- [ ] extmod/modssl_mbedtls: Add cert time validation. #11896This may close #8647, #9071, #8177, #5840, #7315, #9015