Embedded HandbookApps
All sections › VI. Practice
STM32F407 · level advanced · 25 min read
Open in the interactive handbook

37а. What TLS costs on a microcontroller: Mbed TLS in flash and RAM

Section 37 puts it briefly: encryption on the F407 is a library, and it takes tens of kilobytes. 107.7 gives a first estimate: a TLS 1.2 client on Mbed TLS is 57 KB of flash memory, 12–36 KB of heap and 85 million instructions per handshake. Here the same cost is taken apart: what exactly occupies flash memory, where the heap goes, at which step of the handshake the time is spent and what changes if you switch off what is not needed in the configuration, take RSA instead of curves, lengthen the certificate chain or shrink the record buffers.

All the numbers in this section come from runs. Mbed TLS 3.6.4 (github.com/Mbed-TLS/mbedtls, release v3.6.4, Apache-2.0 license) is built with arm-none-eabi-gcc 12.2 -Os for Cortex-M4. Flash memory is counted on a "client only" build. Time and heap are measured in QEMU (the mps2-an386 board, Cortex-M4): an Mbed TLS client and server in one program, the "wire" being two buffers in memory. The record buffers and the max_fragment_length extension were also checked on a PC: two programs with different settings over a pair of sockets, and the client against openssl s_server.

Prerequisites: what is visible in TLS before encryption and the first estimate of the cost — 107.5 and 107.7; the ECDSA signature, SHA-256 and the options MBEDTLS_ECP_NIST_OPTIM, MBEDTLS_HAVE_ASM — 48а; the kernel heap — 25; hardware AES and HASH — 43; the AES, HASH and PKA blocks of the U585 — 2е; a device with HTTP and MQTT where TLS will be needed — 198з and 198и; where the client gets its random numbers — 43а.

Terms used in the documentation

Term Meaning
cipher suite a set of algorithms: key exchange, signature, record cipher and hash under one name
key exchange how the two sides obtain a shared session secret
forward secrecy recorded traffic cannot be decrypted with a stolen server key
intermediate CA an intermediate certificate authority: a link between the root and the server certificate
TLS record a piece of the stream up to 16 KB with its own header and tag
maximum fragment length (MFL) the "maximum record length" extension
handshake state a handshake step in the library's state machine
peak heap usage the largest amount of heap occupied at the same time
ROM tables tables computed in advance and stored in flash memory
fixed-point optimization speeding up multiplication of the curve's base point with a table
alternative implementation (*_ALT) your own implementation of a function instead of the library's

37а.1 What makes up the cost

A TLS client has three bills.

What Where What it depends on
code flash memory which protocols, cipher suites, curves and hashes are enabled
record buffers heap MBEDTLS_SSL_IN_CONTENT_LEN and MBEDTLS_SSL_OUT_CONTENT_LEN
certificates and keys heap chain length, RSA or curves, whether to keep the server certificate
handshake time processor the key exchange and the number of signature verifications

The cipher suite ECDHE-ECDSA-AES128-GCM-SHA256 reads part by part: ECDHE key exchange on a curve (each side makes up a one-time key, the shared secret is a point multiplication), an ECDSA server signature, the record cipher AES-128 in GCM mode, the SHA-256 hash. Every word in the name is code in flash memory and operations during the handshake.

All the builds in this section share the same client code. Setting up the connection is all in one function; it also shows where the library takes its memory from:

int tls_client_init(tls_client_t *c, const unsigned char *ca_der, size_t ca_len, const char *host,
                    unsigned char mfl_code, entropy_fn entropy, void *entropy_ctx)
{
    static const unsigned char pers[] = "tls37a";
    int r;
    mbedtls_platform_set_calloc_free(tls_calloc, tls_free);
#if defined(MBEDTLS_PSA_CRYPTO_C)
    if (psa_crypto_init() != PSA_SUCCESS)
        return -1;
#endif
    mbedtls_ctr_drbg_init(&c->drbg);
    mbedtls_x509_crt_init(&c->ca);
    mbedtls_ssl_config_init(&c->conf);
    mbedtls_ssl_init(&c->ssl);
    r = mbedtls_ctr_drbg_seed(&c->drbg, entropy, entropy_ctx, pers, sizeof pers - 1);
    if (r == 0)
        r = mbedtls_x509_crt_parse_der(&c->ca, ca_der, ca_len);         /* доверенный корень */
    if (r == 0)
        r = mbedtls_ssl_config_defaults(&c->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM,
                                        MBEDTLS_SSL_PRESET_DEFAULT);
    if (r == 0) {
        mbedtls_ssl_conf_authmode(&c->conf, MBEDTLS_SSL_VERIFY_REQUIRED);  /* без проверки — не TLS */
        mbedtls_ssl_conf_ca_chain(&c->conf, &c->ca, NULL);
        mbedtls_ssl_conf_rng(&c->conf, mbedtls_ctr_drbg_random, &c->drbg);
        if (mfl_code != MBEDTLS_SSL_MAX_FRAG_LEN_NONE)
            r = mbedtls_ssl_conf_max_frag_len(&c->conf, mfl_code);       /* просить короткие записи */
    }
    if (r == 0)
        r = mbedtls_ssl_setup(&c->ssl, &c->conf);                         /* здесь выделяются буферы */
    if (r == 0)
        r = mbedtls_ssl_set_hostname(&c->ssl, host);                      /* SNI и сверка имени */
    return r;
}

The source of randomness entropy is supplied by the caller: in the experiment it is a repeatable counter, so that runs match instruction for instruction; in a device it is the RNG generator (how to connect and check it — 43а). The tls_client_t structure with the library contexts is 1048 bytes and lives in .bss; everything else the library takes from the heap.


37а.2 Configuration: what to switch off

Mbed TLS is built from a single configuration header. The file mbedtls_config.h shipped with it enables almost everything: TLS 1.2 and 1.3, client and server, RSA, all curves, DTLS, PSA, sockets and files. A minimal client needs a shorter list — only what the dependency check in check_config.h cannot do without:

/* Раздел 37а: минимальный клиент TLS 1.2 на Mbed TLS 3.6.4.
 * Один набор шифров ECDHE-ECDSA-AES128-GCM-SHA256, одна кривая P-256,
 * проверка сертификата сервера. Сервера, TLS 1.3, RSA, SHA-1/SHA-512,
 * CBC, PSA, файлов, сокетов и часов нет. */

/* ── платформа: своя куча, ни файлов, ни сокетов, ни часов ── */
#define MBEDTLS_HAVE_ASM
#define MBEDTLS_PLATFORM_C
#define MBEDTLS_PLATFORM_MEMORY

/* ── протокол: только клиент TLS 1.2 ── */
#define MBEDTLS_SSL_TLS_C
#define MBEDTLS_SSL_CLI_C
#define MBEDTLS_SSL_PROTO_TLS1_2
#define MBEDTLS_SSL_MAX_FRAGMENT_LENGTH
#define MBEDTLS_SSL_SERVER_NAME_INDICATION
#define MBEDTLS_SSL_EXTENDED_MASTER_SECRET
#define MBEDTLS_SSL_CIPHERSUITES MBEDTLS_TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256

/* ── обмен ключами и подпись: ECDHE и ECDSA на P-256 ── */
#define MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
#define MBEDTLS_ECP_C
#define MBEDTLS_ECP_NIST_OPTIM
#define MBEDTLS_ECDH_C
#define MBEDTLS_ECDSA_C
#define MBEDTLS_BIGNUM_C
#define MBEDTLS_MPI_MAX_SIZE 32           /* 256 бит — больше P-256 не нужно */
#define MBEDTLS_ECP_WINDOW_SIZE 2         /* меньше таблица точек в куче */
#define MBEDTLS_ECP_FIXED_POINT_OPTIM 0

/* ── шифр записи и хэш: AES-128-GCM и SHA-256 ── */
#define MBEDTLS_AES_C
#define MBEDTLS_AES_ROM_TABLES            /* таблицы AES во флеш-памяти, не в ОЗУ */
#define MBEDTLS_GCM_C
#define MBEDTLS_CIPHER_C
#define MBEDTLS_SHA256_C
#define MBEDTLS_MD_C
#define MBEDTLS_CTR_DRBG_C

/* ── сертификаты: разбор X.509 и открытых ключей ── */
#define MBEDTLS_X509_USE_C
#define MBEDTLS_X509_CRT_PARSE_C
#define MBEDTLS_PK_C
#define MBEDTLS_PK_PARSE_C
#define MBEDTLS_ASN1_PARSE_C
#define MBEDTLS_ASN1_WRITE_C
#define MBEDTLS_OID_C

The header is passed with the build option -DMBEDTLS_CONFIG_FILE="cfg_min.h", and additions on top of it with -DMBEDTLS_USER_CONFIG_FILE. What is deliberately left in here:

And what is deliberately switched off. MBEDTLS_SSL_KEEP_PEER_CERTIFICATE (enabled in the shipped configuration): with it the parsed server certificate stays in the heap until the end of the connection. MBEDTLS_HAVE_TIME_DATE: without a clock the library does not check certificate validity periods at all — mbedtls_x509_time_is_past() in this build always answers "not expired". This is a conscious trade-off: without a real-time clock and synchronization there is nothing to check the dates against (107.8), but an expired certificate will pass too.

The other variants are small files on top of the minimal one: cfg_small.h adds MBEDTLS_AES_FEWER_TABLES, MBEDTLS_AES_ONLY_128_BIT_KEY_LENGTH, MBEDTLS_BLOCK_CIPHER_NO_DECRYPT (GCM decrypts with the same forward block encryption) and MBEDTLS_SHA256_SMALLER; cfg_ramtab.h removes MBEDTLS_AES_ROM_TABLES; cfg_ecpfast.h brings back window 4 and the base point table, as in the shipped configuration; cfg_rsa.h adds the ECDHE-RSA suite with MBEDTLS_RSA_C; cfg_rsakx.h is the old RSA key exchange with no curves at all; cfg_tls13.h is TLS 1.3 instead of 1.2. For comparison, mbedtls_config.h is also built as is — with only what a bare chip lacks removed: sockets, files, the clock, the system entropy source and the x86 and ARMv8 accelerators.


37а.3 Flash memory by configuration

The same client program, eight configuration headers, --gc-sections throws out everything the client does not reach:

Configuration Flash memory, bytes .bss, bytes Versus minimal
minimal, cfg_min.h 59 176 1084 —
trimmed, cfg_small.h 48 752 988 −10 424
AES tables in RAM, cfg_ramtab.h 50 944 9836 −8232 of flash memory, +8752 of RAM
curves with window 4, cfg_ecpfast.h 61 288 1084 +2112
plus RSA certificates, cfg_rsa.h 65 732 1084 +6556
RSA key exchange, cfg_rsakx.h 55 048 1084 −4128
TLS 1.3 instead of 1.2, cfg_tls13.h 75 020 3124 +15 844
as is, cfg_default.h 297 000 11 744 5.0 times larger

The cost of a TLS client on Mbed TLS 3.6.4 for Cortex-M4: on the left, the flash memory of a client-only build for eight configurations — from 47.6 KB trimmed and 57.8 KB minimal to 73.3 KB with TLS 1.3 and 290 KB with the default configuration; on the right, the client's heap peak during the handshake — 11 888 bytes with 4 KB buffers and max_fragment_length, 24 176 with a 4 KB transmit buffer, 36 464 for the minimal one with 16 KB buffers, up to 51 835 for TLS 1.3; at the bottom, the number of handshake instructions per variant

Of the 59 176 bytes of the minimal client, 55 042 are the library itself, 1346 are our own code and 2612 are the C library. The root certificate embedded in the firmware is another 407 bytes for EC and 805 for RSA-2048; they are already included in the table numbers. The top five object files by size:

File Bytes What it is
aes.o 11 260 AES with tables in flash memory
ssl_tls.o 5126 the common part of the protocol
ecp.o 4846 curve point arithmetic
bignum.o 4274 big numbers
x509_crt.o 4194 certificate parsing and verification

The biggest chunk is the AES tables, and they can be traded. MBEDTLS_AES_FEWER_TABLES with the other options of cfg_small.h shrinks aes.o from 11 260 to 2384 bytes; the client loses 10 424 bytes, and the handshake gets 0.3 % more expensive (37а.5). Removing MBEDTLS_AES_ROM_TABLES is the opposite trade: the tables are built in RAM on the first call, flash memory shrinks by 8232 bytes, and .bss grows by 8752 bytes. On the F407 RAM is more precious than flash memory, so MBEDTLS_AES_ROM_TABLES is worth keeping.

The default configuration gives 297 000 bytes: the largest is ecp_curves.o (34 500 bytes, all the curves), plus the TLS 1.2 and 1.3 server code (14 741 and 13 026 bytes), even though the program is client only: the linker did not throw it out. TLS 1.3 costs 15 844 bytes over the minimal 1.2 client — mostly the PSA layer (psa_crypto.o — 8051 bytes), through which TLS 1.3 in Mbed TLS 3.6 always works.

The F103C8 has 64 KB of flash memory, and the minimal client would take 90 % of it — together with a TCP/IP stack (37.3) it does not fit. The F407 with 1 MB has room even for the default configuration, but 290 KB for one library also means space for the second image during an update (48) and time to transfer the firmware.


37а.4 Heap: record buffers and max_fragment_length

The heap counter is our own calloc and free, which the library receives through mbedtls_platform_set_calloc_free(). In front of each block there is a header with its size, so free knows how much to give back:

void *tls_calloc(size_t n, size_t size)
{
    size_t b = n * size;
    hdr_t *h;
    if (size && b / size != n)
        return NULL;                 /* переполнение n * size */
    h = malloc(sizeof(hdr_t) + b);
    if (!h)
        return NULL;
    memset(h + 1, 0, b);
    h->size = (uint32_t)b;
    h->who = (uint32_t)heap_who;
    heap_live[h->who] += h->size;
    if (heap_live[h->who] > heap_peak[h->who])
        heap_peak[h->who] = heap_live[h->who];
    return h + 1;
}

void tls_free(void *p)
{
    hdr_t *h;
    if (!p)
        return;
    h = (hdr_t *)p - 1;
    heap_live[h->who] -= h->size;
    free(h);
}

The counter adds up the requested bytes; the headers and housekeeping fields of malloc itself are not included in the numbers — their cost for heap_4 is covered in 25.3. Under the kernel the same two functions are built on pvPortMalloc and vPortFree. The who field is needed only by the experiment, where the client and the server share one heap.

Most of the heap is the two record buffers that mbedtls_ssl_setup() allocates right away. Each one is the record content plus the header and room for encryption: 16 429 bytes with MBEDTLS_SSL_IN_CONTENT_LEN = 16384 and 4141 with 4096. A run in QEMU, Cortex-M4 client:

Variant After setup Peak during handshake After handshake
16 KB buffers (minimal) 34 353 36 464 35 113
receive 16 KB, transmit 4 KB 22 065 24 176 22 825
4 KB buffers, max_fragment_length 4096 9777 11 888 10 537
16 KB, keep the server certificate 34 337 37 424 36 081
16 KB, curves with window 4 34 353 38 181 35 113
16 KB, server with RSA-2048 34 939 40 695 35 959
16 KB, TLS 1.3 34 417 51 835 38 417

The handshake adds little on top of the buffers: for the minimal client the peak is 2111 bytes above the level right after setup. The peak is held at the steps where the server certificate has been parsed and the signature is being verified, and at the FINISHED steps. Keeping the server certificate (MBEDTLS_SSL_KEEP_PEER_CERTIFICATE) costs 968 bytes for the whole life of the connection. Window 4 on the curve is another 1717 bytes at the peak. An RSA-2048 certificate is 4231 bytes of peak. For TLS 1.3 the peak is 15 371 bytes higher than for 1.2, and it falls on the very first step — CLIENT_HELLO; the client's stack for TLS 1.3 is 2284 bytes versus 1288. After tls_client_free() the heap of TLS 1.2 is empty to the byte; with TLS 1.3, 104 bytes remain in it.

The transmit buffer can be shrunk freely: the client itself decides how long the records it sends are, and the server will accept any length. Minus 12 288 bytes with no conditions.

Shrinking the receive buffer is dangerous: the length of incoming records is chosen by the server, and by the standard it is up to 16 KB. An experiment on a PC — a client with a 4 KB buffer and an Mbed TLS server with its own 16 KB buffers, the server's response being 8000 bytes in one record:

Server Client asks for MFL 4096 What arrived Result
Mbed TLS no an 8024-byte record error -0x7100
Mbed TLS yes two records, the largest 4120 bytes 8000 bytes, ok
Mbed TLS without MFL support yes an 8024-byte record error -0x7100
Mbed TLS, a 3000-byte response no a 3024-byte record 3000 bytes, ok
openssl s_server 3.0.20 no — error -0x7100
openssl s_server 3.0.20 yes — 8045 bytes with the HTTP header, ok

The code -0x7100 is MBEDTLS_ERR_SSL_BAD_INPUT_DATA, "bad input data to the function", even though the record length is to blame: in the debug log Mbed TLS writes "requesting more data than fits". The handshake itself succeeds — it fits in 4 KB — and the very first long response breaks. Hence the rule: a receive buffer smaller than 16 KB only together with mbedtls_ssl_conf_max_frag_len() and a server that understands this extension. The extension costs 5 bytes in the client hello (231 bytes instead of 226). A server that does not know it silently ignores the request, and the client finds out only on a long response.

On a PC the heap of the same client is 1005 bytes larger — 37 469 and 12 893 bytes versus 36 464 and 11 888 on Cortex-M4: pointers and structure fields there are 8 bytes each.


37а.5 Handshake time step by step

In the experiment the client goes through the handshake not with a single call to mbedtls_ssl_handshake() but one step at a time with mbedtls_ssl_handshake_step(). Before a step the number of the state machine's state is saved, and after it, how many instructions passed and what the heap peak was:

    /* рукопожатие: клиент — по одному состоянию, сервер — пока ему есть что делать */
    while (guard++ < 200 && (!mbedtls_ssl_is_handshake_over(&cli.ssl) || !done_s)) {
        if (!mbedtls_ssl_is_handshake_over(&cli.ssl)) {
            int st = cli.ssl.state;
            heap_who = 0;
            heap_peak[0] = heap_live[0];
            top = stack_paint();
            t0 = ticks();
            r = mbedtls_ssl_handshake_step(&cli.ssl);
            t0 = ticks() - t0;

Instructions are counted by SysTick: QEMU with the option -icount shift=0 executes one instruction per nanosecond, and SysTick ticks from the 25 MHz processor clock — 40 instructions per tick; the scale is checked by a loop of 20 million instructions, which gave exactly 500 000 ticks. The minimal client, the server certificate signed by the root:

Client step Million instructions What happens
SERVER_CERTIFICATE 28.59 parsing the server certificate and verifying its ECDSA signature
SERVER_KEY_EXCHANGE 28.52 verifying the ECDSA signature over the server's ECDHE parameters
CLIENT_KEY_EXCHANGE 27.69 our own one-time ECDHE key and the shared secret — two point multiplications
CERTIFICATE_VERIFY 0.29 deriving the session keys from the shared secret
CLIENT_FINISHED, SERVER_FINISHED 0.05 + 0.05 the check messages under AES-GCM
all other steps less than 0.01 hellos and parsing short messages
total 85.21

The same operations one by one:

Operation Million instructions
ECDSA P-256 signature verification 28.58
ECDSA P-256 signing (the server's work) 14.44
one-time ECDHE key 13.81
ECDHE shared secret 13.82
RSA-2048 signature verification (public key) 2.54
RSA-2048 decryption with the private key (the server's work) 75.13
SHA-256 of one kilobyte 0.044
AES-128-GCM of one kilobyte 0.168

Two ECDSA verifications and two point multiplications are 84.79 of the 85.21 million instructions, 99.5 % of the whole handshake. Everything else — hashes, the cipher, parsing — is invisible against that. Hence the cost of the variants:

Variant Client, million instructions Server, million instructions From the server, bytes On the F407, 168 MHz
ECDHE-ECDSA, certificate from the root 85.21 42.62 732 at least 0.51 s
the same through an intermediate certificate 113.96 42.62 1142 at least 0.68 s
ECDHE-ECDSA, curves with window 4 50.33 21.60 732 at least 0.30 s
ECDHE-RSA, RSA-2048 certificate 33.52 105.53 1314 at least 0.20 s
RSA key exchange, RSA-2048 6.83 76.26 970 at least 0.04 s
TLS 1.3, ECDHE-ECDSA 85.29 42.66 788 at least 0.51 s

The time on the F407 is a lower bound: one instruction per clock cycle, no flash wait states.

The chain. An intermediate certificate is one more signature verification: the SERVER_CERTIFICATE step grew from 28.59 to 57.32 million instructions, the server sent 410 bytes more, the heap peak is 485 bytes higher. Every link of the chain on the curve adds 28.6 million instructions.

RSA versus ECDHE. The RSA public operation with exponent 65537 is cheap, the private one is expensive, and both expensive operations fall to the server. A client with an RSA-2048 certificate spends 2.76 million instructions on the certificate instead of 28.59, but the point multiplications for ECDHE remain. The old RSA key exchange removes them too: the client encrypts the session secret with the server's public key, and the whole handshake costs 6.83 million instructions — 12 times cheaper. The price is forward secrecy: whoever recorded the traffic and later obtained the server's private key will decrypt everything. TLS 1.3 has no such key exchange at all: the Mbed TLS configuration for 1.3 has only the PSK and ECDHE modes and their combination.

The curve window. Window 4 and a table for the base point (MBEDTLS_ECP_WINDOW_SIZE 4, MBEDTLS_ECP_FIXED_POINT_OPTIM 1, as in the shipped configuration) remove 41 % of the handshake instructions for 2112 bytes of flash memory and 1717 bytes of heap peak. How MBEDTLS_ECP_NIST_OPTIM and MBEDTLS_HAVE_ASM affect the same signature verification — see 48а.

TLS 1.3 has the same number of heavy operations: the one-time key (13.88 million instructions in CLIENT_HELLO), the shared secret (14.01 in SERVER_HELLO), verification of the certificate (28.51) and of the server signature (28.47). It is more expensive in flash memory and heap, not in time.

Data after the handshake is almost free: a 37-byte request and a 1000-byte response take 183 thousand instructions, about 0.2 % of the handshake. That is why a device keeps one connection and does not reconnect for every message (37.2): every reconnection costs half a second again.


37а.6 Hardware accelerators

Of the cryptography blocks the F407 has only the random number generator: the header stm32f407xx.h declares RNG_BASE, but neither CRYP_BASE nor HASH_BASE. The CRYP and HASH blocks exist on the F417 (without GCM mode), and on the F437 CRYP already has GCM (section 43). The U585 has AES, SAES, HASH, PKA and RNG (2е); PKA computes public-key arithmetic, that is, exactly the point multiplications that take 99.5 % of the handshake.

Mbed TLS does not know these blocks by itself. You connect them with your own functions instead of the library ones: mbedtls_config.h has the options MBEDTLS_AES_ALT, MBEDTLS_GCM_ALT, MBEDTLS_SHA256_ALT, MBEDTLS_ECDSA_VERIFY_ALT, MBEDTLS_ECDH_GEN_PUBLIC_ALT, MBEDTLS_ECDH_COMPUTE_SHARED_ALT and MBEDTLS_ECP_ALT for this, and for the source of randomness, MBEDTLS_ENTROPY_HARDWARE_ALT (43а). The tables in 37а.5 show that the public key is what is worth accelerating: hardware AES saves fractions of a percent on the handshake, and on data about 165 instructions per byte. How much PKA really gives is not measured in this section: QEMU does not model these blocks.


37а.7 Pitfalls

The server's first long response breaks the connection with -0x7100. The receive buffer is smaller than 16 KB, and max_fragment_length was not requested or the server does not know it. The handshake still succeeds — the data breaks.

A "bad input data" error although the call is correct. In Mbed TLS MBEDTLS_ERR_SSL_BAD_INPUT_DATA also means "the record did not fit in the buffer".

The handshake with a server fails with -0x7780. This is MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE: the server sent an alert and aborted the handshake. This is how a client that knows only ECDSA failed against openssl s_server with an RSA-2048 certificate. The client's cipher suites must cover the certificates of the real servers it talks to.

An expired certificate passes verification. Without MBEDTLS_HAVE_TIME_DATE the dates are not checked at all; with it, you need a correct clock (107.8).

The heap ran out during the handshake, though there was enough at setup. The peak is higher than the starting level: by 2111 bytes for the minimal client, by 17 418 for TLS 1.3. Size the heap by the peak, not by mbedtls_ssl_setup().

The handshake takes seconds. A long chain: every link on P-256 adds 28.6 million instructions. For the device's server, use a short path to the root, and put the root into the firmware.

A 290 KB client. It was built with the shipped mbedtls_config.h: all the curves, TLS 1.3, the server. Start with an empty list and add.

TLS 1.3 does not build. In Mbed TLS 3.6 it requires MBEDTLS_SSL_KEEP_PEER_CERTIFICATE and the PSA layer — check_config.h will tell you so with an error.

Data goes fast, but the connection "lags". The device reconnects for every message and pays half a second of handshake each time.


37а.8 Summary

Full source code

tls37a_client.c — a TLS 1.2 client: connection setup.

/* Раздел 37а: клиент TLS 1.2 на Mbed TLS 3.6.4 — настройка соединения.
 * Один и тот же файл собирается под Cortex-M4 (флеш-память, QEMU) и на ПК (обмен с сервером). */
#include "mbedtls/platform.h"
#if defined(MBEDTLS_PSA_CRYPTO_C)
#include "psa/crypto.h"
#endif
#include "tls37a.h"

int tls_client_init(tls_client_t *c, const unsigned char *ca_der, size_t ca_len, const char *host,
                    unsigned char mfl_code, entropy_fn entropy, void *entropy_ctx)
{
    static const unsigned char pers[] = "tls37a";
    int r;
    mbedtls_platform_set_calloc_free(tls_calloc, tls_free);
#if defined(MBEDTLS_PSA_CRYPTO_C)
    if (psa_crypto_init() != PSA_SUCCESS)
        return -1;
#endif
    mbedtls_ctr_drbg_init(&c->drbg);
    mbedtls_x509_crt_init(&c->ca);
    mbedtls_ssl_config_init(&c->conf);
    mbedtls_ssl_init(&c->ssl);
    r = mbedtls_ctr_drbg_seed(&c->drbg, entropy, entropy_ctx, pers, sizeof pers - 1);
    if (r == 0)
        r = mbedtls_x509_crt_parse_der(&c->ca, ca_der, ca_len);         /* доверенный корень */
    if (r == 0)
        r = mbedtls_ssl_config_defaults(&c->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_STREAM,
                                        MBEDTLS_SSL_PRESET_DEFAULT);
    if (r == 0) {
        mbedtls_ssl_conf_authmode(&c->conf, MBEDTLS_SSL_VERIFY_REQUIRED);  /* без проверки — не TLS */
        mbedtls_ssl_conf_ca_chain(&c->conf, &c->ca, NULL);
        mbedtls_ssl_conf_rng(&c->conf, mbedtls_ctr_drbg_random, &c->drbg);
        if (mfl_code != MBEDTLS_SSL_MAX_FRAG_LEN_NONE)
            r = mbedtls_ssl_conf_max_frag_len(&c->conf, mfl_code);       /* просить короткие записи */
    }
    if (r == 0)
        r = mbedtls_ssl_setup(&c->ssl, &c->conf);                         /* здесь выделяются буферы */
    if (r == 0)
        r = mbedtls_ssl_set_hostname(&c->ssl, host);                      /* SNI и сверка имени */
    return r;
}

void tls_client_free(tls_client_t *c)
{
    mbedtls_ssl_free(&c->ssl);
    mbedtls_ssl_config_free(&c->conf);
    mbedtls_x509_crt_free(&c->ca);
    mbedtls_ctr_drbg_free(&c->drbg);
}

cfg_rsakx.h — configuration for the old RSA key exchange.

/* Раздел 37а: клиент со старым обменом ключами RSA (TLS-RSA-WITH-AES-128-GCM-SHA256):
 * кривых нет вовсе, ключ сессии шифруется открытым ключом сервера. */
#define MBEDTLS_HAVE_ASM
#define MBEDTLS_PLATFORM_C
#define MBEDTLS_PLATFORM_MEMORY
#define MBEDTLS_SSL_TLS_C
#define MBEDTLS_SSL_CLI_C
#define MBEDTLS_SSL_PROTO_TLS1_2
#define MBEDTLS_SSL_MAX_FRAGMENT_LENGTH
#define MBEDTLS_SSL_SERVER_NAME_INDICATION
#define MBEDTLS_SSL_EXTENDED_MASTER_SECRET
#define MBEDTLS_SSL_CIPHERSUITES MBEDTLS_TLS_RSA_WITH_AES_128_GCM_SHA256
#define MBEDTLS_KEY_EXCHANGE_RSA_ENABLED
#define MBEDTLS_RSA_C
#define MBEDTLS_PKCS1_V15
#define MBEDTLS_BIGNUM_C
#define MBEDTLS_MPI_MAX_SIZE 256
#define MBEDTLS_AES_C
#define MBEDTLS_AES_ROM_TABLES
#define MBEDTLS_GCM_C
#define MBEDTLS_CIPHER_C
#define MBEDTLS_SHA256_C
#define MBEDTLS_MD_C
#define MBEDTLS_CTR_DRBG_C
#define MBEDTLS_X509_USE_C
#define MBEDTLS_X509_CRT_PARSE_C
#define MBEDTLS_PK_C
#define MBEDTLS_PK_PARSE_C
#define MBEDTLS_ASN1_PARSE_C
#define MBEDTLS_ASN1_WRITE_C
#define MBEDTLS_OID_C

cfg_tls13.h — configuration of a TLS 1.3 client.

/* Раздел 37а: для сравнения — клиент TLS 1.3 без TLS 1.2: один набор TLS_AES_128_GCM_SHA256,
 * обмен ключами ECDHE на P-256, подпись ECDSA. TLS 1.3 в Mbed TLS 3.6 работает через PSA. */
#define MBEDTLS_HAVE_ASM
#define MBEDTLS_PLATFORM_C
#define MBEDTLS_PLATFORM_MEMORY
#define MBEDTLS_NO_PLATFORM_ENTROPY
#define MBEDTLS_ENTROPY_HARDWARE_ALT
#define MBEDTLS_ENTROPY_C
#define MBEDTLS_PSA_CRYPTO_C
#define MBEDTLS_SSL_TLS_C
#define MBEDTLS_SSL_CLI_C
#define MBEDTLS_SSL_PROTO_TLS1_3
#define MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_EPHEMERAL_ENABLED
#define MBEDTLS_SSL_MAX_FRAGMENT_LENGTH
#define MBEDTLS_SSL_SERVER_NAME_INDICATION
#define MBEDTLS_SSL_KEEP_PEER_CERTIFICATE  /* TLS 1.3 в 3.6 без него не собирается */
#define MBEDTLS_SSL_CIPHERSUITES MBEDTLS_TLS1_3_AES_128_GCM_SHA256
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
#define MBEDTLS_ECP_C
#define MBEDTLS_ECP_NIST_OPTIM
#define MBEDTLS_ECDH_C
#define MBEDTLS_ECDSA_C
#define MBEDTLS_BIGNUM_C
#define MBEDTLS_MPI_MAX_SIZE 32
#define MBEDTLS_ECP_WINDOW_SIZE 2
#define MBEDTLS_ECP_FIXED_POINT_OPTIM 0
#define MBEDTLS_AES_C
#define MBEDTLS_AES_ROM_TABLES
#define MBEDTLS_GCM_C
#define MBEDTLS_CIPHER_C
#define MBEDTLS_SHA256_C
#define MBEDTLS_MD_C
#define MBEDTLS_HKDF_C
#define MBEDTLS_CTR_DRBG_C
#define MBEDTLS_X509_USE_C
#define MBEDTLS_X509_CRT_PARSE_C
#define MBEDTLS_PK_C
#define MBEDTLS_PK_PARSE_C
#define MBEDTLS_ASN1_PARSE_C
#define MBEDTLS_ASN1_WRITE_C
#define MBEDTLS_OID_C

heap37a.c — our own heap counter for Mbed TLS.

/* Раздел 37а: свой счётчик кучи для Mbed TLS — calloc и free с заголовком перед блоком.
 * Подключается вызовом mbedtls_platform_set_calloc_free(tls_calloc, tls_free). */
#include <stdlib.h>
#include <string.h>
#include "tls37a.h"

uint32_t heap_live[2], heap_peak[2];
int heap_who;

typedef struct {                     /* заголовок перед каждым блоком: размер и чей он */
    uint32_t size, who;
    uint64_t align;                  /* блок после заголовка выровнен на 8 */
} hdr_t;

void *tls_calloc(size_t n, size_t size)
{
    size_t b = n * size;
    hdr_t *h;
    if (size && b / size != n)
        return NULL;                 /* переполнение n * size */
    h = malloc(sizeof(hdr_t) + b);
    if (!h)
        return NULL;
    memset(h + 1, 0, b);
    h->size = (uint32_t)b;
    h->who = (uint32_t)heap_who;
    heap_live[h->who] += h->size;
    if (heap_live[h->who] > heap_peak[h->who])
        heap_peak[h->who] = heap_live[h->who];
    return h + 1;
}

void tls_free(void *p)
{
    hdr_t *h;
    if (!p)
        return;
    h = (hdr_t *)p - 1;
    heap_live[h->who] -= h->size;
    free(h);
}

Next: section 38 "Consolidated project catalog" — all the practical projects of the handbook in one list.

Open in the interactive handbook