1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2024-12-29 04:50:03 +01:00
RIOT/examples/suit_update/README.native.md
2022-06-03 08:49:44 +02:00

13 KiB

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

  1. Set up networking with:
$ sudo dist/tools/tapsetup/tapsetup -c
$ sudo ip address add 2001:db8::1/64 dev tapbr0
  1. Start a CoAP server in a separate shell and leave it running:
$ aiocoap-fileserver coaproot
  1. 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

> ifconfig 5 add 2001:db8::2/64
  1. Generate a payload and a signed manifest for the payload:
$ 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
  1. Pull the manifest from the native instance:
> suit fetch coap://[2001:db8::1]/suit_manifest.signed
  1. Verify the content of the storage location
> storage_content .ram.0 0 64
41414242434344440A

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

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

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

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

$ 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:

$ sudo ip address add 2001:db8::1/64 dev tapbr0

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:

$ aiocoap-fileserver coaproot

This should be left running in the background. A different directory can be used if preferred.

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:

$ 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:

> 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

The native instance has two shell commands to inspect the storage backends for the payloads.

  • The lsstorage command shows the available storage locations:
> lsstorage
lsstorage
RAM slot 0: ".ram.0"
RAM slot 1: ".ram.1"
VFS 0: "/nvm0/SLOT0.txt"
VFS 1: "/nvm0/SLOT1.txt"

As shown above, four storage locations are available, RAM based storage: .ram.0 and .ram.1 as well as VFS based storage: /nvm0/SLOT0.TXT and /nvm0/SLOT1.TXT. While multiple slots are available, in this example only the content of the .ram.0 slot will be updated, but the procedure is the same for any other slot, just replace `.ram.0

  • The storage_content command can be used to display a hex dump command of one of the RAM storage locations. It requires a location string, an offset and a number of bytes to print:
> storage_content .ram.0 0 64

As the storage location is empty on boot, nothing is printed.

For VFS based storage the vfs command can be used instead:

> vfs r /nvm0/SLOT0.txt
Error opening file "/nvm0/SLOT0.txt": -ENOENT

But as the file does not initially exist, nothing is printed.

Generating the payload and manifest

To update the storage location we first need a payload. A trivial payload is used in this example:

$ 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.

$ dist/tools/suit/gen_manifest.py --urlroot coap://[2001:db8::1]/ --seqnr 1 -o suit.tmp coaproot/payload.bin:0:ram:0

or for vfs storage:

$ dist/tools/suit/gen_manifest.py --urlroot coap://[2001:db8::1]/ --seqnr 1 -o suit.tmp coaproot/payload.bin:0:/nvm0/SLOT0.txt

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:

{
    "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:

$ 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:

$ 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.

$ 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

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.

> suit fetch 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:

> storage_content .ram.0 0 64
41414242434344440A

Or for vfs storage:

> vfs r /nvm0/SLOT0.txt
vfs r /nvm0/SLOT0.txt
00000000: 4141 4242 4343 4444 0a                   AABBCCDD.
-- EOF --

The process can be done multiple times for any of the slots and different payloads. Keep in mind that the sequence number is a strict monotonically number and must be increased after every update.