This repository contains a secure bootloader for Atmel AVR microcontrollers along with an example application and Python‑based support tools. Before starting the application, the bootloader checks the signature and integrity of the encrypted firmware image so that only authorised software can run and the device cannot be compromised by fake or corrupted files.
- 🔒 Encryption: The firmware image is encrypted twice using a fixed AES‑128 key and an RSA‑2048 key pair; only someone with the private key can decrypt the file.
- 🧪 Integrity: CRC16‑CCITT is calculated for every packet and for the
entire image. Packets with wrong CRC or length are rejected with a
NACK. - 🔌 Serial update: The bootloader operates over RS485/USART and talks to a PyQt‑based flasher application.
- ✅ Image check: The bootloader checks the
BOOT_MAGICand CRC fields and jumps straight to the application if the image is valid. - 🔁 Shared memory: A
shared_memory_tstructure shares the boot command and baud rate between the bootloader and the application. - 📦 Modular directories: The code is organised into separate
Bootloader,App,CommonandToolsfolders.
The aim of this project is to produce a unique firmware image for each
customer. Only the holder of the RSA private key can decrypt the image. Because
encrypt_image.py uses a random AES‑256 key and IV, every build is unique even
for the same customer.
| Directory | Description |
|---|---|
Bootloader/ |
Bootloader code, UART and CRC drivers, the AES library (tiny‑AES‑c submodule) and a custom linker script. |
App/ |
Example application that places an image_header_t in the correct section and jumps to the bootloader when it receives |
the command BOOT\n. |
|
Common/ |
Shared definitions (image.h, crc.h, uart.h etc.) and data structures used by both the bootloader and the app. |
Tools/ |
Python scripts for encrypting, decrypting and flashing firmware images via the serial port, plus a PyQt5 GUI. |
Bootloader/tiny‑AES‑c/ |
A tiny C implementation of the AES algorithm used by the bootloader in CBC mode. |
The bootloader resides in the upper flash addresses (for example the .text
section starts at 0x7000). On reset the boot_main.c function runs and executes
these steps:
- Hardware initialisation: Interrupt vectors are relocated to the boot area.
The watchdog timer, LED, RS485 direction pins, a 1 ms timer and the UART are
set up. The
shared_area.boot_keyis cleared. - Application validation: The bootloader reads the
BOOT_MAGICand image CRC values from flash. If the image is valid andboot_keyis not set, it jumps directly to the application. - Update mode: If validation fails or
boot_keyis set, the bootloader waits on the serial port for an update. It sends aBOOT_CMD_HEADERcontaining the image header every 250 ms until a response is received. - Handshake: The host sends a
BOOT_CMD_INFOpacket. The bootloader takes the 16‑byte IV from this packet, initialises the AES context and replies with an ACK. - Data transfer: The host sends
BOOT_CMD_FLASHpackets containing the length, target offset and AES‑CBC encrypted data. The bootloader decrypts the payload, writes it to flash and responds with an ACK for correct packets or a NACK for incorrect packets. - Completion: When the last block is received, the bootloader calculates the CRC of the new image and compares it with the header value. If they match it jumps to the application; otherwise it resets.
- Reset: The host can send
BOOT_CMD_RESETat any time to reboot the device.
Every bootloader command starts with a start‑of‑text marker (STX), followed
by the command code and a length byte. The payload follows and finally the
16‑bit CRC. The packet format can be summarised as:
| STX (0xAA) | CMD | LEN | DATA[n] | CRC_L | CRC_H |
- STX (Start of Text): Constant 0xAA at the start of every packet.
- CMD: Command code (see table below). Values range from 0xB0 to 0xB7.
- LEN: Length of the
DATAfield in bytes. - DATA: Variable‑length data depending on the command (encrypted payload, offset, IV, etc.).
- CRC_L/CRC_H: Low and high bytes of the CRC16‑CCITT result.
Main command codes:
| Command | Code | Description |
|---|---|---|
BOOT_CMD_HEADER |
0xB0 |
Bootloader sends the current image header. |
BOOT_CMD_INFO |
0xB1 |
Host sends IV; bootloader responds with ACK and sets up AES. |
BOOT_CMD_FLASH |
0xB3 |
Encrypted data block and offset; bootloader decrypts and writes. |
BOOT_CMD_ACK |
0xB5 |
Acknowledgement for a valid packet. |
BOOT_CMD_NACK |
0xB6 |
Negative acknowledgement if length or CRC is wrong. |
BOOT_CMD_RESET |
0xB7 |
Reset the bootloader or MCU. |
Both the bootloader and the application share the same structure to hold image
metadata. It is defined in Common/image.h and placed in the .image_header
section. Fields:
magic(32‑bit): A constant (0xEFEFEFEF) indicating that the image is valid.sw_version: Software version (major, minor, revision, build).hw_version: Hardware version (major, minor, revision).compile_dateandcompile_time: Build date and time (__DATE__/__TIME__).avr_gcc_version: AVR‑GCC version used to build the image (__VERSION__).reserved: Reserved for future use.image_size: Size of the application in flash (read by the bootloader).crc: CRC16 of the application image. The bootloader verifies this after flashing. The field in the header is reserved for future use; integrity is verified using the two CRC bytes appended to the end of the image.
The linker scripts (Bootloader/Bootloader_Custom.ld and App/App_Custom.ld)
define the memory layout. Key points:
- Application (0x0000–0x6FFF): The application’s
.textstarts at the beginning of flash. At address 0x0000 is the interrupt vector table; the.image_header(88 bytes) is placed right after the vectors. The remaining space is used for application code. - Bootloader (0x7000–0x7FFF): The bootloader’s
.textsection starts at 0x7000 via the-section-start=.text=0x7000linker option so that the application cannot overwrite it. - Shared memory and .data (0x800000+):
Bootloader_Custom.ldplaces an eight‑byte.shared_memorysection at 0x800100 and.datastarts at 0x800108. Both the application and the bootloader access this area. - CRC:
encrypt_image.pyappends a two‑byte CRC to the end of the image. Thecrcfield inimage_header_tis reserved for later use; integrity is checked using the two trailing bytes.
Two‑level encryption is used to protect the firmware during updates, handled
automatically by Tools/encrypt_image.py:
- Read the compiled HEX file, update the
image_sizefield and calculate the image CRC, appending it to the end of the data. - Pad the data to 16‑byte blocks using PKCS#7.
- Encrypt the data with a fixed AES‑128 key and a randomly generated IV in AES‑CBC mode.
- Create a “secure block” containing the header and IV and encrypt it again with a random AES‑256 key.
- Encrypt this AES‑256 key and IV with the user‑supplied RSA‑2048 public key.
- The final file contains the RSA‑encrypted key/IV, the AES‑256‑encrypted
block and a SHA‑256 digest. The flasher uses the private key to reverse
these steps;
decrypt_image.pyis only for testing.
The fixed AES key is defined in the bootloader (Bootloader/bootloader.c)
as aes_key and appears in encrypt_image.py in hexadecimal. You can replace
this key if your security requirements demand it.
The example in App/ demonstrates normal operation:
- An
image_header_tinstance is placed in the.image_headersection and contains version and build information. - The
mainfunction toggles an LED and sends “Hello World!!\r\n” every 250 ms. - When “BOOT\n” is received over UART, it writes
BOOT_KEYintoshared_area.boot_keyand triggers a short watchdog reset to enter the bootloader.
The application’s Makefile automatically calls the Python script after
building to encrypt app_main.hex and produce an _encrypted.bin file, ready
for the flasher.
Encrypts a compiled .hex file into a .bin suitable for the bootloader. For
example:
python Tools/encrypt_image.py -f App/app_main.hex -k public.pem
Here public.pem is your 2048‑bit RSA public key; the output is
App/app_main_encrypted.bin.
Opens an encrypted .bin with an RSA private key, extracts the secure block
and shows the image contents as a hexdump. Because the flasher performs this
automatically via image_extract.py, this script is intended for testing only.
Tools/Flasher/main.py is a graphical interface written with PyQt5. With it
you can:
- Connect to a serial port and choose the baud rate.
- Use the Boot Mode button to send
BOOT\nand enter the bootloader; use the Reset button to restart the device. - Select an encrypted
.binand RSA private key to decrypt the header, view version/size/date/CRC information and load the firmware via the Flash button. - Monitor progress and status messages during the update.
You can also flash via the command‑line scripts. The flasher depends on
pyserial and PyQt5.
You can build this project in two ways: either open the .cproj files in
Microchip Studio on Windows or run make with avr‑gcc in each directory. In
both cases you need AVR‑GCC/avr‑libc installed, Python 3 with intelhex,
cryptography, pyserial and PyQt5 packages, and OpenSSL to generate keys.
cd Bootloader
make
The Makefile defines the target MCU, clock frequency and linker addresses.
The .text section starts at 0x7000; the resulting bootloader.hex can be
programmed into the device with your favourite programmer.
To ensure the MCU starts in the bootloader after reset, configure the fuses
appropriately. For example, on an ATmega328P set BOOTRST=0 (start from the
boot area) and BOOTSZ=00 (reserve a 4 KB boot section at 0x7000). You can
set these fuses via Microchip Studio or with avrdude.
cd App
make
The Makefile invokes encrypt_image.py after the build to create
_encrypted.bin, which the flasher can load directly.
A pair of RSA keys is required to load encrypted firmware. For example:
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -pubout -in private.pem -out public.pem
Use private.pem with the flasher and decrypt_image.py; provide
public.pem to encrypt_image.py. Keep your private key in a safe place.
- While the normal application is running, send
BOOT\nto switch to boot mode. - Run
python Tools/Flasher/main.py, select the correct serial port and baud rate. - Click Connect and then Boot Mode; the bootloader will start sending header packets.
- Choose your encrypted
.binfile and the RSA private key; the flasher will display the header information. - Click Flash to start the update; when finished the device will run the new application.
- AES key: The default 16‑byte key is defined in
bootloader.candencrypt_image.py. If you want to use your own key, update both files consistently. - Boot command and key: Modify the
BOOT_COMMANDandBOOT_KEYconstants inCommon/image.hto customise the mechanism for switching between the application and the bootloader. - Memory addresses: The bootloader and application address ranges are set in the Makefiles; adjust them for different MCU types or bootloader sizes.
- Serial communication: UART and RS485 parameters are defined in
uart.handboot_main.c. You can select a different baud rate usingshared_area.baud_rateand by modifying the Makefile.
All keys in this project are examples. For a secure product:
- Generate a unique RSA key pair for each customer and keep the private key secret.
- Replace the hard‑coded AES‑128 key in the bootloader and the
AES_KEYconstant in the script with your own values. - Use different keys for different customers so that images cannot be substituted between devices.
- You are responsible for any security issues that arise while using this project.
Planned improvements for this repository:
- Optimise the bootloader to reduce its flash usage and free space for new features.
- The
baud_ratefield inshared_areais currently unused; it should either be used to exchange the baud rate between the app and bootloader or removed. - Add error codes to
NACKpackets for better error handling. - Rewrite the flasher to improve code quality.
- Add timeout checks to handle lock‑up situations automatically.
This project is released under the MIT Licence; see the LICENSE file for
details. Contributions and pull requests are welcome. The project is for
educational purposes; for commercial use you should perform your own security
analysis.