Thanks to visit codestin.com
Credit goes to github.com

Skip to content

extmod/modussl_mbedtls: Updated Wire in support for DTLS. #15764

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 2 commits into from
Feb 14, 2025

Conversation

keenanjohnson
Copy link
Contributor

This is an update to a previous PR: #10062, which updates the PR based on the latest refactoring of the micropython repo.

I opened this PR as I wasn't sure if the original author of #10062 was still interested in pushing the code change through, but I am quite interested in seeing it happen.

What is this PR trying to solve?

This PR enables support for DTLS (i.e. TLS over datagram transport protocols like UDP). While support for DTLS is absent in cpython, it is worth supporting it in micropython because it is the basis of the ubiquitous CoAP protocol, used in many IOT projects. See #5270

How does this PR solve the problem?

A new set of "protocols" are added to SSLContext:

ssl.PROTOCOL_DTLS_CLIENT
ssl.PROTOCOL_DTLS_SERVER
They act as you expect. If one of these is set, the library assumes that the underlying socket is a datagram-like socket (i.e. UDP or similar).

Implement our own timer callbacks as the out of the box implementation relies on gettimeofday().

TODO: To fully support asyncio for DTLS socket, we will need to return a readable or writable event in poll(MP_STREAM_POLL, ...) if _mbedtls_timing_get_delay(self) >= 1. This is left for future work so as not to interfere with #9871.

@keenanjohnson keenanjohnson changed the title extmod/modussl_mbedtls: Wire in support for DTLS. extmod/modussl_mbedtls: Updated Wire in support for DTLS. Sep 2, 2024
Copy link

codecov bot commented Sep 2, 2024

Codecov Report

Attention: Patch coverage is 94.11765% with 1 line in your changes missing coverage. Please review.

Project coverage is 98.53%. Comparing base (aef6705) to head (8987b39).
Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
extmod/modtls_mbedtls.c 94.11% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #15764      +/-   ##
==========================================
- Coverage   98.53%   98.53%   -0.01%     
==========================================
  Files         169      169              
  Lines       21806    21822      +16     
==========================================
+ Hits        21487    21502      +15     
- Misses        319      320       +1     

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

Copy link

github-actions bot commented Sep 2, 2024

Code size report:

   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64: +8520 +1.004% standard[incl +192(data)]
      stm32:    +0 +0.000% PYBV10
     mimxrt:    +0 +0.000% TEENSY40
        rp2: +4896 +0.536% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

@projectgus projectgus self-requested a review September 3, 2024 00:51
@keenanjohnson keenanjohnson marked this pull request as draft September 3, 2024 06:09
@keenanjohnson keenanjohnson marked this pull request as ready for review September 3, 2024 06:20
@dpgeorge dpgeorge added the extmod Relates to extmod/ directory in source label Sep 19, 2024
Copy link
Contributor

@projectgus projectgus left a comment

Choose a reason for hiding this comment

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

Thanks for keeping this PR alive, @keenanjohnson! I can see it being quite useful for anyone using CoAP, we could even consider enabling it by default on some of the bigger ports (such as ESP32) provided the code size impact is not too big ( 🤞 ).

The main concern I have here is adding all these extra elements to the ssl module and its types. This is a CPython specific module, and we're trying to cut down on incompatibilities in these.

(If nothing else, there's also the awkward possibility that some day CPython adds DTLS support and then we have to figure out how to make this API compatible with whatever CPython adopts.)

There is potentially a way forward here. As of b802f0f, there is a tls module (non-CPython compatible) and an ssl module (CPython compatible). The current ssl module just does from tls import * but it doesn't have to do that, it could be changed to only import the CPython-compatible names.

To take the code in this direction probably means significant additional work, though. I think applying the tls/ssl split here would look something like this:

  • Make a separate tls.DTLSSocket (or similar) class, so a CPython-compatible ssl.socket doesn't gain any of the other functions. As a bonus, tls.DTLSSocket can also be minimal and only expose the functions used by DTLS.
  • Can probably leave ssl_context_make_new implementation the same, apart from having it return a tls.DTLSSocket object if a DTLS protocol is requested. The behaviour will stay CPython-compatible unless the caller passes in an SSLContext with the DTLS-only type set.
  • Update ssl.py in micropython-lib so it only imports the CPython-compatible names rather than everything.
  • Add a doc for tls and describe how it's the non-CPython-compatible version of the ssl module. Docs for DTLS can be placed there.

How does that sound? Does it sound reasonable? I admit that it does seem like it could become confusing (i.e. two very similar modules with slightly different behaviour), but it's an awkward situation either way.

We can probably help with some of the steps as well (in particular the last two points), if they become sticking points.

@projectgus
Copy link
Contributor

projectgus commented Oct 8, 2024

Make a separate tls.DTLSSocket (or similar) class, so a CPython-compatible ssl.socket doesn't gain any of the other functions. As a bonus, tls.DTLSSocket can also be minimal and only expose the functions used by DTLS.

I mentioned this to @dpgeorge and he's not keen on a new class for size reasons, so the main compatibility change may just be to change ssl module to not pick up the new protocol enum values. We can probably do this part separately.

EDIT: Removed the part in this comment about not having recv/sendto/etc in the DTLS socket, as this is useful to keep a UDP-compatible API.

@keenanjohnson
Copy link
Contributor Author

Hey @projectgus yes that all makes sense. I see that #15905 has been merged as well. Other than fixing the test above, is there any other outstanding changes I can make here before we bring this in?

@keenanjohnson
Copy link
Contributor Author

Tests are now fixed. The outstanding items are improving the test coverage and fixing the commit messages.

@keenanjohnson
Copy link
Contributor Author

@projectgus it seems like no matter what I do I can't appease the code coverage metric.

@keenanjohnson
Copy link
Contributor Author

It seems like the main issue is that the coverage test is skipping my dtls test: https://github.com/micropython/micropython/actions/runs/12241647550/job/34147207388#step:5:880

@projectgus or @dpgeorge am I doing something wrong is setting up the tests here?

I am enabling the DTLS support only in the unix/mbedtls port, but perhaps that is not the correct place?

@projectgus
Copy link
Contributor

projectgus commented Dec 10, 2024

It seems like the main issue is that the coverage test is skipping my dtls test: https://github.com/micropython/micropython/actions/runs/12241647550/job/34147207388#step:5:880

It's a bit fiddly, what is happening is that the test runner is running the test in MicroPython, where it prints nothing and passes. Then it runs it in CPython, where it prints "SKIP". Then it's comparing the output and saying "the test has failed", because the output doesn't match.!

The documentation for this has just improved, see the new explanation of test types here: https://github.com/micropython/micropython/tree/master/tests#micropython-test-suite

You can run this test locally and reproduce the failure by:

  1. Build the micropython unix port. You can build the exact "coverage" variant by passing VARIANT=coverage if you need, but the test also fails on the standard variant which is easier to run as its the default that the test runner uses. So recommend just building the standard (default) variant.
  2. Go to the tests directory and run ./run-tests.py -i ports/unix/ssl_dtls

What I suggest for a fix is to have the test print something on success, and then add an .exp file with that expected output. For example:

diff --git c/tests/ports/unix/ssl_dtls.py i/tests/ports/unix/ssl_dtls.py
index ec45eb877a..a40736c785 100644
--- c/tests/ports/unix/ssl_dtls.py
+++ i/tests/ports/unix/ssl_dtls.py
@@ -14,3 +14,4 @@ except NameError:
 # Test constructing with valid arguments
 dtls_client = ssl.SSLContext(PROTOCOL_DTLS_CLIENT)
 dtls_server = ssl.SSLContext(PROTOCOL_DTLS_SERVER)
+print("OK")
diff --git c/tests/ports/unix/ssl_dtls.py.exp i/tests/ports/unix/ssl_dtls.py.exp
new file mode 100644
index 0000000000..d86bac9de5
--- /dev/null
+++ i/tests/ports/unix/ssl_dtls.py.exp
@@ -0,0 +1 @@
+OK

@projectgus
Copy link
Contributor

Once you have the CI passing, the other step to do is to rebase the branch and squash all these commits down to one. Jimmo has a MicroPython-specific guide here: https://github.com/jimmo/git-and-micropython#README - although any git-related guide about "rebasing" and "squashing" will be able to give you the gist.

If you get totally stuck, one of us can help you. Please don't solve the problem by closing this PR and opening a new one, this shouldn't be necessary! Cheers.

@keenanjohnson
Copy link
Contributor Author

OK

Once you have the CI passing, the other step to do is to rebase the branch and squash all these commits down to one. Jimmo has a MicroPython-specific guide here: https://github.com/jimmo/git-and-micropython#README - although any git-related guide about "rebasing" and "squashing" will be able to give you the gist.

If you get totally stuck, one of us can help you. Please don't solve the problem by closing this PR and opening a new one, this shouldn't be necessary! Cheers.

Thanks @projectgus ! I'm comfortable with rebasing, so I'll do that once all the builds are working :)

@keenanjohnson
Copy link
Contributor Author

Ok I have reduced the duplicated test, added the basic .exp file, and moved the enable flag to the correct file as suggested by @projectgus in discord, but it still seems to be skipping the test whether I use the tls or ssl module in the test. Am I missing something else obvious?

@keenanjohnson
Copy link
Contributor Author

I think the main implementation here is all done, but just trying to get the code coverage metric up. Just one more tricky timing line to hit before the code coverage bot is satisfied. Am I doing the testing in the preferred way for micropython @projectgus ?

@dpgeorge dpgeorge self-assigned this Feb 4, 2025
@projectgus projectgus removed their request for review February 4, 2025 02:55
@projectgus
Copy link
Contributor

I saw that you self requested the review again @projectgus and I appreciate it! I believe I've addressed all previous feedback, so to should be ready. Appreciate you and all the work you do!

Looks like Damien will do the final review for this, but thanks for the kind words!

@dpgeorge
Copy link
Member

dpgeorge commented Feb 7, 2025

@keenanjohnson I'm doing a final review here and trying to test it, but not having any luck. Do you have some (simple) example (Micro)Python code for both a DTLS server and DTLS client that can talk to each other, just send a small amount of data between each other?

@@ -37,6 +37,8 @@
#include "py/stream.h"
#include "py/objstr.h"
#include "py/reader.h"
#include "py/smallint.h"
Copy link
Member

Choose a reason for hiding this comment

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

This header shouldn't be needed anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed!


def write(self, data):
self.write_buffer.extend(data)
return len(data)
Copy link
Member

Choose a reason for hiding this comment

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

The write_buffer is never actually used. So this function could just do return len(data).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed!

def readinto(self, buf):
l = min(len(self.read_buffer), len(buf))
if l == 0:
return None
Copy link
Member

Choose a reason for hiding this comment

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

The read_buffer is always empty, so this function could just always do return None.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed!

@dpgeorge
Copy link
Member

I finally managed to write a test where MicroPython is both a DTLS server and client, and they connect to each other, do a handshake, and transfer some data between themselves.

It works well for unix<->unix.

It works well for unix<->PYBD_SF2 and unix<->RPI_PICO_W, when unix is the DTLS server.

But when PYBD_SF2 or RPI_PICO_W is the DTLS server then it rarely works. I'm pretty sure this is because the lwIP driver on those boards can only queue one UDP packet at a time. With a quick hack to queue up to two UDP packets, these boards as a DTLS server work much more reliably.

Note: to properly implement a DTLS server, we need to implement socket.recvfrom(n, MSG_PEEK) (I worked around this in my test, but it would be good to add that peek feature, it's quite easy to add).

@keenanjohnson
Copy link
Contributor Author

Hey @dpgeorge ! Thanks for taking a look. I was traveling for a few days and didn't get around to emails I guess.

That makes sense that there might be some issues with the DTLS server on all ports. I've only tested on unix and esp32 and mostly with the device as a client rather than the server (which is the more common usecase I think).

I've addressed your small comments above and as for the message peek feature should we try to implement that in this PR or would you prefer I remove the server functionality in this PR for now and address adding that with the peak in a separate changeset as I imagine the desire for the server side of DTLS is pretty small in the community?

@keenanjohnson
Copy link
Contributor Author

If we do want to add the peek feature here, I assume we would modify the extmod/modsocket.c implementation of recvfrom()? Also if you post your test code @dpgeorge I can try to verify with the exact same code here to save the hassle of me trying to recreate what you've already done.

@keenanjohnson keenanjohnson force-pushed the pr/dtls branch 2 times, most recently from 85cb312 to 7be8329 Compare February 13, 2025 21:11
@keenanjohnson
Copy link
Contributor Author

Resolved a small merge conflict.

@dpgeorge
Copy link
Member

Also if you post your test code @dpgeorge I can try to verify with the exact same code here to save the hassle of me trying to recreate what you've already done.

I've now added a commit to this PR with the test that I made. It runs well under unix/unix with the multi-test running:

$ cd tests
$ ./run-multitests.py multi_net/tls_dtls_server_client.py

That should pass. It should run as part of the CI as well.

If we do want to add the peek feature here,

No, let's not add that here, it's a separate feature. And it's not necessary for the test.

And let's keep this PR as-is with both server and client support. It works fine with the unix port.

@keenanjohnson
Copy link
Contributor Author

Great and thanks for your support! I think the summarize there two follow up change-sets we've discussed after this is merged:

  1. Splitting out the TLS / SSL docs to make them more reasonable.
  2. Implementing the socket.recvfrom(n, MSG_PEEK) to fully support the DTLS server on PYBD_SF2 or RPI_PICO_W

Is there anything else open on this PR that I can help with?

@dpgeorge
Copy link
Member

Is there anything else open on this PR that I can help with?

Were you able to successfully run the test that I added? If you are happy with that test, then this PR should be good to go.

@keenanjohnson
Copy link
Contributor Author

Yes the test runs locally for me! I can also confirm that the CI system runs it successfully!
https://github.com/micropython/micropython/actions/runs/13318657734/job/37198676006#step:4:3454

keenanjohnson and others added 2 commits February 14, 2025 12:55
This commit enables support for DTLS, i.e. TLS over datagram transport
protocols like UDP.  While support for DTLS is absent in CPython, it is
worth supporting it in MicroPython because it is the basis of the
ubiquitous CoAP protocol, used in many IoT projects.

To select DTLS, a new set of "protocols" are added to SSLContext:
- ssl.PROTOCOL_DTLS_CLIENT
- ssl.PROTOCOL_DTLS_SERVER

If one of these is set, the library assumes that the underlying socket is a
datagram-like socket (i.e. UDP or similar).

Our own timer callbacks are implemented because the out of the box
implementation relies on `gettimeofday()`.

This new DTLS feature is enabled on all ports that use mbedTLS.

This commit is an update to a previous PR micropython#10062.

Addresses issue micropython#5270 which requested DTLS support.

Signed-off-by: Keenan Johnson <[email protected]>
This adds a multi-test for DTLS server and client behaviour.  It works on
all ports that enable this feature (eg unix, esp32, rp2, stm32), but
bare-metal ports that use lwIP are not reliable as the DTLS server because
the lwIP bindings only support queuing one UDP packet at a time (that needs
to be fixed).

Also, to properly implement a DTLS server sockets need to support
`socket.recvfrom(n, MSG_PEEK)`.  That can be implemented in the future.

Signed-off-by: Damien George <[email protected]>
@dpgeorge dpgeorge merged commit 8987b39 into micropython:master Feb 14, 2025
64 of 65 checks passed
@dpgeorge
Copy link
Member

Thanks for testing.

Now merged!

@keenanjohnson
Copy link
Contributor Author

Thank you for the review!

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.

3 participants