Embedded HandbookПриложения
Все разделы › VI. Практикум
STM32F407 · уровень продвинутый · 25 мин чтения
Открыть в интерактивном справочнике

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_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 раза больше

Цена клиента TLS на Mbed TLS 3.6.4 под Cortex-M4: слева флеш-память сборки только клиента по восьми настройкам — от 47,6 КБ ужатого и 57,8 КБ минимального до 73,3 КБ с TLS 1.3 и 290 КБ у настроек по умолчанию; справа пик кучи клиента за рукопожатие — 11 888 байт при буферах 4 КБ с max_fragment_length, 24 176 при буфере передачи 4 КБ, 36 464 у минимального с буферами 16 КБ, до 51 835 у TLS 1.3; внизу число команд рукопожатия по вариантам

Из 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 Итоги

Полные исходники

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 «Сводный каталог проектов» — все практические проекты справочника одним списком.

Открыть в интерактивном справочнике