A Setup for Firmware Updates over the Air – Part 3 (Wireless Sensor Nodes: MCUboot)

Hands on the wireless sensor nodes – MCUboot

Last time, a zephyr based application was flashed. In this part, we will flash MCUboot, an open-source bootloader, which checks signatures of new applications before starting them. With MCUboot flashed, safely updating the application is possible. Today, MCUboot is installed, an application for MCUboot is built, flashed, and upgraded (still over cable) in a safe manner afterwards.

MCUboot

Unfortunately, just flashing zephyr applications directly onto the board is not sufficient for our over-the-air updates. The application, which will search for updates, can not overwrite itself. Therefore, as explained in the first part, we need a bootloader like MCUboot which ensures that only our software is started on the board. The idea is that MCUboot is the main application that will never be replaced. It contains a public key and bootstraps our business application, which is signed with the corresponding private key.

Installation

Let us download MCUboot on our system and install requirements (don’t forget to activate your virtualenv from part 2):

fota@demo:~/zephyrproject$ git clone https://github.com/JuulLabs-OSS/mcuboot
fota@demo:~/zephyrproject$ cd mcuboot
fota@demo:~/zephyrproject/mcuboot$ git checkout 9686e702e4b915b6bc24e9f829524346b7ee9e0a # I checked out this commit to ensure that we have a stable and working revision.
fota@demo:~/zephyrproject/mcuboot$ pip3 install -r scripts/requirements.txt

Modify the board layout

MCUboot partitions the flash space into four segments which are specified in the board layout file. For my board – the 96B Nitrogen – the boot partition is almost too small for the current MCUboot state (8 pages a 4KB). Therefore, if you use a newer MCUboot version, maybe the boot partition is too small, and you have to modify the flash layout.

If you use a newer version of zephyr, this step is unnecessary – we created a merge request with the fix described below, which already got merged. But maybe, this information is still relevant for you if MCUboot drastically increases in size, and you have to modify the layout again. In this case, do the following:

# replace vim with your favourite text editor and the boardname with your board
fota@demo:~/zephyrproject$ vim zephyr/boards/arm/96b_nitrogen/96b_nitrogen.dts

Modify the flash layout in a way that the boot_partition is big enough and make slot0 and slot1 smaller. Please note: slot0 and slot1 must be equally sized; otherwise a swap update is not possible!

A working layout can be found here. In this example, the boot_partition has size 0xa000 (10 pages, two more than before), slot0 and slot1 have size 0x33000 (each one page less than before). Also, the start addresses have been modified. Keep the start address from slot1 in mind (0x3d000) since we need it later when we manually flash an update.

Build and flash MCUboot

Now we can build and flash MCUboot. MCUboot is just a zephyr application and can be built and flashed like our hello_world example before.

fota@demo:~/zephyrproject$ cd ~/builds
fota@demo:~/zephyrproject/builds$ west build -d build-mcuboot -b 96b_nitrogen -s ../mcuboot/boot/zephyr/
fota@demo:~/zephyrproject/builds$ west flash -d build-mcuboot
ninja: no work to do.
Using runner: pyocd # if you use another board, you may see another runner here
Flashing Target Device
0000739:INFO:rom_table:AP#0 ROM table #0 @ 0xe00ff000 (designer=244 part=006)
0000772:INFO:rom_table:[0]
0000789:INFO:rom_table:[1]
0000810:INFO:rom_table:[2]
0000823:INFO:rom_table:[3]
0000836:INFO:rom_table:[4]
0000851:INFO:rom_table:[5]
[====================] 100%
0008773:INFO:loader:Erased chip, programmed 36864 bytes (9 pages), skipped 0 bytes (0 pages) at 4.68 kB/s

Be careful: If the number of programmed bytes is bigger than your boot partition, you will not be able to update your firmware. In this case, make your boot partition bigger like explained above. In our case, 10 pages each 4KB are enough.

If everything worked so far, you get an output like the following on the board:

***** Booting Zephyr OS v1.14.0 *****
[00:00:00.003,082]  mcuboot: Starting bootloader
[00:00:00.009,216]  mcuboot: Primary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.019,195]  mcuboot: Scratch: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.028,656]  mcuboot: Boot source: primary slot
[00:00:00.037,261]  mcuboot: Swap type: none
[00:00:00.042,449]  mcuboot: Unable to find bootable image

Don’t worry about the „<err> mcuboot: Unable to find bootable image“ message – currently, we just flashed the bootloader itself. The application which will be chain loaded is still missing. We will build that one next!

An application for MCUboot

We now build an application which will be loaded by MCUboot at start:

# Please note the CONFIG_BOOTLOADER_MCUBOOT option!
fota@demo:~/zephyrproject/builds$ west build -d build-hello_world_mcuboot -b 96b_nitrogen -s ../zephyr/samples/hello_world -- -DCONFIG_BOOTLOADER_MCUBOOT=y

The command line is almost identical to the previous one, except for the additional -DCONFIG_BOOTLOADER_MCUBOOT=y option. This tells the build system to compile for usage with MCUboot and sets the start of the code fragment on the start address of slot0 instead of 0 (where MCUboot currently is placed!). Otherwise, the application would jump to the wrong address or load the wrong data. The good thing is, with this option, you can compile all zephyr applications for usage together with MCUboot without touching your application code!

Now, a new step is added: We have to sign our application. MCUboot checks this signature and will not load unsigned images or images signed with another private key. We built MCUboot with the default RSA-key in the MCUboot repo, so we will also have to sign our applications with this key.

# --key specifys the key to sign the application image, it must be the same key compiled into MCUboot!
fota@demo:~/zephyrproject/builds$ west sign -t imgtool -p ../mcuboot/scripts/imgtool.py -d build-hello_world_mcuboot/ -- --key ../mcuboot/root-rsa-2048.pem

Please note: The private key we use here is publicly available in the repository of MCUboot, so everyone can sign images and load them onto our boards. This is only for demonstration purposes. For production, you must create and use your own private key (and keep it secret!)

Now, we can flash our signed application onto our board.

# --hex-file specifys the file to flash, the board partition is in the build directory
fota@demo:~/zephyrproject/builds$ west flash -d build-hello_world_mcuboot --hex-file zephyr.signed.hex

west will manage to add it into slot0 and does not overwrite our MCUboot boot loader.

If everything worked well, you now get another output on your board terminal:

***** Booting Zephyr OS zephyr-v1.14.0 *****
[00:00:00.003,082]  mcuboot: Starting bootloader
[00:00:00.009,246]  mcuboot: Primary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.019,226]  mcuboot: Scratch: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.028,686]  mcuboot: Boot source: primary slot
[00:00:00.037,292]  mcuboot: Swap type: none
[00:00:00.122,650]  mcuboot: Bootloader chainload address offset: 0xa000
[00:00:00.130,279]  mcuboot: Jumping to the first image slot
***** Booting Zephyr OS zephyr-v1.14.0 *****
Hello World! 96b_nitrogen

Unlike before, MCUboot now finds a bootable image and starts it. The specified address 0xa000 is the starting address of slot0.

You can now try to load an unsigned image or one signed with another key onto the board – MCUboot will not load it and print an error message!

Updating an application

Last but not least, let’s try our first secure update!

Therefore, let us modify our hello world application a bit.

# you can also copy the sample program first before modifying it 
fota@demo:~/zephyrproject/builds$ vim ~/zephyrproject/zephyr/samples/hello_world/src/main.c
# Now change something in the application. For example, add "New " in the print statement

We could also flash the same application unmodified, but we want to see that something changed!

We build it again like before, but in a new directory:

fota@demo:~/zephyrproject/builds$ west build -d build-new_hello_mcuboot -b 96b_nitrogen -s ../zephyr/samples/hello_world -- -DCONFIG_BOOTLOADER_MCUBOOT=y

Manually sign and flash

Unfortunately, now it gets a bit tricky. Currently, west does not support padding the image out of the box or flashing to a specific address (which is needed for our update), so we use the scripts directly.

Sign our new image with the following, a bit complex command:

# imgtool.py is a script shipped together with mcuboot, the most important options here explained
# --key specifies the key which should be used
# --slot-size is the size of the slot we changed in the board layout.
# --pad adds a trailer to the image with all informations needed to be used as an update
# see the MCUboot imgtool page for a detailed information on all arguments
fota@demo:~/zephyrproject/builds$ ../mcuboot/scripts/imgtool.py sign --key ../mcuboot/root-rsa-2048.pem --header-size 0x200 --align 8 --version 1.3 --slot-size 0x33000 --pad build-new_hello_mcuboot/zephyr/zephyr.bin signed-new_hello.bin

You will find a new binary „signed-new_hello.bin“ in the current directory (check that with ls)

Now, we have to manually flash the new signed image into slot1 so MCUboot can verify, swap and load it. We cannot use west here and have to use our own flash runner, in our case pyocd and specify the address manually. You get the address from the board layout page we modified before (the start address of slot1).

# replace pyocd with your runner -a with the argument to specify your flash address.
# -t is here to fix the pyocd bug explained before
fota@demo:~/zephyrproject/builds$ pyocd flash -a 0x3d000 -t nrf52 signed-new_hello.bin
[====================] 100%

Okay, that was not as comfortable as before, but who said you get updates without any effort? If you did everything correctly, you get an output like the following:

***** Booting Zephyr OS zephyr-v1.14.0 *****
[00:00:00.003,051]  mcuboot: Starting bootloader
[00:00:00.009,216]  mcuboot: Primary image: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.019,165]  mcuboot: Scratch: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.028,625]  mcuboot: Boot source: primary slot
[00:00:00.037,231]  mcuboot: Swap type: test
[00:00:01.110,717]  mcuboot: Bootloader chainload address offset: 0xa000
[00:00:01.118,255]  mcuboot: Jumping to the first image slot
***** Booting Zephyr OS zephyr-v1.14.0 *****
Hello New World! 96b_nitrogen

Pay attention to the „New“ in the text, and the „Swap type: test“. MCUboot verifies the signature, swaps both images, and loads our new application. The update worked!
But, the new image does not set itself as OK, so after a reset, the old image will be swapped back to slot0 and loaded:

***** Booting Zephyr OS zephyr-v1.14.0 *****
[00:00:00.003,143]  mcuboot: Starting bootloader
[00:00:00.009,307]  mcuboot: Primary image: magic=good, swap_type=0x2, copy_done=0x1, image_ok=0x3
[00:00:00.019,195]  mcuboot: Scratch: magic=unset, swap_type=0x1, copy_done=0x3, image_ok=0x3
[00:00:00.028,656]  mcuboot: Boot source: none
[00:00:00.034,057]  mcuboot: Swap type: revert 
[00:00:01.106,994]  mcuboot: Bootloader chainload address offset: 0xa000
[00:00:01.114,501]  mcuboot: Jumping to the first image slot
***** Booting Zephyr OS zephyr-v1.14.0 *****
Hello World! 96b_nitrogen

You did your first secure update – but still over cable. Of course, this is not enough – we do not want to plug our devices to a computer and update them manually. Therefore, we will connect our boards to a gateway over bluetooth in the next part to make over-the-air updates possible. We will use a Raspberry Pi Zero W, but every board with bluetooth and wireless is fine.