From 2b45e3f07291860342c586ef1f39f58f9633e5bd Mon Sep 17 00:00:00 2001 From: Koen Zandberg Date: Fri, 12 Feb 2021 16:49:42 +0100 Subject: [PATCH] examples/suit_update: Add compatibility with native --- examples/suit_update/Makefile | 8 +- examples/suit_update/Makefile.board.dep | 6 + examples/suit_update/README.hardware.md | 667 +++++++++++++++++++++++ examples/suit_update/README.md | 681 +----------------------- examples/suit_update/README.native.md | 343 ++++++++++++ examples/suit_update/main.c | 70 +++ examples/suit_update/native_steps.svg | 3 + 7 files changed, 1110 insertions(+), 668 deletions(-) create mode 100644 examples/suit_update/Makefile.board.dep create mode 100644 examples/suit_update/README.hardware.md create mode 100644 examples/suit_update/README.native.md create mode 100644 examples/suit_update/native_steps.svg diff --git a/examples/suit_update/Makefile b/examples/suit_update/Makefile index 1b10091ad2..ac0d3c8bae 100644 --- a/examples/suit_update/Makefile +++ b/examples/suit_update/Makefile @@ -40,7 +40,7 @@ QUIET ?= 1 # USEMODULE += nanocoap_sock sock_util -USEMODULE += suit suit_transport_coap suit_storage_flashwrite +USEMODULE += suit suit_transport_coap # Display a progress bar during firmware download USEMODULE += progress_bar @@ -54,6 +54,12 @@ CFLAGS += -DSUIT_MANIFEST_RESOURCE=\"$(SUIT_COAP_ROOT)/$(SUIT_NOTIFY_MANIFEST)\" # Enable test_utils_interactive_sync, only used when running automatic test DEFAULT_MODULE += test_utils_interactive_sync +ifeq ($(BOARD),native) + USE_ETHOS ?= 0 + # Configure two RAM regions with 2K each + CFLAGS += -DCONFIG_SUIT_STORAGE_RAM_REGIONS=2 -DCONFIG_SUIT_STORAGE_RAM_SIZE=2048 +endif + # Change this to 0 to not use ethos USE_ETHOS ?= 1 diff --git a/examples/suit_update/Makefile.board.dep b/examples/suit_update/Makefile.board.dep new file mode 100644 index 0000000000..cb109236cb --- /dev/null +++ b/examples/suit_update/Makefile.board.dep @@ -0,0 +1,6 @@ +ifeq ($(BOARD),native) + USEMODULE += suit_storage_ram + USEMODULE += gnrc_netdev_default +else + USEMODULE += suit_storage_flashwrite +endif diff --git a/examples/suit_update/README.hardware.md b/examples/suit_update/README.hardware.md new file mode 100644 index 0000000000..f3ada5be5c --- /dev/null +++ b/examples/suit_update/README.hardware.md @@ -0,0 +1,667 @@ +# Running SUIT on real hardware + +This guide shows how to perform an firmware update on a microcontroller running +RIOT. + +Table of contents: + +- [Setup][setup] + - [Setup a wired device using ethos][setup-wired] + - [Provision the device][setup-wired-provision] + - [Configure the network][setup-wired-network] + - [Alternative: Setup a wireless device behind a border router][setup-wireless] + - [Provision the wireless device][setup-wireless-provision] + - [Configure the wireless network][setup-wireless-network] + - [Alternative: Setup a wireless ble device and Linux host][setup-wireless] + - [Start aiocoap fileserver][start-aiocoap-fileserver] +- [Perform an update][update] + - [Build and publish the firmware update][update-build-publish] + - [Notify an update to the device][update-notify] +- [Detailed explanation][detailed-explanation] +- [Automatic test][test] + +## Setup +[setup]: #Setup + +### Setup a wired device using ethos +[setup-wired]: #Setup-a-wired-device-using-ethos + +#### Configure the network +[setup-wired-network]: #Configure-the-network + +In one terminal, start: + + $ sudo dist/tools/ethos/setup_network.sh riot0 2001:db8::/64 + +This will create a tap interface called `riot0`, owned by the user. It will +also run an instance of uhcpcd, which starts serving the prefix +`2001:db8::/64`. Keep the shell open as long as you need the network. +Make sure to exit the "make term" instance from the next section *before* +exiting this, as otherwise the "riot0" interface doesn't get cleaned up +properly. + +#### Provision the device +[setup-wired-provision]: #Provision-the-device + +In order to get a SUIT capable firmware onto the node, run + + $ BOARD=samr21-xpro make -C examples/suit_update clean flash -j4 + +This command also generates the cryptographic keys (private/public) used to +sign and verify the manifest and images. See the "Key generation" section in +[SUIT detailed explanation][detailed-explanation] for details. + +From another terminal on the host, add a routable address on the host `riot0` +interface: + + $ sudo ip address add 2001:db8::1/128 dev riot0 + +In another terminal, run: + + $ BOARD=samr21-xpro make -C examples/suit_update/ term + +### Alternative: Setup a wireless device behind a border router +[setup-wireless]: #Setup-a-wireless-device-behind-a-border-router + +If the workflow for updating using ethos is successful, you can try doing the +same over wireless network interfaces, by updating a node that is connected +wirelessly with a border router in between. + +Depending on your device you can use BLE or 802.15.4. + +#### Configure the wireless network + +[setup-wireless-network]: #Configure-the-wireless-network + +A wireless node has no direct connection to the Internet so a border router (BR) +between 802.15.4/BLE and Ethernet must be configured. +Any board providing a 802.15.4/BLE radio can be used as BR. + +If configuring a BLE network when flashing the device include +`USEMODULE+=nimble_autoconn_ipsp` in the application Makefile, or prefix all +your make commands with it (for the BR as well as the device), e.g.: + + $ USEMODULE+=nimble_autoconn_ipsp make BOARD=
+ +Plug the BR board on the computer and flash the +[gnrc_border_router](https://github.com/RIOT-OS/RIOT/tree/master/examples/gnrc_border_router) +application on it: + + $ make BOARD=
-C examples/gnrc_border_router flash + +In on terminal, start the network (assuming on the host the virtual port of the +board is `/dev/ttyACM0`): + + $ sudo ./dist/tools/ethos/start_network.sh /dev/ttyACM0 riot0 2001:db8::/64 + +Keep this terminal open. + +From another terminal on the host, add a routable address on the host `riot0` +interface: + + $ sudo ip address add 2001:db8::1/128 dev riot0 + +#### Provision the wireless device +[setup-wireless-provision]: #Provision-the-wireless-device +First un-comment L28 in the application [Makefile](Makefile) so `gnrc_netdev_default` +is included in the build. In this scenario the node will be connected through a border +router. Ethos must be disabled in the firmware when building and flashing the firmware: + + $ USE_ETHOS=0 BOARD=samr21-xpro make -C examples/suit_update clean flash -j4 + +Open a serial terminal on the device to get its global address: + + $ USE_ETHOS=0 BOARD=samr21-xpro make -C examples/suit_update term + +If the Border Router is already set up when opening the terminal you should get + + ... + + Iface 6 HWaddr: 0D:96 Channel: 26 Page: 0 NID: 0x23 + Long HWaddr: 79:7E:32:55:13:13:8D:96 + TX-Power: 0dBm State: IDLE max. Retrans.: 3 CSMA Retries: 4 + AUTOACK ACK_REQ CSMA L2-PDU:102 MTU:1280 HL:64 RTR + RTR_ADV 6LO IPHC + Source address length: 8 + Link type: wireless + inet6 addr: fe80::7b7e:3255:1313:8d96 scope: link VAL + inet6 addr: 2001:db8::7b7e:3255:1313:8d96 scope: global VAL + inet6 group: ff02::2 + inet6 group: ff02::1 + inet6 group: ff02::1:ff17:dd59 + inet6 group: ff02::1:ff00:2 + + suit_coap: started. + +Here the global IPv6 is `2001:db8::7b7e:3255:1313:8d96`. +**The address will be different according to your device and the chosen prefix**. +In this case the RIOT node can be reached from the host using its global address: + + $ ping6 2001:db8::7b7e:3255:1313:8d96 + +_NOTE_: when using BLE the connection might take a little longer, and you might not +see the global address right away. But the global address will always consist of the +the prefix (`2001:db8::`) and the EUI64 suffix, in this case `7b7e:3255:1313:8d96`. + +### Alternative: Setup a wireless ble device and Linux host + +- Make sure you fulfill the "Prerequisites" and "Preparing Linux" section in [README.ipv6-over-ble.md](../../pkg/nimble/README.ipv6-over-ble.md). + +- Provision the wireless ble device: + +``` + $ CFLAGS=-DCONFIG_GNRC_IPV6_NIB_SLAAC=1 USEMODULE+=nimble_autoconn_ipsp USE_ETHOS=0 BOARD=nrf52dk make -C examples/suit_update clean flash -j4 +``` + +- Open a serial terminal on the device to get its local address: + +``` + $ USE_ETHOS=0 BOARD=nrf52dk make -C examples/suit_update term +``` + + ... + Iface 8 HWaddr: E4:DD:E0:8F:73:65 + L2-PDU:1280 MTU:1280 HL:64 RTR + 6LO IPHC + Source address length: 6 + Link type: wireless + inet6 addr: fe80::e4dd:e0ff:fe8f:7365 scope: local VAL + inet6 group: ff02::2 + inet6 group: ff02::1 + inet6 group: ff02::1:ff8f:7365 + ... + + +**NOTE 2:** Currently, Linux does not support 6LoWPAN neighbor discovery (which +RIOT uses per default with BLE), so RIOT needs to be compiled to use stateless +address auto configuration (SLAAC) -> `CFLAGS=-DCONFIG_GNRC_IPV6_NIB_SLAAC=1`. + +- Use `bluetoothctl` on Linux to scan for the device. Once `bluetoothctl` has + started, issue `scan on` to start scanning. The default name for the RIOT + device is set to `RIOT-autoconn`, so you should see it pop up. You can also + use `devices` to list scanned devices. + + ... + $ bluetoothctl + Agent registered + [bluetooth]# scan on + Discovery started + [CHG] Controller F4:5C:89:9F:AC:7A Discovering: yes + [CHG] Device E4:DD:E0:8F:73:65 RSSI: -49 + [CHG] Device 43:1A:39:CD:39:B9 RSSI: -94 + ... + ... + [bluetooth]# devices + Device F0:36:27:6B:F1:8F Decathlon Dual HR + Device 69:B3:82:0B:73:C9 69-B3-82-0B-73-C9 + Device 43:1A:39:CD:39:B9 43-1A-39-CD-39-B9 + Device E4:DD:E0:8F:73:65 RIOT-autoconn + ... + +- Once you have the address, simply connect Linux to RIOT using the following +command: + + # Put your device address here... + # Note: the 2 after the address denotes a BLE public random address, default + # used by `nimble_netif` + echo "connect UU:VV:WW:XX:YY:ZZ 2" > /sys/kernel/debug/bluetooth/6lowpan_control + +- Verify that the ble interface has been correctly created: + + + $ ifconfig bt0 + + ... + bt0: flags=4161 mtu 1280 + inet6 fe80::19:86ff:fe00:16ca prefixlen 64 scopeid 0x20 + unspec 00-19-86-00-16-CA-00-1E-00-00-00-00-00-00-00-00 txqueuelen 1000 (UNSPEC) + RX packets 330 bytes 22891 (22.8 KB) + RX errors 0 dropped 0 overruns 0 frame 0 + TX packets 354 bytes 30618 (30.6 KB) + TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 + ... + + +- You should now be able to ping the device + + $ ping6 fe80::e4dd:e0ff:fe8f:7365%bt0 + +- **optional**: follow the guide for distributing a routable Prefix in + [README.ipv6-over-ble.md](../../pkg/nimble/README.ipv6-over-ble.md). + +If this was performed correctly then the `bt0` interface should now have a global +address: + + bt0: flags=4161 mtu 1280 + inet6 2001:db8::19:86ff:fe00:16ca prefixlen 64 scopeid 0x0 + inet6 fe80::19:86ff:fe00:16ca prefixlen 64 scopeid 0x20 + inet6 2001:db8::b004:c58:891f:aa09 prefixlen 64 scopeid 0x0 + unspec 00-19-86-00-16-CA-00-14-00-00-00-00-00-00-00-00 txqueuelen 1000 (UNSPEC) + RX packets 3 bytes 120 (120.0 B) + RX errors 0 dropped 0 overruns 0 frame 0 + TX packets 34 bytes 3585 (3.5 KB) + TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 + +In this case the address to use for `SUIT_COAP_SERVER` can be either the EUI64 +generated global address `[2001:db8::19:86ff:fe00:16ca]` or the random global address +`[2001:db8::b004:c58:891f:aa09]`. + +If for some reason this didn't work, you can manually set up an address for +the subnet: + + $ sudo ip address add 2001:db8::1/64 dev bt0 + +In this case the address used for `SUIT_COAP_SERVER` should be [`2001:db8::1`]. + +Route traffic going towards your subnet through bt0: + + $ sudo route -A inet6 add 2001:db8::/64 dev bt0 + +In either case the address used for `SUIT_CLIENT` should be the suffix of the link +local address for that device (`e4dd:e0ff:fe8f:7365` in our examples) and the +distributed prefix, i.e.: `SUIT_CLIENT=[2001:db8::e4dd:e0ff:fe8f:7365]` + +If this optional step is skipped then `SUIT_COAP_SERVER` will be +the link local address of the `bt0` interface and `SUIT_CLIENT` will be +the link local address of the device, with the interface specified. e.g: + + + SUIT_COAP_SERVER=[fe80::19:86ff:fe00:16ca] + SUIT_CLIENT=[fe80::e4dd:e0ff:fe8f:7365%bt0] + +### Start aiocoap-fileserver +[Start-aiocoap-fileserver]: #start-aiocoap-fileserver + +`aiocoap-fileserver` is used for hosting the firmwares available for updates. +Devices retrieve the new firmware using the CoAP protocol. + +Start `aiocoap-fileserver`: + + $ mkdir -p coaproot + $ aiocoap-fileserver coaproot + +Keep the server running in the terminal. + +## Perform an update +[update]: #Perform-an-update + +### Build and publish the firmware update +[update-build-publish]: #Build-and-publish-the-firmware-update + +Currently, the build system assumes that it can publish files by simply copying +them to a configurable folder. + +For this example, aiocoap-fileserver serves the files via CoAP. + +- To publish an update for a node in wired mode (behind ethos): + + $ BOARD=samr21-xpro SUIT_COAP_SERVER=[2001:db8::1] make -C examples/suit_update suit/publish + +- To publish an update for a node in wireless mode (behind a border router): + + $ BOARD=samr21-xpro USE_ETHOS=0 SUIT_COAP_SERVER=[2001:db8::1] make -C examples/suit_update suit/publish + +This publishes into the server a new firmware for a samr21-xpro board. You should +see 6 pairs of messages indicating where (filepath) the file was published and +the corresponding coap resource URI + + ... + published "/home/francisco/workspace/RIOT/examples/suit_update/bin/samr21-xpro/suit_update-riot.suitv3_signed.1557135946.bin" + as "coap://[2001:db8::1]/fw/samr21-xpro/suit_update-riot.suitv3_signed.1557135946.bin" + published "/home/francisco/workspace/RIOT/examples/suit_update/bin/samr21-xpro/suit_update-riot.suitv3_signed.latest.bin" + as "coap://[2001:db8::1]/fw/samr21-xpro/suit_update-riot.suitv3_signed.latest.bin" + ... + +### Notify an update to the device +[update-notify]: #Norify-an-update-to-the-device + +If the network has been started with a standalone node, the RIOT node should be +reachable via link-local EUI64 address on the ethos interface, e.g: + + + Iface 5 HWaddr: 02:BE:74:C0:2F:B9 + L2-PDU:1500 MTU:1500 HL:64 RTR + RTR_ADV + Source address length: 6 + Link type: wired + inet6 addr: fe80::7b7e:3255:1313:8d96 scope: link VAL + inet6 addr: fe80::2 scope: link VAL + inet6 group: ff02::2 + inet6 group: ff02::1 + inet6 group: ff02::1:ffc0:2fb9 + inet6 group: ff02::1:ff00:2 + +the EUI64 link local address is `fe80::7b7e:3255:1313:8d96` and +SUIT_CLIENT=[fe80::7b7e:3255:1313:8d96%riot0]. + +If it was setup as a wireless device it will be reachable via its global +address, e.g: + + + Iface 6 HWaddr: 0D:96 Channel: 26 Page: 0 NID: 0x23 + Long HWaddr: 79:7E:32:55:13:13:8D:96 + TX-Power: 0dBm State: IDLE max. Retrans.: 3 CSMA Retries: 4 + AUTOACK ACK_REQ CSMA L2-PDU:102 MTU:1280 HL:64 RTR + RTR_ADV 6LO IPHC + Source address length: 8 + Link type: wireless + inet6 addr: fe80::7b7e:3255:1313:8d96 scope: link VAL + inet6 addr: 2001:db8::7b7e:3255:1313:8d96 scope: global VAL + inet6 group: ff02::2 + inet6 group: ff02::1 + inet6 group: ff02::1:ff17:dd59 + inet6 group: ff02::1:ff00:2 + + +the global address is `2001:db8::7b7e:3255:1313:8d96` and +SUIT_CLIENT=[2001:db8::7b7e:3255:1313:8d96]. + +- In wired mode: + + $ SUIT_COAP_SERVER=[2001:db8::1] SUIT_CLIENT=[fe80::7b7e:3255:1313:8d96%riot] BOARD=samr21-xpro make -C examples/suit_update suit/notify + +- In wireless mode: + + $ SUIT_COAP_SERVER=[2001:db8::1] SUIT_CLIENT=[2001:db8::7b7e:3255:1313:8d96] BOARD=samr21-xpro make -C examples/suit_update suit/notify + + +This notifies the node of a new available manifest. Once the notification is +received by the device, it fetches it. + +If using `suit-v3` the node hangs for a couple of seconds when verifying the +signature: + + .... + suit_coap: got manifest with size 470 + suit: verifying manifest signature + .... + +Once the signature is validated it continues validating other parts of the +manifest. +Among these validations it checks some condition like firmware offset position +in regards to the running slot to see witch firmware image to fetch. + + .... + suit: validated manifest version + )suit: validated sequence number + )validating vendor ID + Comparing 547d0d74-6d3a-5a92-9662-4881afd9407b to 547d0d74-6d3a-5a92-9662-4881afd9407b from manifest + validating vendor ID: OK + validating class id + .... + +Once the manifest validation is complete, the application fetches the image +and starts flashing. +This step takes some time to fetch and write to flash. A progress bar is +displayed during this step: + + .... + Fetching firmware |█████████████ | 50% + .... + +Once the new image is written, a final validation is performed and, in case of +success, the application reboots on the new slot: + + Finalizing payload store + Verifying image digest + Starting digest verification against image + Install correct payload + Verifying image digest + Starting digest verification against image + Install correct payload + Image magic_number: 0x544f4952 + Image Version: 0x5fa52bcc + Image start address: 0x00201400 + Header chksum: 0x53bb3d33 + suit_coap: rebooting... + + main(): This is RIOT! (Version: )) + RIOT SUIT update example application + Running from slot 1 + ... + +The slot number should have changed from after the application reboots. +You can do the publish-notify sequence several times to verify this. + +## Detailed explanation +[detailed-explanation]: #Detailed-explanation + +### Node + +For the suit_update to work there are important modules that aren't normally built +in a RIOT application: + +* riotboot + * riotboot_flashwrite +* suit + * suit_transport_coap + +#### riotboot + +To be able to receive updates, the firmware on the device needs a bootloader +that can decide from witch of the firmware images (new one and olds ones) to boot. + +For suit updates you need at least two slots in the current conception on riotboot. +The flash memory will be divided in the following way: + +``` +|------------------------------- FLASH ------------------------------------------------------------| +|-RIOTBOOT_LEN-|------ RIOTBOOT_SLOT_SIZE (slot 0) ------|------ RIOTBOOT_SLOT_SIZE (slot 1) ------| + |----- RIOTBOOT_HDR_LEN ------| |----- RIOTBOOT_HDR_LEN ------| + --------------------------------------------------------------------------------------------------| +| riotboot | riotboot_hdr_1 + filler (0) | slot_0_fw | riotboot_hdr_2 + filler (0) | slot_1_fw | + --------------------------------------------------------------------------------------------------| +``` + +The riotboot part of the flash will not be changed during suit_updates but +be flashed a first time with at least one slot with suit_capable fw. + + $ BOARD=samr21-xpro make -C examples/suit_update clean flash + +When calling make with the `flash` argument it will flash the bootloader +and then to slot0 a copy of the firmware you intend to build. + +New images must be of course written to the inactive slot, the device mist be able +to boot from the previous image in case the update had some kind of error, eg: +the image corresponds to the wrong slot. + +On boot the bootloader will check the `riotboot_hdr` and boot on the newest +image. + +`riotboot_flashwrite` module is needed to be able to write the new firmware to +the inactive slot. + +riotboot is not supported by all boards. The default board is `samr21-xpro`, +but any board supporting `riotboot`, `flashpage` and with 256kB of flash should +be able to run the demo. + +#### suit + +The suit module encloses all the other suit_related module. Formally this only +includes the `sys/suit` directory into the build system dirs. + +- **suit_transport_coap** + +To enable support for suit_updates over coap a new thread is created. +This thread will expose 4 suit related resources: + +* /suit/slot/active: a resource that returns the number of their active slot +* /suit/slot/inactive: a resource that returns the number of their inactive slot +* /suit/trigger: this resource allows POST/PUT where the payload is assumed +tu be a url with the location of a manifest for a new firmware update on the +inactive slot. +* /suit/version: this resource is currently not implemented and return "NONE", +it should return the version of the application running on the device. + +When a new manifest url is received on the trigger resource a message is resent +to the coap thread with the manifest's url. The thread will then fetch the +manifest by a block coap request to the specified url. + +- **support for v3** + +This includes v3 manifest support. When a url is received in the /suit/trigger +coap resource it will trigger a coap blockwise fetch of the manifest. When this +manifest is received it will be parsed. The signature of the manifest will be +verified and then the rest of the manifest content. If the received manifest is valid it +will extract the url for the firmware location from the manifest. + +It will then fetch the firmware, write it to the inactive slot and reboot the device. +Digest validation is done once all the firmware is written to flash. +From there the bootloader takes over, verifying the slot riotboot_hdr and boots +from the newest image. + +#### Key Generation + +To sign the manifest and for the device to verify the manifest a pair of keys +must be generated. Note that this is done automatically when building an +updatable RIOT image with `riotboot` or `suit/publish` make targets. + +This is simply done using the `suit/genkey` make target: + + $ BOARD=samr21-xpro make -C examples/suit_update suit/genkey + +You will get this message in the terminal: + + Generated public key: 'a0fc7fe714d0c81edccc50c9e3d9e6f9c72cc68c28990f235ede38e4553b4724' + +### Network + +For connecting the device with the internet we are using ethos (a simple +ethernet over serial driver). + +When executing $RIOTBASE/dist/tools/ethos: + + $ sudo ./start_network.sh /dev/ttyACM0 riot0 2001:db8::1/64 + +A tap interface named `riot0` is setup. `fe80::1/64` is set up as it's +link local address and `fd00:dead:beef::1/128` as the "lo" unique link local address. + +Also `2001:db8::1/64` is configured- as a prefix for the network. It also sets-up +a route to the `2001:db8::1/64` subnet through `fe80::2`. Where `fe80::2` is the default +link local address of the UHCP interface. + +Finally when: + + $ sudo ip address add 2001:db8::1/128 dev riot0 + +We are adding a routable address to the riot0 tap interface. The device can +now send messages to the the coap server through the riot0 tap interface. You could +use a different address for the coap server as long as you also add a routable +address, so: + + $ sudo ip address add $(SUIT_COAP_SERVER) dev riot0 + +When using a border router the same thing is happening although the node is no +longer reachable through its link local address but routed through to border router +so we can reach it with its global address. + +NOTE: if we weren't using a local server you would need to have ipv6 support +on your network or use tunneling. + +NOTE: using `fd00:dead:beef::1` as an address for the coap server would also +work and you wouldn't need to add a routable address to the tap interface since +a route to the loopback interface (`lo`) is already configured. + +### Server and file system variables + +The following variables are defined in makefiles/suit.inc.mk: + + SUIT_COAP_BASEPATH ?= firmware/$(APPLICATION)/$(BOARD) + SUIT_COAP_SERVER ?= localhost + SUIT_COAP_ROOT ?= coap://$(SUIT_COAP_SERVER)/$(SUIT_COAP_BASEPATH) + SUIT_COAP_FSROOT ?= $(RIOTBASE)/coaproot + SUIT_PUB_HDR ?= $(BINDIR)/riotbuild/public_key.h + +The following convention is used when naming a manifest + + SUIT_MANIFEST ?= $(BINDIR_APP)-riot.suitv3.$(APP_VER).bin + SUIT_MANIFEST_LATEST ?= $(BINDIR_APP)-riot.suitv3.latest.bin + SUIT_MANIFEST_SIGNED ?= $(BINDIR_APP)-riot.suitv3_signed.$(APP_VER).bin + SUIT_MANIFEST_SIGNED_LATEST ?= $(BINDIR_APP)-riot.suitv3_signed.latest.bin + +The following default values are using for generating the manifest: + + SUIT_VENDOR ?= "riot-os.org" + SUIT_SEQNR ?= $(APP_VER) + SUIT_CLASS ?= $(BOARD) + SUIT_KEY ?= default + SUIT_KEY_DIR ?= $(RIOTBASE)/keys + SUIT_SEC ?= $(SUIT_KEY_DIR)/$(SUIT_KEY).pem + +All files (both slot binaries, both manifests, copies of manifests with +"latest" instead of `$APP_VER` in riotboot build) are copied into the folder +`$(SUIT_COAP_FSROOT)/$(SUIT_COAP_BASEPATH)`. The manifests contain URLs to +`$(SUIT_COAP_ROOT)/*` and are signed that way. + +The whole tree under `$(SUIT_COAP_FSROOT)` is expected to be served via CoAP +under `$(SUIT_COAP_ROOT)`. This can be done by e.g., `aiocoap-fileserver $(SUIT_COAP_FSROOT)`. + +### Makefile recipes + +The following recipes are defined in makefiles/suit.inc.mk: + +suit/manifest: creates a non signed and signed manifest, and also a latest tag for these. + It uses following parameters: + + - $(SUIT_KEY): name of key to sign the manifest + - $(SUIT_COAP_ROOT): coap root address + - $(SUIT_CLASS) + - $(SUIT_VERSION) + - $(SUIT_VENDOR) + +suit/publish: makes the suit manifest, `slot*` bin and publishes it to the + aiocoap-fileserver + + 1.- builds slot0 and slot1 bin's + 2.- builds manifest + 3.- creates $(SUIT_COAP_FSROOT)/$(SUIT_COAP_BASEPATH) directory + 4.- copy's binaries to $(SUIT_COAP_FSROOT)/$(SUIT_COAP_BASEPATH) + - $(SUIT_COAP_ROOT): root url for the coap resources + +suit/notify: triggers a device update, it sends two requests: + + 1.- COAP get to check which slot is inactive on the device + 2.- COAP POST with the url where to fetch the latest manifest for + the inactive slot + + - $(SUIT_CLIENT): define the client ipv6 address + - $(SUIT_COAP_ROOT): root url for the coap resources + - $(SUIT_NOTIFY_MANIFEST): name of the manifest to notify, `latest` by + default. + +suit/genkey: this recipe generates a ed25519 key to sign the manifest + +**NOTE**: to plugin a new server you would only have to change the suit/publish +recipe, respecting or adjusting to the naming conventions.** + +## Automatic test +[Automatic test]: #test + +This applications ships with an automatic test. The test script itself expects +the application and bootloader to be flashed. It will then create two more +manifests with increasing version numbers and update twice, confirming after +each update that the newly flashed image is actually running. + +To run the test, + +- ensure the [prerequisites] are installed + +- make sure aiocoap-fileserver is in $PATH + +- compile and flash the application and bootloader: + +``` + $ make -C examples/suit_update clean all flash -j4 +``` + +- [set up the network][setup-wired-network] (in another shell): + +``` + $ sudo dist/tools/ethos/setup_network.sh riot0 2001:db8::/64 +``` + +- run the test: + +``` + $ make -C examples/suit_update test +``` diff --git a/examples/suit_update/README.md b/examples/suit_update/README.md index d77fe2ffdb..1360ef1906 100644 --- a/examples/suit_update/README.md +++ b/examples/suit_update/README.md @@ -8,25 +8,14 @@ the manifest format specified in **WARNING**: This code should not be considered production ready for the time being. It has not seen much exposure or security auditing. -Table of contents: +This document describes the preliminary requirements for using the SUIT workflow +to update binaries on RIOT. + +Table of Contents: - [Prerequisites][prerequisites] - - [Ble][prerequisites-ble] -- [Setup][setup] - [Signing key management][key-management] - - [Setup a wired device using ethos][setup-wired] - - [Provision the device][setup-wired-provision] - - [Configure the network][setup-wired-network] - - [Alternative: Setup a wireless device behind a border router][setup-wireless] - - [Provision the wireless device][setup-wireless-provision] - - [Configure the wireless network][setup-wireless-network] - - [Alternative: Setup a wireless ble device and Linux host][setup-wireless] - - [Start aiocoap fileserver][start-aiocoap-fileserver] -- [Perform an update][update] - - [Build and publish the firmware update][update-build-publish] - - [Notify an update to the device][update-notify] -- [Detailed explanation][detailed-explanation] -- [Automatic test][test] +- [workflows][workflows] ## Prerequisites [prerequisites]: #Prerequisites @@ -53,8 +42,8 @@ Table of contents: $ git clone https://github.com/RIOT-OS/RIOT $ cd RIOT -- In all setup below, `ethos` (EThernet Over Serial) is used to provide an IP - link between the host computer and a board. +- In all hardware-based setup below, `ethos` (EThernet Over Serial) is used to + provide an IP link between the host computer and a board. Just build `ethos` and `uhcpd` with the following commands: @@ -66,14 +55,6 @@ Table of contents: See [update] for more information. -### Ble -[prerequisites-ble]: #Ble - -Make sure you fulfill the "Prerequisites" and "Preparing Linux" section in [README.ipv6-over-ble.md](../../pkg/nimble/README.ipv6-over-ble.md). - -## Setup -[setup]: #Setup - ### Key Management [key-management]: #Key-management @@ -86,646 +67,12 @@ private key file, but has an extra `.pub` appended. If the chosen key doesn't exist, it will be generated automatically. That step can be done manually using the `suit/genkey` target. -### Setup a wired device using ethos -[setup-wired]: #Setup-a-wired-device-using-ethos +## Workflows +[workflows]: #workflows -#### Configure the network -[setup-wired-network]: #Configure-the-network +Two workflows are available with this example. The first one demonstrates the +SUIT workflow on a RIOT native instance on Linux. +The workflow described aims to update the firmware on real-world hardware. -In one terminal, start: - - $ sudo dist/tools/ethos/setup_network.sh riot0 2001:db8::/64 - -This will create a tap interface called `riot0`, owned by the user. It will -also run an instance of uhcpcd, which starts serving the prefix -`2001:db8::/64`. Keep the shell open as long as you need the network. -Make sure to exit the "make term" instance from the next section *before* -exiting this, as otherwise the "riot0" interface doesn't get cleaned up -properly. - -#### Provision the device -[setup-wired-provision]: #Provision-the-device - -In order to get a SUIT capable firmware onto the node, run - - $ BOARD=samr21-xpro make -C examples/suit_update clean flash -j4 - -This command also generates the cryptographic keys (private/public) used to -sign and verify the manifest and images. See the "Key generation" section in -[SUIT detailed explanation][detailed-explanation] for details. - -From another terminal on the host, add a routable address on the host `riot0` -interface: - - $ sudo ip address add 2001:db8::1/128 dev riot0 - -In another terminal, run: - - $ BOARD=samr21-xpro make -C examples/suit_update/ term - -### Alternative: Setup a wireless device behind a border router -[setup-wireless]: #Setup-a-wireless-device-behind-a-border-router - -If the workflow for updating using ethos is successful, you can try doing the -same over wireless network interfaces, by updating a node that is connected -wirelessly with a border router in between. - -Depending on your device you can use BLE or 802.15.4. - -#### Configure the wireless network - -[setup-wireless-network]: #Configure-the-wireless-network - -A wireless node has no direct connection to the Internet so a border router (BR) -between 802.15.4/BLE and Ethernet must be configured. -Any board providing a 802.15.4/BLE radio can be used as BR. - -If configuring a BLE network when flashing the device include -`USEMODULE+=nimble_autoconn_ipsp` in the application Makefile, or prefix all -your make commands with it (for the BR as well as the device), e.g.: - - $ USEMODULE+=nimble_autoconn_ipsp make BOARD=
- -Plug the BR board on the computer and flash the -[gnrc_border_router](https://github.com/RIOT-OS/RIOT/tree/master/examples/gnrc_border_router) -application on it: - - $ make BOARD=
-C examples/gnrc_border_router flash - -In on terminal, start the network (assuming on the host the virtual port of the -board is `/dev/ttyACM0`): - - $ sudo ./dist/tools/ethos/start_network.sh /dev/ttyACM0 riot0 2001:db8::/64 - -Keep this terminal open. - -From another terminal on the host, add a routable address on the host `riot0` -interface: - - $ sudo ip address add 2001:db8::1/128 dev riot0 - -#### Provision the wireless device -[setup-wireless-provision]: #Provision-the-wireless-device -First un-comment L28 in the application [Makefile](Makefile) so `gnrc_netdev_default` -is included in the build. In this scenario the node will be connected through a border -router. Ethos must be disabled in the firmware when building and flashing the firmware: - - $ USE_ETHOS=0 BOARD=samr21-xpro make -C examples/suit_update clean flash -j4 - -Open a serial terminal on the device to get its global address: - - $ USE_ETHOS=0 BOARD=samr21-xpro make -C examples/suit_update term - -If the Border Router is already set up when opening the terminal you should get - - ... - - Iface 6 HWaddr: 0D:96 Channel: 26 Page: 0 NID: 0x23 - Long HWaddr: 79:7E:32:55:13:13:8D:96 - TX-Power: 0dBm State: IDLE max. Retrans.: 3 CSMA Retries: 4 - AUTOACK ACK_REQ CSMA L2-PDU:102 MTU:1280 HL:64 RTR - RTR_ADV 6LO IPHC - Source address length: 8 - Link type: wireless - inet6 addr: fe80::7b7e:3255:1313:8d96 scope: link VAL - inet6 addr: 2001:db8::7b7e:3255:1313:8d96 scope: global VAL - inet6 group: ff02::2 - inet6 group: ff02::1 - inet6 group: ff02::1:ff17:dd59 - inet6 group: ff02::1:ff00:2 - - suit_coap: started. - -Here the global IPv6 is `2001:db8::7b7e:3255:1313:8d96`. -**The address will be different according to your device and the chosen prefix**. -In this case the RIOT node can be reached from the host using its global address: - - $ ping6 2001:db8::7b7e:3255:1313:8d96 - -_NOTE_: when using BLE the connection might take a little longer, and you might not -see the global address right away. But the global address will always consist of the -the prefix (`2001:db8::`) and the EUI64 suffix, in this case `7b7e:3255:1313:8d96`. - -### Alternative: Setup a wireless ble device and Linux host - -- Complete [Ble][prerequisites-ble]. - -- Provision the wireless ble device: - -``` - $ CFLAGS=-DCONFIG_GNRC_IPV6_NIB_SLAAC=1 USEMODULE+=nimble_autoconn_ipsp USE_ETHOS=0 BOARD=nrf52dk make -C examples/suit_update clean flash -j4 -``` - -- Open a serial terminal on the device to get its local address: - -``` - $ USE_ETHOS=0 BOARD=nrf52dk make -C examples/suit_update term -``` - - ... - Iface 8 HWaddr: E4:DD:E0:8F:73:65 - L2-PDU:1280 MTU:1280 HL:64 RTR - 6LO IPHC - Source address length: 6 - Link type: wireless - inet6 addr: fe80::e4dd:e0ff:fe8f:7365 scope: local VAL - inet6 group: ff02::2 - inet6 group: ff02::1 - inet6 group: ff02::1:ff8f:7365 - ... - - -**NOTE 2:** Currently, Linux does not support 6LoWPAN neighbor discovery (which -RIOT uses per default with BLE), so RIOT needs to be compiled to use stateless -address auto configuration (SLAAC) -> `CFLAGS=-DCONFIG_GNRC_IPV6_NIB_SLAAC=1`. - -- Use `bluetoothctl` on Linux to scan for the device. Once `bluetoothctl` has - started, issue `scan on` to start scanning. The default name for the RIOT - device is set to `RIOT-autoconn`, so you should see it pop up. You can also - use `devices` to list scanned devices. - - ... - $ bluetoothctl - Agent registered - [bluetooth]# scan on - Discovery started - [CHG] Controller F4:5C:89:9F:AC:7A Discovering: yes - [CHG] Device E4:DD:E0:8F:73:65 RSSI: -49 - [CHG] Device 43:1A:39:CD:39:B9 RSSI: -94 - ... - ... - [bluetooth]# devices - Device F0:36:27:6B:F1:8F Decathlon Dual HR - Device 69:B3:82:0B:73:C9 69-B3-82-0B-73-C9 - Device 43:1A:39:CD:39:B9 43-1A-39-CD-39-B9 - Device E4:DD:E0:8F:73:65 RIOT-autoconn - ... - -- Once you have the address, simply connect Linux to RIOT using the following -command: - - # Put your device address here... - # Note: the 2 after the address denotes a BLE public random address, default - # used by `nimble_netif` - echo "connect UU:VV:WW:XX:YY:ZZ 2" > /sys/kernel/debug/bluetooth/6lowpan_control - -- Verify that the ble interface has been correctly created: - - - $ ifconfig bt0 - - ... - bt0: flags=4161 mtu 1280 - inet6 fe80::19:86ff:fe00:16ca prefixlen 64 scopeid 0x20 - unspec 00-19-86-00-16-CA-00-1E-00-00-00-00-00-00-00-00 txqueuelen 1000 (UNSPEC) - RX packets 330 bytes 22891 (22.8 KB) - RX errors 0 dropped 0 overruns 0 frame 0 - TX packets 354 bytes 30618 (30.6 KB) - TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 - ... - - -- You should now be able to ping the device - - $ ping6 fe80::e4dd:e0ff:fe8f:7365%bt0 - -- **optional**: follow the guide for distributing a routable Prefix in - [README.ipv6-over-ble.md](../../pkg/nimble/README.ipv6-over-ble.md). - -If this was performed correctly then the `bt0` interface should now have a global -address: - - bt0: flags=4161 mtu 1280 - inet6 2001:db8::19:86ff:fe00:16ca prefixlen 64 scopeid 0x0 - inet6 fe80::19:86ff:fe00:16ca prefixlen 64 scopeid 0x20 - inet6 2001:db8::b004:c58:891f:aa09 prefixlen 64 scopeid 0x0 - unspec 00-19-86-00-16-CA-00-14-00-00-00-00-00-00-00-00 txqueuelen 1000 (UNSPEC) - RX packets 3 bytes 120 (120.0 B) - RX errors 0 dropped 0 overruns 0 frame 0 - TX packets 34 bytes 3585 (3.5 KB) - TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 - -In this case the address to use for `SUIT_COAP_SERVER` can be either the EUI64 -generated global address `[2001:db8::19:86ff:fe00:16ca]` or the random global address -`[2001:db8::b004:c58:891f:aa09]`. - -If for some reason this didn't work, you can manually set up an address for -the subnet: - - $ sudo ip address add 2001:db8::1/64 dev bt0 - -In this case the address used for `SUIT_COAP_SERVER` should be [`2001:db8::1`]. - -Route traffic going towards your subnet through bt0: - - $ sudo route -A inet6 add 2001:db8::/64 dev bt0 - -In either case the address used for `SUIT_CLIENT` should be the suffix of the link -local address for that device (`e4dd:e0ff:fe8f:7365` in our examples) and the -distributed prefix, i.e.: `SUIT_CLIENT=[2001:db8::e4dd:e0ff:fe8f:7365]` - -If this optional step is skipped then `SUIT_COAP_SERVER` will be -the link local address of the `bt0` interface and `SUIT_CLIENT` will be -the link local address of the device, with the interface specified. e.g: - - - SUIT_COAP_SERVER=[fe80::19:86ff:fe00:16ca] - SUIT_CLIENT=[fe80::e4dd:e0ff:fe8f:7365%bt0] - -### Start aiocoap-fileserver -[Start-aiocoap-fileserver]: #start-aiocoap-fileserver - -`aiocoap-fileserver` is used for hosting the firmwares available for updates. -Devices retrieve the new firmware using the CoAP protocol. - -Start `aiocoap-fileserver`: - - $ mkdir -p coaproot - $ aiocoap-fileserver coaproot - -Keep the server running in the terminal. - -## Perform an update -[update]: #Perform-an-update - -### Build and publish the firmware update -[update-build-publish]: #Build-and-publish-the-firmware-update - -Currently, the build system assumes that it can publish files by simply copying -them to a configurable folder. - -For this example, aiocoap-fileserver serves the files via CoAP. - -- To publish an update for a node in wired mode (behind ethos): - - $ BOARD=samr21-xpro SUIT_COAP_SERVER=[2001:db8::1] make -C examples/suit_update suit/publish - -- To publish an update for a node in wireless mode (behind a border router): - - $ BOARD=samr21-xpro USE_ETHOS=0 SUIT_COAP_SERVER=[2001:db8::1] make -C examples/suit_update suit/publish - -This publishes into the server a new firmware for a samr21-xpro board. You should -see 6 pairs of messages indicating where (filepath) the file was published and -the corresponding coap resource URI - - ... - published "/home/francisco/workspace/RIOT/examples/suit_update/bin/samr21-xpro/suit_update-riot.suitv3_signed.1557135946.bin" - as "coap://[2001:db8::1]/fw/samr21-xpro/suit_update-riot.suitv3_signed.1557135946.bin" - published "/home/francisco/workspace/RIOT/examples/suit_update/bin/samr21-xpro/suit_update-riot.suitv3_signed.latest.bin" - as "coap://[2001:db8::1]/fw/samr21-xpro/suit_update-riot.suitv3_signed.latest.bin" - ... - -### Notify an update to the device -[update-notify]: #Norify-an-update-to-the-device - -If the network has been started with a standalone node, the RIOT node should be -reachable via link-local EUI64 address on the ethos interface, e.g: - - - Iface 5 HWaddr: 02:BE:74:C0:2F:B9 - L2-PDU:1500 MTU:1500 HL:64 RTR - RTR_ADV - Source address length: 6 - Link type: wired - inet6 addr: fe80::7b7e:3255:1313:8d96 scope: link VAL - inet6 addr: fe80::2 scope: link VAL - inet6 group: ff02::2 - inet6 group: ff02::1 - inet6 group: ff02::1:ffc0:2fb9 - inet6 group: ff02::1:ff00:2 - -the EUI64 link local address is `fe80::7b7e:3255:1313:8d96` and -SUIT_CLIENT=[fe80::7b7e:3255:1313:8d96%riot0]. - -If it was setup as a wireless device it will be reachable via its global -address, e.g: - - - Iface 6 HWaddr: 0D:96 Channel: 26 Page: 0 NID: 0x23 - Long HWaddr: 79:7E:32:55:13:13:8D:96 - TX-Power: 0dBm State: IDLE max. Retrans.: 3 CSMA Retries: 4 - AUTOACK ACK_REQ CSMA L2-PDU:102 MTU:1280 HL:64 RTR - RTR_ADV 6LO IPHC - Source address length: 8 - Link type: wireless - inet6 addr: fe80::7b7e:3255:1313:8d96 scope: link VAL - inet6 addr: 2001:db8::7b7e:3255:1313:8d96 scope: global VAL - inet6 group: ff02::2 - inet6 group: ff02::1 - inet6 group: ff02::1:ff17:dd59 - inet6 group: ff02::1:ff00:2 - - -the global address is `2001:db8::7b7e:3255:1313:8d96` and -SUIT_CLIENT=[2001:db8::7b7e:3255:1313:8d96]. - -- In wired mode: - - $ SUIT_COAP_SERVER=[2001:db8::1] SUIT_CLIENT=[fe80::7b7e:3255:1313:8d96%riot] BOARD=samr21-xpro make -C examples/suit_update suit/notify - -- In wireless mode: - - $ SUIT_COAP_SERVER=[2001:db8::1] SUIT_CLIENT=[2001:db8::7b7e:3255:1313:8d96] BOARD=samr21-xpro make -C examples/suit_update suit/notify - - -This notifies the node of a new available manifest. Once the notification is -received by the device, it fetches it. - -If using `suit-v3` the node hangs for a couple of seconds when verifying the -signature: - - .... - suit_coap: got manifest with size 470 - suit: verifying manifest signature - .... - -Once the signature is validated it continues validating other parts of the -manifest. -Among these validations it checks some condition like firmware offset position -in regards to the running slot to see witch firmware image to fetch. - - .... - suit: validated manifest version - )suit: validated sequence number - )validating vendor ID - Comparing 547d0d74-6d3a-5a92-9662-4881afd9407b to 547d0d74-6d3a-5a92-9662-4881afd9407b from manifest - validating vendor ID: OK - validating class id - .... - -Once the manifest validation is complete, the application fetches the image -and starts flashing. -This step takes some time to fetch and write to flash. A progress bar is -displayed during this step: - - .... - Fetching firmware |█████████████ | 50% - .... - -Once the new image is written, a final validation is performed and, in case of -success, the application reboots on the new slot: - - Finalizing payload store - Verifying image digest - Starting digest verification against image - Install correct payload - Verifying image digest - Starting digest verification against image - Install correct payload - Image magic_number: 0x544f4952 - Image Version: 0x5fa52bcc - Image start address: 0x00201400 - Header chksum: 0x53bb3d33 - suit_coap: rebooting... - - main(): This is RIOT! (Version: )) - RIOT SUIT update example application - Running from slot 1 - ... - - -The slot number should have changed from after the application reboots. -You can do the publish-notify sequence several times to verify this. - -## Detailed explanation -[detailed-explanation]: #Detailed-explanation - -### Node - -For the suit_update to work there are important modules that aren't normally built -in a RIOT application: - -* riotboot - * riotboot_flashwrite -* suit - * suit_transport_coap - -#### riotboot - -To be able to receive updates, the firmware on the device needs a bootloader -that can decide from witch of the firmware images (new one and olds ones) to boot. - -For suit updates you need at least two slots in the current conception on riotboot. -The flash memory will be divided in the following way: - -``` -|------------------------------- FLASH ------------------------------------------------------------| -|-RIOTBOOT_LEN-|------ RIOTBOOT_SLOT_SIZE (slot 0) ------|------ RIOTBOOT_SLOT_SIZE (slot 1) ------| - |----- RIOTBOOT_HDR_LEN ------| |----- RIOTBOOT_HDR_LEN ------| - --------------------------------------------------------------------------------------------------| -| riotboot | riotboot_hdr_1 + filler (0) | slot_0_fw | riotboot_hdr_2 + filler (0) | slot_1_fw | - --------------------------------------------------------------------------------------------------| -``` - -The riotboot part of the flash will not be changed during suit_updates but -be flashed a first time with at least one slot with suit_capable fw. - - $ BOARD=samr21-xpro make -C examples/suit_update clean flash - -When calling make with the `flash` argument it will flash the bootloader -and then to slot0 a copy of the firmware you intend to build. - -New images must be of course written to the inactive slot, the device mist be able -to boot from the previous image in case the update had some kind of error, eg: -the image corresponds to the wrong slot. - -On boot the bootloader will check the `riotboot_hdr` and boot on the newest -image. - -`riotboot_flashwrite` module is needed to be able to write the new firmware to -the inactive slot. - -riotboot is not supported by all boards. The default board is `samr21-xpro`, -but any board supporting `riotboot`, `flashpage` and with 256kB of flash should -be able to run the demo. - -#### suit - -The suit module encloses all the other suit_related module. Formally this only -includes the `sys/suit` directory into the build system dirs. - -- **suit_transport_coap** - -To enable support for suit_updates over coap a new thread is created. -This thread will expose 4 suit related resources: - -* /suit/slot/active: a resource that returns the number of their active slot -* /suit/slot/inactive: a resource that returns the number of their inactive slot -* /suit/trigger: this resource allows POST/PUT where the payload is assumed -tu be a url with the location of a manifest for a new firmware update on the -inactive slot. -* /suit/version: this resource is currently not implemented and return "NONE", -it should return the version of the application running on the device. - -When a new manifest url is received on the trigger resource a message is resent -to the coap thread with the manifest's url. The thread will then fetch the -manifest by a block coap request to the specified url. - -- **support for v3** - -This includes v3 manifest support. When a url is received in the /suit/trigger -coap resource it will trigger a coap blockwise fetch of the manifest. When this -manifest is received it will be parsed. The signature of the manifest will be -verified and then the rest of the manifest content. If the received manifest is valid it -will extract the url for the firmware location from the manifest. - -It will then fetch the firmware, write it to the inactive slot and reboot the device. -Digest validation is done once all the firmware is written to flash. -From there the bootloader takes over, verifying the slot riotboot_hdr and boots -from the newest image. - -#### Key Generation - -To sign the manifest and for the device to verify the manifest a pair of keys -must be generated. Note that this is done automatically when building an -updatable RIOT image with `riotboot` or `suit/publish` make targets. - -This is simply done using the `suit/genkey` make target: - - $ BOARD=samr21-xpro make -C examples/suit_update suit/genkey - -You will get this message in the terminal: - - Generated public key: 'a0fc7fe714d0c81edccc50c9e3d9e6f9c72cc68c28990f235ede38e4553b4724' - -### Network - -For connecting the device with the internet we are using ethos (a simple -ethernet over serial driver). - -When executing $RIOTBASE/dist/tools/ethos: - - $ sudo ./start_network.sh /dev/ttyACM0 riot0 2001:db8::1/64 - -A tap interface named `riot0` is setup. `fe80::1/64` is set up as it's -link local address and `fd00:dead:beef::1/128` as the "lo" unique link local address. - -Also `2001:db8::1/64` is configured- as a prefix for the network. It also sets-up -a route to the `2001:db8::1/64` subnet through `fe80::2`. Where `fe80::2` is the default -link local address of the UHCP interface. - -Finally when: - - $ sudo ip address add 2001:db8::1/128 dev riot0 - -We are adding a routable address to the riot0 tap interface. The device can -now send messages to the the coap server through the riot0 tap interface. You could -use a different address for the coap server as long as you also add a routable -address, so: - - $ sudo ip address add $(SUIT_COAP_SERVER) dev riot0 - -When using a border router the same thing is happening although the node is no -longer reachable through its link local address but routed through to border router -so we can reach it with its global address. - -NOTE: if we weren't using a local server you would need to have ipv6 support -on your network or use tunneling. - -NOTE: using `fd00:dead:beef::1` as an address for the coap server would also -work and you wouldn't need to add a routable address to the tap interface since -a route to the loopback interface (`lo`) is already configured. - -### Server and file system variables - -The following variables are defined in makefiles/suit.inc.mk: - - SUIT_COAP_BASEPATH ?= firmware/$(APPLICATION)/$(BOARD) - SUIT_COAP_SERVER ?= localhost - SUIT_COAP_ROOT ?= coap://$(SUIT_COAP_SERVER)/$(SUIT_COAP_BASEPATH) - SUIT_COAP_FSROOT ?= $(RIOTBASE)/coaproot - SUIT_PUB_HDR ?= $(BINDIR)/riotbuild/public_key.h - -The following convention is used when naming a manifest - - SUIT_MANIFEST ?= $(BINDIR_APP)-riot.suitv3.$(APP_VER).bin - SUIT_MANIFEST_LATEST ?= $(BINDIR_APP)-riot.suitv3.latest.bin - SUIT_MANIFEST_SIGNED ?= $(BINDIR_APP)-riot.suitv3_signed.$(APP_VER).bin - SUIT_MANIFEST_SIGNED_LATEST ?= $(BINDIR_APP)-riot.suitv3_signed.latest.bin - -The following default values are using for generating the manifest: - - SUIT_VENDOR ?= "riot-os.org" - SUIT_SEQNR ?= $(APP_VER) - SUIT_CLASS ?= $(BOARD) - SUIT_KEY ?= default - SUIT_KEY_DIR ?= $(RIOTBASE)/keys - SUIT_SEC ?= $(SUIT_KEY_DIR)/$(SUIT_KEY).pem - -All files (both slot binaries, both manifests, copies of manifests with -"latest" instead of `$APP_VER` in riotboot build) are copied into the folder -`$(SUIT_COAP_FSROOT)/$(SUIT_COAP_BASEPATH)`. The manifests contain URLs to -`$(SUIT_COAP_ROOT)/*` and are signed that way. - -The whole tree under `$(SUIT_COAP_FSROOT)` is expected to be served via CoAP -under `$(SUIT_COAP_ROOT)`. This can be done by e.g., `aiocoap-fileserver $(SUIT_COAP_FSROOT)`. - -### Makefile recipes - -The following recipes are defined in makefiles/suit.inc.mk: - -suit/manifest: creates a non signed and signed manifest, and also a latest tag for these. - It uses following parameters: - - - $(SUIT_KEY): name of key to sign the manifest - - $(SUIT_COAP_ROOT): coap root address - - $(SUIT_CLASS) - - $(SUIT_VERSION) - - $(SUIT_VENDOR) - -suit/publish: makes the suit manifest, `slot*` bin and publishes it to the - aiocoap-fileserver - - 1.- builds slot0 and slot1 bin's - 2.- builds manifest - 3.- creates $(SUIT_COAP_FSROOT)/$(SUIT_COAP_BASEPATH) directory - 4.- copy's binaries to $(SUIT_COAP_FSROOT)/$(SUIT_COAP_BASEPATH) - - $(SUIT_COAP_ROOT): root url for the coap resources - -suit/notify: triggers a device update, it sends two requests: - - 1.- COAP get to check which slot is inactive on the device - 2.- COAP POST with the url where to fetch the latest manifest for - the inactive slot - - - $(SUIT_CLIENT): define the client ipv6 address - - $(SUIT_COAP_ROOT): root url for the coap resources - - $(SUIT_NOTIFY_MANIFEST): name of the manifest to notify, `latest` by - default. - -suit/genkey: this recipe generates a ed25519 key to sign the manifest - -**NOTE**: to plugin a new server you would only have to change the suit/publish -recipe, respecting or adjusting to the naming conventions.** - -## Automatic test -[Automatic test]: #test - -This applications ships with an automatic test. The test script itself expects -the application and bootloader to be flashed. It will then create two more -manifests with increasing version numbers and update twice, confirming after -each update that the newly flashed image is actually running. - -To run the test, - -- ensure the [prerequisites] are installed - -- make sure aiocoap-fileserver is in $PATH - -- compile and flash the application and bootloader: - -``` - $ make -C examples/suit_update clean all flash -j4 -``` - -- [set up the network][setup-wired-network] (in another shell): - -``` - $ sudo dist/tools/ethos/setup_network.sh riot0 2001:db8::/64 -``` - -- run the test: - -``` - $ make -C examples/suit_update test -``` +- [SUIT on RIOT native](README.native.md) +- [SUIT on hardware](README.hardware.md) diff --git a/examples/suit_update/README.native.md b/examples/suit_update/README.native.md new file mode 100644 index 0000000000..96887a515f --- /dev/null +++ b/examples/suit_update/README.native.md @@ -0,0 +1,343 @@ +# Testing Without Hardware + +The SUIT update example is compatible with the native application to try out the +update process without requiring separate hardware. While it is not possible to +update the running code in the application, the workflow can be used to +update memory-backed storage. + +- [Quickest start][quickest-start] +- [Introduction][introduction] +- [Workflow][workflow] + - [Setting up networking][setting-up-networking] + - [Starting the CoAP server][starting-the-coap-server] + - [Building and starting the example][building-and-starting-the-example] + - [Exploring the native instance][exploring-the-native-instance] + - [Generating the payload and manifest][generating-the-payload-and-manifest] + - [Updating the storage location][updating-the-storage-location] + +## Quickest start +[quickest-start]: #quickest-start + +1. Set up networking with: +```console +$ sudo dist/tools/tapsetup/tapsetup -c +$ sudo ip address add 2001:db8::1/64 dev tapbr0 +``` + +2. Start a CoAP server in a separate shell and leave it running: +``` +$ aiocoap-fileserver coaproot +``` + +3. Build and start the native instance: +``` +$ BOARD=native make -C examples/suit_update all term +``` + and add an address from the same range to the interface in RIOT +```console +> ifconfig 5 add 2001:db8::2/64 +``` + +4. Generate a payload and a signed manifest for the payload: +```console +$ echo "AABBCCDD" > coaproot/payload.bin +$ dist/tools/suit/gen_manifest.py --urlroot coap://[2001:db8::1]/ --seqnr 1 -o suit.tmp coaproot/payload.bin:0:ram:0 +$ dist/tools/suit/suit-manifest-generator/bin/suit-tool create -f suit -i suit.tmp -o coaproot/suit_manifest +$ dist/tools/suit/suit-manifest-generator/bin/suit-tool sign -k keys/default.pem -m coaproot/suit_manifest -o coaproot/suit_manifest.signed +``` + +5. Pull the manifest from the native instance: +``` +> suit coap://[2001:db8::1]/suit_manifest.signed +``` + +6. Verify the content of the storage location + +```Console +> storage_content .ram.0 0 64 +41414242434344440A +``` + +## Introduction +[introduction]: #introduction + +When building the example application for the native target, the firmware update +capability is removed. Instead two in-memory slots are created that can be +updated with new payloads. These act as a demonstrator for the SUIT +capabilities. + +The steps described here show how to use SUIT manifests to deliver content +updates to a RIOT instance. The full workflow is described, including the setup +of simple infrastructure. + +![Native execution steps](native_steps.svg?sanitize=true) + +The steps are as follow: First the network configuration is done. A CoAP server +is started to host the files for the RIOT instance. The necessary keys to sign +the manifest with are generated. After this the RIOT native instance is compiled +and launched. With this infrastructure running, the content and the manifest is +generated. Finally the RIOT instance is instructed to fetch the manifest and +update the storage location with the content. + +## Workflow +[workflow]: #workflow + +While the above examples use make targets to create and submit the manifest, +this workflow aims to provide a better view of the SUIT manifest and signature +workflow. Because of this the steps below use the low level scripts to manually +creates a payload and manifest and sign it. + +### Setting up networking +[setting-up-networking]: #setting-up-networking + +To deliver the payload to the native instance, a network connection between a +coap server and the instance is required. + +First a bridge with two tap devices is created: + +echo "AABBCCDD" > coaproot/payload.bin +dist/tools/suit/gen_manifest.py --urlroot coap://[2001:db8::1]/ --seqnr 1 -o suit.tmp coaproot/payload.bin:0:ram:0 +dist/tools/suit/suit-manifest-generator/bin/suit-tool create -f suit -i suit.tmp -o coaproot/suit_manifest +dist/tools/suit/suit-manifest-generator/bin/suit-tool sign -k keys/default.pem -m coaproot/suit_manifest -o coaproot/suit_manifest.signed +```console +$ sudo dist/tools/tapsetup/tapsetup -c +``` + +This creates a bridge called `tapbr0` and a `tap0` and `tap1`. These last two +tap devices are used by native instances to inject and receive network packets +to and from. + +On the bridge device `tapbr0` an routable IP address is added such as +`2001:db8::1/64`: + +```console +$ sudo ip address add 2001:db8::1/64 dev tapbr0 +``` + +### Starting the CoAP server +[starting-the-coap-server]: #starting-the-coap-server + +As mentioned above, a CoAP server is required to allow the native instance to +retrieve the manifest and payload. The `aiocoap-fileserver` is used for this, +hosting files under the `coaproot` directory: + +```console +$ aiocoap-fileserver coaproot +``` + +This should be left running in the background. A different directory can be used +if preferred. + +### Building and starting the example +[building-and-starting-the-example]: #building-and-starting-the-example + +Before the natice instance can be started, it must be compiled first. +Compilation can be started from the root of your RIOT directory with: + +``` +$ BOARD=native make -C examples/suit_update +``` + +Then start the example with: + +```console +$ BOARD=native make -C examples/suit_update term +``` + +This starts an instance of the suit_update example as a process on your +computer. It can be stopped by pressing `ctrl+c` from within the application. + +The instance must also be provided with a routable IP address in the same range +configured on the `tapbr0` interface on the host. In the RIOT shell, this can be +done with: + +```console +> ifconfig 5 add 2001:db8::2/64 +``` + +Where 5 is the interface number of the interface shown with the `ifconfig` +command. + + +### Exploring the native instance +[exploring-the-native-instance]: #exploring-the-native-instance + +The native instance has two shell commands to inspect the storage backends for +the payloads. + +- The `lsstorage` command shows the available storage locations: + +```console +> lsstorage +lsstorage +RAM slot 0: ".ram.0" +RAM slot 1: ".ram.1" +``` + +As shown above, two storage locations are available, `.ram.0` and `.ram.1`. +While two slots are available, in this example only the content of the `.ram.0` +slot will be updated. + +- The `storage_content` command can be used to display a hex dump command of one + of the storage locations. It requires a location string, an offset and a + number of bytes to print: + +```console +> storage_content .ram.0 0 64 + +``` +As the storage location is empty on boot, nothing is printed. + +### Generating the payload and manifest +[generating-the-payload-and-manifest]: #generating-the-payload-and-manifest + +To update the storage location we first need a payload. A trivial payload is +used in this example: + +```console +$ echo "AABBCCDD" > coaproot/payload.bin +``` + +Make sure to store it in the directory selected for the CoAP file server. + +Next, a manifest template is created. This manifest template is a JSON file that +acts as a template for the real SUIT manifest. Within RIOT, the script +`dist/tools/suit/gen_manifest.py` is used. + +```console +$ dist/tools/suit/gen_manifest.py --urlroot coap://[2001:db8::1]/ --seqnr 1 -o suit.tmp coaproot/payload.bin:0:ram:0 +``` + +This generates a suit manifest template with the sequence number set to `1`, a +payload that should be stored at slot offset zero in slot `.ram.0`. The url for +the payload starts with `coap://[fe80::4049:bfff:fe60:db09]/`. Make sure to +match these with the locations and IP addresses used on your own device. + +SUIT supports a check for a slot offset. Within RIOT this is normally used to +distinguish between the different firmware slots on a device. As this is not +used on a native instance, it is set to zero here. The location within a SUIT +manifest is an array of path components. Which character is used to separate +these path components is out of the scope of the SUIT manifest. The +`gen_manifest.py` command uses colons (`:`) to separate these components. +Within the manifest this will show up as an array containing `[ "ram", "0" ]`. + +The content of this template file should look like this: + +```json +{ + "manifest-version": 1, + "manifest-sequence-number": 1, + "components": [ + { + "install-id": [ + "ram", + "0" + ], + "vendor-id": "547d0d746d3a5a9296624881afd9407b", + "class-id": "bcc90984fe7d562bb4c9a24f26a3a9cd", + "file": "coaproot/suit_test.bin", + "uri": "coap://[fe80::4049:bfff:fe60:db09]/suit_test.bin", + "bootable": false + } + ] +} +``` + +The manifest version indicates the SUIT manifest specification version numbers, +this will always be 1 for now. The sequence number is the monotonically +increasing anti-rollback counter. + +Each component, or payload, also has a number of parameters. The install-id +indicates the unique path where this component must be installed. +The vendor and class ID are used in manifest conditionals to ensure that the +payload is valid for the device it is going to be installed in. It is generated +based on the UUID(v5) of `riot-os.org` and the board name (`native`). + +The file and uri are used to generated the URL parameter and the digest in the +manifest. The bootable flag specifies if the manifest generator should instruct +the node to reboot after applying the update. + +Generating the actual SUIT manifest from this is done with: + +```console +$ dist/tools/suit/suit-manifest-generator/bin/suit-tool create -f suit -i suit.tmp -o coaproot/suit_manifest +``` + +This generates the manifest in SUIT CBOR format. The content can be inspected by +using the `parse` subcommand: + +```console +$ dist/tools/suit/suit-manifest-generator/bin/suit-tool parse -m coaproot/suit_manifest +``` + +The manifest generated doesn't have an authentication wrapper, it is unsigned +and will not pass inspection on the device or RIOT instance. The manifest can be +signed with the `sign` subcommand together with the keys generated earlier. + +```console +$ dist/tools/suit/suit-manifest-generator/bin/suit-tool sign -k keys/default.pem -m coaproot/suit_manifest -o coaproot/suit_manifest.signed +``` + +This generates an authentication to the manifest. This is visible when +inspecting with the `parse` subcommand. The URL to this signed manifest will be +submitted to the instance so it can retrieve it and in turn retrieve the +component payload specified by the manifest. + +### Updating the storage location +[updating-the-storage-location]: #updating-the-storage-location + +The update process is a two stage process where first the instance pulls in the +manifest via a supplied url. It will download the manifest and verify the +content. After the manifest is verified, it will proceed with executing the +command sequences in the manifest and download the payload when instructed to. + +The URL for the manifest can be supplied to the instance via the command line. + +```console +> suit coap://[2001:db8::1]/suit_manifest.signed +``` + +The payload is the full URL to the signed manifest. The native instance should +respond on this by downloading and executing the manifest. If all went well, the +output of the native instance should look something like this: + +``` +suit coap://[2001:db8::1]/suit_manifest.signed +suit_coap: trigger received +suit_coap: downloading "coap://[2001:db8::1]/suit_manifest.signed" +suit_coap: got manifest with size 276 +suit: verifying manifest signature +suit: validated manifest version +Retrieved sequence number: 0 +Manifest seq_no: 1, highest available: 0 +suit: validated sequence number +Formatted component name: .ram.0 +validating vendor ID +Comparing 547d0d74-6d3a-5a92-9662-4881afd9407b to 547d0d74-6d3a-5a92-9662-4881afd9407b from manifest +validating vendor ID: OK +validating class id +Comparing bcc90984-fe7d-562b-b4c9-a24f26a3a9cd to bcc90984-fe7d-562b-b4c9-a24f26a3a9cd from manifest +validating class id: OK +SUIT policy check OK. +Formatted component name: .ram.0 +Fetching firmware |█████████████████████████| 100% +Finalizing payload store +Verifying image digest +Starting digest verification against image +Install correct payload +Verifying image digest +Starting digest verification against image +Install correct payload +``` + +The storage location can now be inspected using the built-in command. If the +same payload as suggested above was used, it should look like this: + +```Console +> storage_content .ram.0 0 64 +41414242434344440A +``` + +The process can be done multiple times with both slot `.ram.0` and `.ram.1` and +different payloads. Keep in mind that the sequence number is a strict +monotonically number and must be increased after every update. diff --git a/examples/suit_update/main.c b/examples/suit_update/main.c index e8b241914c..27ff9a9cdd 100644 --- a/examples/suit_update/main.c +++ b/examples/suit_update/main.c @@ -19,6 +19,7 @@ #include +#include "fmt.h" #include "thread.h" #include "irq.h" #include "net/nanocoap_sock.h" @@ -27,7 +28,12 @@ #include "shell.h" #include "suit/transport/coap.h" +#ifdef MODULE_SUIT_STORAGE_FLASHWRITE #include "riotboot/slot.h" +#endif + +#include "suit/storage.h" +#include "suit/storage/ram.h" #ifdef MODULE_PERIPH_GPIO #include "periph/gpio.h" @@ -68,6 +74,7 @@ static void cb(void *arg) } #endif +#ifdef MODULE_SUIT_STORAGE_FLASHWRITE static int cmd_print_riotboot_hdr(int argc, char **argv) { (void)argc; @@ -102,10 +109,71 @@ static int cmd_print_current_slot(int argc, char **argv) irq_restore(state); return 0; } +#endif + +static int cmd_print_slot_content(int argc, char **argv) +{ + char *slot; + uint32_t offset; + size_t len; + + if (argc < 4) { + printf("usage: %s \n", argv[0]); + return -1; + } + + slot = argv[1]; + offset = atoi(argv[2]); + len = atoi(argv[3]); + + suit_storage_t *storage = suit_storage_find_by_id(slot); + if (!storage) { + printf("No storage with id \"%s\" present\n", slot); + return -1; + } + + suit_storage_set_active_location(storage, slot); + + if (suit_storage_has_readptr(storage)) { + const uint8_t *buf; + size_t available; + suit_storage_read_ptr(storage, &buf, &available); + + size_t to_print = available < offset + len ? available - offset : len; + for (size_t i = offset; i < to_print; i++) { + print_byte_hex(buf[i]); + }; + puts(""); + } + + return 0; +} + +static int cmd_lsstorage(int argc, char **argv) +{ + (void)argc; + (void)argv; + + if (IS_ACTIVE(MODULE_SUIT_STORAGE_RAM)) { + for (unsigned i = 0; i < CONFIG_SUIT_STORAGE_RAM_REGIONS; i++) { + printf("RAM slot %u: \"%s%u\"\n", i, + CONFIG_SUIT_STORAGE_RAM_LOCATION_PREFIX, i); + } + } + if (IS_ACTIVE(MODULE_SUIT_STORAGE_FLASHWRITE)) { + puts("Flashwrite slot 0: \"\"\n"); + } + + return 0; +} static const shell_command_t shell_commands[] = { +#ifdef MODULE_SUIT_STORAGE_FLASHWRITE { "current-slot", "Print current slot number", cmd_print_current_slot }, { "riotboot-hdr", "Print current slot header", cmd_print_riotboot_hdr }, +#endif + { "storage_content", "Print the slot content", cmd_print_slot_content }, + { "lsstorage", "Print the available storage paths", cmd_lsstorage }, { NULL, NULL, NULL } }; @@ -119,8 +187,10 @@ int main(void) gpio_init_int(BTN0_PIN, BTN0_MODE, GPIO_FALLING, cb, NULL); #endif +#ifdef MODULE_SUIT_STORAGE_FLASHWRITE cmd_print_current_slot(0, NULL); cmd_print_riotboot_hdr(0, NULL); +#endif /* start suit coap updater thread */ suit_coap_run(); diff --git a/examples/suit_update/native_steps.svg b/examples/suit_update/native_steps.svg new file mode 100644 index 0000000000..ebfc588686 --- /dev/null +++ b/examples/suit_update/native_steps.svg @@ -0,0 +1,3 @@ + + +
RIOT instance
RIOT instance
CoAP Server
CoAP Server
:ram:0
:ram:0
:ram:0
:ram:0
2001:db8::2/64
2001:db8::2/64
2001:db8::1/64
2001:db8::1/64
3. Notify
3. Notify
4. GET manifest
4. GET manifest
5. Reply with manifest
5. Reply with manifest
5. Verify
and parse
manifest
5. Verify...
6. GET/blockwise payload
6. GET/blockwise payload
7. Reply with payload
7. Reply with payload
8. Verify
payload
8. Verify...
Admin
Admin
1. Push new
payload
1. Push new...
2.Push new
Manifest
2.Push new...
Viewer does not support full SVG 1.1
\ No newline at end of file