Secure Firmware Upgrade Bootloader and Application

From Stm32World Wiki
Jump to navigation Jump to search

This page will document how to implement a secure firmware update.

NOTICE! Do not rely on the code on this page or in the repository for production. This is meant to document the principles.

This page expands on an earlier DFU bootloader, documented here: Custom DFU Bootloader

Flash Partitioning options

In principle, two approaches are viable. Either the bootloader will occupy the first part of the flash, or it will occupy the end of the flash.

If locating the bootloader in the beginning of the flash, the layout will look like this:

Bootloader beginning.jpeg
  Memory Address       Memory Region / Sector                               Size
+--------------------+---------------------------------------------------+ --------
|  0x0807 FFFF       |                                                   |    ^
|        :           |  User Application Area                            |    |
|        :           |  (Upper Flash Space)                              |  448 KB
|        :           |                                                   |    |
|  0x0801 0000       |                                                   |    v
+--------------------+---------------------------------------------------+ --------
|  0x0800 FFFF       |  Bootloader Code / Vector Table                   |    ^
|        :           |  (Lower Flash Space)                              |   64 KB
|  0x0800 0000       |                                                   |    v
+--------------------+---------------------------------------------------+ --------

However, if placing the bootloader towards the end of the flash, the layout will look like this:

Bootloader end.jpeg
  Memory Address        Memory Region / Sector                             Size
+--------------------+---------------------------------------------------+ --------
|  0x0807 FFFF       |  Bootloader Code                                  |    ^
|        :           |  (Upper Flash Space)                              |   64 KB
|  0x0807 0000       |                                                   |    v
+--------------------+---------------------------------------------------+ --------
|  0x0806 FFFF       |                                                   |    ^
|        :           |  User Application Area                            |    |
|        :           |  (Lower Flash Space)                              |  448 KB
|        :           |                                                   |    |
|  0x0800 0000       |  (Application Vector Table & Entry Point)         |    v
+--------------------+---------------------------------------------------+ --------

Signing firmware

The signature will be stored in a 128 byte block which will be merged with the actual firmware binary. The format of that structure is like this:

typedef struct {
    uint32_t magic;          // 4 bytes: Magic identifier (e.g. 0x4150504B / "KAPP")
    uint32_t payload_len;    // 4 bytes: Size of binary firmware in bytes
    uint8_t signature[64];  // 64 bytes: ECDSA raw signature (32-byte R + 32-byte S)
    uint8_t reserved[56];   // 56 bytes: Zero padding to align header to 128 bytes
} Header128_t;

First step will be to generate a private key:

openssl ecparam -name prime256v1 -genkey -noout -out private_key.pem

This key must be kept private. Anyone gaining access to it will be able to sign their own firmware.

After generating a private key, a public key can be derived:

openssl ec -in private_key.pem -pubout -out public_key.pem

This key is public and the actual contents of the public_key.pem is included in the code.

Signing a binary takes multiple steps and for ease of use we've created a shell script:

#!/usr/bin/env bash
set -euo pipefail

# Configurable paths
FIRMWARE_BIN="${1:-app.bin}"
PRIVATE_KEY="${2:-private_key.pem}"
OUTPUT_BIN="${3:-app_signed.bin}"

# Check required files
if [[ ! -f "$FIRMWARE_BIN" ]] || [[ ! -f "$PRIVATE_KEY" ]]; then
    echo "Usage: $0 <firmware.bin> <private_key.pem> [output.bin]"
    exit 1
fi

TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT

echo "[1/4] Calculating SHA-256 hash and ECDSA signature..."
# 1. Output DER signature from OpenSSL
openssl dgst -sha256 -sign "$PRIVATE_KEY" -out "$TEMP_DIR/sig.der" "$FIRMWARE_BIN"

# 2. Extract raw R and S components from DER signature into raw 64-byte binary
openssl asn1parse -in "$TEMP_DIR/sig.der" -inform DER > "$TEMP_DIR/sig.asn1"

# Extract hex for integer R and S
R_HEX=$(openssl asn1parse -in "$TEMP_DIR/sig.der" -inform DER | awk -F':' '/INTEGER/ {print $4}' | sed -n '1p')
S_HEX=$(openssl asn1parse -in "$TEMP_DIR/sig.der" -inform DER | awk -F':' '/INTEGER/ {print $4}' | sed -n '2p')

# Pad or strip lead zeroes so each is exactly 32 bytes (64 hex characters)
R_HEX=$(printf "%064s" "${R_HEX#00}" | tr ' ' '0' | tail -c 64)
S_HEX=$(printf "%064s" "${S_HEX#00}" | tr ' ' '0' | tail -c 64)

# Convert R + S hex to raw binary (64 bytes)
echo -n "${R_HEX}${S_HEX}" | xxd -r -p > "$TEMP_DIR/sig_raw.bin"

echo "[2/4] Building 128-byte Header..."
# Magic Bytes: "KAPP" (0x4150504B in Little-Endian)
printf '\x4B\x50\x50\x41' > "$TEMP_DIR/header.bin"

# Payload Length: 4-byte Little-Endian uint32
PAYLOAD_SIZE=$(stat -c%s "$FIRMWARE_BIN" 2>/dev/null || stat -f%z "$FIRMWARE_BIN")
python3 -c "import struct, sys; sys.stdout.buffer.write(struct.pack('<I', $PAYLOAD_SIZE))" >> "$TEMP_DIR/header.bin"

# Append 64-byte Raw Signature
cat "$TEMP_DIR/sig_raw.bin" >> "$TEMP_DIR/header.bin"

# Append 56-byte zero padding to complete the 128-byte header
dd if=/dev/zero bs=1 count=56 2>/dev/null >> "$TEMP_DIR/header.bin"

echo "[3/4] Concatenating Header and Firmware Payload..."
cat "$TEMP_DIR/header.bin" "$FIRMWARE_BIN" > "$OUTPUT_BIN"

echo "[4/4] Done!"
echo "Generated Signed Firmware : $OUTPUT_BIN"
echo "Original Payload Size     : $PAYLOAD_SIZE bytes"
echo "Final Image Size          : $(stat -c%s "$OUTPUT_BIN" 2>/dev/null || stat -f%z "$OUTPUT_BIN") bytes"

This script can be used to sign the output binary:

lth@ncpws04:~/src/stm32fun/examples/sl_mcu_stm32f407_bootloader_signed_app$ ./package_firmware.sh Debug/sl_mcu_stm32f407_bootloader_signed_app.bin 
[1/4] Calculating SHA-256 hash and ECDSA signature...
[2/4] Building 128-byte Header...
[3/4] Concatenating Header and Firmware Payload...
[4/4] Done!
Generated Signed Firmware : app_signed.bin
Original Payload Size     : 61900 bytes
Final Image Size          : 62028 bytes

STM32CubeMX Pinout

The pinout of the STM32F407 has been configured like this:

STM32F407 CubeMX Pinout.png

TinyUSB DFU

TinyUSB src added to project.png

The earlier approach was using HAL USB DFU, but as ST in their wisdom have decided that they couldn't be bothered to support all their MCUs any longer, I am going to use TinyUSB for this example. The src/ directory needs to be copied or linked in the project directory (as seen on the right).

This folder will have to be added both as "include" and as "source" in the project properties.


MbedTLS Signature Checking

To be added

Miscellaneous links