From 9dc619b7809cfdc2c5bde498fbcddbb58fc850e6 Mon Sep 17 00:00:00 2001 From: Jakob Hirschler Date: Mon, 15 Jun 2026 16:43:41 +0200 Subject: [PATCH 1/3] feat(examples): add suit-updatable example Copy the existing updatable-async example as a baseline for a SUIT-based variant. Signed-off-by: Jakob Hirschler --- examples/suit-updatable/Cargo.toml | 42 +++ examples/suit-updatable/README.md | 27 ++ examples/suit-updatable/async-payload.cwasm | Bin 0 -> 29400 bytes examples/suit-updatable/client.cosekey | 1 + examples/suit-updatable/client.diag | 12 + examples/suit-updatable/laze.yml | 17 ++ examples/suit-updatable/peers.yml | 12 + examples/suit-updatable/src/main.rs | 275 ++++++++++++++++++++ 8 files changed, 386 insertions(+) create mode 100644 examples/suit-updatable/Cargo.toml create mode 100644 examples/suit-updatable/README.md create mode 100644 examples/suit-updatable/async-payload.cwasm create mode 100644 examples/suit-updatable/client.cosekey create mode 100644 examples/suit-updatable/client.diag create mode 100644 examples/suit-updatable/laze.yml create mode 100644 examples/suit-updatable/peers.yml create mode 100644 examples/suit-updatable/src/main.rs diff --git a/examples/suit-updatable/Cargo.toml b/examples/suit-updatable/Cargo.toml new file mode 100644 index 0000000..79f5147 --- /dev/null +++ b/examples/suit-updatable/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "updatable-async" +license.workspace = true +edition.workspace = true +publish = false + +[lints] +workspace = true + +[dependencies] +ariel-os = { workspace = true, features = ["coap", "random", "i2c", "defmt"] } + +ariel-os-bindings = { path = "../../src/ariel-os-bindings", features = [ + "log", + "time", + "rng", +] } + +embedded-nal-coap = "=0.1.0-alpha.5" +coap-handler-implementations = "0.6.1" +coap-handler = "0.2.0" +coap-message = "0.3.2" +coap-numbers = "0.2" +# error_title allows sending out messages; error_request_body_error_position +# would also be welcome, but so far I don't think wasmtime exports that (plus +# it probably take a lot of source map magic to make real use of it). +coap-message-utils = { version = "0.3.0", features = ["error_title"] } + +wasmtime = { workspace = true, default-features = false, features = [ + "pulley", + "runtime", + "component-model", +] } + +rand_core = { workspace = true } + +once_cell = { version = "1.21.3", default-features = false, features = [ + "critical-section", +] } + +embassy-sync = { version = "0.7.2" } +embassy-futures = { version = "0.1.1", default-features = false } diff --git a/examples/suit-updatable/README.md b/examples/suit-updatable/README.md new file mode 100644 index 0000000..f50d492 --- /dev/null +++ b/examples/suit-updatable/README.md @@ -0,0 +1,27 @@ +# Updatable async Capsule + +## About + +This example shows how to run and update a WebAssembly capsule which uses async function calls internally using treVM. + +## How to run + +Look [here](https://ariel-os.github.io/ariel-os/dev/docs/book/networking.html) for information about network configuration in Ariel OS. + +```sh +# Example for running on the ESP32-C6-DevKitC-1 using wifi +CONFIG_WIFI_NETWORK=... CONFIG_WIFI_PASSWORD=... laze build -b espressif-esp32-c6-devkitc-1 -s coap-server-config-unprotected run +``` + +Once the server is set up, in another terminal, you can send a request to `PUT` another capsule by using +```sh +# add --credential client.diag to use secure connections +pipx run --spec 'aiocoap[oscore, prettyprint]' aiocoap-client coap://
/path/to/resource +``` + +For most resources, you will need to add `--credentials ./client.diag` to authorize the access. + +It's possible to get the resources that are provided by the server by `GET`ting `.well-known/core` + +This example has been tested on the following boards: +- ESP32-C6-DevKitC-1 diff --git a/examples/suit-updatable/async-payload.cwasm b/examples/suit-updatable/async-payload.cwasm new file mode 100644 index 0000000000000000000000000000000000000000..be1341c23ff4c7e99edb3f42a009d4c8b8649e27 GIT binary patch literal 29400 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 literal 0 HcmV?d00001 diff --git a/examples/suit-updatable/client.cosekey b/examples/suit-updatable/client.cosekey new file mode 100644 index 0000000..c7a7a63 --- /dev/null +++ b/examples/suit-updatable/client.cosekey @@ -0,0 +1 @@ +{1: 2, -1: 1, -4: h'fb13adeb6518cee5f88417660841142e830a81fe334380a953406a1305e8706b'} diff --git a/examples/suit-updatable/client.diag b/examples/suit-updatable/client.diag new file mode 100644 index 0000000..7843cbc --- /dev/null +++ b/examples/suit-updatable/client.diag @@ -0,0 +1,12 @@ +{ + "coap://*": { + "edhoc-oscore": { + "suite": 2, + "method": 3, + "own_cred_style": "by-key-id", + "own_cred": {14: {2: "42-50-31-FF-EF-37-32-39", 8: {1: {1: 2, 2: h'2b', -1: 1, -2: h'ac75e9ece3e50bfc8ed60399889522405c47bf16df96660a41298cb4307f7eb6', -3: h'6e5de611388a4b8a8211334ac7d37ecb52a387d257e6db3c2a93df21ff3affc8'}}}}, + "private_key_file": "client.cosekey", + "peer_cred": {14: {8:{1:{1:2, 2:h'', -1:1, -2:h'7ca4b31a9f2add90b012efe4f5fdc46618568c98aa4e5f040d9a8306df6dd578'}}}}, + } + }, +} diff --git a/examples/suit-updatable/laze.yml b/examples/suit-updatable/laze.yml new file mode 100644 index 0000000..5e80fd7 --- /dev/null +++ b/examples/suit-updatable/laze.yml @@ -0,0 +1,17 @@ +apps: + - name: updatable-async + context: + - espressif-esp32-c6-devkitc-1 + selects: + - coap-server + - ?coap-server-config-storage + - alloc + env: + global: + # Must be enough for two payloads, so requirements depend on the transmitted + # payloads' sizes. + heapsize_required: + - "200000" + RUSTFLAGS: + # Disable SIMD in Wasmtime's interpreter + - --cfg pulley_disable_interp_simd diff --git a/examples/suit-updatable/peers.yml b/examples/suit-updatable/peers.yml new file mode 100644 index 0000000..70624cd --- /dev/null +++ b/examples/suit-updatable/peers.yml @@ -0,0 +1,12 @@ +- from: unauthenticated + scope: + /.well-known/core: GET + +- kccs: | + # The CWT Claims Set that needs to be used (by value or by reference) by + # the client to gain access to the device. + # + # It is expressed in CBOR diagnostic notation (which at the YAML level is + # just a string), and compatible with aiocoap's credentials. + {2: "42-50-31-FF-EF-37-32-39", 8: {1: {1: 2, 2: h'2b', -1: 1, -2: h'ac75e9ece3e50bfc8ed60399889522405c47bf16df96660a41298cb4307f7eb6', -3: h'6e5de611388a4b8a8211334ac7d37ecb52a387d257e6db3c2a93df21ff3affc8'}}} + scope: allow-all diff --git a/examples/suit-updatable/src/main.rs b/examples/suit-updatable/src/main.rs new file mode 100644 index 0000000..718a9a7 --- /dev/null +++ b/examples/suit-updatable/src/main.rs @@ -0,0 +1,275 @@ +#![no_main] +#![no_std] +extern crate alloc; + +use alloc::boxed::Box; +use alloc::vec::Vec; +use ariel_os::coap::coap_run; +use ariel_os::debug::log::{Debug2Format, info}; + +use coap_handler::Handler; +use coap_handler_implementations::{HandlerBuilder, ReportingHandlerBuilder, new_dispatcher}; + +use coap_message::{Code, OptionNumber}; + +use coap_message_utils::Error as CoapError; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::signal::Signal; + +use embassy_futures::select::{Either, select}; + +use wasmtime::component::{Component, HasSelf, Linker, bindgen}; +use wasmtime::{Config, Engine, Store}; + +use ariel_os_bindings::wasm::ArielOSHost; + +#[derive(Debug)] +enum UpdateMsg { + Install(Vec), + Stop, +} + +static UPDATE: Signal = Signal::new(); + +bindgen!({ + world: "example-async", + path: "../../wit/", + with: { + "ariel:wasm-bindings/log-api": ariel_os_bindings::wasm::log, + "ariel:wasm-bindings/time-api": ariel_os_bindings::wasm::time, + "ariel:wasm-bindings/rng-api": ariel_os_bindings::wasm::rng, + + }, + require_store_data_send: true, +}); + +enum Payload { + Static(&'static [u8]), + Owned(Box<[u8]>), +} + +impl Payload { + fn as_bytes(&self) -> &[u8] { + match self { + Payload::Static(bytes) => bytes, + Payload::Owned(bytes) => bytes, + } + } +} + +struct VmControl { + program: Vec, +} + +impl VmControl { + fn new() -> Self { + Self { + program: Vec::new(), + } + } +} + +impl Handler for VmControl { + type RequestData = (Option, u8); + + type ExtractRequestError = coap_message_utils::Error; + type BuildResponseError = coap_message_utils::Error; + + fn extract_request_data( + &mut self, + request: &M, + ) -> Result { + use coap_message::MessageOption; + use coap_message_utils::OptionsExt; + + match request.code().into() { + coap_numbers::code::DELETE => { + info!("Received DELETE request for program "); + request.options().ignore_elective_others()?; + + self.program.clear(); + UPDATE.signal(UpdateMsg::Stop); + + Ok((None, coap_numbers::code::DELETED)) + } + + coap_numbers::code::PUT => { + info!("Received PUT request for program "); + let mut block1: Option = None; + + request + .options() + .filter(|o| { + if o.number() == coap_numbers::option::BLOCK1 + && let Some(n) = o.value_uint() + && block1.is_none() + { + block1 = Some(n); + false + } else { + true + } + }) + .ignore_elective_others()?; + + // This is a bit of a simplification, but ignoring the block size and just + // appending is really kind'a fine IMO. + let block1 = block1.unwrap_or(0); + + // FIXME there's probably a Size1 option; if so, reallocate to fail early. + + let szx = block1 & 0x7; + let blocksize = 1usize << (4 + szx); + let offset = (block1 >> 4) as usize * blocksize; + + if offset == 0 { + self.program.clear(); + } + if self.program.len() != offset { + return Ok((None, coap_numbers::code::REQUEST_ENTITY_INCOMPLETE)); + } + + let payload = request.payload(); + self.program.try_reserve_exact(payload.len()).map_err(|e| { + info!( + "Failed to reserve memory for program: {:?}", + Debug2Format(&e) + ); + CoapError::internal_server_error() + })?; + self.program.extend_from_slice(payload); + + if (block1 & 0x8) == 0x8 { + Ok((Some(block1), coap_numbers::code::CONTINUE)) + } else { + let image = core::mem::take(&mut self.program); + UPDATE.signal(UpdateMsg::Install(image)); + Ok((Some(block1), coap_numbers::code::CHANGED)) + } + } + + _ => Err(CoapError::method_not_allowed()), + } + } + + fn estimate_length(&mut self, _request: &Self::RequestData) -> usize { + 1 + } + + fn build_response( + &mut self, + response: &mut M, + request: Self::RequestData, + ) -> Result<(), Self::BuildResponseError> { + let (block1, code) = request; + response.set_code(M::Code::new(code).map_err(CoapError::from_unionerror)?); + + if let Some(block1) = block1 { + response + .add_option_uint( + M::OptionNumber::new(coap_numbers::option::BLOCK1) + .map_err(CoapError::from_unionerror)?, + block1 as u32, + ) + .map_err(CoapError::from_unionerror)?; + } + Ok(()) + } +} + +#[ariel_os::task(autostart)] +async fn coap_task() { + let control = VmControl::new(); + + let handler = new_dispatcher() + .at_with_attributes(&["vm-control"], &[], control) + .with_wkc(); + + info!("Starting CoAP handler"); + coap_run(handler).await; +} + +#[ariel_os::task(autostart)] +async fn runner_task() { + let engine = make_engine(); + let initial_payload = include_bytes!("../async-payload.cwasm").as_slice(); + let mut payload = Payload::Static(initial_payload); + + info!("Initial payload size: {} bytes", initial_payload.len()); + + let mut linker = Linker::new(&engine); + ExampleAsync::add_to_linker::<_, HasSelf<_>>(&mut linker, |state| state).unwrap(); + + loop { + info!("New payload size: {} bytes", payload.as_bytes().len()); + let payload_option = run_payload(&engine, &linker, &payload).await; + payload = match payload_option { + Some(payload) => payload, + None => wait_for_payload().await, + } + } +} + +fn make_engine() -> Engine { + let mut cfg = Config::default(); + cfg.wasm_custom_page_sizes(true); + cfg.target("pulley32").unwrap(); + + // Must match precompilation + cfg.table_lazy_init(false); + cfg.memory_reservation(0); + cfg.memory_init_cow(false); + cfg.memory_may_move(false); + + // Runtime-only tuning + cfg.max_wasm_stack(2048); + cfg.memory_reservation_for_growth(0); + cfg.async_stack_size(4096); + + Engine::new(&cfg).unwrap() +} + +async fn wait_for_payload() -> Payload { + loop { + match UPDATE.wait().await { + UpdateMsg::Install(new_img) => { + info!("Accepted new capsule image ({} bytes)", new_img.len()); + return Payload::Owned(new_img.into_boxed_slice()); + } + UpdateMsg::Stop => {} + } + } +} + +async fn run_payload( + engine: &Engine, + linker: &Linker, + payload: &Payload, +) -> Option { + let bytes = payload.as_bytes(); + let mem = core::ptr::NonNull::from(bytes); + let component = unsafe { Component::deserialize_raw(engine, mem.into()) }.unwrap(); + + let host = ArielOSHost::default(); + let mut store = Store::new(engine, host); + + let bindings = ExampleAsync::instantiate_async(&mut store, &component, linker) + .await + .unwrap(); + + info!("Running payload"); + + let run_fut = bindings.run.call_async(&mut store, &[], &mut []); + let update_fut = UPDATE.wait(); + + let next = match select(update_fut, run_fut).await { + Either::First(UpdateMsg::Install(new_img)) => { + Some(Payload::Owned(new_img.into_boxed_slice())) + } + Either::First(UpdateMsg::Stop) => None, + Either::Second(_) => None, + }; + + info!("Payload done!"); + next +} From ba946330bf04283652fd949c23ce567d72b91faf Mon Sep 17 00:00:00 2001 From: Jakob Hirschler Date: Tue, 16 Jun 2026 18:08:20 +0200 Subject: [PATCH 2/3] feat(examples): implement SUIT update support Extend the updatable async example with SUIT manifest verification. Add demo signing material and README instructions to use them. Signed-off-by: Jakob Hirschler --- Cargo.lock | 83 ++-- examples/laze.yml | 1 + examples/suit-updatable/Cargo.toml | 9 +- examples/suit-updatable/README.md | 60 ++- examples/suit-updatable/laze.yml | 3 +- .../{async-payload.cwasm => payload.cwasm} | Bin examples/suit-updatable/src/coap_fetch.rs | 188 ++++++++ examples/suit-updatable/src/main.rs | 247 +++++++--- examples/suit-updatable/src/suit.rs | 447 ++++++++++++++++++ .../suit-updatable/suit/demo-private-key.pem | 5 + .../suit/demo-public-key-p256.bin | Bin 0 -> 65 bytes .../suit/demo-public-key-p256.pem | 4 + examples/suit-updatable/suit/manifest.json | 14 + rebuild-all-payloads.sh | 1 + 14 files changed, 940 insertions(+), 122 deletions(-) rename examples/suit-updatable/{async-payload.cwasm => payload.cwasm} (100%) create mode 100644 examples/suit-updatable/src/coap_fetch.rs create mode 100644 examples/suit-updatable/src/suit.rs create mode 100644 examples/suit-updatable/suit/demo-private-key.pem create mode 100644 examples/suit-updatable/suit/demo-public-key-p256.bin create mode 100644 examples/suit-updatable/suit/demo-public-key-p256.pem create mode 100644 examples/suit-updatable/suit/manifest.json diff --git a/Cargo.lock b/Cargo.lock index ad832e1..bb1c215 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -725,7 +725,7 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -745,7 +745,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cexpr", "clang-sys", "itertools 0.13.0", @@ -820,9 +820,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "ble-scanner-trevm" @@ -1398,20 +1398,14 @@ dependencies = [ ] [[package]] -name = "cose_minicbor" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9ec26940b5faee22a6a5802087cc1d0eb90d0f0d7dfecdd8232159b012f21d4" +name = "cose-nostd" +version = "0.1.0" +source = "git+https://codeberg.org/COLORADIO-Project/cose-nostd.git#47af660e945c2476499e2fce3c7adac1e718df5e" dependencies = [ - "heapless 0.9.2", - "hkdf", + "hex-literal", "minicbor", - "minicbor-adapters", "p256", "sha2", - "sha3", - "suit_cbor", - "thiserror", ] [[package]] @@ -1904,9 +1898,9 @@ dependencies = [ [[package]] name = "dress-up" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc99825ce4dc1c2e3ee5e497913b236f52f48127ae9dd2fcf9b972fe0cacce0d" +source = "git+https://github.com/ariel-os/dress-up?branch=async#0ec541ba6d20423799ba5b2e5f372efd74ab952c" dependencies = [ + "bitflags 2.11.1", "ctutils", "digest", "generic-array 1.4.1", @@ -1915,6 +1909,7 @@ dependencies = [ "minicbor", "num_enum 0.7.6", "sha2", + "sha3", "uuid", ] @@ -2080,7 +2075,7 @@ name = "embassy-nrf" version = "0.8.0" source = "git+https://github.com/ariel-os/embassy?rev=3940a79a29ae578a35625cae949bea473abf21d1#3940a79a29ae578a35625cae949bea473abf21d1" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "cfg-if", "cortex-m", "cortex-m-rt", @@ -2156,7 +2151,7 @@ source = "git+https://github.com/ariel-os/embassy?rev=1b75c3d6e61a99a312e2d12f03 dependencies = [ "aligned", "bit_field", - "bitflags 2.10.0", + "bitflags 2.11.1", "block-device-driver", "cfg-if", "cortex-m", @@ -2616,7 +2611,7 @@ version = "1.0.0" source = "git+https://github.com/ariel-os/esp-hal?rev=531c629afdd80ea464682ce7f4db8baed97967a6#531c629afdd80ea464682ce7f4db8baed97967a6" dependencies = [ "bitfield 0.19.4", - "bitflags 2.10.0", + "bitflags 2.11.1", "bytemuck", "cfg-if", "critical-section", @@ -3482,6 +3477,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" + [[package]] name = "hexfloat2" version = "0.1.3" @@ -4905,7 +4906,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", ] [[package]] @@ -5126,7 +5127,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.11.0", @@ -5302,6 +5303,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.9" @@ -5549,7 +5556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "suit-validation" +name = "suit-updatable" version = "0.0.0" dependencies = [ "ariel-os", @@ -5559,23 +5566,20 @@ dependencies = [ "coap-message", "coap-message-utils", "coap-numbers", - "cose_minicbor", + "coap-request", + "coap-request-implementations", + "cose-nostd", "dress-up", + "embassy-futures", + "embassy-sync 0.7.2", "embedded-nal-coap", - "minicbor", + "generic-array 1.4.1", + "once_cell", + "rand_core 0.6.4", + "uuid", "wasmtime", ] -[[package]] -name = "suit_cbor" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd16ad3caa7dc09d4a4c0c585968494ef61d260600119ba79094c047863552e" -dependencies = [ - "minicbor", - "thiserror", -] - [[package]] name = "svgbobdoc" version = "0.3.0" @@ -5975,6 +5979,7 @@ checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", + "sha1_smol", "wasm-bindgen", ] @@ -6122,7 +6127,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap 2.14.0", "semver 1.0.27", @@ -6134,7 +6139,7 @@ version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.1", "hashbrown 0.16.1", "indexmap 2.14.0", "semver 1.0.27", @@ -6159,7 +6164,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb1ed5899dde98357cfdcf647a4614498798719793898245b4b34e663addabf" dependencies = [ "async-trait", - "bitflags 2.10.0", + "bitflags 2.11.1", "bumpalo", "cc", "cfg-if", @@ -6340,7 +6345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2192a77a00b9a67800c2b4e1c70fb6abca79d6b529e53a2ef9dcdcc36090330d" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.11.1", "heck 0.5.0", "indexmap 2.14.0", "wit-parser 0.245.1", @@ -6675,7 +6680,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.11.1", "indexmap 2.14.0", "log", "serde", diff --git a/examples/laze.yml b/examples/laze.yml index 6b1f6ee..006dfa2 100644 --- a/examples/laze.yml +++ b/examples/laze.yml @@ -12,3 +12,4 @@ subdirs: - fake-sensor - sandbox-no-bindings - updatable-async + - suit-updatable diff --git a/examples/suit-updatable/Cargo.toml b/examples/suit-updatable/Cargo.toml index 79f5147..6d2372d 100644 --- a/examples/suit-updatable/Cargo.toml +++ b/examples/suit-updatable/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "updatable-async" +name = "suit-updatable" license.workspace = true edition.workspace = true publish = false @@ -25,11 +25,18 @@ coap-numbers = "0.2" # would also be welcome, but so far I don't think wasmtime exports that (plus # it probably take a lot of source map magic to make real use of it). coap-message-utils = { version = "0.3.0", features = ["error_title"] } +coap-request = "0.2.0-alpha.2" +coap-request-implementations = "0.1.0-alpha.4" +cose-nostd = {git = "https://codeberg.org/COLORADIO-Project/cose-nostd.git", version = "0.1.0", features = ["sign1", "p-256", "verify"]} +dress-up = { git = "https://github.com/ariel-os/dress-up", branch = "async", features = ["async"] } +generic-array = {version = "1.4.1", default-features = false } +uuid = { version = "1.23.1", default-features = false, features = ["v5"] } wasmtime = { workspace = true, default-features = false, features = [ "pulley", "runtime", "component-model", + "async" ] } rand_core = { workspace = true } diff --git a/examples/suit-updatable/README.md b/examples/suit-updatable/README.md index f50d492..8af7341 100644 --- a/examples/suit-updatable/README.md +++ b/examples/suit-updatable/README.md @@ -1,27 +1,69 @@ -# Updatable async Capsule +# SUIT updatable Capsule ## About -This example shows how to run and update a WebAssembly capsule which uses async function calls internally using treVM. +This example shows how to update an async WebAssembly capsule over CoAP using a signed SUIT manifest. + +The update is kept in memory only. It replaces the currently running capsule for the lifetime of the process, but it is not written to flash and does not survive a reboot. + +For lower peak memory usage, the example drops the currently running capsule after the SUIT envelope has been authenticated, but before the new `payload.cwasm` is fetched and validated. If the fetch or validation fails, the runner will be left without a loaded capsule until another valid update is sent. + +## Requirements + +The commands in this example require: + +- Arm's `suit-tool`: https://gitlab.arm.com/research/ietf-suit/suit-tool +- aiocoap's command-line tools: `aiocoap-fileserver`, `aiocoap-client` ## How to run -Look [here](https://ariel-os.github.io/ariel-os/dev/docs/book/networking.html) for information about network configuration in Ariel OS. +All commands below are intended to be run from the example root directory. + +Run the ESP: ```sh -# Example for running on the ESP32-C6-DevKitC-1 using wifi CONFIG_WIFI_NETWORK=... CONFIG_WIFI_PASSWORD=... laze build -b espressif-esp32-c6-devkitc-1 -s coap-server-config-unprotected run ``` -Once the server is set up, in another terminal, you can send a request to `PUT` another capsule by using +Build the update payload: + ```sh -# add --credential client.diag to use secure connections -pipx run --spec 'aiocoap[oscore, prettyprint]' aiocoap-client coap://
/path/to/resource +cargo +nightly -Z script ../../precompile_wasm.rs --path ../../payloads/async-bindings/Cargo.toml --config ../../payloads/.cargo/config.toml -o payload.cwasm --fuel ``` -For most resources, you will need to add `--credentials ./client.diag` to authorize the access. +Edit `suit/manifest.json` and set the `uri` field to the host serving `payload.cwasm`, for example: -It's possible to get the resources that are provided by the server by `GET`ting `.well-known/core` +```json +"uri": "coap://192.168.1.100:5683/payload.cwasm" +``` + +For local testing, the example accepts the same `manifest-sequence-number` more than once and logs a warning. Lower sequence numbers are rejected while the board is running. + +Regenerate and sign the SUIT manifest: + +```sh +suit-tool create -i suit/manifest.json -o suit/manifest.suit + +suit-tool sign -m suit/manifest.suit -k suit/demo-private-key.pem -o suit/manifest.signed.suit +``` + +Ensure that the host firewall allows inbound UDP traffic on port 5683. +Then serve the update payload: + +```sh +aiocoap-fileserver . +``` + +Send the signed manifest to the board: + +```sh +aiocoap-client -m PUT --payload @suit/manifest.signed.suit coap:///vm-control +``` + +The key pair in `suit/` is for this example only. Firmware verification uses `suit/demo-public-key-p256.bin`. + +## Testing This example has been tested on the following boards: + - ESP32-C6-DevKitC-1 diff --git a/examples/suit-updatable/laze.yml b/examples/suit-updatable/laze.yml index 5e80fd7..e062982 100644 --- a/examples/suit-updatable/laze.yml +++ b/examples/suit-updatable/laze.yml @@ -1,8 +1,9 @@ apps: - - name: updatable-async + - name: suit-updatable context: - espressif-esp32-c6-devkitc-1 selects: + - coap-client - coap-server - ?coap-server-config-storage - alloc diff --git a/examples/suit-updatable/async-payload.cwasm b/examples/suit-updatable/payload.cwasm similarity index 100% rename from examples/suit-updatable/async-payload.cwasm rename to examples/suit-updatable/payload.cwasm diff --git a/examples/suit-updatable/src/coap_fetch.rs b/examples/suit-updatable/src/coap_fetch.rs new file mode 100644 index 0000000..627d1f0 --- /dev/null +++ b/examples/suit-updatable/src/coap_fetch.rs @@ -0,0 +1,188 @@ +use core::net::SocketAddr; + +use alloc::vec::Vec; +use ariel_os::time::Duration; +use coap_message::Code; +use coap_message::{MessageOption, MinimalWritableMessage, OptionNumber, ReadableMessage}; +use coap_message_utils::OptionsExt; +use coap_request::Stack; +use coap_request_implementations::AsUriPath; + +use ariel_os::reexports::embassy_time::with_timeout; + +const BLOCK2_SZX: u32 = 6; // 2^(6 + 4) = 1024 bytes + +#[derive(Debug)] +pub enum CoapFetchError { + RequestFailed, + TooLarge, + Empty, + Timeout, + AllocationFailed { size: usize }, +} + +pub async fn get_blockwise( + addr: SocketAddr, + path: &str, + max_size: usize, + timeout: Duration, +) -> Result, CoapFetchError> { + let client = ariel_os::coap::coap_client().await; + + let mut body = Vec::new(); + + body.try_reserve_exact(max_size) + .map_err(|_| CoapFetchError::AllocationFailed { size: max_size })?; + let mut num = 0; + + loop { + let more = with_timeout( + timeout, + client.to(addr).request(GetBlock2 { + path, + num, + body: &mut body, + max_size, + }), + ) + .await + .map_err(|_| CoapFetchError::Timeout)? + .map_err(|_| CoapFetchError::RequestFailed)??; + + if !more { + break; + } + + num += 1; + } + + if body.is_empty() { + return Err(CoapFetchError::Empty); + } + + Ok(body) +} + +fn block2_value(num: u32) -> u32 { + // NUM | M=0 | SZX + (num << 4) | BLOCK2_SZX +} + +fn parse_block2(v: u32) -> Result<(u32, usize, bool), CoapFetchError> { + let szx = v & 0x7; + + if szx > 6 { + return Err(CoapFetchError::RequestFailed); + } + + let num = v >> 4; + let more = (v & 0x8) != 0; + let size = 1usize << (4 + szx); + + Ok((num, size, more)) +} + +struct GetBlock2<'a> { + path: &'a str, + num: u32, + body: &'a mut Vec, + max_size: usize, +} + +impl coap_request::Request for GetBlock2<'_> +where + S: coap_request::Stack + ?Sized, +{ + type Carry = (); + type Output = Result; + + async fn build_request( + &mut self, + req: &mut S::RequestMessage<'_>, + ) -> Result<(), S::RequestUnionError> { + let code = + as MinimalWritableMessage>::Code::new(coap_numbers::code::GET) + .map_err(S::RequestMessage::convert_code_error)?; + + req.set_code(code); + + for part in self.path.as_uri_path() { + req.add_option( + as MinimalWritableMessage>::OptionNumber::new( + coap_numbers::option::URI_PATH, + ) + .map_err(S::RequestMessage::convert_option_number_error)?, + part.as_bytes(), + ) + .map_err(S::RequestMessage::convert_add_option_error)?; + } + + req.add_option_uint( + as MinimalWritableMessage>::OptionNumber::new( + coap_numbers::option::BLOCK2, + ) + .map_err(S::RequestMessage::convert_option_number_error)?, + block2_value(self.num), + ) + .map_err(S::RequestMessage::convert_add_option_error)?; + + Ok(()) + } + + async fn process_response(&mut self, res: &S::ResponseMessage<'_>, _carry: ()) -> Self::Output { + let code: u8 = res.code().into(); + + if !matches!( + coap_numbers::code::classify(code), + coap_numbers::code::Range::Response(coap_numbers::code::Class::Success) + ) { + return Err(CoapFetchError::RequestFailed); + } + + let mut block2 = None; + + res.options() + .filter(|option| { + if option.number() == coap_numbers::option::BLOCK2 { + block2 = option.value_uint(); + false + } else { + true + } + }) + .ignore_elective_others() + .map_err(|_| CoapFetchError::RequestFailed)?; + + let payload = res.payload(); + + let more = if let Some(block2) = block2 { + let (num, size, more) = parse_block2(block2)?; + + if num != self.num || payload.len() > size { + return Err(CoapFetchError::RequestFailed); + } + + more + } else { + if self.num != 0 { + return Err(CoapFetchError::RequestFailed); + } + + false + }; + + let new_len = self + .body + .len() + .checked_add(payload.len()) + .ok_or(CoapFetchError::TooLarge)?; + + if new_len > self.max_size { + return Err(CoapFetchError::TooLarge); + } + + self.body.extend_from_slice(payload); + + Ok(more) + } +} diff --git a/examples/suit-updatable/src/main.rs b/examples/suit-updatable/src/main.rs index 718a9a7..fcd95ce 100644 --- a/examples/suit-updatable/src/main.rs +++ b/examples/suit-updatable/src/main.rs @@ -2,10 +2,12 @@ #![no_std] extern crate alloc; +use core::ptr::NonNull; + use alloc::boxed::Box; use alloc::vec::Vec; use ariel_os::coap::coap_run; -use ariel_os::debug::log::{Debug2Format, info}; +use ariel_os::debug::log::{Debug2Format, error, info, warn}; use coap_handler::Handler; use coap_handler_implementations::{HandlerBuilder, ReportingHandlerBuilder, new_dispatcher}; @@ -14,22 +16,20 @@ use coap_message::{Code, OptionNumber}; use coap_message_utils::Error as CoapError; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::channel::Channel; use embassy_sync::signal::Signal; use embassy_futures::select::{Either, select}; use wasmtime::component::{Component, HasSelf, Linker, bindgen}; -use wasmtime::{Config, Engine, Store}; +use wasmtime::{Config, Engine, Error as WasmtimeError, Store}; use ariel_os_bindings::wasm::ArielOSHost; -#[derive(Debug)] -enum UpdateMsg { - Install(Vec), - Stop, -} +use crate::suit::{UpdateError, build_and_authenticate_manifest, fetch_and_verify_update}; -static UPDATE: Signal = Signal::new(); +mod coap_fetch; +mod suit; bindgen!({ world: "example-async", @@ -43,28 +43,25 @@ bindgen!({ require_store_data_send: true, }); -enum Payload { - Static(&'static [u8]), - Owned(Box<[u8]>), -} +static SUIT_VERIFY_SIGNAL: Signal> = Signal::new(); +static VM_DROP_REQUESTS: Channel = Channel::new(); +static VM_STATUS_SIGNAL: Channel = Channel::new(); +static UPDATE_RESULTS: Channel, ()>, 1> = Channel::new(); -impl Payload { - fn as_bytes(&self) -> &[u8] { - match self { - Payload::Static(bytes) => bytes, - Payload::Owned(bytes) => bytes, - } - } +#[derive(Debug)] +enum VmEvent { + Dropped, + Finished, } struct VmControl { - program: Vec, + payload: Vec, } impl VmControl { fn new() -> Self { Self { - program: Vec::new(), + payload: Vec::new(), } } } @@ -84,11 +81,10 @@ impl Handler for VmControl { match request.code().into() { coap_numbers::code::DELETE => { - info!("Received DELETE request for program "); + info!("Received DELETE request for SUIT-Manifest"); request.options().ignore_elective_others()?; - self.program.clear(); - UPDATE.signal(UpdateMsg::Stop); + self.payload.clear(); Ok((None, coap_numbers::code::DELETED)) } @@ -114,37 +110,37 @@ impl Handler for VmControl { // This is a bit of a simplification, but ignoring the block size and just // appending is really kind'a fine IMO. - let block1 = block1.unwrap_or(0); + let block1_value = block1.unwrap_or(0); // FIXME there's probably a Size1 option; if so, reallocate to fail early. - let szx = block1 & 0x7; + let szx = block1_value & 0x7; let blocksize = 1usize << (4 + szx); - let offset = (block1 >> 4) as usize * blocksize; + let offset = (block1_value >> 4) as usize * blocksize; if offset == 0 { - self.program.clear(); + self.payload.clear(); } - if self.program.len() != offset { + if self.payload.len() != offset { return Ok((None, coap_numbers::code::REQUEST_ENTITY_INCOMPLETE)); } let payload = request.payload(); - self.program.try_reserve_exact(payload.len()).map_err(|e| { + self.payload.try_reserve_exact(payload.len()).map_err(|e| { info!( "Failed to reserve memory for program: {:?}", Debug2Format(&e) ); CoapError::internal_server_error() })?; - self.program.extend_from_slice(payload); + self.payload.extend_from_slice(payload); - if (block1 & 0x8) == 0x8 { - Ok((Some(block1), coap_numbers::code::CONTINUE)) + if (block1_value & 0x8) == 0x8 { + Ok((block1, coap_numbers::code::CONTINUE)) } else { - let image = core::mem::take(&mut self.program); - UPDATE.signal(UpdateMsg::Install(image)); - Ok((Some(block1), coap_numbers::code::CHANGED)) + let image = core::mem::take(&mut self.payload); + SUIT_VERIFY_SIGNAL.signal(image.into_boxed_slice()); + Ok((block1, coap_numbers::code::CHANGED)) } } @@ -189,24 +185,103 @@ async fn coap_task() { coap_run(handler).await; } +#[ariel_os::task(autostart)] +async fn suit_update_task() { + let mut accepted_sequence_number = None; + loop { + let envelope = SUIT_VERIFY_SIGNAL.wait().await; + info!("[SUIT] Received update request"); + + let (manifest, sequence_number) = match build_and_authenticate_manifest(&envelope) { + Ok(manifest) => manifest, + Err(e) => { + info!("[SUIT] Update rejected: {:?}", Debug2Format(&e)); + continue; + } + }; + + if let Some(current) = accepted_sequence_number { + if sequence_number < current { + warn!( + "[SUIT] Update rejected: {:?}", + Debug2Format(&UpdateError::RollbackDetected { + current, + attempted: sequence_number, + }) + ); + continue; + } + + if sequence_number == current { + warn!( + "[SUIT] accepting repeated manifest sequence number {:?} for testing", + sequence_number + ); + } + } + + info!("[SUIT] Update authenticated. Requesting drop of old capsule..."); + VM_DROP_REQUESTS.send(()).await; + match VM_STATUS_SIGNAL.receive().await { + VmEvent::Dropped => { + info!("[SUIT] Capsule dropped. Fetching new capsule..."); + } + other => { + info!("[SUIT] Unexpected VM event {:?}", Debug2Format(&other)); + continue; + } + } + + match fetch_and_verify_update(manifest).await { + Ok(capsule) => { + accepted_sequence_number = Some( + accepted_sequence_number + .map_or(sequence_number, |current| current.max(sequence_number)), + ); + + info!( + "[SUIT] Successfully fetched capsule with a length of {} bytes. Requesting install...", + capsule.len() + ); + UPDATE_RESULTS.send(Ok(capsule)).await + } + Err(e) => { + warn!("[SUIT] Failed to retrieve capsule: {:?}", Debug2Format(&e)); + UPDATE_RESULTS.send(Err(())).await + } + } + } +} + #[ariel_os::task(autostart)] async fn runner_task() { let engine = make_engine(); - let initial_payload = include_bytes!("../async-payload.cwasm").as_slice(); - let mut payload = Payload::Static(initial_payload); - - info!("Initial payload size: {} bytes", initial_payload.len()); + let initial_capsule = include_bytes!("../payload.cwasm").as_slice(); + let mut capsule: Vec = Vec::from(initial_capsule); let mut linker = Linker::new(&engine); ExampleAsync::add_to_linker::<_, HasSelf<_>>(&mut linker, |state| state).unwrap(); + let mut host = ArielOSHost::default(); + loop { - info!("New payload size: {} bytes", payload.as_bytes().len()); - let payload_option = run_payload(&engine, &linker, &payload).await; - payload = match payload_option { - Some(payload) => payload, - None => wait_for_payload().await, + let (returned_host, result) = run_capsule(&engine, &linker, capsule, host).await; + match result { + Ok(VmEvent::Dropped) => { + info!("Capsule stopped externally"); + } + Ok(VmEvent::Finished) => { + info!("Capsule finished on its own"); + } + Err(e) => { + error!("run_capsule crashed: {:?}", Debug2Format(&e)); + } } + + host = returned_host; + + info!("Waiting for new capsule..."); + capsule = wait_for_capsule().await; } } @@ -226,50 +301,78 @@ fn make_engine() -> Engine { cfg.memory_reservation_for_growth(0); cfg.async_stack_size(4096); + cfg.consume_fuel(true); + Engine::new(&cfg).unwrap() } -async fn wait_for_payload() -> Payload { +async fn wait_for_capsule() -> Vec { loop { - match UPDATE.wait().await { - UpdateMsg::Install(new_img) => { - info!("Accepted new capsule image ({} bytes)", new_img.len()); - return Payload::Owned(new_img.into_boxed_slice()); + let cmd_fut = VM_DROP_REQUESTS.receive(); + let update_fut = UPDATE_RESULTS.receive(); + + match select(cmd_fut, update_fut).await { + Either::First(()) => { + info!("No capsule loaded; acknowledging drop request"); + VM_STATUS_SIGNAL.send(VmEvent::Dropped).await; + } + Either::Second(Ok(capsule)) => { + info!("Received new capsule"); + return capsule; + } + Either::Second(Err(())) => { + info!("Update failed; still waiting for capsule"); } - UpdateMsg::Stop => {} } } } -async fn run_payload( +async fn run_capsule( engine: &Engine, linker: &Linker, - payload: &Payload, -) -> Option { - let bytes = payload.as_bytes(); - let mem = core::ptr::NonNull::from(bytes); - let component = unsafe { Component::deserialize_raw(engine, mem.into()) }.unwrap(); + mut capsule: Vec, + host: ArielOSHost, +) -> (ArielOSHost, Result) { + let component = + match unsafe { Component::deserialize_raw(&engine, NonNull::from(capsule.as_mut())) } { + Ok(component) => component, + Err(e) => { + error!("Failed to deserialize component: {:?}", Debug2Format(&e)); + + return (host, Err(e)); + } + }; - let host = ArielOSHost::default(); - let mut store = Store::new(engine, host); + let mut store = Store::new(&engine, host); - let bindings = ExampleAsync::instantiate_async(&mut store, &component, linker) - .await - .unwrap(); + store.set_fuel(u64::MAX).expect("failed to set fuel"); - info!("Running payload"); + store + .fuel_async_yield_interval(Some(1_000)) + .expect("failed to set fuel async yield interval"); + + let bindings = match ExampleAsync::instantiate_async(&mut store, &component, linker).await { + Ok(bindings) => bindings, + Err(e) => { + let host = store.into_data(); + return (host, Err(e)); + } + }; let run_fut = bindings.run.call_async(&mut store, &[], &mut []); - let update_fut = UPDATE.wait(); + let drop_requested_fut = VM_DROP_REQUESTS.receive(); - let next = match select(update_fut, run_fut).await { - Either::First(UpdateMsg::Install(new_img)) => { - Some(Payload::Owned(new_img.into_boxed_slice())) - } - Either::First(UpdateMsg::Stop) => None, - Either::Second(_) => None, + let result = match select(drop_requested_fut, run_fut).await { + Either::First(_) => Ok(VmEvent::Dropped), + Either::Second(Ok(_)) => Ok(VmEvent::Finished), + Either::Second(Err(e)) => Err(e), }; - info!("Payload done!"); - next + let host = store.into_data(); + + if matches!(result, Ok(VmEvent::Dropped)) { + VM_STATUS_SIGNAL.send(VmEvent::Dropped).await; + } + + (host, result) } diff --git a/examples/suit-updatable/src/suit.rs b/examples/suit-updatable/src/suit.rs new file mode 100644 index 0000000..007bac8 --- /dev/null +++ b/examples/suit-updatable/src/suit.rs @@ -0,0 +1,447 @@ +use core::{cell::RefCell, net::SocketAddr, str::FromStr}; + +use alloc::vec::Vec; +use ariel_os::debug::log::{Debug2Format, error}; +use ariel_os::time::Duration; +use dress_up::manifest::Manifest; +use dress_up::{AsyncOperatingHooks, Authenticated, SuitManifest}; +use uuid::Uuid; + +use cose_nostd::{ + iana::{Algorithm, EllipticCurve, KeyOperation, KeyType, key_labels}, + key::CoseKeyBuilder, + signature::sign1::CoseSign1, +}; + +use crate::coap_fetch::{CoapFetchError, get_blockwise}; + +pub const MAX_CAPSULE_SIZE: usize = 100 * 1024; +const STAGING_SLOT: u64 = 1; + +pub const PUBKEY_P256: &[u8; 65] = include_bytes!("../suit/demo-public-key-p256.bin"); + +pub fn suit_vendor_id() -> Uuid { + Uuid::new_v5(&Uuid::NAMESPACE_DNS, "example.com".as_bytes()) +} + +pub fn suit_class_id() -> Uuid { + Uuid::new_v5(&suit_vendor_id(), "trevm-suit-updatable-demo".as_bytes()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SuitPhase { + ParseEnvelope, + Authentication, + PayloadFetch, + PayloadInstallation, + ImageValidation, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum UpdateError { + UnsupportedManifestVersion, + EmptyCapsule, + CapsuleTooLarge, + InvalidSlot, + OutOfBoundsRead, + MalformedUri, + CoapRequestFailed, + CoapTimeout, + AllocationFailed { + size: usize, + }, + + VendorIdMismatch { + expected: Uuid, + actual: Uuid, + }, + ClassIdMismatch { + expected: Uuid, + actual: Uuid, + }, + ComponentSlotMismatch { + expected: u64, + actual: u64, + }, + + RollbackDetected { + current: u64, + attempted: u64, + }, + UnsupportedWriteContent, + + SuitAuthenticationFailed, + SuitMissingAuthentication, + SuitMissingSequenceNumber, + SuitPayloadFetchConditionFailed { + position: usize, + }, + SuitPayloadInstallationConditionFailed { + position: usize, + }, + SuitImageValidationConditionFailed { + position: usize, + }, + SuitConditionFailed { + phase: SuitPhase, + position: usize, + }, + SuitExecutionFailed, + + DressUp { + phase: SuitPhase, + error: dress_up::error::Error, + }, +} + +impl UpdateError { + fn from_suit_error(phase: SuitPhase, e: dress_up::error::Error) -> Self { + match e { + dress_up::error::Error::UnsupportedManifestVersion => Self::UnsupportedManifestVersion, + dress_up::error::Error::AuthenticationFailure => Self::SuitAuthenticationFailed, + dress_up::error::Error::NoAuthObject => match phase { + SuitPhase::Authentication => Self::SuitMissingAuthentication, + _ => Self::SuitExecutionFailed, + }, + dress_up::error::Error::NoSequenceNumber => Self::SuitMissingSequenceNumber, + dress_up::error::Error::ConditionMatchFail { position } => match phase { + SuitPhase::PayloadFetch => Self::SuitPayloadFetchConditionFailed { position }, + SuitPhase::PayloadInstallation => { + Self::SuitPayloadInstallationConditionFailed { position } + } + SuitPhase::ImageValidation => Self::SuitImageValidationConditionFailed { position }, + _ => Self::SuitConditionFailed { phase, position }, + }, + dress_up::error::Error::EndOfInput => Self::EmptyCapsule, + _ => Self::DressUp { phase, error: e }, + } + } +} + +struct TrevmSuitHooks { + staging: RefCell>, + last_error: RefCell>, +} + +impl TrevmSuitHooks { + fn new() -> Self { + Self { + staging: RefCell::new(Vec::new()), + last_error: RefCell::new(None), + } + } + + fn into_capsule(self) -> Result, UpdateError> { + let capsule = self.staging.into_inner(); + if capsule.is_empty() { + return Err(UpdateError::EmptyCapsule); + } + + Ok(capsule) + } + + fn remember_error(&self, error: UpdateError) -> dress_up::error::Error { + *self.last_error.borrow_mut() = Some(error); + + match error { + UpdateError::EmptyCapsule => dress_up::error::Error::EndOfInput, + + UpdateError::CapsuleTooLarge + | UpdateError::InvalidSlot + | UpdateError::OutOfBoundsRead => { + dress_up::error::Error::ConditionMatchFail { position: 0 } + } + + _ => dress_up::error::Error::ConditionMatchFail { position: 0 }, + } + } + + fn remember_condition_mismatch(&self, error: UpdateError) { + *self.last_error.borrow_mut() = Some(error); + } + + fn take_last_error(&self) -> Option { + self.last_error.borrow_mut().take() + } + + fn clear_last_error(&self) { + *self.last_error.borrow_mut() = None; + } + + fn map_phase_error(&self, phase: SuitPhase, err: dress_up::error::Error) -> UpdateError { + match self.take_last_error() { + Some(e) => e, + None => UpdateError::from_suit_error(phase, err), + } + } +} + +impl AsyncOperatingHooks for TrevmSuitHooks { + type ReadWriteBufferSize = generic_array::typenum::U512; + async fn match_vendor_id( + &self, + uuid: Uuid, + _component: &dress_up::component::Component<'_>, + ) -> Result { + let ok = uuid == suit_vendor_id(); + if !ok { + self.remember_condition_mismatch(UpdateError::VendorIdMismatch { + expected: suit_vendor_id(), + actual: uuid, + }); + } + Ok(ok) + } + + async fn match_class_id( + &self, + uuid: Uuid, + _component: &dress_up::component::Component<'_>, + ) -> Result { + let ok = uuid == suit_class_id(); + if !ok { + self.remember_condition_mismatch(UpdateError::ClassIdMismatch { + expected: suit_class_id(), + actual: uuid, + }); + } + + Ok(ok) + } + + async fn match_component_slot( + &self, + _component: &dress_up::component::Component<'_>, + slot: u64, + ) -> Result { + let ok = slot == STAGING_SLOT; + if !ok { + self.remember_condition_mismatch(UpdateError::ComponentSlotMismatch { + expected: STAGING_SLOT, + actual: slot, + }); + } + Ok(ok) + } + + async fn component_capacity( + &self, + _component: &dress_up::component::Component<'_>, + ) -> Result { + Ok(MAX_CAPSULE_SIZE) + } + + async fn component_size( + &self, + _component: &dress_up::component::Component<'_>, + ) -> Result { + Ok(self.staging.borrow().len()) + } + + async fn component_read( + &self, + _component: &dress_up::component::Component<'_>, + slot: Option, + offset: usize, + bytes: &mut [u8], + ) -> Result<(), dress_up::error::Error> { + if slot.unwrap_or(STAGING_SLOT) != STAGING_SLOT { + return Err(self.remember_error(UpdateError::InvalidSlot)); + } + + let staging = self.staging.borrow(); + let end = offset + .checked_add(bytes.len()) + .ok_or_else(|| self.remember_error(UpdateError::OutOfBoundsRead))?; + + let src = staging + .get(offset..end) + .ok_or_else(|| self.remember_error(UpdateError::OutOfBoundsRead))?; + + bytes.copy_from_slice(src); + Ok(()) + } + + async fn component_write( + &self, + _component: &dress_up::component::Component<'_>, + _slot: Option, + _offset: usize, + _bytes: &[u8], + ) -> Result<(), dress_up::error::Error> { + *self.last_error.borrow_mut() = Some(UpdateError::UnsupportedWriteContent); + + Err(dress_up::error::Error::UnsupportedCommand { + command: dress_up::consts::SuitCommand::WriteContent.into(), + }) + } + + async fn fetch( + &self, + _component: &dress_up::component::Component<'_>, + slot: Option, + uri: &str, + ) -> Result<(), dress_up::error::Error> { + if slot.unwrap_or(STAGING_SLOT) != STAGING_SLOT { + return Err(self.remember_error(UpdateError::InvalidSlot)); + } + let path = uri + .strip_prefix("coap://") + .ok_or_else(|| self.remember_error(UpdateError::MalformedUri))?; + + let slash_idx = path + .find('/') + .ok_or_else(|| self.remember_error(UpdateError::MalformedUri))?; + let (addr_str, path) = path.split_at(slash_idx); + + let addr = SocketAddr::from_str(addr_str) + .map_err(|_| self.remember_error(UpdateError::MalformedUri))?; + + self.staging.borrow_mut().clear(); + + let body = get_blockwise(addr, path, MAX_CAPSULE_SIZE, Duration::from_secs(1)) + .await + .map_err(|e| self.remember_error(e.into()))?; + + *self.staging.borrow_mut() = body; + Ok(()) + } +} + +pub fn build_and_authenticate_manifest<'a>( + envelope_bytes: &'a impl AsRef<[u8]>, +) -> Result<(Manifest<'a, Authenticated>, u64), UpdateError> { + let suit = SuitManifest::from_bytes(envelope_bytes) + .authenticate(verify_cose_signature) + .map_err(|e| UpdateError::from_suit_error(SuitPhase::Authentication, e))?; + + let envelope = suit + .envelope() + .map_err(|e| UpdateError::from_suit_error(SuitPhase::ParseEnvelope, e))?; + + let manifest = envelope + .manifest() + .map_err(|e| UpdateError::from_suit_error(SuitPhase::ParseEnvelope, e))?; + + let version = manifest + .version() + .map_err(|e| UpdateError::from_suit_error(SuitPhase::ParseEnvelope, e))?; + + if version != 1 { + return Err(UpdateError::UnsupportedManifestVersion); + } + + let sequence_number = manifest + .sequence_number() + .map_err(|e| UpdateError::from_suit_error(SuitPhase::ParseEnvelope, e))?; + + Ok((manifest, sequence_number)) +} + +pub async fn fetch_and_verify_update( + manifest: Manifest<'_, Authenticated>, +) -> Result, UpdateError> { + let hooks = TrevmSuitHooks::new(); + + if manifest + .has_payload_fetch() + .map_err(|e| UpdateError::from_suit_error(SuitPhase::PayloadFetch, e))? + { + manifest + .async_execute_payload_fetch(&hooks) + .await + .map_err(|e| hooks.map_phase_error(SuitPhase::PayloadFetch, e))?; + } + hooks.clear_last_error(); + if manifest + .has_payload_installation() + .map_err(|e| UpdateError::from_suit_error(SuitPhase::PayloadInstallation, e))? + { + manifest + .async_execute_payload_installation(&hooks) + .await + .map_err(|e| hooks.map_phase_error(SuitPhase::PayloadInstallation, e))?; + } + hooks.clear_last_error(); + + if manifest + .has_image_validation() + .map_err(|e| UpdateError::from_suit_error(SuitPhase::ImageValidation, e))? + { + manifest + .async_execute_image_validation(&hooks) + .await + .map_err(|e| hooks.map_phase_error(SuitPhase::PayloadFetch, e))?; + } + + let capsule = hooks.into_capsule()?; + + Ok(capsule) +} + +fn verify_cose_signature( + cose_sign1: &[u8], + detached_payload: &[u8], +) -> Result { + // Expected SEC1 uncompressed form (as in the const above): + // 0x04 || x[32] || y[32] + if PUBKEY_P256.len() != 65 || PUBKEY_P256[0] != 0x04 { + error!("P-256 public key is not uncompressed SEC1 format"); + return Err(dress_up::error::Error::AuthenticationFailure); + } + + let x = &PUBKEY_P256[1..33]; + let y = &PUBKEY_P256[33..65]; + + let mut key_buf = [0u8; 128]; + + let verification_key = CoseKeyBuilder::new(key_buf.as_mut_slice(), 6) + .and_then(|b| { + b.add_generic_params( + KeyType::EC2, + None, + Some(Algorithm::Es256), + Some(&[KeyOperation::Verify]), + None, + ) + }) + .and_then(|b| b.add_param(key_labels::ec::CRV, EllipticCurve::P256)) + .and_then(|b| b.add_param_bytes(key_labels::ec::X, x)) + .and_then(|b| b.add_param_bytes(key_labels::ec::Y, y)) + .and_then(|b| b.build()) + .map_err(|e| { + error!( + "[SUIT] failed to build COSE verification key: {:?}", + Debug2Format(&e) + ); + dress_up::error::Error::AuthenticationFailure + })?; + + let verifier = CoseSign1::from_slice(cose_sign1).map_err(|e| { + error!("[SUIT] failed to decode COSE_Sign1: {:?}", Debug2Format(&e)); + dress_up::error::Error::AuthenticationFailure + })?; + + match verifier.verify_detached(detached_payload, &verification_key, None, None) { + Ok(_) => Ok(true), + Err(e) => { + error!( + "[SUIT] COSE_Sign1 verification failed: {:?}", + Debug2Format(&e) + ); + Ok(false) + } + } +} + +impl From for UpdateError { + fn from(value: CoapFetchError) -> Self { + match value { + CoapFetchError::Empty => UpdateError::EmptyCapsule, + CoapFetchError::TooLarge => UpdateError::CapsuleTooLarge, + CoapFetchError::RequestFailed => UpdateError::CoapRequestFailed, + CoapFetchError::Timeout => UpdateError::CoapTimeout, + CoapFetchError::AllocationFailed { size } => UpdateError::AllocationFailed { size }, + } + } +} diff --git a/examples/suit-updatable/suit/demo-private-key.pem b/examples/suit-updatable/suit/demo-private-key.pem new file mode 100644 index 0000000..f4c7568 --- /dev/null +++ b/examples/suit-updatable/suit/demo-private-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIElku2UCKhI/1THKz0MOMSSIen5pL/+0NT2erMAu7DkZoAoGCCqGSM49 +AwEHoUQDQgAEVNid8XAeLEShMlPgFYnEYuxAss5kGdRaWsSBdg2g1e0+mk4YH5+B +sgutpmRShgLjRRXLADLm+UMU6CzHTLMvvA== +-----END EC PRIVATE KEY----- diff --git a/examples/suit-updatable/suit/demo-public-key-p256.bin b/examples/suit-updatable/suit/demo-public-key-p256.bin new file mode 100644 index 0000000000000000000000000000000000000000..05b6289d430d5e1f16ead9951a7e9096431c0810 GIT binary patch literal 65 zcmV-H0KWeORM?&Ia2_l~p)yn86^X=R>_D>4WEs?2TEu~N4WQNSKAKJ#AD@A;3$3PP XQicNKMHR~cGUoY16zD9+OtUY%>GK|9 literal 0 HcmV?d00001 diff --git a/examples/suit-updatable/suit/demo-public-key-p256.pem b/examples/suit-updatable/suit/demo-public-key-p256.pem new file mode 100644 index 0000000..e53f168 --- /dev/null +++ b/examples/suit-updatable/suit/demo-public-key-p256.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVNid8XAeLEShMlPgFYnEYuxAss5k +GdRaWsSBdg2g1e0+mk4YH5+BsgutpmRShgLjRRXLADLm+UMU6CzHTLMvvA== +-----END PUBLIC KEY----- diff --git a/examples/suit-updatable/suit/manifest.json b/examples/suit-updatable/suit/manifest.json new file mode 100644 index 0000000..54b76be --- /dev/null +++ b/examples/suit-updatable/suit/manifest.json @@ -0,0 +1,14 @@ +{ + "components" : [ + { + "install-id" : ["01"], + "file": "payload.cwasm", + "uri": "coap://:5683/payload.cwasm", + "vendor-id": "cfbff0d1-9375-5685-968c-48ce8b15ae17", + "class-id": "2ecbf570-9780-5c2a-bef7-4da60942464e" + } + ], + "manifest-version": 1, + "manifest-sequence-number": 1 +} + diff --git a/rebuild-all-payloads.sh b/rebuild-all-payloads.sh index 5597cb6..5d2178a 100755 --- a/rebuild-all-payloads.sh +++ b/rebuild-all-payloads.sh @@ -17,5 +17,6 @@ cargo +nightly-2026-01-20 -Z script precompile_wasm.rs --path payloads/simple-up cp examples/simple-updates/*.cwasm examples/insecure-updates/ cp examples/async-bindings/payload.cwasm examples/updatable-async/async-payload.cwasm +cp examples/async-bindings/payload.cwasm examples/suit-updatable/payload.cwasm cargo +nightly-2026-01-20 -Z script precompile_wasm.rs --path payloads/sensors/Cargo.toml -o examples/fake-sensor/payload.cwasm --config payloads/.cargo/config.toml --toolchain +nightly-2026-01-20 From ff4ee0d1de5a6ff57f71116f2369f29c9a81b221 Mon Sep 17 00:00:00 2001 From: Jakob Hirschler Date: Thu, 6 Aug 2026 18:17:21 +0200 Subject: [PATCH 3/3] feat(suit-updatable): extract vm-control related code from main.rs Signed-off-by: Jakob Hirschler --- examples/suit-updatable/src/main.rs | 151 +-------------------- examples/suit-updatable/src/vm_control.rs | 155 ++++++++++++++++++++++ 2 files changed, 158 insertions(+), 148 deletions(-) create mode 100644 examples/suit-updatable/src/vm_control.rs diff --git a/examples/suit-updatable/src/main.rs b/examples/suit-updatable/src/main.rs index fcd95ce..e9ac621 100644 --- a/examples/suit-updatable/src/main.rs +++ b/examples/suit-updatable/src/main.rs @@ -4,20 +4,11 @@ extern crate alloc; use core::ptr::NonNull; -use alloc::boxed::Box; use alloc::vec::Vec; -use ariel_os::coap::coap_run; use ariel_os::debug::log::{Debug2Format, error, info, warn}; -use coap_handler::Handler; -use coap_handler_implementations::{HandlerBuilder, ReportingHandlerBuilder, new_dispatcher}; - -use coap_message::{Code, OptionNumber}; - -use coap_message_utils::Error as CoapError; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::channel::Channel; -use embassy_sync::signal::Signal; use embassy_futures::select::{Either, select}; @@ -27,9 +18,11 @@ use wasmtime::{Config, Engine, Error as WasmtimeError, Store}; use ariel_os_bindings::wasm::ArielOSHost; use crate::suit::{UpdateError, build_and_authenticate_manifest, fetch_and_verify_update}; +use crate::vm_control::{VmEvent, wait_for_update_request}; mod coap_fetch; mod suit; +mod vm_control; bindgen!({ world: "example-async", @@ -43,153 +36,15 @@ bindgen!({ require_store_data_send: true, }); -static SUIT_VERIFY_SIGNAL: Signal> = Signal::new(); static VM_DROP_REQUESTS: Channel = Channel::new(); static VM_STATUS_SIGNAL: Channel = Channel::new(); static UPDATE_RESULTS: Channel, ()>, 1> = Channel::new(); -#[derive(Debug)] -enum VmEvent { - Dropped, - Finished, -} - -struct VmControl { - payload: Vec, -} - -impl VmControl { - fn new() -> Self { - Self { - payload: Vec::new(), - } - } -} - -impl Handler for VmControl { - type RequestData = (Option, u8); - - type ExtractRequestError = coap_message_utils::Error; - type BuildResponseError = coap_message_utils::Error; - - fn extract_request_data( - &mut self, - request: &M, - ) -> Result { - use coap_message::MessageOption; - use coap_message_utils::OptionsExt; - - match request.code().into() { - coap_numbers::code::DELETE => { - info!("Received DELETE request for SUIT-Manifest"); - request.options().ignore_elective_others()?; - - self.payload.clear(); - - Ok((None, coap_numbers::code::DELETED)) - } - - coap_numbers::code::PUT => { - info!("Received PUT request for program "); - let mut block1: Option = None; - - request - .options() - .filter(|o| { - if o.number() == coap_numbers::option::BLOCK1 - && let Some(n) = o.value_uint() - && block1.is_none() - { - block1 = Some(n); - false - } else { - true - } - }) - .ignore_elective_others()?; - - // This is a bit of a simplification, but ignoring the block size and just - // appending is really kind'a fine IMO. - let block1_value = block1.unwrap_or(0); - - // FIXME there's probably a Size1 option; if so, reallocate to fail early. - - let szx = block1_value & 0x7; - let blocksize = 1usize << (4 + szx); - let offset = (block1_value >> 4) as usize * blocksize; - - if offset == 0 { - self.payload.clear(); - } - if self.payload.len() != offset { - return Ok((None, coap_numbers::code::REQUEST_ENTITY_INCOMPLETE)); - } - - let payload = request.payload(); - self.payload.try_reserve_exact(payload.len()).map_err(|e| { - info!( - "Failed to reserve memory for program: {:?}", - Debug2Format(&e) - ); - CoapError::internal_server_error() - })?; - self.payload.extend_from_slice(payload); - - if (block1_value & 0x8) == 0x8 { - Ok((block1, coap_numbers::code::CONTINUE)) - } else { - let image = core::mem::take(&mut self.payload); - SUIT_VERIFY_SIGNAL.signal(image.into_boxed_slice()); - Ok((block1, coap_numbers::code::CHANGED)) - } - } - - _ => Err(CoapError::method_not_allowed()), - } - } - - fn estimate_length(&mut self, _request: &Self::RequestData) -> usize { - 1 - } - - fn build_response( - &mut self, - response: &mut M, - request: Self::RequestData, - ) -> Result<(), Self::BuildResponseError> { - let (block1, code) = request; - response.set_code(M::Code::new(code).map_err(CoapError::from_unionerror)?); - - if let Some(block1) = block1 { - response - .add_option_uint( - M::OptionNumber::new(coap_numbers::option::BLOCK1) - .map_err(CoapError::from_unionerror)?, - block1 as u32, - ) - .map_err(CoapError::from_unionerror)?; - } - Ok(()) - } -} - -#[ariel_os::task(autostart)] -async fn coap_task() { - let control = VmControl::new(); - - let handler = new_dispatcher() - .at_with_attributes(&["vm-control"], &[], control) - .with_wkc(); - - info!("Starting CoAP handler"); - coap_run(handler).await; -} - #[ariel_os::task(autostart)] async fn suit_update_task() { let mut accepted_sequence_number = None; loop { - let envelope = SUIT_VERIFY_SIGNAL.wait().await; + let envelope = wait_for_update_request().await; info!("[SUIT] Received update request"); let (manifest, sequence_number) = match build_and_authenticate_manifest(&envelope) { diff --git a/examples/suit-updatable/src/vm_control.rs b/examples/suit-updatable/src/vm_control.rs new file mode 100644 index 0000000..aac264e --- /dev/null +++ b/examples/suit-updatable/src/vm_control.rs @@ -0,0 +1,155 @@ +use alloc::boxed::Box; +use alloc::vec::Vec; + +use ariel_os::coap::coap_run; +use ariel_os::debug::log::{Debug2Format, info}; + +use coap_handler::Handler; +use coap_handler_implementations::{HandlerBuilder, ReportingHandlerBuilder, new_dispatcher}; +use coap_message::{Code, OptionNumber}; +use coap_message_utils::Error as CoapError; +use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; +use embassy_sync::signal::Signal; + +static SUIT_VERIFY_SIGNAL: Signal> = Signal::new(); + +#[derive(Debug)] +pub enum VmEvent { + Dropped, + Finished, +} + +struct VmControl { + payload: Vec, +} + +impl VmControl { + fn new() -> Self { + Self { + payload: Vec::new(), + } + } +} + +impl Handler for VmControl { + type RequestData = (Option, u8); + + type ExtractRequestError = coap_message_utils::Error; + type BuildResponseError = coap_message_utils::Error; + + fn extract_request_data( + &mut self, + request: &M, + ) -> Result { + use coap_message::MessageOption; + use coap_message_utils::OptionsExt; + + match request.code().into() { + coap_numbers::code::DELETE => { + info!("Received DELETE request for SUIT-Manifest"); + request.options().ignore_elective_others()?; + + self.payload.clear(); + + Ok((None, coap_numbers::code::DELETED)) + } + + coap_numbers::code::PUT => { + info!("Received PUT request for program "); + let mut block1: Option = None; + + request + .options() + .filter(|o| { + if o.number() == coap_numbers::option::BLOCK1 + && let Some(n) = o.value_uint() + && block1.is_none() + { + block1 = Some(n); + false + } else { + true + } + }) + .ignore_elective_others()?; + + // This is a bit of a simplification, but ignoring the block size and just + // appending is really kind'a fine IMO. + let block1_value = block1.unwrap_or(0); + + // FIXME there's probably a Size1 option; if so, reallocate to fail early. + + let szx = block1_value & 0x7; + let blocksize = 1usize << (4 + szx); + let offset = (block1_value >> 4) as usize * blocksize; + + if offset == 0 { + self.payload.clear(); + } + if self.payload.len() != offset { + return Ok((None, coap_numbers::code::REQUEST_ENTITY_INCOMPLETE)); + } + + let payload = request.payload(); + self.payload.try_reserve_exact(payload.len()).map_err(|e| { + info!( + "Failed to reserve memory for program: {:?}", + Debug2Format(&e) + ); + CoapError::internal_server_error() + })?; + self.payload.extend_from_slice(payload); + + if (block1_value & 0x8) == 0x8 { + Ok((block1, coap_numbers::code::CONTINUE)) + } else { + let image = core::mem::take(&mut self.payload); + SUIT_VERIFY_SIGNAL.signal(image.into_boxed_slice()); + Ok((block1, coap_numbers::code::CHANGED)) + } + } + + _ => Err(CoapError::method_not_allowed()), + } + } + + fn estimate_length(&mut self, _request: &Self::RequestData) -> usize { + 1 + } + + fn build_response( + &mut self, + response: &mut M, + request: Self::RequestData, + ) -> Result<(), Self::BuildResponseError> { + let (block1, code) = request; + response.set_code(M::Code::new(code).map_err(CoapError::from_unionerror)?); + + if let Some(block1) = block1 { + response + .add_option_uint( + M::OptionNumber::new(coap_numbers::option::BLOCK1) + .map_err(CoapError::from_unionerror)?, + block1 as u32, + ) + .map_err(CoapError::from_unionerror)?; + } + Ok(()) + } +} + +pub async fn wait_for_update_request() -> Box<[u8]> { + SUIT_VERIFY_SIGNAL.wait().await +} + +#[ariel_os::task(autostart)] +async fn coap_task() { + let control = VmControl::new(); + + let handler = new_dispatcher() + .at_with_attributes(&["vm-control"], &[], control) + .with_wkc(); + + info!("Starting CoAP handler"); + coap_run(handler).await; +}