From b899a9f36277d565477b87e0f53fc92e4b8acac4 Mon Sep 17 00:00:00 2001 From: Kaspar Schleiser Date: Fri, 5 Jul 2019 11:59:06 +0200 Subject: [PATCH] examples/suit_update: add SUIT draft v4 example & test This commit adds an example application showcasing SUIT draft v4 firmware updates. It includes a test script suitable for local or CI testing. Co-authored-by: Alexandre Abadie Co-authored-by: Koen Zandberg Co-authored-by: Francisco Molina --- dist/tools/suit_v4/gen_manifest.py | 61 +-- examples/suit_update/Makefile | 97 +++++ examples/suit_update/README.md | 559 +++++++++++++++++++++++++++ examples/suit_update/coap_handler.c | 32 ++ examples/suit_update/main.c | 77 ++++ examples/suit_update/tests/01-run.py | 165 ++++++++ sys/suit/v4/cbor.c | 4 +- sys/suit/v4/handlers.c | 5 +- 8 files changed, 970 insertions(+), 30 deletions(-) create mode 100644 examples/suit_update/Makefile create mode 100644 examples/suit_update/README.md create mode 100644 examples/suit_update/coap_handler.c create mode 100644 examples/suit_update/main.c create mode 100755 examples/suit_update/tests/01-run.py diff --git a/dist/tools/suit_v4/gen_manifest.py b/dist/tools/suit_v4/gen_manifest.py index eaf825d4c3..d41a661c02 100755 --- a/dist/tools/suit_v4/gen_manifest.py +++ b/dist/tools/suit_v4/gen_manifest.py @@ -13,8 +13,7 @@ import os import hashlib import json import uuid - -import click +import argparse from suit_manifest_encoder_04 import compile_to_suit @@ -23,7 +22,7 @@ def str2int(x): if x.startswith("0x"): return int(x, 16) else: - return x + return int(x) def sha256_from_file(filepath): @@ -32,37 +31,43 @@ def sha256_from_file(filepath): return sha256.digest() -@click.command() -@click.option("--template", "-t", required=True, type=click.File()) -@click.option("--urlroot", "-u", required=True, type=click.STRING) -@click.option("--offsets", "-O", required=True, type=click.STRING) -@click.option("--seqnr", "-s", required=True, type=click.INT) -@click.option("--output", "-o", type=click.File(mode="wb")) -@click.option("--uuid-vendor", "-V", required=True) -@click.option("--uuid-class", "-C", required=True) -@click.option("--keyfile", "-K", required=False, type=click.File()) -@click.argument("slotfiles", nargs=2, type=click.Path()) -def main(template, urlroot, offsets, slotfiles, output, seqnr, uuid_vendor, - uuid_class, keyfile): +def parse_arguments(): + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('--template', '-t', help='Manifest template file path') + parser.add_argument('--urlroot', '-u', help='') + parser.add_argument('--offsets', '-O', help='') + parser.add_argument('--seqnr', '-s', + help='Sequence number of the manifest') + parser.add_argument('--output', '-o', nargs='?', + help='Manifest output binary file path') + parser.add_argument('--uuid-vendor', '-V', + help='Manifest vendor uuid') + parser.add_argument('--uuid-class', '-C', + help='Manifest class uuid') + parser.add_argument('slotfiles', nargs=2, + help='The list of slot file paths') + return parser.parse_args() - uuid_vendor = uuid.uuid5(uuid.NAMESPACE_DNS, uuid_vendor) - uuid_class = uuid.uuid5(uuid_vendor, uuid_class) - template = json.load(template) - slotfiles = list(slotfiles) - template["sequence-number"] = seqnr +def main(args): + uuid_vendor = uuid.uuid5(uuid.NAMESPACE_DNS, args.uuid_vendor) + uuid_class = uuid.uuid5(uuid_vendor, args.uuid_class) + with open(args.template, 'r') as f: + template = json.load(f) + + template["sequence-number"] = int(args.seqnr) template["conditions"] = [ {"condition-vendor-id": uuid_vendor.hex}, {"condition-class-id": uuid_class.hex}, ] - offsets = offsets.split(",") - offsets = [str2int(x) for x in offsets] + offsets = [str2int(offset) for offset in args.offsets.split(",")] - for slot, slotfile in enumerate(slotfiles): + for slot, slotfile in enumerate(args.slotfiles): filename = slotfile size = os.path.getsize(filename) - uri = os.path.join(urlroot, os.path.basename(filename)) + uri = os.path.join(args.urlroot, os.path.basename(filename)) offset = offsets[slot] _image_slot = template["components"][0]["images"][slot] @@ -77,11 +82,13 @@ def main(template, urlroot, offsets, slotfiles, output, seqnr, uuid_vendor, _image_slot["file"] = filename result = compile_to_suit(template) - if output: - output.write(result) + if args.output is not None: + with open(args.output, 'wb') as f: + f.write(result) else: print(result) if __name__ == "__main__": - main() + _args = parse_arguments() + main(_args) diff --git a/examples/suit_update/Makefile b/examples/suit_update/Makefile new file mode 100644 index 0000000000..09f8dc9777 --- /dev/null +++ b/examples/suit_update/Makefile @@ -0,0 +1,97 @@ +# name of your application +APPLICATION = suit_update + +# If no BOARD is found in the environment, use this default: +BOARD ?= samr21-xpro + +# This has to be the absolute path to the RIOT base directory: +RIOTBASE ?= $(CURDIR)/../.. + +BOARD_INSUFFICIENT_MEMORY := arduino-duemilanove arduino-mega2560 arduino-nano \ + arduino-uno b-l072z-lrwan1 chronos lsn50 msb-430 \ + msb-430h nucleo-f031k6 nucleo-f042k6 nucleo-l031k6 \ + nucleo-f030r8 nucleo-f302r8 nucleo-f303k8 \ + nucleo-f334r8 nucleo-l053r8 nucleo-l073rz ruuvitag \ + saml10-xpro saml11-xpro stm32f0discovery thingy52 \ + telosb waspmote-pro wsn430-v1_3b wsn430-v1_4 z1 + +# lower pktbuf size to something sufficient for this application +CFLAGS += -DGNRC_PKTBUF_SIZE=2000 + +# +# Networking +# +# Include packages that pull up and auto-init the link layer. +# NOTE: 6LoWPAN will be included if IEEE802.15.4 devices are present + +# uncomment this to compile in support for a possibly available radio +#USEMODULE += gnrc_netdev_default + +USEMODULE += auto_init_gnrc_netif +# Specify the mandatory networking modules for IPv6 and UDP +USEMODULE += gnrc_ipv6_router_default +USEMODULE += gnrc_udp +USEMODULE += gnrc_sock_udp +# Additional networking modules that can be dropped if not needed +USEMODULE += gnrc_icmpv6_echo + +# include this for printing IP addresses +USEMODULE += shell_commands + +# Set this to 1 to enable code in RIOT that does safety checking +# which is not needed in a production environment but helps in the +# development process: +DEVELHELP ?= 0 + +# Change this to 0 show compiler invocation lines by default: +QUIET ?= 1 + +# +# SUIT update specific stuff +# + +USEMODULE += nanocoap_sock sock_util +USEMODULE += suit suit_coap + +# SUIT draft v4 support: +USEMODULE += suit_v4 + +# Change this to 0 to not use ethos +USE_ETHOS ?= 1 + +ifeq (1,$(USE_ETHOS)) + GNRC_NETIF_NUMOF := 2 + USEMODULE += stdio_ethos + USEMODULE += gnrc_uhcpc + + # ethos baudrate can be configured from make command + ETHOS_BAUDRATE ?= 115200 + CFLAGS += -DETHOS_BAUDRATE=$(ETHOS_BAUDRATE) + + # make sure ethos and uhcpd are built + TERMDEPS += host-tools + + # For local testing, run + # + # $ cd dist/tools/ethos; sudo ./setup_network.sh riot0 2001:db8::0/64 + # + #... in another shell and keep it running. + export TAP ?= riot0 + TERMPROG = $(RIOTTOOLS)/ethos/ethos + TERMFLAGS = $(TAP) $(PORT) +endif + +# The test needs the linked slot binaries without header in order to be able to +# create final binaries with specific APP_VER values. The CI RasPi test workers +# don't compile themselves and re-create signed images, thus add the required +# files here so they will be submitted along with the test jobs. +TEST_EXTRA_FILES += $(SLOT_RIOT_ELFS) $(SUIT_SEC) $(SUIT_PUB) + +include $(RIOTBASE)/Makefile.include + +.PHONY: host-tools + +host-tools: + $(Q)env -u CC -u CFLAGS make -C $(RIOTTOOLS) + +include $(RIOTMAKE)/default-channel.inc.mk diff --git a/examples/suit_update/README.md b/examples/suit_update/README.md new file mode 100644 index 0000000000..4dbc70e2a1 --- /dev/null +++ b/examples/suit_update/README.md @@ -0,0 +1,559 @@ +# Overview + +This example shows how to integrate SUIT-compliant firmware updates into a +RIOT application. It implements basic support of the SUIT architecture using +the manifest format specified in +[draft-moran-suit-manifest-04](https://datatracker.ietf.org/doc/draft-moran-suit-manifest/04/). + +**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: + +- [Prerequisites][prerequisites] +- [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] + - [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] + +## Prerequisites +[prerequisites]: #Prerequisites + +- Install python dependencies (only Python3.6 and later is supported): + + $ pip3 install --user ed25519 pyasn1 cbor + +- Install aiocoap from the source + + $ pip3 install --user --upgrade "git+https://github.com/chrysn/aiocoap#egg=aiocoap[all]" + + See the [aiocoap installation instructions](https://aiocoap.readthedocs.io/en/latest/installation.html) + for more details. + +- add `~/.local/bin` to PATH + + The aiocoap tools are installed to `~/.local/bin`. Either add + "export `PATH=$PATH:~/.local/bin"` to your `~/.profile` and re-login, or execute + that command *in every shell you use for this tutorial*. + +- Clone this repository: + + $ 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. + + Just build `ethos` and `uhcpd` with the following commands: + + $ make -C dist/tools/ethos clean all + $ make -C dist/tools/uhcpd clean all + + It is possible to interact with the device over it's serial terminal as usual + using `make term`, but that requires an already set up tap interface. + See [update] for more information. + +## Setup +[setup]: #Setup + +### Key Management +[key-management]: #Key-management + +SUIT keys consist of a private and a public key file, stored in `$(SUIT_KEY_DIR)`. +Similar to how ssh names its keyfiles, the public key filename equals the +private key file, but has an extra `.pub` appended. + +`SUIT_KEY_DIR` defaults to the `keys/` folder at the top of a RIOT checkout. + +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 + +#### 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 "real" network interfaces, by updating a node that is connected +wirelessly with a border router in between. + +#### 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 and Ethernet must be configured. +Any board providing a 802.15.4 radio can be used as BR. + +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: local 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 + +### Start aiocoap-fileserver +[Start-aiocoap-fileserver]: #start-aiocoap-fileserver + +`aiocoap-fileserver` is used for hosting 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.suitv4_signed.1557135946.bin" + as "coap://[2001:db8::1]/fw/samr21-xpro/suit_update-riot.suitv4_signed.1557135946.bin" + published "/home/francisco/workspace/RIOT/examples/suit_update/bin/samr21-xpro/suit_update-riot.suitv4_signed.latest.bin" + as "coap://[2001:db8::1]/fw/samr21-xpro/suit_update-riot.suitv4_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 `fe80::2%riot0` on the ethos interface. If it was setup as a +wireless device it will be reachable via its global address, something like `2001:db8::7b7e:3255:1313:8d96` + +- In wired mode: + + $ SUIT_COAP_SERVER=[2001:db8::1] SUIT_CLIENT=[fe80::2%riot0] 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-v4` the node hangs for a couple of seconds when verifying the +signature: + + .... + INFO # suit_coap: got manifest with size 545 + INFO # jumping into map + INFO # )got key val=1 + INFO # handler res=0 + INFO # got key val=2 + INFO # 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. + + .... + INFO # Handling handler with key 10 at 0x2b981 + INFO # Comparing manifest offset 4096 with other slot offset 4096 + .... + INFO # Handling handler with key 10 at 0x2b981 + INFO # Comparing manifest offset 133120 with other slot offset 4096 + INFO # Sequence handler error + .... + +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 series of messages like +the following are printed to the terminal: + + .... + riotboot_flashwrite: processing bytes 1344-1407 + riotboot_flashwrite: processing bytes 1408-1471 + riotboot_flashwrite: processing bytes 1472-1535 + ... + +Once the new image is written, a final validation is performed and, in case of +success, the application reboots on the new slot: + + 2019-04-05 16:19:26,363 - INFO # riotboot: verifying digest at 0x20003f37 (img at: 0x20800 size: 80212) + 2019-04-05 16:19:26,704 - INFO # handler res=0 + 2019-04-05 16:19:26,705 - INFO # got key val=10 + 2019-04-05 16:19:26,707 - INFO # no handler found + 2019-04-05 16:19:26,708 - INFO # got key val=12 + 2019-04-05 16:19:26,709 - INFO # no handler found + 2019-04-05 16:19:26,711 - INFO # handler res=0 + 2019-04-05 16:19:26,713 - INFO # suit_v4_parse() success + 2019-04-05 16:19:26,715 - INFO # SUIT policy check OK. + 2019-04-05 16:19:26,718 - INFO # suit_coap: finalizing image flash + 2019-04-05 16:19:26,725 - INFO # riotboot_flashwrite: riotboot flashing completed successfully + 2019-04-05 16:19:26,728 - INFO # Image magic_number: 0x544f4952 + 2019-04-05 16:19:26,730 - INFO # Image Version: 0x5ca76390 + 2019-04-05 16:19:26,733 - INFO # Image start address: 0x00020900 + 2019-04-05 16:19:26,738 - INFO # Header chksum: 0x13b466db + + + main(): This is RIOT! (Version: 2019.04-devel-606-gaa7b-ota_suit_v2) + RIOT SUIT update example application + running from slot 1 + Waiting for address autoconfiguration... + +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_hdr +* riotboot_slot +* suit + * suit_coap + * suit_v4 + +#### 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 riotboot/flash + +When calling make with the riotboot/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. + +The active/inactive coap resources is used so the publisher can send a manifest +built for the inactive slot. + +On boot the bootloader will check the riotboot_hdr and boot on the newest +image. + +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_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 v4** + +This includes v4 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.suitv4.$(APP_VER).bin + SUIT_MANIFEST_LATEST ?= $(BINDIR_APP)-riot.suitv4.latest.bin + SUIT_MANIFEST_SIGNED ?= $(BINDIR_APP)-riot.suitv4_signed.$(APP_VER).bin + SUIT_MANIFEST_SIGNED_LATEST ?= $(BINDIR_APP)-riot.suitv4_signed.latest.bin + +The following default values are using for generating the manifest: + + SUIT_VENDOR ?= RIOT + SUIT_VERSION ?= $(APP_VER) + SUIT_CLASS ?= $(BOARD) + SUIT_KEY ?= default + SUIT_KEY_DIR ?= $(RIOTBASE)/keys + SUIT_SEC ?= $(SUIT_KEY_DIR)/$(SUIT_KEY) + SUIT_PUB ?= $(SUIT_KEY_DIR)/$(SUIT_KEY).pub + +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 keypair 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/coap_handler.c b/examples/suit_update/coap_handler.c new file mode 100644 index 0000000000..b3c6aedc5c --- /dev/null +++ b/examples/suit_update/coap_handler.c @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2019 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +#include +#include +#include + +#include "net/nanocoap.h" +#include "suit/coap.h" + +static ssize_t _riot_board_handler(coap_pkt_t *pkt, uint8_t *buf, size_t len, void *context) +{ + (void)context; + return coap_reply_simple(pkt, COAP_CODE_205, buf, len, + COAP_FORMAT_TEXT, (uint8_t*)RIOT_BOARD, strlen(RIOT_BOARD)); +} + +/* must be sorted by path (ASCII order) */ +const coap_resource_t coap_resources[] = { + COAP_WELL_KNOWN_CORE_DEFAULT_HANDLER, + { "/riot/board", COAP_GET, _riot_board_handler, NULL }, + + /* this line adds the whole "/suit"-subtree */ + SUIT_COAP_SUBTREE, +}; + +const unsigned coap_resources_numof = ARRAY_SIZE(coap_resources); diff --git a/examples/suit_update/main.c b/examples/suit_update/main.c new file mode 100644 index 0000000000..10bb23b925 --- /dev/null +++ b/examples/suit_update/main.c @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup examples + * @{ + * + * @file + * @brief SUIT updates over CoAP example server application (using nanocoap) + * + * @author Kaspar Schleiser + * @} + */ + +#include + +#include "irq.h" +#include "net/nanocoap_sock.h" +#include "xtimer.h" + +#include "suit/coap.h" +#include "riotboot/slot.h" + +#define COAP_INBUF_SIZE (256U) + +#define MAIN_QUEUE_SIZE (8) +static msg_t _main_msg_queue[MAIN_QUEUE_SIZE]; + +/* import "ifconfig" shell command, used for printing addresses */ +extern int _gnrc_netif_config(int argc, char **argv); + +int main(void) +{ + puts("RIOT SUIT update example application"); + + int current_slot = riotboot_slot_current(); + if (current_slot != -1) { + /* Sometimes, udhcp output messes up the following printfs. That + * confuses the test script. As a workaround, just disable interrupts + * for a while. + */ + irq_disable(); + printf("running from slot %d\n", current_slot); + printf("slot start addr = %p\n", (void *)riotboot_slot_get_hdr(current_slot)); + riotboot_slot_print_hdr(current_slot); + irq_enable(); + } + else { + printf("[FAILED] You're not running riotboot\n"); + } + + /* nanocoap_server uses gnrc sock which uses gnrc which needs a msg queue */ + msg_init_queue(_main_msg_queue, MAIN_QUEUE_SIZE); + + puts("Waiting for address autoconfiguration..."); + xtimer_sleep(3); + + /* print network addresses */ + puts("Configured network interfaces:"); + _gnrc_netif_config(0, NULL); + + /* start suit coap updater thread */ + suit_coap_run(); + + /* initialize nanocoap server instance */ + uint8_t buf[COAP_INBUF_SIZE]; + sock_udp_ep_t local = { .port=COAP_PORT, .family=AF_INET6 }; + nanocoap_server(&local, buf, sizeof(buf)); + + /* should be never reached */ + return 0; +} diff --git a/examples/suit_update/tests/01-run.py b/examples/suit_update/tests/01-run.py new file mode 100755 index 0000000000..e2114f7283 --- /dev/null +++ b/examples/suit_update/tests/01-run.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2019 Inria +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +import os +import subprocess +import sys +import tempfile + +from testrunner import run + +# Default test over loopback interface +COAP_HOST = "[fd00:dead:beef::1]" + +UPDATING_TIMEOUT = 10 +MANIFEST_TIMEOUT = 15 + +USE_ETHOS = int(os.getenv("USE_ETHOS", "1")) +TAP = os.getenv("TAP", "riot0") +TMPDIR = tempfile.TemporaryDirectory() + + +def start_aiocoap_fileserver(): + aiocoap_process = subprocess.Popen( + "exec aiocoap-fileserver %s" % TMPDIR.name, shell=True + ) + + return aiocoap_process + + +def cleanup(aiocoap_process): + aiocoap_process.kill() + TMPDIR.cleanup() + + +def notify(coap_server, client_url, version=None): + cmd = [ + "make", + "suit/notify", + "SUIT_COAP_SERVER={}".format(coap_server), + "SUIT_CLIENT={}".format(client_url), + ] + if version is not None: + cmd.append("SUIT_NOTIFY_VERSION={}".format(version)) + assert not subprocess.call(cmd) + + +def publish(server_dir, server_url, app_ver, latest_name=None): + cmd = [ + "make", + "suit/publish", + "SUIT_COAP_FSROOT={}".format(server_dir), + "SUIT_COAP_SERVER={}".format(server_url), + "APP_VER={}".format(app_ver), + "RIOTBOOT_SKIP_COMPILE=1", + ] + if latest_name is not None: + cmd.append("SUIT_MANIFEST_SIGNED_LATEST={}".format(latest_name)) + + assert not subprocess.call(cmd) + + +def wait_for_update(child): + return child.expect([r"riotboot_flashwrite: processing bytes (\d+)-(\d+)", + "riotboot_flashwrite: riotboot flashing " + "completed successfully"], + timeout=UPDATING_TIMEOUT) + + +def get_ipv6_addr(child): + if USE_ETHOS == 0: + # Get device global address + child.expect( + r"inet6 addr: (?P[0-9a-fA-F:]+:[A-Fa-f:0-9]+)" + " scope: global VAL" + ) + addr = child.match.group("gladdr").lower() + else: + # Get device local address + child.expect_exact("Link type: wired") + child.expect( + r"inet6 addr: (?P[0-9a-fA-F:]+:[A-Fa-f:0-9]+)" + " scope: local VAL" + ) + addr = "{}%{}".format(child.match.group("lladdr").lower(), TAP) + return addr + + +def ping6(client): + print("pinging node...") + ping_ok = False + for _i in range(10): + try: + subprocess.check_call(["ping", "-q", "-c1", "-w1", client]) + ping_ok = True + break + except subprocess.CalledProcessError: + pass + + if not ping_ok: + print("pinging node failed. aborting test.") + sys.exit(1) + else: + print("pinging node succeeded.") + return ping_ok + + +def testfunc(child): + """For one board test if specified application is updatable""" + + # Initial Setup and wait for address configuration + child.expect_exact("main(): This is RIOT!") + + # get version of currently running image + # "Image Version: 0x00000000" + child.expect(r"Image Version: (?P0x[0-9a-fA-F:]+)") + current_app_ver = int(child.match.group("app_ver"), 16) + + for version in [current_app_ver + 1, current_app_ver + 2]: + # Get address, if using ethos it will change on each reboot + client_addr = get_ipv6_addr(child) + client = "[{}]".format(client_addr) + # Wait for suit_coap thread to start + # Ping6 + ping6(client_addr) + child.expect_exact("suit_coap: started.") + # Trigger update process, verify it validates manifest correctly + publish(TMPDIR.name, COAP_HOST, version) + notify(COAP_HOST, client, version) + child.expect_exact("suit_coap: trigger received") + child.expect_exact("suit: verifying manifest signature...") + child.expect( + r"riotboot_flashwrite: initializing update to target slot (\d+)", + timeout=MANIFEST_TIMEOUT, + ) + target_slot = int(child.match.group(1)) + # Wait for update to complete + while wait_for_update(child) == 0: + pass + + # Verify running slot + child.expect(r"running from slot (\d+)") + assert target_slot == int(child.match.group(1)), "BOOTED FROM SAME SLOT" + + print("TEST PASSED") + + +if __name__ == "__main__": + try: + res = 1 + aiocoap_process = start_aiocoap_fileserver() + # TODO: wait for coap port to be available + + res = run(testfunc, echo=True) + + except Exception as e: + print(e) + finally: + cleanup(aiocoap_process) + + sys.exit(res) diff --git a/sys/suit/v4/cbor.c b/sys/suit/v4/cbor.c index 45d4afc7c4..2bc00851e6 100644 --- a/sys/suit/v4/cbor.c +++ b/sys/suit/v4/cbor.c @@ -43,7 +43,7 @@ typedef suit_manifest_handler_t (*suit_manifest_handler_getter_t)(int key); int suit_cbor_map_iterate_init(CborValue *map, CborValue *it) { if (!cbor_value_is_map(map)) { - LOG_INFO("suit_v4_parse(): manifest not an map\n)"); + LOG_INFO("suit_v4_parse(): manifest not a map\n)"); return SUIT_ERR_INVALID_MANIFEST; } @@ -152,7 +152,7 @@ static int _v4_parse(suit_v4_manifest_t *manifest, const uint8_t *buf, map = it; if (suit_cbor_map_iterate_init(&map, &it) != SUIT_OK) { - LOG_DEBUG("manifest not map!\n"); + LOG_DEBUG("manifest not a map!\n"); return SUIT_ERR_INVALID_MANIFEST; } diff --git a/sys/suit/v4/handlers.c b/sys/suit/v4/handlers.c index 0512b5b139..30dde7a3f6 100644 --- a/sys/suit/v4/handlers.c +++ b/sys/suit/v4/handlers.c @@ -250,6 +250,9 @@ static int _dtv_fetch(suit_v4_manifest_t *manifest, int key, CborValue *_it) } memcpy(manifest->urlbuf, url, url_len); manifest->urlbuf[url_len] = '\0'; + + cbor_value_leave_container(&url_it, &url_value_it); + cbor_value_leave_container(&it, &url_it); } LOG_DEBUG("_dtv_fetch() fetching \"%s\" (url_len=%u)\n", manifest->urlbuf, (unsigned)url_len); @@ -411,7 +414,7 @@ static int _component_handler(suit_v4_manifest_t *manifest, int key, } manifest->state |= SUIT_MANIFEST_HAVE_COMPONENTS; - cbor_value_enter_container(it, &arr); + cbor_value_leave_container(it, &arr); LOG_DEBUG("storing components done\n)"); return 0;