SSL/TLS

TLS for servers

In this section, we give a very short and quick description on how to enable SSL/TLS for a server (listening) connection - for example, HTTPS server.

  • Obtain TLS certificate and private key files (or create your own self-signed certificate, see below)
  • In the event handler function, call mg_tls_init() on the MG_EV_ACCEPT event:
    static void fn(struct mg_connection *c, int ev, void *ev_data) {
      if (ev == MG_EV_ACCEPT) {
        struct mg_tls_opts opts = {.cert = mg_unpacked("/certs/server_cert.pem"),
                                   .key = mg_unpacked("/certs/server_key.pem")};
        mg_tls_init(c, &opts);
      }
      ...
    }
    
  • Rebuild the application, see build instructions below

You can see example code in

TLS for clients

In order to TLS-enable client connections,

  • Obtain the CA (Certificate Authority) certificate file

  • In the event handler function, call mg_tls_init() on the MG_EV_CONNECT event:

    static void fn(struct mg_connection *c, int ev, void *ev_data) {
      if (ev == MG_EV_CONNECT) {
        struct mg_tls_opts opts = {.ca = mg_unpacked("/certs/ca.pem")};
        mg_tls_init(c, &opts);
      }
      ...
    }
    
    • Optionally, you may also set name to enable host name validation. If you also omit the ca entry, no certificate verification will take place, only host name will be checked.
  • If you need to do something once the TLS handshake is finished, handle the MG_EV_TLS_HS event:

  • Rebuild the application, see build instructions below

You can see example code in

Certificates overview

TLS provides two major benefits:

  • traffic encryption, which makes it impossible to sniff and look inside the traffic, and
  • authentication, which makes it possible to one side of the TLS connection to verify the identity of the other side.

Here we're talking about authentication. Authentication is implemented via certificates. A certificate has two parts - public and private. Talking in practical terms, three files are required to implement TLS authentication:

  • TLS certificate. This is a "public" part. For example, a TLS-enabled server sends it to the client during the TLS handshake
  • TLS private key. This is a "private" part
  • TLS Certificate Authority (CA) file. It is used for verification of the "public" certificate sent by the server

TLS certificates are obtained from services like Let's Encrypt. The other possibility is self-signed certificates, which are mainly used for development.

Self-signed certificates

It is possible to generate certificate files. Note that servers using those files will not be trusted by browsers, because browsers use pre-installed CA files and know nothing about your generated certificates.

The command below uses the openssl command line tool to generate a self-signed server certificate and a key file:

$ openssl req -nodes -new -x509 -keyout key.pem -out cert.pem

When generating keys for embedded hardware, you may want to use shorter elliptic-curve cryptography (ECC) options:

$ openssl ecparam -name prime256v1 -genkey -noout -out key.pem
$ openssl req -new -key key.pem -x509 -nodes -out cert.pem

Two-way TLS

Normally, when a client makes a connection to a TLS-enabled server, the server sends its certificate to the client and the client verifies it using its own CA (Certificate Authority) file. This way the client authenticates the server.

In the most common situation, the client verifies the server, but the server does not verify the client. For example, browsers (clients) use a big CA file (or many CA files) to verify HTTPS servers.

Clients can also provide certificates during the TLS handshake, and the server can verify it using a CA file. When both client and server use certificates, and verify the other side using a CA file, it is called two-way TLS. In order to implement two-way TLS, now both client and server must have their own cert, key and ca specified in mg_tls_opts:

For a server:

  struct mg_tls_opts opts = {.ca = mg_unpacked("/certs/ca.pem"),
                             .cert = mg_unpacked("/certs/server_cert.pem"),
                             .key = mg_unpacked("/certs/server_key.pem")};
  mg_tls_init(c, &opts);

For a client:

  struct mg_tls_opts opts = {.ca = mg_unpacked("/certs/ca.pem"),
                             .cert = mg_unpacked("/certs/client_cert.pem"),
                             .key = mg_unpacked("/certs/client_key.pem")};
  mg_tls_init(c, &opts);

Two-way TLS certificates

It is possible to generate self-signed ca.pem, and cert + key pairs for both client and server for two-way (mutual) authentication. This is an example on how to do it using the openssl command line tool:

## Common parameters
$ SUBJ="/C=IE/ST=Dublin/L=Docks/O=MyCompany/CN=howdy"

## Generate CA
$ openssl genrsa -out ca.key 2048
$ openssl req -new -x509 -days 365 -key ca.key -out ca.crt \
  -subj /C=IE/ST=Dublin/L=Docks/O=mos/CN=me 

## Generate client cert
$ openssl genrsa -out client.key 2048
$ openssl req -new -key client.key -out client.csr -subj $SUBJ
$ openssl x509 -req -days 365 -in client.csr -CA ca.crt \
  -CAkey ca.key -set_serial 01 -out client.crt

## Generate server cert
$ openssl genrsa -out server.key 2048
$ openssl req -new -key server.key -out server.csr -subj $SUBJ
$ openssl x509 -req -days 365 -in server.csr -CA ca.crt \
  -CAkey ca.key -set_serial 01 -out server.crt

How to load credentials

  • Certificates and keys can be loaded from a file in a supported filesystem using mg_file_read:

      struct mg_str data = mg_file_read(&mg_fs_posix, "test/data/ca.pem");
      struct mg_tls_opts opts = {.ca = data};
      ...
      free(data.buf);
    

    This copies the file in memory that has to be freed later when the program exits.

  • They can also be loaded from a file in an embedded filesystem, using mg_unpacked() as shown above:

    struct mg_tls_opts opts = {.ca = mg_unpacked("/certs/ca.pem")};
    

    This accesses the file directly from the code, where it has been embedded.

  • Finally, certificates and keys can be defined as C strings in source code:

How to build

Use the MG_TLS macro in your compile options:

  • MG_TLS=MG_TLS_BUILTIN to use Mongoose built-in TLS functions
  • MG_TLS=MG_TLS_MBED to build using MbedTLS
  • MG_TLS=MG_TLS_OPENSSL to build using OpenSSL
  • MG_TLS=MG_TLS_WOLFSSL to build using WolfSSL. This library also requires that you define the macro EXTERNAL_OPTS_OPENVPN

Many of our examples default to building with Mongoose built-in TLS support. For those that don't, just do

make all CFLAGS_EXTRA="-DMG_TLS=MG_TLS_BUILTIN"

Most examples can pull and build a known and tested version of MbedTLS. Then, depending on your operating system, you may have additional options

The following instructions assume you've already followed the Build Tools tutorial to setup your development environment.

Embedded hardware

Most of our Wizard-generated examples default to building with Mongoose built-in TLS support; except for those cases where the default build environment already has its own TLS library.

To build other embedded hardware examples with Mongoose built-in TLS support, just do

make build CFLAGS_EXTRA="-DMG_TLS=MG_TLS_BUILTIN"

Some platforms include support for MbedTLS. In CMake platforms, like Espressif SDK, it is just a matter of setting the proper Mongoose compile option in the CMakeList file:

component_compile_options(-DMG_TLS=MG_TLS_MBED)

However, beyond the simple first try, we recommend keeping all Mongoose configuration options for an embedded target in its mongoose_config.h file:

#define MG_TLS MG_TLS_MBED

See for example the device-dashboard tutorial for the ESP32.

For bare metal examples, some include support to clone MbedTLS and build with it. Just run:

make TLS=mbedtls

However, all devices with enough memory can take advantage of Mongoose's built-in TLS 1.3 library, just add this to your mongoose_config.h file.

#define MG_TLS MG_TLS_BUILTIN

See for example the device-dashboard tutorial for the STM32 series.

Linux

Assuming Ubuntu Linux, start a terminal and install the library of your choice:

sudo apt -y update
sudo apt -y install libmbedtls-dev
sudo apt -y install libssl-dev

Then use the CFLAGS_EXTRA argument to pass the necessary additional compile options, as follows:

  • make CFLAGS_EXTRA="-DMG_TLS=MG_TLS_MBED -lmbedtls -lmbedcrypto -lmbedx509" to build for MbedTLS
  • make CFLAGS_EXTRA="-DMG_TLS=MG_TLS_OPENSSL -lssl -lcrypto" to build for OpenSSL
  • make CFLAGS_EXTRA="-DMG_TLS=MG_TLS_WOLFSSL -DEXTERNAL_OPTS_OPENVPN -lwolfssl" to build for WolfSSL

For other similar distributions and operating systems, check pkg-config below

MacOS

Start a terminal, and install the library of your choice:

brew install mbedtls
brew install openssl
brew install wolfssl

Then use the CFLAGS_EXTRA argument to pass the necessary additional compile options, as follows:

  • make CFLAGS_EXTRA="-DMG_TLS=MG_TLS_MBED -lmbedtls -lmbedcrypto -lmbedx509 -I$(MBEDTLS_DIR)/include -L$(MBEDTLS_DIR)/lib" to build for MbedTLS
  • make CFLAGS_EXTRA="-DMG_TLS=MG_TLS_OPENSSL -lssl -lcrypto -I$(OPENSSL_DIR)/include -L$(OPENSSL_DIR)/lib" to build for OpenSSL
  • make CFLAGS_EXTRA="-DMG_TLS=MG_TLS_WOLFSSL -DEXTERNAL_OPTS_OPENVPN -lwolfssl -I$(WOLFSSL_DIR)/include -L$(WOLFSSL_DIR)/lib" to build for WolfSSL

If you use brew, you can easily get those paths by asking it:

$ MBEDTLS_DIR=$(echo $(brew --cellar)/mbedtls*/*)
$ OPENSSL_DIR=$(echo $(brew --cellar)/openssl*/*)
$ WOLFSSL_DIR=$(echo $(brew --cellar)/wolfssl*/*)

If you don't use brew, or still see compiler or linker errors messages, see pkg-config below

Windows

Only what the example provides in its Makefile is supported

pkg-config

If, when using your system libraries, you see a compiler error message like "Cannot find .... file", your system needs extra paths for includes and/or libs, and/or link options; usually the former. This means adding -I path/to/include to tell where installed TLS headers are, and/or -L path/to/lib to tell where installed TLS shared libs are.

These paths and link options are usually determined in GNU/Linux-like systems using pkg-config. Sometimes the package uses the standard paths and no extra paths are needed; others they are installed in a different path to avoid conflicts and flags are required:

$ pkg-config --cflags --libs openssl
-lssl -lcrypto
$ pkg-config --cflags --libs openssl11
-I/usr/include/openssl11  -L/usr/lib64/openssl11 -lssl -lcrypto
$ pkg-config --cflags --libs-only-L openssl11
-I/usr/include/openssl11  -L/usr/lib64/openssl11
$ pkg-config --libs-only-l openssl11
-lssl -lcrypto

This requires that the relevant packages have installed their packagename.pc files in the default path where pkg-config will search for them; otherwise you have to set the PKG_CONFIG_PATH environment variable to where they reside.

To pass these extra options to the make command, extend the CFLAGS_EXTRA argument as, for example, we did for MacOS above.

The link options are mostly the same among systems and usually there is no need to add to them.

Check your system for relevant paths and compiler flags