37а. Сколько стоит TLS на микроконтроллере: Mbed TLS по флешу и ОЗУ
В разделе 37 сказано коротко: шифрование на F407 — библиотека, и она весит десятки килобайт. В 107.7 дана первая оценка: клиент TLS 1.2 на Mbed TLS — 57 КБ флеш-памяти, 12–36 КБ кучи и 85 млн команд на рукопожатие. Здесь та же цена разобрана по частям: что именно занимает флеш-память, откуда берётся куча, на каком шаге рукопожатия уходит время и что меняется, если выключить в настройках лишнее, взять RSA вместо кривых, удлинить цепочку сертификатов или уменьшить буферы записи.
Все числа раздела — из прогонов. Mbed TLS 3.6.4 (github.com/Mbed-TLS/mbedtls,
выпуск v3.6.4, лицензия Apache-2.0) собран arm-none-eabi-gcc 12.2 -Os
под Cortex-M4. Флеш-память считается по сборке «только клиент». Время и куча —
в QEMU (плата mps2-an386, Cortex-M4): клиент и сервер Mbed TLS в одной
программе, «провод» — два буфера в памяти. Буферы записи и расширение
max_fragment_length проверены ещё и на ПК: две программы с разными
настройками через пару сокетов и клиент против openssl s_server.
Опоры: что видно в TLS до шифрования и первая оценка цены — 107.5 и 107.7;
подпись ECDSA, SHA-256 и ключи MBEDTLS_ECP_NIST_OPTIM, MBEDTLS_HAVE_ASM —
48а; куча ядра — 25; аппаратные AES и HASH — 43; блоки AES, HASH, PKA у U585 —
2е; прибор с HTTP и MQTT, где TLS понадобится, — 198з и 198и; откуда клиент
берёт случайные числа — 43а.
Как это называется в документации
Оригинал Перевод cipher suite набор шифров: обмен ключами, подпись, шифр записи и хеш одним именем key exchange обмен ключами: как стороны получают общий секрет сеанса forward secrecy прямая секретность: записанный трафик не раскрыть украденным ключом сервера intermediate CA промежуточный центр сертификации: звено между корнем и сертификатом сервера TLS record запись TLS: кусок потока до 16 КБ со своим заголовком и меткой maximum fragment length (MFL) расширение «наибольшая длина записи» handshake state шаг рукопожатия в автомате библиотеки peak heap usage пик кучи: наибольший объём, занятый одновременно ROM tables таблицы, заранее посчитанные и лежащие во флеш-памяти fixed-point optimization ускорение умножения базовой точки кривой по таблице alternative implementation ( *_ALT)своя реализация функции вместо библиотечной
37а.1 Из чего складывается цена
У клиента TLS три счёта.
| Что | Где | От чего зависит |
|---|---|---|
| код | флеш-память | какие протоколы, наборы шифров, кривые и хеши включены |
| буферы записей | куча | MBEDTLS_SSL_IN_CONTENT_LEN и MBEDTLS_SSL_OUT_CONTENT_LEN |
| сертификаты и ключи | куча | длина цепочки, RSA или кривые, хранить ли сертификат сервера |
| время рукопожатия | процессор | обмен ключами и число проверок подписи |
Набор шифров ECDHE-ECDSA-AES128-GCM-SHA256 читается по частям: обмен ключами ECDHE на кривой (каждая сторона придумывает разовый ключ, общий секрет — умножение точки), подпись сервера ECDSA, шифр записей AES-128 в режиме GCM, хеш SHA-256. Каждое слово в имени — это код во флеш-памяти и операции на рукопожатии.
Код клиента у всех сборок раздела один. Настройка соединения — вся в одной функции; в ней же видно, где библиотека берёт память:
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;
}
Источник случайности entropy подаёт вызывающий: в опыте это повторяемый
счётчик, чтобы прогоны совпадали команда в команду, в приборе — генератор
RNG (как его подключить и проверить — 43а). Структура tls_client_t с
контекстами библиотеки — 1048 байт, она лежит в .bss; всё остальное
библиотека берёт из кучи.
37а.2 Настройки: что выключить
Mbed TLS собирается по одному заголовку настроек. Файл mbedtls_config.h
из поставки включает почти всё: TLS 1.2 и 1.3, клиент и сервер, RSA, все
кривые, DTLS, PSA, сокеты и файлы. Минимальному клиенту нужен список
покороче — только то, без чего не сойдётся проверка зависимостей
check_config.h:
/* Раздел 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
Заголовок подключают ключом сборки
-DMBEDTLS_CONFIG_FILE="cfg_min.h", а добавки поверх него —
-DMBEDTLS_USER_CONFIG_FILE. Что здесь сознательно оставлено:
MBEDTLS_SSL_MAX_FRAGMENT_LENGTH— без него клиент не сможет попросить у сервера короткие записи, и буфер приёма меньше 16 КБ станет опасным (37а.4).MBEDTLS_SSL_SERVER_NAME_INDICATION— имя сервера в приветствии (107.5); сервер со многими сайтами без него покажет не тот сертификат.MBEDTLS_SSL_EXTENDED_MASTER_SECRET— привязка ключей сеанса к тексту всего рукопожатия; кода на неё почти не уходит.
И что сознательно выключено. MBEDTLS_SSL_KEEP_PEER_CERTIFICATE (в поставке
включён): с ним разобранный сертификат сервера живёт в куче до конца
соединения. MBEDTLS_HAVE_TIME_DATE: без часов библиотека вообще не
сверяет сроки сертификатов — mbedtls_x509_time_is_past() в этой сборке
всегда отвечает «не истёк». Это осознанный обмен: без часов реального
времени и синхронизации сверять сроки не с чем (107.8), но и просроченный
сертификат пройдёт.
Остальные варианты — маленькие файлы поверх минимального: cfg_small.h
добавляет MBEDTLS_AES_FEWER_TABLES, MBEDTLS_AES_ONLY_128_BIT_KEY_LENGTH,
MBEDTLS_BLOCK_CIPHER_NO_DECRYPT (GCM расшифровывает тем же прямым
шифрованием блока) и MBEDTLS_SHA256_SMALLER; cfg_ramtab.h убирает
MBEDTLS_AES_ROM_TABLES; cfg_ecpfast.h возвращает окно 4 и таблицу
базовой точки, как в поставке; cfg_rsa.h добавляет набор ECDHE-RSA
с MBEDTLS_RSA_C; cfg_rsakx.h — старый обмен ключами RSA без кривых
совсем; cfg_tls13.h — TLS 1.3 вместо 1.2. Для сравнения собран и
mbedtls_config.h как есть — убрано только то, чего на голом камне нет:
сокеты, файлы, часы, системный источник энтропии и ускорители x86 и ARMv8.
37а.3 Флеш-память по настройкам
Одна и та же программа-клиент, восемь заголовков настроек, --gc-sections
выбрасывает всё, до чего клиент не дотягивается:
| Настройки | Флеш-память, байт | .bss, байт |
Против минимального |
|---|---|---|---|
минимальный, cfg_min.h |
59 176 | 1084 | — |
ужатый, cfg_small.h |
48 752 | 988 | −10 424 |
таблицы AES в ОЗУ, cfg_ramtab.h |
50 944 | 9836 | −8232 флеш-памяти, +8752 ОЗУ |
кривые с окном 4, cfg_ecpfast.h |
61 288 | 1084 | +2112 |
плюс сертификаты RSA, cfg_rsa.h |
65 732 | 1084 | +6556 |
обмен ключами RSA, cfg_rsakx.h |
55 048 | 1084 | −4128 |
TLS 1.3 вместо 1.2, cfg_tls13.h |
75 020 | 3124 | +15 844 |
как есть, cfg_default.h |
297 000 | 11 744 | в 5,0 раза больше |
Из 59 176 байт минимального клиента 55 042 — сама библиотека, 1346 — свой код и 2612 — библиотека C. Корневой сертификат, вшитый в прошивку, — ещё 407 байт для EC и 805 для RSA-2048, они в числах таблицы уже есть. Первые пять объектных файлов по размеру:
| Файл | Байт | Что это |
|---|---|---|
aes.o |
11 260 | AES с таблицами во флеш-памяти |
ssl_tls.o |
5126 | общая часть протокола |
ecp.o |
4846 | арифметика точек кривой |
bignum.o |
4274 | большие числа |
x509_crt.o |
4194 | разбор и проверка сертификатов |
Самый крупный кусок — таблицы AES, и ими можно торговать.
MBEDTLS_AES_FEWER_TABLES с остальными ключами cfg_small.h сжимает aes.o
с 11 260 до 2384 байт; клиент худеет на 10 424 байта, а рукопожатие
дорожает на 0,3 % (37а.5). Убрать MBEDTLS_AES_ROM_TABLES — обратный
обмен: таблицы строятся при первом вызове в ОЗУ, флеш-памяти на 8232 байта
меньше, а .bss на 8752 байта больше. На F407 ОЗУ дороже флеш-памяти,
так что MBEDTLS_AES_ROM_TABLES стоит оставить.
Настройки по умолчанию дают 297 000 байт: больше всех — ecp_curves.o
(34 500 байт, все кривые), а ещё код сервера TLS 1.2 и 1.3 (14 741 и 13 026
байт), хотя программа — только клиент: компоновщик его не выбросил. TLS 1.3 стоит 15 844 байта сверх
минимального клиента 1.2 — в основном это слой PSA (psa_crypto.o — 8051 байт),
через который TLS 1.3 в Mbed TLS 3.6 работает всегда.
У F103C8 64 КБ флеш-памяти, и минимальный клиент занял бы 90 % её — вместе со стеком TCP/IP (37.3) не помещается. У F407 с 1 МБ хватит и на настройки по умолчанию, но 290 КБ на одну библиотеку — это и место под второй образ при обновлении (48), и время передачи прошивки.
37а.4 Куча: буферы записей и max_fragment_length
Счётчик кучи — свои calloc и free, которые библиотека получает через
mbedtls_platform_set_calloc_free(). Перед каждым блоком — заголовок с его
размером, поэтому free знает, сколько вернуть:
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);
}
Счётчик складывает запрошенные байты; заголовки и служебные поля самого
malloc в числа не входят — их цену у heap_4 разбирает 25.3. Под ядром
те же две функции строят на pvPortMalloc и vPortFree. Поле who
нужно только опыту, где клиент и сервер делят одну кучу.
Большая часть кучи — два буфера записей, которые mbedtls_ssl_setup()
выделяет сразу. Каждый — содержимое записи плюс заголовок и место под
шифрование: 16 429 байт при MBEDTLS_SSL_IN_CONTENT_LEN = 16384 и 4141 —
при 4096. Прогон в QEMU, клиент Cortex-M4:
| Вариант | После настройки | Пик за рукопожатие | После рукопожатия |
|---|---|---|---|
| буферы 16 КБ (минимальный) | 34 353 | 36 464 | 35 113 |
| приём 16 КБ, передача 4 КБ | 22 065 | 24 176 | 22 825 |
| буферы 4 КБ, max_fragment_length 4096 | 9777 | 11 888 | 10 537 |
| 16 КБ, хранить сертификат сервера | 34 337 | 37 424 | 36 081 |
| 16 КБ, кривые с окном 4 | 34 353 | 38 181 | 35 113 |
| 16 КБ, сервер с RSA-2048 | 34 939 | 40 695 | 35 959 |
| 16 КБ, TLS 1.3 | 34 417 | 51 835 | 38 417 |
Рукопожатие поверх буферов добавляет немного: у минимального клиента
пик на 2111 байт выше, чем сразу после настройки. Пик держится на шагах,
где разобран сертификат сервера и идёт проверка подписи, и на шагах
FINISHED. Хранить сертификат сервера (MBEDTLS_SSL_KEEP_PEER_CERTIFICATE)
стоит 968 байт на всё время соединения. Окно 4 на кривой — ещё 1717 байт
на пике. Сертификат RSA-2048 — 4231 байт пика. У TLS 1.3 пик на 15 371 байт
выше, чем у 1.2, и приходится он на первый же шаг — CLIENT_HELLO; стек
клиента у TLS 1.3 — 2284 байта против 1288. После tls_client_free() куча
TLS 1.2 пуста до байта; у TLS 1.3 в ней остаются 104 байта.
Буфер передачи уменьшают свободно: клиент сам решает, какой длины записи отправлять, и сервер примет любую. Минус 12 288 байт без всяких условий.
Буфер приёма уменьшать опасно: длину входящих записей выбирает сервер, и по стандарту она до 16 КБ. Опыт на ПК — клиент с буфером 4 КБ и сервер Mbed TLS со своими буферами 16 КБ, ответ сервера — 8000 байт одной записью:
| Сервер | Клиент просит MFL 4096 | Что пришло | Итог |
|---|---|---|---|
| Mbed TLS | нет | запись 8024 байта | ошибка -0x7100 |
| Mbed TLS | да | две записи, наибольшая 4120 байт | 8000 байт, ок |
| Mbed TLS без поддержки MFL | да | запись 8024 байта | ошибка -0x7100 |
| Mbed TLS, ответ 3000 байт | нет | запись 3024 байта | 3000 байт, ок |
openssl s_server 3.0.20 |
нет | — | ошибка -0x7100 |
openssl s_server 3.0.20 |
да | — | 8045 байт с заголовком HTTP, ок |
Код -0x7100 — MBEDTLS_ERR_SSL_BAD_INPUT_DATA, «неверные входные данные
функции», хотя виновата длина записи: в журнале отладки Mbed TLS пишет
«requesting more data than fits». Рукопожатие при этом проходит успешно —
оно умещается в 4 КБ, — и ломается первый же длинный ответ. Отсюда правило:
буфер приёма меньше 16 КБ — только вместе с mbedtls_ssl_conf_max_frag_len()
и сервером, который это расширение понимает. Расширение стоит 5 байт
в приветствии клиента (231 байт вместо 226). Сервер, который его не знает,
молча игнорирует просьбу, и клиент узнаёт об этом только на длинном ответе.
На ПК куча того же клиента на 1005 байт больше — 37 469 и 12 893 байта против 36 464 и 11 888 на Cortex-M4: указатели и поля структур там по 8 байт.
37а.5 Время рукопожатия по шагам
Клиент в опыте идёт по рукопожатию не одним вызовом mbedtls_ssl_handshake(),
а по одному шагу mbedtls_ssl_handshake_step(). Перед шагом запоминается
номер состояния автомата, после — сколько команд прошло и какой был пик кучи:
/* рукопожатие: клиент — по одному состоянию, сервер — пока ему есть что делать */
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;
Команды считает SysTick: QEMU с ключом -icount shift=0 выполняет одну
команду за наносекунду, а SysTick тикает от частоты процессора 25 МГц —
40 команд на тик; масштаб проверяет цикл из 20 млн команд, он дал ровно
500 000 тиков. Минимальный клиент, сертификат сервера подписан корнем:
| Шаг клиента | Млн команд | Что делается |
|---|---|---|
SERVER_CERTIFICATE |
28,59 | разбор сертификата сервера и проверка его подписи ECDSA |
SERVER_KEY_EXCHANGE |
28,52 | проверка подписи ECDSA под параметрами ECDHE сервера |
CLIENT_KEY_EXCHANGE |
27,69 | свой разовый ключ ECDHE и общий секрет — два умножения точки |
CERTIFICATE_VERIFY |
0,29 | вывод ключей сеанса из общего секрета |
CLIENT_FINISHED, SERVER_FINISHED |
0,05 + 0,05 | контрольные сообщения под AES-GCM |
| все остальные шаги | меньше 0,01 | приветствия и разбор коротких сообщений |
| всего | 85,21 |
Те же операции по отдельности:
| Операция | Млн команд |
|---|---|
| проверка подписи ECDSA P-256 | 28,58 |
| подпись ECDSA P-256 (работа сервера) | 14,44 |
| разовый ключ ECDHE | 13,81 |
| общий секрет ECDHE | 13,82 |
| проверка подписи RSA-2048 (открытый ключ) | 2,54 |
| расшифровка RSA-2048 закрытым ключом (работа сервера) | 75,13 |
| SHA-256 одного килобайта | 0,044 |
| AES-128-GCM одного килобайта | 0,168 |
Две проверки ECDSA и два умножения точки — 84,79 из 85,21 млн команд, 99,5 % всего рукопожатия. Всё остальное — хеши, шифр, разбор — на этом фоне не видно. Отсюда и цена вариантов:
| Вариант | Клиент, млн команд | Сервер, млн команд | От сервера, байт | На F407, 168 МГц |
|---|---|---|---|---|
| ECDHE-ECDSA, сертификат от корня | 85,21 | 42,62 | 732 | не меньше 0,51 с |
| то же через промежуточный сертификат | 113,96 | 42,62 | 1142 | не меньше 0,68 с |
| ECDHE-ECDSA, кривые с окном 4 | 50,33 | 21,60 | 732 | не меньше 0,30 с |
| ECDHE-RSA, сертификат RSA-2048 | 33,52 | 105,53 | 1314 | не меньше 0,20 с |
| обмен ключами RSA, RSA-2048 | 6,83 | 76,26 | 970 | не меньше 0,04 с |
| TLS 1.3, ECDHE-ECDSA | 85,29 | 42,66 | 788 | не меньше 0,51 с |
Время на F407 — нижняя граница: одна команда за такт, без ожидания флеш-памяти.
Цепочка. Промежуточный сертификат — это ещё одна проверка подписи:
шаг SERVER_CERTIFICATE вырос с 28,59 до 57,32 млн команд, сервер прислал
на 410 байт больше, пик кучи — на 485 байт выше. Каждое звено цепочки
на кривой — плюс 28,6 млн команд.
RSA против ECDHE. Открытая операция RSA с показателем 65537 дешёвая, закрытая — дорогая, и обе дорогие операции достаются серверу. Клиент с сертификатом RSA-2048 тратит 2,76 млн команд на сертификат вместо 28,59, но умножения точки для ECDHE остаются. Старый обмен ключами RSA убирает и их: клиент шифрует секрет сеанса открытым ключом сервера, и всё рукопожатие стоит 6,83 млн команд — в 12 раз дешевле. Цена — прямая секретность: кто записал трафик и потом добыл закрытый ключ сервера, расшифрует всё. В TLS 1.3 такого обмена ключами нет вовсе: в настройках Mbed TLS для 1.3 есть только режимы PSK, ECDHE и их сочетание.
Окно кривой. Окно 4 и таблица для базовой точки
(MBEDTLS_ECP_WINDOW_SIZE 4, MBEDTLS_ECP_FIXED_POINT_OPTIM 1, как в
поставке) снимают 41 % команд рукопожатия за 2112 байт флеш-памяти и 1717 байт
пика кучи. Как на ту же проверку подписи влияют MBEDTLS_ECP_NIST_OPTIM и
MBEDTLS_HAVE_ASM — 48а.
TLS 1.3 по числу тяжёлых операций такой же: разовый ключ (13,88 млн
команд на CLIENT_HELLO), общий секрет (14,01 на SERVER_HELLO), проверка
сертификата (28,51) и подписи сервера (28,47). Он дороже по флеш-памяти
и куче, а не по времени.
Данные после рукопожатия почти бесплатны: запрос 37 байт и ответ 1000 байт — 183 тысячи команд, около 0,2 % рукопожатия. Поэтому прибор держит одно соединение и не переподключается на каждое сообщение (37.2): каждое переподключение — заново полсекунды.
37а.6 Аппаратные ускорители
У F407 из криптографии есть только генератор случайных чисел: в заголовке
stm32f407xx.h объявлен RNG_BASE, но нет ни CRYP_BASE, ни HASH_BASE.
Блоки CRYP и HASH есть у F417 (без режима GCM), у F437 — CRYP уже с GCM
(раздел 43). У U585 — AES, SAES, HASH, PKA и RNG (2е); PKA считает
арифметику открытого ключа, то есть ровно те умножения точки, на которые
уходит 99,5 % рукопожатия.
Mbed TLS сам этих блоков не знает. Подключают их своими функциями вместо
библиотечных: в mbedtls_config.h для этого есть ключи MBEDTLS_AES_ALT,
MBEDTLS_GCM_ALT, MBEDTLS_SHA256_ALT, MBEDTLS_ECDSA_VERIFY_ALT,
MBEDTLS_ECDH_GEN_PUBLIC_ALT, MBEDTLS_ECDH_COMPUTE_SHARED_ALT и
MBEDTLS_ECP_ALT, а для источника случайности — MBEDTLS_ENTROPY_HARDWARE_ALT
(43а). Из таблиц 37а.5 видно, что ускорять стоит открытый ключ:
аппаратный AES на рукопожатии сбережёт доли процента, а на данных — около
165 команд на байт. Сколько даёт PKA на самом деле, в разделе не измерено:
QEMU этих блоков не моделирует.
37а.7 Грабли
Первый длинный ответ сервера рвёт соединение с -0x7100. Буфер приёма
меньше 16 КБ, а max_fragment_length не попросили или сервер его не знает.
Рукопожатие при этом проходит — ломаются данные.
Ошибка «неверные входные данные», хотя вызов правильный. У Mbed TLS
MBEDTLS_ERR_SSL_BAD_INPUT_DATA значит и «запись не влезла в буфер».
Рукопожатие с сервером падает с -0x7780. Это
MBEDTLS_ERR_SSL_FATAL_ALERT_MESSAGE: сервер прислал тревогу и прервал
рукопожатие. Так упал клиент, знающий только ECDSA, против openssl s_server
с сертификатом RSA-2048. Наборы клиента должны покрывать
сертификаты реальных серверов, с которыми он говорит.
Просроченный сертификат проходит проверку. Без MBEDTLS_HAVE_TIME_DATE
сроки не сверяются вовсе; с ним — нужны верные часы (107.8).
Куча кончилась на рукопожатии, а при настройке хватало. Пик выше
стартового: у минимального клиента на 2111 байт, у TLS 1.3 — на 17 418.
Кучу считают по пику, а не по mbedtls_ssl_setup().
Рукопожатие идёт секунды. Длинная цепочка: каждое звено на P-256 — плюс 28,6 млн команд. Сервер прибора лучше отдавать с коротким путём до корня, а корень класть в прошивку.
Клиент на 290 КБ. Собрали с mbedtls_config.h из поставки: все кривые,
TLS 1.3, сервер. Начинать с пустого списка и добавлять.
TLS 1.3 не собирается. В Mbed TLS 3.6 он требует
MBEDTLS_SSL_KEEP_PEER_CERTIFICATE и слой PSA — check_config.h скажет
об этом ошибкой.
Данные идут быстро, а соединение «тормозит». Прибор переподключается на каждое сообщение и каждый раз платит полсекунды рукопожатия.
37а.8 Итоги
- Минимальный клиент TLS 1.2 (ECDHE-ECDSA-AES128-GCM-SHA256, P-256):
59 176 байт флеш-памяти, из них 55 042 — библиотека; ужатый — 48 752;
mbedtls_config.hкак есть — 297 000. - Больше всех берут таблицы AES — 11 260 байт;
MBEDTLS_AES_FEWER_TABLESс соседями ключами снимают 10 424 байта ценой 0,3 % времени. - TLS 1.3 — плюс 15 844 байта флеш-памяти и 15 371 байт пика кучи, время то же.
- Куча — это прежде всего два буфера записей по 16 429 байт; пик клиента — 36 464 байта. Буфер передачи 4 КБ — минус 12 288 байт без условий, буфер приёма 4 КБ — только с max_fragment_length: тогда пик 11 888 байт.
- Без MFL буфер приёма 4 КБ ломается на ответе 8000 байт с
-0x7100;openssl s_server3.0.20 и сервер Mbed TLS просьбу выполняют, сервер без поддержки расширения — нет. - Рукопожатие — 85,21 млн команд, 99,5 % из них — две проверки ECDSA и два умножения точки; на F407 не меньше 0,51 с. Промежуточный сертификат — плюс 28,6 млн, окно 4 — минус 41 %, обмен ключами RSA — 6,83 млн, но без прямой секретности и без TLS 1.3.
- Данные после рукопожатия: 1000 байт ответа — 183 тысячи команд; соединение держат, а не переоткрывают.
- Аппаратные блоки подключают через
*_ALT; ускорять стоит открытый ключ (PKA у U585), а не AES.
Полные исходники
tls37a_client.c — клиент TLS 1.2: настройка соединения.
/* Раздел 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 — настройки для старого обмена ключами RSA.
/* Раздел 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 — настройки клиента TLS 1.3.
/* Раздел 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 — свой счётчик кучи для 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);
}
Дальше: раздел 38 «Сводный каталог проектов» — все практические проекты справочника одним списком.