rustBoot

by Nihal Pasham

rustBoot is a standalone bootloader, written entirely in Rust, designed to run on anything from a microcontroller to a system on chip. It can be used to boot into bare-metal firmware or Linux.

Why rustBoot?

rustBoot prioritizes the following above all else.

  • keep only the bare-essentials
  • secure by default
  • low-integration complexity

rustBoot does the bare minimum needed to securely boot bare-metal firmware (or Linux) i.e. it has a really small trusted computing base. It is secure by default i.e. it does not boot digitally unsigned firmware and uses memory-safe implementations (for crypto and boot-logic) as the default. It also attempts to eliminate the high degree of integration-complexity involved in rolling a production-grade bootloader by adopting a batteries-included approach.

  • For example, we include flash device drivers for all supported boards, written in safe Rust.

Why prioritize the above?

Trusted Computing Base:

Open-source bootloaders have a large trusted computing base i.e. they (pretty much) resemble a mini operating system with

  • a complete networking stack
  • a collection of device drivers and device-tree blob(s)
  • integrated debug and command shells
  • support for every possible filesystem you can think of.  
  • and more stuff.

Note: This includes U-boot, the de-facto standard in the embedded-systems world. DepthCharge is an example of a U-Boot hacking toolkit for security researchers and tinkerers, designed to exploit U-boot's large attack surface.

mental_map_uboot_attack_surface

Memory safety:

A large TCB inevitably equates to a large attack surface. The vast majority of them are written in C or some combination of C and Assembly. A quick analysis of CVEs reported over the last 2 years (in u-boot, bare-box and other open-source ones) show that the bulk of them fall into the memory-safety category.

Note: addressable attack surface is much larger, the above attack surface is only compounded when we add boot-time driver vulnerabilities.

Complexity & boot-time:

Custom secure boot implementations can get quite complex and add latency with

  • redundant hierarchical digital signature verification trust chains or
  • elaborate parsing of custom header or container formats.

Vendor dependencies:

Vendor-specific or custom chain of trust dependencies make it difficult to port bootloader implementations across boards. This is in-part attributable to non-standards based solutions.

Goals

Contrary to prevailing wisdom, writing your own secure-bootloader is a complex project. The effort involved in developing/integrating one can be overwhelming. For example, we'll need to address a plethora of accompanying tasks such as key-management, signing infrastructure, code-safety, trust-chains, reliable back-ups etc. before we even get to the actual booting logic.

rustBoot's purpose is to help simplify the entire process. Its primary goals are

  • complies with key requirements of the IETF-SUIT standard i.e.
    • one of SUIT's requirements - transferring or downloading an update should be delegated to the firmware/OS to avoid size or computational limitations. In other words, the bootloader should NOT be required to download and install an update. This removes the need for a networking stack and provides for a drastic reduction in the bootloader's attack surface.
    • SUIT also does not mandate the use of specific protocols or data link interfaces to transfer updates to a device.
    • rustBoot fully complies with this requirement.
  • reliable updates:
    • reliable updates in rustBoot will take the form of
      • flash swap operations for microcontroller based systems. We'll use the boot/update based multi-slot partitioning method to replace currently active firmware with a newly received update and at the same time store a back-up copy of it in a (passive) secondary partition.
      • ram swap operations for more powerful system-on-chip boards which can boot Linux.
  • predictability over performance:
    • one of rustBoot's core design objectives is to keep it simple and avoid complexity. So, there will be little to no application of meta or async programming constructs.

    Note: We don't actually need the extra performance. rustBoot can hit sub-second secure boot-times as we've stripped it down to the bare-essentials. This assumes flash load times are fast enough and a firmware binary-blob size of < 1MB.

  • zero-dynamic memory allocation:
    • to make it highly portable, apart from its modular design, rustBoot relies on a zero dynamic memory allocation architecture i.e. no heap required.
  • memory safety & type-state programming:
    • the entire bootloader is written in rust's safe-fragment with a limited set of well-defined api(s) for unsafe HW access.
    • as a consequence, it makes rustBoot immune to a whole host of memory safety bugs. ex: things like parsing image-headers (i.e. container-formats) in rustBoot is much safer.
    • rustBoot takes advantage of rust's powerful type-system to make invalid boot-states, unrepresentable at compile time and along with constructs such as sealed states, global singletons, it improves the overall security of the entire code-base.

There is a plan to further add to rustBoot's high levels of assurance by leveraging formal methods such as

  • property-based testing via symbolic execution: to formally verify rustBoot's parser.
  • deductive verification: for critical sections of code (ex: swapping contents of boot and update partitions).

Features currently supported:

  • support for ARM Cortex-M, Cortex-A micro-architectures
  • support for multi-slot partitioning of microcontroller flash memory. This allows us to implement the boot/update approach for bare-metal firmware updates.
  • support for Aarch64 linux booting
  • elliptic curve cryptography for integrity and authenticity verification using RustCrypto crates
  • a tiny hardware abstraction layer for non-volatile memory (i.e. flash) access.
  • anti-rollback protection via version numbering.
  • a fully memory safe core-bootloader implementation with safe parsers and firmware-update logic.
  • power-interruptible firmware updates along with the assurance of fall-back availability.
  • a signing utility to sign bare-metal firmware and fit-image(s), written in pure rust.

Features planned:

  • support for external flash devices (ex: SPI flash) and serial/console logging interfaces.
  • support for ARM TrustZone-M and A and certified secure hardware elements - microchip ATECC608a, NXP SE050, STSAFE-100
  • support for a highly secure and efficient firmware transport method over end-end mutually authenticated and encrypted channels via ockam-networking-libraries.

Design

rustBoot aims to offer an OS and micro-architecture agnostic (i.e. highly portable) standards-based secure bootloader that's easy to integrate into existing embedded software projects. Its architecture is designed around a simple idea – a bootloader handles ONLY the bare-essentials and offloads the rest to systems that are better suited for the job.

What does a bootloader actually do?

  1. it initializes (the bare-minimum) requisite hardware. Ex: cpu-core, flash, gpios, uart, (RAM and hardware secure elements if needed).
  2. digitally verifies or authenticates firmware.
  3. boots or passes control over to firmware or an OS i.e. linux or RTOS or bare-metal and
  4. if an update is available, it validates and applies the update before performing a re-boot.

In this chapter, we'll talk about rustBoot's design and its core components.

Components of rustBoot

At its core, rustBoot is comprised of 4 components

  • the core bootloader
  • a minimal hardware abstraction layer
  • fast and safe crypto drivers
  • rustBoot firmware interface

The core bootloader

  • has a tiny trusted computing base i.e. its less than 32KB in size when compiled to an executable.
  • this includes signature-based authentication, reliable firmware updates with rollbacks and protections against downgrades attacks.

A minimal hardware abstraction layer

rustBoot provides abstractions for the following hardware classes i.e. it exposes a tiny API for you to easily integrate the following types of hardware.

  • flash memory controllers: NVMC, SPI-flash, EMMC block devices etc.
  • TrustZone: Cortex-M or Cortex-A
  • serial interfaces: UART(s), GPIO(s)

Note: To minimize integrational complexity and enhance security, we already provide a number of different hardware drivers written in safe-rust. So, you can use your own drivers using rust-ffi or use existing ones from the repo.

Fast and safe crypto drivers

  • hardware secure elements or accelerators: again, rustBoot offers drivers for crypto hardware or you can use your own.
    • examples of supported vendor-specific crypto modules include ATECC608a.
  • software implementations of crypto-libraries: rustBoot uses the RustCrypto project as its software crypto provider.
    • This includes all crates in the rustcrypto project - hashing, signing, verification, encryption etc.

rustBoot firmware interface

  • rustBoot complies with a key requirement of the IETF-SUIT standard and does not include a networking stack, instead networking is offloaded to the underlying firmware/OS.
  • Firmware updates are downloaded and stored in non-volatile storage.
  • In order to trigger the update, rustBoot provides a simple API that can be called from within bare-metal firmware or linux.

Note: In the above context, firmware refers to either linux or bare-metal firmware.

High Level Overview

rustBoot's architecture reflects its focus on simplicity and security, above everything else.

For a high-level overview, you can think of rustBoot as operating in 2 independent stages.

  • Pre-handover stage: post power-on, the BootROM (or some other intermediate-stage bootloader) executes and hands control over to rustBoot. This is a stage where rustBoot has full execution control.
  • Post-handover stage: firmware has begun executing and has complete execution control. Firmware uses a couple rustBoot dependencies to trigger and confirm updates.

Pre-handover stage:

  • rustBoot provides a minimal hardware abstraction layer for a wide range of ARM microcontrollers (STM32, Nordic, Microchip etc.) and microprocessors (rpi4, NXP etc.). The HAL allows peripherals drivers to initialize requisite hardware such as flash memories, UART controllers, GPIO pins etc.
  • an optional software-based crypto library in-case you don't need (or use) dedicated crypto hardware.
  • rustBoot's core-bootloader houses all of the actual boot-logic such as
    • firmware image integrity and authenticity verification via digital signatures
    • power-interruptible firmware updates along with the assurance of fall-back availability.
    • FIT-Image and device tree parsing while booting linux.
    • multi-slot partitioning of microcontroller flash memory
    • anti-rollback protection via version numbering.

pre_handover_stage

Post-handover stage:

  • At this stage, control has been handed over to firmware (or linux).
  • rustBoot does not have a networking stack. The job of downloading and installing an update is offloaded to firmware or linux (drastically reducing the TCB)
  • Firmware can trigger and confirm updates by setting the state of the update partition via a rustBoot api. This removes the need for a filesystem (again smaller TCB).
    • However, not all systems can boot without a file-system.
    • If you need one, rustBoot offers a FAT 16/32 implementation, written in safe rust.
  • Once an update is triggered, the device is reset (i.e. restarted). rustBoot takes over and attempts to verify the update. If everything checks out, it boots the updated firmware.

post_handover_stage

Notes:

  • rustBoot can replace U-boot in a trust-chain i.e. it can easily be integrated into an existing trust-chain, wherever U-boot is used.
  • As it has a very small hardware abstraction layer, it is highly portable across Cortex-M and Cortex-A architectures.
  • Public-key hashes or trust anchors can be stored in secure hardware or embedded in software.
  • Hardware drivers for different types of secure-hardware (ex: crypto elements) will be made available via the HAL.

rustBoot Images

rustBoot supports 2 types of firmware image formats, depending on the underlying device. It could either be an

  • mcu-image: a simple 256-byte firmware image format for microcontrollers or a
  • fit-image: the flattened-image-tree format for systems capable of booting linux.

MCU image format

rustBoot mcu-images comprise of a 256-byte header pre-pended to a firmware binary and are deliberately designed to be as simple as possible.

  • it does not rely on the use of complex digital certificate formats which keeps the TCB small and avoids unnecessary code-complexity

rustBoot_header

rustBoot image header layout:

The header always starts with a 4-byte magic number, followed by a 4-byte field indicating the size of the firmware image (excluding the header). All header contents are stored in little-endian format.

The 2 (magic and size) fixed fields are followed by one or more TLV(s) or Type, Length, Value tags. A TLV has the following layout

  • Type: 2 bytes to indicate the Type of the tag
  • Length: 2 bytes to indicate the length in bytes of the tag (excluding the type and size bytes).
  • Value: N bytes of tag content

Padding and End of header bytes:

  • An 0xFF byte in the Type field indicates a padding byte. A 'padding' byte does NOT have a size field, and the next byte is interpreted as Type again.
  • A 2 byte value of 0x0000 signals the end of the rustBoot header.

Tags:

Each tag represents some information about the firmware. rustBoot requires the following Tags for firmware validation:

  • The version tag provides firmware version number information.
    • Type: 0x0001
    • Length: 4 bytes
  • The timestamp tag provides the timestamp in unix seconds for when the rustBoot image was created.
    • Type: 0x0002
    • Length: 8 bytes
  • The auth type tag identifies the type of the authentication mechanism in use. Ex: which ECC curve are we using and what's the key strength etc.
    • Type: 0x0030
    • Length: 2 bytes
  • The sha256 digest tag contains a SHA2 hash of the firmware and is used to check firmware integrity.
    • Type: 0x0003
    • Length: 32 bytes
  • The firmware signature tag contains the ECC signature and is used to verify firmware against a known public key.
    • Type: 0x0020
    • Length: 64 bytes

Optional tags:

  • Pubkey Hint: A pubkey hint digest tag can be included in the header.
    • Type: 0x1000
    • Length: 32 bytes
    • This tag contains the SHA256 digest of the public key of the corresponding private-key used by the signing tool. The bootloader may use this field to locate the correct public key in case multiple keys are available.

MCU defaults:

  • By default, a valid rustBoot image is always signed.
  • It relies on the 256-byte header for firmware validation.
  • It will fail to boot an image

FIT-image format

rustBoot leverages Uboot's flattened-uImage-tree format to boot the linux kernel.

The FIT format is essentially an extension of the device-tree format. FIT allows us to combine multiple binaries such as the kernel, ramdisk, device-tree-blob etc. into a single image.

A typical rustBoot fit-image contains 4 items in the following order

  •   kernel
  •   fdt
  •   initrd
  •   rbconfig

An example fit-image source file:

It is also referred to as an image-tree source file or .its file.

/dts-v1/;

/ {
        description = "rustBoot FIT Image";
        #address-cells = <1>;

        images {
                kernel {
                        description = "Kernel";
                        data = /incbin/("vmlinuz");     
                        type = "kernel";
                        arch = "arm64";
                        os = "linux";
                        compression = "none";
                        load = <0x40480000>;
                        entry = <0x40480000>;
                        hash {
                                algo = "sha256";
                        };
                };
                fdt {
                        description = "DTB";
                        data = /incbin/("unpatched-bcm2711-rpi-4-b.dtb");
                        type = "flat_dt";
                        arch = "arm64";
                        compression = "none";
                        load = <0x43000000>;
                        entry = <0x43000000>;
                        hash {
                                algo = "sha256";
                        };
                };
                initrd {
                        description = "Initrd";
                        data = /incbin/("initramfs");
                        type = "ramdisk";
                        arch = "arm64";
                        os = "linux";
                        compression = "none";
                        hash {
                                algo = "sha256";
                        };
                };
                rbconfig {
                        description = "rustBoot Config";
                        data = /incbin/("rbconfig.txt");
                        type = "rustBoot cmdline config";
                        arch = "none";
                        os = "linux";
                        compression = "none";
                        hash {
                                algo = "sha256";
                        };
                };
        };

        configurations {
                default = "bootconfig";
                bootconfig {
                        description = "Boot Config";
                        kernel = "kernel";
                        fdt = "fdt";
                        ramdisk = "initrd";
                        rbconfig = "rbconfig";
                        signature@1 {
				algo = "sha256,ecdsa256,nistp256";
				key-name-hint = "dev";
				signed-images = "fdt", "kernel", "ramdisk", "rbconfig";
                                value = "";
			};
                };
        };

};

The default configuration of an .its file determines which kernel, initrd, fdt and rbconfig is to be used for booting. In the above example, bootconfig is our default configuration.

rustBoot's FIT parser will select the corresponding kernel, fdt, initrd and rbconfig associated with bootconfig for booting

Building a rustBoot compliant fit-image:

As shown in the example above, a rustBoot compliant fit-image contains 4 items -

  • kernel - the linux kernel
  • fdt - the flattened device tree or device tree blob
  • ramdisk- a root filesystem that is embedded into the kernel and loaded at an early stage of the boot process. It is the successor of initrd. It can do things the kernel can't easily do by itself during the boot process. For example: customize the boot process (e.g., print a welcome message)
  • rbconfig - this is rustBoot's kernel configuration. A simple txt file to add kernel command-line arguments.

You can retrieve the first 3 (i.e. kernel, fdt, ramdisk) from a pre-built OS image:

  • Maintainers of a linux distribution provide pre-built OS images. These images usually contain several partitions such as -
    • boot: contains the bootloader, kernel, dtb, ramdisk and other stuff
    • system: contains the root file system
    • others: may contain other partitions for things such as storage etc.
  • simply download an OS image or a pre-built linux distribution from the maintainers website.
    • in this example, I'll be using the apertis distribution.
  • it’s usually a compressed (zImage) format, decompress it using a tool like unarchiver to get a disk image.
  • use partx --show to list all partitions
$ partx --show __linux_image_filepath__
NR  START     END SECTORS SIZE NAME UUID
 1   8192  532479  524288 256M      9730496b-01
 2 532480 3661823 3129344 1.5G      9730496b-02

In the above case, the first partition with a size of 256MB contains the boot-files. It's usually named boot. We can calculate the offset to the boot volume/partition with the following command

$ bc
bc 1.07.1
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006, 2008, 2012-2017 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
> 8192 * 512
> 4194304
> quit

512 is the sector-size. We multiply sector-size with the sector offset to get the actual starting (byte) location of boot.

mount the partition as an ext4 file-system (or fat file-system, whichever)

$ sudo mkdir /mnt/other
$ sudo mount -v -o offset=4194304 -t ext4 /_path_to_file_image/__filename__.img /mnt/other
mount: /dev/loop0 mounted on /mnt/other.

Check mounted image

$ ls /mnt/other

Copy the dtb, ramdisk and vmlinuz image (i.e. kernel) from the mounted partition to a new folder. You can give it any name you want. I'll use pkg for this example.

vmlinuz is a PE (portable executable) i.e. we can jump to it and it will in-turn jump to the kernel's entry point.

rbconfig: Lastly, create a file named rbconfig.txt in the pkg folder. This file will be used by rustBoot to pass command-line parameters to the linux kernel.

Here's an example of the rbconfig.txt file -

bootargs="root=UUID=64bc182a-ca9d-4aa1-8936-d2919863c22a rootwait ro plymouth.ignore-serial-consoles fsck.mode=auto fsck.repair=yes cma=128M"

When you have added all 4 items to the pkg folder, you can build a fit-image by running the following commands.

On a mac:

brew install u-boot-tools

On a linux machine:

sudo apt install u-boot-tools

and then run

mkimage -f rpi4-apertis.its rpi4-test-apertis.itb
  • the input to mkimage is an .its file.
  • and .itb filename we've specified is the name given to the generated fit-image (that's stored in the pkg folder).
  • you can copy the contents of the example fit-image file above into a new .its file named rpi4-apertis.its and add it to the pkg folder.
Output:

rpi4-apertis.its:65.37-70.6: Warning (unit_address_vs_reg): /configurations/bootconfig/signature@1: node has a unit name, but no reg or ranges property
Image contains unit addresses @, this will break signing
FIT description: rustBoot FIT Image
Created:         Sat Jun  4 13:18:45 2022
 Image 0 (kernel)
  Description:  Kernel
  Created:      Sat Jun  4 13:18:45 2022
  Type:         Kernel Image
  Compression:  uncompressed
  Data Size:    29272576 Bytes = 28586.50 KiB = 27.92 MiB
  Architecture: AArch64
  OS:           Linux
  Load Address: 0x40480000
  Entry Point:  0x40480000
  Hash algo:    sha256
  Hash value:   97dcbff24ad0a60514e31a7a6b34a765681fea81f8dd11e4644f3ec81e1044fb
 Image 1 (fdt)
  Description:  DTB
  Created:      Sat Jun  4 13:18:45 2022
  Type:         Flat Device Tree
  Compression:  uncompressed
  Data Size:    25713 Bytes = 25.11 KiB = 0.02 MiB
  Architecture: AArch64
  Load Address: 0x43000000
  Hash algo:    sha256
  Hash value:   3572783be74511b710ed7fca9b3131e97fd8073c620a94269a4e4ce79d331540
 Image 2 (initrd)
  Description:  Initrd
  Created:      Sat Jun  4 13:18:45 2022
  Type:         RAMDisk Image
  Compression:  uncompressed
  Data Size:    32901194 Bytes = 32130.07 KiB = 31.38 MiB
  Architecture: AArch64
  OS:           Linux
  Load Address: unavailable
  Entry Point:  unavailable
  Hash algo:    sha256
  Hash value:   f1290587e2155e3a5c2c870fa1d6e3e2252fb0dddf74992113d2ed86bc67f37c
 Image 3 (rbconfig)
  Description:  rustBoot Config
  Created:      Sat Jun  4 13:18:45 2022
  Type:         Unknown Image
  Compression:  uncompressed
  Data Size:    141 Bytes = 0.14 KiB = 0.00 MiB
  Hash algo:    sha256
  Hash value:   b16d058c4f09abdb8da98561f3a15d06ff271c38a4655c2be11dec23567fd519
 Default Configuration: 'bootconfig'
 Configuration 0 (bootconfig)
  Description:  Boot Config
  Kernel:       kernel
  Init Ramdisk: initrd
  FDT:          fdt
  Sign algo:    sha256,ecdsa256,nistp256:dev
  Sign value:   00
  Timestamp:    unavailable

This .itb file is our fit-image. It does not contain a signature yet i.e. it is not signed - notice the sign-value field is empty.

Signing fit-images

rustBoot fit-images are signed with ecdsa256. The signature includes the kernel, fdt, initrd and rbconfig.

Signing a rustBoot fit-image involves 2 steps:

  • Building a fit-image: As explained in preceding section, FIT images can be built using mkimage - a command-line utility from the uboot-tools package i.e. you can pass an .its file to the mkimage tool and mkimage will produce an .itb blob or a image-tree blob.
  • signing the fit-image: once you've built your fit-image, you can pass the it along with a signing key to rustBoot's rbsigner utility to generate a signed fit-image.

FIT-image defaults:

  • By default, valid rustBoot images are always signed.
  • It will fail to boot an image
    • if the image fails fit-validation i.e. if its not a properly formatted fit-image or if the fit-parser cant find the specified default config or its components.
    • if it isn't signed or if it cannot be verified using the specified algo.
  • rustBoot's fit parser currently supports the following architectures
    • Aarch64

rustBoot Partitions

rustBoot has 2 distinct partitioning schemes, depending on the type of the underlying system.

Micro-controller Partitions:

partition

Note: BOOT, UPDATE and SWAP partitions need NOT be consecutively laid out in flash memory. The above diagram only serves as a visual aid.

rustBoot requires an mcu's non-volatile memory (or flash storage) to be divided into (at-least) 4 non-overlapping memory regions (i.e. partitions).

  • rustBoot: contains the bootloader. This usually starts at address 0x0 in flash-memory.
  • BOOT: contains boot firmware. rustBoot always boots from this partition address.
  • UPDATE: contains update firmware i.e. downloaded update firmware is placed in this partition.
  • SWAP: is an empty partition that is used to swap contents of BOOT and UPDATE, one sector at a time.

All 3 partition boundaries must be aligned to a physical sector as rustBoot erases all flash sectors prior to storing a new firmware image, and swaps the contents of the two partitions, one sector at a time.

To ensure that a partition's sector alignments are maintained, the following points must be considered:

  • BOOT and UPDATE partition must be of the same size.
  • SWAP partition must be larger than or equal to the largest sector in either BOOT or UPDATE partition.

MCU flash memory is partitioned as follows:

  • rustBoot partition starts at address 0x0 in flash memory. It should be at least 32KB in size.
  • BOOT partition starts at a pre-defined address - BOOT_PARTITION_ADDRESS
  • UPDATE partition starts at a pre-defined address - UPDATE_PARTITION_ADDRESS
  • SWAP partition starts at a predefined address - SWAP_PARTITION_ADDRESS
    • swap-space size is defined by SECTOR_SIZE and must be larger than the largest sector in either BOOT or UPDATE partition.

BOOT, UPDATE, SWAP addresses and SECTOR_SIZE, PARTITION_SIZE values can be set via source files - constants.rs.

MCU defaults:

  • By default, public keys used for firmware validation are embedded in rustBoot-firmware during a factory image-burn. However, rustBoot also supports the option to retrieve them from secure-hardware (ex: crypto-elements).
  • The BOOT partition is the only partition from which we can boot a firmware image. The firmware image must be linked so that its entry-point is at address 256 + BOOT_PARTITION_ADDRESS.
  • BOOT firmware is responsible for downloading a new firmware image via a secure channel and installing it in the UPDATE partition.
  • To trigger an update, the BOOT firmware updates the status byte of the UPDATE partition and performs a reboot. This will allow the bootloader to swap the contents of BOOT partition with that of the UPDATE partition.

Linux system partitions:

partition

To boot into a linux system, rustBoot includes support for the fat32 file-system.

Boot-storage media must contain a fat32 partition

  • of at least 150 MiB to accommodate the bootloader, boot + update fit-images and other vendor-specific boot files and
  • to add rustBoot support for your board, you can either implement the BlockDevice trait for your board's boot-storage media controller or simply use an existing implementation from the repo.

Note: rustBoot comes with batteries-included. It provides rusty implementations for basic peripherals such as flash, uart, crypto, gpio (out of the box) along with the necessary arch-specific initialization routines.

  • for example: the rustBoot implementation for rpi4 includes bare-metal drivers for the on-board emmc controller, gpio and uart peripherals.

Secure Boot & Update

rustBoot supports the following boot and update schemes, depending on the underlying device-type.

mcu updates:

rustBoot implements a small state machine leveraging rust's type-system to enforce compile-time state transition checks. Additionally, it uses the sealed-trait pattern to declare and seal (all possible) valid states and partitions.

The image below captures rustBoot's state transitions.

Upon supplying power to a system, execution usually starts in an mcu's BootROM. The BootROM is a piece of immutable code, also referred to as the root of trust. BootROM(s) are programmed into an mcu's ROM in the factory and may support secure-boot i.e. the ability to cryptographically verify (and pass control to) a 2nd stage executable/binary.

State Diagram

In our case, rustBoot is the 2nd stage. It checks the status of BOOT and UPDATE partitions and drives the state machine forward, as shown in the image below.

Note:

  • there may be many intermediate stages/executables between BootROM and rustBoot.
  • rustBoot assumes that the target device contains a root of trust with support for digitally verifying cryptographic signatures.
  • rustBoot will check for firmware integrity and authenticity at appropriate stages during boot/update/rollback.
  • booting, updating and rolling back are determined by the the state machine.

State Diagram

partition status:

rustBoot partitions use a status byte to track the status of firmware in each partition. The status byte is a 1-byte field stored at the end of each partition.

Possible states are:

  • STATE_NEW (0xFF): The image was never staged for boot, or triggered for an update. If an image is present, no flags are active.
  • STATE_UPDATING (0x70): Only valid in the UPDATE partition. The image is marked for update and should replace the current image in BOOT.
  • STATE_TESTING (0x10): Only valid in the BOOT partition. The image has been just updated, and never completed its boot. If present after reboot, it means that the updated image failed to boot, despite being correctly verified. This particular situation triggers a rollback.
  • STATE_SUCCESS (0x00): Only valid in the BOOT partition. The image stored in BOOT has been successfully staged at least once, and the update is now complete.

partition sector flags:

  • When an update is triggered, the contents of UPDATE and BOOT are swapped one sector at a time. SWAP is used to temporarily hold a sector during the swap. This ensures that we always store a backup of the original firmware.
  • rustBoot keeps track of the state of each sector (during a swap), using 4 bits per sector at the end of the UPDATE partition.
  • Each sector-swap operation corresponds to a different flag-value for a sector in the sector flags area. This means if a swap operation is interrupted, it can be resumed upon reboot.

State Diagram

linux-system updates:

linux-system updates rely on an updt.txt file. updt.txt as the name suggests is an update-configuration file containing active and passive image components.

  • active component: refers to the rustBoot compliant fit-image that's currently in-use. This also implies that the active image has been successfully verified and booted at least once.
  • passive component: refers to the rustBoot compliant fit-image that's been marked ready-for-update i.e. the active image has downloaded an update and set the corresponding passive component field in updt.txt. Passive component(s) have an additional field called update_status and can be assigned one of the following values.
    • updating: The image is marked for update and should replace the current active-image.
    • testing: The image has been just updated, and never completed its boot. If present after reboot, it means that the updated image failed to boot, despite being correctly verified. This particular situation triggers a rollback.
    • success: The update has been successfully staged at least once, and the update is now complete.

Notes:

  • A valid updt.txt file must (always) contain an active and a passive component.
  • The active component must contain the fields - image_name and image_version.
  • The passive component may contain optional fields such as image_name, image_version and update_status
  • Example: for what constitutes a valid config file, please see the updt.txt in the rpi4 example.

Here's how it works:

  • rustBoot loads the updt.txt file from the root directory, parses it and retrieves the active and passive components.
  • it then performs the following checks, assuming an update is available
    • does the active component include valid image_name and image_version fields?
    • has the passive component's ready_for_update field been set to true?
    • if yes, have the passive_name, passive_version fields been set and has passive_status been set to either updating or success? i.e. update has been marked as ready (on the next reboot).
    • is the passive_version field greater than the active? A valid update must have a version greater than the active version.
  • if all the above checks pass, we attempt to load the update (fit-image) and if any one of above checks fail, we simply load the currently active-image.
  • lastly, rustBoot verifies the loaded fit-image's cryptographic digital signature and additionally checks whether the version-number from updt.txt matches the fit-image's timestamp (to prevent rollback or downgrade attacks). The fit's version number is retrieved from rustBoot's updt.txt file.
    • if a version mismatch arises in the last step, rustBoot attempts to rollback to the currently active-image

A note on valid updt.txt configs. They must contain

  • an image name that ends with .itb as its file extension
  • a valid unix epoch timestamp as its version number and it must start with the ts_ prefix.
  • config keys that are recognized by rustBoot - [active], [passive]
  • field names that are recognized by rustBoot. - image_name, image_version, ready_for_update_flag, update_status

Signing Utilities

As rustBoot supports 2 types of firmware image formats, depending on the underlying device i.e. either an

  • mcu-image: a simple 256-byte firmware image format for microcontrollers or a
  • fit-image: the flattened-image-tree format for systems capable of booting linux.

rustBoot rbsigner utility can produce 2 different types signed images.

Signing mcu-images:

To sign a mcu-image, rustBoot's image signing utility takes 3 inputs

  • an unsigned mcu-image.
  • a raw signing-key or ecdsa private key.
  • the ecdsa curve-type - (nistp256 only for now).

There are 2 ways to sign a mcu-image

  • First we build the image and then sign it using the following commands.
 cargo [board-name] build pkgs-for

 cargo [board-name] sign pkgs-for [boot-version] [update-version]
Note : Here stm32f411 is used as example board for signing.
output:
command : cargo stm32f411 build pkgs-for

yashwanthsingh@Yashwanths-MBP rustBoot % cargo stm32f411 build pkgs-for
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/xtask stm32f411 build pkgs-for`
$ cargo build --release
warning: unused config key `build.runner` in `/Users/yashwanthsingh/Yash/Projects/git_rustBoot_mcusigner/rustBoot/boards/firmware/stm32f411/boot_fw_blinky_green/.cargo/config.toml`
    Finished release [optimized] target(s) in 0.08s
$ cargo build --release
warning: unused config key `build.runner` in `/Users/yashwanthsingh/Yash/Projects/git_rustBoot_mcusigner/rustBoot/boards/firmware/stm32f411/updt_fw_blinky_red/.cargo/config.toml`
    Finished release [optimized] target(s) in 0.08s
$ cargo build --release
    Finished release [optimized] target(s) in 0.07s
output:
  command : cargo stm32f411 sign pkgs-for 1234 1235


  yashwanthsingh@Yashwanths-MBP rustBoot % cargo stm32f411 sign pkgs-for 1234 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/xtask stm32f411 sign pkgs-for 1234 1235`
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32f411_bootfw -O binary stm32f411_bootfw.bin
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32f411_updtfw -O binary stm32f411_updtfw.bin
$ cargo run mcu-image ../boards/sign_images/signed_images/stm32f411_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `/Users/yashwanthsingh/Yash/Projects/git_rustBoot_mcusigner/rustBoot/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/stm32f411_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234`

Update type:    Firmware
Curve type:       nistp256
Input image:      stm32f411_bootfw.bin
Public key:       ecc256.der
Image version:    1234
Output image:     stm32f411_bootfw_v1234_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 1908 bytes.

$ cargo run mcu-image ../boards/sign_images/signed_images/stm32f411_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `/Users/yashwanthsingh/Yash/Projects/git_rustBoot_mcusigner/rustBoot/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/stm32f411_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235`

Update type:    Firmware
Curve type:       nistp256
Input image:      stm32f411_updtfw.bin
Public key:       ecc256.der
Image version:    1235
Output image:     stm32f411_updtfw_v1235_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 1996 bytes.  

  • Single command to build ,sign and flash.
cargo [board-name] build-sign-flash rustBoot [boot-ver] [updt-ver]
output : 

command : cargo stm32f411 build-sign-flash rustBoot 1234 1235
yashwanthsingh@Yashwanths-MacBook-Pro rustBoot % cargo stm32f411 build-sign-flash rustBoot 1234 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.07s
     Running `target/debug/xtask stm32f411 build-sign-flash rustBoot 1234 1235`
$ cargo build --release
warning: unused config key `build.runner` in `/Users/yashwanthsingh/Yash/Projects/git_rustBoot_mcusigner/rustBoot/boards/firmware/stm32f411/boot_fw_blinky_green/.cargo/config.toml`
    Finished release [optimized] target(s) in 0.10s
$ cargo build --release
warning: unused config key `build.runner` in `/Users/yashwanthsingh/Yash/Projects/git_rustBoot_mcusigner/rustBoot/boards/firmware/stm32f411/updt_fw_blinky_red/.cargo/config.toml`
    Finished release [optimized] target(s) in 0.10s
$ cargo build --release
    Finished release [optimized] target(s) in 0.11s
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32f411_bootfw -O binary stm32f411_bootfw.bin
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32f411_updtfw -O binary stm32f411_updtfw.bin
$ cargo run mcu-image ../boards/sign_images/signed_images/stm32f411_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `/Users/yashwanthsingh/Yash/Projects/git_rustBoot_mcusigner/rustBoot/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/stm32f411_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234`

Update type:    Firmware
Curve type:       nistp256
Input image:      stm32f411_bootfw.bin
Public key:       ecc256.der
Image version:    1234
Output image:     stm32f411_bootfw_v1234_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 1908 bytes.

$ cargo run mcu-image ../boards/sign_images/signed_images/stm32f411_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
     Running `/Users/yashwanthsingh/Yash/Projects/git_rustBoot_mcusigner/rustBoot/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/stm32f411_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235`

Update type:    Firmware
Curve type:       nistp256
Input image:      stm32f411_updtfw.bin
Public key:       ecc256.der
Image version:    1235
Output image:     stm32f411_updtfw_v1235_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 1996 bytes.

$ probe-rs-cli erase --chip stm32f411vetx
$ probe-rs-cli download --format Bin --base-address 0x8020000 --chip stm32f411vetx stm32f411_bootfw_v1234_signed.bin
     Erasing sectors ✔ [00:00:01] [############################] 128.00KiB/128.00KiB @ 65.02KiB/s (eta 0s )
 Programming pages   ✔ [00:00:00] [##############################]  2.00KiB/ 2.00KiB @     677B/s (eta 0s )
    Finished in 2.057s
$ probe-rs-cli download --format Bin --base-address 0x8040000 --chip stm32f411vetx stm32f411_updtfw_v1235_signed.bin
     Erasing sectors ✔ [00:00:01] [############################] 128.00KiB/128.00KiB @ 65.15KiB/s (eta 0s )
 Programming pages   ✔ [00:00:00] [##############################]  2.00KiB/ 2.00KiB @     679B/s (eta 0s )
    Finished in 2.052s
$ cargo flash --chip stm32f411vetx --release
    Finished release [optimized] target(s) in 0.08s
    Flashing /Users/yashwanthsingh/Yash/Projects/git_rustBoot_mcusigner/rustBoot/boards/target/thumbv7em-none-eabihf/release/stm32f411
     Erasing sectors ✔ [00:00:01] [##############################] 48.00KiB/48.00KiB @ 40.79KiB/s (eta 0s )
 Programming pages   ✔ [00:00:01] [##############################] 43.00KiB/43.00KiB @ 17.31KiB/s (eta 0s )
    Finished in 2.267s
yashwanthsingh@Yashwanths-MacBook-Pro rustBoot % 

Signing fit-images:

To sign a fit-image, rustBoot's image signing utility takes 3 inputs

  • an unsigned fit-image in the above format
  • a raw signing-key or ecdsa private key
  • the ecdsa curve-type - (nistp256 only for now).

Simply run the following command from root directory of the rustBoot project.

cargo run ../boards/bootloaders/rpi4/apertis/rpi4-test-apertis.itb ../boards/rbSigner/keygen/ecc256.der nistp256

In the above example:

  • ../boards/bootloaders/rpi4/apertis/rpi4-test-apertis.itb is the path to my fit-image
  • ../boards/rbSigner/keygen/ecc256.der is the path to my test signing-key
  • nistp256 is the type ecdsa curve I'd like to use. Its the only one supported for now.
Output: 

    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `/Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/target/debug/rbsigner ../boards/bootloaders/rpi4/apertis/rpi4-test-apertis.itb ../boards/rbSigner/keygen/ecc256.der nistp256`
signature: ecdsa::Signature<NistP256>([64, 147, 93, 99, 241, 5, 118, 167, 156, 150, 203, 234, 74, 207, 182, 243, 129, 143, 38, 2, 107, 85, 114, 145, 178, 163, 33, 153, 2, 100, 0, 114, 135, 18, 174, 183, 194, 110, 24, 186, 33, 36, 39, 105, 116, 74, 8, 118, 171, 237, 30, 108, 64, 205, 206, 14, 110, 226, 43, 143, 180, 193, 19, 33])
bytes_written: 62202019

In the above example, the signed fit-image will be stored at the following path - ../boards/bootloaders/rpi4/apertis/signed-rpi4-apertis.itb

Build & Flash

This section will detail the steps involved in building and flashing rustBoot onto a specific board. More precisely, it will cover the following topics.

  • Partitioning:
  • Building:
    • a rustBoot build usually involves
      • compiling firmware i.e. boot and update firmware.
      • signing the compiled firmware. We have 2 different signing schemes, depending on the target device.
      • compiling rustBoot i.e. the bootloader
  • Programming:
    • After building the required artefacts, the next step is to program the board's non-volatile storage memory.
    • Again, depending on the target device, we employ different loading/programming strategies.
      • mcu(s): typically, rustBoot will use the probe-run utility for programming (and debugging).
      • sbc(s): this will depending the on type of single-board computer. For example: the raspberry-pi uses a sd card for storage and booting.
  • Verifying:
    • To verify that everything works as expected, rustBoot outputs boot-logs.
      • mcu(s): we use a combination of boot-logs and blinking-leds to verify that secure boot and update works as expected. For specifics, please refer to the usage page for the board.
      • sbc(s): rustBoot simply outputs logs to a UART-terminal. For specifics, please refer to usage page for the board.
    • Among other things, rustBoot logs will indicate image-authentication status.

Note: drivers for peripherals such as flash-memory, uart, gpio etc. are included for each board.

nrf52840

The nrf52840 example uses a maker-diary board. It has a custom led configuration.

Note:

  • If you're using a different version of the board, you'll probably need to edit your firmware's partition-addresses to accommodate for differences.
  • Just make sure you don't change the names of files or the folder structure, as cargo xtask looks for these file/folder names.

Partitioning:

The first step in integrating rustBoot is flash-memory partitioning i.e. we divide the nrf52840's flash-memory into 4 partitions, taking into account the geometry of the flash memory.

You can read more about mcu partitioning here

In this example, we'll be using the following partitioning scheme. You can locate these constants in the constants module

#![allow(unused)]
fn main() {
#[cfg(feature = "nrf52840")]
pub const SECTOR_SIZE: usize = 0x1000;
#[cfg(feature = "nrf52840")]
pub const PARTITION_SIZE: usize = 0x28000;
#[cfg(feature = "nrf52840")]
pub const BOOT_PARTITION_ADDRESS: usize = 0x2f000;
#[cfg(feature = "nrf52840")]
pub const SWAP_PARTITION_ADDRESS: usize = 0x57000;
#[cfg(feature = "nrf52840")]
pub const UPDATE_PARTITION_ADDRESS: usize = 0x58000;
}
  • RUSTBOOT partition: contains the bootloader (its code and data) and a (test) public-key embedded as part of the bootloader image, starts at address 0x0.
  • BOOT partition: contains boot firmware, starts at address PARTITION_BOOT_ADDRESS.
  • UPDATE partition: contains update firmware, starts at address UPDATE_PARTITION_ADDRESS. The boot firmware is responsible for downloading and installing the update firmware into this partition via a secure channel.
  • SWAP partition: is the temporary swap space, starts at address SWAP_PARTITION_ADDRESS.

Compiling, Signing and Programming:

Now that we have properly partitioned the nrf52840's on-board flash-memory, the next step is - compiling, signing and programming

We will compile the following

  • bootloader
  • boot and update firmware

sign both pieces of firmware with a (test) private-key and finally create valid rustBoot mcu-images i.e. signed boot and update firmware images.

Note:

  • the ecc256.der file contains a public-key and a private-key, the first 64 bytes being the public-key and remaining 32 bytes make up the private-key.
  • This is a test key file and is to be used for testing purposes only.

Compiling, signing and programming can be performed via a single command

cargo nrf52840 build-sign-flash rustBoot

Note:

  • The updt-ver number must be greater than boot-ver.

This will build, sign and flash all 3 packages (i.e. bootloader + boot-fw + update-fw) onto the board.

Note:

  • The corresponding public-key is embedded in the bootloader's source.
  • In order to test this example, you'll have to install a couple of pre-requisites as it uses probe-run to flash the binary.
cargo install probe-rs-cli 
cargo install cargo-flash
cargo install cargo-binutils

Here's the command line output that should be produced.

PS C:\Users\Nil\devspace\rust\projects\rb> cargo nrf52840 build-sign-flash rustBoot 1234 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/xtask nrf52840 build-sign-flash rustBoot 1234 1235`
$ cargo build --release
   Compiling rand_core v0.6.3
   Compiling subtle v2.4.1
   Compiling nb v1.0.0
   Compiling cfg-if v1.0.0
   ...
   ...
   Compiling rustBoot-hal v0.1.0 (/Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/boards/hal)
   Compiling rustBoot-update v0.1.0 (/Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/boards/update)
   Compiling nrf52840_bootfw v0.1.0 (/Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/boards/firmware/nrf52840/boot_fw_blinky_green)
    Finished release [optimized] target(s) in 10.60s
$ cargo build --release
   Compiling nrf52840_updtfw v0.1.0 (/Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/boards/firmware/nrf52840/updt_fw_blinky_red)
    Finished release [optimized] target(s) in 0.43s
$ cargo build --release
   Compiling rand_core v0.6.3
   Compiling subtle v2.4.1
   Compiling cfg-if v1.0.0
   Compiling nb v1.0.0
   ...
   ...
   Compiling rustBoot-hal v0.1.0 (/Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/boards/hal)
   Compiling rustBoot-update v0.1.0 (/Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/boards/update)
   Compiling nrf52840 v0.1.0 (/Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/boards/bootloaders/nrf52840)
    Finished release [optimized] target(s) in 10.96s
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/nrf52840_bootfw -O binary nrf52840_bootfw.bin
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/nrf52840_updtfw -O binary nrf52840_updtfw.bin
$ cargo run mcu-image ../boards/sign_images/signed_images/nrf52840_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `/Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/nrf52840_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234`

Image type:       mcu-image
Curve type:       nistp256
Input image:      nrf52840_bootfw.bin
Public key:       ecc256.der
Image version:    1234
Output image:     nrf52840_bootfw_v1234_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 1696 bytes.

$ cargo run mcu-image ../boards/sign_images/signed_images/nrf52840_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `/Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/nrf52840_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235`

Image type:       mcu-image
Curve type:       nistp256
Input image:      nrf52840_updtfw.bin
Public key:       ecc256.der
Image version:    1235
Output image:     nrf52840_updtfw_v1235_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 1724 bytes.

$ probe-rs-cli erase --chip nRF52840_xxAA
$ probe-rs-cli download --format Bin --base-address 0x2f000 --chip nRF52840_xxAA nrf52840_bootfw_v1234_signed.bin
     Erasing sectors ✔ [00:00:00] [##########]  4.00KiB/ 4.00KiB @ 15.97KiB/s (eta 0s )
 Programming pages   ✔ [00:00:00] [##########]  4.00KiB/ 4.00KiB @  6.77KiB/s (eta 0s )
    Finished in 0.73s
$ probe-rs-cli download --format Bin --base-address 0x58000 --chip nRF52840_xxAA nrf52840_updtfw_v1235_signed.bin
     Erasing sectors ✔ [00:00:00] [##########]  4.00KiB/ 4.00KiB @ 16.34KiB/s (eta 0s )
 Programming pages   ✔ [00:00:00] [##########]  4.00KiB/ 4.00KiB @  6.86KiB/s (eta 0s )
    Finished in 0.713s
$ cargo flash --chip nRF52840_xxAA --release
    Finished release [optimized] target(s) in 0.06s
    Flashing /Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/boards/target/thumbv7em-none-eabihf/release/nrf52840
     Erasing sectors ✔ [00:00:01] [##########] 44.00KiB/44.00KiB @ 25.20KiB/s (eta 0s )
 Programming pages   ✔ [00:00:03] [##########] 44.00KiB/44.00KiB @  5.62KiB/s (eta 0s )
    Finished in 4.808s

Verifying:

blinky leds are used to confirm that rustBoot works as expected. Here's the flow

  • Upon supplying power to the board, rustBoot takes over
    • validates the firmware image stored in the BOOT partition i.e.
    • verifies the signature attached against a known public key stored in the rustBoot image.
  • If the signature checks out, rustBoot boots into the bootfw and blinks a green-led for a few seconds,
    • post which, the boot firmware triggers the update and performs a system reset.
  • Upon reset, the rustBoot again takes over
    • validates the firmware image stored in the UPDATE partition
    • swaps the contents of the BOOT and the UPDATE partitions
    • marks the new firmware in the BOOT partition as in state STATE_TESTING
    • boots into the UPDATE'd firmware
  • Now that execution-control has been transferred to the UPDATE'd firmware
    • it will attempt to blink a red-led
    • and set a confirmation flag to indicate that the update was successful.
    • post which, it continuously blinks a red-led.

rp2040

The rp2040 example uses a Raspberry Pi Pico Board. The board has 1 user LED which will be blinking at different frequencies depending on whether we're executing boot-firmware or update-firmware.

Raspberry Pi Pico's Boot Sequence

Raspberry Pi Pico board has an external QSPI Flash chip W25Q080 with support for cached execute-in-place. It has a first stage bootloader baked into ROM. This is always the very first thing that runs when the RP2040 starts up. The built-in bootloader has a fixed sequence, it checks if the BOOTSEL button is pressed, and if so, enters the USB mass storage mode for code upload. If the BOOTSEL button is not pressed, and it looks like the flash contains a valid program, then it starts executing the “program” from flash.

In flash, first 256 bytes are second-stage bootloader which are required to configure external QSPI flash for high speed and efficient code access.

Note: For this example we are using two Pico boards, one board will be used as debug probe and other will be a target board. You can learn more about this here

Partitioning

The first step in integrating rustBoot is flash-memory partitioning i.e. we divide the Pico's flash-memory into 4 partitions, taking into account the geometry of the flash memory.

You can read more about mcu partitioning here

In this example, we'll be using the following partitioning scheme. You can locate these constants in the constants module

#![allow(unused)]
fn main() {
#[cfg(feature = "rp2040")]
pub const SECTOR_SIZE: usize = 0x1000;
#[cfg(feature = "rp2040")]
pub const PARTITION_SIZE: usize = 0x20000;
#[cfg(feature = "rp2040")]
pub const BOOT_PARTITION_ADDRESS: usize = 0x10020000;
#[cfg(feature = "rp2040")]
pub const UPDATE_PARTITION_ADDRESS: usize = 0x10040000;
#[cfg(feature = "rp2040")]
pub const SWAP_PARTITION_ADDRESS: usize = 0x10060000;
}

Note: Choose the number of sectors based on your boot and update firmware sizes.

RUSTBOOT partition: contains the bootloader (its code and data) and a (test) public-key embedded as part of the bootloader image, starts at address 0x10000100.

  • BOOT partition: contains boot firmware, starts at address PARTITION_BOOT_ADDRESS.
  • UPDATE partition: contains update firmware, starts at address UPDATE_PARTITION_ADDRESS. The boot firmware is responsible for downloading and installing the update firmware into this partition via a secure channel.
  • SWAP partition: is the temporary swap space, starts at address SWAP_PARTITION_ADDRESS.

Compiling, Signing and Programming:

Now that we have properly partitioned the Pico's on-board flash-memory, the next step is - compiling, signing and programming

We will compile the following

  • bootloader
  • boot and update firmware

sign both pieces of firmware with a (test) private-key and finally create valid rustBoot mcu-images i.e. signed boot and update firmware images.

Note:

  • the ecc256.der file contains a public-key and a private-key, the first 64 bytes being the public-key and remaining 32 bytes make up the private-key.
  • This is a test key file and is to be used for testing purposes only.

Compiling, signing and programming can be performed via a single command

cargo stm32f334 build-sign-flash rustBoot [boot-ver] [updt-ver]

Note:

  • The updt-ver number should be greater than boot-ver.

This will build, sign and flash all 3 packages (i.e. bootloader + boot-fw + update-fw) onto the board.

Note:

  • The corresponding public-key is embedded in the bootloader's source.
  • In order to test this example, you'll have to install a couple of pre-requisites as it uses probe-run to flash the binary.
cargo install probe-rs-cli 
cargo install cargo-flash
cargo install cargo-binutils

Here's the command and its output that should be produced.

cargo rp2040 build-sign-flash rustBoot 1234 1235
C:\Users\ABHISHEK\Documents\rustBoot> cargo rp2040 build-sign-flash rustBoot 1234 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.07s
     Running `target\debug\xtask.exe rp2040 build-sign-flash rustBoot 1234 1235`
$ cargo build --release
    Finished release [optimized] target(s) in 0.21s
$ cargo build --release
    Finished release [optimized] target(s) in 0.16s
$ cargo build --release
    Finished release [optimized] target(s) in 0.26s
$ rust-objcopy -I elf32-littlearm ../../target/thumbv6m-none-eabi/release/rp2040_bootfw -O binary rp2040_bootfw.bin
$ rust-objcopy -I elf32-littlearm ../../target/thumbv6m-none-eabi/release/rp2040_updtfw -O binary rp2040_updtfw.bin
$ cargo run mcu-image ../boards/sign_images/signed_images/rp2040_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234
    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
     Running `C:\Users\ABHISHEK\Documents\rustBoot\target\debug\rbsigner.exe mcu-image ../boards/sign_images/signed_images/rp2040_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234`

Update type:    Firmware
Curve type:       nistp256
Input image:      rp2040_bootfw.bin
Public key:       ecc256.der
Image version:    1234
Output image:     rp2040_bootfw_v1234_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 1644 bytes.

$ cargo run mcu-image ../boards/sign_images/signed_images/rp2040_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
     Running `C:\Users\ABHISHEK\Documents\rustBoot\target\debug\rbsigner.exe mcu-image ../boards/sign_images/signed_images/rp2040_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235`

Update type:    Firmware
Curve type:       nistp256
Input image:      rp2040_updtfw.bin
Public key:       ecc256.der
Image version:    1235
Output image:     rp2040_updtfw_v1235_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 1612 bytes.

$ probe-rs-cli download --format Bin --base-address 0x10020000 --chip RP2040 rp2040_bootfw_v1234_signed.bin
     Erasing sectors ✔ [00:00:00] [################################################################]  4.00KiB/ 4.00KiB @ 23.50KiB/s (eta 0s )
 Programming pages   ✔ [00:00:00] [################################################################]  4.00KiB/ 4.00KiB @ 10.21KiB/s (eta 0s )
    Finished in 0.494s
$ probe-rs-cli download --format Bin --base-address 0x10040000 --chip RP2040 rp2040_updtfw_v1235_signed.bin
     Erasing sectors ✔ [00:00:00] [################################################################]  4.00KiB/ 4.00KiB @ 23.77KiB/s (eta 0s )
 Programming pages   ✔ [00:00:00] [################################################################]  4.00KiB/ 4.00KiB @ 10.27KiB/s (eta 0s )
    Finished in 0.495s
$ cargo flash --chip RP2040 --release
    Finished release [optimized] target(s) in 0.18s
    Flashing C:\Users\ABHISHEK\Documents\rustBoot\boards\target\thumbv6m-none-eabi\release\rp2040
     Erasing sectors ✔ [00:00:00] [################################################################] 48.00KiB/48.00KiB @ 45.42KiB/s (eta 0s )
 Programming pages   ✔ [00:00:02] [################################################################] 48.00KiB/48.00KiB @  9.73KiB/s (eta 0s )
    Finished in 3.174s

Verifying:

user led is used to confirm that rustBoot works as expected. Here's the flow

  • Upon supplying power to the board, rustBoot takes over
    • validates the firmware image stored in the BOOT partition
    • verifies the signature attached against a known public key stored in the rustBoot image.
  • If the signature checks out, rustBoot boots into the bootfw and blinks a user led for a few seconds, at an interval of 1 second five times.
    • post which, the boot firmware triggers the update and performs a system reset.
  • Upon reset, the rustBoot again takes over
    • validates the firmware image stored in the UPDATE partition
    • swaps the contents of the BOOT and the UPDATE partitions
    • marks the new firmware in the BOOT partition as in state STATE_TESTING
    • boots into the UPDATE'd firmware
  • Now that execution-control has been transferred to the UPDATE'd firmware
    • and set a confirmation flag to indicate that the update was successful.
    • post which, it continuously blinks a user led at an interval of 0.125 second continuously.

raspberry-pi 4

Table of contents:

🥧   raspberry-pi 4 boot-sequence:

rpi4 has an unconventional boot process

  • Upon initial power-on, the bcm2711 SoC (CPU is offline but GPU is powered on) executes from the onboard bootROM i.e. 1st stage bootloader

Note: the GPU contains a tiny risc core that executes the bootROM.

  • bootROM checks an onboard SPI-EEPROM for a 2nd stage bootloader
  • This 2nd stage bootloader is loaded into the GPU's L2 cache for the GPU to execute.
    • It initializes system clocks and SDRAM
    • loads GPU firmware (start4.elf) into RAM
  • the GPU firmware performs RAM allocations i.e.
    • RAM is shared between CPU and GPU
    • enables BCM7211 CPU.
    • loads rustBoot-bootloader from the SD card into CPU-assigned RAM and passes control of the ARMv8 core to rustBoot

Note: At this point, rustBoot has complete control over the CPU.

🦀   rustBoot execution-sequence:

  • By default, rpi4 will always start executing in EL2. Since we are booting a traditional Kernel (i.e. linux), we have to transition into the more appropriate EL1.

Note: EL1 and EL2 are abbreviations for ARMv8-A exception levels

So, rustBoot checks the following

  • is the core executing in EL2?
  • are we executing on the boot-core i.e. is it core 0?
  • if the answer to any of the above questions is no, then we park the core i.e. go into an infinite wait state.
  • If yes, then we initialize DRAM, zero out bss, transition to EL1 and finally jump to an early initialization routine called kernel_init.
  • kernel_init is an early initialization routine. It takes care of the following -
    • enables exception handling
    • enables the MMU along with instruction + data caching
    • initializes a small set of peripheral drivers i.e EMMC controller, UART, GPIO
    • and passes control to the core bootloader routine called kernel_main.
  • kernel_main takes care of loading, verifying and booting fit-images.
    • it uses rustBoot's (fat32) file-system to retrieve the first partition (or volume).

    Note: rustBoot does not support GUID Partition Table disks.

    • If the first volume is a valid fat32 partition, it loads the supplied fit-image into RAM and attempts to verify its signature using the ecdsa algorithm.
    • If the fit-image is authentic i.e. the signature check passes, it relocates the following components to an appropriate location in memory.
      • linux kernel
      • fdt or dtb
      • ramdisk or initramfs
    • additionally, it will patch the dtb with any supplied (boot-time) kernel command-line arguments.

    Note: kernel cmd-line arguments are set at package-build time i.e. when building the fit-image and cannot be interactively set at run time.

    • Finally, it disables the MMU and boots the linux kernel by jumping to its (relocated) entry point.

💾   Booting from an SD card:

Raspberry Pi computers use a micro SD card to store a bootable image.

SD card preparation:

  • make 2 partitions
    • the first one must be a fat32 partition named firmware. The fat partition needs to be (at-least) 150MB(s). But to keep things simple, you can use a 256MB partition.
    • the second one can be ext2/3/4 partition. This is used to host the root file system.

FAT32 partition contents:

  • Create a file named config.txt with the following contents in the fat partition.
    arm_64bit=1
    enable_uart=1
    init_uart_clock=4000000
    kernel=rustBoot.bin

Note: Should it not work on your rpi4, try renaming start4.elf to start.elf (without the 4) on the SD card.

⌛   Compiling rustBoot:

You must have rust installed. You can install rust by following the installation instructions here. After installing rust, you'll need to switch to rust's nightly toolchain and add the aarch64 compilation-target. This will allow us to compile code for the rpi4

rustup default nightly
rustup target add aarch64-unknown-none-softfloat

Additionally you'll need a few binary utilities to extract the bootloader as a .bin file. They can be installed via the following commands.

cargo install cargo-binutils
rustup component add llvm-tools-preview

To verify that you have the pre-requisites installed correctly, run the following command

rustup show

In my case, rustup show returns the following output.

rustup show
Default host: aarch64-apple-darwin
rustup home:  /Users/nihal.pasham/.rustup

installed toolchains
--------------------

stable-aarch64-apple-darwin
nightly-aarch64-apple-darwin (default)

installed targets for active toolchain
--------------------------------------

aarch64-apple-darwin
aarch64-unknown-none-softfloat
thumbv7em-none-eabihf

active toolchain
----------------

nightly-aarch64-apple-darwin (default)
rustc 1.63.0-nightly (ee160f2f5 2022-05-23)

You should be able to see aarch64-unknown-none-softfloat as one of the installed targets. To compile the rustBoot-bootloader, simply clone the rustBoot repo and run the following command

cargo rpi4 build rustBoot-only

The above command should output the following (output will be longer when compiling for the first time) logs, produce an executable bootloader named rustBoot.bin and store it in the following path ./rustBoot/boards/bootloaders/rpi4

Output:
   Compiling xtask v0.1.0 (/Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/xtask)
    Finished dev [unoptimized + debuginfo] target(s) in 0.64s
     Running `target/debug/xtask rpi4 build rustBoot-only`
$ cargo build --release
   Compiling rustBoot v0.1.0 (/Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/rustBoot)
   Compiling rustBoot-hal v0.1.0 (/Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/boards/hal)
   Compiling rpi4 v0.1.0 (/Users/nihal.pasham/devspace/rust/projects/prod/rustBoot/boards/bootloaders/rpi4)
    Finished release [optimized] target(s) in 4.77s
$ rust-objcopy --strip-all -O binary ../../target/aarch64-unknown-none-softfloat/release/kernel rustBoot.bin

If you run into any linker issues during the compilation process in a windows environment, please ensure you have C++ build tools installed on your machine. You can download and install the visual studio's build tools from the microsoft website.

After compiling rustBoot, copy rustBoot.bin file onto the sd card's fat32 partition.

The last step in preparing a bootable SD card is to copy the rustBoot fit-image that you'd like to boot onto the sd card's fat32 partition.

Note:

Finally, once you've added the above mentioned files to your sd card. The fat32 partition should contain the following files:

  •   config.txt
  •   fixup4.dat
  •   start4.elf
  •   bcm2711-rpi-4-b.dtb
  •   rustBoot.bin
  •   signed-example-image.itb

💼   Adding a root file system:

There are many ways to add a root file-system to the second ext2/3/4 partition. One way is to copy a root file system to an empty ext2/3/4 drive:

  • Maintainers of a linux distribution provide pre-built OS images. These images usually contain several partitions such as -
    • boot: contains the bootloader, kernel, dtb, ramdisk and other stuff
    • system: contains the root file system
    • others: may contain other partitions for things such as storage etc.
  • simply download an OS image or a pre-built linux distribution from the maintainers website.
    • in this example, I'll be using the apertis distribution.
  • it’s usually in a compressed (zImage) format, decompress it using a tool like unarchiver to get a disk image.
  • use partx --show to list all partitions
$ partx --show __linux_image_filepath__
NR  START     END SECTORS SIZE NAME UUID
 1   8192  532479  524288 256M      9730496b-01
 2 532480 3661823 3129344 1.5G      9730496b-02
  • in the above example, partition 2 with a size of 1.5G contains the root file system. It's usually named system.
  • calculate the offset to the system volume/partition. You can do this with the bc command.

512 is the sector-size. We multiply sector-size with the sector offset to get the actual starting (byte) location of system.

$ bc
bc 1.07.1
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006, 2008, 2012-2017 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
> 532480 * 512
> 272629760
> quit
  • mount the partition as an ext4 filesystem
$ sudo mkdir /mnt/other
$ sudo mount -v -o offset=272629760 -t ext4 /_path_to_file_image/__filename__.img /mnt/other
mount: /dev/loop0 mounted on /mnt/other.

Check mounted image

$ ls /mnt/other
  • copy all of (system partition's) contents to sd card's ext2/3/4 partition using the cp command.

Note:

  • this method only works on linux or macOS and does not work on WSL2.
  • all symbolic links need to be copied. If required you can create symbolic links using ln command. Here' an example that creates a symbolic link called sbin to usr/bin
ln -s usr/sbin sbin 

🚌   UART communication:

rustBoot will output boot-logs via the raspberry-pi 4's UART interface. These logs can be sent to a host computer (i.e. laptop/desktop).

We'll need extra hardware for this:

  • a usb-to-serial ttl converter: is a tiny piece of hardware that allows us to send serial data from the rpi4's uart interface to the host
    • You can find USB-to-serial cables that should work right away at [1] [2], but many others will work too. Ideally, your cable is based on the CP2102 chip.
    • You connect it to GND and GPIO pins 14/15 as shown below.

Connect the USB-serial converter to your host computer as shown in the wiring diagram wiring diagram.

  • make sure that you DO NOT connect the power pin of the USB serial. Only RX/TX and GND.
  • connect the rpi4 to the (USB) power cable and observe the output:

Serial console: To view rpi4's output on a host machine, you'll need a tool/app/console that handles sending and receiving of serial data. There are a number of ways to interact with a serial console. I'll be using

  • minicom on linux
  • screen on the mac
  • terminal-s on windows

❗ NOTE: Depending on your host operating system, the device name might differ. For example, on macOS, it might be something like /dev/tty.usbserial-0001. In this case, please give the name explicitly:

  • Using minicom:
    • install minicom with sudo apt-get install minicom, so you can emulate a terminal connected over serial.
    • and run
    minicom -b 115200 -D /dev/tty.usbserial-0001
    
    • boot the Pi.
    • within a few seconds, you should see data in your session.
  • Using screen:
    • on a mac, run
    screen /dev/tty.usbserial-0001 115200
    
    • boot the Pi.
    • within a few seconds, you should see data in your session.
  • Using terminal-s:
    • on a windows machine, install terminal-s (a python-based serial terminal) with pip install terminal-s
    • and run
    terminal-s
    
    • no need to provide a baud-rate. It will auto-detect the port and baud-rate (assuming its lower than 115200).

Note:

  • To exit the screen session, press Ctrl-A, then Ctrl-K, and confirm you want to exit when using minicom or screen
  • To exit terminal-s, press Ctrl-]

🧪   Power-on and test:

Now that you have a fully bootable SD card containing

  • a fat32 formatted boot partition populated with the relevant boot files and
  • a ext2/3/4 formatted root-file-system

and have your uart-usb interface set-up, you are now ready to flip the switch i.e.

  • insert the sd card into the pi's sd slot and
  • supply power to your pi.

Your serial console should now start receiving boot-logs from the rpi4

stm32f411

The stm32f411 example uses a STM32F411EDISCOVERY BOARD.The board has four LEDs of which two LEDs are used in this example.

Note:

  • If you're using a different version of the board, you'll probably need to edit your firmware's partition-addresses to accommodate for differences.
  • Just make sure you don't change the names of files or the folder structure, as cargo xtask looks for these file/folder names.

Partitioning:

The first step in integrating rustBoot is flash-memory partitioning i.e. we divide the stm32f411's flash-memory into 4 partitions, taking into account the geometry of the flash memory.

You can read more about mcu partitioning here

In this example, we'll be using the following partitioning scheme. You can locate these constants in the constants module

#![allow(unused)]
fn main() {
#[cfg(feature = "stm32f411")]
pub const SECTOR_SIZE: usize = 0x20000;
#[cfg(feature = "stm32f411")]
pub const PARTITION_SIZE: usize = 0x20000;
#[cfg(feature = "stm32f411")]
pub const BOOT_PARTITION_ADDRESS: usize = 0x08020000;
#[cfg(feature = "stm32f411")]
pub const SWAP_PARTITION_ADDRESS: usize = 0x08060000;
#[cfg(feature = "stm32f411")]
pub const UPDATE_PARTITION_ADDRESS: usize = 0x08040000;
}
  • RUSTBOOT partition: contains the bootloader (its code and data) and a (test) public-key embedded as part of the bootloader image, starts at address 0x0800_0000.
  • BOOT partition: contains boot firmware, starts at address PARTITION_BOOT_ADDRESS.
  • UPDATE partition: contains update firmware, starts at address UPDATE_PARTITION_ADDRESS. The boot firmware is responsible for downloading and installing the update firmware into this partition via a secure channel.
  • SWAP partition: is the temporary swap space, starts at address SWAP_PARTITION_ADDRESS.

Compiling, Signing and Programming:

Now that we have properly partitioned the stm32f411's on-board flash-memory, the next step is - compiling, signing and programming

We will compile the following

  • bootloader
  • boot and update firmware

sign both pieces of firmware with a (test) private-key and finally create valid rustBoot mcu-images i.e. signed boot and update firmware images.

Note:

  • the ecc256.der file contains a public-key and a private-key, the first 64 bytes being the public-key and remaining 32 bytes make up the private-key.
  • This is a test key file and is to be used for testing purposes only.

Compiling, signing and programming can be performed via a single command

cargo stm32f411 build-sign-flash rustBoot [boot-ver] [updt-ver]

Note:

  • The updt-ver number should be greater than boot-ver.

This will build, sign and flash all 3 packages (i.e. bootloader + boot-fw + update-fw) onto the board.

Note:

  • The corresponding public-key is embedded in the bootloader's source.
  • In order to test this example, you'll have to install a couple of pre-requisites as it uses probe-run to flash the binary.
 cargo install probe-rs-cli 
 cargo install cargo-flash 
 cargo install cargo-binutils

Here's the command line output that should be produced.

yashwanthsingh@Yashwanths-MacBook-Pro rustBoot % cargo stm32f411 build-sign-flash rustBoot 1234 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.07s
     Running `target/debug/xtask stm32f411 build-sign-flash rustBoot 1234 1235`
$ cargo build --release
warning: unused config key `build.runner` in `/Users/yashwanthsingh/Yash/Projects/git_rustBoot_mcusigner/rustBoot/boards/firmware/stm32f411/boot_fw_blinky_green/.cargo/config.toml`
    Finished release [optimized] target(s) in 0.10s
$ cargo build --release
warning: unused config key `build.runner` in `/Users/yashwanthsingh/Yash/Projects/git_rustBoot_mcusigner/rustBoot/boards/firmware/stm32f411/updt_fw_blinky_red/.cargo/config.toml`
    Finished release [optimized] target(s) in 0.10s
$ cargo build --release
    Finished release [optimized] target(s) in 0.11s
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32f411_bootfw -O binary stm32f411_bootfw.bin
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32f411_updtfw -O binary stm32f411_updtfw.bin
$ cargo run mcu-image ../boards/sign_images/signed_images/stm32f411_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `/Users/yashwanthsingh/Yash/Projects/git_rustBoot_mcusigner/rustBoot/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/stm32f411_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234`

Update type:    Firmware
Curve type:       nistp256
Input image:      stm32f411_bootfw.bin
Public key:       ecc256.der
Image version:    1234
Output image:     stm32f411_bootfw_v1234_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 1908 bytes.

$ cargo run mcu-image ../boards/sign_images/signed_images/stm32f411_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
     Running `/Users/yashwanthsingh/Yash/Projects/git_rustBoot_mcusigner/rustBoot/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/stm32f411_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235`

Update type:    Firmware
Curve type:       nistp256
Input image:      stm32f411_updtfw.bin
Public key:       ecc256.der
Image version:    1235
Output image:     stm32f411_updtfw_v1235_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 1996 bytes.

$ probe-rs-cli erase --chip stm32f411vetx
$ probe-rs-cli download --format Bin --base-address 0x8020000 --chip stm32f411vetx stm32f411_bootfw_v1234_signed.bin
     Erasing sectors ✔ [00:00:01] [############################] 128.00KiB/128.00KiB @ 65.02KiB/s (eta 0s )
 Programming pages   ✔ [00:00:00] [##############################]  2.00KiB/ 2.00KiB @     677B/s (eta 0s )
    Finished in 2.057s
$ probe-rs-cli download --format Bin --base-address 0x8040000 --chip stm32f411vetx stm32f411_updtfw_v1235_signed.bin
     Erasing sectors ✔ [00:00:01] [############################] 128.00KiB/128.00KiB @ 65.15KiB/s (eta 0s )
 Programming pages   ✔ [00:00:00] [##############################]  2.00KiB/ 2.00KiB @     679B/s (eta 0s )
    Finished in 2.052s
$ cargo flash --chip stm32f411vetx --release
    Finished release [optimized] target(s) in 0.08s
    Flashing /Users/yashwanthsingh/Yash/Projects/git_rustBoot_mcusigner/rustBoot/boards/target/thumbv7em-none-eabihf/release/stm32f411
     Erasing sectors ✔ [00:00:01] [##############################] 48.00KiB/48.00KiB @ 40.79KiB/s (eta 0s )
 Programming pages   ✔ [00:00:01] [##############################] 43.00KiB/43.00KiB @ 17.31KiB/s (eta 0s )
    Finished in 2.267s
yashwanthsingh@Yashwanths-MacBook-Pro rustBoot % 

Verifying:

blinky leds are used to confirm that rustBoot works as expected. Here's the flow

  • Upon supplying power to the board, rustBoot takes over
    • validates the firmware image stored in the BOOT partition
    • verifies the signature attached against a known public key stored in the rustBoot image.
  • If the signature checks out, rustBoot boots into the bootfw and blinks a green-led for a few seconds,
    • post which, the boot firmware triggers the update and performs a system reset.
  • Upon reset, the rustBoot again takes over
    • validates the firmware image stored in the UPDATE partition
    • swaps the contents of the BOOT and the UPDATE partitions
    • marks the new firmware in the BOOT partition as in state STATE_TESTING
    • boots into the UPDATE'd firmware
  • Now that execution-control has been transferred to the UPDATE'd firmware
    • it will attempt to blink a red-led
    • and set a confirmation flag to indicate that the update was successful.
    • post which, it continuously blinks a red-led.

stm32f446 Nucleo

The stm32f446 example uses a STM32F446 Nucleo Board. The board has 1 user LED which will be blinking at different frequencies depending on whether we're executing boot-firmware or update-firmware.

Note

  • If you're using a different version of the board, you'll probably need to edit your firmware's partition-addresses to accommodate for differences.
  • Just make sure you don't change the names of files or the folder structure, as cargo xtask looks for these file/folder names.

Partitioning

The first step in integrating rustBoot is flash-memory partitioning i.e. we divide the stm32f446's flash-memory into 4 partitions, taking into account the geometry of the flash memory.

You can read more about mcu partitioning here

In this example, we'll be using the following partitioning scheme. You can locate these constants in the constants module

#![allow(unused)]
fn main() {
#[cfg(feature = "stm32f446")]
pub const SECTOR_SIZE: usize = 0x20000;
#[cfg(feature = "stm32f446")]
pub const PARTITION_SIZE: usize = 0x20000;
#[cfg(feature = "stm32f446")]
pub const BOOT_PARTITION_ADDRESS: usize = 0x08020000;
#[cfg(feature = "stm32f446")]
pub const SWAP_PARTITION_ADDRESS: usize = 0x08060000;
#[cfg(feature = "stm32f446")]
pub const UPDATE_PARTITION_ADDRESS: usize = 0x08040000;
}

RUSTBOOT partition: contains the bootloader (its code and data) and a (test) public-key embedded as part of the bootloader image, starts at address 0x0800_0000.

  • BOOT partition: contains boot firmware, starts at address PARTITION_BOOT_ADDRESS.
  • UPDATE partition: contains update firmware, starts at address UPDATE_PARTITION_ADDRESS. The boot firmware is responsible for downloading and installing the update firmware into this partition via a secure channel.
  • SWAP partition: is the temporary swap space, starts at address SWAP_PARTITION_ADDRESS.

Compiling, Signing and Programming:

Now that we have properly partitioned the stm32f446 Nucleo's on-board flash-memory, the next step is - compiling, signing and programming

We will compile the following

  • bootloader
  • boot and update firmware

sign both pieces of firmware with a (test) private-key and finally create valid rustBoot mcu-images i.e. signed boot and update firmware images.

Note:

  • the ecc256.der file contains a public-key and a private-key, the first 64 bytes being the public-key and remaining 32 bytes make up the private-key.
  • This is a test key file and is to be used for testing purposes only.

Compiling, signing and programming can be performed via a single command

cargo stm32f446 build-sign-flash rustBoot [boot-ver] [updt-ver]

Note:

  • The updt-ver number should be greater than boot-ver.

This will build, sign and flash all 3 packages (i.e. bootloader + boot-fw + update-fw) onto the board. With the respective firmware versions.

Note:

  • The corresponding public-key is embedded in the bootloader's source.
  • In order to test this example, you'll have to install a couple of pre-requisites as it uses probe-run to flash the binary.
cargo install probe-rs-cli 
cargo install cargo-flash
cargo install cargo-binutils

Here's the command line output that should be produced.

anand@anand-VirtualBox:~/Desktop/dev_space/Prod/rustBoot_mcusigner$ cargo stm32f446 build-sign-flash rustBoot 1234 1235
Compiling version_check v0.9.4
   Compiling typenum v1.15.0
   Compiling subtle v2.4.1
   Compiling rand_core v0.6.3
   Compiling const-oid v0.7.1
   Compiling zeroize v1.4.3
   Compiling memchr v2.5.0
   Compiling cfg-if v1.0.0
   Compiling base16ct v0.1.1
   Compiling log v0.4.17
   Compiling opaque-debug v0.3.0
   Compiling cpufeatures v0.2.2
   Compiling anyhow v1.0.58
   Compiling minimal-lexical v0.2.1
   Compiling stable_deref_trait v1.2.0
   Compiling byteorder v1.4.3
   Compiling xshell-macros v0.1.17
   Compiling der v0.5.1
   Compiling ff v0.11.1
   Compiling as-slice v0.2.1
   Compiling group v0.11.0
...
...
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/xtask stm32f446 build-sign-flash rustBoot 1234 1235`
$ cargo build --release
    Finished release [optimized] target(s) in 0.03s
$ cargo build --release
    Finished release [optimized] target(s) in 0.03s
$ cargo build --release
    Finished release [optimized] target(s) in 0.03s
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32f446_bootfw -O binary stm32f446_bootfw.bin
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32f446_updtfw -O binary stm32f446_updtfw.bin
$ cargo run mcu-image ../boards/sign_images/signed_images/stm32f446_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234
   Compiling libc v0.2.126
   Compiling rustBoot v0.1.0 (/home/anand/Desktop/dev_space/Prod/rustBoot_mcusigner/rustBoot)
   Compiling filetime v0.2.17
   Compiling rbsigner v0.1.0 (/home/anand/Desktop/dev_space/Prod/rustBoot_mcusigner/rbsigner)
    Finished dev [unoptimized + debuginfo] target(s) in 1.18s
     Running `/home/anand/Desktop/dev_space/Prod/rustBoot_mcusigner/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/stm32f446_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234`

Update type:    Firmware
Curve type:       nistp256
Input image:      stm32f446_bootfw.bin
Public key:       ecc256.der
Image version:    1234
Output image:     stm32f446_bootfw_v1234_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 1948 bytes.

$ cargo run mcu-image ../boards/sign_images/signed_images/stm32f446_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `/home/anand/Desktop/dev_space/Prod/rustBoot_mcusigner/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/stm32f446_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235`

Update type:    Firmware
Curve type:       nistp256
Input image:      stm32f446_updtfw.bin
Public key:       ecc256.der
Image version:    1235
Output image:     stm32f446_updtfw_v1235_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 2092 bytes.

$ probe-rs-cli erase --chip stm32f446retx
$ probe-rs-cli download --format Bin --base-address 0x8020000 --chip stm32f446retx stm32f446_bootfw_v1234_signed.bin
     Erasing sectors ✔ [00:00:03] [##############################################################################################################] 128.00KiB/128.00KiB @ 37.61KiB/s (eta 0s )
 Programming pages   ✔ [00:00:01] [################################################################################################################]  2.00KiB/ 2.00KiB @     296B/s (eta 0s )
    Finished in 5.804s
$ probe-rs-cli download --format Bin --base-address 0x8040000 --chip stm32f446retx stm32f446_updtfw_v1235_signed.bin
     Erasing sectors ✔ [00:00:03] [##############################################################################################################] 128.00KiB/128.00KiB @ 37.75KiB/s (eta 0s )
 Programming pages   ✔ [00:00:02] [################################################################################################################]  3.00KiB/ 3.00KiB @     353B/s (eta 0s )
    Finished in 6.273s
$ cargo flash --chip stm32f446vetx --release
    Finished release [optimized] target(s) in 0.03s
    Flashing /home/anand/Desktop/dev_space/Prod/rustBoot_mcusigner/boards/target/thumbv7em-none-eabihf/release/stm32f446
     Erasing sectors ✔ [00:00:03] [################################################################################################################] 48.00KiB/48.00KiB @ 11.88KiB/s (eta 0s )
 Programming pages   ✔ [00:00:21] [################################################################################################################] 43.00KiB/43.00KiB @  1.69KiB/s (eta 0s )
    Finished in 25.449s

Verifying:

user led is used to confirm that rustBoot works as expected. Here's the flow

  • Upon supplying power to the board, rustBoot takes over
    • validates the firmware image stored in the BOOT partition
    • verifies the signature attached against a known public key stored in the rustBoot image.
  • If the signature checks out, rustBoot boots into the bootfw and blinks a user led for a few seconds, at an interval of 0.5 seconds.
    • post which, the boot firmware triggers the update and performs a system reset.
  • Upon reset, the rustBoot again takes over
    • validates the firmware image stored in the UPDATE partition
    • swaps the contents of the BOOT and the UPDATE partitions
    • marks the new firmware in the BOOT partition as in state STATE_TESTING
    • boots into the UPDATE'd firmware
  • Now that execution-control has been transferred to the UPDATE'd firmware
    • it will attempt to blink a user led at an interval of 1 second.
    • and set a confirmation flag to indicate that the update was successful.
    • post which, it continuously blinks a user led at an interval of 1 second.

stm32f469

The stm32f469 example uses a STM32F469-IDISCOVERY BOARD. The board has four front LEDs, three of which are used in this example.

Note:

  • If you're using a different version of the board, you'll probably need to edit your firmware's partition-addresses to accommodate for differences.
  • Just make sure you don't change the names of files or the folder structure, as cargo xtask looks for these file/folder names.
  • On the discovery board, an external 1MB N25Q128A flash is mounted (in addition to the STM32 internal 1MB flash), but we will only use the internal flash in this example.

Partitioning:

The first step in integrating rustBoot is flash-memory partitioning i.e. we divide the stm32f469's flash-memory into 4 partitions, taking into account the geometry of the flash memory.

You can read more about mcu partitioning here

In this example, we'll be using the following partitioning scheme. You can locate these constants in the constants module

#![allow(unused)]
fn main() {
#[cfg(feature = "stm32f469")]
pub const SECTOR_SIZE: usize = 0x20000;
#[cfg(feature = "stm32f469")]
pub const PARTITION_SIZE: usize = 0x60000;
#[cfg(feature = "stm32f469")]
pub const BOOT_PARTITION_ADDRESS: usize = 0x08020000;
#[cfg(feature = "stm32f469")]
pub const UPDATE_PARTITION_ADDRESS: usize = 0x08080000;
#[cfg(feature = "stm32f469")]
pub const SWAP_PARTITION_ADDRESS: usize = 0x080e0000;
}
  • RUSTBOOT partition: contains the bootloader (its code and data) and a (test) public-key embedded as part of the bootloader image, starts at address 0x0800_0000.
  • BOOT partition: contains boot firmware, starts at address PARTITION_BOOT_ADDRESS.
  • UPDATE partition: contains update firmware, starts at address UPDATE_PARTITION_ADDRESS. The boot firmware is responsible for downloading and installing the update firmware into this partition via a secure channel.
  • SWAP partition: is the temporary swap space, starts at address SWAP_PARTITION_ADDRESS.

Compiling, Signing and Programming:

Now that we have properly partitioned the stm32f469's on-board flash-memory, the next step is - compiling, signing and programming

We will compile the following

  • bootloader
  • boot and update firmware

sign both pieces of firmware with a (test) private-key and finally create valid rustBoot mcu-images i.e. signed boot and update firmware images.

Note:

  • the ecc256.der file contains a public-key and a private-key, the first 64 bytes being the public-key and remaining 32 bytes make up the private-key.
  • This is a test key file and is to be used for testing purposes only.

Compiling, signing and programming can be performed via a single command

cargo stm32f469 build-sign-flash rustBoot [boot-ver] [updt-ver]

Note:

  • The updt-ver number should be greater than boot-ver.

This will build, sign and flash all 3 packages (i.e. bootloader + boot-fw + update-fw) onto the board.

Note:

  • The corresponding public-key is embedded in the bootloader's source.
  • In order to test this example, you'll have to install a couple of pre-requisites as it uses probe-run to flash the binary.
 cargo install probe-rs-cli 
 cargo install cargo-flash 
 cargo install cargo-binutils

Here's the command line output that should be produced.

lionel@saturn:/tmp/rustBoot$ cargo stm32f469 build-sign-flash rustBoot 1234 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/xtask stm32f469 build-sign-flash rustBoot 1234 1235`
$ cargo build --release
warning: unused config key `build.runner` in `/tmp/rustBoot/boards/firmware/stm32f469/boot_fw_blinky_green/.cargo/config.toml`
    Finished release [optimized] target(s) in 0.05s
$ cargo build --release
warning: unused config key `build.runner` in `/tmp/rustBoot/boards/firmware/stm32f469/updt_fw_blinky_red/.cargo/config.toml`
    Finished release [optimized] target(s) in 0.04s
$ cargo build --release
    Finished release [optimized] target(s) in 0.04s
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32f469_bootfw -O binary stm32f469_bootfw.bin
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32f469_updtfw -O binary stm32f469_updtfw.bin
$ cargo run mcu-image ../boards/sign_images/signed_images/stm32f469_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `/tmp/rustBoot/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/stm32f469_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234`

Image type:       mcu-image
Curve type:       nistp256
Input image:      stm32f469_bootfw.bin
Public key:       ecc256.der
Image version:    1234
Output image:     stm32f469_bootfw_v1234_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 2004 bytes.

$ cargo run mcu-image ../boards/sign_images/signed_images/stm32f469_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `/tmp/rustBoot/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/stm32f469_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235`

Image type:       mcu-image
Curve type:       nistp256
Input image:      stm32f469_updtfw.bin
Public key:       ecc256.der
Image version:    1235
Output image:     stm32f469_updtfw_v1235_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 2232 bytes.

$ probe-rs-cli erase --chip STM32F469NIHx
$ probe-rs-cli download --format Bin --base-address 0x8020000 --chip STM32F469NIHx stm32f469_bootfw_v1234_signed.bin
     Erasing sectors ✔ [00:00:02] [##############################################################################################################] 128.00KiB/128.00KiB @ 60.69KiB/s (eta 0s )
 Programming pages   ✔ [00:00:00] [################################################################################################################]  2.00KiB/ 2.00KiB @     630B/s (eta 0s )
    Finished in 2.249s
$ probe-rs-cli download --format Bin --base-address 0x8080000 --chip STM32F469NIHx stm32f469_updtfw_v1235_signed.bin
     Erasing sectors ✔ [00:00:02] [##############################################################################################################] 128.00KiB/128.00KiB @ 60.92KiB/s (eta 0s )
 Programming pages   ✔ [00:00:00] [################################################################################################################]  3.00KiB/ 3.00KiB @     772B/s (eta 0s )
    Finished in 2.244s
$ cargo flash --chip STM32F469NIHx --release
    Finished release [optimized] target(s) in 0.07s
    Flashing /tmp/rustBoot/boards/target/thumbv7em-none-eabihf/release/stm32f469
     Erasing sectors ✔ [00:00:01] [################################################################################################################] 48.00KiB/48.00KiB @ 39.74KiB/s (eta 0s )
 Programming pages   ✔ [00:00:01] [################################################################################################################] 40.00KiB/40.00KiB @ 14.95KiB/s (eta 0s )
    Finished in 2.443s
lionel@saturn:/tmp/rustBoot$

Verifying:

blinky leds are used to confirm that rustBoot works as expected. On the STM32F469-IDISCOVERY board, the LEDs we are referring to below correspond to the LEDs next to the LCD screen. Here's the flow

  • After flashing the demo, upon supplying power to the board or pressing the black (reset) button, rustBoot takes over
    • validates the firmware image stored in the BOOT partition
    • verifies the signature attached against a known public key stored in the rustBoot image.
  • If the signature checks out, rustBoot boots into the bootfw and blinks a green-led for a few seconds,
    • post which, the boot firmware triggers the update and performs a system reset.
  • Upon reset, the rustBoot again takes over
    • validates the firmware image stored in the UPDATE partition
    • swaps the contents of the BOOT and the UPDATE partitions
    • marks the new firmware in the BOOT partition as in state STATE_TESTING
    • boots into the UPDATE'd firmware
  • Now that execution-control has been transferred to the UPDATE'd firmware
    • it will attempt to blink a red-led
    • and set a confirmation flag to indicate that the update was successful.
    • post which, it will also turn on a blue-led and continue blinking a red-led.

stm32h723

The stm32h723 example uses a Nucleo-h723zg board. The board has three LEDs of which two LEDs are used in this example.

Note:

  • If you're using a different version of the board, you'll probably need to edit your firmware's partition-addresses to accommodate for differences.
  • Just make sure you don't change the names of files or the folder structure, as cargo xtask looks for these file/folder names.

Partitioning:

The first step in integrating rustBoot is flash-memory partitioning i.e. we divide the stm32h723's flash-memory into 4 partitions, taking into account the geometry of the flash memory.

Note:

  • Error correction code memory is a type of flash memory data storage that uses error correction code to detect and correct n-bit data corruption that occurs in memory.
  • Since the STM32H7 series devices have a 10-bit ECC function, it is not possible to write partition status/sector flags more than once, so use a bigger PARTITION_SIZE. The last 128KB sector in each partition will be reserved for bootloader flags or partition status.

You can read more about mcu partitioning here

In this example, we'll be using the following partitioning scheme. You can locate these constants in the constants module

#![allow(unused)]

fn main() {
#[cfg(feature = "stm32h723")]
pub const SECTOR_SIZE: usize = 0x20000;
#[cfg(feature = "stm32h723")]
pub const PARTITION_SIZE: usize = 0x40000;
#[cfg(feature = "stm32h723")]
pub const BOOT_PARTITION_ADDRESS: usize = 0x08020000;
#[cfg(feature = "stm32h723")]
pub const SWAP_PARTITION_ADDRESS: usize = 0x080A0000;
#[cfg(feature = "stm32h723")]
pub const UPDATE_PARTITION_ADDRESS: usize = 0x08060000;
}
  • RUSTBOOT partition: contains the bootloader (its code and data) and a (test) public-key embedded as part of the bootloader image, starts at address 0x0.
  • BOOT partition: contains boot firmware, starts at address PARTITION_BOOT_ADDRESS.
  • UPDATE partition: contains update firmware, starts at address UPDATE_PARTITION_ADDRESS. The boot firmware is responsible for downloading and installing the update firmware into this partition via a secure channel.
  • SWAP partition: is the temporary swap space, starts at address SWAP_PARTITION_ADDRESS.

Compiling, Signing and Programming:

Now that we have properly partitioned the stm32h723's on-board flash-memory, the next step is - compiling, signing and programming

We will compile the following

  • bootloader
  • boot and update firmware

sign both pieces of firmware with a (test) private-key and finally create valid rustBoot mcu-images i.e. signed boot and update firmware images.

Note:

  • the ecc256.der file contains a public-key and a private-key, the first 64 bytes being the public-key and remaining 32 bytes make up the private-key.
  • This is a test key file and is to be used for testing purposes only.

Compiling, signing and programming can be performed via a single command

cargo stm32h723 build-sign-flash rustBoot [boot-ver] [updt-ver]

Note:

  • The updt-ver number should be greater than boot-ver.

This will build, sign and flash all 3 packages (i.e. bootloader + boot-fw + update-fw) onto the board.

Note:

  • The corresponding public-key is embedded in the bootloader's source.
  • In order to test this example, you'll have to install a couple of pre-requisites - probe-rs-cli, cargo-binutils and cargo-flash.
$ cargo install probe-rs-cli

$ cargo install cargo-binutils

$ cargo install cargo-flash

Here's the command line output that should be produced.

$ cargo stm32h723 build-sign-flash rustBoot 1234 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
     Running `target/debug/xtask stm32h723 build-sign-flash rustBoot 1234 1235`
$ cargo build --release
   Compiling version_check v0.9.4
   Compiling typenum v1.15.0
   ...
   ...
   Compiling rustBoot-update v0.1.0 (/Users/imrankhaleelsab/Imran/Boschspace/RB_workspace/rustBoot-mcusigner/rustBoot/boards/update)
    Finished release [optimized] target(s) in 53.55s
$ cargo build --release
   Compiling stm32h723_updtfw v0.1.0 (/Users/imrankhaleelsab/Imran/Boschspace/RB_workspace/rustBoot-mcusigner/rustBoot/boards/firmware/stm32h723/updt_fw_blinky_red)
    Finished release [optimized] target(s) in 1.54s
$ cargo build --release
   Compiling stm32h723 v0.1.0 (/Users/imrankhaleelsab/Imran/Boschspace/RB_workspace/rustBoot-mcusigner/rustBoot/boards/bootloaders/stm32h723)
    Finished release [optimized] target(s) in 2.25s
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32h723_bootfw -O binary stm32h723_bootfw.bin
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32h723_updtfw -O binary stm32h723_updtfw.bin
$ cargo run mcu-image ../boards/rbSigner/signed_images/stm32h723_bootfw.bin nistp256 ../boards/rbSigner/keygen/ecc256.der 1234
   Compiling rbsigner v0.1.0 (/Users/imrankhaleelsab/Imran/Boschspace/RB_workspace/rustBoot-mcusigner/rustBoot/rbsigner)
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s
     Running `/Users/imrankhaleelsab/Imran/Boschspace/RB_workspace/rustBoot-mcusigner/rustBoot/target/debug/rbsigner mcu-image ../boards/rbSigner/signed_images/stm32h723_bootfw.bin nistp256 ../boards/rbSigner/keygen/ecc256.der 1234`

Update type       :Firmware
Curve type        :nistp256
Input image       :stm32h723_bootfw.bin
Public key        :ecc256.der
Image version     :1234
Output image      :stm32h723_bootfw_v1234_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 4608 bytes.

$ cargo run mcu-image ../boards/rbSigner/signed_images/stm32h723_updtfw.bin nistp256 ../boards/rbSigner/keygen/ecc256.der 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `/Users/imrankhaleelsab/Imran/Boschspace/RB_workspace/rustBoot-mcusigner/rustBoot/target/debug/rbsigner mcu-image ../boards/rbSigner/signed_images/stm32h723_updtfw.bin nistp256 ../boards/rbSigner/keygen/ecc256.der 1235`

Update type       :Firmware
Curve type        :nistp256
Input image       :stm32h723_updtfw.bin
Public key        :ecc256.der
Image version     :1235
Output image      :stm32h723_updtfw_v1235_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 4624 bytes.

$ probe-rs-cli erase --chip stm32h723ZGTx
$ probe-rs-cli download --format Bin --base-address 0x8020000 --chip STM32H723ZGTx stm32h723_bootfw_v1234_signed.bin
     Erasing sectors ✔ [00:00:02] [############################] 128.00KiB/128.00KiB @ 63.21KiB/s (eta 0s )
 Programming pages   ✔ [00:00:00] [##############################]  5.00KiB/ 5.00KiB @  1.05KiB/s (eta 0s )
    Finished in 2.165s
$ probe-rs-cli download --format Bin --base-address 0x8060000 --chip STM32H723ZGTx stm32h723_updtfw_v1235_signed.bin
     Erasing sectors ✔ [00:00:01] [############################] 128.00KiB/128.00KiB @ 64.68KiB/s (eta 0s )
 Programming pages   ✔ [00:00:00] [##############################]  5.00KiB/ 5.00KiB @  1.08KiB/s (eta 0s )
    Finished in 2.117s
$ cargo flash --chip stm32h723ZGTx --release
    Finished release [optimized] target(s) in 0.08s
    Flashing /Users/imrankhaleelsab/Imran/Boschspace/RB_workspace/rustBoot-mcusigner/rustBoot/boards/target/thumbv7em-none-eabihf/release/stm32h723
        WARN probe_rs::config::target > Using custom sequence for STM32H7
     Erasing sectors ✔ [00:00:01] [############################] 128.00KiB/128.00KiB @ 66.35KiB/s (eta 0s )
 Programming pages   ✔ [00:00:00] [##############################] 44.00KiB/44.00KiB @ 13.45KiB/s (eta 0s )
    Finished in 2.878s

Verifying:

blinky leds are used to confirm that rustBoot works as expected. Here's the flow

  • Upon supplying power to the board, rustBoot takes over
    • validates the firmware image stored in the BOOT partition
    • verifies the signature attached against a known public key stored in the rustBoot image.
  • If the signature checks out, rustBoot boots into the bootfw and blinks a green-led for a few seconds,
    • post which, the boot firmware triggers the update and performs a system reset.
  • Upon reset, the rustBoot again takes over
    • validates the firmware image stored in the UPDATE partition
    • swaps the contents of the BOOT and the UPDATE partitions
    • marks the new firmware in the BOOT partition as in state STATE_TESTING
    • boots into the UPDATE'd firmware
  • Now that execution-control has been transferred to the UPDATE'd firmware
    • it will attempt to blink a red-led
    • and set a confirmation flag to indicate that the update was successful.
    • post which, it continuously blinks a red-led.

stm32f746

The stm32f746 example uses a Nucleo-f746zg board. The board has three LEDs of which two LEDs are used in this example.

Note:

  • If you're using a different version of the board, you'll probably need to edit your firmware's partition-addresses to accommodate for differences.
  • Just make sure you don't change the names of files or the folder structure, as cargo xtask looks for these file/folder names.

Partitioning:

The first step in integrating rustBoot is flash-memory partitioning i.e. we divide the stm32f746's flash-memory into 4 partitions, taking into account the geometry of the flash memory.

You can read more about mcu partitioning here

In this example, we'll be using the following partitioning scheme. You can locate these constants in the constants module

#![allow(unused)]

fn main() {
#[cfg(feature = "stm32f746")]
pub const SECTOR_SIZE: usize = 0x40000; 
#[cfg(feature = "stm32f746")]
pub const PARTITION_SIZE: usize = 0x40000;
#[cfg(feature = "stm32f746")]
pub const BOOT_PARTITION_ADDRESS: usize = 0x08040000;
#[cfg(feature = "stm32f746")]
pub const SWAP_PARTITION_ADDRESS: usize = 0x080C0000;
#[cfg(feature = "stm32f746")]
pub const UPDATE_PARTITION_ADDRESS: usize = 0x08080000;
}
  • RUSTBOOT partition: contains the bootloader (its code and data) and a (test) public-key embedded as part of the bootloader image, starts at address 0x0.
  • BOOT partition: contains boot firmware, starts at address PARTITION_BOOT_ADDRESS.
  • UPDATE partition: contains update firmware, starts at address UPDATE_PARTITION_ADDRESS. The boot firmware is responsible for downloading and installing the update firmware into this partition via a secure channel.
  • SWAP partition: is the temporary swap space, starts at address SWAP_PARTITION_ADDRESS.

Compiling, Signing and Programming:

Now that we have properly partitioned the stm32f746's on-board flash-memory, the next step is - compiling, signing and programming

We will compile the following

  • bootloader
  • boot and update firmware

sign both pieces of firmware with a (test) private-key and finally create valid rustBoot mcu-images i.e. signed boot and update firmware images.

Note:

  • the ecc256.der file contains a public-key and a private-key, the first 64 bytes being the public-key and remaining 32 bytes make up the private-key.
  • This is a test key file and is to be used for testing purposes only.

Compiling, signing and programming can be performed via a single command

cargo stm32f746 build-sign-flash rustBoot [boot-ver] [updt-ver]

Note:

  • The updt-ver number should be greater than boot-ver.

This will build, sign and flash all 3 packages (i.e. bootloader + boot-fw + update-fw) onto the board.

Note:

  • The corresponding public-key is embedded in the bootloader's source.
  • In order to test this example, you'll have to install a couple of pre-requisites as it uses probe-run to flash the binary.
cargo install probe-rs-cli 
cargo install cargo-flash
cargo install cargo-binutils

Here's the command line output that should be produced.

udayakumar@udayakumar-VirtualBox:~/devspace/rustBoot$ cargo stm32f746 build-sign-flash rustBoot 1234 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/xtask stm32f746 build-sign-flash rustBoot 1234 1235`
$ cargo build --release
   Compiling stm32f746_bootfw v0.1.0 (/home/udayakumar/devspace/rustBoot/boards/firmware/stm32f746/boot_fw_blinky_green)
    Finished release [optimized] target(s) in 0.99s
$ cargo build --release
   Compiling stm32f746_updtfw v0.1.0 (/home/udayakumar/devspace/rustBoot/boards/firmware/stm32f746/updt_fw_blinky_red)
    Finished release [optimized] target(s) in 0.95s
$ cargo build --release
    Finished release [optimized] target(s) in 0.09s
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32f746_bootfw -O binary stm32f746_bootfw.bin
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32f746_updtfw -O binary stm32f746_updtfw.bin
$ cargo run mcu-image ../boards/sign_images/signed_images/stm32f746_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234
   Compiling libc v0.2.126
   Compiling rustBoot v0.1.0 (/home/udayakumar/devspace/rustBoot/rustBoot)
   Compiling filetime v0.2.17
   Compiling rbsigner v0.1.0 (/home/udayakumar/devspace/rustBoot/rbsigner)
    Finished dev [unoptimized + debuginfo] target(s) in 2.17s
     Running `/home/udayakumar/devspace/rustBoot/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/stm32f746_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234`

Update type:    Firmware
Curve type:       nistp256
Input image:      stm32f746_bootfw.bin
Public key:       ecc256.der
Image version:    1234
Output image:     stm32f746_bootfw_v1234_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 2780 bytes.

$ cargo run mcu-image ../boards/sign_images/signed_images/stm32f746_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `/home/udayakumar/devspace/rustBoot/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/stm32f746_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235`

Update type:    Firmware
Curve type:       nistp256
Input image:      stm32f746_updtfw.bin
Public key:       ecc256.der
Image version:    1235
Output image:     stm32f746_updtfw_v1235_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 2748 bytes.

$ probe-rs-cli erase --chip stm32f746zgtx
$ probe-rs-cli download --format Bin --base-address 0x8040000 --chip stm32f746zgtx stm32f746_bootfw_v1234_signed.bin
     Erasing sectors ✔ [00:00:03] [################################################################################################################] 256.00KiB/256.00KiB @ 72.27KiB/s (eta 0s )
 Programming pages   ✔ [00:00:02] [##################################################################################################################]  3.00KiB/ 3.00KiB @     353B/s (eta 0s )
    Finished in 6.153s
$ probe-rs-cli download --format Bin --base-address 0x8080000 --chip stm32f746zgtx stm32f746_updtfw_v1235_signed.bin
     Erasing sectors ✔ [00:00:03] [################################################################################################################] 256.00KiB/256.00KiB @ 72.63KiB/s (eta 0s )
 Programming pages   ✔ [00:00:02] [##################################################################################################################]  3.00KiB/ 3.00KiB @     351B/s (eta 0s )
    Finished in 6.171s
$ cargo flash --chip stm32f746zgtx --release
    Finished release [optimized] target(s) in 0.08s
    Flashing /home/udayakumar/devspace/rustBoot/boards/target/thumbv7em-none-eabihf/release/stm32f746
     Erasing sectors ✔ [00:00:02] [##################################################################################################################] 64.00KiB/64.00KiB @ 22.76KiB/s (eta 0s )
 Programming pages   ✔ [00:00:20] [##################################################################################################################] 43.00KiB/43.00KiB @  1.88KiB/s (eta 0s )
    Finished in 22.968s
udayakumar@udayakumar-VirtualBox:~/devspace/rustBoot$ 

Verifying:

blinky leds are used to confirm that rustBoot works as expected. Here's the flow

  • Upon supplying power to the board, rustBoot takes over
    • validates the firmware image stored in the BOOT partition
    • verifies the signature attached against a known public key stored in the rustBoot image.
  • If the signature checks out, rustBoot boots into the bootfw and blinks a green-led for a few seconds,
    • post which, the boot firmware triggers the update and performs a system reset.
  • Upon reset, the rustBoot again takes over
    • validates the firmware image stored in the UPDATE partition
    • swaps the contents of the BOOT and the UPDATE partitions
    • marks the new firmware in the BOOT partition as in state STATE_TESTING
    • boots into the UPDATE'd firmware
  • Now that execution-control has been transferred to the UPDATE'd firmware
    • it will attempt to blink a red-led
    • and set a confirmation flag to indicate that the update was successful.
    • post which, it continuously blinks a red-led.

stm32f334

The stm32f334 example uses a STM32F334 Nucleo-64 Board. The board has 1 user LED which will be blinking at different frequencies depending on whether we're executing boot-firmware or update-firmware.

Note

  • If you're using a different version of the board, you'll probably need to edit your firmware's partition-addresses to accommodate for differences.
  • Just make sure you don't change the names of files or the folder structure, as cargo xtask looks for these file/folder names.

Partitioning

The first step in integrating rustBoot is flash-memory partitioning i.e. we divide the stm32f334's flash-memory into 4 partitions, taking into account the geometry of the flash memory.

You can read more about mcu partitioning here

In this example, we'll be using the following partitioning scheme. You can locate these constants in the constants module

#![allow(unused)]
fn main() {
#[cfg(feature = "stm32f334")]
pub const SECTOR_SIZE: usize = 0x1800; 
#[cfg(feature = "stm32f334")]
pub const PARTITION_SIZE: usize = 0x1800;
#[cfg(feature = "stm32f334")]
pub const BOOT_PARTITION_ADDRESS: usize = 0x0800b800; //its is of 3 pages starting from this address as the boot firmware is 2.47KiB
#[cfg(feature = "stm32f334")]
pub const SWAP_PARTITION_ADDRESS: usize = 0x0800e800; //its is of 3 pages starting from this address
#[cfg(feature = "stm32f334")]
pub const UPDATE_PARTITION_ADDRESS: usize = 0x0800d000; //its is of 3 pages starting from this address as the update firmware is 2.50KiB

> Note: Choose the number of pages based on your boot and update firmware sizes.

}

RUSTBOOT partition: contains the bootloader (its code and data) and a (test) public-key embedded as part of the bootloader image, starts at address 0x0800_0000.

  • BOOT partition: contains boot firmware, starts at address PARTITION_BOOT_ADDRESS.
  • UPDATE partition: contains update firmware, starts at address UPDATE_PARTITION_ADDRESS. The boot firmware is responsible for downloading and installing the update firmware into this partition via a secure channel.
  • SWAP partition: is the temporary swap space, starts at address SWAP_PARTITION_ADDRESS.

Compiling, Signing and Programming:

Now that we have properly partitioned the stm32f334 Nucleo's on-board flash-memory, the next step is - compiling, signing and programming

We will compile the following

  • bootloader
  • boot and update firmware

sign both pieces of firmware with a (test) private-key and finally create valid rustBoot mcu-images i.e. signed boot and update firmware images.

Note:

  • the ecc256.der file contains a public-key and a private-key, the first 64 bytes being the public-key and remaining 32 bytes make up the private-key.
  • This is a test key file and is to be used for testing purposes only.

Compiling, signing and programming can be performed via a single command

cargo stm32f334 build-sign-flash rustBoot [boot-ver] [updt-ver]

Note:

  • The updt-ver number should be greater than boot-ver.

This will build, sign and flash all 3 packages (i.e. bootloader + boot-fw + update-fw) onto the board.

Note:

  • The corresponding public-key is embedded in the bootloader's source.
  • In order to test this example, you'll have to install a couple of pre-requisites as it uses probe-run to flash the binary.
cargo install probe-rs-cli 
cargo install cargo-flash 

Here's the command line output that should be produced.

sarathk@sarath:~/stm32f/pull_req/rustBoot$ cargo stm32f334 build-sign-flash rustBoot 1234 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/xtask stm32f334 build-sign-flash rustBoot 1234 1235`
$ cargo build --release
   Compiling rustBoot-hal v0.1.0 (/home/sarathk/stm32f/pull_req/rustBoot/boards/hal)
   Compiling rustBoot-update v0.1.0 (/home/sarathk/stm32f/pull_req/rustBoot/boards/update)
   Compiling stm32f334_bootfw v0.1.0 (/home/sarathk/stm32f/pull_req/rustBoot/boards/firmware/stm32f334/boot_fw_blinky_green)
    Finished release [optimized] target(s) in 1.60s
$ cargo build --release
   Compiling stm32f334_updtfw v0.1.0 (/home/sarathk/stm32f/pull_req/rustBoot/boards/firmware/stm32f334/updt_fw_blinky_red)
    Finished release [optimized] target(s) in 1.17s
$ cargo build --release
   Compiling defmt v0.3.2
   Compiling defmt-macros v0.3.2
   Compiling critical-section v0.2.7
   Compiling defmt-parser v0.3.1
   Compiling bare-metal v1.0.0
   Compiling bitflags v1.3.2
   Compiling defmt-rtt v0.3.2
   Compiling stm32f334 v0.1.0 (/home/sarathk/stm32f/pull_req/rustBoot/boards/bootloaders/stm32f334)
   Compiling proc-macro-error-attr v1.0.4
   Compiling proc-macro-error v1.0.4
    Finished release [optimized] target(s) in 9.29s
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32f334_bootfw -O binary stm32f334_bootfw.bin
$ rust-objcopy -I elf32-littlearm ../../target/thumbv7em-none-eabihf/release/stm32f334_updtfw -O binary stm32f334_updtfw.bin
$ cargo run mcu-image ../boards/sign_images/signed_images/stm32f334_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234
   Compiling libc v0.2.126
   Compiling filetime v0.2.17
   Compiling rustBoot v0.1.0 (/home/sarathk/stm32f/pull_req/rustBoot/rustBoot)
   Compiling rbsigner v0.1.0 (/home/sarathk/stm32f/pull_req/rustBoot/rbsigner)
    Finished dev [unoptimized + debuginfo] target(s) in 3.89s
     Running `/home/sarathk/stm32f/pull_req/rustBoot/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/stm32f334_bootfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1234`

Update type:    Firmware
Curve type:       nistp256
Input image:      stm32f334_bootfw.bin
Public key:       ecc256.der
Image version:    1234
Output image:     stm32f334_bootfw_v1234_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 2508 bytes.

$ cargo run mcu-image ../boards/sign_images/signed_images/stm32f334_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `/home/sarathk/stm32f/pull_req/rustBoot/target/debug/rbsigner mcu-image ../boards/sign_images/signed_images/stm32f334_updtfw.bin nistp256 ../boards/sign_images/keygen/ecc256.der 1235`

Update type:    Firmware
Curve type:       nistp256
Input image:      stm32f334_updtfw.bin
Public key:       ecc256.der
Image version:    1235
Output image:     stm32f334_updtfw_v1235_signed.bin
Calculating sha256 digest...
Signing the firmware...
Done.
Output image successfully created with 2484 bytes.

$ probe-rs-cli erase --chip stm32f334r8tx
$ probe-rs-cli download --format Bin --base-address 0x800b800 --chip stm32f334r8tx stm32f334_bootfw_v1234_signed.bin
     Erasing sectors ✔ [00:00:01] [################################################################################################]  4.00KiB/ 4.00KiB @  1.81KiB/s (eta 0s )
 Programming pages   ✔ [00:00:02] [################################################################################################]  3.00KiB/ 3.00KiB @     557B/s (eta 0s )
    Finished in 4.483s
$ probe-rs-cli download --format Bin --base-address 0x800d000 --chip stm32f334r8tx stm32f334_updtfw_v1235_signed.bin
     Erasing sectors ✔ [00:00:01] [################################################################################################]  4.00KiB/ 4.00KiB @  1.85KiB/s (eta 0s )
 Programming pages   ✔ [00:00:02] [################################################################################################]  3.00KiB/ 3.00KiB @     554B/s (eta 0s )
    Finished in 4.491s
$ cargo flash --chip stm32f334r8tx --release
    Finished release [optimized] target(s) in 0.08s
    Flashing /home/sarathk/stm32f/pull_req/rustBoot/boards/target/thumbv7em-none-eabihf/release/stm32f334
     Erasing sectors ✔ [00:00:10] [################################################################################################] 44.00KiB/44.00KiB @  4.03KiB/s (eta 0s )
 Programming pages   ✔ [00:00:20] [################################################################################################] 43.00KiB/43.00KiB @  1.31KiB/s (eta 0s )
    Finished in 31.232s

Verifying:

user led is used to confirm that rustBoot works as expected. Here's the flow

  • Upon supplying power to the board, rustBoot takes over
    • validates the firmware image stored in the BOOT partition
    • verifies the signature attached against a known public key stored in the rustBoot image.
  • If the signature checks out, rustBoot boots into the bootfw and blinks a user led for a few seconds, at an interval of 1 second three times.
    • post which, the boot firmware triggers the update and performs a system reset.
  • Upon reset, the rustBoot again takes over
    • validates the firmware image stored in the UPDATE partition
    • swaps the contents of the BOOT and the UPDATE partitions
    • marks the new firmware in the BOOT partition as in state STATE_TESTING
    • boots into the UPDATE'd firmware
  • Now that execution-control has been transferred to the UPDATE'd firmware
    • and set a confirmation flag to indicate that the update was successful.
    • post which, it continuously blinks a user led at an interval of 0.25 second continuously.

Secure Coding guidelines

Testing

Continuous Integration

For Developers

Reference Guide

Contributors

Here is a list of the contributors who have helped improve rustBoot. Big shout-out to them!

  1. Imran K
  2. Yashwanth Singh M
  3. Anand Gedam
  4. Udayakumar Hidakal
  5. Bobbili Sarath Kumar
  6. Abhishek Dhamale
  7. Lionel Ains

If you feel you're missing from this list, feel free to add yourself in a PR.