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 Что защищает каждая мера и чего не даёт
| Мера | Что даёт | Чего не даёт |
|---|---|---|
| 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 советует по умолчанию:
- SHA-256 и HMAC в загрузчике уже есть (198щ). Для GCM нужен ещё GHASH — умножение в поле из 2^128 элементов: новый код и новые таблицы.
- Повтор одноразового числа в GCM ломает и метку (43.4): по двум сообщениям подделывают метку без ключа. HMAC от одноразового числа не зависит — повтор раскроет текст, но не даст подделать образ.
- Метку можно проверить, не расшифровывая: ей нужен только ключ
k_mac. Это позволяет держать в буферном слоте шифротекст, если хочется. - CTR — поток: шифр работает только в прямую сторону, дополнять до 16 байт не надо, расшифровка идёт кусками любой длины по мере приёма — буфер на весь образ не нужен.
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.
Ограничения, о которых надо знать:
- код, запущенный из флеш-памяти, читает всё (таблица 17), в том числе ключ в секторе загрузчика. Ошибка в приложении, позволяющая читать память по запросу извне, выдаёт и ключ. PCROP — защита сектора от чтения данными — есть только у F42x/F43x (3.7.5), у F405 её нет;
- у F103 второго уровня RDP нет вовсе (11.6): ключ закрыт только первым;
- первый уровень защиты от чтения у ряда семейств STM32 исследователи обходили на кристалле. Поэтому ключ изделия, а не партии: обход на одном приборе стоит ключа одного прибора.
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 Итоги
- CRC — от порчи, HMAC и подпись — от подмены, шифрование — от чтения в канале и в файле. Шифрование без метки подмену не ловит: перевёрнутый бит шифротекста CTR переворачивает ровно один бит прошивки.
- Режим — AES-128-CTR, метка HMAC-SHA-256 по 32 байтам полей заголовка и шифротексту; заголовок 64 байта; метка проверяется до записи в рабочий слот.
- Одноразовое число — 12 случайных байт на выпуск. При повторе с одним ключом v2 восстанавливается из v1 целиком, 4300 байт из 4300; с ключом из заголовка у повтора 13 случайных нулей из 4300.
- Свой AES на C сошёлся с FIPS-197, тремя векторами RFC 3686 и с библиотекой cryptography на 3000 случайных сообщениях; у F405 и F407 блока CRYP нет.
- Ключ изделия — HMAC от мастер-ключа и номера кристалла; мастер-ключ только на сервере. На F405 ключ — в секторе 0 загрузчика под WRP и RDP; OTP на 512 байт переживает снятие RDP, поэтому годится только при RDP 2.
- Отказы: бит тела, поднятая версия, чужое изделие, ключ партии, обрезанный и удлинённый образ, повтор и откат — рабочий слот остаётся на v1.
- Разница 48в считается по открытым образам и шифруется: 114 байт против 4334 по шифротекстам.
- Цена: 2690 байт кода на Cortex-M3, 520 байт ОЗУ, около 406 команд на байт приёма — для 256 КБ не меньше 0,63 с при 168 МГц.
Полные исходники
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, своя утилита прошивки» — загрузчик, который уже есть в каждом кристалле: как в него попасть и как говорить с ним своей утилитой.