This tutorial introduces the specifics of using DTLS (as opposed to TLS) with mbed TLS. It assumes you're familiar with using TLS connections with mbed TLS; otherwise we recommend starting with the mbed TLS tutorial.
- You need to register timer callbacks and context with
mbedtls_ssl_set_timer_cb(); suitable callbacks for blocking I/O are provided in timing.c (for event-based I/O you'll need to write your own callbacks based on the event framework you're using).
- Server-side, you need to register cookie callbacks with
mbedtls_ssl_conf_dtls_cookie(); an implementation is provided in ssl_cookie.c and requires the context to be set up with
If you prefer to deal with code right away you can skip to our dtls_client.c and dtls_server.c examples in the programs/ssl directory and start hacking around. However, this article provides more background information, so we recommend reading it in order to make more informed choices.
Protocol differences and additional settings
TLS usually runs on top of TCP, and provides the same guarantees as TCP, plus authentication, integrity and confidentiality. Just like TCP, it delivers a stream of bytes in order and does not preserve packet boundaries. DTLS usually runs on top of UDP and, once the handshake is done, it provides the same guarantees as UDP, plus authentication, integrity and confidentiality. Just like UDP, it delivers datagrams of bytes. Some datagrams may be lost, some may be re-ordered, but unlike UDP, DTLS can detect and discard duplicated datagrams if needed. In mbed TLS, this is controlled by the compile-time flag MBEDTLS_SSL_DTLS_ANTI_REPLAY and the run-time setting
mbedtls_ssl_conf_dtls_anti_replay(), both enabled by default.
With TLS, when a record is received that does not pass the integrity check, the connection is immediately terminated. This denies attackers an opportunity to do more than one guess at the message authentication key, while not introducing any new DoS vectors (injecting bad records is just as hard as injecting a TCP RST to tear down the connection). However, with DTLS over UDP, injecting bad records is very easy (an attacker only needs to know the source and destination IP and port), so the DTLS standard, section 18.104.22.168 recommends not to tear down the connection. In mbed TLS, it is possible to set a limit to the number of bad records before the connection is torn down; this is controlled by the compile-time flag MBEDTLS_SSL_DTLS_BADMAC_LIMIT (enabled by default) and the run-time setting
mbedtls_ssl_conf_dtls_badmac_limit() (unlimited by default).
Retransmission: timer callbacks
The (D)TLS handshake is a lock-step procedure: messages need to arrive in a certain order and cannot be skipped. To achieve this on top of UDP, DTLS has its own retransmission mechanism, which needs timers. In mbed TLS, the SSL module accepts a pair of callbacks for timer functions, which can be set using
mbedtls_ssl_set_timer_cb(). Example callbacks (for Unix and Windows) are provided in timing.c, namely
mbedtls_timing_get_delay(), that are suitable for use with blocking I/O.
The callbacks have the following interface:
void mbedtls_timing_set_delay( void *data, uint32_t int_ms, uint32_t fin_ms ); int mbedtls_timing_get_delay( void *data );
In both cases,
data is a context shared by the callbacks. The setting function accepts two delays: an intermediate and a final one, and the getting function tells the caller which of these delays are expired if any (see the documentation of
mbedtls_ssl_set_timer_cb() for details). The final delay is used to indicate when retransmission should happen, while the intermediate delay is an internal implementation detail whose semantic may evolve in future versions.
The interface was designed to allow a variety of implementation strategies, of which two are described below.
Timestamps. The setting function records a timestamp and the values of the delay in the context, and the getting function compares the stored timestamp with the current time.
This is the strategy used by the example callbacks in timing.c. It is suitable when you know the application will call
mbedtls_ssl_handshake() repeatedly until it returns
0 or a fatal error, which is usually the case when using blocking I/O.
Timers and events. The setting function ensures (for example using a hardware timer or a system call) that a timeout handler will be called when one of the delays expires. This timeout handler needs to at least record the information about which delay expired so that the getting function can return the proper value. For the intermediate delay, this is all you need to do (the information may be used internally if another event, such as an incoming packet, causes
mbedtls_ssl_handshake() to be called again before the final delay expires).
For the final delay however, if you are using an event-driven style of programming, the timeout handler needs to generate an event that will cause
mbedtls_ssl_handshake() to be called again. Our DTLS handshake code will then internally call the
get_delay() function, notice the delays are expired, and take the appropriate action (either retransmit the last flight of messages or give up on the handshake and return a timeout error).
Note: you need to make sure that calling
set_delay() while a timer is already running cancels it (more precisely, that no event will be generated when the final delay expires). In particular, after a call like
set_delay(0, 0), no timer should be running any more. Said otherwise, there should be at most one running timer at any given time.
Note: if you have multiple concurrent connections, you need to make sure each has its own independent set of timers, and that, when a timeout event is generated for one connection,
mbedtls_ssl_handshake() is called with the appropriate
ssl_context for that connection (the
data argument for the callbacks can be used to store the required information). You also need to avoid making multiple calls to
mbedtls_ssl_handshake() with the same
ssl_context at the same time.
Note: with event-based I/O you don't want to use read timeouts (said otherwise, you don't want to call
mbedtls_ssl_conf_read_timeout() with a non-zero value), for two reasons: (1) you don't need it, as you're only going to call
mbedtls_ssl_read() when data is ready to be read anyway, and (2) that would make your timeout handler more complex as it would have to know whether the timeout happened during handshake or read in order to schedule the appropriate function.
Retransmission: timeout values
The retransmission delay starts with a minimum value, then doubles on each retransmission until its maximum value is reached, in which case a handshake timeout is reported to the application. The minimum and maximum can be set using
mbedtls_ssl_conf_handshake_timeout() (default: 1 second to 60 seconds).
See the documentation of this function for the meaning of those values if you need to tune them according to the characteristics of your network in order to achieve optimal performance/reliability. Even if your timeout values are perfectly tuned, your application should still be prepared to see failing handshakes and react appropriately.
Note: though it might look like there is a parallel between
set_delay() as they both accept two durations as arguments, this is not the case. The "final delay" will take various values from
max (doubling every time), while the "intermediate delay" is an internal implementation detail.
Server-side only: Cookies for client verification
Without going into the full details of a (D)TLS handshake, let's mention that the client starts by sending a (possibly very short) ClientHello message, to which the server replies with a series of messages that can be long (these typically include the server's certificate chain). Without specific protection, this would make it easy for an evil client to use DTLS servers as amplifiers in DDoS attacks: since it is trivial to spoof the source address of a UDP packet, evil clients could send a few bytes of ClientHello to innocent DTLS servers pretending to be a third machine (the victim) and the innocent DTLS servers would then send (and retransmit) kilobytes of data to the victim, unknowingly DDoSing it.
As a good internet citizen, you don't want your server to be used that way. Of course the DTLS standard has provisions against that, in the form of a cookie exchange (AKA "ClientHello verify") that ensures verification of the client address. mbed TLS obviously implement this, in a stateless way, in order to avoid DoS vectors against your own server, as recommended by the standard.
This mechanism uses secret server-side keys, in order to prevent an attacker from generating valid cookies. As usual, the SSL layer only expects callbacks so that you can provide your own implementation if desired, and a default implementation is provided, in ssl_cookie.c.
The keys are stored in an
mbedtls_ssl_cookie_ctx that you need to declare or allocate, then initialize with
mbedtls_ssl_cookie_setup(). You then register the context and callbacks with
mbedtls_ssl_conf_dtls_cookies(). If you are in a threaded environment, this should happen in the main thread during initialization. Then, for each client that attempts to connect, you need to call
mbedtls_ssl_set_client_transport_id() with the client address that will be verified (generally it's an IPv4 or IPv6 address).
That's it! Optionally, if you log handshake errors, you might want to treat MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED in a special way for logging, as it is expected to happen for half of the handshakes. However, it still means you should destroy or reset
mbedtls_ssl_context and start the next handshake with a fresh context (remember, we don't want to keep state for unverified clients).
A final note on defaults: the cookie callbacks that are registered by default always fail. The rationale is as follows:
- We cannot register working callbacks by default since we cannot create and setup the cookie context in an automated way (it needs to be shared amongst SSL contexts).
- We do not want to silently disable the feature by default as that would mean insecure defaults.
- Failing callbacks force you to notice something needs to be done.
You can, if you are 100% sure that amplification attacks against third parties are not an issue in your particular deployment, disable ClientHello verification at run-time by registering
NULL callbacks, or at compile-time by undefining MBEDTLS_SSL_DTLS_HELLO_VERIFY in config.h.