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

48а. Firmware signing: the bootloader checks who built it

The bootloader from section 48 checks the image with CRC-32, and that protects only against accidental corruption. Anyone who changes the firmware can recompute the CRC in a second, and the bootloader will dutifully start it. To make the device start only your firmware, you sign it: on the computer with a private key, and in the bootloader you verify it with the public key. A signature cannot be forged without the private key, and the private key is not in the device at all.

This section is an experiment with the mbedTLS 3.6.4 library from the pack: an ECDSA signature on the P-256 curve with a SHA-256 hash, five attempts to fool the bootloader, a cross-check with another library, and the size of signature verification in Cortex-M4 firmware. The programs are in _verify/sboot48a/. The check is _verify/sboot48a_check.py; the verification time is in _verify/fixw2a0/sig48 and _verify/fixw2a0_check.py.

Terms used in the documentation

Term Meaning
digital signature a digital signature
private key, public key the private key and the public key
ECDSA, curve P-256 (secp256r1) a signature on the elliptic curve P-256
message digest, SHA-256 a hash (digest), SHA-256
deterministic ECDSA (RFC 6979) a deterministic signature: the same data give the same signature
anti-rollback, security counter protection against rolling back to an old version
root of trust the root of trust: what checks first and never changes itself
chain of trust the chain of trust

48а.1 The scheme

Where What it holds What it does
the developer's computer or the build server private key (32 bytes) computes the SHA-256 of the image, signs the hash
the firmware image header + body + signature (64 bytes) travels to the device by any route
the bootloader in the device public key (65 bytes) and the lowest allowed version computes the hash, verifies the signature, checks the version

The key point: the header is signed too. Otherwise the version, size and start address could be replaced without touching the signed body.

The public key is not a secret; you can show it to everyone. The secret is the private key, and it must get neither into the device nor into the repository.


48а.2 Experiment: a signature and five attempts to cheat

Signed image: the header and the body are under the hash, the signature is at the end; the bootloader verifies the signature, then the version

The image: a header {magic FWSG, version 3, size} and 4096 bytes of body, in total 4108 bytes. The key is derived deterministically (SHA-256 of a string; this is done only in the experiment so that the run is repeatable; a product key is generated from a real random source and kept outside the repository), and the signature follows RFC 6979, where the random number is derived from the key and the hash, so the run gives the same bytes every time:

sha256=698c2f41db06f429f74bbcd60f5c2b38c00535e9b1062e0f719be752087f12ce
signature=c38bdb835bfe9f6cd4356d36ad82e5506097c047ea1600ae51cfbdb664e3d8bd
          bc345987fa0c4e9884f892ce87f659ffbc70c2ab8e1c1112070e18c90e8e3865

The check in the bootloader; the order matters:

if (h.magic != 0x47535746u)
    return BAD_MAGIC;
if (h.size > slot - sizeof h)          /* размер ещё не проверен подписью: сначала граница области */
    return BAD_SIZE;
uint8_t hash[32];
mbedtls_sha256(img, sizeof h + h.size, hash, 0);

then mbedtls_ecdsa_verify and only after that the version comparison.

Before the signature is verified, the bootloader takes from the header only what the hash cannot be computed without, the size, and immediately checks it against the boundary of the area slot. Otherwise a forgery with a huge size makes the bootloader read past the end of Flash, and it crashes with a bus fault. The experiment checks this: the bootloader rejects the size 0xFFFFFF00 before hashing.

The original image and five forgeries:

What was done to the image Bootloader verdict
nothing start
one bit of the body changed signature mismatch
one bit of the signature changed signature mismatch
version in the header raised to 9 without re-signing signature mismatch
verified with someone else's public key signature mismatch
an old image of version 2, honestly signed with the same key version below the allowed one

The last case is a rollback: the attacker holds an old, genuine, correctly signed firmware with a known vulnerability. The signature lets it through: it really is yours. Without the version comparison the bootloader would start it, and the experiment showed that too. That is why the lowest allowed version is stored in the device and only grows: it is raised after a successful update.

And the order: the version is compared after the signature is verified; before that the number in the header cannot be trusted.

Signed image: SHA-256 covers the header and the body together, the 64-byte signature is separate, the bootloader holds the 65-byte public key and the lowest allowed version; of six experiments only the untouched image starts, the signature rejects four forgeries, and the old honestly signed image of version 2 is caught only by the version comparison


48а.3 Verification with another library

The signature made by mbedTLS was verified with the cryptography library (Python, based on OpenSSL), and the verification passed. Moreover, this library can sign according to the same RFC 6979, and its signature of the same image with the same key matched the mbedTLS signature byte for byte. The public key computed by both from the same private key matched as well.

This matters in practice: signing is usually done on the build server (OpenSSL, Python, vendor utilities), and verification in the device (mbedTLS). The format, two halves of 32 bytes each (r and s), must match; many utilities output the signature by default in DER format, and you have to split it into r and s.


48а.4 What it costs in the firmware

The same verification was built for Cortex-M4 (arm-none-eabi-gcc -Os, a trimmed mbedTLS config: only big numbers, the P-256 curve, ECDSA and SHA-256):

Program Flash (code + constants)
without signature verification 5780 bytes
with signature verification 23028 bytes
difference 17248 bytes

About 17 KB is a noticeable but manageable part of a bootloader. mbedTLS allocates memory for big numbers through calloc, so the bootloader needs a heap, or its own memory allocation function.

Time. The emulator does not count cycles, but it counts instructions exactly. In this trimmed config the verification takes 99.5 million instructions. Two lines from the regular mbedTLS build, MBEDTLS_ECP_NIST_OPTIM (fast modular reduction for the NIST curves) and MBEDTLS_HAVE_ASM (big-number multiplication in assembly), cut it to 17.1 million instructions on Cortex-M4 and 18.4 million on Cortex-M3 at the cost of half a kilobyte of Flash. An instruction costs one cycle or more, so this is a lower bound: on the F407 at 168 MHz it is at least 0.1 s, on the F103 at 72 MHz at least 0.26 s, and in the trimmed config 1.4 s. The real time is measured on the board with the cycle counter DWT->CYCCNT (section 21.9). It is added to every start-up and must fit into the bootloader's watchdog interval.

For series with TrustZone there are ready-made solutions: for example, for the L5 the installed packs contain the TF-M platform (STM32L5xx_TFM-PF) with image verification. For some families without TrustZone there is the vendor's secure boot package (X-CUBE-SBSFU) and open-source bootloaders with signature verification; you write your own, as here, when their size or design does not fit.


48а.5 Where to keep the public key

The key itself is not a secret, but replacing it is a complete win for the attacker: having replaced the key with his own, he can sign anything. Therefore:

The private key is kept outside the repository, preferably in a hardware store on the build server; losing the key means you have nothing left to update the shipped devices with.


48а.6 Pitfalls

Only the body was signed. The version in the header is changed without re-signing, and a rollback gets through. Hash the header and the body together.

The version was checked before the signature. A number from unsigned data can be anything. The signature first, then the version.

No rollback protection. An old signed firmware with a hole starts: the experiment showed "start" without the version comparison.

The signature "does not match" although everything is right. The server outputs DER, while the bootloader expects r‖s of 32 bytes each, or the other way round.

The hash is computed over the wrong bytes. Alignment, padding to a page, different body boundaries in the signer and in the bootloader. Take the boundaries from the signed header.

A forgery with a huge size crashes the bootloader. The size from a not yet verified header was not checked against the area boundary, and the hash reads past the end of Flash.

The private key is in the repository. It will leak, and the signature means nothing.

The bootloader has no write protection. The key is replaced together with the bootloader.

The bootloader ran out of memory. mbedTLS allocates memory for numbers through calloc: you need a heap.


48а.7 Summary

Full sources

sboot.c: image signing and verification in the bootloader on mbedTLS, five attempts to cheat.

/* Раздел 48а: подпись прошивки ECDSA P-256 + SHA-256 на mbedTLS 3.6.4.
 *
 * Образ: заголовок {магия, версия, размер} + тело. Подписывается хеш заголовка и тела вместе.
 * Подписант (компьютер) держит закрытый ключ; загрузчик (прибор) — только открытый и минимальную версию.
 * Ключ и подпись детерминированы: закрытый ключ — SHA-256 от строки, подпись — RFC 6979, поэтому
 * прогон каждый раз даёт одни и те же байты, и их можно сверить посторонней библиотекой. */
#include <stdint.h>
#include <stdio.h>
#include <string.h>

#include "mbedtls/ecdsa.h"
#include "mbedtls/ecp.h"
#include "mbedtls/hmac_drbg.h"
#include "mbedtls/md.h"
#include "mbedtls/sha256.h"

#define BODY 4096

struct hdr { uint32_t magic, version, size; };

static uint8_t image[sizeof(struct hdr) + BODY];

static void hex(const char *name, const uint8_t *p, size_t n)
{
    printf("%s=", name);
    for (size_t i = 0; i < n; i++)
        printf("%02x", p[i]);
    printf("\n");
}

static void build(uint32_t version)
{
    struct hdr h = {0x47535746u /* 'FWSG' */, version, BODY};
    memcpy(image, &h, sizeof h);
    for (int i = 0; i < BODY; i++)
        image[sizeof h + i] = (uint8_t)(i * 7 + 3);          /* «прошивка» — любой повторяемый узор */
}

/* ── подписант ── */
static mbedtls_ecp_group grp;
static mbedtls_mpi d;                  /* закрытый ключ */
static mbedtls_ecp_point Q;            /* открытый ключ */
static mbedtls_hmac_drbg_context drbg; /* только для защиты умножения точки, на результат не влияет */

static void keygen(const char *seed, mbedtls_mpi *dk, mbedtls_ecp_point *Qk)
{
    uint8_t h[32];
    mbedtls_sha256((const uint8_t *)seed, strlen(seed), h, 0);
    mbedtls_mpi_read_binary(dk, h, 32);
    mbedtls_mpi_mod_mpi(dk, dk, &grp.N);
    mbedtls_ecp_mul(&grp, Qk, dk, &grp.G, mbedtls_hmac_drbg_random, &drbg);
}

static void sign(uint8_t sig[64])
{
    uint8_t h[32];
    mbedtls_mpi r, s;
    mbedtls_mpi_init(&r);
    mbedtls_mpi_init(&s);
    mbedtls_sha256(image, sizeof image, h, 0);
    mbedtls_ecdsa_sign_det_ext(&grp, &r, &s, &d, h, 32, MBEDTLS_MD_SHA256, mbedtls_hmac_drbg_random, &drbg);
    mbedtls_mpi_write_binary(&r, sig, 32);
    mbedtls_mpi_write_binary(&s, sig + 32, 32);
    mbedtls_mpi_free(&r);
    mbedtls_mpi_free(&s);
}

/* ── загрузчик ── */
enum verdict { BOOT_OK, BAD_MAGIC, BAD_SIZE, BAD_SIGNATURE, TOO_OLD };
static const char *const VERDICT[] = {"запуск", "не образ", "размер больше области", "подпись не сошлась", "версия ниже разрешённой"};

/* slot — размер области, где лежит образ: размер из заголовка до проверки подписи — только в её пределах */
static enum verdict boot_check(const uint8_t *img, size_t slot, const uint8_t sig[64], const mbedtls_ecp_point *pub,
                               uint32_t min_version)
{
    struct hdr h;
    memcpy(&h, img, sizeof h);
    if (h.magic != 0x47535746u)
        return BAD_MAGIC;
    if (h.size > slot - sizeof h)          /* размер ещё не проверен подписью: сначала граница области */
        return BAD_SIZE;
    uint8_t hash[32];
    mbedtls_sha256(img, sizeof h + h.size, hash, 0);
    mbedtls_mpi r, s;
    mbedtls_mpi_init(&r);
    mbedtls_mpi_init(&s);
    mbedtls_mpi_read_binary(&r, sig, 32);
    mbedtls_mpi_read_binary(&s, sig + 32, 32);
    int rc = mbedtls_ecdsa_verify(&grp, hash, 32, pub, &r, &s);
    mbedtls_mpi_free(&r);
    mbedtls_mpi_free(&s);
    if (rc != 0)
        return BAD_SIGNATURE;
    if (h.version < min_version)          /* проверяется ПОСЛЕ подписи: версии без подписи верить нельзя */
        return TOO_OLD;
    return BOOT_OK;
}

int main(void)
{
    mbedtls_ecp_group_init(&grp);
    mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1);
    mbedtls_mpi_init(&d);
    mbedtls_ecp_point_init(&Q);
    mbedtls_hmac_drbg_init(&drbg);
    mbedtls_hmac_drbg_seed_buf(&drbg, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), (const uint8_t *)"48a", 3);
    keygen("handbook-48a signing key", &d, &Q);

    uint8_t pub[65];
    size_t plen;
    mbedtls_ecp_point_write_binary(&grp, &Q, MBEDTLS_ECP_PF_UNCOMPRESSED, &plen, pub, sizeof pub);
    hex("pubkey", pub, plen);

    build(3);
    uint8_t h[32], sig[64], sig_v2[64];
    mbedtls_sha256(image, sizeof image, h, 0);
    printf("image_bytes=%zu\n", sizeof image);
    hex("sha256", h, 32);
    sign(sig);
    hex("signature", sig, 64);
    uint8_t sig2[64];
    sign(sig2);
    printf("deterministic=%d\n", memcmp(sig, sig2, 64) == 0);
    FILE *f = fopen("/tmp/image.bin", "wb");
    if (f) { fwrite(image, 1, sizeof image, f); fclose(f); }

    const uint32_t MINV = 3;
    printf("case original: %s\n", VERDICT[boot_check(image, sizeof image, sig, &Q, MINV)]);
    image[sizeof(struct hdr) + 1000] ^= 0x01;
    printf("case one_bit_in_body: %s\n", VERDICT[boot_check(image, sizeof image, sig, &Q, MINV)]);
    image[sizeof(struct hdr) + 1000] ^= 0x01;
    sig[40] ^= 0x80;
    printf("case one_bit_in_signature: %s\n", VERDICT[boot_check(image, sizeof image, sig, &Q, MINV)]);
    sig[40] ^= 0x80;
    image[4] = 9;                                       /* подняли версию в заголовке, не переподписав */
    printf("case version_patched: %s\n", VERDICT[boot_check(image, sizeof image, sig, &Q, MINV)]);
    image[4] = 3;
    {
        mbedtls_mpi d2;
        mbedtls_ecp_point Q2;
        mbedtls_mpi_init(&d2);
        mbedtls_ecp_point_init(&Q2);
        keygen("someone else's key", &d2, &Q2);
        printf("case other_public_key: %s\n", VERDICT[boot_check(image, sizeof image, sig, &Q2, MINV)]);
    }
    {
        const uint32_t huge = 0xFFFFFF00u;               /* подделали размер: загрузчик не должен читать за областью */
        const uint32_t body = BODY;
        memcpy(image + 8, &huge, sizeof huge);
        printf("case huge_size: %s\n", VERDICT[boot_check(image, sizeof image, sig, &Q, MINV)]);
        memcpy(image + 8, &body, sizeof body);
    }
    /* откат: старый образ версии 2 — подписан тем же ключом и честно */
    build(2);
    sign(sig_v2);
    printf("case old_signed_v2: %s\n", VERDICT[boot_check(image, sizeof image, sig_v2, &Q, MINV)]);
    printf("case old_signed_v2_without_version_check: %s\n", VERDICT[boot_check(image, sizeof image, sig_v2, &Q, 0)]);
    printf("sizes pubkey=%zu signature=64 hash=32\n", plen);
    return 0;
}

Next: section 48б "MCUboot on STM32: two slots, image swap and rollback" — a ready-made bootloader: slots, swap through scratch, a trailer with flags, and rollback.

Open in the interactive handbook