Secure connections using Mbed TLS. I thought it would just be a case of some #define, but it took a lot more effort to get it to work.

There are still many open questions, and a few more experiments, but overall it is possible to make an HTTP/TLS request on the Raspberry Pi Pico.

This page describes the most straight-forward, minimal effort to have secure connections. There are probably many things I’m doing wrong here, but… learning by doing.

Mbed TLS

There aren’t that many TLS libraries, and only a few are reasonable candidates. lwIP works with Mbed TLS, and it’s possibly the obvious choice. However, as far as I could see, even though it’s probably possibly to make it work with version 3.2.1, I’ve had more success “out of the box” with version 2.28.1.

The simplest way I found to use Mbed TLS is to add it as a submodule:

$ git submodule add https://github.com/Mbed-TLS/mbedtls.git
$ cd mbedtls
$ git switch --detach tags/v2.28.1

The intention is not to make any changes in this directory.

Let’s add it to the build:

add_subdirectory(mbedtls)

...

target_link_libraries(<project-name>
        mbedtls
        mbedx509
        mbedcrypto
        )

But trying to build this fails immediately:

   50 | #error "Platform entropy sources only work on Unix and Windows, see MBEDTLS_NO_PLATFORM_ENTROPY in config.h"
      |  ^~~~~      

The problem is the default configuration. What we want is a “bare metal” configuration instead, with a few tweaks. To get such a configuration, we can start with:

$ cd mbedtls
$ scripts/config.py baremetal

This updates the file include/mbedtls/config.h in place. But that’s not ideal - now we have changes in a repository that we don’t own. An alternative is to use a user config file, but use the changes from the command above as a basis.

To do that, we define MBEDTLS_USER_CONFIG_FILE and this file will be included after the default file.

I am going to use the following:

/* This can be enabled lated when we use SNTP */
#undef MBEDTLS_HAVE_TIME_DATE

#define MBEDTLS_PLATFORM_MEMORY
#define MBEDTLS_PLATFORM_EXIT_ALT
#define MBEDTLS_PLATFORM_PRINTF_ALT
#define MBEDTLS_PLATFORM_SNPRINTF_ALT
#define MBEDTLS_PLATFORM_VSNPRINTF_ALT
#define MBEDTLS_CHECK_PARAMS
#define MBEDTLS_CHECK_PARAMS_ASSERT
#define MBEDTLS_ENTROPY_HARDWARE_ALT
#define MBEDTLS_AES_ROM_TABLES
#define MBEDTLS_AES_FEWER_TABLES
#define MBEDTLS_CAMELLIA_SMALL_MEMORY
#define MBEDTLS_CHECK_RETURN_WARNING
#undef MBEDTLS_FS_IO
#define MBEDTLS_NO_PLATFORM_ENTROPY
#define MBEDTLS_PSA_CRYPTO_CLIENT
#define MBEDTLS_PSA_CRYPTO_DRIVERS
#define MBEDTLS_SHA256_SMALLER
#define MBEDTLS_SHA512_SMALLER
#define MBEDTLS_SSL_ASYNC_PRIVATE
#define MBEDTLS_SSL_VARIABLE_BUFFER_LENGTH
#define MBEDTLS_USE_PSA_CRYPTO
#undef MBEDTLS_NET_C
#undef MBEDTLS_PSA_CRYPTO_STORAGE_C
#undef MBEDTLS_PSA_ITS_FILE_C
#undef MBEDTLS_TIMING_C

Some of those options save memory, some are required (as the option is not supported on the Raspberry Pi Pico).

Adding this to CMakeLists.txt (just before add_directory):

add_definitions(-DMBEDTLS_USER_CONFIG_FILE="../../mbedtls-config-changes.h")

And now it builds.

But adding the first call (which is mandatory):

    if (psa_crypto_init() != PSA_SUCCESS) {
        printf("Failed to initialize PSA Crypto\n");
        exit(1);
    }

Causes the build to fail with:

undefined reference to `mbedtls_hardware_poll'

That’s because of #define MBEDTLS_ENTROPY_HARDWARE_ALT which requires that function. To be honest, there is no good source of entropy on the Raspberry Pi Pico, so the options are:

  1. Use a weak source of entropy, for example following https://forums.raspberrypi.com/viewtopic.php?t=302960
  2. Use some hardware (for example the Adafruit Infineon Trust M Breakout Board).

Adding lwIP

Luckily lwIP has “built-in” supports for Mbed TLS. For ease I’ll be using pico_cyw43_arch_lwip_threadsafe_background. Add pico_lwip_mbedtls to the list of libraries.

This means that we also need lwipopts.h (can be copied from pico_w/lwipopts_examples_common.h under pico-examples). However I found that I had to update a couple of options:

#define LWIP_ALTCP_TLS_MBEDTLS      1
#define MEM_SIZE                    40320
#define TCP_WND                     (16 * TCP_MSS)

The numbers are a bit random, but 4000 bytes is just not enough for TLS, and the library complains about the window size without the TCP_WND change.

Next comes the bit of magic that connects Mbed TLS to lwIP:

    struct altcp_tls_config *conf = altcp_tls_create_config_client(CERTIFICATE, strlen(CERTIFICATE) + 1);
    altcp_allocator_t allocator = {
            .alloc = altcp_tls_alloc,
            .arg = conf,
    };
    struct altcp_pcb *pcb = altcp_new(&allocator);

It is possible to fetch a (CA) certificate from the browser, which is what I did. The certificate is in PEM format which Mbed TLS supports. A tiny issue is that the function is defined as altcp_tls_create_config_client(const u8_t *ca, size_t ca_len) but my certificate is:

static const char *CERTIFICATE =
        "-----BEGIN CERTIFICATE-----\n"
        "MIIF3TCCBMWgAwIBAgIQCdeQBRwpI/Am9FwU+yXN8TANBgkqhkiG9w0BAQsFADBG\n"         
        ...

So there’s a small pointer type mismatch, but I can live with that.

Now we have a TLS-backed pcb (protocol control block), which can be used for calls such as altcp_connect, altcp_recv, etc.

One important thing to note is that altcp_connect only takes an IP address, but part of TLS is hostname verification (for the client to make sure that it’s talking to the right host). In order to do that, mbedtls_ssl_set_hostname must be called before the altcp_connect call.

The rest is very similar to Part I.