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

48г. Шифрование образа прошивки: ключ, режим AES и где ключ живёт в приборе

В загрузчике 198щ ключ есть, но он только подтверждает образ: HMAC показывает, что образ сделал владелец ключа и что в нём не изменён ни один бит. Сам образ идёт открытым. Его можно прочитать везде, где он лежит или проходит: в канале Bluetooth и UART, в файле внутри приложения на телефоне (189а), на сервере обновлений. Кто прочитал образ, тот получил прошивку целиком — дизассемблируй и копируй. В таблице 48.9 шифрование образа занимает одну строку. Этот раздел разбирает его целиком: какой режим AES, где взять одноразовое число, откуда ключ и где он лежит в приборе.

Раздел: что даёт каждая мера и чего не даёт; режим AES-128-CTR с меткой HMAC-SHA-256 по шифротексту; свой AES на C, сверенный с эталонами; ключ изделия из мастер-ключа и номера кристалла; где хранить ключ на F405 и F103; скрипт выпуска на Python и приём в загрузчике кадрами; опыты с подделками, повтором одноразового числа и откатом; цена в байтах и командах.

Опоры: FIPS-197 (AES; векторы приложения C), RFC 3686 (AES-CTR; три вектора — те, что в самопроверке mbedTLS 3.6.4, github.com/Mbed-TLS/mbedtls), RFC 2104 (HMAC), RM0090 Rev 19 (3.7.3 — уровни RDP, таблица 17 — доступ по уровням; 3.7.4 и таблица 15 — WRP; 3.7.5 — PCROP; 3.8 и таблица 18 — OTP; 23 — блок CRYP, таблица 111; 25 — блок HASH), заголовки stm32f405xx.h, stm32f407xx.h, stm32f415xx.h, stm32f417xx.h из пакета STM32F4xx_DFP 2.17.1; уровни защиты F1 — раздел 11.6. Независимый расчёт — библиотека cryptography 49.0.0 для Python. Код прибора собран gcc 12.2.0 в stm-sys:1 и arm-none-eabi-gcc 12.2.1 в stm-arm:1, запущен в QEMU 7.2.22 на машине netduinoplus2 (это STM32F405). На плате не запускался. Лицензия: свой код раздела — свободно.

Как это называется в документации

Оригинал Перевод
encrypt-then-MAC сначала шифровать, потом ставить метку по шифротексту
keystream гамма: поток байт из шифра, который складывают с данными
malleability податливость: правка шифротекста предсказуемо правит открытый текст
nonce reuse повтор одноразового числа с тем же ключом
master key мастер-ключ: из него выводят ключи изделий
device-unique key ключ изделия: свой у каждого прибора
fleet key, batch key ключ партии: один на много приборов
known plaintext известный открытый текст
constant-time comparison сравнение без раннего выхода
one-time programmable area (OTP) однократно записываемая область
mass erase стирание всей флеш-памяти разом
readout protection bypass обход защиты от чтения

48г.1 Что защищает каждая мера и чего не даёт

Путь шифрованного образа: на компьютере сборка app_v2.bin 4300 байт, скрипт выпуска шифрует AES-128-CTR и ставит метку HMAC-SHA-256 по заголовку и шифротексту, выходит v2_a.enc 4364 байта; мастер-ключ только на сервере, ключ изделия — HMAC от мастер-ключа и номера кристалла; канал Bluetooth и UART, файл в телефоне и сервер видят только шифротекст; в приборе ключ изделия 32 байта в секторе 0 загрузчика под WRP и RDP даёт k_mac и k_enc, dec_feed кадр за кадром считает метку по шифротексту, расшифровывает и считает CRC-32 в буферный слот, dec_end проверяет метку, потом версию, и только после DEC_OK образ идёт в рабочий слот; код в загрузчике 2690 байт, приём около 406 команд на байт

Мера Что даёт Чего не даёт
CRC-32 образа (48.4) ловит случайную порчу подмену: CRC пересчитает любой
HMAC с общим ключом (198щ) подлинность и целостность: подделать без ключа нельзя тайну кода; ключ один на партию — вскрыли один прибор, ключ есть от всех
подпись (48а) подлинность; в приборе только открытый ключ, он не секрет тайну кода
шифрование тайну кода в канале, в файле и на сервере защиту от подмены — без метки

Шифрование без метки от подмены не защищает. В режиме CTR шифротекст — это открытый текст, сложенный побитно с гаммой. Переверните бит шифротекста — перевернётся ровно тот же бит открытого текста, и больше ничего. Опыт: в шифрованном образе перевёрнут бит 3 байта 1000 тела; загрузчик отказал по метке, а то, что он успел расшифровать в буферный слот, отличается от настоящей прошивки ровно одним битом:

  A <- v2_a, бит 3 байта 1000 тела перевёрнут: DEC_EAUTH, рабочий слот: версия 1, CRC-32 d66e75ac
    без проверки метки расшифровано бы: отличий от app_v2.bin 1 бит, в байте 1000 — тот же бит

Злоумышленник, знающий, где в образе лежит константа или команда перехода, правит её точно, не зная ключа. Поэтому порядок такой: сначала шифровать, потом ставить метку по шифротексту; в приборе — проверить метку и только потом пускать расшифрованное дальше. Есть и режимы с меткой внутри (GCM из 43.4); здесь выбран первый путь, почему — в 48г.2.

Шифрование и подпись не исключают друг друга. Подпись из 48а отвечает на вопрос «кто сделал», метка с ключом изделия — на тот же вопрос, но с секретом в приборе. Если прибор хранит секрет для расшифровки, метка с тем же секретом почти ничего не стоит; подпись поверх нужна, когда ключ расшифровки может утечь, а запускать чужое всё равно нельзя.


48г.2 Режим: AES-128-CTR и метка HMAC-SHA-256

Выбор — AES-128 в режиме CTR и HMAC-SHA-256 по заголовку и шифротексту. Почему не GCM, который 43.4 советует по умолчанию:

AES-128, а не AES-256: ключ 16 байт, 10 раундов вместо 14; на нашем коде это 321 команда на байт против 445 (48г.6). Формат от этого не зависит: ENC_KEYLEN 32 переключает код на AES-256.

Заголовок — 64 байта: 32 байта полей и метка.

#define ENC_MAGIC     0x31434E45u   /* байты 'E','N','C','1' */
#define ENC_HDR_SIZE  64u
#define ENC_KEYLEN    16u           /* AES-128; 32 — AES-256, формат тот же */
#define APP_ADDR      0x08020000u   /* рабочий слот F405 (секторы 5–6, как в 198щ) */
#define SLOT_SIZE     0x40000u      /* 256 КБ */

typedef struct {
    uint32_t magic;
    uint32_t version;       /* номер выпуска: растёт; защищён меткой */
    uint32_t size;          /* байт тела: шифротекст той же длины, что открытый образ */
    uint32_t kind;          /* 0 — образ целиком, 1 — разница 48в */
    uint8_t  nonce[12];     /* одноразовое число выпуска */
    uint32_t load_addr;     /* куда ляжет образ: APP_ADDR */
    uint8_t  tag[32];       /* HMAC-SHA-256(k_mac, байты 0…31 заголовка || шифротекст) */
} enc_hdr_t;
_Static_assert(sizeof(enc_hdr_t) == ENC_HDR_SIZE, "enc_hdr_t");
_Static_assert(offsetof(enc_hdr_t, tag) == 32u, "tag");

Одноразовое число — 12 случайных байт на каждый выпуск (os.urandom в скрипте выпуска), за ним 4 байта номера блока с нуля. Что будет, если число повторить с тем же ключом, — опыт на двух настоящих образах: v1 и v2 одной прошивки отличаются 76 байтами из 4300. Наивная схема — один ключ на все выпуски и одно и то же число:

  один ключ, один nonce, v1 и v2: байт 4300, XOR шифротекстов = 0 в 4224, образы совпадают в 4224
  зная v1, восстановлено байт v2: 4300 из 4300

XOR двух шифротекстов равен XOR двух открытых текстов: там, где прошивки совпадают, — нули, и видно, в каких 76 байтах выпуск отличается. Знает злоумышленник старую версию — например, она когда-то вышла без шифрования или прочитана из прибора без защиты — он получает новую целиком, все 4300 байт, без ключа.

Поэтому ключ шифрования здесь свой у каждого заголовка: k_enc выводится из ключа изделия и всех 32 байт полей заголовка — версии, длины, одноразового числа. Даже если число по ошибке повторилось, у другой версии другой ключ и другая гамма:

  ключ из заголовка (версия 1 и 2), тот же nonce: XOR = 0 в 13 из 4300

13 нулей из 4300 — столько и даёт случай: в среднем один байт из 256. Защита это запасная. Одноразовое число всё равно берут новое на каждый выпуск.


48г.3 Свой AES на C и сверка с эталонами

У F405 и F407 блока CRYP нет: в заголовках stm32f405xx.h и stm32f407xx.h нет CRYP_BASE, а в stm32f415xx.h и stm32f417xx.h он есть. RM0090 (глава 23) так и пишет: блок есть у F415/F417 и F43x, а GCM — только у F42x/F43x; у F42x самого CRYP нет, так что на деле GCM — у F43x. Блок HASH у F415/F417 считает SHA-1 и MD5, SHA-256 — только у F43x (глава 25). Значит, на F405 и тем более на F103 AES программный.

Ниже — компактный AES по FIPS-197: таблица подстановки 256 байт, ключи раундов считаются один раз и лежат в ОЗУ. Только шифрование блока: в CTR расшифровка — та же операция, обратный AES не нужен.

/* Умножение на x в поле GF(2^8) по модулю x^8 + x^4 + x^3 + x + 1. */
static uint8_t xtime(uint8_t x)
{
    return (uint8_t)((x << 1) ^ ((x >> 7) * 0x1Bu));
}
/* Ключи раундов (FIPS-197, 5.2). keylen — 16 или 32 байта. */
int aes_setkey(aes_t *a, const uint8_t *key, uint32_t keylen)
{
    uint32_t nk = keylen / 4u, total;
    uint8_t rcon = 1u, t[4], u;

    if (keylen != 16u && keylen != 32u)
        return -1;
    a->nr = nk + 6u;
    total = 16u * (a->nr + 1u);
    for (uint32_t i = 0; i < keylen; i++)
        a->rk[i] = key[i];
    for (uint32_t i = keylen; i < total; i += 4u) {
        for (int j = 0; j < 4; j++)
            t[j] = a->rk[i - 4u + (uint32_t)j];
        if ((i / 4u) % nk == 0u) {               /* RotWord, SubWord и константа раунда */
            u = t[0];
            t[0] = (uint8_t)(sbox[t[1]] ^ rcon);
            t[1] = sbox[t[2]];
            t[2] = sbox[t[3]];
            t[3] = sbox[u];
            rcon = xtime(rcon);
        } else if (nk == 8u && (i / 4u) % nk == 4u) {   /* только у AES-256 */
            for (int j = 0; j < 4; j++)
                t[j] = sbox[t[j]];
        }
        for (int j = 0; j < 4; j++)
            a->rk[i + (uint32_t)j] = (uint8_t)(a->rk[i - keylen + (uint32_t)j] ^ t[j]);
    }
    return 0;
}
/* Один блок 16 байт. Состояние — по столбцам: байт строки i столбца c лежит в s[4c + i]. */
void aes_encrypt(const aes_t *a, const uint8_t in[16], uint8_t out[16])
{
    uint8_t s[16], t[16], x, p0;
    const uint8_t *rk = a->rk;

    for (int i = 0; i < 16; i++)
        s[i] = (uint8_t)(in[i] ^ rk[i]);
    for (uint32_t r = 1; r <= a->nr; r++) {
        rk += 16;
        for (int c = 0; c < 4; c++)              /* SubBytes и ShiftRows: строка i сдвинута на i */
            for (int i = 0; i < 4; i++)
                t[4 * c + i] = sbox[s[4 * ((c + i) & 3) + i]];
        if (r < a->nr) {                         /* MixColumns — во всех раундах, кроме последнего */
            for (int c = 0; c < 4; c++) {
                uint8_t *p = t + 4 * c;
                x = (uint8_t)(p[0] ^ p[1] ^ p[2] ^ p[3]);
                p0 = p[0];
                p[0] ^= (uint8_t)(x ^ xtime((uint8_t)(p[0] ^ p[1])));
                p[1] ^= (uint8_t)(x ^ xtime((uint8_t)(p[1] ^ p[2])));
                p[2] ^= (uint8_t)(x ^ xtime((uint8_t)(p[2] ^ p[3])));
                p[3] ^= (uint8_t)(x ^ xtime((uint8_t)(p[3] ^ p0)));
            }
        }
        for (int i = 0; i < 16; i++)             /* AddRoundKey */
            s[i] = (uint8_t)(t[i] ^ rk[i]);
    }
    for (int i = 0; i < 16; i++)
        out[i] = s[i];
}

Режим CTR: блок счётчика шифруется, получается 16 байт гаммы, гамма складывается с данными; счётчик растёт на единицу во всех 128 битах.

/* Начальный блок счётчика; ключ уже в c->aes. */
void ctr_start(ctr_t *c, const uint8_t iv[16])
{
    for (int i = 0; i < 16; i++)
        c->ctr[i] = iv[i];
    c->used = 16u;
}
/* Шифрование и расшифровка — одно и то же: buf ^= гамма. Куски любой длины, по мере приёма. */
void ctr_xor(ctr_t *c, uint8_t *buf, uint32_t n)
{
    while (n--) {
        if (c->used == 16u) {
            aes_encrypt(&c->aes, c->ctr, c->ks);
            for (int i = 15; i >= 0; i--)        /* счётчик +1, все 128 бит, старший байт первым */
                if (++c->ctr[i] != 0u)
                    break;
            c->used = 0u;
        }
        *buf++ ^= c->ks[c->used++];
    }
}

Сверка. Векторы FIPS-197 (приложение C) и три вектора RFC 3686 — те же, что лежат в самопроверке mbedTLS; вектор CTR подаётся кусками 5 байт и остаток, чтобы проверить разрыв гаммы. SHA-256 и HMAC — со стандартными hashlib и hmac Python:

  FIPS-197 C.1, AES-128: совпал
  FIPS-197 C.3, AES-256: совпал
  RFC 3686, вектор 1 (16 байт): совпал
  RFC 3686, вектор 2 (32 байт): совпал
  RFC 3686, вектор 3 (36 байт): совпал
  SHA-256("abc") = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
  HMAC-SHA-256(0x0b x 20, "Hi There") = b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7

Потом — 3000 случайных сообщений длиной до 4096 байт: ключи AES-128 и AES-256 через одно, у каждого десятого счётчик начинается с ff ff ff ff в младших байтах, чтобы проверить перенос; шифрование и HMAC — кусками от 1 до 300 байт, ключ HMAC от 1 до 100 байт (длиннее 64 — сворачивается хешем). Код на C печатает свёртку SHA-256 всех шифротекстов и меток; Python тем же генератором чисел повторяет всё через cryptography и получает ту же свёртку:

  сообщений 3000 (AES-256 — 1500), байт 6173408, обратно расшифровано верно 3000
  свёртка SHA-256 всех шифротекстов и меток: b4b90709206bf6d502694048b521c6fcae4dbf43ac7c424608a344deef9cb323

SHA-256 и HMAC — sha48g.c: тот же счёт блока, что в 198щ, но HMAC потоковый (hmac_init, hmac_update, hmac_final) — метка считается кадр за кадром.


48г.4 Ключи: партия или изделие, и где ключ живёт в приборе

Ключ партии один на много приборов: сервер шифрует один файл на партию. Просто, но вскрыли один прибор — ключ есть от всех приборов партии: все их образы читаются, а с ключом метки ещё и подделываются.

Ключ изделия — свой у каждого прибора. Хранить тысячи ключей не нужно: сервер выводит ключ изделия из мастер-ключа и 96 бит номера кристалла (у F4 номер лежит с 0x1FFF7A10, 198щ.4). Вывод — HMAC-SHA-256 с меткой назначения (KDF):

/* Ключ из ключа: HMAC-SHA-256(key, label || 0x00 || ctx). Разные метки — независимые ключи. */
void kdf(const uint8_t *key, uint32_t keylen, const char *label, const uint8_t *ctx, uint32_t ctxlen,
         uint8_t out[32])
{
    hmac_t m;
    uint8_t zero = 0u;
    uint32_t n = 0u;
    while (label[n])
        n++;
    hmac_init(&m, key, keylen);
    hmac_update(&m, (const uint8_t *)label, n);
    hmac_update(&m, &zero, 1u);
    hmac_update(&m, ctx, ctxlen);
    hmac_final(&m, out);
}
def device_key(uid, master=MASTER):
    """Ключ изделия: из мастер-ключа и 96 бит номера кристалла. На заводе пишется в прибор."""
    return kdf(master, "48g-dev", uid)

На заводе ключ изделия считают и записывают в прибор; мастер-ключ в прибор не попадает никогда. Вскрыли прибор — получили ключ одного прибора. Цена — сервер шифрует образ под каждое изделие отдельно и должен знать номер кристалла каждого прибора. Компромисс — ключ на партию поменьше (сотня приборов): утечка задевает сотню, файлов на выпуск — по числу партий.

В приборе из ключа изделия выводятся два рабочих ключа: k_mac — один на все выпуски, k_enc — свой на каждый заголовок (48г.2). Учебные ключи раздела:

  ключ изделия A: d7fdaa9870130ffc56eee380daf4b1a48c6c1e3ab3c27385395b348c5eec21ef
  ключ изделия B: 19a40bb70c708c242453d17dc7c4ff32d037db65faa28059473aba78a60c234f
  ключ партии 7:  13c65ef0932d07bd943ad6b5fa3631a08d18b2049c53a58b84c4a61ca34ce1c7

Мастер-ключ в опытах — строка 48g-demo-key-not-for-devices прямо в скрипте. Так в изделие нельзя: настоящий мастер-ключ живёт только на сервере выпуска, в хранилище ключей, и не попадает ни в репозиторий, ни в прошивку.

Где ключ изделия лежит на F405 — по RM0090:

Где Что говорит RM0090 Годится?
в приложении при запуске из флеш-памяти код читает всю флеш-память (таблица 17) нет: приложение обновляется и само может утечь
сектор 0 загрузчика + RDP 1 отладчик и запуск из ОЗУ или системного загрузчика флеш-память не читают; снятие RDP стирает флеш-память и резервное ОЗУ (3.7.3) да: вместе с защитой уходит и ключ
сектор 0 + WRP запись и стирание сектора с битом nWRP = 0 запрещены, флаг WRPERR (3.7.4); у F405 — секторы 0–11 (таблица 15) да: ошибочная запись не сотрёт ни загрузчик, ни ключ; сам бит снимают байтами конфигурации
сектор 0 + RDP 2 необратимо: отладка отключена навсегда, байты конфигурации больше не меняются (3.7.3) лучше всего, но отладки нет и у вас
OTP: 16 блоков по 32 байта, с 0x1FFF7800 не стирается никогда, и полное стирание при снятии RDP её не трогает (3.7.3, 3.8) только с RDP 2

Последняя строка — ловушка. Ключ в OTP переживает стирание при снятии защиты: злоумышленник переводит RDP с 1 на 0, флеш-память стирается, а OTP цела, и на уровне 0 отладчик читает её спокойно. Ключ в секторе загрузчика при том же снятии стирается вместе с прошивкой. Значит, при RDP 1 ключ — во флеш-памяти загрузчика, а OTP годится только при RDP 2.

Ограничения, о которых надо знать:


48г.5 Выпуск на компьютере и приём в загрузчике

Скрипт выпуска — на Python. AES-CTR — библиотека cryptography, HMAC и KDF — стандартные hmac и hashlib. Сначала шифрование, потом метка:

def seal(kdev, body, version, nonce, kind=0):
    """Заголовок 64 байта (32 байта полей и метка) и шифротекст. Сначала шифровать, потом метка."""
    hdr = struct.pack("<IIII", MAGIC, version, len(body), kind) + nonce + struct.pack("<I", APP_ADDR)
    assert len(hdr) == 32 and len(nonce) == 12
    k_enc = kdf(kdev, "48g-enc", hdr)[:KEYLEN]
    ct = ctr(k_enc, nonce + b"\x00" * 4, body)
    tag = hmac.new(kdf(kdev, "48g-mac"), hdr + ct, hashlib.sha256).digest()
    return hdr + tag + ct
python mkenc48g.py seal app.bin out.enc --version 3 --uid 003400473235510b31383830

Одноразовое число при выпуске — os.urandom(12); в учебном наборе оно выводится из имени файла, чтобы файлы повторялись байт в байт и проверка могла их пересобрать.

Загрузчик. Три функции. dec_begin разбирает заголовок, проверяет только границы (магия, длина не больше слота, адрес) и готовит ключи. Версии он не верит: она ещё не проверена меткой.

/* Ключи из ключа прибора kdev: k_mac — один на все выпуски, k_enc — свой на каждый заголовок
 * (версия, длина, одноразовое число): повтор nonce в другом выпуске не повторит гамму. */
int dec_begin(dec_t *d, const uint8_t kdev[32], const uint8_t hdr[ENC_HDR_SIZE])
{
    uint8_t k[32], iv[16];

    d->h.magic = rd32(hdr + 0);
    d->h.version = rd32(hdr + 4);
    d->h.size = rd32(hdr + 8);
    d->h.kind = rd32(hdr + 12);
    for (int i = 0; i < 12; i++)
        d->h.nonce[i] = hdr[16 + i];
    d->h.load_addr = rd32(hdr + 28);
    for (int i = 0; i < 32; i++)
        d->h.tag[i] = hdr[32 + i];
    d->got = 0u;
    d->crc = 0u;
    if (d->h.magic != ENC_MAGIC)
        return DEC_EMAGIC;
    if (d->h.size == 0u || d->h.size > SLOT_SIZE || d->h.kind > 1u)
        return DEC_ESIZE;                        /* до проверки метки — только границы */
    if (d->h.load_addr != APP_ADDR)
        return DEC_EADDR;

    kdf(kdev, 32u, "48g-mac", 0, 0u, k);
    hmac_init(&d->mac, k, 32u);
    hmac_update(&d->mac, hdr, 32u);              /* заголовок без метки — под меткой */
    kdf(kdev, 32u, "48g-enc", hdr, 32u, k);
    aes_setkey(&d->ctr.aes, k, ENC_KEYLEN);
    for (int i = 0; i < 12; i++)
        iv[i] = d->h.nonce[i];
    iv[12] = iv[13] = iv[14] = iv[15] = 0u;
    ctr_start(&d->ctr, iv);
    wipe(k, sizeof k);
    return DEC_OK;
}

dec_feed — кадр любой длины: сначала метка по шифротексту, потом расшифровка на месте, CRC-32 открытого текста и запись в буферный слот.

/* Кусок тела любой длины: сначала в метку (шифротекст), потом расшифровка на месте и запись. */
int dec_feed(dec_t *d, uint8_t *buf, uint32_t n, slot_write_t wr)
{
    if (n > d->h.size - d->got)
        return DEC_ELONG;
    hmac_update(&d->mac, buf, n);
    ctr_xor(&d->ctr, buf, n);
    d->crc = crc32_update(d->crc, buf, n);
    if (wr(d->got, buf, n) != 0)
        return DEC_EWRITE;
    d->got += n;
    return DEC_OK;
}

dec_end — всё принято: метка сравнивается без раннего выхода (иначе по времени ответа подбирают метку байт за байтом), потом версия. Ключи стираются в любом случае.

/* Всё принято: метка сравнивается без раннего выхода, потом версия. Ключи стираются в любом случае. */
int dec_end(dec_t *d, uint32_t floor_version)
{
    uint8_t t[32], diff = 0u;
    int rc = DEC_OK;

    if (d->got != d->h.size) {
        rc = DEC_ESHORT;
    } else {
        hmac_final(&d->mac, t);
        for (int i = 0; i < 32; i++)
            diff |= (uint8_t)(t[i] ^ d->h.tag[i]);
        if (diff != 0u)
            rc = DEC_EAUTH;
        else if (d->h.version <= floor_version)
            rc = DEC_EOLD;                       /* число версии уже проверено меткой */
    }
    wipe(&d->mac, sizeof d->mac);
    wipe(&d->ctr, sizeof d->ctr);
    return rc;
}

Стирание — через volatile: обычный memset перед выходом компилятор вправе выбросить.

/* Стереть секрет так, чтобы компилятор не выбросил запись. */
static void wipe(void *p, uint32_t n)
{
    volatile uint8_t *v = (volatile uint8_t *)p;
    while (n--)
        *v++ = 0u;
}

Как это ложится на 198щ. Кадры приходят тем же протоколом (198щ.3): в on_start вместо длины — 64 байта заголовка и dec_begin; в on_data вместо записи слов — dec_feed; в on_end вместо проверки CRC и HMAC открытого образа — dec_end, и только при DEC_OK — запись READY в журнал с CRC-32 из dec_t.crc. Дальше всё по 198щ: копирование из буферного слота (секторы 7–8) в рабочий (секторы 5–6) по секторам, CRC после копии, обрыв питания. В буферном слоте лежит открытый образ — но буферный слот закрыт тем же RDP, а запускается только рабочий. С телефона (189а) файл .enc отправляется как есть: приложению ключ не нужен, а файл в нём читать бесполезно.

Разностное обновление (48в). Разница считается по открытым образам — на сервере, где они есть, — а шифруется сама разница тем же форматом (kind = 1). Разница между шифротекстами бесполезна: шифротекст выглядит случайным, и разница v1 → v2 по шифротекстам — 4334 байта, больше самого образа в 4300 байт. По открытым образам — 114 байт, шифрованный файл разницы — 178 (64 байта заголовка и 114). Загрузчик принимает его и расшифровывает побайтно верно:

  A <- d12_a (разница 48в, 114 байт): DEC_OK, CRC-32 b7f2d0d6, = v1v2.dlt: да

Наложение разницы — код 48в; вместе с расшифровкой оно здесь не прогонялось.


48г.6 Опыты: отказы, QEMU и цена

Приём и отказы. Прибор A с установленной v1 (версия 1). Образы зашифрованы на Python, загрузчик — тот же C-код — принимает их кадрами по 256 байт. Тот же выпуск, собранный своей функцией на C, совпал с файлом Python байт в байт.

  A <- v2_a (свой ключ, версия 2): DEC_OK, рабочий слот: версия 2, CRC-32 0ba4917e
  буферный слот после приёма = app_v2.bin: да, побайтно
  A <- v2_a ещё раз (повтор): DEC_EOLD, рабочий слот: версия 2, CRC-32 0ba4917e
  A <- v1_a (откат на версию 1): DEC_EOLD, рабочий слот: версия 2, CRC-32 0ba4917e

Повтор того же образа и откат на v1 — DEC_EOLD: версия прошла метку, но не новее установленной. Дальше каждая подделка — против прибора с v1:

  A <- v2_a, бит 3 байта 1000 тела перевёрнут: DEC_EAUTH, рабочий слот: версия 1, CRC-32 d66e75ac
  A <- v2_a, версия в заголовке 2 -> 3: DEC_EAUTH, рабочий слот: версия 1, CRC-32 d66e75ac
  A <- v2_a, испорчена магия: DEC_EMAGIC, рабочий слот: версия 1, CRC-32 d66e75ac
  B <- v2_a (образ для чужого изделия): DEC_EAUTH, рабочий слот: версия 1, CRC-32 d66e75ac
  A <- v2_b (зашифрован для B): DEC_EAUTH, рабочий слот: версия 1, CRC-32 d66e75ac
  A <- v2_batch (ключ партии, а у A ключ изделия): DEC_EAUTH, рабочий слот: версия 1, CRC-32 d66e75ac
  A <- v2_a без последних 100 байт: DEC_ESHORT, рабочий слот: версия 1, CRC-32 d66e75ac
  A <- v2_a и лишний байт: DEC_ELONG, рабочий слот: версия 1, CRC-32 d66e75ac
  прибор партии 7 <- v2_batch: DEC_OK, рабочий слот: версия 2, CRC-32 0ba4917e

Версия, поднятая в заголовке без ключа, ловится меткой: заголовок под ней. Образ чужого изделия и образ партии прибору A не подходят; прибор партии 7 тот же файл партии принимает. Во всех отказах рабочий слот остался на v1 — CRC-32 d66e75ac.

В QEMU. Тот же код собран под Cortex-M4 и Cortex-M3 и запущен на netduinoplus2: шифрованный образ лежит во флеш-памяти, приходит кадрами по 256 байт, расшифровывается в ОЗУ; CRC-32 открытого образа сходится с app_v2.bin, перевёрнутый бит даёт код 6 — DEC_EAUTH:

receive v2_a: rc=0 body=4300 crc=0ba4917e
receive v2_a, bit flipped in body byte 1000: rc=6

Размер. arm-none-eabi-gcc 12.2.1 с -Os, только код загрузчика:

Файл Cortex-M3 Cortex-M4 Что внутри
aes48g.o 922 922 AES и CTR; таблица подстановки 256 байт
sha48g.o 1140 1136 SHA-256, HMAC, KDF; константы 256 байт
dec48g.o 628 628 заголовок, приём, CRC-32 (таблица 64 байта)
всего 2690 2686

ОЗУ: dec_t — 520 байт (ключи раундов 240, состояние HMAC 168, остальное — заголовок и счётчики). Стек по -fstack-usage — не больше 832 байт по самому длинному пути: dec_begin 88 → kdf 200 → hmac_init 152 → sha_final 32 → sha_update 24 → sha_block 336.

Время — только оценка. QEMU считает не такты, а команды: с -icount время в нём идёт по числу выполненных команд, отсчёты SysTick калибруются циклом из 2 000 000 команд (так же мерили в 48а.4). На кристалле команда стоит такт и больше, у флеш-памяти есть такты ожидания — это нижняя граница.

Что Команд на байт
AES-128-CTR, 16 КБ 321
AES-256-CTR, 16 КБ 445
HMAC-SHA-256, 16 КБ 56
CRC-32 по полубайтам, 16 КБ 11
приём v2_a целиком: метка, CTR, CRC, запись в ОЗУ 406

Подготовка ключей в dec_begin (два KDF и ключи раундов) — около 45 тысяч команд. Образ на 256 КБ при 406 командах на байт — не меньше 0,63 с при 168 МГц и 1,48 с у F103 на 72 МГц. На фоне канала это мало: кадр 256 байт расшифровывается за 0,62 мс при 168 МГц, а идёт по UART 115200 (264 байта кадра) 22,9 мс. Аппаратный CRYP у F415/F417 тратит на блок AES-128 в CTR 14 тактов (RM0090, таблица 111) — меньше такта на байт против наших сотен команд; если скорость важна, берут камень с CRYP.


48г.7 Грабли

Зашифровали — значит, защитили. Без метки в режиме CTR злоумышленник правит образ побитно, не зная ключа (48г.1). Шифрование отвечает на вопрос «кто прочтёт», не «кто подменит».

Метка по открытому тексту. Тогда, чтобы её проверить, сначала надо расшифровать — и податливость шифротекста работает до проверки. Метка — по шифротексту и заголовку.

Одно и то же одноразовое число. Число из счётчика в ОЗУ, константа в скрипте, «номер версии» вместо случайного — и при повторе XOR двух выпусков показывает разницу, а знание одного раскрывает другой (48г.2).

Версия вне метки. Подняли версию в заголовке — и старый образ проходит защиту от отката. Метка покрывает все 32 байта полей заголовка.

Версию проверили до метки. Решать по ней можно только после dec_end: до метки это просто число из канала.

Сравнение метки через memcmp. Ранний выход по первому несовпавшему байту даёт по времени ответа подсказку, сколько байт угадано. Сравнивать все 32 байта всегда.

Ключ в OTP при RDP 1. OTP не стирается при снятии защиты, а на уровне 0 читается отладчиком (48г.4). При RDP 1 ключ — во флеш-памяти загрузчика.

Ключ в приложении. Приложение обновляется, отлаживается, бывает с ошибками — ключу там не место. Даже ключ в загрузчике приложение на F405 прочитать может: PCROP у этого камня нет.

Один ключ на все приборы. Вскрыли один — читаются и подделываются образы для всех. Ключ изделия из мастер-ключа и номера кристалла.

Разница по шифротекстам. Шифротексты двух версий не похожи — разница больше образа (4334 байта при образе 4300). Разница — по открытым образам, на сервере, и шифруется сама.

Ключи в ОЗУ после приёма. dec_t остаётся на стеке или в статической памяти и переживает переход в приложение. Стирать ключи через volatile, обычный memset компилятор выбросит.


48г.8 Итоги

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

aes48g.c — компактный AES, только шифрование блока.

/* aes48g.c — раздел 48г: компактный AES по FIPS-197, только шифрование блока: режиму CTR
 * обратное преобразование не нужно — расшифровка в нём та же операция, что и шифрование. */
#include "aes48g.h"

static const uint8_t sbox[256] = {
    0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
    0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
    0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
    0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
    0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
    0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
    0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
    0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
    0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
    0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
    0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
    0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
    0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
    0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
    0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
    0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16,
};

/* Умножение на x в поле GF(2^8) по модулю x^8 + x^4 + x^3 + x + 1. */
static uint8_t xtime(uint8_t x)
{
    return (uint8_t)((x << 1) ^ ((x >> 7) * 0x1Bu));
}

/* Ключи раундов (FIPS-197, 5.2). keylen — 16 или 32 байта. */
int aes_setkey(aes_t *a, const uint8_t *key, uint32_t keylen)
{
    uint32_t nk = keylen / 4u, total;
    uint8_t rcon = 1u, t[4], u;

    if (keylen != 16u && keylen != 32u)
        return -1;
    a->nr = nk + 6u;
    total = 16u * (a->nr + 1u);
    for (uint32_t i = 0; i < keylen; i++)
        a->rk[i] = key[i];
    for (uint32_t i = keylen; i < total; i += 4u) {
        for (int j = 0; j < 4; j++)
            t[j] = a->rk[i - 4u + (uint32_t)j];
        if ((i / 4u) % nk == 0u) {               /* RotWord, SubWord и константа раунда */
            u = t[0];
            t[0] = (uint8_t)(sbox[t[1]] ^ rcon);
            t[1] = sbox[t[2]];
            t[2] = sbox[t[3]];
            t[3] = sbox[u];
            rcon = xtime(rcon);
        } else if (nk == 8u && (i / 4u) % nk == 4u) {   /* только у AES-256 */
            for (int j = 0; j < 4; j++)
                t[j] = sbox[t[j]];
        }
        for (int j = 0; j < 4; j++)
            a->rk[i + (uint32_t)j] = (uint8_t)(a->rk[i - keylen + (uint32_t)j] ^ t[j]);
    }
    return 0;
}

/* Один блок 16 байт. Состояние — по столбцам: байт строки i столбца c лежит в s[4c + i]. */
void aes_encrypt(const aes_t *a, const uint8_t in[16], uint8_t out[16])
{
    uint8_t s[16], t[16], x, p0;
    const uint8_t *rk = a->rk;

    for (int i = 0; i < 16; i++)
        s[i] = (uint8_t)(in[i] ^ rk[i]);
    for (uint32_t r = 1; r <= a->nr; r++) {
        rk += 16;
        for (int c = 0; c < 4; c++)              /* SubBytes и ShiftRows: строка i сдвинута на i */
            for (int i = 0; i < 4; i++)
                t[4 * c + i] = sbox[s[4 * ((c + i) & 3) + i]];
        if (r < a->nr) {                         /* MixColumns — во всех раундах, кроме последнего */
            for (int c = 0; c < 4; c++) {
                uint8_t *p = t + 4 * c;
                x = (uint8_t)(p[0] ^ p[1] ^ p[2] ^ p[3]);
                p0 = p[0];
                p[0] ^= (uint8_t)(x ^ xtime((uint8_t)(p[0] ^ p[1])));
                p[1] ^= (uint8_t)(x ^ xtime((uint8_t)(p[1] ^ p[2])));
                p[2] ^= (uint8_t)(x ^ xtime((uint8_t)(p[2] ^ p[3])));
                p[3] ^= (uint8_t)(x ^ xtime((uint8_t)(p[3] ^ p0)));
            }
        }
        for (int i = 0; i < 16; i++)             /* AddRoundKey */
            s[i] = (uint8_t)(t[i] ^ rk[i]);
    }
    for (int i = 0; i < 16; i++)
        out[i] = s[i];
}

/* Начальный блок счётчика; ключ уже в c->aes. */
void ctr_start(ctr_t *c, const uint8_t iv[16])
{
    for (int i = 0; i < 16; i++)
        c->ctr[i] = iv[i];
    c->used = 16u;
}

/* Шифрование и расшифровка — одно и то же: buf ^= гамма. Куски любой длины, по мере приёма. */
void ctr_xor(ctr_t *c, uint8_t *buf, uint32_t n)
{
    while (n--) {
        if (c->used == 16u) {
            aes_encrypt(&c->aes, c->ctr, c->ks);
            for (int i = 15; i >= 0; i--)        /* счётчик +1, все 128 бит, старший байт первым */
                if (++c->ctr[i] != 0u)
                    break;
            c->used = 0u;
        }
        *buf++ ^= c->ks[c->used++];
    }
}

sha48g.h — интерфейс SHA-256, HMAC и KDF.

/* sha48g.h — раздел 48г: SHA-256, HMAC-SHA-256 потоком и ключ из ключа (KDF). */
#ifndef SHA48G_H
#define SHA48G_H
#include <stdint.h>

typedef struct {
    uint32_t h[8];
    uint8_t  blk[64];
    uint32_t nblk;          /* байт в blk */
    uint32_t total;         /* всего байт */
} sha_t;

typedef struct {
    sha_t   in;             /* внутренний хеш: (ключ ^ 0x36) || данные */
    uint8_t okey[64];       /* ключ ^ 0x5C — для внешнего хеша */
} hmac_t;

void sha_init(sha_t *s);
void sha_update(sha_t *s, const uint8_t *p, uint32_t n);
void sha_final(sha_t *s, uint8_t out[32]);
void hmac_init(hmac_t *m, const uint8_t *key, uint32_t keylen);
void hmac_update(hmac_t *m, const uint8_t *p, uint32_t n);
void hmac_final(hmac_t *m, uint8_t out[32]);
void kdf(const uint8_t *key, uint32_t keylen, const char *label, const uint8_t *ctx, uint32_t ctxlen,
         uint8_t out[32]);

#endif

sha48g.c — SHA-256, HMAC-SHA-256 потоком и KDF на HMAC.

/* sha48g.c — раздел 48г: SHA-256 (FIPS 180-4), HMAC-SHA-256 (RFC 2104) потоком и KDF на HMAC. */
#include "sha48g.h"

static const uint32_t K[64] = {
    0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u, 0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u,
    0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u, 0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u,
    0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu, 0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau,
    0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u, 0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u,
    0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u, 0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u,
    0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u, 0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u,
    0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u, 0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u,
    0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u, 0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u,
};

#define ROR(x, n) (((x) >> (n)) | ((x) << (32 - (n))))

static void sha_block(sha_t *s, const uint8_t *p)
{
    uint32_t w[64], a, b, c, d, e, f, g, h, t1, t2;
    for (int i = 0; i < 16; i++)
        w[i] = (uint32_t)p[4 * i] << 24 | (uint32_t)p[4 * i + 1] << 16 | (uint32_t)p[4 * i + 2] << 8 | p[4 * i + 3];
    for (int i = 16; i < 64; i++)
        w[i] = (ROR(w[i - 2], 17) ^ ROR(w[i - 2], 19) ^ (w[i - 2] >> 10)) + w[i - 7]
             + (ROR(w[i - 15], 7) ^ ROR(w[i - 15], 18) ^ (w[i - 15] >> 3)) + w[i - 16];
    a = s->h[0]; b = s->h[1]; c = s->h[2]; d = s->h[3];
    e = s->h[4]; f = s->h[5]; g = s->h[6]; h = s->h[7];
    for (int i = 0; i < 64; i++) {
        t1 = h + (ROR(e, 6) ^ ROR(e, 11) ^ ROR(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i];
        t2 = (ROR(a, 2) ^ ROR(a, 13) ^ ROR(a, 22)) + ((a & b) ^ (a & c) ^ (b & c));
        h = g; g = f; f = e; e = d + t1; d = c; c = b; b = a; a = t1 + t2;
    }
    s->h[0] += a; s->h[1] += b; s->h[2] += c; s->h[3] += d;
    s->h[4] += e; s->h[5] += f; s->h[6] += g; s->h[7] += h;
}

void sha_init(sha_t *s)
{
    static const uint32_t h0[8] = { 0x6a09e667u, 0xbb67ae85u, 0x3c6ef372u, 0xa54ff53au,
                                    0x510e527fu, 0x9b05688cu, 0x1f83d9abu, 0x5be0cd19u };
    for (int i = 0; i < 8; i++)
        s->h[i] = h0[i];
    s->nblk = 0u;
    s->total = 0u;
}

void sha_update(sha_t *s, const uint8_t *p, uint32_t n)
{
    s->total += n;
    while (n) {
        if (s->nblk == 0u && n >= 64u) {         /* целый блок — прямо из буфера, без копии */
            sha_block(s, p);
            p += 64;
            n -= 64u;
            continue;
        }
        s->blk[s->nblk++] = *p++;
        n--;
        if (s->nblk == 64u) {
            sha_block(s, s->blk);
            s->nblk = 0u;
        }
    }
}

void sha_final(sha_t *s, uint8_t out[32])
{
    uint64_t bits = (uint64_t)s->total * 8u;
    uint8_t pad = 0x80u;
    sha_update(s, &pad, 1u);
    pad = 0u;
    while (s->nblk != 56u)
        sha_update(s, &pad, 1u);
    for (int i = 7; i >= 0; i--) {
        uint8_t b = (uint8_t)(bits >> (8 * i));
        sha_update(s, &b, 1u);
    }
    for (int i = 0; i < 32; i++)
        out[i] = (uint8_t)(s->h[i / 4] >> (24 - 8 * (i % 4)));
}

/* Ключ длиннее блока (64 байта) сначала сворачивается в SHA-256 — так требует RFC 2104. */
void hmac_init(hmac_t *m, const uint8_t *key, uint32_t keylen)
{
    uint8_t k[64], ipad[64];
    for (int i = 0; i < 64; i++)
        k[i] = 0u;
    if (keylen > 64u) {
        sha_init(&m->in);
        sha_update(&m->in, key, keylen);
        sha_final(&m->in, k);
    } else {
        for (uint32_t i = 0; i < keylen; i++)
            k[i] = key[i];
    }
    for (int i = 0; i < 64; i++) {
        ipad[i] = (uint8_t)(k[i] ^ 0x36u);
        m->okey[i] = (uint8_t)(k[i] ^ 0x5Cu);
    }
    sha_init(&m->in);
    sha_update(&m->in, ipad, 64u);
}

void hmac_update(hmac_t *m, const uint8_t *p, uint32_t n)
{
    sha_update(&m->in, p, n);
}

void hmac_final(hmac_t *m, uint8_t out[32])
{
    uint8_t inner[32];
    sha_final(&m->in, inner);
    sha_init(&m->in);
    sha_update(&m->in, m->okey, 64u);
    sha_update(&m->in, inner, 32u);
    sha_final(&m->in, out);
}

/* Ключ из ключа: HMAC-SHA-256(key, label || 0x00 || ctx). Разные метки — независимые ключи. */
void kdf(const uint8_t *key, uint32_t keylen, const char *label, const uint8_t *ctx, uint32_t ctxlen,
         uint8_t out[32])
{
    hmac_t m;
    uint8_t zero = 0u;
    uint32_t n = 0u;
    while (label[n])
        n++;
    hmac_init(&m, key, keylen);
    hmac_update(&m, (const uint8_t *)label, n);
    hmac_update(&m, &zero, 1u);
    hmac_update(&m, ctx, ctxlen);
    hmac_final(&m, out);
}

dec48g.h — заголовок шифрованного образа и интерфейс приёма.

/* dec48g.h — раздел 48г: заголовок шифрованного образа и приём в загрузчике. */
#ifndef DEC48G_H
#define DEC48G_H
#include <stddef.h>
#include <stdint.h>
#include "aes48g.h"
#include "sha48g.h"

#define ENC_MAGIC     0x31434E45u   /* байты 'E','N','C','1' */
#define ENC_HDR_SIZE  64u
#define ENC_KEYLEN    16u           /* AES-128; 32 — AES-256, формат тот же */
#define APP_ADDR      0x08020000u   /* рабочий слот F405 (секторы 5–6, как в 198щ) */
#define SLOT_SIZE     0x40000u      /* 256 КБ */

typedef struct {
    uint32_t magic;
    uint32_t version;       /* номер выпуска: растёт; защищён меткой */
    uint32_t size;          /* байт тела: шифротекст той же длины, что открытый образ */
    uint32_t kind;          /* 0 — образ целиком, 1 — разница 48в */
    uint8_t  nonce[12];     /* одноразовое число выпуска */
    uint32_t load_addr;     /* куда ляжет образ: APP_ADDR */
    uint8_t  tag[32];       /* HMAC-SHA-256(k_mac, байты 0…31 заголовка || шифротекст) */
} enc_hdr_t;
_Static_assert(sizeof(enc_hdr_t) == ENC_HDR_SIZE, "enc_hdr_t");
_Static_assert(offsetof(enc_hdr_t, tag) == 32u, "tag");

enum { DEC_OK = 0, DEC_EMAGIC, DEC_ESIZE, DEC_EADDR, DEC_ELONG, DEC_ESHORT, DEC_EAUTH, DEC_EOLD, DEC_EWRITE };

typedef int (*slot_write_t)(uint32_t off, const uint8_t *p, uint32_t n);

typedef struct {
    enc_hdr_t h;
    hmac_t    mac;          /* метка считается по шифротексту — по мере приёма */
    ctr_t     ctr;          /* расшифровка — тоже по мере приёма */
    uint32_t  got;          /* принято байт тела */
    uint32_t  crc;          /* CRC-32 открытого образа: для журнала и сверки копии (198щ) */
} dec_t;

int dec_begin(dec_t *d, const uint8_t kdev[32], const uint8_t hdr[ENC_HDR_SIZE]);
int dec_feed(dec_t *d, uint8_t *buf, uint32_t n, slot_write_t wr);
int dec_end(dec_t *d, uint32_t floor_version);
uint32_t crc32_update(uint32_t crc, const uint8_t *p, uint32_t n);

#endif

dec48g.c — приём шифрованного образа в загрузчике.

/* dec48g.c — раздел 48г: приём шифрованного образа в загрузчике. Метка — по шифротексту,
 * расшифровка — по мере приёма в буферный слот; в рабочий слот образ попадает только после dec_end. */
#include "dec48g.h"

static const uint32_t crc_nib[16] = {
    0x00000000u, 0x1DB71064u, 0x3B6E20C8u, 0x26D930ACu, 0x76DC4190u, 0x6B6B51F4u, 0x4DB26158u, 0x5005713Cu,
    0xEDB88320u, 0xF00F9344u, 0xD6D6A3E8u, 0xCB61B38Cu, 0x9B64C2B0u, 0x86D3D2D4u, 0xA00AE278u, 0xBDBDF21Cu,
};

/* CRC-32 как у zlib, по полубайтам: таблица 64 байта. */
uint32_t crc32_update(uint32_t crc, const uint8_t *p, uint32_t n)
{
    crc = ~crc;
    while (n--) {
        crc ^= *p++;
        crc = (crc >> 4) ^ crc_nib[crc & 15u];
        crc = (crc >> 4) ^ crc_nib[crc & 15u];
    }
    return ~crc;
}

static uint32_t rd32(const uint8_t *p)
{
    return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24;
}

/* Стереть секрет так, чтобы компилятор не выбросил запись. */
static void wipe(void *p, uint32_t n)
{
    volatile uint8_t *v = (volatile uint8_t *)p;
    while (n--)
        *v++ = 0u;
}

/* Ключи из ключа прибора kdev: k_mac — один на все выпуски, k_enc — свой на каждый заголовок
 * (версия, длина, одноразовое число): повтор nonce в другом выпуске не повторит гамму. */
int dec_begin(dec_t *d, const uint8_t kdev[32], const uint8_t hdr[ENC_HDR_SIZE])
{
    uint8_t k[32], iv[16];

    d->h.magic = rd32(hdr + 0);
    d->h.version = rd32(hdr + 4);
    d->h.size = rd32(hdr + 8);
    d->h.kind = rd32(hdr + 12);
    for (int i = 0; i < 12; i++)
        d->h.nonce[i] = hdr[16 + i];
    d->h.load_addr = rd32(hdr + 28);
    for (int i = 0; i < 32; i++)
        d->h.tag[i] = hdr[32 + i];
    d->got = 0u;
    d->crc = 0u;
    if (d->h.magic != ENC_MAGIC)
        return DEC_EMAGIC;
    if (d->h.size == 0u || d->h.size > SLOT_SIZE || d->h.kind > 1u)
        return DEC_ESIZE;                        /* до проверки метки — только границы */
    if (d->h.load_addr != APP_ADDR)
        return DEC_EADDR;

    kdf(kdev, 32u, "48g-mac", 0, 0u, k);
    hmac_init(&d->mac, k, 32u);
    hmac_update(&d->mac, hdr, 32u);              /* заголовок без метки — под меткой */
    kdf(kdev, 32u, "48g-enc", hdr, 32u, k);
    aes_setkey(&d->ctr.aes, k, ENC_KEYLEN);
    for (int i = 0; i < 12; i++)
        iv[i] = d->h.nonce[i];
    iv[12] = iv[13] = iv[14] = iv[15] = 0u;
    ctr_start(&d->ctr, iv);
    wipe(k, sizeof k);
    return DEC_OK;
}

/* Кусок тела любой длины: сначала в метку (шифротекст), потом расшифровка на месте и запись. */
int dec_feed(dec_t *d, uint8_t *buf, uint32_t n, slot_write_t wr)
{
    if (n > d->h.size - d->got)
        return DEC_ELONG;
    hmac_update(&d->mac, buf, n);
    ctr_xor(&d->ctr, buf, n);
    d->crc = crc32_update(d->crc, buf, n);
    if (wr(d->got, buf, n) != 0)
        return DEC_EWRITE;
    d->got += n;
    return DEC_OK;
}

/* Всё принято: метка сравнивается без раннего выхода, потом версия. Ключи стираются в любом случае. */
int dec_end(dec_t *d, uint32_t floor_version)
{
    uint8_t t[32], diff = 0u;
    int rc = DEC_OK;

    if (d->got != d->h.size) {
        rc = DEC_ESHORT;
    } else {
        hmac_final(&d->mac, t);
        for (int i = 0; i < 32; i++)
            diff |= (uint8_t)(t[i] ^ d->h.tag[i]);
        if (diff != 0u)
            rc = DEC_EAUTH;
        else if (d->h.version <= floor_version)
            rc = DEC_EOLD;                       /* число версии уже проверено меткой */
    }
    wipe(&d->mac, sizeof d->mac);
    wipe(&d->ctr, sizeof d->ctr);
    return rc;
}

mkenc48g.py — выпуск шифрованного образа на компьютере.

# -*- coding: utf-8 -*-
"""Раздел 48г: выпуск шифрованного образа на компьютере (сервере обновлений).

python mkenc48g.py seal app.bin out.enc --version 3 --uid 003400473235510b31383830
python mkenc48g.py seal app.bin out.enc --version 3 --batch 7
python mkenc48g.py tests DIR        — учебный набор раздела (одноразовые числа — из имени, только для опытов)
python mkenc48g.py cdata IN.enc OUT.c --uid HEX   — образ и ключ прибора массивами C (для опыта в QEMU)

AES-CTR — библиотека cryptography; HMAC и KDF — стандартные hmac и hashlib. Мастер-ключ здесь учебный
и лежит в открытом виде: так в изделие нельзя — он живёт только на сервере выпуска, в хранилище ключей."""
import hashlib
import hmac
import os
import struct
import sys
import zlib

MASTER = b"48g-demo-key-not-for-devices"      # учебный мастер-ключ: в настоящем выпуске его здесь нет
MAGIC = 0x31434E45                            # 'E','N','C','1'
APP_ADDR = 0x08020000
KEYLEN = 16                                   # AES-128
UID_A = bytes.fromhex("003400473235510b31383830")
UID_B = bytes.fromhex("003400473235510b31383831")


def kdf(key, label, ctx=b""):
    """HMAC-SHA-256(key, label || 0x00 || ctx) — как kdf() в sha48g.c."""
    return hmac.new(key, label.encode() + b"\x00" + ctx, hashlib.sha256).digest()


def device_key(uid, master=MASTER):
    """Ключ изделия: из мастер-ключа и 96 бит номера кристалла. На заводе пишется в прибор."""
    return kdf(master, "48g-dev", uid)


def batch_key(batch, master=MASTER):
    """Ключ партии: один на все приборы партии."""
    return kdf(master, "48g-batch", struct.pack("<I", batch))


def ctr(key, iv, data):
    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
    e = Cipher(algorithms.AES(key), modes.CTR(iv)).encryptor()
    return e.update(data) + e.finalize()


def seal(kdev, body, version, nonce, kind=0):
    """Заголовок 64 байта (32 байта полей и метка) и шифротекст. Сначала шифровать, потом метка."""
    hdr = struct.pack("<IIII", MAGIC, version, len(body), kind) + nonce + struct.pack("<I", APP_ADDR)
    assert len(hdr) == 32 and len(nonce) == 12
    k_enc = kdf(kdev, "48g-enc", hdr)[:KEYLEN]
    ct = ctr(k_enc, nonce + b"\x00" * 4, body)
    tag = hmac.new(kdf(kdev, "48g-mac"), hdr + ct, hashlib.sha256).digest()
    return hdr + tag + ct


def open_sealed(kdev, img, floor):
    """Тот же разбор, что dec48g.c, — для сверки: (код, открытый образ)."""
    magic, version, size, kind = struct.unpack_from("<IIII", img, 0)
    if magic != MAGIC:
        return 1, b""
    hdr, tag, ct = img[:32], img[32:64], img[64:]
    if len(ct) != size:
        return 5 if len(ct) < size else 4, b""
    if not hmac.compare_digest(hmac.new(kdf(kdev, "48g-mac"), hdr + ct, hashlib.sha256).digest(), tag):
        return 6, b""
    if version <= floor:
        return 7, b""
    return 0, ctr(kdf(kdev, "48g-enc", hdr)[:KEYLEN], hdr[16:28] + b"\x00" * 4, ct)


def test_nonce(name):
    """Только для опытов: одноразовое число из имени, чтобы файлы повторялись байт в байт."""
    return hashlib.sha256(b"48g-test-nonce:" + name.encode()).digest()[:12]


TESTS = {  # имя: (открытый файл, версия, чей ключ, kind)
    "v1_a": ("app_v1.bin", 1, "A", 0),
    "v2_a": ("app_v2.bin", 2, "A", 0),
    "v2_b": ("app_v2.bin", 2, "B", 0),
    "v2_batch": ("app_v2.bin", 2, "batch", 0),
    "d12_a": ("v1v2.dlt", 2, "A", 1),
}


def key_of(who):
    return {"A": device_key(UID_A), "B": device_key(UID_B), "batch": batch_key(7)}[who]


def make_tests(d):
    out = {}
    for name, (src, ver, who, kind) in TESTS.items():
        body = open(os.path.join(d, src), "rb").read()
        out[name] = seal(key_of(who), body, ver, test_nonce(name), kind)
    return out


def cdata(enc, uid):
    """Образ и ключ изделия массивами C — опыт в QEMU (ключ «записан на заводе»)."""
    kd = device_key(uid)
    rows = lambda b: ",\n".join("    " + ", ".join("0x%02x" % x for x in b[i:i + 16]) for i in range(0, len(b), 16))
    return ("/* создан mkenc48g.py cdata: образ %d байт и ключ изделия (учебный) */\n#include <stdint.h>\n"
            "const uint32_t enc_len = %du;\nconst uint8_t enc_img[%d] = {\n%s\n};\n"
            "const uint8_t kdev[32] = {\n%s\n};\n") % (len(enc), len(enc), len(enc), rows(enc), rows(kd))


if __name__ == "__main__":
    a = sys.argv[1:]
    if a[0] == "seal":
        body = open(a[1], "rb").read()
        ver = int(a[a.index("--version") + 1])
        kd = device_key(bytes.fromhex(a[a.index("--uid") + 1])) if "--uid" in a else batch_key(int(a[a.index("--batch") + 1]))
        img = seal(kd, body, ver, os.urandom(12))     # одноразовое число — новое на каждый выпуск
        open(a[2], "wb").write(img)
        print("%s: версия %d, тело %d байт, CRC-32 открытого %08x" % (a[2], ver, len(body), zlib.crc32(body)))
    elif a[0] == "tests":
        for name, img in make_tests(a[1]).items():
            open(os.path.join(a[1], name + ".enc"), "wb").write(img)
            print("%s.enc: %d байт" % (name, len(img)))
    elif a[0] == "cdata":
        open(a[2], "w").write(cdata(open(a[1], "rb").read(), bytes.fromhex(a[a.index("--uid") + 1])))

Дальше: раздел 48д «Заводской загрузчик ST в системной памяти: вход по BOOT0 и из прошивки, протокол USART, своя утилита прошивки» — загрузчик, который уже есть в каждом кристалле: как в него попасть и как говорить с ним своей утилитой.

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