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

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. По шине идут хеш, подпись и открытый ключ — всё это и так не секрет.

Подпись хеша защищённым элементом: прошивка F103 с драйвером отправляет по I²C на адрес 0x60 хеш командой Nonce и команду Sign, получает подпись r‖s 64 байта; закрытый ключ ячейки 0 остаётся внутри, чтение ячейки возвращает отказ 0x0F; поиск ключа по дампу находит его в образе с массивом const и не находит в образе с защищённым элементом

Что это не даёт. Микросхему можно унести и подписывать ею, не зная ключа: защищённый элемент отвечает на вопрос «где ключ», а не «кто командует». Против этого — связка с микроконтроллером (общий секрет для команд, защищённая загрузка), это за рамками раздела.

Как проверено без платы. Тот же 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…):

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

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

Каталог _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» — операционный усилитель и компаратор, которые не нужно распаивать.

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