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:
MBEDTLS_SSL_MAX_FRAGMENT_LENGTH— without it the client cannot ask the server for short records, and a receive buffer smaller than 16 KB becomes dangerous (37а.4).MBEDTLS_SSL_SERVER_NAME_INDICATION— the server name in the hello (107.5); without it a server with many sites will present the wrong certificate.MBEDTLS_SSL_EXTENDED_MASTER_SECRET— binding the session keys to the text of the whole handshake; it costs almost no code.
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 |
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
- A minimal TLS 1.2 client (ECDHE-ECDSA-AES128-GCM-SHA256, P-256):
59 176 bytes of flash memory, 55 042 of them the library; trimmed — 48 752;
mbedtls_config.has is — 297 000. - The AES tables take the most — 11 260 bytes;
MBEDTLS_AES_FEWER_TABLESwith its neighbouring options removes 10 424 bytes at the price of 0.3 % of the time. - TLS 1.3 adds 15 844 bytes of flash memory and 15 371 bytes of heap peak, the time is the same.
- The heap is above all two record buffers of 16 429 bytes each; the client's peak is 36 464 bytes. A 4 KB transmit buffer saves 12 288 bytes unconditionally, a 4 KB receive buffer only with max_fragment_length: then the peak is 11 888 bytes.
- Without MFL a 4 KB receive buffer breaks on an 8000-byte response with
-0x7100;openssl s_server3.0.20 and the Mbed TLS server honour the request, a server without support for the extension does not. - The handshake is 85.21 million instructions, 99.5 % of them two ECDSA verifications and two point multiplications; at least 0.51 s on the F407. An intermediate certificate adds 28.6 million, window 4 removes 41 %, the RSA key exchange takes 6.83 million, but without forward secrecy and without TLS 1.3.
- Data after the handshake: a 1000-byte response takes 183 thousand instructions; keep the connection open rather than reopening it.
- Hardware blocks are connected through
*_ALT; the public key is what is worth accelerating (PKA on the U585), not AES.
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.