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
| 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:
- SHA-256 and HMAC are already in the bootloader (198щ). GCM also needs GHASH — multiplication in a field of 2^128 elements: new code and new tables.
- Nonce reuse in GCM also breaks the tag (43.4): from two messages the tag can be forged without the key. HMAC does not depend on the nonce — reuse reveals the text but does not let anyone forge the image.
- The tag can be checked without decrypting: it needs only the key
k_mac. This lets you keep ciphertext in the buffer slot if you want to. - CTR is a stream: the cipher runs only in the forward direction, no padding to 16 bytes is needed, decryption goes in chunks of any length as data arrives — no buffer for the whole image is needed.
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:
- code running from Flash reads everything (Table 17), including the key in the bootloader sector. A bug in the application that lets memory be read on external request also leaks the key. PCROP — protection of a sector against data reads — exists only on the F42x/F43x (3.7.5); the F405 does not have it;
- the F103 has no second RDP level at all (11.6): the key is protected only by the first;
- researchers have bypassed the first level of readout protection on the silicon of a number of STM32 families. That is why a device key, not a batch key: a bypass on one device costs the key of one device.
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
- CRC protects against corruption, HMAC and signature against substitution, encryption against reading on the link and in the file. Encryption without a tag does not catch substitution: a flipped bit of CTR ciphertext flips exactly one bit of the firmware.
- The mode is AES-128-CTR with an HMAC-SHA-256 tag over the 32 bytes of header fields and the ciphertext; the header is 64 bytes; the tag is checked before writing to the active slot.
- The nonce is 12 random bytes per release. If it is reused with the same key, v2 is recovered from v1 completely, 4300 bytes out of 4300; with a key derived from the header, the reuse gives 13 random zeros out of 4300.
- Our own AES in C matched FIPS-197, three RFC 3686 vectors and the cryptography library on 3000 random messages; the F405 and F407 have no CRYP block.
- The device key is an HMAC of the master key and the chip's unique ID; the master key is only on the server. On the F405 the key is in bootloader sector 0 under WRP and RDP; the 512-byte OTP survives removing RDP, so it is suitable only with RDP 2.
- Rejections: a body bit, a raised version, another device, a batch key, a truncated and an extended image, replay and rollback — the active slot stays at v1.
- The 48в difference is computed over the plain images and encrypted: 114 bytes versus 4334 over ciphertexts.
- Cost: 2690 bytes of code on Cortex-M3, 520 bytes of RAM, about 406 instructions per byte received — at least 0.63 s for 256 KB at 168 MHz.
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.