SWUpdate for Cubieboard QEMU
- July 21, 2024
Software update is an important part of design of modern embedded systems. It allows faster time to market, since fixes, security or stability updates or even new features can be pushed to the devices after they have been deployed.
The update mechanism usually depends on the internet connectivity, but if device is physically accessible it can also be done using some local interface, like USB or ethernet connection.
When it comes to the Embedded Linux, there are different software update solutions which integrate well with Yocto
All of the mentioned solutions provide similar functionalities and require similar steps when integrating into an existing build process: bootloader integration, special memory partitioning, a way to perform fallback in case of a failed update.
In this post the steps for integrating SWUpdate with Yocto in order to enable software update will be described.
All of the changes that will be described, plus a few more which are required for this to work, are available in the
scarthgap
branch of meta-mistra repository in Github.
The following items will be covered in this post
Software update basics
The design of software update mechanism for an embedded system should take several items into account:
- robustness: the upgrade process should be immune to sudden power-outages and should always have a way to return to functional state, even in a case of a failed or incomplete update
- minimal downtime: during the upgrade process the system should provide designed functionality ideally without interruptions
- security: there should be a way to check if the update file is not corrupted and that it comes from a trusted source
Of course, there are the usual factors which affect the embedded system design (cost, time, quality). Therefore, as with everything else, a form of compromise must be made when making the software update design.
From a high-level view, the robustness and length of downtime can be affected by the way the system memory is organized, whereas the security topic requires extra steps, both during the creation of the update file and during the upgrade process itself.
System memory partitioning
The premise is that the root filesystem partition cannot be updated while the system is running from it, so some other way has to be found to perform the update.
The two standard ways of partitioning system memory when talking about software update are the single (or recovery) and the double (or A/B) partitioning.
Recovery partitioning
In the single partitioning scheme, besides the root filesystem partition, there is also a recovery partition. The recovery partition holds only the minimal root filesystem, enough to perform the update.
When the system update is initiated, the system reboots into the recovery partition. Once in recovery, the update of the root filesystem partition is performed since the system is not running from it anymore. After the update, the system reboots into the regular root filesystem partition.
If the upgrade is interrupted or if the new version of the system does not work as expected, the system will reboot again into the recovery partition, so the robustness of the upgrade is maintained. It is then up to the recovery partition to handle that situation, either by flashing again “the golden image” (if it is available), or by connecting to the update server to obtain a new update file, or by enabling some physical interface (like USB) to perform the update manually. Of course, the rollback can also be available if there is room to store previous update images before starting an update with a new one, or to store compressed copy of the old root filesystem.
Looking at the downtime, it is not insignificant, since the system will not be running in normal operating mode across two reboots, and during the time it takes to perform the update while in recovery mode.
A/B partitioning
In the A/B partitioning there are two partitions that are used for holding copies of the root filesystem (A and B partitions). One partition is active, meaning the system has booted from it and it is used as current root filesystem partition. The other partition is inactive and not used during the current run.
When the update is started, the inactive partition is updated from the current active partition. Once update process is done, the reboot is performed and active and inactive partitions are switched, so the system will boot from the “other” partition, which is the active one now.
That way, in case of an update failure, there is always a partition to roll back to, since the partition that is inactive will hold the previous installation. It is just the matter of rebooting and changing the bootloader configuration to select the correct partition for mounting root filesystem.
This scheme is much better compared to the recovery scheme with respect to the downtime, since the system is not operational only during the reboot from current active partition to the new active partition. However, it requires more storage memory than the recovery-partition approach, since two root partitions are used.
More details about different partitioning approaches can be found in the SWupdate documentation.
In this example the A/B partitioning is used.
Read-only root filesystem
The robustness can be further improved by using a read-only access to the root filesystem.
Having the root filesystem read-only prevents all changes to the root filesystem, both unwanted (malicious or caused by mistake), but also improves the lifetime of the product since the underlying flash storage is not worn out.
However, having the whole root filesystem as read-only can impair the functionality of some services which need writable access. In order to mitigate this, different methods can be used to have parts of the filesystem writable by using overlayfs or by bind-mounting a writable directory over a certain path in the filesystem. Some of the examples for Yocto are the
- volatile-binds: allows providing list of paths that should have writable access; these paths will be mounted as tmpfs overlay if possible, otherwise a “copy-bind” mount will be performed
- overlayfs: allows specifying the paths that should be overlay mounted and backing storage for the overlay mount; also has separate class for /etc directory
There are other options, like
overlayroot,
which can make a writable overlay over the whole root filesystem, but that one requires initramfs to be able to execute
chroot
before system is initialized.
Besides the regular userspace services, the customer applications might need to store persistent information, so it is always good to have a persistent data storage. That is usually a separate partition which is mounted as read-write.
In this example the /data
partition is used for rw data storage, and volatile-binds
are already bundled and can be
extended.
SWUpdate introduction
SWUpdate is a software update mechanism for Linux systems, targeted at embedded systems. It supports different channels for providing the update file:
- directly from local filesystem
- local web interface (static web page that provides upload functionality)
- remote download, using integrated download client
- remote download, initiated by the remote server
The update file is a cpio archive, with sw-description
file in the beginning, followed by different parts of the
update binaries or images, as well as shell or lua scripts. Inside the sw-description
different partitions can be
specified, as well as bootloader commands or environment updates which need to be triggered during the update process.
SWUpdate also supports different methods for signing the update file and its contents, as well as encryption of the update file.
SWUpdate configuration
SWUpdate is highly customizable. The configuration is done in two steps, through the config
file (same as U-Boot or
Linux kernel, menuconfig
can be used) and the swupdate.cfg
file.
The config
file specifies the features that will be compiled into the swupdate
binary, which it will support at
runtime. That means that the specific instance can be configured to support only local web interface download, or to
support update file signing using the RSA key, but not to encrypt it.
The swupdate.cfg
file specifies the device configuration for the interfaces that are supported, like hardware ID, or
port to use for the remote connection.
Since the default SWupdate configuration has local update and update via local web server already enabled, the additional changes to the configuration are only to remove lua support (regular bash script will be used for pre/post hooks), enable systemd and few other minor changes.
# CONFIG_MTD is not set
# CONFIG_LUA is not set
CONFIG_SYSTEMD=y
CONFIG_ARCHIVE=y
CONFIG_LOCALE=y
CONFIG_BOOTLOADERHANDLER=y
CONFIG_UPDATE_STATE_CHOICE_BOOTLOADER=y
The remote update will not be considered in this blog post, since it also requires configuration of the hawkbit instance.
The swupdate.cfg
file holds only the configuration for the signing key and the local web server configuration
globals :
{
verbose = true;
loglevel = 5;
syslog = true;
public-key-file = "/lib/swupdate/mistra-test.cert.pem";
};
webserver :
{
document_root = "/www";
userid = 0;
groupid = 0;
};
Bootloader configuration
As with other software update mechanisms, the bootloader integration is the most important one.
The bootloader is supposed to handle the information about the active partition and to handle the rollback conditions (this will be covered in Rollback mechanism).
Since root partition information is passed as kernel boot argument, the bootloader should handle the partition selection during the update process. The information about the active partition is stored in the bootloader environment.
if test -z "$rootpart"; then
setenv rootpart 2
saveenv
fi
Rollback mechanism
The rollback mechanism allows to recover the system in the case of a failed update. A failed update in this context would be an update after which the system cannot boot, either due to kernel or root filesystem not being copied properly, or due to some misconfiguration.
A failed update could also be if the applications that implement the system functionality are not behaving as they should, but in those cases a different approach to rollback should be taken, where a certain “monitoring” application can detect and decide that something is wrong.
In order to handle situations where system keeps rebooting after a failed update, the U-Boot bootloader has built-in
support for boot counters. The boot counter can be enabled using the following config items (for Yocto it is enough to
add the .cfg
file with these lines to the u-boot recipe)
CONFIG_BOOTCOUNT_LIMIT=y
CONFIG_BOOTCOUNT_ENV=y
CONFIG_BOOTCOUNT_BOOTLIMIT=3
The boot counter starts from zero and is incremented every time the system boots if the variable upgrade_available
is
set to 1. This way, the counter will not be changed when performing a regular reboot.
If the counter value reaches the maximum configured, then the U-Boot will run the altbootcmd
, which can be configured
to perform the switching of the active partition.
if test -z "$altbootcmd"; then
setenv altbootcmd 'echo Rollback upgrade
if test ${rootpart} = 2; then setenv rootpart 3; else setenv rootpart 2; fi
setenv upgrade_available 0; setenv bootcount 0; saveenv
boot'
saveenv
fi
Since U-Boot environment will be holding the information about the boot counters, as well as the information about whether an upgrade is available, the userspace applications should also have a way to access those variables. The access to bootcounters is needed to be able to reset them on a successful system start, and upgrade in progress information should be set once the upgrade is initiated.
In order to provide the access, the libubootenv
is used. It provides access to the fw_printenv
and fw_setenv
applications, which can be used to read and write the U-Boot variables.
The only thing that is required for the libubootenv
to work is to configure where the environment is located, using
the fw_env.config
file. In the case of Allwinner-A10, the environment is kept in the boot partition (FAT32 formatted)
by default, so the contents of the fw_env.config
file are
/boot/uboot.env 0x0000 0x10000
Upgrade done service
Since the upgrade_available
variable is used to allow incrementing the boot counter when starting the system, it needs
to be handled properly from the userspace.
When the upgrade is performed, the variable needs to be set to 1. That should be specified in the sw-description
so it
will be handled by the swupdate
.
Once upgrade is done, the userspace has to confirm that the upgrade has been a success, and the best way is to set the
upgrade_available
to zero. This can be done in a script that runs at startup, but also can be a separate application
which monitors the system behavior after an upgrade.
An example of a systemd service which clears upgrade_available
with delay of 30 seconds from the start is presented
and used in this example (adapted from
RAUC)
[Unit]
Description=Service to mark the upgrade as success
After=boot-complete.target
Requires=boot-complete.target
DefaultDependencies=no
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=sleep 30 && fw_setenv upgrade_available 0
[Install]
WantedBy=multi-user.target
Signing the update file
The integrity of an update is an important aspect to consider.
In order to maintain integrity, the system should know that the update file comes from a trusted source. One way of doing it is having a trusted source sign the update file with a private key, and then provide the public certificate to the system. This way, the system can perform a runtime check if the certificate matches the key that was used for the signature.
The SWupdate manual has instructions on creating the CMS key and certificate.
openssl req -x509 -newkey rsa:4096 -nodes -keyout mistra-test.key.pem \
-out mistra-test.cert.pem -subj "/O=SWUpdate /CN=target"
After the certificate is available, the SWupdate configuration needs to be updated to use the CMS key and to generate
the sw-description.sig
file.
CONFIG_HASH_VERIFY=y
CONFIG_SIGNED_IMAGES=y
CONFIG_SIGALG_CMS=y
CONFIG_CMS_IGNORE_EXPIRED_CERTIFICATE=y
CONFIG_CMS_IGNORE_CERTIFICATE_PURPOSE=y
The key needs to be integrated into the build process, and the certificate should be copied to the root filesystem.
In this example, it was easy to put the key and the certificate in the
meta-mistra
layer, and update theswupdate
recipe to use them from that location. However, in a real build system, the private key should be kept in a secure location and obtained temporarily, only to facilitate the signing of the update file.
Testing
Several tests are interesting when it comes to testing software update.
The first and the most important is that it is working :) For that test, both the upgrade via command line and upgrade via local web server interface can be used.
Another thing that is very important is to verify that our signature checking is working. We can make a new temporary certificate, copy it to the image and try and upgrade, which should fail since the certificate will not match the key.
In the end, for testing robustness, we can verify that the boot counters are working as expected, and that if we do not
clear the upgrade_available
variable from userspace and keep rebooting the system, that the altbootcmd
will be
executed.
Building initial image
The original mistra-image
is not suitable for the A/B partition upgrade, since it has only one root filesystem
partition. Therefore, a new image, mistra-swupdate
is created, with the corresponding wks
file
part u-boot --source rawcopy --sourceparams="file=${SPL_BINARY}" --ondisk mmcblk0 --no-table --align 8
part /boot --source bootimg-partition --ondisk mmcblk0 --fstype=vfat --label boot --active --align 2048 --fixed-size ${SUNXI_BOOT_SPACE}
part / --source rootfs --ondisk mmcblk0 --fstype=ext4 --label root_A --align 2048 --size 256M
part / --source rootfs --ondisk mmcblk0 --fstype=ext4 --label root_B --align 2048 --size 256M
part --size 1G --ondisk mmcblk0 --fstype=ext4 --label data --align 2048
The wks
file defines a boot partition, two root filesystem partitions, and a data partition, intended to be used a
persistent storage between upgrades.
The fstab
is also updated to mount the /boot
and /data
partitions on every start
# We need to mount the first partition to access the U-Boot environment
/dev/mmcblk0p1 /boot auto defaults,sync 0 0
# Mount persistent data directory
/dev/mmcblk0p4 /data auto defaults,sync 0 0
The mistra-swupdate
image recipe is presented here
require recipes-extended/images/mistra-image.bb
CORE_IMAGE_EXTRA_INSTALL += " \
libubootenv-bin \
swupdate \
swupdate-www \
kernel-image \
kernel-devicetree \
"
IMAGE_FSTYPES = "ext4.gz wic.gz"
IMAGE_BOOT_FILES = "boot.scr"
IMAGE_FEATURES += "read-only-rootfs"
create_data_dir() {
mkdir -p ${IMAGE_ROOTFS}/data
}
IMAGE_PREPROCESS_COMMAND += "create_data_dir;"
WKS_FILES = "mistra-swupdate.wks.in"
It is built using
. ./setup-environment build_fb
MACHINE=cubieboard-ng DISTRO=mistra-framebuffer bitbake mistra-swupdate
After the build is done, the compressed wic image will be in
tmp/deploy/images/cubieboard-ng/mistra-swupdate-cubieboard-ng.rootfs.wic.gz
. The image is converted to sd.img
qemu-img create sd.img 2G
gunzip -c tmp/deploy/images/cubieboard-ng/mistra-swupdate-cubieboard-ng.rootfs.wic.gz | dd of=sd.img bs=1M iflag=fullblock oflag=direct conv=fsync
qemu-img resize sd.img 2G
and run inside QEMU using the following commands
qemu-system-arm -machine cubieboard \
-m 1G \
-drive file=sd.img,format=raw,if=sd \
-serial mon:stdio \
-nographic \
-net nic \
-net tap,ifname=qemu-tap0,script=no,downscript=no
U-Boot SPL 2024.01-g (Jan 08 2024 - 15:37:48 +0000)
DRAM: 1024 MiB
CPU: 1008000000Hz, AXI/AHB/APB: 3/2/2
Trying to boot from MMC1
U-Boot 2024.01-g (Jan 08 2024 - 15:37:48 +0000) Allwinner Technology
CPU: Allwinner A10 (SUN4I)
Model: Cubietech Cubieboard
DRAM: 1 GiB
Core: 74 devices, 24 uclasses, devicetree: separate
WDT: Not starting watchdog@1c20c90
MMC: mmc@1c0f000: 0
Loading Environment from FAT... Unable to read "uboot.env" from mmc0:1...
Unknown monitor
Unknown monitor
In: serial,usbkbd
Out: serial,vidconsole
Err: serial,vidconsole
Net:
Error: ethernet@1c0b000 No valid MAC address found.
No ethernet found.
starting USB...
Bus usb@1c14000: USB EHCI 0.00
Bus usb@1c14400: USB OHCI 0.0
Bus usb@1c1c000: USB EHCI 0.00
Bus usb@1c1c400: USB OHCI 0.0
scanning bus usb@1c14000 for devices... 1 USB Device(s) found
scanning bus usb@1c14400 for devices... 1 USB Device(s) found
scanning bus usb@1c1c000 for devices... 1 USB Device(s) found
scanning bus usb@1c1c400 for devices... 1 USB Device(s) found
scanning usb for storage devices... 0 Storage Device(s) found
Hit any key to stop autoboot: 0
...
Mistra FrameBuffer 4.0 cubieboard-ng ttyS0
cubieboard-ng login:
The SD card partition can be validated using fdisk
fdisk -l /dev/mmcblk0
Disk /dev/mmcblk0: 2048 MB, 2147483648 bytes, 4194304 sectors
32768 cylinders, 4 heads, 32 sectors/track
Units: sectors of 1 * 512 = 512 bytes
Device Boot StartCHS EndCHS StartLBA EndLBA Sectors Size Id Type
/dev/mmcblk0p1 * 32,0,1 671,3,32 4096 86015 81920 40.0M c Win95 FAT32 (LBA)
/dev/mmcblk0p2 672,0,1 1023,3,32 86016 767589 681574 332M 83 Linux
/dev/mmcblk0p3 1023,3,32 1023,3,32 770048 1451621 681574 332M 83 Linux
/dev/mmcblk0p4 1023,3,32 1023,3,32 1454080 3551231 2097152 1024M 83 Linux
The new U-Boot variables can be checked using the fw_printenv
command
fw_printenv bootcount rootpart upgrade_available
bootcount=1
rootpart=2
upgrade_available=
The
upgrade_available
is empty since the upgrade has not been started.
The swupdate
service will be running, which also means that it is accessible from the local web server interface at
“192.168.123.1:8080”
systemctl status swupdate
* swupdate.service - SWUpdate daemon
Loaded: loaded (/usr/lib/systemd/system/swupdate.service; enabled; preset: enabled)
Active: active (running) since Mon 2024-07-15 22:14:27 UTC; 6min ago
TriggeredBy: * swupdate.socket
Docs: https://github.com/sbabic/swupdate
https://sbabic.github.io/swupdate
Main PID: 198 (swupdate)
CPU: 4.339s
CGroup: /system.slice/swupdate.service
|-198 /usr/bin/swupdate -H cubieboard-ng 1.0 -e stable copy2 -p "'reboot'" -f /etc/swupdate.cfg -w "-r /www -p 8080"
`-220 /usr/bin/swupdate -H cubieboard-ng 1.0 -e stable copy2 -p "'reboot'" -f /etc/swupdate.cfg -w "-r /www -p 8080"
Installing update
The update-image
recipe is used to build the update image. It contains the rootfs defined by the mistra-swupdate
image, and bundles the sw-description
DESCRIPTION = "Example image demonstrating how to build SWUpdate compound image"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
inherit swupdate
SWUPDATE_SIGNING="CMS"
SWUPDATE_CMS_CERT="${TOPDIR}/../sources/meta-mistra/certs/mistra-test.cert.pem"
SWUPDATE_CMS_KEY="${TOPDIR}/../sources/meta-mistra/certs/mistra-test.key.pem"
SRC_URI = "\
file://shellscript.sh \
file://sw-description \
"
# images to build before building swupdate image
IMAGE_DEPENDS = "mistra-swupdate"
# images and files that will be included in the .swu image
SWUPDATE_IMAGES = "mistra-swupdate-cubieboard-ng.rootfs"
SWUPDATE_IMAGES_FSTYPES[mistra-swupdate-cubieboard-ng.rootfs] = ".ext4.gz"
It is built using the following command
MACHINE=cubieboard-ng DISTRO=mistra-framebuffer bitbake update-image
After the build is done, the resulting swu
file can be found in
tmp/deploy/images/cubieboard-ng/update-image-cubieboard-ng.rootfs.swu
.
Using the command line
In order to perform an update, the file needs to be transferred to the board. Since ssh is not configured, we will copy
the file to the /data
partition before starting the QEMU.
The partition can be mounted using the same steps as in QEMU board emulation part 2
sudo kpartx -av ./sd.img
sudo mkdir -p /run/mount/data
sudo mount /dev/mapper/loop0p4 /run/mount/data
After the partition is mounted, the file can be copied and partition unmounted
sudo cp tmp/deploy/images/cubieboard-ng/update-image-cubieboard-ng.rootfs.swu /run/mount/data
sudo umount /run/mount/data
sudo kpartx -d ./sd.img
The QEMU can be started, and after the system has booted, the upgrade process is run using
swupdate -H cubieboard-ng:1.0 -e stable,copy2 -i update-image-cubieboard-ng.rootfs.swu
SWUpdate v2024.05.2
Licensed under GPLv2. See source distribution for detailed copyright notices.
...
[INFO ] : SWUPDATE started : Software Update started !
[TRACE] : SWUPDATE running : [network_initializer] : Software update started
[TRACE] : SWUPDATE running : [extract_file_to_tmp] : Found file
[TRACE] : SWUPDATE running : [extract_file_to_tmp] : filename sw-description
[TRACE] : SWUPDATE running : [extract_file_to_tmp] : size 1904
[TRACE] : SWUPDATE running : [extract_file_to_tmp] : Found file
[TRACE] : SWUPDATE running : [extract_file_to_tmp] : filename sw-description.sig
[TRACE] : SWUPDATE running : [extract_file_to_tmp] : size 2161
[TRACE] : SWUPDATE running : [swupdate_verify_file] : Verified OK
[DEBUG] : SWUPDATE running : [parse_cfg] : Parsing config file /tmp/sw-description
[TRACE] : SWUPDATE running : [get_common_fields] : Version 0.1.0
[TRACE] : SWUPDATE running : [get_common_fields] : reboot_required 1
[TRACE] : SWUPDATE running : [parse_hw_compatibility] : Accepted Hw Revision : 1.0
[TRACE] : SWUPDATE running : [_parse_images] : Found compressed Image: mistra-swupdate-cubieboard-ng.rootfs.ext4.gz in device : /dev/mmcblk0p3 for handler raw
[TRACE] : SWUPDATE running : [_parse_scripts] : Found Script: shellscript.sh
[TRACE] : SWUPDATE running : [_parse_bootloader] : Bootloader var: upgrade_available = 1
[TRACE] : SWUPDATE running : [_parse_bootloader] : Bootloader var: rootpart = 3
[TRACE] : SWUPDATE running : [check_hw_compatibility] : Hardware cubieboard-ng Revision: 1.0
[TRACE] : SWUPDATE running : [check_hw_compatibility] : Hardware compatibility verified
[DEBUG] : SWUPDATE running : [preupdatecmd] : Running Pre-update command
[TRACE] : SWUPDATE running : [extract_files] : Found file
[TRACE] : SWUPDATE running : [extract_files] : filename shellscript.sh
[TRACE] : SWUPDATE running : [extract_files] : size 394 required
[TRACE] : SWUPDATE running : [extract_files] : Found file
[TRACE] : SWUPDATE running : [extract_files] : filename mistra-swupdate-cubieboard-ng.rootfs.ext4.gz
[TRACE] : SWUPDATE running : [extract_files] : size 23926605 required
[TRACE] : SWUPDATE running : [extract_padding] : Expecting up to 512 padding bytes at end-of-file
[TRACE] : SWUPDATE running : [network_initializer] : Valid image found: copying to FLASH
[INFO ] : SWUPDATE running : Installation in progress
[TRACE] : SWUPDATE running : [read_lines_notify] : call do_preinst
[TRACE] : SWUPDATE running : [read_lines_notify] : do_preinst
[TRACE] : SWUPDATE running : [__run_cmd] : /tmp/scripts/shellscript.sh preinst command returned 0
[TRACE] : SWUPDATE running : [install_single_image] : Found installer for stream mistra-swupdate-cubieboard-ng.rootfs.ext4.gz raw
[TRACE] : SWUPDATE running : [read_lines_notify] : call do_postinst
[TRACE] : SWUPDATE running : [read_lines_notify] : do_postinst
[TRACE] : SWUPDATE running : [__run_cmd] : /tmp/scripts/shellscript.sh postinst command returned 0
[INFO ] : SWUPDATE successful ! SWUPDATE successful !
[INFO ] : SWUPDATE running : [endupdate] : SWUpdate was successful !
[DEBUG] : SWUPDATE running : [postupdate] : Running Post-update command
The
-e stable,copy2
is required, and it changes based on the current partition from which the system is running. In this case, the system is running from /dev/mmcblk0p2 (copy1 partition), so the target is set to /dev/mmcblk0p3 (copy2)
Once the upgrade is complete, the system can be restarted using
reboot
, and we can check that the system is now running from the other partition
fw_printenv bootcount rootpart upgrade_available
bootcount=1
rootpart=3
upgrade_available=1
Of course, if the same command is run again after 30 seconds, it will just say that the upgrade_available
is equal to
zero
fw_printenv bootcount rootpart upgrade_available
bootcount=1
rootpart=3
upgrade_available=0
Using the local web server interface
If the local web server interface is used, we only need to provide the path to the correct update file, and it will do everything for us
During the update the progress bar is visible, and again after the upgrade we can check the U-Boot environment variables, to confirm that the upgrade is done.
Test with wrong certificate
In order to perform the test with the wrong certificate, a new certificate can be created following the steps from above.
Once the certificate is created, it can be copied to the root filesystem partition at
/usr/lib/swupdate/mistra-test.cert.pem
, and upgrade process can be started.
The output of SWupdate in that case is
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running : [network_thread] : Incoming network request: processing...
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [INFO ] : SWUPDATE started : Software Update started !
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running : [network_initializer] : Software update started
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running : [extract_file_to_tmp] : Found file
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running : [extract_file_to_tmp] : filename sw-description
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running : [extract_file_to_tmp] : size 1904
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running : [extract_file_to_tmp] : Found file
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running : [extract_file_to_tmp] : filename sw-description.sig
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [TRACE] : SWUPDATE running : [extract_file_to_tmp] : size 2161
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: 600311B6:error:17000064:CMS routines:cms_signerinfo_verify_cert:certificate verify error:/usr/src/debug/openssl/3.2.2/crypto/cms/cms_smime.c:290:Verify error: self-signed certificate
Jul 20 23:26:09 cubieboard-ng swupdate.sh[198]: [ERROR] : SWUPDATE failed [0] ERROR swupdate_cms_verify.c : swupdate_verify_file : 275 : Signature verification failed
The output indicates that the certificate on the root filesystem does not match the key that was used for signing the bundle, which is expected.
Testing rollback
In order to test the rollback, we can perform the upgrade, and then stop and restart QEMU for four times after the U-Boot loads the Linux kernel.
This way, the U-Boot will increment the boot counter every time, but the userspace service which is supposed to clear
upgrade_available
flag will not be executed.
After the fourth start, the altbootcmd
will be executed, which will cause the system to roll back to the old root
partition. The following U-Boot output can be noticed
Warning: Bootlimit (3) exceeded. Using altbootcmd.
Hit any key to stop autoboot: 0
Rollback upgrade
Also, the partitions can be checked to verify that the rollback has been performed.
Summary
In this post the steps for adding the SWupdate update mechanism to the Cubieboard-ng Yocto BSP were shown. Upgrade support is very important for modern embedded systems, since it allows both adding new features, improving security and deployment of bug fixes remotely after the device is put into the field.
Similar steps can be taken for other boards, but also for other upgrade mechanisms, like RAUC or Mender.
Some of the interesting improvements to the approach that is shown in this post would be to add separate partitions for the kernel and bootloader, which would allow individual updates of those components (not only with the whole root filesystem update as it is done now). Another thing that would be interesting is to use the eMMC bootparts for storing either U-Boot environment, or for storing copies of U-Boot, but QEMU does not support that level of detail in the eMMC memory emulation, so it requires a “real” board to implement and test.
In the end, the remote updates using Hawkbit server are left for some other post.