Developer guide: Mbed TLS

This guide describes the implementation of a TLS client in Mbed TLS.

The guide covers basic aspects of initiating a secure TLS connection, including certificate validation and hostname verification. When various alternative approaches are possible, the guide presents each of them and specifies their use cases to help you choose which approach suits your needs best.

  • We work with the API in C of Mbed TLS, version 2.16.9.
  • We assume the server to communicate with is at x509errors.org and accepts TLS connections on a standard port 443.

Preparing necessary data structures

Mbed TLS requires quite a lot of structures to be initialized before we start.

#include "mbedtls/ctr_drbg.h"
#include "mbedtls/entropy.h"
#include "mbedtls/net_sockets.h"
#include "mbedtls/ssl.h"

/* Wrapper of the socket descriptor.
** This will take care of the underlying TCP/IP connection. */
mbedtls_net_context server_fd;
mbedtls_net_init(&server_fd);

/* Entropy (randomness source) context.
** Necessary to produce random data during the TLS handshake. */
mbedtls_entropy_context entropy;
mbedtls_entropy_init(&entropy);

/* Context for random number generation.
** Again, needed to produce random data during the handshake. */
mbedtls_ctr_drbg_context drbg;
mbedtls_ctr_drbg_init(&drbg);

/* TLS context which represents our session. */
mbedtls_ssl_context ssl;
mbedtls_ssl_init(&ssl);

/* Configuration to use within TLS. */
mbedtls_ssl_config conf;
mbedtls_ssl_config_init(&conf);

/* Seed the random number generator. */
if (mbedtls_ctr_drbg_seed(&drbg, mbedtls_entropy_func, &entropy, NULL, 0) != 0) {
    exit(EXIT_FAILURE);
}

/* Assign the random number generator to the TLS config. */
if (mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &drbg) != 0) {
    exit(EXIT_FAILURE);
}

/* Assign the TLS config to the TLS context. */
if (mbedtls_ssl_setup(&ssl, &conf) != 0) {
    exit(EXIT_FAILURE);
}

Configuring the session settings

For the connection to be functional and secure, we must set multiple options beforehand.

/* Set defaults for the TLS configuration.
** This is the recommended setting. */
if (mbedtls_ssl_config_defaults(&conf, MBEDTLS_SSL_IS_CLIENT,
                                        MBEDTLS_SSL_TRANSPORT_STREAM,
                                        MBEDTLS_SSL_PRESET_DEFAULT) != 0) {
    exit(EXIT_FAILURE);
}

/* However, we accept only TLS 1.2 and higher. */
mbedtls_ssl_conf_min_version(&conf, MBEDTLS_SSL_MAJOR_VERSION_3,
                                    MBEDTLS_SSL_MINOR_VERSION_3);
/* We need to set the option to validate the peer certificate chain.
** If we skipped this step, an active attacker could impersonate the server. */
mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_REQUIRED);

/* Set hostname for verification.
** Not setting the hostname would mean that we would accept a certificate of any trusted server.
** It also sets the Server Name Indication TLS extension.
** This is required when multiple servers are running at the same IP address (virtual hosting). */
if (mbedtls_ssl_set_hostname(&ssl, "x509errors.org") != 0) {
    exit(EXIT_FAILURE);
}

Specifying trusted root authority certificates

Trusted root certs are usually found within a directory such as /etc/ssl/certs. If they are not concatenated, concatenate them using e.g. the cat command. We will now assume that a file trusted_certs.pem contains all trusted root certificates.

In some cases, it might be useful to trust an arbitrary certificate authority. This could be the case during testing, or within company intranets. In that case, use arbitrary trusted CA certificate files instead.

/* Structure to load trusted root certs into. */
mbedtls_x509_crt ca_certs;
mbedtls_x509_crt_init(&ca_certs);

/* Parse the file with root certificates. */
if (mbedtls_x509_crt_parse_file(&ca_certs, "trusted_certs.pem") != 0) {
    exit(EXIT_FAILURE);
}

/* Set the certificates as trusted for this session. */
mbedtls_ssl_conf_ca_chain(&conf, &ca_certs, NULL);

Optional: Checking revocation using local CRLs

Mbed TLS natively provides only offline revocation checking. That is, the revocation list must already be present locally. If the CRL is contained in crl.pem, we include it in the configuration as follows.

In the most recent versions (Mbed TLS 3.7), it may be possible to implement online revocation checks manually. We will include it in the guide when this version becomes more widely adapted.

/* Structure to load the CRL into. */
mbedtls_x509_crl crl;
mbedtls_x509_crl_init(&crl);

/* Load the CRL from file. */
if (mbedtls_x509_crl_parse_file(&crl, "crl.pem") != 0) {
    exit(EXIT_FAILURE);
}

/* Assign it to the config, together with the trusted CA file. */
mbedtls_ssl_conf_ca_chain(&conf, &ca_certs, &crl);

Initializing a TLS connection

At this point, we can initialize a TCP/IP connection and then build the TLS connection on top.

/* Initialize the underlying TCP/IP connection */
if (mbedtls_net_connect(&server_fd, , opts.port, MBEDTLS_NET_PROTO_TCP) != 0) {
    exit(EXIT_FAILURE);
}

/* Link the socket wrapper to our TLS session structure.
** Also set the onput/ouput function that we will use to transfer application data. */
mbedtls_ssl_set_bio(&ssl, &server_fd, mbedtls_net_send, mbedtls_net_recv, NULL);

/* Perform the TLS handshake.
** During this procedure, the peer certificate is validated. */
if (mbedtls_ssl_handshake(&ssl) != 0) {
    exit(EXIT_FAILURE);
}

Optional: Checking the result of peer certificate validation

If certificate validation fails, mbedtls_ssl_handshake() will always fail with the same error message. In that case, it is often useful to examine the specific certificate validation error as follows. You can find explanations of certificate validation messages in the official documentation or on our page.

/* Manually retrieve the result of certificate validation. */
uint32_t res = mbedtls_ssl_get_verify_result(&ssl);

/* Print the result of certificate validation as a string into the standard error output. */
char message_buffer[2048];
mbedtls_x509_crt_verify_info(message_buffer, 2048, "", res);
fprintf(stderr, "%s", message_buffer);

Sending and receiving data using the TLS connection

When the connection is successfully established, we can share application data with the server. These two functions provide the basic interface.

/* Prepare a message and send it to the server. */
char *message = "Hello server";
if (mbedtls_ssl_write(ssl, message, strlen(message)) != 1) {
    exit(EXIT_FAILURE);
}

/* Prepare a static buffer for the response and read the response into that buffer. */
char buffer[4096];
if (mbedtls_ssl_read(ssl, buffer, 4096) != 1) {
    exit(EXIT_FAILURE);
}

Closing the TLS connection

The client is usually the one to indicate that the connection is finished. When we want the connection closed, the following steps are performed.

/* Gracefully close the connection by sending the "close notify" message to the server. */
if (mbedtls_ssl_close_notify(&ssl) != 0) {
    exit(EXIT_FAILURE);
}

/* Clean up all used resources and structures. */
mbedtls_ssl_free(&ssl);
mbedtls_x509_crt_free(&ca_certs);
mbedtls_ssl_config_free(&conf);
mbedtls_net_free(&server_fd);
mbedtls_ctr_drbg_free(&drbg);
mbedtls_entropy_free(&entropy);