Introduction

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.

Short version

  • 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 need to write your own callbacks based on your event framework.
  • Server-side, register cookie callbacks with mbedtls_ssl_conf_dtls_cookie().
    • An implementation is provided in ssl_cookie.c and requires context set up with mbedtls_ssl_cookie_setup().

If you prefer to begin with code right away, you can skip to our dtls_client.c and dtls_server.c examples in the programs/ssl directory. 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, in addition to authentication, integrity, and confidentiality.

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 finished, provides the same guarantees as UDP as well as authentication, integrity, and confidentiality.

Just like UDP, it delivers datagrams of bytes. Some datagrams may be lost or 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, without 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 4.1.2.7 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_set_delay() and 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, two of which two are:

  • 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.

With event-based I/O, don't use read timeouts by calling mbedtls_ssl_conf_read_timeout() with a non-zero value, for two reasons:

  • It's unnecessary, as you only call mbedtls_ssl_read() when data is ready to be read.
  • It makes 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 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:: There might seem to be a parallel between mbedtls_ssl_conf_handshake_timeout() and set_delay() as they both accept two durations as arguments, but this is not the case. The final delay will take various values from min to max, doubling every time, while the intermediate delay is an internal implementation detail.

Server-side only: Cookies for client verification

  1. The client starts by sending a ClientHello message.

  2. 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 a malicious client to use DTLS servers as amplifiers in DDoS attacks. Since it is trivial to fake the source address of a UDP packet, malicious 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.

The DTLS standard has provisions against this misuse, in the form of a cookie exchange (ClientHello verify) that ensures verification of the client address. Mbed TLS implements 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.

  1. The keys are stored in an mbedtls_ssl_cookie_ctx that you need to declare or allocate.

  2. Initialize with mbedtls_ssl_cookie_init() and mbedtls_ssl_cookie_setup().

  3. 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.
  4. 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 an IPv4 or IPv6 address).

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).

Defaults

The cookie callbacks that are registered by default always fail. The rationale is as follows:

  • You cannot register working callbacks by default since you cannot create and setup the cookie context in an automated way (it needs to be shared among SSL contexts).
  • You 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 sure that amplification attacks against third parties are not an issue in your particular deployment, disable ClientHello verification at run-time:

  • Register NULL callbacks.
  • Alternatively, at compilation: Undefine MBEDTLS_SSL_DTLS_HELLO_VERIFY in config.h.

Did this help?