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 <alexandre.abadie@inria.fr> Co-authored-by: Koen Zandberg <koen@bergzand.net> Co-authored-by: Francisco Molina <femolina@uc.cl>
This commit is contained in:
parent
fb12c4aa8d
commit
b899a9f362
61
dist/tools/suit_v4/gen_manifest.py
vendored
61
dist/tools/suit_v4/gen_manifest.py
vendored
@ -13,8 +13,7 @@ import os
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
|
import argparse
|
||||||
import click
|
|
||||||
|
|
||||||
from suit_manifest_encoder_04 import compile_to_suit
|
from suit_manifest_encoder_04 import compile_to_suit
|
||||||
|
|
||||||
@ -23,7 +22,7 @@ def str2int(x):
|
|||||||
if x.startswith("0x"):
|
if x.startswith("0x"):
|
||||||
return int(x, 16)
|
return int(x, 16)
|
||||||
else:
|
else:
|
||||||
return x
|
return int(x)
|
||||||
|
|
||||||
|
|
||||||
def sha256_from_file(filepath):
|
def sha256_from_file(filepath):
|
||||||
@ -32,37 +31,43 @@ def sha256_from_file(filepath):
|
|||||||
return sha256.digest()
|
return sha256.digest()
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
def parse_arguments():
|
||||||
@click.option("--template", "-t", required=True, type=click.File())
|
parser = argparse.ArgumentParser(
|
||||||
@click.option("--urlroot", "-u", required=True, type=click.STRING)
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||||
@click.option("--offsets", "-O", required=True, type=click.STRING)
|
parser.add_argument('--template', '-t', help='Manifest template file path')
|
||||||
@click.option("--seqnr", "-s", required=True, type=click.INT)
|
parser.add_argument('--urlroot', '-u', help='')
|
||||||
@click.option("--output", "-o", type=click.File(mode="wb"))
|
parser.add_argument('--offsets', '-O', help='')
|
||||||
@click.option("--uuid-vendor", "-V", required=True)
|
parser.add_argument('--seqnr', '-s',
|
||||||
@click.option("--uuid-class", "-C", required=True)
|
help='Sequence number of the manifest')
|
||||||
@click.option("--keyfile", "-K", required=False, type=click.File())
|
parser.add_argument('--output', '-o', nargs='?',
|
||||||
@click.argument("slotfiles", nargs=2, type=click.Path())
|
help='Manifest output binary file path')
|
||||||
def main(template, urlroot, offsets, slotfiles, output, seqnr, uuid_vendor,
|
parser.add_argument('--uuid-vendor', '-V',
|
||||||
uuid_class, keyfile):
|
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"] = [
|
template["conditions"] = [
|
||||||
{"condition-vendor-id": uuid_vendor.hex},
|
{"condition-vendor-id": uuid_vendor.hex},
|
||||||
{"condition-class-id": uuid_class.hex},
|
{"condition-class-id": uuid_class.hex},
|
||||||
]
|
]
|
||||||
|
|
||||||
offsets = offsets.split(",")
|
offsets = [str2int(offset) for offset in args.offsets.split(",")]
|
||||||
offsets = [str2int(x) for x in offsets]
|
|
||||||
|
|
||||||
for slot, slotfile in enumerate(slotfiles):
|
for slot, slotfile in enumerate(args.slotfiles):
|
||||||
filename = slotfile
|
filename = slotfile
|
||||||
size = os.path.getsize(filename)
|
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]
|
offset = offsets[slot]
|
||||||
|
|
||||||
_image_slot = template["components"][0]["images"][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
|
_image_slot["file"] = filename
|
||||||
|
|
||||||
result = compile_to_suit(template)
|
result = compile_to_suit(template)
|
||||||
if output:
|
if args.output is not None:
|
||||||
output.write(result)
|
with open(args.output, 'wb') as f:
|
||||||
|
f.write(result)
|
||||||
else:
|
else:
|
||||||
print(result)
|
print(result)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
_args = parse_arguments()
|
||||||
|
main(_args)
|
||||||
|
|||||||
97
examples/suit_update/Makefile
Normal file
97
examples/suit_update/Makefile
Normal file
@ -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
|
||||||
559
examples/suit_update/README.md
Normal file
559
examples/suit_update/README.md
Normal file
@ -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=<BR 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
|
||||||
|
```
|
||||||
32
examples/suit_update/coap_handler.c
Normal file
32
examples/suit_update/coap_handler.c
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 Kaspar Schleiser <kaspar@schleiser.de>
|
||||||
|
*
|
||||||
|
* 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 <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#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);
|
||||||
77
examples/suit_update/main.c
Normal file
77
examples/suit_update/main.c
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2019 Kaspar Schleiser <kaspar@schleiser.de>
|
||||||
|
*
|
||||||
|
* 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 <kaspar@schleiser.de>
|
||||||
|
* @}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
165
examples/suit_update/tests/01-run.py
Executable file
165
examples/suit_update/tests/01-run.py
Executable file
@ -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<gladdr>[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<lladdr>[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: (?P<app_ver>0x[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)
|
||||||
@ -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)
|
int suit_cbor_map_iterate_init(CborValue *map, CborValue *it)
|
||||||
{
|
{
|
||||||
if (!cbor_value_is_map(map)) {
|
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;
|
return SUIT_ERR_INVALID_MANIFEST;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ static int _v4_parse(suit_v4_manifest_t *manifest, const uint8_t *buf,
|
|||||||
map = it;
|
map = it;
|
||||||
|
|
||||||
if (suit_cbor_map_iterate_init(&map, &it) != SUIT_OK) {
|
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;
|
return SUIT_ERR_INVALID_MANIFEST;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -250,6 +250,9 @@ static int _dtv_fetch(suit_v4_manifest_t *manifest, int key, CborValue *_it)
|
|||||||
}
|
}
|
||||||
memcpy(manifest->urlbuf, url, url_len);
|
memcpy(manifest->urlbuf, url, url_len);
|
||||||
manifest->urlbuf[url_len] = '\0';
|
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);
|
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;
|
manifest->state |= SUIT_MANIFEST_HAVE_COMPONENTS;
|
||||||
cbor_value_enter_container(it, &arr);
|
cbor_value_leave_container(it, &arr);
|
||||||
|
|
||||||
LOG_DEBUG("storing components done\n)");
|
LOG_DEBUG("storing components done\n)");
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user