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

Merged
merged 4 commits into from
Jul 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 42 additions & 6 deletions docs/library/ssl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class SSLContext
Set the available ciphers for sockets created with this context. *ciphers* should be
a list of strings in the `IANA cipher suite format <https://wiki.mozilla.org/Security/Cipher_Suites>`_ .

.. method:: SSLContext.wrap_socket(sock, *, server_side=False, do_handshake_on_connect=True, server_hostname=None)
.. method:: SSLContext.wrap_socket(sock, *, server_side=False, do_handshake_on_connect=True, server_hostname=None, client_id=None)

Takes a `stream` *sock* (usually socket.socket instance of ``SOCK_STREAM`` type),
and returns an instance of ssl.SSLSocket, wrapping the underlying stream.
Expand All @@ -89,6 +89,9 @@ class SSLContext
server certificate. It also sets the name for Server Name Indication (SNI), allowing the server
to present the proper certificate.

- *client_id* is a MicroPython-specific extension argument used only when implementing a DTLS
Server. See :ref:`dtls` for details.

.. warning::

Some implementations of ``ssl`` module do NOT validate server certificates,
Expand Down Expand Up @@ -117,6 +120,8 @@ Exceptions

This exception does NOT exist. Instead its base class, OSError, is used.

.. _dtls:

DTLS support
------------

Expand All @@ -125,16 +130,47 @@ DTLS support

This is a MicroPython extension.

This module supports DTLS in client and server mode via the `PROTOCOL_DTLS_CLIENT`
and `PROTOCOL_DTLS_SERVER` constants that can be used as the ``protocol`` argument
of `SSLContext`.
On most ports, this module supports DTLS in client and server mode via the
`PROTOCOL_DTLS_CLIENT` and `PROTOCOL_DTLS_SERVER` constants that can be used as
the ``protocol`` argument of `SSLContext`.

In this case the underlying socket is expected to behave as a datagram socket (i.e.
like the socket opened with ``socket.socket`` with ``socket.AF_INET`` as ``af`` and
``socket.SOCK_DGRAM`` as ``type``).

DTLS is only supported on ports that use mbed TLS, and it is not enabled by default:
it requires enabling ``MBEDTLS_SSL_PROTO_DTLS`` in the specific port configuration.
DTLS is only supported on ports that use mbedTLS, and it is enabled by default
in most configurations but can be manually disabled by defining
``MICROPY_PY_SSL_DTLS`` to 0.

DTLS server support
^^^^^^^^^^^^^^^^^^^

MicroPython's DTLS server support is configured with "Hello Verify" as required
for DTLS 1.2. This is transparent for DTLS clients, but there are relevant
considerations when implementing a DTLS server in MicroPython:

- The server should pass an additional argument *client_id* when calling
`SSLContext.wrap_socket()`. This ID must be a `bytes` object (or similar) with
a transport-specific identifier representing the client.

The simplest approach is to convert the tuple of ``(client_ip, client_port)``
returned from ``socket.recv_from()`` into a byte string, i.e.::

_, client_addr = sock.recvfrom(1, socket.MSG_PEEK)
sock.connect(client_addr) # Connect back to the client
sock = ssl_ctx.wrap_socket(sock, server_side=True,
client_id=repr(client_addr).encode())

- The first time a client connects, the server call to ``wrap_socket`` will fail
with a `OSError` error "Hello Verify Required". This is because the DTLS
"Hello Verify" cookie is not yet known by the client. If the same client
connects a second time then ``wrap_socket`` will succeed.

- DTLS cookies for "Hello Verify" are associated with the `SSLContext` object,
so the same `SSLContext` object should be used to wrap a subsequent connection
from the same client. The cookie implementation includes a timeout and has
constant memory use regardless of how many clients connect, so it's OK to
reuse the same `SSLContext` object for the lifetime of the server.

Constants
---------
Expand Down
14 changes: 13 additions & 1 deletion extmod/mbedtls/mbedtls_config_common.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
#ifndef MICROPY_INCLUDED_MBEDTLS_CONFIG_COMMON_H
#define MICROPY_INCLUDED_MBEDTLS_CONFIG_COMMON_H

#include "py/mpconfig.h"

// If you want to debug MBEDTLS uncomment the following and
// pass "3" to mbedtls_debug_set_threshold in socket_new.
// #define MBEDTLS_DEBUG_C
Expand Down Expand Up @@ -89,12 +91,18 @@
#define MBEDTLS_SHA384_C
#define MBEDTLS_SHA512_C
#define MBEDTLS_SSL_CLI_C
#define MBEDTLS_SSL_PROTO_DTLS
#define MBEDTLS_SSL_SRV_C
#define MBEDTLS_SSL_TLS_C
#define MBEDTLS_X509_CRT_PARSE_C
#define MBEDTLS_X509_USE_C

#if MICROPY_PY_SSL_DTLS
#define MBEDTLS_SSL_PROTO_DTLS
#define MBEDTLS_SSL_DTLS_ANTI_REPLAY
#define MBEDTLS_SSL_DTLS_HELLO_VERIFY
#define MBEDTLS_SSL_COOKIE_C
#endif

// A port may enable this option to select additional bare-metal configuration.
#if MICROPY_MBEDTLS_CONFIG_BARE_METAL

Expand All @@ -115,4 +123,8 @@ void m_tracked_free(void *ptr);

#endif

// Workaround for a mimxrt platform driver header that defines ARRAY_SIZE,
// which is also defined in some mbedtls source files.
#undef ARRAY_SIZE

#endif // MICROPY_INCLUDED_MBEDTLS_CONFIG_COMMON_H
58 changes: 53 additions & 5 deletions extmod/modtls_mbedtls.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
#include "mbedtls/ecdsa.h"
#include "mbedtls/asn1.h"
#endif
#ifdef MBEDTLS_SSL_DTLS_HELLO_VERIFY
#include "mbedtls/ssl_cookie.h"
#endif

#ifndef MICROPY_MBEDTLS_CONFIG_BARE_METAL
#define MICROPY_MBEDTLS_CONFIG_BARE_METAL (0)
Expand All @@ -75,7 +78,7 @@
#define MP_PROTOCOL_TLS_CLIENT 0
#define MP_PROTOCOL_TLS_SERVER MP_ENDPOINT_IS_SERVER
#define MP_PROTOCOL_DTLS_CLIENT MP_TRANSPORT_IS_DTLS
#define MP_PROTOCOL_DTLS_SERVER MP_ENDPOINT_IS_SERVER | MP_TRANSPORT_IS_DTLS
#define MP_PROTOCOL_DTLS_SERVER (MP_ENDPOINT_IS_SERVER | MP_TRANSPORT_IS_DTLS)

// This corresponds to an SSLContext object.
typedef struct _mp_obj_ssl_context_t {
Expand All @@ -92,6 +95,10 @@ typedef struct _mp_obj_ssl_context_t {
#if MICROPY_PY_SSL_ECDSA_SIGN_ALT
mp_obj_t ecdsa_sign_callback;
#endif
#ifdef MBEDTLS_SSL_DTLS_HELLO_VERIFY
bool is_dtls_server;
mbedtls_ssl_cookie_ctx cookie_ctx;
#endif
} mp_obj_ssl_context_t;

// This corresponds to an SSLSocket object.
Expand All @@ -117,7 +124,8 @@ static const mp_obj_type_t ssl_socket_type;
static const MP_DEFINE_STR_OBJ(mbedtls_version_obj, MBEDTLS_VERSION_STRING_FULL);

static mp_obj_t ssl_socket_make_new(mp_obj_ssl_context_t *ssl_context, mp_obj_t sock,
bool server_side, bool do_handshake_on_connect, mp_obj_t server_hostname);
bool server_side, bool do_handshake_on_connect, mp_obj_t server_hostname,
mp_obj_t client_id);

/******************************************************************************/
// Helper functions.
Expand Down Expand Up @@ -320,6 +328,19 @@ static mp_obj_t ssl_context_make_new(const mp_obj_type_t *type_in, size_t n_args
mbedtls_ssl_conf_dbg(&self->conf, mbedtls_debug, NULL);
#endif

#ifdef MBEDTLS_SSL_DTLS_HELLO_VERIFY
self->is_dtls_server = (protocol == MP_PROTOCOL_DTLS_SERVER);
if (self->is_dtls_server) {
mbedtls_ssl_cookie_init(&self->cookie_ctx);
ret = mbedtls_ssl_cookie_setup(&self->cookie_ctx, mbedtls_ctr_drbg_random, &self->ctr_drbg);
if (ret != 0) {
mbedtls_raise_error(ret);
}
mbedtls_ssl_conf_dtls_cookies(&self->conf, mbedtls_ssl_cookie_write, mbedtls_ssl_cookie_check,
&self->cookie_ctx);
}
#endif // MBEDTLS_SSL_DTLS_HELLO_VERIFY

return MP_OBJ_FROM_PTR(self);
}

Expand Down Expand Up @@ -366,6 +387,11 @@ static mp_obj_t ssl_context___del__(mp_obj_t self_in) {
mbedtls_ctr_drbg_free(&self->ctr_drbg);
mbedtls_entropy_free(&self->entropy);
mbedtls_ssl_config_free(&self->conf);
#ifdef MBEDTLS_SSL_DTLS_HELLO_VERIFY
if (self->is_dtls_server) {
mbedtls_ssl_cookie_free(&self->cookie_ctx);
}
#endif
return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_1(ssl_context___del___obj, ssl_context___del__);
Expand Down Expand Up @@ -468,11 +494,14 @@ static mp_obj_t ssl_context_load_verify_locations(mp_obj_t self_in, mp_obj_t cad
static MP_DEFINE_CONST_FUN_OBJ_2(ssl_context_load_verify_locations_obj, ssl_context_load_verify_locations);

static mp_obj_t ssl_context_wrap_socket(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
enum { ARG_server_side, ARG_do_handshake_on_connect, ARG_server_hostname };
enum { ARG_server_side, ARG_do_handshake_on_connect, ARG_server_hostname, ARG_client_id };
static const mp_arg_t allowed_args[] = {
{ MP_QSTR_server_side, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = false} },
{ MP_QSTR_do_handshake_on_connect, MP_ARG_KW_ONLY | MP_ARG_BOOL, {.u_bool = true} },
{ MP_QSTR_server_hostname, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} },
#ifdef MBEDTLS_SSL_DTLS_HELLO_VERIFY
{ MP_QSTR_client_id, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_rom_obj = MP_ROM_NONE} },
#endif
};

// Parse arguments.
Expand All @@ -481,9 +510,14 @@ static mp_obj_t ssl_context_wrap_socket(size_t n_args, const mp_obj_t *pos_args,
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args - 2, pos_args + 2, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);

mp_obj_t client_id = mp_const_none;
#ifdef MBEDTLS_SSL_DTLS_HELLO_VERIFY
client_id = args[ARG_client_id].u_obj;
#endif

// Create and return the new SSLSocket object.
return ssl_socket_make_new(self, sock, args[ARG_server_side].u_bool,
args[ARG_do_handshake_on_connect].u_bool, args[ARG_server_hostname].u_obj);
args[ARG_do_handshake_on_connect].u_bool, args[ARG_server_hostname].u_obj, client_id);
}
static MP_DEFINE_CONST_FUN_OBJ_KW(ssl_context_wrap_socket_obj, 2, ssl_context_wrap_socket);

Expand Down Expand Up @@ -580,7 +614,7 @@ static int _mbedtls_timing_get_delay(void *ctx) {
#endif

static mp_obj_t ssl_socket_make_new(mp_obj_ssl_context_t *ssl_context, mp_obj_t sock,
bool server_side, bool do_handshake_on_connect, mp_obj_t server_hostname) {
bool server_side, bool do_handshake_on_connect, mp_obj_t server_hostname, mp_obj_t client_id) {

// Store the current SSL context.
store_active_context(ssl_context);
Expand Down Expand Up @@ -635,6 +669,20 @@ static mp_obj_t ssl_socket_make_new(mp_obj_ssl_context_t *ssl_context, mp_obj_t
mbedtls_ssl_set_timer_cb(&o->ssl, o, _mbedtls_timing_set_delay, _mbedtls_timing_get_delay);
#endif

#ifdef MBEDTLS_SSL_DTLS_HELLO_VERIFY
if (ssl_context->is_dtls_server) {
// require the client_id parameter for DTLS (as per mbedTLS requirement)
ret = MBEDTLS_ERR_SSL_BAD_INPUT_DATA;
mp_buffer_info_t buf;
if (mp_get_buffer(client_id, &buf, MP_BUFFER_READ)) {
ret = mbedtls_ssl_set_client_transport_id(&o->ssl, buf.buf, buf.len);
}
if (ret != 0) {
goto cleanup;
}
}
#endif

mbedtls_ssl_set_bio(&o->ssl, &o->sock, _mbedtls_ssl_send, _mbedtls_ssl_recv, NULL);

if (do_handshake_on_connect) {
Expand Down
5 changes: 5 additions & 0 deletions py/mpconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -1941,6 +1941,11 @@ typedef time_t mp_timestamp_t;
#define MICROPY_PY_SSL_MBEDTLS_NEED_ACTIVE_CONTEXT (MICROPY_PY_SSL_ECDSA_SIGN_ALT)
#endif

// Whether to support DTLS protocol (non-CPython feature)
#ifndef MICROPY_PY_SSL_DTLS
#define MICROPY_PY_SSL_DTLS (MICROPY_SSL_MBEDTLS && MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_EXTRA_FEATURES)
#endif

// Whether to provide the "vfs" module
#ifndef MICROPY_PY_VFS
#define MICROPY_PY_VFS (MICROPY_CONFIG_ROM_LEVEL_AT_LEAST_CORE_FEATURES && MICROPY_VFS)
Expand Down
12 changes: 11 additions & 1 deletion tests/extmod/tls_dtls.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,19 @@ def ioctl(self, req, arg):
# Wrap the DTLS Server
dtls_server_ctx = SSLContext(PROTOCOL_DTLS_SERVER)
dtls_server_ctx.verify_mode = CERT_NONE
dtls_server = dtls_server_ctx.wrap_socket(server_socket, do_handshake_on_connect=False)
dtls_server = dtls_server_ctx.wrap_socket(
server_socket, do_handshake_on_connect=False, client_id=b'dummy_client_id'
)
print("Wrapped DTLS Server")

# wrap DTLS server with invalid client_id
try:
dtls_server = dtls_server_ctx.wrap_socket(
server_socket, do_handshake_on_connect=False, client_id=4
)
except OSError:
print("Failed to wrap DTLS Server with invalid client_id")

# Wrap the DTLS Client
dtls_client_ctx = SSLContext(PROTOCOL_DTLS_CLIENT)
dtls_client_ctx.verify_mode = CERT_NONE
Expand Down
1 change: 1 addition & 0 deletions tests/extmod/tls_dtls.py.exp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
Wrapped DTLS Server
Failed to wrap DTLS Server with invalid client_id
Wrapped DTLS Client
OK
55 changes: 30 additions & 25 deletions tests/multi_net/tls_dtls_server_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,36 @@ def instance0():

multitest.next()

# Wait for the client to connect.
data, client_addr = s.recvfrom(1)
print("incoming connection", data)

# Connect back to the client, so the UDP socket can be used like a stream.
s.connect(client_addr)

# Create the DTLS context and load the certificate.
ctx = tls.SSLContext(tls.PROTOCOL_DTLS_SERVER)
ctx.load_cert_chain(cert, key)

# Wrap the UDP socket in server mode.
print("wrap socket")
s = ctx.wrap_socket(s, server_side=1)

# Transfer some data.
for _ in range(4):
print(s.recv(16))
s.send(b"server to client")

# Close the DTLS and UDP connection.
s.close()
# Because of "hello verify required", we expect the peer
# to connect twice: once to set the cookie, then second time
# successfully.
#
# As this isn't a real server, we hard-code two connection attempts
for _ in range(2):
print("waiting")
# Wait for the client to connect so we know their address
_, 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())
except OSError as e:
print(e)
continue # wait for second connection

# Transfer some data.
for i in range(4):
print(s.recv(32))
s.send(b"server to client " + str(i).encode())

# Close the DTLS and UDP connection.
s.close()
break


# DTLS client.
Expand All @@ -68,9 +76,6 @@ def instance1():
print("connect")
s.connect(addr)

# Send one byte to indicate a connection, and so the server can obtain our address.
s.write("X")

# Create a DTLS context and load the certificate.
ctx = tls.SSLContext(tls.PROTOCOL_DTLS_CLIENT)
ctx.verify_mode = tls.CERT_REQUIRED
Expand All @@ -81,9 +86,9 @@ def instance1():
s = ctx.wrap_socket(s, server_hostname="micropython.local")

# Transfer some data.
for _ in range(4):
s.send(b"client to server")
print(s.recv(16))
for i in range(4):
s.send(b"client to server " + str(i).encode())
print(s.recv(32))

# Close the DTLS and UDP connection.
s.close()
23 changes: 13 additions & 10 deletions tests/multi_net/tls_dtls_server_client.py.exp
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
--- instance0 ---
incoming connection b'X'
wrap socket
b'client to server'
b'client to server'
b'client to server'
b'client to server'
waiting
incoming connection
(-27264, 'MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED')
waiting
incoming connection
b'client to server 0'
b'client to server 1'
b'client to server 2'
b'client to server 3'
--- instance1 ---
connect
wrap socket
b'server to client'
b'server to client'
b'server to client'
b'server to client'
b'server to client 0'
b'server to client 1'
b'server to client 2'
b'server to client 3'
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy