Skip to content

extmod/mbedtls: Implement DTLS HelloVerify, update test for MSG_PEEK #17441

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

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

projectgus
Copy link
Contributor

Summary

  • Updates the DTLS support initially added in extmod/modussl_mbedtls: Updated Wire in support for DTLS. #15764.
  • DTLS 1.2 requires the HelloVerify protocol. This was already enabled on esp32 port (part of ESP-IDF config), and this PR enables it on bare metal ports as well as anti-replay attack protection.
  • HelloVerify requires a session cookie store to be implemented in the application that calls mbedTLS. This function is added to the tls module SSLContext, and is transparent to Python code (provided the same SSLContext is reused).
  • Add a config flag to disable DTLS on bare metal ports that don't need it. Enabled on "Extra" and above by default, so I think all ports with mbedTLS will get DTLS by default.
  • Updated the multi_net test to work with HelloVerify and the new MSG_PEEK receive flag added in extmod/modlwip,esp32,unix: Add support for socket recv flags argument #17312. No longer needs an additional byte to be sent at the start of each connection.

TODO

  • The current session cookie algorithm is too naive and doesn't achieve the purpose of HelloVerify (and can be used to DoS the server by creating too many cookies). It should be updated along the lines of the suggestion in the DTLS specification (Cookie = HMAC(Secret, Client-IP, Client-Parameters)).

Testing

  • [*] Ran the new DTLS multi_net test on rp2, stm32 (PYBD_SF2) and esp32 ports. I'm still having (seemingly unrelated) issues with high outgoing UDP packet loss on rp2, but stm32 and esp32 ports passed the test in both directions.
  • Re-run test with the new session cookie algorithm.
  • Manually test the simple DTLS server provided with mbedTLS against a MicroPython client.

Trade-offs and Alternatives

  • It's not clear how high the risk of MicroPython DTLS servers amplifying a DDoS attack is, so perhaps the HelloVerify support is not needed on the server side. However, connecting to a (compliant) DTLS server requires the DTLS client to implement HelloVerify, and mbedTLS (reasonably) treats HelloVerify as enabled for both server and client concurrently so we need to implement something on the server side.

This work was funded through GitHub sponsors.

@projectgus projectgus added the extmod Relates to extmod/ directory in source label Jun 6, 2025
@projectgus
Copy link
Contributor Author

While reviewing this myself I realised the server DTLS cookie support I implemented is far too naive. Have marked as draft until I have time to improve it.

Copy link

codecov bot commented Jun 6, 2025

Codecov Report

Attention: Patch coverage is 86.66667% with 2 lines in your changes missing coverage. Please review.

Project coverage is 98.53%. Comparing base (9bde125) to head (25d8871).

Files with missing lines Patch % Lines
extmod/modtls_mbedtls.c 86.66% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #17441      +/-   ##
==========================================
- Coverage   98.54%   98.53%   -0.01%     
==========================================
  Files         169      169              
  Lines       21941    21954      +13     
==========================================
+ Hits        21621    21632      +11     
- Misses        320      322       +2     

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

github-actions bot commented Jun 6, 2025

Code size report:

   bare-arm:    +0 +0.000% 
minimal x86:    +0 +0.000% 
   unix x64: +2256 +0.264% standard
      stm32:    +0 +0.000% PYBV10
     mimxrt:    +0 +0.000% TEENSY40
        rp2: +1240 +0.135% RPI_PICO_W
       samd:    +0 +0.000% ADAFRUIT_ITSYBITSY_M4_EXPRESS
  qemu rv32:    +0 +0.000% VIRT_RV32

@projectgus projectgus force-pushed the bugfix/dtls_session_cookies branch from 1722c92 to 4f55510 Compare June 6, 2025 01:45
@projectgus
Copy link
Contributor Author

projectgus commented Jun 12, 2025

  • The current session cookie algorithm is too naive and doesn't achieve the purpose of HelloVerify (and can be used to DoS the server by creating too many cookies). It should be updated along the lines of the suggestion in the DTLS specification (Cookie = HMAC(Secret, Client-IP, Client-Parameters)).

This is a lot harder than I initially thought, because in MicroPython modmbedtls works with an abstract stream and doesn't have any knowledge of the client IP. Also, adding this will probably add even more code size. But what's currently in this draft is woefully inadequate.

There's two possible ways forward:

  1. Add another Python API extension, so the server code does something like s = ctx.wrap_socket(s, server_side=1, client_cookie=hmac_result) or s = ctx.dtls_wrap_socket(...). This offloads the responsibility (and code size cost) for calculating a secure cookie to Python code (and we could provide a sample implementation or a thin wrapper in micropython-lib that does it correctly). The server code can choose to pass a constant string here, at the cost of potentially becoming a DDoS vector if exposed to untrusted clients.
  2. Drop DTLS server support, support DTLS client only. This makes testing a little harder as we'll need a DTLS server endpoint, but a simple no-op server should be easy enough to stand up (and safe to stand up on the public internet if it implements Hello Verify!)

Leaning towards (2) at the moment because I think the use case for DTLS client in MicroPython is much stronger than the case for needing a DTLS server, but that would mean removing functionality that was in v1.25 (albeit not fully compliant in that version). @keenanjohnson you implemented the original DTLS support in #15764, do you have any thought about this?

@@ -669,6 +685,51 @@ static mp_obj_t ssl_socket_make_new(mp_obj_ssl_context_t *ssl_context, mp_obj_t
mbedtls_raise_error(ret);
}

#if defined(MBEDTLS_SSL_DTLS_HELLO_VERIFY) && defined(MBEDTLS_SSL_SRV_C)
// DTLS Server session cookie support
static int ssl_cookie_write(void *ctx, unsigned char **p, unsigned char *end,
Copy link
Member

Choose a reason for hiding this comment

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

Would it be simpler and more general to require that these functions be implemented in Python, and the user passes them through when wrapping the socket? Eg:

cookie_dict = {}
def dtls_cookie_handler(ctx, key, value):
    if value is None:
        # create and return new cookie
        cookie = os.urandom(32)
        cookie_dict[key] = cookie
        return cookie
    else:
        # check cookie
        return cookie_dict.get(key) == value
ctx.wrap_socket(..., dtls_cookie=dtls_cookie_handler)

@keenanjohnson
Copy link
Contributor

In regards to your question @projectgus , I personally don't see a lot of use for the DTLS server on an embedded device. However, I do think that having the server implemented in micropython does make the unit testing vastly less brittle. Every time I have tied unit tests to some sort of live public service (like you suggest in option 2), I have personally regretted the choice in the long run as it tends to be brittle, people start to encounter all sorts of spurious local test failiures, etc etc.

Given that our main usecase for the server is just unit tests, I feel like moving the cookie calculation to python isn't the worst as long as we caveat the potential for a DDOS in the docs or something in case anyone ever comes along and has a real use case for the server.

Thanks for thinking of me and great work on the project as always.

This is already enabled in the ESP-IDF mbedTLS config, so provide an
implementation of the cookie store functions. This allows DTLS connections
between two esp32 boards.

The session cookie store is a very simple dictionary associated with the
SSLContext. To work, the server needs to reuse the same SSLContext (but
cookies are never cleaned up, so a server with a high number of clients
should recycle the context periodically.)

Server code still needs to handle the MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED
error by waiting for the next UDP packet from the client.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
- DTLS spec recommends HelloVerify and Anti Replay protection be enabled,
  and these are enabled in the default mbedTLS config. Implement them here.

- To help compensate for the possible increase in code size, add a
  MICROPY_PY_SSL_DTLS build config macro that's enabled for EXTRA and
  above by default.

This allows bare metal mbedTLS ports to use DTLS with HelloVerify support.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
The original version of this test had to exchange a 1 byte UDP packet
before the DTLS handshake. This is no longer needed due to MSG_PEEK
support.

The test also doesn't work with HelloVerify enabled, as the first
connection attempt always fails with an
MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED result. Anticipate this by listening
for the client twice on the server side.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
This is an annoying regression caused by including mpconfig.h in 36922df -
the mimxrt platform headers define ARRAY_SIZE and mbedtls also defines in
some source files, using a different parameter name which is a warning in
gcc.

Technically mimxrt SDK is to blame here, but as this isn't a named warning
in gcc the only way to work around it in the mimxrt port would be to
disable all warnings when building this particular mbedTLS source file.

This work was funded through GitHub Sponsors.

Signed-off-by: Angus Gratton <angus@redyak.com.au>
@projectgus projectgus force-pushed the bugfix/dtls_session_cookies branch from 4f55510 to 25d8871 Compare June 12, 2025 05:47
@projectgus
Copy link
Contributor Author

projectgus commented Jun 12, 2025

Thanks @dpgeorge and @keenanjohnson for your thoughts:

Alternative 1

I've pushed a version that uses the mbedTLS ssl_cookie implementation in C. All that the Python code needs to do is supply a client transport identifier (i.e. bytes representing IP & port) when it wraps the socket.

Code size cost is less bad than I thought: rp2: +1240. ESP32 impact will be a little lower as some of this was compiled in already. But still a lot of code size!

Alternative 2

Alternative to this, along the lines suggested by @dpgeorge above, would be to implement the cookie support in Python - but it probably still need to be a two step process in order to pass through the client's transport id:

cookie_dict = {}
def dtls_cookie_handler(ctx, key, value):
    encoded_cookie = binary encoding of (timestamp, HMAC(secret, key + timestamp)
    if value is None:
        return encoded_cookie
    else:
        # compare encoded_cookie matches `value` and also check the prepended timestamp isn't too old

ctx.set_dtls_cookie_handler(dtls_cookier_handler)

while True:
        _, client_addr = s.recvfrom(1, socket.MSG_PEEK)
        print("incoming connection")
        s.connect(client_addr)  # Connect back to the client

        # Wrap the UDP socket in server mode.
        try:
            s = ctx.wrap_socket(s, server_side=1, client_id=repr(client_addr).encode())

Alternative 3

There's a slightly hackier version where we pass it all through to wrap_socket() in one go, i.e.:

  client_id = repr(client_addr).encode()
  s = ctx.wrap_socket(s, server_side=1,
            cookie_handler= lambda ctx, key, value: dtls_client_handler(ctx, key, value, client_id))

... but the necessary implementation is hacky as would need to pass the mp_obj_t pointer via the client_id bytes and then decode it on the other side. If cookie_handler needs to stay valid after wrap_socket returns (unclear) then it'd need to also be stored somewhere in the ssl socket object so GC doesn't pick it up.

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