43б. Защищённый элемент ATECC608: ключ, который не читается из прошивки
В 43.4 сказано прямо: ключ в массиве const uint8_t key[16] читается из
прошивки за минуту. В 11в показано, что и защита чтения на F1 этого не
спасает. В 48а закрытый ключ живёт на сервере сборки, а прибор хранит только
открытый — так можно, пока прибор лишь проверяет. Но прибору часто нужно
подписывать самому: доказать серверу, что он настоящий, или поднять TLS
со своим сертификатом. Тогда закрытый ключ должен быть в приборе — и не
должен читаться никак.
Ответ — отдельная микросхема рядом с микроконтроллером: защищённый элемент. Ключ создаётся у неё внутри и наружу не выходит; прошивка передаёт хеш и получает подпись. Одна из самых распространённых таких микросхем — ATECC608 на шине I²C. Здесь написан свой драйвер её протокола (пакет, CRC, пробуждение, семь команд), модель микросхемы на Python для прогона драйвера на компьютере, проверка подписи посторонней программой и поиск ключа в двух образах прошивки F103.
Опоры: протокол — по публичному описанию производителя (Microchip,
репозиторий github.com/MicrochipTech/cryptoauthlib — только как описание:
лицензия библиотеки разрешает её только с микросхемами Microchip, поэтому
код здесь свой). Даташит ATECC608 с разделом не сверялся: числа, взятые из
описания, помечены. Независимо сверены: ответ на пробуждение 04 11 33 43,
пауза 1500 мкс и слово-адрес команды 0x03 — по открытому стеку LoRaWAN
LoRaMac-node (github.com/zephyrproject-rtos/loramac-node, коммит fb00b38;
файл для ATECC608A — под лицензией Revised BSD), адрес 0x60 — по примеру
сканера шины github.com/openmv/openmv (коммит 6236993, лицензия MIT). I²C на F103 —
RM0008 (DocID13902), подтяжки — DS5319 (DocID13587).
Как это называется в документации
Оригинал Перевод secure element защищённый элемент slot, key slot ячейка (ключа) configuration zone, data zone зона конфигурации, зона данных LockConfig, LockValue байты блокировки конфигурации и данных word address слово-адрес — первый байт записи wake, idle, sleep пробуждение, простой, сон watchdog сторож TempKey временный регистр внутри микросхемы nonce, pass-through mode одноразовое число; режим «как есть» GenKey, Sign создать ключ, подписать status byte, execution error байт состояния, ошибка выполнения digest хеш
43б.1 Где ключ и что видит нарушитель
| Где закрытый ключ | Что достаёт дамп флеша | Цена в прошивке F103 |
|---|---|---|
массив const + mbedTLS |
ключ целиком, одним поиском | образ 16 560 байт |
| защищённая область с RDP (43.4, 11в) | на F1 — ключ после обхода уровня 1 | как в первой строке |
| защищённый элемент | драйвер, ключа нет | образ 1 996 байт |
Защищённый элемент меняет саму задачу. Закрытый ключ создаёт команда
GenKey внутри микросхемы, отдаёт она только открытый ключ X‖Y (64 байта).
Подпись тоже делает микросхема: прошивка кладёт хеш во внутренний регистр
TempKey командой Nonce и просит Sign. По шине идут хеш, подпись и
открытый ключ — всё это и так не секрет.
Что это не даёт. Микросхему можно унести и подписывать ею, не зная ключа: защищённый элемент отвечает на вопрос «где ключ», а не «кто командует». Против этого — связка с микроконтроллером (общий секрет для команд, защищённая загрузка), это за рамками раздела.
Как проверено без платы. Тот же se43b.c, что уходит в прошивку,
собран gcc на компьютере. Нижний уровень I²C там — четыре строки обмена с
моделью model43b.py: запись, чтение, «SDA в ноль на столько-то
микросекунд», пауза. Модель отвечает так, как описывает производитель:
NACK, пока спит или занята, ответ с CRC, отказ при неверной CRC. Закрытый
ключ живёт только в модели. Подпись модели проверяет openssl — посторонняя
программа. Модель — не микросхема: времена команд в ней — оценки, сторож и
правила блокировок — по описанию; с микросхемой ничего не сверено.
43б.2 Пакет команды и CRC-16
Запись в микросхему начинается со слова-адреса: 0x03 — команда,
0x02 — простой, 0x01 — сон, 0x00 — сброс указателя ответа. За ним
пакет:
| Байт | Что | Пример: Info |
|---|---|---|
| 0 | count — длина пакета без слова-адреса, считая себя и CRC | 07 |
| 1 | код команды | 30 |
| 2 | p1 — режим | 00 |
| 3–4 | p2, младший байт первым | 00 00 |
| 5… | данные (у Nonce — 32 байта) |
— |
| последние 2 | CRC-16, младший байт первым | 03 5d |
Ответ устроен так же, без слова-адреса и кода: [count][данные][CRC].
Ответ из четырёх байт — это байт состояния: 00 — успех, 0F — ошибка
выполнения, FF — микросхема получила пакет с неверной CRC, 11 — только
что проснулась.
CRC — 16 бит, полином 0x8005, начальное значение 0. Тонкость в порядке
бит: биты каждого байта подаются младшим вперёд, а регистр сдвигается
влево, без разворота результата. Привычные варианты с тем же полиномом не
подходят: с разворотом входа и результата строка 123456789 даёт
bb3d, без разворотов — fee8, а у ATECC — bcdd (то
же, что первый, но с неразвёрнутым результатом).
/* CRC-16, полином 0x8005, начальное 0. Биты байта идут младшим вперёд,
* регистр сдвигается влево; в пакет — младший байт первым. */
void se_crc16(const uint8_t *data, size_t n, uint8_t crc[2])
{
uint16_t r = 0;
for (size_t i = 0; i < n; i++) {
for (uint8_t m = 0x01u; m != 0u; m = (uint8_t)(m << 1)) {
unsigned in = (data[i] & m) ? 1u : 0u;
unsigned top = r >> 15;
r = (uint16_t)(r << 1);
if (in != top)
r ^= 0x8005u;
}
}
crc[0] = (uint8_t)(r & 0xFFu);
crc[1] = (uint8_t)(r >> 8);
}
Сборка и разбор:
/* pkt: [слово-адрес 0x03][count][код][p1][p2 мл.][p2 ст.][данные][CRC мл.][CRC ст.].
* count считает себя, код, p1, p2, данные и CRC — всё, кроме слова-адреса. */
size_t se_build(uint8_t *pkt, uint8_t op, uint8_t p1, uint16_t p2, const uint8_t *data, size_t dlen)
{
uint8_t count = (uint8_t)(7u + dlen);
pkt[0] = SE_WA_COMMAND;
pkt[1] = count;
pkt[2] = op;
pkt[3] = p1;
pkt[4] = (uint8_t)(p2 & 0xFFu);
pkt[5] = (uint8_t)(p2 >> 8);
if (dlen)
memcpy(&pkt[6], data, dlen);
se_crc16(&pkt[1], (size_t)count - 2u, &pkt[1 + count - 2]);
return 1u + count;
}
/* Ответ: [count][полезное][CRC мл.][CRC ст.]. count = 4 — один байт состояния. */
int se_parse(const uint8_t *rsp, size_t n, const uint8_t **payload, size_t *plen)
{
uint8_t crc[2];
if (n < 4u || rsp[0] != n)
return SE_ERR_LEN;
se_crc16(rsp, n - 2u, crc);
if (crc[0] != rsp[n - 2u] || crc[1] != rsp[n - 1u])
return SE_ERR_CRC;
*payload = &rsp[1];
*plen = n - 3u;
return SE_OK;
}
Сверка. У se_crc16 и у модели одна запись алгоритма; поэтому
ref43b.py считает по-другому — разворачивает каждый байт и гонит обычную
табличную CRC «старшим вперёд». Совпали все пакеты:
| Что | Байты | Откуда сверено |
|---|---|---|
| ответ на пробуждение | 04 11 → CRC 33 43 |
LoRaMac-node: 04 11 33 43 |
строка 123456789 |
CRC dd bc |
ref43b.py — проверочное число для своей реализации |
Info |
03 07 30 00 00 00 03 5d |
драйвер = ref43b.py |
Random |
03 07 1b 00 00 00 24 cd |
драйвер = ref43b.py |
GenKey, ячейка 0 |
03 07 40 04 00 00 83 87 |
драйвер = ref43b.py |
Sign, внешний хеш |
03 07 41 80 00 00 28 05 |
драйвер = ref43b.py |
Nonce с хешем |
40 байт с адресом, CRC e6 ae |
драйвер = ref43b.py |
43б.3 Пробуждение, сон и ожидание ответа
В покое микросхема спит и на свой адрес не отвечает. Разбудить её — держать
SDA в нуле не меньше 60 мкс (по описанию производителя, с даташитом не
сверено), отпустить и выждать 1500 мкс. После этого первое чтение даёт
04 11 33 43: длина 4, состояние 11 «проснулась», CRC.
int se_wake_us(uint32_t low_us)
{
uint8_t r[4];
const uint8_t *p;
size_t pl;
se_hal_sda_low_us(low_us);
se_hal_delay_us(SE_WAKE_HIGH_US);
if (se_hal_read(SE_ADDR7, r, sizeof r) != 0)
return SE_ERR_NACK;
int e = se_parse(r, sizeof r, &p, &pl);
if (e != SE_OK)
return e;
return (p[0] == 0x11u) ? SE_OK : SE_ERR_WAKE;
}
Прогон на модели: импульс 20 мкс — микросхема спит дальше, чтение
получает NACK, se_wake_us(20) = -1 (SE_ERR_NACK); импульс
80 мкс — ответ 04 11 33 43, результат 0.
Сколько ждать ответа на команду, драйвер не знает и не гадает: пока микросхема считает, она не подтверждает свой адрес. Драйвер каждые 1000 мкс пробует прочитать первый байт; NACK — ещё занята. Предел — 300 попыток. Потом дочитывает остаток: длина известна из первого байта.
/* Команда целиком: запись пакета, опрос, пока микросхема занята (NACK), чтение длины,
* затем остатка, проверка CRC. want — сколько байт ждём; 1 — только байт состояния.
* Возврат: 0 — данные в out; >0 — состояние микросхемы; <0 — ошибка драйвера. */
int se_cmd(uint8_t op, uint8_t p1, uint16_t p2, const uint8_t *data, size_t dlen,
uint8_t *out, size_t want)
{
uint8_t pkt[8 + 32];
uint8_t rsp[3 + 64];
const uint8_t *p;
size_t pl;
if (dlen > 32u || want > 64u || want == 0u)
return SE_ERR_LEN;
size_t n = se_build(pkt, op, p1, p2, data, dlen);
if (se_hal_write(SE_ADDR7, pkt, n) != 0)
return SE_ERR_NACK;
for (se_last_polls = 1u;; se_last_polls++) {
se_hal_delay_us(SE_POLL_US);
if (se_hal_read(SE_ADDR7, rsp, 1u) == 0)
break;
if (se_last_polls >= SE_POLL_MAX)
return SE_ERR_TIMEOUT;
}
if (rsp[0] < 4u || rsp[0] > sizeof rsp)
return SE_ERR_LEN;
if (se_hal_read(SE_ADDR7, &rsp[1], (size_t)rsp[0] - 1u) != 0)
return SE_ERR_NACK;
int e = se_parse(rsp, rsp[0], &p, &pl);
if (e != SE_OK)
return e;
if (pl == 1u && want != 1u)
return p[0] ? p[0] : SE_ERR_LEN; /* вместо данных пришло состояние */
if (pl != want)
return SE_ERR_LEN;
if (want == 1u)
return p[0]; /* 0 — успех */
memcpy(out, p, want);
return SE_OK;
}
Простой и сон — одна запись слова-адреса:
void se_idle(void)
{
uint8_t wa = SE_WA_IDLE;
(void)se_hal_write(SE_ADDR7, &wa, 1u);
}
void se_sleep(void)
{
uint8_t wa = SE_WA_SLEEP;
(void)se_hal_write(SE_ADDR7, &wa, 1u);
}
Разница важна: в простое содержимое TempKey сохраняется, во сне —
пропадает. А сторож усыпляет микросхему сам, через фиксированное время
после пробуждения (по описанию — около 1,3 с; в модели 1 300 000 мкс),
что бы ни происходило на шине. Опыт: Nonce, пауза 1,4 с, Sign — запись
команды получила NACK, результат -1. Хеш и подпись должны
уложиться в одно пробуждение.
43б.4 Версия, конфигурация, случайные числа и блокировка
int se_info(uint8_t rev[4])
{
return se_cmd(SE_OP_INFO, 0x00u, 0x0000u, NULL, 0u, rev, 4u);
}
int se_random(uint8_t out[32])
{
return se_cmd(SE_OP_RANDOM, 0x00u, 0x0000u, NULL, 0u, out, 32u);
}
/* Зона конфигурации — 128 байт, четыре блока по 32. p1: бит 7 — читать 32 байта, зона 0. */
int se_read_config(uint8_t cfg[128])
{
for (uint8_t blk = 0; blk < 4u; blk++) {
int e = se_cmd(SE_OP_READ, 0x80u, (uint16_t)(blk << 3), NULL, 0u, &cfg[32u * blk], 32u);
if (e != SE_OK)
return e;
}
return SE_OK;
}
/* mode: биты 1:0 — зона (0 конфигурация, 1 данные), бит 7 — не сверять CRC зоны.
* Блокировка НЕОБРАТИМА: команды «разблокировать» у микросхемы нет. */
int se_lock(uint8_t mode, uint16_t zone_crc)
{
return se_cmd(SE_OP_LOCK, mode, zone_crc, NULL, 0u, NULL, 1u);
}
Зона конфигурации — 128 байт: номер микросхемы, версия, адрес I²C, права каждой ячейки, два байта блокировки. Прогон на модели:
| Что | Модель ответила |
|---|---|
Info, режим 0 |
00 00 60 03 |
| байт 16 — адрес I²C | 0xC0 = 0x60 × 2, то есть 8-битная запись |
| байты 86 и 87 — LockValue и LockConfig | 0x55, 0x55 — обе зоны открыты |
Random до блокировки конфигурации |
ffff0000ffff0000… |
GenKey до блокировки |
состояние 0x0F — отказ |
| CRC прочитанной зоны (128 байт) | f7 7f |
Lock с CRC, отличной на один бит |
0x0F — отказ, ничего не заблокировано |
Lock с верной CRC |
0; байт 87 стал 0x00 |
второй Lock той же зоны |
0x0F — блокировка одна и навсегда |
Random после блокировки, два раза |
9609dff202404b6b…, 3aba95cfa6cd9ca8… |
До блокировки конфигурации Random возвращает не случайные числа, а
узор ffff0000, повторённый восемь раз (по описанию производителя; модель
делает так же). Это защита от ошибки «настроил кое-как и забыл
заблокировать», но взять такой «шум» в генератор из 43а — катастрофа.
Lock с режимом 0 сверяет CRC всей зоны с той, что передана в p2. Смысл:
блокируется ровно та конфигурация, которую вы задумали, а не та, что
оказалась в микросхеме после сбоя записи. Поэтому на производстве CRC
считают по эталонной конфигурации из своего файла, а не по прочитанной,
как в опыте выше. Режим с битом 7 (без сверки) проще и опаснее.
43б.5 Ключ внутри, подпись снаружи
/* Режим 0x04 — создать новый закрытый ключ в ячейке; наружу — только открытый X‖Y. */
int se_genkey(uint8_t slot, uint8_t pub[64])
{
return se_cmd(SE_OP_GENKEY, 0x04u, slot, NULL, 0u, pub, 64u);
}
/* Подпись готового хеша: Nonce в режиме «как есть» кладёт 32 байта во внутренний
* регистр TempKey, Sign с режимом 0x80 (внешнее сообщение) подписывает его ключом ячейки. */
int se_sign_digest(uint8_t slot, const uint8_t digest[32], uint8_t sig[64])
{
int e = se_cmd(SE_OP_NONCE, 0x03u, 0x0000u, digest, 32u, NULL, 1u);
if (e != SE_OK)
return e;
return se_cmd(SE_OP_SIGN, 0x80u, slot, NULL, 0u, sig, 64u);
}
В модели GenKey работает только после блокировки конфигурации, а Sign —
после блокировки данных (так в модели; точные правила у микросхемы зависят
от настройки ячеек и с даташитом не сверены). Прогон: Sign при открытой
зоне данных — 0x0F; Lock данных — 0; затем подпись.
Сообщение — строка «firmware 43b: подпиши меня», 37 байт в UTF-8,
SHA-256 05c9470b6f24ba7c…. Микросхема отдаёт открытый ключ X‖Y и подпись r‖s —
по 64 байта, старшим байтом вперёд. openssl ждёт другие обёртки: ключ — в
структуре SubjectPublicKeyInfo с идентификатором кривой, подпись — в DER.
Перевод в der43b.py:
def der_int(b):
b = b.lstrip(b"\x00") or b"\x00"
if b[0] & 0x80:
b = b"\x00" + b # старший бит 1 — иначе число станет отрицательным
return bytes([0x02, len(b)]) + b
def der_sig(rs):
body = der_int(rs[:32]) + der_int(rs[32:])
return bytes([0x30, len(body)]) + body
Ключ в DER — 91 байт (26 байт заголовка, 04 и X‖Y). Подпись —
71 байт у первой и 70 у второй: если старший бит r или s
единица, к числу спереди добавляется ноль, иначе DER прочтёт его как
отрицательное. Длина подписи в DER — от прогона к прогону разная, это норма.
Проверка openssl 3.0.20:
| Проверка | Подпись 1 | Подпись 2 |
|---|---|---|
сообщение целиком, openssl dgst -sha256 -verify |
«Verified OK» | «Verified OK» |
| в сообщении 43b → 43c | «Verification failure» | «Verification failure» |
хеш, openssl pkeyutl -verify |
«Signature Verified Successfully» | «Signature Verified Successfully» |
| в хеше изменён первый байт | «Signature Verification Failure» | «Signature Verification Failure» |
Две подписи одного хеша разные: одноразовое число k для каждой берётся из генератора микросхемы. Обе верны.
А теперь попытка прочитать ключ: Read ячейки 0 — состояние
0x0F (ошибка выполнения). Модель вела журнал всего, что прошло по
шине в обе стороны: 89 транзакций, 1 042 байта. Поиск 32 байт
закрытого ключа в этом журнале — 0 совпадений, поиск его
половин по 16 байт — тоже 0.
Помехи. Перевёрнутый бит в отправленной команде — микросхема ответила
состоянием FF, драйвер вернул 255 (0xFF). Перевёрнутый бит в принятом
ответе Random — драйвер вернул -3 (SE_ERR_CRC) и не отдал
испорченные байты.
43б.6 Прошивка F103 и поиск ключа в образе
Нижний уровень на F103 — I2C1 на PB6/PB7, 100 кГц от HSI 8 МГц
(CCR = 8 000 000 / (2 × 100 000) = 40, TRISE = 9, как в 17.3).
Пробуждение — PB7 на время переводится в обычный выход с открытым стоком:
/* Пробуждение: PB7 на время в обычный выход с открытым стоком и в ноль, потом обратно в I2C */
void se_hal_sda_low_us(uint32_t us)
{
GPIOB_BSRR = 1u << (16 + 7);
GPIOB_CRL = (GPIOB_CRL & ~(0xFu << 28)) | (0x6u << 28); /* CNF = 01, MODE = 10 */
se_hal_delay_us(us);
GPIOB_BSRR = 1u << 7; /* отпустить: линию поднимет подтяжка */
GPIOB_CRL = (GPIOB_CRL & ~(0xFu << 28)) | (0xEu << 28);
}
Приём N байт на F1 — по RM0008, «Method 2»: один байт, два байта и больше двух — три разных порядка снятия ACK. Драйвер читает сначала один байт длины, потом остаток — 3, 6, 34 или 66 байт; случай N = 2 ему не встречается, но оставлен, чтобы функция годилась для любой длины:
int se_hal_read(uint8_t addr7, uint8_t *buf, size_t n)
{
if (n == 0u)
return 1;
I2C1_CR1 |= I2C_CR1_ACK;
if (n == 2u)
I2C1_CR1 |= I2C_CR1_POS;
if (start_addr((uint8_t)((addr7 << 1) | 1u)))
return 1;
if (n == 1u) { /* ACK снять до сброса ADDR */
I2C1_CR1 &= ~I2C_CR1_ACK;
(void)I2C1_SR1;
(void)I2C1_SR2;
I2C1_CR1 |= I2C_CR1_STOP;
if (wait_sr1(I2C_SR1_RXNE))
return 1;
buf[0] = (uint8_t)I2C1_DR;
return 0;
}
(void)I2C1_SR1;
(void)I2C1_SR2;
if (n == 2u) { /* POS: ACK управляет следующим байтом */
I2C1_CR1 &= ~I2C_CR1_ACK;
if (wait_sr1(I2C_SR1_BTF))
return stop_fail();
I2C1_CR1 |= I2C_CR1_STOP;
buf[0] = (uint8_t)I2C1_DR;
buf[1] = (uint8_t)I2C1_DR;
I2C1_CR1 &= ~I2C_CR1_POS;
return 0;
}
for (size_t i = 0; i < n - 3u; i++) {
if (wait_sr1(I2C_SR1_RXNE))
return stop_fail();
buf[i] = (uint8_t)I2C1_DR;
}
if (wait_sr1(I2C_SR1_BTF)) /* N−2 в DR, N−1 в сдвиговом регистре, SCL прижат */
return stop_fail();
I2C1_CR1 &= ~I2C_CR1_ACK;
buf[n - 3u] = (uint8_t)I2C1_DR;
if (wait_sr1(I2C_SR1_BTF)) /* последний байт принят с NACK */
return stop_fail();
I2C1_CR1 |= I2C_CR1_STOP;
buf[n - 2u] = (uint8_t)I2C1_DR;
if (wait_sr1(I2C_SR1_RXNE))
return 1;
buf[n - 1u] = (uint8_t)I2C1_DR;
return 0;
}
Две прошивки делают одно и то же — подписывают один хеш (05c9470b6f24ba7c…):
FW_CONST— ключ в массивеconst uint8_t dev_key[32], подпись mbedTLS 3.6.4 (детерминированная ECDSA, конфигурацияcfg43b.h);FW_SE— ключ в ячейке 0 защищённого элемента, в прошивкеse43b.cиi2c_f103.c.
arm-none-eabi-gcc 12.2.1, Cortex-M3, -Os, сборка мусора компоновщиком:
| Код (text) | Образ .bin | |
|---|---|---|
se43b.o |
734 | — |
i2c_f103.o |
784 | — |
прошивка FW_SE |
1 996 | 1 996 |
прошивка FW_CONST (mbedTLS) |
16 452 | 16 560 |
Самые крупные функции драйвера: se_hal_read — 328 байт, se_cmd —
192, i2c1_init — 124, se_crc16 — 66.
Теперь — нарушитель с дампом. find43b.py ищет 32 байта ключа в образе,
как это сделал бы любой поиск байтов:
| Образ | Ключ из dev_key |
Ключ модели (ячейка 0) |
|---|---|---|
FW_CONST |
найден 1 раз | 0 совпадений |
FW_SE |
0 совпадений | 0 совпадений |
Адрес совпадения — 0x080038d4, это символ dev_key из карты
компоновки. Поиску не нужно знать, где ключ: 32 байта ищутся по всему
образу. В варианте с защищённым элементом искать нечего.
43б.7 Грабли
Блокировка необратима. Lock конфигурации и данных — одна попытка на
всю жизнь микросхемы, команды «разблокировать» нет. В модели второй Lock
той же зоны получил 0F. Ошибка в правах ячеек после блокировки не
исправляется ничем, кроме замены микросхемы. Порядок: эталонная
конфигурация в файле → запись → чтение и сравнение → Lock с CRC эталона.
Отлаживать — на запасных микросхемах.
Random до блокировки — не случайный. Узор ffff0000, а не ошибка: код
ответа успешный. Кто сеет генератор из 43а этими байтами до блокировки
конфигурации, получает одинаковые ключи на всех приборах.
Версия микросхемы. Info в режиме 0 отдаёт 4 байта версии; в модели —
00 00 60 03. Значения у живых ревизий раздел не сверял. Версию стоит читать при
первом запуске, писать в журнал и сравнивать с той, на которой проверяли
прошивку.
Время команд — не константа. В модели GenKey занял 98
шагов опроса по 1 мс, Sign — 43, Random — 20,
Info — 1. Это оценки модели, а не микросхемы. Жёсткая пауза
«на всякий случай» либо мала, либо тратит время; опрос по NACK с пределом
работает при любом времени.
Сторож. Около 1,3 с после пробуждения микросхема засыпает сама (по
описанию; не сверено) и теряет TempKey. Остановка отладчиком между Nonce и
Sign дольше секунды ломает подпись — в модели это -1.
Слабая подтяжка. У F103 в режиме альтернативной функции внутренние подтяжки отключены (RM0008, 9.1.9) — без внешних резисторов шины нет. Где внутреннюю подтяжку включить можно, она слабая: у F103 30–50 кОм (DS5319). По формуле фронта из 17.3 40 кОм на 50 пФ дают 1 695 нс — больше предела 1000 нс даже для 100 кГц; 4,7 кОм — 199 нс.
Пробуждение «записью по адресу 0x00». Распространённый приём: послать
адрес 0x00 — SDA лежит в нуле восемь тактов SCL. На 100 кГц это
80 мкс — хватает; на 400 кГц — 20 мкс, меньше 60, и
микросхема не просыпается. Либо будить на 100 кГц, либо выводом SDA
напрямую, как в se_hal_sda_low_us.
Адрес «не тот». Один и тот же адрес пишут как 0x60 (7 бит) и 0xC0
(8 бит, так он лежит в байте 16 конфигурации). HAL, ждущий 8-битный
адрес, получит от 0x60 вызов устройства 0x30. И адрес бывает другим:
у заранее настроенных вариантов он свой — в LoRaMac-node для
ATECC608A-TNGLORA записан 0x59. Перед кодом — сканер шины.
CRC от соседней микросхемы. У ATAES132A той же фирмы CRC другой
вариант: драйвер Zephyr (Apache-2.0) для 04 11 даёт 98 65 вместо
33 43. И даже открытые константы бывают неверны: в LoRaMac-node ответ
«сбой самопроверки» записан как 04 07 C4 40, а CRC от 04 07 равна
40 c2. Ответ проверяют вычислением CRC, а не сравнением с
константой.
Лицензия. cryptoauthlib разрешено использовать только с продуктами Microchip. Код раздела написан заново по описанию протокола; из библиотеки не скопировано ничего.
43б.8 Итоги
- Защищённый элемент держит закрытый ключ у себя:
GenKeyотдаёт только открытый X‖Y (64 байта),Sign— подпись r‖s (64 байта),Readячейки ключа — отказ0x0F. В журнале шины (1 042 байта) ключ не встретился ни разу. - Пакет: слово-адрес
03, count, код, p1, p2, данные, CRC-16 с полиномом0x8005(биты байта младшим вперёд, младший байт CRC первым). Сверка:04 11→33 43совпало с открытым кодом; пакеты драйвера совпали с независимым пересчётом. - Пробуждение: SDA в нуле не меньше 60 мкс (80 в драйвере), затем
1500 мкс; ответ
04 11 33 43. 20 мкс — не проснулась. - Ожидание — опросом по NACK, без жёстких пауз; сторож модели — 1,3 с,
NonceиSign— в одно пробуждение. - Блокировка необратима;
Lockс CRC эталона защищает от блокировки испорченной конфигурации; до блокировкиRandomотдаётffff0000…. - Подписи модели проверил openssl: обе «Verified OK», изменённый байт хеша или сообщения — отказ.
- F103: прошивка с защищённым элементом — 1 996 байт, с ключом в
constи mbedTLS — 16 560; поиск по образу находит ключ в 0x080038d4 и не находит ничего во втором образе.
Полные исходники
Каталог _verify/secel43b/. На компьютере:
gcc -std=c11 -O2 host43b.c se43b.c -o host43b, затем
python3 model43b.py ./host43b <SHA-256 сообщения>. Прошивки — как в
run43b.sh (шаг build, mbedTLS 3.6.4).
se43b.h — интерфейс драйвера: адрес, коды команд, ошибки, нижний уровень.
/* se43b.h — раздел 43б: свой драйвер протокола защищённого элемента ATECC608 (I2C).
* Один и тот же se43b.c собирается в прошивку (i2c_f103.c) и на компьютере (host43b.c + модель). */
#ifndef SE43B_H
#define SE43B_H
#include <stddef.h>
#include <stdint.h>
#define SE_ADDR7 0x60u /* 7-битный адрес; в 8-битной записи 0xC0 */
#define SE_WAKE_LOW_US 80u /* SDA в нуле: по описанию производителя не меньше 60 мкс */
#define SE_WAKE_HIGH_US 1500u /* пауза после пробуждения до первого обмена */
#define SE_POLL_US 1000u /* шаг опроса, пока микросхема занята командой */
#define SE_POLL_MAX 300u /* 300 шагов = 0,3 с, дольше не ждём */
/* Слово-адрес — первый байт любой записи */
#define SE_WA_RESET 0x00u
#define SE_WA_SLEEP 0x01u
#define SE_WA_IDLE 0x02u
#define SE_WA_COMMAND 0x03u
/* Коды команд */
#define SE_OP_READ 0x02u
#define SE_OP_NONCE 0x16u
#define SE_OP_LOCK 0x17u
#define SE_OP_RANDOM 0x1Bu
#define SE_OP_INFO 0x30u
#define SE_OP_GENKEY 0x40u
#define SE_OP_SIGN 0x41u
/* Ошибки драйвера (отрицательные); положительное — байт состояния микросхемы */
#define SE_OK 0
#define SE_ERR_NACK (-1) /* адрес не подтверждён: спит, занята или нет на шине */
#define SE_ERR_TIMEOUT (-2) /* не ответила за SE_POLL_MAX шагов */
#define SE_ERR_CRC (-3) /* ответ пришёл с неверной CRC */
#define SE_ERR_LEN (-4) /* длина ответа не та, что ждали */
#define SE_ERR_WAKE (-5) /* после пробуждения не 0x11 */
/* Нижний уровень: реализуют i2c_f103.c (прошивка) или host43b.c (компьютер). 0 — ACK, иначе NACK. */
int se_hal_write(uint8_t addr7, const uint8_t *buf, size_t n);
int se_hal_read(uint8_t addr7, uint8_t *buf, size_t n);
void se_hal_sda_low_us(uint32_t us);
void se_hal_delay_us(uint32_t us);
void se_crc16(const uint8_t *data, size_t n, uint8_t crc[2]);
size_t se_build(uint8_t *pkt, uint8_t op, uint8_t p1, uint16_t p2, const uint8_t *data, size_t dlen);
int se_parse(const uint8_t *rsp, size_t n, const uint8_t **payload, size_t *plen);
int se_wake_us(uint32_t low_us);
int se_wake(void);
void se_idle(void);
void se_sleep(void);
int se_cmd(uint8_t op, uint8_t p1, uint16_t p2, const uint8_t *data, size_t dlen,
uint8_t *out, size_t want);
int se_info(uint8_t rev[4]);
int se_random(uint8_t out[32]);
int se_read_config(uint8_t cfg[128]);
int se_lock(uint8_t mode, uint16_t zone_crc);
int se_genkey(uint8_t slot, uint8_t pub[64]);
int se_sign_digest(uint8_t slot, const uint8_t digest[32], uint8_t sig[64]);
extern uint32_t se_last_polls; /* сколько шагов опроса ушло на последнюю команду */
#endif
se43b.c — драйвер протокола: CRC, пакет, разбор, пробуждение, опрос, команды.
/* se43b.c — раздел 43б: протокол ATECC608 поверх I2C. Без библиотеки производителя:
* пакет команды, CRC-16, разбор ответа, пробуждение, опрос занятости, семь команд. */
#include <string.h>
#include "se43b.h"
uint32_t se_last_polls;
/* CRC-16, полином 0x8005, начальное 0. Биты байта идут младшим вперёд,
* регистр сдвигается влево; в пакет — младший байт первым. */
void se_crc16(const uint8_t *data, size_t n, uint8_t crc[2])
{
uint16_t r = 0;
for (size_t i = 0; i < n; i++) {
for (uint8_t m = 0x01u; m != 0u; m = (uint8_t)(m << 1)) {
unsigned in = (data[i] & m) ? 1u : 0u;
unsigned top = r >> 15;
r = (uint16_t)(r << 1);
if (in != top)
r ^= 0x8005u;
}
}
crc[0] = (uint8_t)(r & 0xFFu);
crc[1] = (uint8_t)(r >> 8);
}
/* pkt: [слово-адрес 0x03][count][код][p1][p2 мл.][p2 ст.][данные][CRC мл.][CRC ст.].
* count считает себя, код, p1, p2, данные и CRC — всё, кроме слова-адреса. */
size_t se_build(uint8_t *pkt, uint8_t op, uint8_t p1, uint16_t p2, const uint8_t *data, size_t dlen)
{
uint8_t count = (uint8_t)(7u + dlen);
pkt[0] = SE_WA_COMMAND;
pkt[1] = count;
pkt[2] = op;
pkt[3] = p1;
pkt[4] = (uint8_t)(p2 & 0xFFu);
pkt[5] = (uint8_t)(p2 >> 8);
if (dlen)
memcpy(&pkt[6], data, dlen);
se_crc16(&pkt[1], (size_t)count - 2u, &pkt[1 + count - 2]);
return 1u + count;
}
/* Ответ: [count][полезное][CRC мл.][CRC ст.]. count = 4 — один байт состояния. */
int se_parse(const uint8_t *rsp, size_t n, const uint8_t **payload, size_t *plen)
{
uint8_t crc[2];
if (n < 4u || rsp[0] != n)
return SE_ERR_LEN;
se_crc16(rsp, n - 2u, crc);
if (crc[0] != rsp[n - 2u] || crc[1] != rsp[n - 1u])
return SE_ERR_CRC;
*payload = &rsp[1];
*plen = n - 3u;
return SE_OK;
}
int se_wake_us(uint32_t low_us)
{
uint8_t r[4];
const uint8_t *p;
size_t pl;
se_hal_sda_low_us(low_us);
se_hal_delay_us(SE_WAKE_HIGH_US);
if (se_hal_read(SE_ADDR7, r, sizeof r) != 0)
return SE_ERR_NACK;
int e = se_parse(r, sizeof r, &p, &pl);
if (e != SE_OK)
return e;
return (p[0] == 0x11u) ? SE_OK : SE_ERR_WAKE;
}
int se_wake(void)
{
return se_wake_us(SE_WAKE_LOW_US);
}
void se_idle(void)
{
uint8_t wa = SE_WA_IDLE;
(void)se_hal_write(SE_ADDR7, &wa, 1u);
}
void se_sleep(void)
{
uint8_t wa = SE_WA_SLEEP;
(void)se_hal_write(SE_ADDR7, &wa, 1u);
}
/* Команда целиком: запись пакета, опрос, пока микросхема занята (NACK), чтение длины,
* затем остатка, проверка CRC. want — сколько байт ждём; 1 — только байт состояния.
* Возврат: 0 — данные в out; >0 — состояние микросхемы; <0 — ошибка драйвера. */
int se_cmd(uint8_t op, uint8_t p1, uint16_t p2, const uint8_t *data, size_t dlen,
uint8_t *out, size_t want)
{
uint8_t pkt[8 + 32];
uint8_t rsp[3 + 64];
const uint8_t *p;
size_t pl;
if (dlen > 32u || want > 64u || want == 0u)
return SE_ERR_LEN;
size_t n = se_build(pkt, op, p1, p2, data, dlen);
if (se_hal_write(SE_ADDR7, pkt, n) != 0)
return SE_ERR_NACK;
for (se_last_polls = 1u;; se_last_polls++) {
se_hal_delay_us(SE_POLL_US);
if (se_hal_read(SE_ADDR7, rsp, 1u) == 0)
break;
if (se_last_polls >= SE_POLL_MAX)
return SE_ERR_TIMEOUT;
}
if (rsp[0] < 4u || rsp[0] > sizeof rsp)
return SE_ERR_LEN;
if (se_hal_read(SE_ADDR7, &rsp[1], (size_t)rsp[0] - 1u) != 0)
return SE_ERR_NACK;
int e = se_parse(rsp, rsp[0], &p, &pl);
if (e != SE_OK)
return e;
if (pl == 1u && want != 1u)
return p[0] ? p[0] : SE_ERR_LEN; /* вместо данных пришло состояние */
if (pl != want)
return SE_ERR_LEN;
if (want == 1u)
return p[0]; /* 0 — успех */
memcpy(out, p, want);
return SE_OK;
}
int se_info(uint8_t rev[4])
{
return se_cmd(SE_OP_INFO, 0x00u, 0x0000u, NULL, 0u, rev, 4u);
}
int se_random(uint8_t out[32])
{
return se_cmd(SE_OP_RANDOM, 0x00u, 0x0000u, NULL, 0u, out, 32u);
}
/* Зона конфигурации — 128 байт, четыре блока по 32. p1: бит 7 — читать 32 байта, зона 0. */
int se_read_config(uint8_t cfg[128])
{
for (uint8_t blk = 0; blk < 4u; blk++) {
int e = se_cmd(SE_OP_READ, 0x80u, (uint16_t)(blk << 3), NULL, 0u, &cfg[32u * blk], 32u);
if (e != SE_OK)
return e;
}
return SE_OK;
}
/* mode: биты 1:0 — зона (0 конфигурация, 1 данные), бит 7 — не сверять CRC зоны.
* Блокировка НЕОБРАТИМА: команды «разблокировать» у микросхемы нет. */
int se_lock(uint8_t mode, uint16_t zone_crc)
{
return se_cmd(SE_OP_LOCK, mode, zone_crc, NULL, 0u, NULL, 1u);
}
/* Режим 0x04 — создать новый закрытый ключ в ячейке; наружу — только открытый X‖Y. */
int se_genkey(uint8_t slot, uint8_t pub[64])
{
return se_cmd(SE_OP_GENKEY, 0x04u, slot, NULL, 0u, pub, 64u);
}
/* Подпись готового хеша: Nonce в режиме «как есть» кладёт 32 байта во внутренний
* регистр TempKey, Sign с режимом 0x80 (внешнее сообщение) подписывает его ключом ячейки. */
int se_sign_digest(uint8_t slot, const uint8_t digest[32], uint8_t sig[64])
{
int e = se_cmd(SE_OP_NONCE, 0x03u, 0x0000u, digest, 32u, NULL, 1u);
if (e != SE_OK)
return e;
return se_cmd(SE_OP_SIGN, 0x80u, slot, NULL, 0u, sig, 64u);
}
regs43b.h — регистры F103 для I2C1, GPIOB и счётчика тактов.
/* regs43b.h — раздел 43б: регистры STM32F103 для I2C1 на PB6/PB7 без заголовков CMSIS.
* Адреса и биты сверены с stm32f10x.h (проверка secel43b_check.py). */
#ifndef REGS43B_H
#define REGS43B_H
#include <stdint.h>
#define REG(a) (*(volatile uint32_t *)(a))
#define RCC_APB2ENR REG(0x40021018u)
#define RCC_APB1ENR REG(0x4002101Cu)
#define RCC_APB2ENR_IOPBEN (1u << 3)
#define RCC_APB1ENR_I2C1EN (1u << 21)
#define GPIOB_CRL REG(0x40010C00u)
#define GPIOB_BSRR REG(0x40010C10u)
#define I2C1_CR1 REG(0x40005400u)
#define I2C1_CR2 REG(0x40005404u)
#define I2C1_DR REG(0x40005410u)
#define I2C1_SR1 REG(0x40005414u)
#define I2C1_SR2 REG(0x40005418u)
#define I2C1_CCR REG(0x4000541Cu)
#define I2C1_TRISE REG(0x40005420u)
#define I2C_CR1_PE (1u << 0)
#define I2C_CR1_START (1u << 8)
#define I2C_CR1_STOP (1u << 9)
#define I2C_CR1_ACK (1u << 10)
#define I2C_CR1_POS (1u << 11)
#define I2C_CR1_SWRST (1u << 15)
#define I2C_SR1_SB (1u << 0)
#define I2C_SR1_ADDR (1u << 1)
#define I2C_SR1_BTF (1u << 2)
#define I2C_SR1_RXNE (1u << 6)
#define I2C_SR1_TXE (1u << 7)
#define I2C_SR1_AF (1u << 10)
#define I2C_SR2_BUSY (1u << 1)
/* Счётчик тактов ядра Cortex-M3 — для пауз в микросекундах */
#define DEMCR REG(0xE000EDFCu)
#define DWT_CTRL REG(0xE0001000u)
#define DWT_CYCCNT REG(0xE0001004u)
#define DEMCR_TRCENA (1u << 24)
#define DWT_CTRL_CYCCNTENA (1u << 0)
#endif
i2c_f103.c — нижний уровень на F103: I2C1 регистрами, пробуждение выводом SDA.
/* i2c_f103.c — раздел 43б: нижний уровень se43b.c на STM32F103, I2C1 (PB6 — SCL, PB7 — SDA),
* 100 кГц от HSI 8 МГц. Приём N байт — по RM0008, «Method 2» (случаи N = 1, N = 2, N > 2).
* На плате не проверялся: собран arm-none-eabi-gcc. */
#include "regs43b.h"
#include "se43b.h"
#define APB1_MHZ 8u /* после сброса: HSI 8 МГц, делители 1 */
#define CPU_MHZ 8u
#define I2C_TMO 20000u /* опросов флага до отказа — без этого обрыв провода вешает прибор */
void i2c1_init(void)
{
RCC_APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC_APB1ENR |= RCC_APB1ENR_I2C1EN;
/* PB6, PB7: альтернативная функция, открытый сток, 2 МГц (CNF = 11, MODE = 10) */
GPIOB_CRL = (GPIOB_CRL & ~(0xFFu << 24)) | (0xEu << 24) | (0xEu << 28);
I2C1_CR1 = I2C_CR1_SWRST;
I2C1_CR1 = 0;
I2C1_CR2 = APB1_MHZ; /* частота APB1 в МГц */
I2C1_CCR = APB1_MHZ * 1000000u / (2u * 100000u); /* 40: стандартный режим 100 кГц */
I2C1_TRISE = APB1_MHZ + 1u; /* фронт до 1000 нс */
I2C1_CR1 = I2C_CR1_PE;
DEMCR |= DEMCR_TRCENA;
DWT_CYCCNT = 0;
DWT_CTRL |= DWT_CTRL_CYCCNTENA;
}
void se_hal_delay_us(uint32_t us)
{
uint32_t t0 = DWT_CYCCNT;
while (DWT_CYCCNT - t0 < us * CPU_MHZ) {
}
}
/* Пробуждение: PB7 на время в обычный выход с открытым стоком и в ноль, потом обратно в I2C */
void se_hal_sda_low_us(uint32_t us)
{
GPIOB_BSRR = 1u << (16 + 7);
GPIOB_CRL = (GPIOB_CRL & ~(0xFu << 28)) | (0x6u << 28); /* CNF = 01, MODE = 10 */
se_hal_delay_us(us);
GPIOB_BSRR = 1u << 7; /* отпустить: линию поднимет подтяжка */
GPIOB_CRL = (GPIOB_CRL & ~(0xFu << 28)) | (0xEu << 28);
}
/* 0 — флаг поднялся; 1 — NACK (AF); 2 — время вышло */
static int wait_sr1(uint32_t bit)
{
for (uint32_t n = 0; n < I2C_TMO; n++) {
uint32_t s = I2C1_SR1;
if (s & bit)
return 0;
if (s & I2C_SR1_AF)
return 1;
}
return 2;
}
static int stop_fail(void)
{
I2C1_SR1 = ~I2C_SR1_AF & 0xFFFFu; /* AF сбрасывается записью нуля */
I2C1_CR1 |= I2C_CR1_STOP;
I2C1_CR1 &= ~I2C_CR1_POS;
return 1;
}
static int start_addr(uint8_t a8)
{
for (uint32_t n = 0; I2C1_SR2 & I2C_SR2_BUSY; n++)
if (n >= I2C_TMO)
return 1;
I2C1_CR1 |= I2C_CR1_START;
if (wait_sr1(I2C_SR1_SB))
return stop_fail();
I2C1_DR = a8;
if (wait_sr1(I2C_SR1_ADDR))
return stop_fail(); /* занятая или спящая микросхема отвечает NACK */
return 0;
}
int se_hal_write(uint8_t addr7, const uint8_t *buf, size_t n)
{
if (start_addr((uint8_t)(addr7 << 1)))
return 1;
(void)I2C1_SR1;
(void)I2C1_SR2;
for (size_t i = 0; i < n; i++) {
if (wait_sr1(I2C_SR1_TXE))
return stop_fail();
I2C1_DR = buf[i];
}
if (wait_sr1(I2C_SR1_BTF))
return stop_fail();
I2C1_CR1 |= I2C_CR1_STOP;
return 0;
}
int se_hal_read(uint8_t addr7, uint8_t *buf, size_t n)
{
if (n == 0u)
return 1;
I2C1_CR1 |= I2C_CR1_ACK;
if (n == 2u)
I2C1_CR1 |= I2C_CR1_POS;
if (start_addr((uint8_t)((addr7 << 1) | 1u)))
return 1;
if (n == 1u) { /* ACK снять до сброса ADDR */
I2C1_CR1 &= ~I2C_CR1_ACK;
(void)I2C1_SR1;
(void)I2C1_SR2;
I2C1_CR1 |= I2C_CR1_STOP;
if (wait_sr1(I2C_SR1_RXNE))
return 1;
buf[0] = (uint8_t)I2C1_DR;
return 0;
}
(void)I2C1_SR1;
(void)I2C1_SR2;
if (n == 2u) { /* POS: ACK управляет следующим байтом */
I2C1_CR1 &= ~I2C_CR1_ACK;
if (wait_sr1(I2C_SR1_BTF))
return stop_fail();
I2C1_CR1 |= I2C_CR1_STOP;
buf[0] = (uint8_t)I2C1_DR;
buf[1] = (uint8_t)I2C1_DR;
I2C1_CR1 &= ~I2C_CR1_POS;
return 0;
}
for (size_t i = 0; i < n - 3u; i++) {
if (wait_sr1(I2C_SR1_RXNE))
return stop_fail();
buf[i] = (uint8_t)I2C1_DR;
}
if (wait_sr1(I2C_SR1_BTF)) /* N−2 в DR, N−1 в сдвиговом регистре, SCL прижат */
return stop_fail();
I2C1_CR1 &= ~I2C_CR1_ACK;
buf[n - 3u] = (uint8_t)I2C1_DR;
if (wait_sr1(I2C_SR1_BTF)) /* последний байт принят с NACK */
return stop_fail();
I2C1_CR1 |= I2C_CR1_STOP;
buf[n - 2u] = (uint8_t)I2C1_DR;
if (wait_sr1(I2C_SR1_RXNE))
return 1;
buf[n - 1u] = (uint8_t)I2C1_DR;
return 0;
}
fw43b.c — две прошивки: ключ в const с mbedTLS и защищённый элемент.
/* fw43b.c — раздел 43б: две прошивки F103, которые делают одно и то же — подписывают хеш.
* FW_CONST — закрытый ключ в массиве const, подпись mbedTLS (как «ключ в исходниках» из 43.4);
* FW_SE — ключ внутри защищённого элемента, в прошивке только драйвер se43b.c.
* Собираются arm-none-eabi-gcc; на плате не запускались. */
#include <stdint.h>
#include <string.h>
static const uint8_t digest[32] = { /* SHA-256 сообщения — подписываем всегда его */
0x05, 0xc9, 0x47, 0x0b, 0x6f, 0x24, 0xba, 0x7c, 0x5e, 0x3f, 0x25, 0x3d, 0x21, 0x57, 0xcc, 0x77,
0x69, 0xd8, 0x2a, 0x0a, 0x58, 0xc1, 0x5d, 0x19, 0x85, 0x6f, 0xf6, 0xd2, 0x26, 0x31, 0x65, 0xc9};
volatile uint8_t sig_out[64];
volatile int result;
#if defined(FW_CONST)
#include "mbedtls/ecdsa.h"
static const uint8_t dev_key[32] = { /* так делать нельзя: ключ лежит в образе как есть */
0x70, 0xc0, 0x6d, 0x3f, 0x58, 0x5f, 0x76, 0x7d, 0x30, 0xab, 0xfe, 0x1c, 0x08, 0x5f, 0xb1, 0x22,
0xcb, 0x9d, 0xb3, 0x68, 0xe3, 0xfe, 0x54, 0x8a, 0xcc, 0xf4, 0x6d, 0xbd, 0xc4, 0xa8, 0x89, 0x16};
/* Случайность для маскировки вычислений; в настоящей прошивке — источник из 43а. Здесь заглушка. */
static int blind_rng(void *p, unsigned char *out, size_t n)
{
static uint32_t x = 0x43B43B43u;
(void)p;
for (size_t i = 0; i < n; i++) {
x ^= x << 13; x ^= x >> 17; x ^= x << 5;
out[i] = (unsigned char)x;
}
return 0;
}
static int sign(void)
{
mbedtls_ecp_group grp;
mbedtls_mpi d, r, s;
unsigned char rs[64];
mbedtls_ecp_group_init(&grp);
mbedtls_mpi_init(&d); mbedtls_mpi_init(&r); mbedtls_mpi_init(&s);
int e = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
if (!e) e = mbedtls_mpi_read_binary(&d, dev_key, sizeof dev_key);
if (!e) e = mbedtls_ecdsa_sign_det_ext(&grp, &r, &s, &d, digest, sizeof digest,
MBEDTLS_MD_SHA256, blind_rng, NULL);
if (!e) e = mbedtls_mpi_write_binary(&r, rs, 32);
if (!e) e = mbedtls_mpi_write_binary(&s, rs + 32, 32);
if (!e) memcpy((void *)sig_out, rs, 64);
mbedtls_mpi_free(&d); mbedtls_mpi_free(&r); mbedtls_mpi_free(&s);
mbedtls_ecp_group_free(&grp);
return e;
}
#elif defined(FW_SE)
#include "se43b.h"
void i2c1_init(void);
static int sign(void)
{
uint8_t rs[64];
i2c1_init();
int e = se_wake();
if (e == SE_OK)
e = se_sign_digest(0, digest, rs); /* ключ создан в ячейке 0 при выпуске прибора */
se_sleep();
if (e == SE_OK)
memcpy((void *)sig_out, rs, 64);
return e;
}
#endif
int main(void)
{
result = sign();
for (;;) {
}
}
extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
void Reset_Handler(void)
{
uint32_t *s = &_sidata, *d = &_sdata;
while (d < &_edata)
*d++ = *s++;
for (d = &_sbss; d < &_ebss;)
*d++ = 0u;
main();
}
static void Default_Handler(void)
{
for (;;) {
}
}
typedef void (*vector_t)(void);
__attribute__((section(".isr_vector"), used)) const vector_t vectors[16] = {
(vector_t)&_estack, Reset_Handler, Default_Handler, Default_Handler, Default_Handler, Default_Handler,
Default_Handler, 0, 0, 0, 0, Default_Handler, Default_Handler, 0, Default_Handler, Default_Handler,
};
cfg43b.h — конфигурация mbedTLS для подписи в прошивке FW_CONST.
/* cfg43b.h — раздел 43б: mbedTLS 3.6 только для подписи ECDSA P-256 ключом из массива const. */
#define MBEDTLS_BIGNUM_C
#define MBEDTLS_ECP_C
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
#define MBEDTLS_ECDSA_C
#define MBEDTLS_ECDSA_DETERMINISTIC /* одноразовое k из ключа и хеша (RFC 6979) */
#define MBEDTLS_HMAC_DRBG_C
#define MBEDTLS_MD_C
#define MBEDTLS_SHA256_C
#define MBEDTLS_SHA256_SMALLER
#define MBEDTLS_ASN1_PARSE_C /* ECDSA требует их, даже если DER не нужен */
#define MBEDTLS_ASN1_WRITE_C
fw43b.ld — карта памяти F103C8.
/* fw43b.ld — раздел 43б: STM32F103C8, 64 КБ флеша, 20 КБ ОЗУ. */
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.text : { *(.text .text.*) *(.rodata .rodata.*) . = ALIGN(4); } > FLASH
.ARM.exidx : { *(.ARM.exidx*) } > FLASH
.data : { _sdata = .; *(.data .data.*) . = ALIGN(4); _edata = .; } > RAM AT > FLASH
_sidata = LOADADDR(.data);
.bss (NOLOAD) : { _sbss = .; *(.bss .bss.*) *(COMMON) . = ALIGN(8); _ebss = .; end = .; } > RAM
}
host43b.c — сценарий на компьютере: нижний уровень I²C — строки к модели.
/* host43b.c — раздел 43б: драйвер se43b.c на компьютере. Нижний уровень I2C — строки
* в stdout/stdin, на другом конце модель микросхемы model43b.py:
* «W aa байты» → «A»/«N»; «R aa n» → «A байты»/«N»; «L мкс», «D мкс» → «K»; «P текст» — печать.
* Запуск: python3 model43b.py ./host43b <SHA-256 сообщения, 64 hex-цифры> */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "se43b.h"
static int noise_tx, noise_rx; /* помеха: перевернуть бит в следующей записи / чтении */
static void hex(char *s, const uint8_t *b, size_t n)
{
for (size_t i = 0; i < n; i++)
sprintf(s + 2 * i, "%02x", b[i]);
s[2 * n] = 0;
}
static int reply(char *line, size_t cap)
{
fflush(stdout);
if (!fgets(line, (int)cap, stdin))
exit(3);
return line[0] == 'A' ? 0 : 1;
}
int se_hal_write(uint8_t addr7, const uint8_t *buf, size_t n)
{
uint8_t b[64];
char s[160], line[32];
memcpy(b, buf, n);
if (noise_tx && n > 4u) {
b[3] ^= 0x01u; /* бит в параметре p1 — уже после подсчёта CRC */
noise_tx = 0;
}
hex(s, b, n);
printf("W %02x %s\n", addr7, s);
return reply(line, sizeof line);
}
int se_hal_read(uint8_t addr7, uint8_t *buf, size_t n)
{
char line[256];
printf("R %02x %u\n", addr7, (unsigned)n);
if (reply(line, sizeof line))
return 1;
for (size_t i = 0; i < n; i++) {
unsigned v;
if (sscanf(line + 2 + 2 * i, "%2x", &v) != 1)
return 1;
buf[i] = (uint8_t)v;
}
if (noise_rx && n > 4u) {
buf[5] ^= 0x10u;
noise_rx = 0;
}
return 0;
}
void se_hal_sda_low_us(uint32_t us)
{
char line[8];
printf("L %u\n", (unsigned)us);
reply(line, sizeof line);
}
void se_hal_delay_us(uint32_t us)
{
char line[8];
printf("D %u\n", (unsigned)us);
reply(line, sizeof line);
}
static void say(const char *fmt, const char *tag, const uint8_t *b, size_t n, int r)
{
char s[300], line[8];
hex(s, b, n);
printf("P ");
printf(fmt, tag, r, s, (unsigned)se_last_polls);
printf("\n");
reply(line, sizeof line);
}
#define SAY(tag, b, n, r) say("%s r=%d hex=%s polls=%u", tag, b, n, r)
int main(int argc, char **argv)
{
uint8_t pkt[48], crc[2], rev[4], cfg[128], rnd[32], pub[64], sig[64], dig[32], v4[4];
if (argc < 2 || strlen(argv[1]) != 64)
return 2;
for (int i = 0; i < 32; i++) {
unsigned v;
sscanf(argv[1] + 2 * i, "%2x", &v);
dig[i] = (uint8_t)v;
}
/* 1. Пакеты и CRC без микросхемы */
se_crc16((const uint8_t *)"\x04\x11", 2u, crc);
SAY("crc_0411", crc, 2u, 0);
se_crc16((const uint8_t *)"\x04\x07", 2u, crc);
SAY("crc_0407", crc, 2u, 0);
SAY("pkt_info", pkt, se_build(pkt, SE_OP_INFO, 0, 0, NULL, 0), 0);
SAY("pkt_random", pkt, se_build(pkt, SE_OP_RANDOM, 0, 0, NULL, 0), 0);
SAY("pkt_genkey", pkt, se_build(pkt, SE_OP_GENKEY, 0x04, 0, NULL, 0), 0);
SAY("pkt_sign", pkt, se_build(pkt, SE_OP_SIGN, 0x80, 0, NULL, 0), 0);
SAY("pkt_nonce", pkt, se_build(pkt, SE_OP_NONCE, 0x03, 0, dig, 32u), 0);
/* 2. Пробуждение: короткий импульс и правильный */
SAY("wake_20us", NULL, 0, se_wake_us(20u));
SAY("wake_80us", NULL, 0, se_wake());
/* 3. Версия и конфигурация */
SAY("info", rev, 4u, se_info(rev));
int r = se_read_config(cfg);
SAY("config", cfg, 128u, r);
v4[0] = cfg[16]; v4[1] = cfg[86]; v4[2] = cfg[87]; v4[3] = 0;
SAY("cfg_i2c_lockdata_lockcfg", v4, 3u, r);
/* 4. До блокировки конфигурации */
SAY("random_unlocked", rnd, 32u, se_random(rnd));
SAY("genkey_unlocked", pub, 0u, se_genkey(0, pub));
/* 5. Блокировка конфигурации: сначала с чужой CRC, затем с CRC прочитанной зоны */
se_crc16(cfg, 128u, crc);
SAY("cfg_crc", crc, 2u, 0);
SAY("lock_cfg_badcrc", NULL, 0, se_lock(0x00, (uint16_t)(crc[0] | crc[1] << 8) ^ 0x0001u));
SAY("lock_cfg", NULL, 0, se_lock(0x00, (uint16_t)(crc[0] | crc[1] << 8)));
SAY("lock_cfg_again", NULL, 0, se_lock(0x00, (uint16_t)(crc[0] | crc[1] << 8)));
r = se_read_config(cfg);
v4[0] = cfg[16]; v4[1] = cfg[86]; v4[2] = cfg[87];
SAY("cfg_i2c_lockdata_lockcfg", v4, 3u, r);
SAY("random_1", rnd, 32u, se_random(rnd));
SAY("random_2", rnd, 32u, se_random(rnd));
/* 6. Ключ: создаётся внутри, наружу — только открытый */
SAY("genkey", pub, 64u, se_genkey(0, pub));
SAY("sign_data_unlocked", sig, 0u, se_sign_digest(0, dig, sig));
SAY("lock_data", NULL, 0, se_lock(0x81, 0));
SAY("sign", sig, 64u, se_sign_digest(0, dig, sig));
SAY("read_slot0", rnd, 0u, se_cmd(SE_OP_READ, 0x82, 0 << 3, NULL, 0, rnd, 32u));
/* 7. Помехи на шине */
noise_tx = 1;
SAY("info_tx_noise", rev, 0u, se_info(rev));
noise_rx = 1;
SAY("random_rx_noise", rnd, 0u, se_random(rnd));
/* 8. Сторож: между Nonce и Sign прошло больше, чем живёт пробуждение */
se_idle();
SAY("wake", NULL, 0, se_wake());
SAY("nonce", NULL, 0, se_cmd(SE_OP_NONCE, 0x03, 0, dig, 32u, NULL, 1u));
se_hal_delay_us(1400000u);
SAY("sign_after_1400ms", sig, 0u, se_cmd(SE_OP_SIGN, 0x80, 0, NULL, 0, sig, 64u));
SAY("wake", NULL, 0, se_wake());
SAY("sign_2", sig, 64u, se_sign_digest(0, dig, sig));
se_sleep();
printf("Q\n");
fflush(stdout);
return 0;
}
model43b.py — модель микросхемы поверх транзакций I²C, своя арифметика P-256.
# -*- coding: utf-8 -*-
"""model43b.py — раздел 43б: модель защищённого элемента «как ATECC608» поверх транзакций I2C.
python3 model43b.py ./host43b <sha256-hex> — запустить драйвер и обслуживать его шину;
python3 model43b.py --secret — напечатать закрытый ключ, который модель создаст
(только для опыта поиска ключа в образе прошивки).
Только стандартная библиотека Python. Закрытый ключ живёт в self.priv и наружу не выдаётся:
драйвер получает открытый ключ и подпись. Времена команд, сторож и правила блокировок — модель,
с микросхемой не сверены (см. раздел)."""
import hashlib
import subprocess
import sys
# ── P-256 (FIPS 186-4, D.1.2.3): своя арифметика, аффинные координаты ──
P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
A = P - 3
N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551
G = (0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296,
0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5)
def ec_add(p1, p2):
if p1 is None:
return p2
if p2 is None:
return p1
(x1, y1), (x2, y2) = p1, p2
if x1 == x2 and (y1 + y2) % P == 0:
return None
if p1 == p2:
lam = (3 * x1 * x1 + A) * pow(2 * y1, -1, P) % P
else:
lam = (y2 - y1) * pow(x2 - x1, -1, P) % P
x3 = (lam * lam - x1 - x2) % P
return x3, (lam * (x1 - x3) - y1) % P
def ec_mul(k, pt):
acc = None
while k:
if k & 1:
acc = ec_add(acc, pt)
pt = ec_add(pt, pt)
k >>= 1
return acc
def crc16(data):
"""Тот же алгоритм, что se_crc16 в se43b.c (полином 0x8005, биты байта младшим вперёд)."""
r = 0
for b in data:
for i in range(8):
top = r >> 15
r = (r << 1) & 0xFFFF
if ((b >> i) & 1) != top:
r ^= 0x8005
return bytes([r & 0xFF, r >> 8])
def packet(payload):
body = bytes([len(payload) + 3]) + payload
return body + crc16(body)
EXEC_US = {0x30: 1000, 0x02: 1000, 0x1B: 23000, 0x16: 7000, 0x17: 32000, 0x40: 115000, 0x41: 50000}
WAKE_LOW_MIN, WAKE_HIGH, WATCHDOG = 60, 1500, 1300000
OK, PARSE, EXEC, BADCRC = 0x00, 0x03, 0x0F, 0xFF
class Chip:
def __init__(self, seed=b"model43b"):
self.seed, self.ctr = seed, {}
self.t = 0 # время модели, мкс
self.state = "sleep" # sleep / idle / awake
self.ready_at = self.deadline = self.busy_until = 0
self.out, self.ptr = b"", 0
self.tempkey = None
self.priv = {} # ячейка → закрытый ключ (не покидает модель)
cfg = bytearray(128)
cfg[0:4] = bytes([0x01, 0x23, 0x5A, 0x17]) # SN[0:3]: 01 23 и номер
cfg[4:8] = bytes([0x00, 0x00, 0x60, 0x03]) # RevNum
cfg[8:13] = bytes([0x9C, 0x41, 0x0B, 0x7E, 0xEE]) # SN[4:8]
cfg[14], cfg[16] = 0x01, 0xC0 # I2C_Enable; I2C_Address — 8-битная запись
cfg[86], cfg[87] = 0x55, 0x55 # LockValue (данные), LockConfig: 0x55 — открыто
self.cfg = cfg
self.log, self.events = [], []
def rand32(self, tag=b"rnd"):
"""Генератор модели: SHA-256(зерно ‖ назначение ‖ счётчик); у ключей ячеек свой счётчик."""
self.ctr[tag] = self.ctr.get(tag, 0) + 1
return hashlib.sha256(self.seed + tag + self.ctr[tag].to_bytes(4, "big")).digest()
def scalar(self, tag=b"rnd"):
while True:
k = int.from_bytes(self.rand32(tag), "big")
if 1 <= k < N:
return k
# ── время и сторож ──
def tick(self, us):
self.t += us
if self.state == "awake" and self.t >= self.deadline:
self.state, self.tempkey = "sleep", None
self.events.append("сторож: сон через %d мкс после пробуждения" % WATCHDOG)
def bus_time(self, nbytes):
self.tick((1 + nbytes) * 9 * 10) # 100 кГц: 9 тактов на байт с адресом
def answers(self, addr):
return addr == self.cfg[16] >> 1 and self.state == "awake" and self.t >= self.ready_at \
and self.t >= self.busy_until
# ── транзакции ──
def sda_low(self, us):
self.tick(us)
if self.state != "awake" and us >= WAKE_LOW_MIN:
self.state, self.ready_at = "awake", self.t + WAKE_HIGH
self.deadline = self.t + WATCHDOG
self.out, self.ptr = packet(bytes([0x11])), 0
self.events.append("пробуждение: SDA в нуле %d мкс" % us)
elif self.state != "awake":
self.events.append("SDA в нуле %d мкс — мало, спит дальше" % us)
def write(self, addr, data):
self.bus_time(len(data))
if not self.answers(addr):
return False
wa = data[0]
if wa == 0x01:
self.state, self.tempkey = "sleep", None
elif wa == 0x02:
self.state = "idle"
elif wa == 0x00:
self.ptr = 0
elif wa == 0x03:
self.command(data[1:])
return True
def read(self, addr, n):
self.bus_time(n)
if not self.answers(addr):
return None
chunk = self.out[self.ptr:self.ptr + n]
self.ptr += n
return chunk + b"\xff" * (n - len(chunk))
# ── команды ──
def status(self, code):
return packet(bytes([code]))
def command(self, pkt):
self.ptr = 0
if len(pkt) < 7 or pkt[0] != len(pkt) or crc16(pkt[:-2]) != pkt[-2:]:
self.out = self.status(BADCRC)
return
op, p1, p2, data = pkt[1], pkt[2], pkt[3] | pkt[4] << 8, pkt[5:-2]
self.busy_until = self.t + EXEC_US.get(op, 1000)
self.out = self.execute(op, p1, p2, data)
def execute(self, op, p1, p2, data):
cfg_locked, data_locked = self.cfg[87] == 0x00, self.cfg[86] == 0x00
if op == 0x30 and p1 == 0x00: # Info: версия
return packet(bytes(self.cfg[4:8]))
if op == 0x02: # Read
zone, size = p1 & 3, 32 if p1 & 0x80 else 4
if zone == 0:
start = ((p2 >> 3) & 3) * 32 + (p2 & 7) * 4
return packet(bytes(self.cfg[start:start + size]))
return self.status(EXEC) # ячейки ключей не читаются
if op == 0x17: # Lock
zone, nocrc = p1 & 3, p1 & 0x80
if zone == 0:
if cfg_locked or (not nocrc and int.from_bytes(crc16(bytes(self.cfg)), "little") != p2):
return self.status(EXEC)
self.cfg[87] = 0x00
return self.status(OK)
if zone == 1 and cfg_locked and not data_locked and nocrc:
self.cfg[86] = 0x00
return self.status(OK)
return self.status(EXEC)
if op == 0x1B: # Random
return packet(self.rand32() if cfg_locked else bytes([0xFF, 0xFF, 0x00, 0x00]) * 8)
if op == 0x40 and p1 == 0x04: # GenKey: новый ключ
if not cfg_locked or data_locked:
return self.status(EXEC)
self.priv[p2] = self.scalar(b"key")
x, y = ec_mul(self.priv[p2], G)
return packet(x.to_bytes(32, "big") + y.to_bytes(32, "big"))
if op == 0x16 and p1 == 0x03 and len(data) == 32: # Nonce «как есть»
self.tempkey = bytes(data)
return self.status(OK)
if op == 0x41 and p1 == 0x80: # Sign внешнего хеша
if not data_locked or self.tempkey is None or p2 not in self.priv:
return self.status(EXEC)
e, d = int.from_bytes(self.tempkey, "big"), self.priv[p2]
self.tempkey = None
while True:
k = self.scalar()
r = ec_mul(k, G)[0] % N
s = pow(k, -1, N) * (e + r * d) % N
if r and s:
return packet(r.to_bytes(32, "big") + s.to_bytes(32, "big"))
return self.status(PARSE)
def serve(argv):
chip = Chip()
proc = subprocess.Popen(argv, stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1)
bus = [] # всё, что прошло по шине, в обе стороны
last, rep = None, 0
def note(s):
nonlocal last, rep
if s == last:
rep += 1
return
if last is not None:
print("BUS " + last + (" ×%d" % (rep + 1) if rep else ""))
last, rep = s, 0
for line in proc.stdout:
f = line.split()
if not f:
continue
if f[0] == "Q":
break
if f[0] == "P":
if last is not None:
print("BUS " + last + (" ×%d" % (rep + 1) if rep else ""))
last, rep = None, 0
print(" ".join(f[1:]) + " t_us=%d" % chip.t)
ans = "K"
elif f[0] == "W":
data = bytes.fromhex(f[2])
okw = chip.write(int(f[1], 16), data)
if okw:
bus.append(data)
note("W %s %s %s" % (f[1], f[2] if len(f[2]) <= 24 else f[2][:24] + "…(%d)" % len(data), "A" if okw else "N"))
ans = "A" if okw else "N"
elif f[0] == "R":
got = chip.read(int(f[1], 16), int(f[2]))
if got is not None:
bus.append(got)
h = got.hex() if got is not None else ""
note("R %s %s %s" % (f[1], f[2], ("A " + (h if len(h) <= 24 else h[:24] + "…")) if got is not None else "N"))
ans = ("A " + h) if got is not None else "N"
elif f[0] == "L":
chip.sda_low(int(f[1]))
ans = "K"
elif f[0] == "D":
chip.tick(int(f[1]))
ans = "K"
else:
ans = "N"
proc.stdin.write(ans + "\n")
proc.stdin.flush()
if last is not None:
print("BUS " + last + (" ×%d" % (rep + 1) if rep else ""))
proc.wait()
for e in chip.events:
print("EVENT " + e)
wire = b"".join(bus)
key = chip.priv[0].to_bytes(32, "big")
print("bus_transactions=%d bus_bytes=%d" % (len(bus), len(wire)))
print("bus_key_hits=%d bus_key_half_hits=%d" % (wire.count(key), wire.count(key[:16]) + wire.count(key[16:])))
print("key_sha256=%s" % hashlib.sha256(key).hexdigest()[:16])
print("host_exit=%d model_t_us=%d" % (proc.returncode, chip.t))
if __name__ == "__main__":
if sys.argv[1:] == ["--secret"]:
print("model_key=%064x" % Chip().scalar(b"key")) # первый ключ, который создаст GenKey
else:
serve(sys.argv[1:])
der43b.py — открытый ключ и подпись — в форматы openssl.
# -*- coding: utf-8 -*-
"""der43b.py — раздел 43б: открытый ключ X‖Y и подпись r‖s от микросхемы → форматы openssl.
python3 der43b.py out.txt → pub.pem, sig1.der, sig2.der, dig.bin, digbad.bin (в текущем каталоге)."""
import base64
import hashlib
import re
import sys
# SubjectPublicKeyInfo для P-256: id-ecPublicKey + prime256v1, затем BIT STRING с 04‖X‖Y
SPKI_P256 = bytes.fromhex("3059301306072a8648ce3d020106082a8648ce3d030107034200")
def der_int(b):
b = b.lstrip(b"\x00") or b"\x00"
if b[0] & 0x80:
b = b"\x00" + b # старший бит 1 — иначе число станет отрицательным
return bytes([0x02, len(b)]) + b
def der_sig(rs):
body = der_int(rs[:32]) + der_int(rs[32:])
return bytes([0x30, len(body)]) + body
out = open(sys.argv[1], encoding="utf-8").read()
val = lambda tag: bytes.fromhex(re.search(r"^%s r=0 hex=([0-9a-f]+) " % tag, out, re.M).group(1))
pub = val("genkey")
der = SPKI_P256 + b"\x04" + pub
pem = "-----BEGIN PUBLIC KEY-----\n" + base64.encodebytes(der).decode() + "-----END PUBLIC KEY-----\n"
open("pub.pem", "w").write(pem)
for n, tag in ((1, "sign"), (2, "sign_2")):
sig = der_sig(val(tag))
open("sig%d.der" % n, "wb").write(sig)
print("sig%d_der_len=%d" % (n, len(sig)))
dig = hashlib.sha256(open("msg.bin", "rb").read()).digest()
open("dig.bin", "wb").write(dig)
open("digbad.bin", "wb").write(bytes([dig[0] ^ 0x01]) + dig[1:])
print("pub_der_len=%d" % len(der))
ref43b.py — независимый пересчёт CRC, пакетов, фронтов и пробуждения.
# -*- coding: utf-8 -*-
"""ref43b.py — раздел 43б: независимый пересчёт. Только стандартная библиотека.
CRC здесь считается иначе, чем в se43b.c и model43b.py: каждый байт сначала разворачивается
(бит 0 ↔ бит 7), потом идёт обычная табличная CRC-16 «старшим вперёд» с полиномом 0x8005.
Пакеты команд собираются заново по описанию формата, а не вызовом драйвера."""
import hashlib
POLY = 0x8005
TABLE = []
for i in range(256):
r = i << 8
for _ in range(8):
r = ((r << 1) ^ POLY) & 0xFFFF if r & 0x8000 else (r << 1) & 0xFFFF
TABLE.append(r)
def rev8(b):
return int("{:08b}".format(b)[::-1], 2)
def crc_ref(data):
r = 0
for b in data:
r = ((r << 8) & 0xFFFF) ^ TABLE[(r >> 8) ^ rev8(b)]
return bytes([r & 0xFF, r >> 8])
def crc_ataes132a(data):
"""Вариант из драйвера ATAES132A в Zephyr (Apache-2.0): биты старшим вперёд, перенос бита 8,
результат старшим байтом вперёд. Другая микросхема — другая CRC."""
crc = 0
for b in data:
for j in range(7, -1, -1):
bit = (b >> j) & 1
hi = crc >> 15
carry = (crc & 0x100) << 1
crc = ((crc << 1) & 0xFFFF) | carry
if bit ^ hi:
crc ^= POLY
return bytes([crc >> 8, crc & 0xFF])
def cmd(op, p1, p2, data=b""):
body = bytes([7 + len(data), op, p1, p2 & 0xFF, p2 >> 8]) + data
return bytes([0x03]) + body + crc_ref(body)
def rsp(payload):
body = bytes([len(payload) + 3]) + payload
return body + crc_ref(body)
print("crc_0411=%s (в открытом коде LoRaMac-node ответ пробуждения: 04 11 33 43)" % crc_ref(b"\x04\x11").hex())
print("crc_0407=%s (там же константа сбоя самопроверки: 04 07 c4 40)" % crc_ref(b"\x04\x07").hex())
print("crc_0411_ataes132a=%s" % crc_ataes132a(b"\x04\x11").hex())
print("crc_123456789=%s" % crc_ref(b"123456789").hex())
def crc_generic(data, refin, refout):
"""Обычная CRC-16 с полиномом 0x8005 и нулевым началом — для сравнения с вариантом ATECC."""
r = 0
for b in data:
r ^= (rev8(b) if refin else b) << 8
for _ in range(8):
r = ((r << 1) ^ POLY) & 0xFFFF if r & 0x8000 else (r << 1) & 0xFFFF
return int("{:016b}".format(r)[::-1], 2) if refout else r
print("crc16_refin_refout_123456789=%04x crc16_plain_123456789=%04x atecc_as_number=%04x" % (
crc_generic(b"123456789", True, True), crc_generic(b"123456789", False, False),
int.from_bytes(crc_ref(b"123456789"), "little")))
msg = "firmware 43b: подпиши меня".encode()
dig = hashlib.sha256(msg).digest()
print("msg_len=%d digest=%s" % (len(msg), dig.hex()))
for name, pkt in (("pkt_info", cmd(0x30, 0x00, 0)), ("pkt_random", cmd(0x1B, 0x00, 0)),
("pkt_genkey", cmd(0x40, 0x04, 0)), ("pkt_sign", cmd(0x41, 0x80, 0)),
("pkt_nonce", cmd(0x16, 0x03, 0, dig)), ("pkt_read_cfg0", cmd(0x02, 0x80, 0)),
("pkt_lock_data", cmd(0x17, 0x81, 0)), ("rsp_ok", rsp(b"\x00")),
("rsp_exec", rsp(b"\x0f")), ("rsp_badcrc", rsp(b"\xff")), ("rsp_info", rsp(bytes([0, 0, 0x60, 0x03])))):
print("%s=%s" % (name, pkt.hex()))
# Пробуждение через запись по адресу 0x00: SDA в нуле весь адресный байт — 8 тактов SCL
for f in (100000, 400000):
print("wake_by_addr0 f=%d low_us=%.1f enough=%s" % (f, 8e6 / f, 8e6 / f >= 60))
# Фронт по 17.3: t_r = 0,85 · Rp · Cb; внутренняя подтяжка STM32F103 — 30…50 кОм (DS5319)
for rp, cb in ((40000, 50e-12), (4700, 50e-12), (10000, 100e-12)):
tr = 0.8473 * rp * cb * 1e9
print("rise rp=%d cb_pf=%d tr_ns=%.0f std_ok=%s fast_ok=%s" % (rp, cb * 1e12, tr, tr <= 1000, tr <= 300))
# Пакет команды: сколько байт и сколько это времени на 100 кГц
for name, n in (("info", 8), ("nonce", 40), ("sign_rsp", 67)):
print("bytes %s=%d time_us=%d" % (name, n, (n + 1) * 9 * 10))
find43b.py — поиск ключа в образе прошивки.
# -*- coding: utf-8 -*-
"""find43b.py — раздел 43б: поиск ключа в образе прошивки, как это сделал бы нарушитель с дампом флеша.
python3 find43b.py <образ.bin> <имя> <ключ hex> <адрес начала> — где встречается ключ (и его половины);
python3 find43b.py --const-key fw43b.c — ключ из массива dev_key[] в исходнике."""
import re
import sys
if sys.argv[1] == "--const-key":
body = re.search(r"dev_key\[32\] = \{[^\n]*\n(.*?)\};", open(sys.argv[2], encoding="utf-8").read(), re.S).group(1)
print("".join("%02x" % int(v, 16) for v in re.findall(r"0x([0-9a-f]{2})", body)))
sys.exit(0)
img = open(sys.argv[1], "rb").read()
key, base = bytes.fromhex(sys.argv[3]), int(sys.argv[4], 16)
hits = [m.start() for m in re.finditer(re.escape(key), img)]
half = sum(img.count(h) for h in (key[:16], key[16:]))
rev = img.count(key[::-1])
print("find %s %s size=%d hits=%d at=%s halves=%d reversed=%d" % (
sys.argv[1], sys.argv[2], len(img), len(hits), ",".join("0x%08x" % (base + h) for h in hits) or "-", half, rev))
run43b.sh — прогон: host, ref, build.
#!/bin/sh
# Раздел 43б: прогон. /v — этот каталог, /m — mbedTLS 3.6.4 (только чтение, нужен шагу build).
# sh /v/run43b.sh host в stm-sys:1 — драйвер se43b.c против модели model43b.py, подпись проверяет openssl
# sh /v/run43b.sh ref в stm-sys:1 — независимый пересчёт ref43b.py
# sh /v/run43b.sh build в stm-arm:1 — прошивки F103 «ключ в const» и «защищённый элемент», размеры, поиск ключа
set -u
export LC_ALL=C.UTF-8 PYTHONIOENCODING=utf-8
B=$(mktemp -d)
cd $B
MSG="firmware 43b: подпиши меня"
case "$1" in
host)
echo "== gcc $(gcc -dumpfullversion), $(openssl version | cut -d' ' -f1-2), python $(python3 -c 'import sys; print(sys.version.split()[0])')"
gcc -std=c11 -O2 -Wall -Wextra -Werror -o host43b /v/host43b.c /v/se43b.c 2>&1
echo "build_host=$?"
printf '%s' "$MSG" > msg.bin
DIG=$(openssl dgst -sha256 -r msg.bin | cut -c1-64)
echo "msg_len=$(wc -c < msg.bin) digest=$DIG"
python3 /v/model43b.py ./host43b $DIG > out.txt 2>&1
cat out.txt
python3 /v/der43b.py out.txt
for n in 1 2; do
echo "-- подпись $n: сообщение целиком"
openssl dgst -sha256 -verify pub.pem -signature sig$n.der msg.bin 2>&1
printf '%s' "$MSG" | sed 's/43b/43c/' > bad.bin
openssl dgst -sha256 -verify pub.pem -signature sig$n.der bad.bin 2>&1 | head -1
echo "-- подпись $n: хеш, первый байт подменён"
openssl pkeyutl -verify -pubin -inkey pub.pem -in dig.bin -sigfile sig$n.der 2>&1 | head -1
openssl pkeyutl -verify -pubin -inkey pub.pem -in digbad.bin -sigfile sig$n.der 2>&1 | head -1
done
openssl pkey -pubin -in pub.pem -noout -text 2>&1 | grep -E "ASN1 OID|NIST CURVE"
;;
ref)
echo "== python $(python3 -c 'import sys; print(sys.version.split()[0])')"
python3 /v/ref43b.py
;;
build)
echo "== $(arm-none-eabi-gcc --version | head -1)"
CPU="-mcpu=cortex-m3 -mthumb"
W="$CPU -Os -std=c11 -Wall -Wextra -Werror -ffunction-sections -fdata-sections"
LD="$CPU -nostartfiles --specs=nano.specs --specs=nosys.specs -Wl,--gc-sections -T /v/fw43b.ld"
mkdir lib && for f in /m/library/*.c; do
arm-none-eabi-gcc $CPU -Os -ffunction-sections -fdata-sections -I/v -I/m/include -I/m/library \
-DMBEDTLS_CONFIG_FILE='"cfg43b.h"' -c $f -o lib/$(basename $f .c).o 2>&1 || echo "lib_fail $f"
done
arm-none-eabi-ar rcs libmb.a lib/*.o
for f in se43b.c i2c_f103.c; do
arm-none-eabi-gcc $W -I/v -c /v/$f -o ${f%.c}.o 2>&1
echo "build_${f%.c}=$?"
done
arm-none-eabi-gcc $W -DFW_SE -I/v -c /v/fw43b.c -o fw_se.o 2>&1
echo "build_fw_se=$?"
arm-none-eabi-gcc $W -DFW_CONST -I/v -I/m/include -DMBEDTLS_CONFIG_FILE='"cfg43b.h"' -c /v/fw43b.c -o fw_const.o 2>&1
echo "build_fw_const=$?"
arm-none-eabi-gcc $LD -o fw_se.elf fw_se.o se43b.o i2c_f103.o 2>&1
echo "link_se=$?"
arm-none-eabi-gcc $LD -o fw_const.elf fw_const.o libmb.a 2>&1
echo "link_const=$?"
arm-none-eabi-size se43b.o i2c_f103.o fw_se.elf fw_const.elf
arm-none-eabi-nm -S -t d se43b.o i2c_f103.o | awk '$3 ~ /^[Tt]$/ {print "fn", $4, $2 + 0}' | sort
for e in se const; do arm-none-eabi-objcopy -O binary fw_$e.elf fw_$e.bin; echo "bin_$e=$(wc -c < fw_$e.bin)"; done
KC=$(python3 /v/find43b.py --const-key /v/fw43b.c)
KM=$(python3 /v/model43b.py --secret | cut -d= -f2)
for e in const se; do
python3 /v/find43b.py fw_$e.bin const_key $KC 0x08000000
python3 /v/find43b.py fw_$e.bin model_key $KM 0x08000000
done
arm-none-eabi-nm fw_const.elf | grep -E " dev_key$" | sed 's/^/sym /'
;;
esac
rm -rf $B
Дальше: раздел 44 «Аналоговые блоки внутри кристалла: OPAMP и COMP» — операционный усилитель и компаратор, которые не нужно распаивать.