Embedded HandbookApps
All sections › VI. Practice
STM32F103, STM32F405, STM32F407 · level intermediate · 41 min read
Open in the interactive handbook

48г. Encrypting the firmware image: the key, the AES mode and where the key lives in the device

The bootloader in 198щ has a key, but it only authenticates the image: the HMAC shows that the image was made by the owner of the key and that not a single bit in it was changed. The image itself travels in the clear. It can be read wherever it is stored or passes through: on the Bluetooth and UART link, in a file inside the phone app (189а), on the update server. Whoever reads the image gets the whole firmware — disassemble it and copy it. In Table 48.9 image encryption takes one row. This section covers it in full: which AES mode, where to get the nonce, where the key comes from and where it is stored in the device.

The section: what each measure gives and what it does not; AES-128-CTR mode with an HMAC-SHA-256 tag over the ciphertext; our own AES in C, checked against reference vectors; a device key derived from a master key and the chip's unique ID; where to store the key on the F405 and F103; a release script in Python and frame-by-frame reception in the bootloader; experiments with forgeries, nonce reuse and rollback; the cost in bytes and instructions.

Sources: FIPS-197 (AES; Appendix C vectors), RFC 3686 (AES-CTR; three vectors — the ones in the mbedTLS 3.6.4 self-test, github.com/Mbed-TLS/mbedtls), RFC 2104 (HMAC), RM0090 Rev 19 (3.7.3 — RDP levels, Table 17 — access per level; 3.7.4 and Table 15 — WRP; 3.7.5 — PCROP; 3.8 and Table 18 — OTP; 23 — the CRYP block, Table 111; 25 — the HASH block), the headers stm32f405xx.h, stm32f407xx.h, stm32f415xx.h, stm32f417xx.h from the STM32F4xx_DFP pack 2.17.1; F1 protection levels — section 11.6. Independent computation — the cryptography 49.0.0 library for Python. The device code is built with gcc 12.2.0 in stm-sys:1 and arm-none-eabi-gcc 12.2.1 in stm-arm:1, and run in QEMU 7.2.22 on the netduinoplus2 machine (an STM32F405). It has not been run on a board. License: the section's own code is free to use.

Terms used in the documentation

Term Meaning
encrypt-then-MAC encrypt first, then put a tag over the ciphertext
keystream the byte stream from the cipher that is XORed with the data
malleability editing the ciphertext edits the plaintext predictably
nonce reuse using the same nonce twice with the same key
master key the key from which device keys are derived
device-unique key a key of its own in every device
fleet key, batch key one key shared by many devices
known plaintext plaintext the attacker already knows
constant-time comparison a comparison without early exit
one-time programmable area (OTP) an area that can be written only once
mass erase erasing the whole Flash memory at once
readout protection bypass getting around read protection

48г.1 What each measure protects and what it does not

Path of the encrypted image: on the computer the build produces app_v2.bin of 4300 bytes, the release script encrypts it with AES-128-CTR and puts an HMAC-SHA-256 tag over the header and the ciphertext, producing v2_a.enc of 4364 bytes; the master key is only on the server, the device key is an HMAC of the master key and the chip's unique ID; the Bluetooth and UART link, the file on the phone and the server see only ciphertext; in the device the 32-byte device key in bootloader sector 0 under WRP and RDP yields k_mac and k_enc, dec_feed frame by frame computes the tag over the ciphertext, decrypts and computes CRC-32 into the buffer slot, dec_end checks the tag, then the version, and only after DEC_OK does the image go to the active slot; the bootloader code is 2690 bytes, reception about 406 instructions per byte

Measure What it gives What it does not give
image CRC-32 (48.4) catches accidental corruption protection from substitution: anyone can recompute a CRC
HMAC with a shared key (198щ) authenticity and integrity: it cannot be forged without the key secrecy of the code; one key per batch — break one device and you have the key for all
signature (48а) authenticity; the device holds only the public key, which is not a secret secrecy of the code
encryption secrecy of the code on the link, in the file and on the server protection from substitution — not without a tag

Encryption without a tag does not protect against substitution. In CTR mode the ciphertext is the plaintext XORed bit by bit with the keystream. Flip a bit of the ciphertext and exactly the same bit of the plaintext flips, nothing else. Experiment: in the encrypted image, bit 3 of body byte 1000 is flipped; the bootloader rejected it on the tag, and what it had already decrypted into the buffer slot differs from the real firmware by exactly one bit:

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

An attacker who knows where a constant or a branch instruction lies in the image can edit it precisely without knowing the key. So the order is: encrypt first, then put the tag over the ciphertext; in the device, check the tag and only then pass the decrypted data on. There are also modes with a built-in tag (GCM from 43.4); here the first path is chosen, and 48г.2 explains why.

Encryption and signature do not exclude each other. The signature from 48а answers the question "who made it"; the tag with the device key answers the same question, but with a secret in the device. If the device stores a secret for decryption, a tag with the same secret costs almost nothing; a signature on top is needed when the decryption key may leak, but running foreign code must still be impossible.


48г.2 The mode: AES-128-CTR and an HMAC-SHA-256 tag

The choice is AES-128 in CTR mode and HMAC-SHA-256 over the header and the ciphertext. Why not GCM, which 43.4 recommends by default:

AES-128, not AES-256: a 16-byte key, 10 rounds instead of 14; with our code this is 321 instructions per byte versus 445 (48г.6). The format does not depend on it: ENC_KEYLEN 32 switches the code to AES-256.

The header is 64 bytes: 32 bytes of fields and the tag.

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

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

The nonce is 12 random bytes for each release (os.urandom in the release script), followed by 4 bytes of the block number starting from zero. What happens if the nonce is reused with the same key — an experiment on two real images: v1 and v2 of the same firmware differ in 76 bytes out of 4300. The naive scheme is one key for all releases and the same nonce:

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

The XOR of two ciphertexts equals the XOR of the two plaintexts: where the firmware images match, there are zeros, and you can see in which 76 bytes the release differs. If the attacker knows the old version — say, it was once released unencrypted or read from an unprotected device — they get the new one in full, all 4300 bytes, without the key.

That is why the encryption key here is unique to each header: k_enc is derived from the device key and all 32 bytes of the header fields — version, length, nonce. Even if the nonce is repeated by mistake, a different version has a different key and a different keystream:

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

13 zeros out of 4300 is just what chance gives: on average one byte in 256. This protection is a fallback. A new nonce is still taken for every release.


48г.3 Our own AES in C and checking it against reference vectors

The F405 and F407 have no CRYP block: the headers stm32f405xx.h and stm32f407xx.h have no CRYP_BASE, while stm32f415xx.h and stm32f417xx.h do. RM0090 (Chapter 23) says the same: the block exists on the F415/F417 and F43x, and GCM only on the F42x/F43x; the F42x has no CRYP at all, so in practice GCM is on the F43x. The HASH block on the F415/F417 computes SHA-1 and MD5; SHA-256 is only on the F43x (Chapter 25). So on the F405, and even more so on the F103, AES is done in software.

Below is a compact AES following FIPS-197: a 256-byte substitution table, the round keys are computed once and kept in RAM. Block encryption only: in CTR decryption is the same operation, so the inverse AES is not needed.

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

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

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

CTR mode: the counter block is encrypted, giving 16 bytes of keystream, and the keystream is XORed with the data; the counter is incremented by one across all 128 bits.

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

Checking. The FIPS-197 vectors (Appendix C) and three RFC 3686 vectors — the same ones that are in the mbedTLS self-test; the CTR vector is fed in chunks of 5 bytes and the remainder, to test a break in the keystream. SHA-256 and HMAC are checked against the standard Python hashlib and hmac:

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

Then 3000 random messages up to 4096 bytes long: AES-128 and AES-256 keys alternate, for every tenth one the counter starts with ff ff ff ff in the low bytes to test the carry; encryption and HMAC are done in chunks of 1 to 300 bytes, the HMAC key is 1 to 100 bytes (longer than 64 is reduced by the hash). The C code prints a SHA-256 digest of all ciphertexts and tags; Python with the same number generator repeats everything through cryptography and gets the same digest:

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

SHA-256 and HMAC are in sha48g.c: the same block computation as in 198щ, but the HMAC is streaming (hmac_init, hmac_update, hmac_final) — the tag is computed frame by frame.


48г.4 Keys: per batch or per device, and where the key lives in the device

A batch key is one for many devices: the server encrypts one file per batch. It is simple, but break one device and you have the key for all devices in the batch: all their images can be read, and with the tag key they can also be forged.

A device key is unique to each device. You do not need to store thousands of keys: the server derives the device key from the master key and the 96-bit unique chip ID (on the F4 the ID is at 0x1FFF7A10, 198щ.4). The derivation is HMAC-SHA-256 with a purpose label (KDF):

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

At the factory the device key is computed and written into the device; the master key never gets into the device. Break a device and you get the key of that one device. The cost is that the server encrypts the image for each device separately and must know the unique ID of every device. A compromise is a key per smaller batch (a hundred devices): a leak affects a hundred, and there are as many files per release as there are batches.

In the device two working keys are derived from the device key: k_mac — one for all releases, k_enc — unique to each header (48г.2). The section's demo keys:

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

In the experiments the master key is the string 48g-demo-key-not-for-devices right in the script. Never do this in a product: the real master key lives only on the release server, in a key store, and gets into neither the repository nor the firmware.

Where the device key is stored on the F405 — according to RM0090:

Where What RM0090 says Suitable?
in the application when booting from Flash, code reads the whole Flash memory (Table 17) no: the application is updated and may leak it itself
bootloader sector 0 + RDP 1 the debugger and boot from RAM or from the system bootloader cannot read Flash; removing RDP erases the Flash and backup SRAM (3.7.3) yes: the key goes away together with the protection
sector 0 + WRP writing and erasing a sector with bit nWRP = 0 are forbidden, flag WRPERR (3.7.4); on the F405 — sectors 0–11 (Table 15) yes: an erroneous write will erase neither the bootloader nor the key; the bit itself is cleared via the option bytes
sector 0 + RDP 2 irreversible: debugging is disabled forever, the option bytes can no longer be changed (3.7.3) best of all, but you lose debugging too
OTP: 16 blocks of 32 bytes, from 0x1FFF7800 never erased, and the mass erase on removing RDP does not touch it (3.7.3, 3.8) only with RDP 2

The last row is a trap. A key in OTP survives the erase on removing protection: the attacker changes RDP from 1 to 0, the Flash is erased, but the OTP stays intact, and at level 0 the debugger reads it freely. A key in the bootloader sector is erased together with the firmware by the same regression. So with RDP 1 the key belongs in the bootloader's Flash, and OTP is suitable only with RDP 2.

Limitations you need to know about:


48г.5 Release on the computer and reception in the bootloader

The release script is in Python. AES-CTR comes from the cryptography library, HMAC and KDF from the standard hmac and hashlib. Encryption first, then the tag:

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

In a release the nonce is os.urandom(12); in the demo set it is derived from the file name so that the files are reproduced byte for byte and the check can rebuild them.

The bootloader. Three functions. dec_begin parses the header, checks only the bounds (magic, length not larger than the slot, address) and prepares the keys. It does not trust the version: it has not been verified by the tag yet.

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

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

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

dec_feed takes a frame of any length: first the tag over the ciphertext, then in-place decryption, CRC-32 of the plaintext and writing to the buffer slot.

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

dec_end — everything has been received: the tag is compared without early exit (otherwise the tag can be guessed byte by byte from the response time), then the version. The keys are wiped in any case.

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

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

Wiping goes through volatile: the compiler is allowed to drop an ordinary memset before return.

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

How this fits onto 198щ. Frames arrive over the same protocol (198щ.3): in on_start, instead of the length — the 64-byte header and dec_begin; in on_data, instead of writing words — dec_feed; in on_end, instead of checking the CRC and HMAC of the plain image — dec_end, and only on DEC_OK — writing READY to the log with the CRC-32 from dec_t.crc. From there everything follows 198щ: copying from the buffer slot (sectors 7–8) to the active slot (sectors 5–6) sector by sector, the CRC after the copy, power loss. The buffer slot holds the plain image — but the buffer slot is protected by the same RDP, and only the active slot is executed. From the phone (189а) the .enc file is sent as is: the app does not need the key, and reading the file in it is useless.

Differential update (48в). The difference is computed over the plain images — on the server, where they exist — and the difference itself is encrypted in the same format (kind = 1). A difference between ciphertexts is useless: ciphertext looks random, and the v1 → v2 difference over ciphertexts is 4334 bytes, larger than the image itself at 4300 bytes. Over the plain images it is 114 bytes, and the encrypted difference file is 178 (64 bytes of header and 114). The bootloader accepts it and decrypts it correctly byte for byte:

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

Applying the difference is the code from 48в; together with decryption it has not been run here.


48г.6 Experiments: rejections, QEMU and cost

Reception and rejections. Device A with v1 installed (version 1). The images are encrypted in Python; the bootloader — the same C code — receives them in frames of 256 bytes. The same release built by our own C function matched the Python file byte for byte.

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

Repeating the same image and rolling back to v1 give DEC_EOLD: the version passed the tag but is not newer than the installed one. From here on, each forgery is tried against a device with v1:

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

A version raised in the header without the key is caught by the tag: the header is under it. Another device's image and the batch image do not fit device A; the device of batch 7 accepts the same batch file. In all rejections the active slot stayed at v1 — CRC-32 d66e75ac.

In QEMU. The same code is built for Cortex-M4 and Cortex-M3 and run on netduinoplus2: the encrypted image lies in Flash, arrives in frames of 256 bytes and is decrypted in RAM; the CRC-32 of the plain image matches app_v2.bin, and a flipped bit gives code 6 — DEC_EAUTH:

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

Size. arm-none-eabi-gcc 12.2.1 with -Os, bootloader code only:

File Cortex-M3 Cortex-M4 Contents
aes48g.o 922 922 AES and CTR; 256-byte substitution table
sha48g.o 1140 1136 SHA-256, HMAC, KDF; 256 bytes of constants
dec48g.o 628 628 header, reception, CRC-32 (64-byte table)
total 2690 2686

RAM: dec_t is 520 bytes (round keys 240, HMAC state 168, the rest is the header and counters). Stack per -fstack-usage — no more than 832 bytes on the longest path: dec_begin 88 → kdf 200 → hmac_init 152 → sha_final 32 → sha_update 24 → sha_block 336.

Timing is only an estimate. QEMU counts instructions, not cycles: with -icount time in it runs by the number of executed instructions, and SysTick ticks are calibrated with a loop of 2000000 instructions (measured the same way in 48а.4). On the chip an instruction costs a cycle or more, and Flash has wait states — so this is a lower bound.

What Instructions per byte
AES-128-CTR, 16 KB 321
AES-256-CTR, 16 KB 445
HMAC-SHA-256, 16 KB 56
CRC-32 by nibbles, 16 KB 11
receiving the whole v2_a: tag, CTR, CRC, write to RAM 406

Preparing the keys in dec_begin (two KDFs and the round keys) takes about 45 thousand instructions. A 256 KB image at 406 instructions per byte takes at least 0.63 s at 168 MHz and 1.48 s on the F103 at 72 MHz. Compared with the link this is small: a 256-byte frame is decrypted in 0.62 ms at 168 MHz, while over UART at 115200 (264 bytes per frame) it takes 22.9 ms. The hardware CRYP on the F415/F417 spends 14 cycles on an AES-128 block in CTR (RM0090, Table 111) — less than a cycle per byte versus our hundreds of instructions; if speed matters, choose a chip with CRYP.


48г.7 Pitfalls

Encrypted means protected. Without a tag in CTR mode an attacker edits the image bit by bit without knowing the key (48г.1). Encryption answers the question "who can read it", not "who can substitute it".

A tag over the plaintext. Then, to check it, you must first decrypt — and the malleability of the ciphertext works before the check. The tag goes over the ciphertext and the header.

The same nonce. A nonce from a counter in RAM, a constant in the script, "the version number" instead of a random value — and on reuse the XOR of two releases shows the difference, and knowing one reveals the other (48г.2).

The version outside the tag. Someone raises the version in the header — and an old image passes the rollback protection. The tag covers all 32 bytes of the header fields.

The version checked before the tag. You may act on it only after dec_end: before the tag it is just a number from the link.

Comparing the tag with memcmp. An early exit at the first mismatching byte gives a hint from the response time of how many bytes were guessed. Always compare all 32 bytes.

The key in OTP with RDP 1. OTP is not erased when protection is removed, and at level 0 it is readable by the debugger (48г.4). With RDP 1 the key belongs in the bootloader's Flash.

The key in the application. The application is updated, debugged, and has bugs — the key does not belong there. Even a key in the bootloader can be read by the application on the F405: this chip has no PCROP.

One key for all devices. Break one and the images for all of them can be read and forged. Use a device key derived from the master key and the chip's unique ID.

A difference over ciphertexts. The ciphertexts of two versions are not alike — the difference is larger than the image (4334 bytes for a 4300-byte image). The difference is computed over the plain images, on the server, and is then encrypted itself.

Keys in RAM after reception. dec_t stays on the stack or in static memory and survives the jump to the application. Wipe the keys through volatile; the compiler will drop an ordinary memset.


48г.8 Summary

Full source code

aes48g.c — a compact AES, block encryption only.

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

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

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

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

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

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

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

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

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

sha48g.h — the SHA-256, HMAC and KDF interface.

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

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

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

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

#endif

sha48g.c — SHA-256, streaming HMAC-SHA-256 and a KDF based on HMAC.

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

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

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

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

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

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

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

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

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

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

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

dec48g.h — the encrypted image header and the reception interface.

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

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

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

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

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

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

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

#endif

dec48g.c — reception of the encrypted image in the bootloader.

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

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

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

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

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

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

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

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

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

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

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

mkenc48g.py — releasing the encrypted image on the computer.

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

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

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

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


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


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


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


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


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


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


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


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


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


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


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


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

Next: section 48д "The ST factory bootloader in system memory: entry via BOOT0 and from firmware, the USART protocol, your own flashing utility" — the bootloader that is already in every chip: how to get into it and how to talk to it with your own utility.

Open in the interactive handbook