Embedded HandbookApps
All sections › III. Peripherals
STM32F103 · level intermediate · 55 min read
Open in the interactive handbook

43б. The ATECC608 secure element: a key that cannot be read from the firmware

Section 43.4 says it plainly: a key in an array const uint8_t key[16] can be read from the firmware in a minute. Section 11в shows that read protection on the F1 does not save it either. In 48а the private key lives on the build server, and the device stores only the public one — that works as long as the device only verifies. But a device often needs to sign by itself: to prove to a server that it is genuine, or to bring up TLS with its own certificate. Then the private key must be inside the device — and must not be readable in any way.

The answer is a separate chip next to the microcontroller: a secure element. The key is created inside it and never leaves; the firmware sends a hash and gets a signature back. One of the most widespread chips of this kind is the ATECC608 on the I²C bus. This section contains our own driver for its protocol (packet, CRC, wake-up, seven commands), a Python model of the chip for running the driver on a PC, signature verification by a third-party program, and a search for the key in two F103 firmware images.

Sources: the protocol follows the manufacturer's public description (Microchip, repository github.com/MicrochipTech/cryptoauthlib — used only as a description: the library license allows it only with Microchip chips, so the code here is our own). The ATECC608 datasheet was not checked against this section: numbers taken from the description are marked. Independently checked: the wake-up response 04 11 33 43, the 1500 µs pause and the command word address 0x03 — against the open LoRaWAN stack LoRaMac-node (github.com/zephyrproject-rtos/loramac-node, commit fb00b38; the ATECC608A file is under the Revised BSD license), address 0x60 — against the bus scanner example in github.com/openmv/openmv (commit 6236993, MIT license). I²C on the F103 — RM0008 (DocID13902), pull-ups — DS5319 (DocID13587).

Terms used in the documentation

Term Meaning
secure element a chip that keeps keys inside
slot, key slot key storage cell
configuration zone, data zone configuration and data memory areas
LockConfig, LockValue lock bytes for the configuration and data
word address the first byte of any write
wake, idle, sleep wake-up, idle, sleep
watchdog timer that puts the chip to sleep
TempKey temporary register inside the chip
nonce, pass-through mode number used once; "as is" mode
GenKey, Sign create a key, sign
status byte, execution error status byte, execution failure
digest hash

43б.1 Where the key is and what an attacker sees

Where the private key is What a flash dump yields Cost in F103 firmware
const array + mbedTLS the whole key, with one search image of 16560 bytes
protected area with RDP (43.4, 11в) on the F1, the key after bypassing level 1 as in the first row
secure element the driver, no key image of 1996 bytes

A secure element changes the problem itself. The private key is created by the GenKey command inside the chip, and the chip returns only the public key X‖Y (64 bytes). The signature is also made by the chip: the firmware puts the hash into the internal register TempKey with the Nonce command and asks for Sign. The bus carries the hash, the signature and the public key — none of which is secret anyway.

Signing a hash with a secure element: the F103 firmware with the driver sends the hash with the Nonce command and the Sign command over I²C to address 0x60 and gets back a 64-byte signature r‖s; the private key of slot 0 stays inside, reading the slot returns failure 0x0F; a key search over the dump finds it in the image with the const array and does not find it in the image with the secure element

What it does not give you. The chip can be carried off and used for signing without knowing the key: a secure element answers the question "where is the key", not "who gives the commands". The defence against that is binding it to the microcontroller (a shared secret for commands, secure boot), which is outside the scope of this section.

How it was tested without a board. The same se43b.c that goes into the firmware was built with gcc on a PC. The low I²C layer there is four kinds of text lines exchanged with the model model43b.py: write, read, "SDA low for so many microseconds", delay. The model responds the way the manufacturer describes: NACK while asleep or busy, a response with CRC, a failure on a wrong CRC. The private key lives only in the model. The model's signature is verified by openssl — a third-party program. The model is not the chip: command times in it are estimates, the watchdog and lock rules follow the description; nothing has been checked against the chip.


43б.2 Command packet and CRC-16

A write to the chip begins with the word address: 0x03 — command, 0x02 — idle, 0x01 — sleep, 0x00 — reset the response pointer. It is followed by the packet:

Byte What Example: Info
0 count — packet length without the word address, including itself and the CRC 07
1 command opcode 30
2 p1 — mode 00
3–4 p2, low byte first 00 00
5… data (32 bytes for Nonce) —
last 2 CRC-16, low byte first 03 5d

The response is built the same way, without the word address and opcode: [count][data][CRC]. A four-byte response is a status byte: 00 — success, 0F — execution error, FF — the chip received a packet with a wrong CRC, 11 — it has just woken up.

The CRC is 16 bits, polynomial 0x8005, initial value 0. The subtlety is in the bit order: the bits of each byte are fed LSB first, while the register shifts left, with no reversal of the result. The familiar variants with the same polynomial do not fit: with input and result reflection the string 123456789 gives bb3d, without reflection fee8, while the ATECC gives bcdd (the same as the first, but with an unreflected result).

/* 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);
}

Building and parsing:

/* 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;
}

Cross-check. se_crc16 and the model share one way of writing the algorithm; so ref43b.py computes it differently — it reverses each byte and runs an ordinary table-driven MSB-first CRC. All packets matched:

What Bytes Checked against
wake-up response 04 11 → CRC 33 43 LoRaMac-node: 04 11 33 43
string 123456789 CRC dd bc ref43b.py — check value for our own implementation
Info 03 07 30 00 00 00 03 5d driver = ref43b.py
Random 03 07 1b 00 00 00 24 cd driver = ref43b.py
GenKey, slot 0 03 07 40 04 00 00 83 87 driver = ref43b.py
Sign, external hash 03 07 41 80 00 00 28 05 driver = ref43b.py
Nonce with a hash 40 bytes with the address, CRC e6 ae driver = ref43b.py

43б.3 Wake-up, sleep and waiting for the response

At rest the chip sleeps and does not answer its address. To wake it, hold SDA low for at least 60 µs (per the manufacturer's description, not checked against the datasheet), release it and wait 1500 µs. After that the first read returns 04 11 33 43: length 4, status 11 "woke up", 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;
}

Run on the model: a 20 µs pulse — the chip keeps sleeping, the read gets NACK, se_wake_us(20) = -1 (SE_ERR_NACK); an 80 µs pulse — response 04 11 33 43, result 0.

The driver does not know how long to wait for a command response, and does not guess: while the chip is computing, it does not acknowledge its address. Every 1000 µs the driver tries to read the first byte; NACK means it is still busy. The limit is 300 attempts. Then it reads the rest: the length is known from the first byte.

/* Команда целиком: запись пакета, опрос, пока микросхема занята (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;
}

Idle and sleep are a single write of the word address:

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);
}

The difference matters: in idle the contents of TempKey are kept, in sleep they are lost. And the watchdog puts the chip to sleep by itself, a fixed time after wake-up (per the description about 1.3 s; 1300000 µs in the model), whatever happens on the bus. Experiment: Nonce, a 1.4 s pause, Sign — the command write got NACK, result -1. The hash and the signature must fit into one wake-up.


43б.4 Revision, configuration, random numbers and locking

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);
}

The configuration zone is 128 bytes: the chip serial number, revision, I²C address, the permissions of each slot, two lock bytes. Run on the model:

What The model answered
Info, mode 0 00 00 60 03
byte 16 — I²C address 0xC0 = 0x60 × 2, i.e. the 8-bit form
bytes 86 and 87 — LockValue and LockConfig 0x55, 0x55 — both zones unlocked
Random before the configuration is locked ffff0000ffff0000…
GenKey before locking status 0x0F — failure
CRC of the zone read (128 bytes) f7 7f
Lock with a CRC that differs by one bit 0x0F — failure, nothing locked
Lock with the correct CRC 0; byte 87 became 0x00
a second Lock of the same zone 0x0F — a lock happens once and forever
Random after locking, twice 9609dff202404b6b…, 3aba95cfa6cd9ca8…

Before the configuration is locked, Random returns not random numbers but the pattern ffff0000 repeated eight times (per the manufacturer's description; the model does the same). This guards against the mistake "configured it somehow and forgot to lock it", but feeding such "noise" into the generator from 43а is a disaster.

Lock with mode 0 checks the CRC of the whole zone against the one passed in p2. The point: exactly the configuration you intended gets locked, not whatever ended up in the chip after a failed write. That is why in production the CRC is computed from the reference configuration in your own file, not from the one read back, as in the experiment above. The mode with bit 7 (no check) is simpler and more dangerous.


43б.5 The key inside, the signature outside

/* Режим 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);
}

In the model GenKey works only after the configuration is locked, and Sign only after the data zone is locked (that is how the model does it; the chip's exact rules depend on the slot configuration and have not been checked against the datasheet). Run: Sign with the data zone unlocked — 0x0F; Lock of the data — 0; then the signature.

The message is the string "firmware 43b: sign me" written in Russian (firmware 43b: подпиши меня), 37 bytes in UTF-8, SHA-256 05c9470b6f24ba7c…. The chip returns the public key X‖Y and the signature r‖s — 64 bytes each, most significant byte first. openssl expects other wrappers: the key in a SubjectPublicKeyInfo structure with the curve identifier, the signature in DER. The conversion is in 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

The key in DER is 91 bytes (26 bytes of header, 04 and X‖Y). The signature is 71 bytes for the first and 70 for the second: if the top bit of r or s is one, a zero is prepended to the number, otherwise DER would read it as negative. The DER signature length varies from run to run; that is normal.

Verification with openssl 3.0.20:

Check Signature 1 Signature 2
whole message, openssl dgst -sha256 -verify "Verified OK" "Verified OK"
43b → 43c in the message "Verification failure" "Verification failure"
hash, openssl pkeyutl -verify "Signature Verified Successfully" "Signature Verified Successfully"
first byte of the hash changed "Signature Verification Failure" "Signature Verification Failure"

Two signatures of the same hash differ: the nonce k for each is taken from the chip's generator. Both are valid.

And now an attempt to read the key: Read of slot 0 — status 0x0F (execution error). The model logged everything that went over the bus in both directions: 89 transactions, 1042 bytes. A search for the 32 bytes of the private key in this log — 0 matches; a search for its 16-byte halves — also 0.

Noise. A flipped bit in a sent command — the chip answered with status FF, the driver returned 255 (0xFF). A flipped bit in a received Random response — the driver returned -3 (SE_ERR_CRC) and did not hand out the corrupted bytes.


43б.6 F103 firmware and searching the image for the key

The low layer on the F103 is I2C1 on PB6/PB7, 100 kHz from HSI 8 MHz (CCR = 8000000 / (2 × 100000) = 40, TRISE = 9, as in 17.3). For wake-up, PB7 is temporarily switched to a plain open-drain output:

/* Пробуждение: 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);
}

Receiving N bytes on the F1 follows RM0008, "Method 2": one byte, two bytes and more than two — three different orders of clearing ACK. The driver first reads one length byte, then the rest — 3, 6, 34 or 66 bytes; the N = 2 case never occurs for it, but it is kept so that the function works for any length:

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;
}

The two firmware images do the same thing — sign the same hash (05c9470b6f24ba7c…):

arm-none-eabi-gcc 12.2.1, Cortex-M3, -Os, linker garbage collection:

Code (text) .bin image
se43b.o 734 —
i2c_f103.o 784 —
firmware FW_SE 1996 1996
firmware FW_CONST (mbedTLS) 16452 16560

The largest driver functions: se_hal_read — 328 bytes, se_cmd — 192, i2c1_init — 124, se_crc16 — 66.

Now the attacker with a dump. find43b.py searches the image for the 32 key bytes, as any byte search would:

Image Key from dev_key Model key (slot 0)
FW_CONST found 1 time 0 matches
FW_SE 0 matches 0 matches

The match address is 0x080038d4, which is the dev_key symbol from the linker map. The search does not need to know where the key is: the 32 bytes are searched across the whole image. In the secure element variant there is nothing to find.


43б.7 Pitfalls

Locking is irreversible. Lock of the configuration and of the data — one attempt for the whole life of the chip; there is no "unlock" command. In the model a second Lock of the same zone got 0F. A mistake in slot permissions after locking cannot be fixed by anything except replacing the chip. The order: reference configuration in a file → write → read back and compare → Lock with the reference CRC. Debug on spare chips.

Random before locking is not random. The pattern ffff0000 is not an error: the response code is success. Whoever seeds the generator from 43а with these bytes before the configuration is locked gets identical keys on all devices.

Chip revision. Info in mode 0 returns 4 revision bytes; in the model 00 00 60 03. This section has not checked the values of real silicon revisions. Read the revision on first start-up, log it and compare it with the one the firmware was tested on.

Command time is not a constant. In the model GenKey took 98 polling steps of 1 ms, Sign 43, Random 20, Info 1. These are model estimates, not the chip's. A fixed delay "just in case" is either too short or wastes time; polling on NACK with a limit works with any timing.

Watchdog. About 1.3 s after wake-up the chip goes to sleep by itself (per the description; not checked) and loses TempKey. Stopping in the debugger between Nonce and Sign for longer than a second breaks the signature — in the model that is -1.

Weak pull-up. On the F103 the internal pull-ups are disabled in alternate function mode (RM0008, 9.1.9) — without external resistors there is no bus. Where an internal pull-up can be enabled, it is weak: 30–50 kΩ on the F103 (DS5319). By the rise-time formula from 17.3, 40 kΩ on 50 pF give 1695 ns — more than the 1000 ns limit even for 100 kHz; 4.7 kΩ give 199 ns.

Waking up by "writing to address 0x00". A common trick: send address 0x00 — SDA stays low for eight SCL cycles. At 100 kHz that is 80 µs — enough; at 400 kHz it is 20 µs, less than 60, and the chip does not wake up. Either wake it at 100 kHz, or drive the SDA pin directly, as in se_hal_sda_low_us.

The "wrong" address. The same address is written as 0x60 (7 bits) and 0xC0 (8 bits, the form stored in byte 16 of the configuration). A HAL expecting an 8-bit address will, given 0x60, call device 0x30. And the address can be different: pre-provisioned variants have their own — LoRaMac-node lists 0x59 for the ATECC608A-TNGLORA. Run a bus scanner before writing code.

CRC from a neighbouring chip. The ATAES132A from the same company uses a different CRC variant: the Zephyr driver (Apache-2.0) gives 98 65 for 04 11 instead of 33 43. And even published constants can be wrong: in LoRaMac-node the "self-test failure" response is written as 04 07 C4 40, while the CRC of 04 07 is 40 c2. Check responses by computing the CRC, not by comparing with a constant.

License. cryptoauthlib may only be used with Microchip products. The code in this section was written from scratch from the protocol description; nothing was copied from the library.


43б.8 Summary

Full source code

Directory _verify/secel43b/. On a PC: gcc -std=c11 -O2 host43b.c se43b.c -o host43b, then python3 model43b.py ./host43b <SHA-256 сообщения>. The firmware is built as in run43b.sh (step build, mbedTLS 3.6.4).

se43b.h — the driver interface: address, opcodes, errors, low layer.

/* 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 — the protocol driver: CRC, packet, parsing, wake-up, polling, commands.

/* 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 registers for I2C1, GPIOB and the cycle counter.

/* 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 — the low layer on the F103: I2C1 through registers, wake-up via the SDA pin.

/* 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 — two firmware variants: the key in const with mbedTLS, and the secure element.

/* 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 configuration for signing in the FW_CONST firmware.

/* 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 memory map.

/* 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 — the PC scenario: the low I²C layer is text lines to the model.

/* 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 — a model of the chip on top of I²C transactions, with its own P-256 arithmetic.

# -*- 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 — the public key and signature into openssl formats.

# -*- 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 — independent recomputation of the CRC, packets, rise times and wake-up.

# -*- 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 — searching the firmware image for the key.

# -*- 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 — the run: 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

Next: section 44 "Analog blocks inside the chip: OPAMP and COMP" — an op-amp and a comparator you do not have to solder.

Open in the interactive handbook