From 3f434e7d7d0959063684e4599810a56ef5debdd2 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 10:51:51 -0700 Subject: [PATCH 1/6] Add SUIT manifest verify + process support (off by default, wolfCOSE-backed) --- .gitmodules | 4 + docs/SUIT.md | 94 ++++++++ include/suit.h | 212 ++++++++++++++++++ lib/wolfCOSE | 1 + options.mk | 23 ++ src/suit/suit_parse.c | 237 ++++++++++++++++++++ src/suit/suit_process.c | 334 +++++++++++++++++++++++++++ src/suit/suit_verify.c | 225 +++++++++++++++++++ src/suit/suit_wolfboot.c | 60 +++++ tests/suit_cross_check.py | 61 +++++ tests/suit_host_test.sh | 33 +++ tests/suit_test.c | 373 +++++++++++++++++++++++++++++++ tests/vectors/suit_envelope.cbor | Bin 0 -> 289 bytes 13 files changed, 1657 insertions(+) create mode 100644 docs/SUIT.md create mode 100644 include/suit.h create mode 160000 lib/wolfCOSE create mode 100644 src/suit/suit_parse.c create mode 100644 src/suit/suit_process.c create mode 100644 src/suit/suit_verify.c create mode 100644 src/suit/suit_wolfboot.c create mode 100644 tests/suit_cross_check.py create mode 100755 tests/suit_host_test.sh create mode 100644 tests/suit_test.c create mode 100644 tests/vectors/suit_envelope.cbor diff --git a/.gitmodules b/.gitmodules index 13396fd2ec..b43a22b01f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,7 @@ [submodule "lib/wolfHAL"] path = lib/wolfHAL url = https://github.com/wolfSSL/wolfHAL.git +[submodule "lib/wolfCOSE"] + path = lib/wolfCOSE + url = https://github.com/wolfSSL/wolfCOSE.git + branch = main diff --git a/docs/SUIT.md b/docs/SUIT.md new file mode 100644 index 0000000000..c7e8081266 --- /dev/null +++ b/docs/SUIT.md @@ -0,0 +1,94 @@ +# SUIT Manifest Support (experimental) + +wolfBoot can verify and process **SUIT manifests** +(`draft-ietf-suit-manifest-34`) as an alternative to its native TLV-signed image +header. SUIT is an IETF-standard, CBOR-encoded, COSE-signed manifest that +describes a firmware update: the image digest, the target device identity +(vendor/class), and the install command sequence. + +This is **off by default** and gated behind `WOLFBOOT_SUIT`. The native TLV path +is unchanged when it is not enabled. SUIT is intended for richer, networked +secure update (e.g. wolfUpdate); the lean TLV path remains the default for plain +single-image secure boot. + +## What it provides + +- **Authenticity + integrity**: the manifest is signed with COSE_Sign1 + (`wc_CoseSign1_Verify`, via the `lib/wolfCOSE` submodule), and the signed + `SUIT_Digest` is bound to `hash(manifest)`. +- **Image binding**: `suit-condition-image-match` hashes the staged component and + compares it to the digest the manifest authorizes. +- **Identity checks**: `suit-condition-vendor-identifier` / + `-class-identifier` against this device's configured identity. + +## Build + +```sh +make WOLFBOOT_SUIT=1 SIGN=ECC256 ... +``` + +The `WOLFBOOT_SUIT` block in `options.mk` adds the SUIT sources plus the wolfCOSE +verify objects (lean, `WOLFCOSE_LEAN_VERIFY`). Fine-tuning macros: + +| Macro | Effect | +| --- | --- | +| `SUIT_INSTALL_HANDOFF` (default) | verify then drive the existing A/B swap | +| `SUIT_INSTALL_DIRECTIVES` | SUIT copy/write directives drive flash directly | +| `SUIT_HAVE_FETCH` / `SUIT_HAVE_TRY_EACH` / `SUIT_HAVE_RUN_SEQUENCE` | optional commands | + +## Architecture + +The processor is host-agnostic: it never touches flash directly. Storage access +is via a pluggable `struct suit_component_ops` (hash/write/copy) the host +supplies. In wolfBoot those wrap the flash HAL; the host unit test wraps a RAM +buffer. This keeps the SUIT code reusable outside wolfBoot. + +- `suit_open()` — parse the envelope + manifest (zero-copy offsets). +- `suit_verify_auth()` — COSE_Sign1 + digest binding. +- `suit_process()` — command-sequence interpreter (conditions/directives). + +## Test + +Three layers (A self, B interop, C frozen): + +```sh +# A: author a full signed SUIT envelope with wolfCOSE and run the whole chain +# (parse, verify, identity-validate, install via directive-write, image-match) +# plus tamper cases. Also writes /tmp/suit_envelope.cbor. +WOLFSSL_DIR=/usr/local ./tests/suit_host_test.sh + +# B: cross-check that envelope with implementations other than wolfCOSE +# (cbor2 + hashlib + cryptography), the interop step. +# C: by default it validates the committed frozen vector tests/vectors/. +python3 tests/suit_cross_check.py # pip install cbor2 cryptography +``` + +A needs a host wolfSSL with ECC (sign+verify) and the `lib/wolfCOSE` submodule +(`git submodule update --init lib/wolfCOSE`). + +## Compliance + +The CBOR/COSE structures and all integer codes follow draft-34 (cross-checked +against the IANA SUIT registry). This implements the minimal "trusted +invocation" profile: the codes/conditions/directives listed above. It is +**format-compliant for that profile and interop-verified** against independent +CBOR/COSE tooling (test B); it is not a full draft-34 implementation. + +- Unrecognized (or known-but-unsupported) commands are **default-denied** (the + sequence fails), as a SUIT processor must, rather than silently skipped. +- Not implemented (and rejected if present): directive-fetch, severable members, + try-each / run-sequence / swap, dependencies/trust-domains, SUIT Reports. + +## Status + +Implemented + host-tested + interop cross-checked: parse, COSE_Sign1 verify + +digest binding, the command interpreter (identity + image-match conditions, +set-component-index / override-parameters / write / copy directives), default +deny, and the `wolfBoot_suit_verify()` entry point. + +Follow-ups: A/B-swap handoff wiring from `wolfBoot_verify_authenticity`, and +payload encryption (COSE_Encrypt0) for confidentiality. + +This PR is gated on the wolfCOSE fixes in wolfSSL/wolfCOSE PR #53; the submodule +is pinned to that work and should be repinned to the wolfCOSE v1.0 tag before +merge. diff --git a/include/suit.h b/include/suit.h new file mode 100644 index 0000000000..63573fc8ff --- /dev/null +++ b/include/suit.h @@ -0,0 +1,212 @@ +/* suit.h + * + * Copyright (C) 2025 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ +/** + * @file suit.h + * @brief SUIT manifest (draft-ietf-suit-manifest-34) processing for wolfBoot. + * + * Compiled only when WOLFBOOT_SUIT is defined. Uses wolfCOSE for CBOR decode and + * COSE_Sign1 verification. Zero dynamic allocation: all state is caller-provided. + */ +#ifndef SUIT_H +#define SUIT_H + +#ifdef WOLFBOOT_SUIT + +#include +#include + +/* Return codes. 0 is success; negatives are errors. */ +#define SUIT_SUCCESS 0 +#define SUIT_E_NOT_IMPLEMENTED (-1) +#define SUIT_E_INVALID_ARG (-2) +#define SUIT_E_PARSE (-3) +#define SUIT_E_AUTH (-4) +#define SUIT_E_DIGEST_MISMATCH (-5) +#define SUIT_E_CONDITION (-6) +#define SUIT_E_UNSUPPORTED (-7) +#define SUIT_E_CRYPTO (-8) +#define SUIT_E_INSTALL (-9) + +/* SUIT_Envelope map keys (IANA SUIT Envelope Elements). */ +enum suit_envelope_key { + SUIT_ENV_AUTHENTICATION_WRAPPER = 2, + SUIT_ENV_MANIFEST = 3, + SUIT_ENV_PAYLOAD_FETCH = 16, + SUIT_ENV_INSTALL = 20, + SUIT_ENV_TEXT = 23 +}; + +/* SUIT_Manifest map keys (IANA SUIT Manifest Elements). */ +enum suit_manifest_key { + SUIT_MAN_VERSION = 1, + SUIT_MAN_SEQUENCE_NUMBER = 2, + SUIT_MAN_COMMON = 3, + SUIT_MAN_REFERENCE_URI = 4, + SUIT_MAN_VALIDATE = 7, + SUIT_MAN_LOAD = 8, + SUIT_MAN_INVOKE = 9, + SUIT_MAN_PAYLOAD_FETCH = 16, + SUIT_MAN_INSTALL = 20, + SUIT_MAN_TEXT = 23 +}; + +/* SUIT_Common map keys (IANA SUIT Common Elements). */ +enum suit_common_key { + SUIT_COMMON_DEPENDENCIES = 1, + SUIT_COMMON_COMPONENTS = 2, + SUIT_COMMON_SHARED_SEQUENCE = 4 +}; + +/* SUIT_Condition codes (IANA SUIT Commands). */ +enum suit_condition { + SUIT_COND_VENDOR_IDENTIFIER = 1, + SUIT_COND_CLASS_IDENTIFIER = 2, + SUIT_COND_IMAGE_MATCH = 3, + SUIT_COND_COMPONENT_SLOT = 5, + SUIT_COND_CHECK_CONTENT = 6, + SUIT_COND_ABORT = 14, + SUIT_COND_DEVICE_IDENTIFIER = 24 +}; + +/* SUIT_Directive codes (IANA SUIT Commands). */ +enum suit_directive { + SUIT_DIR_SET_COMPONENT_INDEX = 12, + SUIT_DIR_TRY_EACH = 15, + SUIT_DIR_WRITE = 18, + SUIT_DIR_OVERRIDE_PARAMETERS = 20, + SUIT_DIR_FETCH = 21, + SUIT_DIR_COPY = 22, + SUIT_DIR_INVOKE = 23, + SUIT_DIR_SWAP = 31, + SUIT_DIR_RUN_SEQUENCE = 32 +}; + +/* SUIT_Parameters map keys (IANA SUIT Parameters). */ +enum suit_parameter { + SUIT_PARAM_VENDOR_IDENTIFIER = 1, + SUIT_PARAM_CLASS_IDENTIFIER = 2, + SUIT_PARAM_IMAGE_DIGEST = 3, + SUIT_PARAM_COMPONENT_SLOT = 5, + SUIT_PARAM_STRICT_ORDER = 12, + SUIT_PARAM_SOFT_FAILURE = 13, + SUIT_PARAM_IMAGE_SIZE = 14, + SUIT_PARAM_CONTENT = 18, + SUIT_PARAM_URI = 21, + SUIT_PARAM_SOURCE_COMPONENT = 22, + SUIT_PARAM_INVOKE_ARGS = 23, + SUIT_PARAM_DEVICE_IDENTIFIER = 24, + SUIT_PARAM_FETCH_ARGUMENTS = 25 +}; + +/* COSE algorithm ids used by SUIT_Digest (COSE Algorithms registry). */ +enum suit_cose_digest_alg { + SUIT_COSE_ALG_SHA_256 = -16, + SUIT_COSE_ALG_SHAKE128 = -18, + SUIT_COSE_ALG_SHA_384 = -43, + SUIT_COSE_ALG_SHA_512 = -44 +}; + +#ifndef SUIT_MAX_COMPONENTS +#define SUIT_MAX_COMPONENTS 1 +#endif + +/* Scratch for COSE_Sign1 Sig_structure reconstruction during verify. 512 bytes + * covers ES256/384/512 and EdDSA; raise for ML-DSA manifests. */ +#ifndef SUIT_SCRATCH_SZ +#define SUIT_SCRATCH_SZ 512 +#endif + +/* Trust-anchor keystore slot used to verify the manifest signature. */ +#ifndef SUIT_KEY_SLOT +#define SUIT_KEY_SLOT 0 +#endif + +/* Parsed SUIT envelope/manifest. Holds zero-copy offsets into the caller's + * input buffer; no copies are made. */ +struct suit_manifest { + const uint8_t* env; /* envelope buffer (caller-owned) */ + size_t envLen; + const uint8_t* manifest; /* bstr-wrapped SUIT_Manifest */ + size_t manifestLen; + const uint8_t* authWrapper; /* suit-authentication-wrapper contents */ + size_t authWrapperLen; + const uint8_t* common; /* suit-common (bstr-wrapped SUIT_Common) */ + size_t commonLen; + const uint8_t* sharedSeq; /* suit-shared-sequence (command sequence) */ + size_t sharedSeqLen; + const uint8_t* validate; /* suit-validate command sequence */ + size_t validateLen; + const uint8_t* install; /* suit-install command sequence */ + size_t installLen; +}; + +/* Parameter store: set by directive-override-parameters, consumed by the + * conditions/directives that follow. Zero-copy into the manifest buffer. */ +struct suit_params { + const uint8_t* imageDigest; /* SUIT_Digest CBOR [alg, bytes] */ + size_t imageDigestLen; + const uint8_t* vendorId; /* RFC 4122 UUID bytes */ + size_t vendorIdLen; + const uint8_t* classId; + size_t classIdLen; + const uint8_t* content; /* directive-write content */ + size_t contentLen; + uint64_t imageSize; + size_t sourceComponent; + int componentSlot; +}; + +/* Host-provided component I/O. The interpreter never touches flash/storage + * directly, which keeps it reusable outside wolfBoot (the host supplies these). + * All return 0 on success, negative on error. */ +struct suit_component_ops { + void* ctx; + int (*hash)(void* ctx, size_t idx, uint8_t* out, size_t outLen); + int (*write)(void* ctx, size_t idx, const uint8_t* src, size_t len); + int (*copy)(void* ctx, size_t idx, size_t srcIdx); +}; + +/* Command-sequence interpreter state. Fixed-size, no heap. */ +struct suit_context { + struct suit_manifest* m; + const struct suit_component_ops* ops; + struct suit_params params; + size_t componentIndex; + const uint8_t* deviceVendorId; /* this device's identity */ + size_t deviceVendorIdLen; + const uint8_t* deviceClassId; + size_t deviceClassIdLen; +}; + +int suit_open(struct suit_manifest* m, const uint8_t* env, size_t len); +int suit_verify_auth(struct suit_manifest* m); +int suit_process(struct suit_context* ctx, struct suit_manifest* m); + +/* wolfBoot entry point: open + authenticate + process a staged SUIT envelope. + * The caller supplies the component I/O ops and this device's identity. */ +int wolfBoot_suit_verify(const uint8_t* env, size_t envLen, + const struct suit_component_ops* ops, + const uint8_t* vendorId, size_t vendorIdLen, + const uint8_t* classId, size_t classIdLen); + +#endif /* WOLFBOOT_SUIT */ + +#endif /* SUIT_H */ diff --git a/lib/wolfCOSE b/lib/wolfCOSE new file mode 160000 index 0000000000..acd83c0291 --- /dev/null +++ b/lib/wolfCOSE @@ -0,0 +1 @@ +Subproject commit acd83c0291530fb9e75edcf6407b09fc3ee8367a diff --git a/options.mk b/options.mk index 37f676d797..3770eaff28 100644 --- a/options.mk +++ b/options.mk @@ -1574,3 +1574,26 @@ endif ifeq ($(TZEN),1) CFLAGS+=-DTZEN endif + +# SUIT manifest update support (draft-ietf-suit-manifest-34), off by default. +# Uses wolfCOSE (lib/wolfCOSE) for CBOR decode + COSE_Sign1 verify. See docs/SUIT.md. +ifeq ($(WOLFBOOT_SUIT),1) + CFLAGS+=-DWOLFBOOT_SUIT + CFLAGS+=-I$(WOLFBOOT_ROOT)/lib/wolfCOSE/include -DWOLFCOSE_LEAN_VERIFY + OBJS+=./src/suit/suit_parse.o ./src/suit/suit_verify.o ./src/suit/suit_process.o ./src/suit/suit_wolfboot.o + OBJS+=./lib/wolfCOSE/src/wolfcose.o ./lib/wolfCOSE/src/wolfcose_cbor.o + ifeq ($(SUIT_INSTALL_DIRECTIVES),1) + CFLAGS+=-DSUIT_INSTALL_DIRECTIVES + else + CFLAGS+=-DSUIT_INSTALL_HANDOFF + endif + ifeq ($(SUIT_HAVE_FETCH),1) + CFLAGS+=-DSUIT_HAVE_FETCH + endif + ifeq ($(SUIT_HAVE_TRY_EACH),1) + CFLAGS+=-DSUIT_HAVE_TRY_EACH + endif + ifeq ($(SUIT_HAVE_RUN_SEQUENCE),1) + CFLAGS+=-DSUIT_HAVE_RUN_SEQUENCE + endif +endif diff --git a/src/suit/suit_parse.c b/src/suit/suit_parse.c new file mode 100644 index 0000000000..ea71424337 --- /dev/null +++ b/src/suit/suit_parse.c @@ -0,0 +1,237 @@ +/* suit_parse.c + * + * Copyright (C) 2025 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ +/** + * @file suit_parse.c + * @brief Parse a SUIT_Envelope (draft-ietf-suit-manifest-34) and locate its + * authentication-wrapper and manifest members. Zero-copy: records offsets into + * the caller's buffer using the wolfCOSE CBOR decoder. + */ +#include "suit.h" + +#ifdef WOLFBOOT_SUIT + +#include + +#define SUIT_CBOR_MAJOR_TAG 6u + +/* Parse SUIT_Common to locate suit-shared-sequence (a bstr-wrapped sequence). */ +static int suit_parse_common(struct suit_manifest* m) +{ + int ret = SUIT_SUCCESS; + WOLFCOSE_CBOR_CTX ctx; + size_t count = 0; + size_t i; + int64_t key = 0; + const uint8_t* data = NULL; + size_t dataLen = 0; + + ctx.buf = NULL; + ctx.cbuf = m->common; + ctx.bufSz = m->commonLen; + ctx.idx = 0; + + if (wc_CBOR_DecodeMapStart(&ctx, &count) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + for (i = 0; (i < count) && (ret == SUIT_SUCCESS); i++) { + if (wc_CBOR_DecodeInt(&ctx, &key) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else if (key == (int64_t)SUIT_COMMON_SHARED_SEQUENCE) { + if (wc_CBOR_DecodeBstr(&ctx, &data, &dataLen) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + m->sharedSeq = data; + m->sharedSeqLen = dataLen; + } + } + else { + if (wc_CBOR_Skip(&ctx) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + } + return ret; +} + +/* Parse the SUIT_Manifest map to locate common, validate and install. The + * command sequences and common are bstr-wrapped per draft-34. */ +static int suit_parse_manifest(struct suit_manifest* m) +{ + int ret = SUIT_SUCCESS; + WOLFCOSE_CBOR_CTX ctx; + size_t count = 0; + size_t i; + int64_t key = 0; + const uint8_t* data = NULL; + size_t dataLen = 0; + + ctx.buf = NULL; + ctx.cbuf = m->manifest; + ctx.bufSz = m->manifestLen; + ctx.idx = 0; + + if (wc_CBOR_DecodeMapStart(&ctx, &count) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + for (i = 0; (i < count) && (ret == SUIT_SUCCESS); i++) { + if (wc_CBOR_DecodeInt(&ctx, &key) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else if (key == (int64_t)SUIT_MAN_COMMON) { + if (wc_CBOR_DecodeBstr(&ctx, &data, &dataLen) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + m->common = data; + m->commonLen = dataLen; + } + } + else if (key == (int64_t)SUIT_MAN_VALIDATE) { + if (wc_CBOR_DecodeBstr(&ctx, &data, &dataLen) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + m->validate = data; + m->validateLen = dataLen; + } + } + else if (key == (int64_t)SUIT_MAN_INSTALL) { + if (wc_CBOR_DecodeBstr(&ctx, &data, &dataLen) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + m->install = data; + m->installLen = dataLen; + } + } + else { + if (wc_CBOR_Skip(&ctx) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + } + if ((ret == SUIT_SUCCESS) && (m->common != NULL)) { + ret = suit_parse_common(m); + } + return ret; +} + +int suit_open(struct suit_manifest* m, const uint8_t* env, size_t len) +{ + int ret = SUIT_SUCCESS; + int cret = WOLFCOSE_SUCCESS; + WOLFCOSE_CBOR_CTX ctx; + size_t count = 0; + size_t i; + int64_t key = 0; + const uint8_t* data = NULL; + size_t dataLen = 0; + uint64_t tag = 0; + + if ((m == NULL) || (env == NULL) || (len == 0u)) { + return SUIT_E_INVALID_ARG; + } + + m->env = env; + m->envLen = len; + m->manifest = NULL; + m->manifestLen = 0; + m->authWrapper = NULL; + m->authWrapperLen = 0; + m->common = NULL; + m->commonLen = 0; + m->sharedSeq = NULL; + m->sharedSeqLen = 0; + m->validate = NULL; + m->validateLen = 0; + m->install = NULL; + m->installLen = 0; + + ctx.buf = NULL; + ctx.cbuf = env; + ctx.bufSz = len; + ctx.idx = 0; + + /* SUIT_Envelope may carry an optional CBOR tag (#6.107). Consume it. */ + if (wc_CBOR_PeekType(&ctx) == SUIT_CBOR_MAJOR_TAG) { + cret = wc_CBOR_DecodeTag(&ctx, &tag); + if (cret != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + + if (ret == SUIT_SUCCESS) { + cret = wc_CBOR_DecodeMapStart(&ctx, &count); + if (cret != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + + for (i = 0; (i < count) && (ret == SUIT_SUCCESS); i++) { + cret = wc_CBOR_DecodeInt(&ctx, &key); + if (cret != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else if (key == (int64_t)SUIT_ENV_AUTHENTICATION_WRAPPER) { + cret = wc_CBOR_DecodeBstr(&ctx, &data, &dataLen); + if (cret != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + m->authWrapper = data; + m->authWrapperLen = dataLen; + } + } + else if (key == (int64_t)SUIT_ENV_MANIFEST) { + cret = wc_CBOR_DecodeBstr(&ctx, &data, &dataLen); + if (cret != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + m->manifest = data; + m->manifestLen = dataLen; + } + } + else { + /* Severable members (text, payload-fetch, install) not needed here. */ + cret = wc_CBOR_Skip(&ctx); + if (cret != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + } + + if (ret == SUIT_SUCCESS) { + if ((m->manifest == NULL) || (m->authWrapper == NULL)) { + ret = SUIT_E_PARSE; + } + } + + if (ret == SUIT_SUCCESS) { + ret = suit_parse_manifest(m); + } + + return ret; +} + +#endif /* WOLFBOOT_SUIT */ diff --git a/src/suit/suit_process.c b/src/suit/suit_process.c new file mode 100644 index 0000000000..69c6b29603 --- /dev/null +++ b/src/suit/suit_process.c @@ -0,0 +1,334 @@ +/* suit_process.c + * + * Copyright (C) 2025 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ +/** + * @file suit_process.c + * @brief SUIT command-sequence interpreter (draft-ietf-suit-manifest-34). + * Executes the shared + validate sequences: parameter store, identity checks, + * and image-match against the staged component (hashed via the host ops). + */ +#include "suit.h" + +#ifdef WOLFBOOT_SUIT + +#include +#include + +#define SUIT_SHA256_SZ 32 + +/* Compare a SUIT_Digest = [ alg, bstr digest ] against a computed hash. */ +static int suit_digest_eq(const uint8_t* suitDigest, size_t suitDigestLen, + const uint8_t* hash, size_t hashLen) +{ + int ret = SUIT_SUCCESS; + WOLFCOSE_CBOR_CTX ctx; + size_t count = 0; + int64_t alg = 0; + const uint8_t* digBytes = NULL; + size_t digBytesLen = 0; + + ctx.buf = NULL; + ctx.cbuf = suitDigest; + ctx.bufSz = suitDigestLen; + ctx.idx = 0; + + if (wc_CBOR_DecodeArrayStart(&ctx, &count) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else if (count < 2u) { + ret = SUIT_E_PARSE; + } + if (ret == SUIT_SUCCESS) { + if (wc_CBOR_DecodeInt(&ctx, &alg) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else if (alg != (int64_t)SUIT_COSE_ALG_SHA_256) { + ret = SUIT_E_UNSUPPORTED; + } + } + if (ret == SUIT_SUCCESS) { + if (wc_CBOR_DecodeBstr(&ctx, &digBytes, &digBytesLen) + != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + if (ret == SUIT_SUCCESS) { + if ((digBytesLen != hashLen) || + (XMEMCMP(digBytes, hash, hashLen) != 0)) { + ret = SUIT_E_DIGEST_MISMATCH; + } + } + return ret; +} + +/* directive-override-parameters: store the { param-key: value } map. */ +static int suit_override(struct suit_context* ctx, WOLFCOSE_CBOR_CTX* c) +{ + int ret = SUIT_SUCCESS; + size_t count = 0; + size_t i; + int64_t pkey = 0; + const uint8_t* data = NULL; + size_t dataLen = 0; + uint64_t uval = 0; + + if (wc_CBOR_DecodeMapStart(c, &count) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + for (i = 0; (i < count) && (ret == SUIT_SUCCESS); i++) { + if (wc_CBOR_DecodeInt(c, &pkey) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else if (pkey == (int64_t)SUIT_PARAM_IMAGE_DIGEST) { + if (wc_CBOR_DecodeBstr(c, &data, &dataLen) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + ctx->params.imageDigest = data; + ctx->params.imageDigestLen = dataLen; + } + } + else if (pkey == (int64_t)SUIT_PARAM_VENDOR_IDENTIFIER) { + if (wc_CBOR_DecodeBstr(c, &data, &dataLen) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + ctx->params.vendorId = data; + ctx->params.vendorIdLen = dataLen; + } + } + else if (pkey == (int64_t)SUIT_PARAM_CLASS_IDENTIFIER) { + if (wc_CBOR_DecodeBstr(c, &data, &dataLen) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + ctx->params.classId = data; + ctx->params.classIdLen = dataLen; + } + } + else if (pkey == (int64_t)SUIT_PARAM_IMAGE_SIZE) { + if (wc_CBOR_DecodeUint(c, &uval) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + ctx->params.imageSize = uval; + } + } + else if (pkey == (int64_t)SUIT_PARAM_CONTENT) { + if (wc_CBOR_DecodeBstr(c, &data, &dataLen) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + ctx->params.content = data; + ctx->params.contentLen = dataLen; + } + } + else if (pkey == (int64_t)SUIT_PARAM_SOURCE_COMPONENT) { + if (wc_CBOR_DecodeUint(c, &uval) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + ctx->params.sourceComponent = (size_t)uval; + } + } + else { + if (wc_CBOR_Skip(c) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + } + return ret; +} + +/* condition-image-match: hash the current component, compare to image-digest. */ +static int suit_image_match(struct suit_context* ctx) +{ + int ret = SUIT_SUCCESS; + uint8_t hash[SUIT_SHA256_SZ]; + + if ((ctx->ops == NULL) || (ctx->ops->hash == NULL)) { + ret = SUIT_E_UNSUPPORTED; + } + else if (ctx->params.imageDigest == NULL) { + ret = SUIT_E_CONDITION; + } + else if (ctx->ops->hash(ctx->ops->ctx, ctx->componentIndex, hash, + sizeof(hash)) != 0) { + ret = SUIT_E_CRYPTO; + } + else { + ret = suit_digest_eq(ctx->params.imageDigest, + ctx->params.imageDigestLen, hash, sizeof(hash)); + } + return ret; +} + +static int suit_id_match(const uint8_t* paramId, size_t paramLen, + const uint8_t* devId, size_t devLen) +{ + int ret = SUIT_SUCCESS; + + if ((paramId == NULL) || (devId == NULL) || (paramLen != devLen) || + (XMEMCMP(paramId, devId, paramLen) != 0)) { + ret = SUIT_E_CONDITION; + } + return ret; +} + +/* Walk one SUIT_Command_Sequence: a flat array of (command, argument) pairs. */ +static int suit_run_sequence(struct suit_context* ctx, const uint8_t* seq, + size_t seqLen) +{ + int ret = SUIT_SUCCESS; + WOLFCOSE_CBOR_CTX c; + size_t count = 0; + size_t i = 0; + int64_t cmd = 0; + uint64_t uval = 0; + + c.buf = NULL; + c.cbuf = seq; + c.bufSz = seqLen; + c.idx = 0; + + if (wc_CBOR_DecodeArrayStart(&c, &count) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + + while ((i + 1u < count) && (ret == SUIT_SUCCESS)) { + if (wc_CBOR_DecodeInt(&c, &cmd) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else if (cmd == (int64_t)SUIT_DIR_SET_COMPONENT_INDEX) { + if (wc_CBOR_DecodeUint(&c, &uval) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + ctx->componentIndex = (size_t)uval; + } + } + else if (cmd == (int64_t)SUIT_DIR_OVERRIDE_PARAMETERS) { + ret = suit_override(ctx, &c); + } + else if (cmd == (int64_t)SUIT_DIR_WRITE) { + if (wc_CBOR_Skip(&c) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else if ((ctx->ops == NULL) || (ctx->ops->write == NULL)) { + ret = SUIT_E_UNSUPPORTED; + } + else if (ctx->params.content == NULL) { + ret = SUIT_E_INSTALL; + } + else if (ctx->ops->write(ctx->ops->ctx, ctx->componentIndex, + ctx->params.content, ctx->params.contentLen) != 0) { + ret = SUIT_E_INSTALL; + } + } + else if (cmd == (int64_t)SUIT_DIR_COPY) { + if (wc_CBOR_Skip(&c) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else if ((ctx->ops == NULL) || (ctx->ops->copy == NULL)) { + ret = SUIT_E_UNSUPPORTED; + } + else if (ctx->ops->copy(ctx->ops->ctx, ctx->componentIndex, + ctx->params.sourceComponent) != 0) { + ret = SUIT_E_INSTALL; + } + } + else if (cmd == (int64_t)SUIT_COND_IMAGE_MATCH) { + if (wc_CBOR_Skip(&c) != WOLFCOSE_SUCCESS) { /* reporting policy */ + ret = SUIT_E_PARSE; + } + else { + ret = suit_image_match(ctx); + } + } + else if (cmd == (int64_t)SUIT_COND_VENDOR_IDENTIFIER) { + if (wc_CBOR_Skip(&c) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + ret = suit_id_match(ctx->params.vendorId, + ctx->params.vendorIdLen, ctx->deviceVendorId, + ctx->deviceVendorIdLen); + } + } + else if (cmd == (int64_t)SUIT_COND_CLASS_IDENTIFIER) { + if (wc_CBOR_Skip(&c) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + ret = suit_id_match(ctx->params.classId, + ctx->params.classIdLen, ctx->deviceClassId, + ctx->deviceClassIdLen); + } + } + else if (cmd == (int64_t)SUIT_COND_ABORT) { + if (wc_CBOR_Skip(&c) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + ret = SUIT_E_CONDITION; + } + } + else { + /* Default-deny: a SUIT processor MUST abort on a command it does not + * implement rather than silently skip it. A skipped command could + * carry security-relevant intent, so an unrecognized (or known but + * unsupported) command fails the whole sequence. */ + ret = SUIT_E_UNSUPPORTED; + } + i += 2u; + } + return ret; +} + +int suit_process(struct suit_context* ctx, struct suit_manifest* m) +{ + int ret = SUIT_SUCCESS; + + if ((ctx == NULL) || (m == NULL)) { + return SUIT_E_INVALID_ARG; + } + ctx->m = m; + + /* suit-shared-sequence runs first (sets up parameters and component). */ + if ((ret == SUIT_SUCCESS) && (m->sharedSeq != NULL)) { + ret = suit_run_sequence(ctx, m->sharedSeq, m->sharedSeqLen); + } + /* suit-validate then asserts the staged image is the authorized one. */ + if ((ret == SUIT_SUCCESS) && (m->validate != NULL)) { + ret = suit_run_sequence(ctx, m->validate, m->validateLen); + } + + /* In SUIT-driven install mode, the install sequence (write/copy directives) + * places the image. In the default handoff mode the caller drives the + * existing A/B swap after this returns success, and install is not run. */ +#ifdef SUIT_INSTALL_DIRECTIVES + if ((ret == SUIT_SUCCESS) && (m->install != NULL)) { + ret = suit_run_sequence(ctx, m->install, m->installLen); + } +#endif + return ret; +} + +#endif /* WOLFBOOT_SUIT */ diff --git a/src/suit/suit_verify.c b/src/suit/suit_verify.c new file mode 100644 index 0000000000..6eb7ce25c4 --- /dev/null +++ b/src/suit/suit_verify.c @@ -0,0 +1,225 @@ +/* suit_verify.c + * + * Copyright (C) 2025 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ +/** + * @file suit_verify.c + * @brief SUIT authentication: COSE_Sign1 over the detached SUIT_Digest, then a + * separate binding of that digest to hash(manifest). draft-ietf-suit-manifest-34 + * section 8.3. ES256 / SHA-256 trusted-bootloader profile. + */ +#include "suit.h" + +#ifdef WOLFBOOT_SUIT + +#include +#include +#include +#include + +/* Provided by wolfBoot's keystore (src/keystore.c / flash_otp_keystore.c). + * Declared locally so this file does not pull in the generated keystore sizing + * header, keeping it buildable in minimal/host contexts. */ +extern uint8_t* keystore_get_buffer(int id); + +#define SUIT_P256_COORD_SZ 32 +#define SUIT_SHA256_SZ 32 + +/* Split SUIT_Authentication = [ bstr SUIT_Digest, bstr COSE_Sign1 ] into its + * two members, returning zero-copy pointers into the wrapper buffer. */ +static int suit_split_auth(const uint8_t* aw, size_t awLen, + const uint8_t** suitDigest, size_t* suitDigestLen, + const uint8_t** coseSign1, size_t* coseSign1Len) +{ + int ret = SUIT_SUCCESS; + WOLFCOSE_CBOR_CTX ctx; + size_t count = 0; + + ctx.buf = NULL; + ctx.cbuf = aw; + ctx.bufSz = awLen; + ctx.idx = 0; + + if (wc_CBOR_DecodeArrayStart(&ctx, &count) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else if (count < 2u) { + ret = SUIT_E_PARSE; + } + + if (ret == SUIT_SUCCESS) { + if (wc_CBOR_DecodeBstr(&ctx, suitDigest, suitDigestLen) + != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + if (ret == SUIT_SUCCESS) { + if (wc_CBOR_DecodeBstr(&ctx, coseSign1, coseSign1Len) + != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + return ret; +} + +/* Confirm the signed SUIT_Digest = [ alg, bstr digest ] matches hash(manifest). + * This is a check distinct from the signature verification: the signature only + * proves the signer signed the digest, not that the digest covers the manifest + * we hold. */ +static int suit_bind_digest(const uint8_t* suitDigest, size_t suitDigestLen, + const uint8_t* manifest, size_t manifestLen) +{ + int ret = SUIT_SUCCESS; + WOLFCOSE_CBOR_CTX ctx; + size_t count = 0; + int64_t alg = 0; + const uint8_t* digBytes = NULL; + size_t digBytesLen = 0; + uint8_t manHash[SUIT_SHA256_SZ]; + + ctx.buf = NULL; + ctx.cbuf = suitDigest; + ctx.bufSz = suitDigestLen; + ctx.idx = 0; + + if (wc_CBOR_DecodeArrayStart(&ctx, &count) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else if (count < 2u) { + ret = SUIT_E_PARSE; + } + if (ret == SUIT_SUCCESS) { + if (wc_CBOR_DecodeInt(&ctx, &alg) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + if (ret == SUIT_SUCCESS) { + if (alg != (int64_t)SUIT_COSE_ALG_SHA_256) { + ret = SUIT_E_UNSUPPORTED; + } + } + if (ret == SUIT_SUCCESS) { + if (wc_CBOR_DecodeBstr(&ctx, &digBytes, &digBytesLen) + != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + if (ret == SUIT_SUCCESS) { + if (digBytesLen != (size_t)SUIT_SHA256_SZ) { + ret = SUIT_E_DIGEST_MISMATCH; + } + } + if (ret == SUIT_SUCCESS) { + if (wc_Hash(WC_HASH_TYPE_SHA256, manifest, (word32)manifestLen, + manHash, (word32)sizeof(manHash)) != 0) { + ret = SUIT_E_CRYPTO; + } + } + if (ret == SUIT_SUCCESS) { + if (XMEMCMP(manHash, digBytes, SUIT_SHA256_SZ) != 0) { + ret = SUIT_E_DIGEST_MISMATCH; + } + } + return ret; +} + +int suit_verify_auth(struct suit_manifest* m) +{ + int ret = SUIT_SUCCESS; + const uint8_t* suitDigest = NULL; + size_t suitDigestLen = 0; + const uint8_t* coseSign1 = NULL; + size_t coseSign1Len = 0; + uint8_t* pub = NULL; + ecc_key eccKey; + int eccInit = 0; + WOLFCOSE_KEY key; + int keyInit = 0; + WOLFCOSE_HDR hdr; + const uint8_t* payload = NULL; + size_t payloadLen = 0; + uint8_t scratch[SUIT_SCRATCH_SZ]; + + if ((m == NULL) || (m->authWrapper == NULL) || (m->manifest == NULL)) { + return SUIT_E_INVALID_ARG; + } + + ret = suit_split_auth(m->authWrapper, m->authWrapperLen, + &suitDigest, &suitDigestLen, &coseSign1, &coseSign1Len); + + if (ret == SUIT_SUCCESS) { + pub = keystore_get_buffer(SUIT_KEY_SLOT); + if (pub == NULL) { + ret = SUIT_E_AUTH; + } + } + if (ret == SUIT_SUCCESS) { + if (wc_ecc_init(&eccKey) != 0) { + ret = SUIT_E_AUTH; + } + else { + eccInit = 1; + } + } + if (ret == SUIT_SUCCESS) { + if (wc_ecc_import_unsigned(&eccKey, pub, pub + SUIT_P256_COORD_SZ, + NULL, ECC_SECP256R1) != 0) { + ret = SUIT_E_AUTH; + } + } + if (ret == SUIT_SUCCESS) { + if (wc_CoseKey_Init(&key) != 0) { + ret = SUIT_E_AUTH; + } + else { + keyInit = 1; + } + } + if (ret == SUIT_SUCCESS) { + if (wc_CoseKey_SetEcc(&key, WOLFCOSE_CRV_P256, &eccKey) != 0) { + ret = SUIT_E_AUTH; + } + } + + /* Step 1: signature check only. Proves the signer signed the SUIT_Digest. */ + if (ret == SUIT_SUCCESS) { + if (wc_CoseSign1_Verify(&key, coseSign1, coseSign1Len, + suitDigest, suitDigestLen, NULL, 0, + scratch, sizeof(scratch), &hdr, &payload, &payloadLen) + != WOLFCOSE_SUCCESS) { + ret = SUIT_E_AUTH; + } + } + + /* Step 2: distinct binding of that digest to the manifest we hold. */ + if (ret == SUIT_SUCCESS) { + ret = suit_bind_digest(suitDigest, suitDigestLen, + m->manifest, m->manifestLen); + } + + if (keyInit != 0) { + wc_CoseKey_Free(&key); + } + if (eccInit != 0) { + wc_ecc_free(&eccKey); + } + return ret; +} + +#endif /* WOLFBOOT_SUIT */ diff --git a/src/suit/suit_wolfboot.c b/src/suit/suit_wolfboot.c new file mode 100644 index 0000000000..4f719aac78 --- /dev/null +++ b/src/suit/suit_wolfboot.c @@ -0,0 +1,60 @@ +/* suit_wolfboot.c + * + * Copyright (C) 2025 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ +/** + * @file suit_wolfboot.c + * @brief wolfBoot entry point for SUIT: run open + authenticate + process over a + * staged SUIT envelope. The caller (e.g. wolfUpdate, or a boot-time dispatch) + * supplies the envelope buffer, the component I/O ops (flash-backed on target), + * and this device's vendor/class identity. + */ +#include "suit.h" + +#ifdef WOLFBOOT_SUIT + +#include + +int wolfBoot_suit_verify(const uint8_t* env, size_t envLen, + const struct suit_component_ops* ops, + const uint8_t* vendorId, size_t vendorIdLen, + const uint8_t* classId, size_t classIdLen) +{ + int ret; + struct suit_manifest m; + struct suit_context ctx; + + ret = suit_open(&m, env, envLen); + if (ret == SUIT_SUCCESS) { + ret = suit_verify_auth(&m); + } + if (ret == SUIT_SUCCESS) { + memset(&ctx, 0, sizeof(ctx)); + ctx.m = &m; + ctx.ops = ops; + ctx.deviceVendorId = vendorId; + ctx.deviceVendorIdLen = vendorIdLen; + ctx.deviceClassId = classId; + ctx.deviceClassIdLen = classIdLen; + ret = suit_process(&ctx, &m); + } + return ret; +} + +#endif /* WOLFBOOT_SUIT */ diff --git a/tests/suit_cross_check.py b/tests/suit_cross_check.py new file mode 100644 index 0000000000..69fb9b02dc --- /dev/null +++ b/tests/suit_cross_check.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# Independent cross-check of a wolfCOSE-authored SUIT envelope, using +# implementations other than wolfCOSE: cbor2 (CBOR), hashlib (SHA-256), and +# cryptography (ECDSA / RFC 9052 Sig_structure). This is the interop step that +# turns "follows the SUIT/COSE format" into "verifiable by independent tools". +# +# ./tests/suit_host_test.sh # writes /tmp/suit_envelope.cbor +# python3 tests/suit_cross_check.py [envelope] # default: frozen vector +import sys +import hashlib +import cbor2 +from cryptography.hazmat.primitives.asymmetric import ec, utils +from cryptography.hazmat.primitives import hashes + +# Fixed P-256 test public key (matches tests/suit_test.c TEST_QX/TEST_QY). +QX = bytes.fromhex( + "60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6") +QY = bytes.fromhex( + "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299") + +DEFAULT = "tests/vectors/suit_envelope.cbor" +path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT +data = open(path, "rb").read() + +# 1. Envelope is valid CBOR with the SUIT envelope members. +env = cbor2.loads(data) +assert isinstance(env, dict) and 2 in env and 3 in env, "envelope map keys 2,3" +print("OK envelope: valid CBOR map with auth-wrapper(2) + manifest(3)") + +auth = cbor2.loads(env[2]) +assert isinstance(auth, list) and len(auth) >= 2, "SUIT_Authentication array" +suit_digest_bytes = auth[0] +cose = cbor2.loads(auth[1]) +assert getattr(cose, "tag", None) == 18, "COSE_Sign1_Tagged (#6.18)" +cose = cose.value +manifest_bytes = env[3] + +# 2. SUIT_Digest binds hash(manifest) [independent SHA-256]. +sd = cbor2.loads(suit_digest_bytes) +assert sd[0] == -16, "digest alg SHA-256 (COSE -16)" +assert hashlib.sha256(manifest_bytes).digest() == sd[1], "digest binds manifest" +print("OK digest: SUIT_Digest == SHA-256(manifest) [independent hashlib]") + +# 3. COSE_Sign1 ES256 signature over the RFC 9052 Sig_structure (detached +# payload = the bstr-wrapped SUIT_Digest) [independent cryptography]. +prot, sig = cose[0], cose[3] +sig_structure = cbor2.dumps(["Signature1", prot, b"", suit_digest_bytes]) +r = int.from_bytes(sig[:32], "big") +s = int.from_bytes(sig[32:], "big") +der = utils.encode_dss_signature(r, s) +pub = ec.EllipticCurvePublicNumbers( + int.from_bytes(QX, "big"), int.from_bytes(QY, "big"), + ec.SECP256R1()).public_key() +pub.verify(der, sig_structure, ec.ECDSA(hashes.SHA256())) +print("OK cose: COSE_Sign1 ES256 verifies [independent cryptography]") + +# 4. Manifest decodes and carries the expected members. +man = cbor2.loads(manifest_bytes) +assert 3 in man, "manifest has suit-common(3)" +print("OK manifest: decodes; keys =", sorted(man.keys()), "[independent cbor2]") +print("CROSS-CHECK PASSED: wolfCOSE SUIT envelope verified by independent tools") diff --git a/tests/suit_host_test.sh b/tests/suit_host_test.sh new file mode 100755 index 0000000000..53fe5bb91e --- /dev/null +++ b/tests/suit_host_test.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# Build and run the SUIT host unit test (tests/suit_test.c) against a host +# wolfSSL plus the lib/wolfCOSE submodule. ES256 / SHA-256 profile. +# +# WOLFSSL_DIR=/usr/local ./tests/suit_host_test.sh +# +# Requires a host wolfSSL with ECC sign+verify and SHA-256, and the wolfCOSE +# submodule checked out: git submodule update --init lib/wolfCOSE +set -e + +WOLFSSL_DIR=${WOLFSSL_DIR:-/usr/local} +CC=${CC:-cc} +ROOT=$(cd "$(dirname "$0")/.." && pwd) +OUT=${OUT:-/tmp/suit_host_test} + +cd "$ROOT" + +# wolfCOSE is built lean for ES256 sign+verify only, matching a minimal host +# wolfSSL; the verify path itself is what wolfBoot uses on-target. +"$CC" -DWOLFBOOT_SUIT -DSUIT_INSTALL_DIRECTIVES \ + -DWOLFCOSE_NO_ENCRYPT0 -DWOLFCOSE_NO_ENCRYPT -DWOLFCOSE_NO_MAC0 \ + -DWOLFCOSE_NO_MAC -DWOLFCOSE_NO_SIGN -DWOLFCOSE_NO_RECIPIENTS \ + -DWOLFCOSE_NO_MLDSA -DWOLFCOSE_NO_KEY_ENCODE -DWOLFCOSE_NO_KEY_DECODE \ + -DWOLFCOSE_NO_EDDSA -DWOLFCOSE_NO_ED448 -DWOLFCOSE_NO_RSAPSS \ + -std=c99 -Wall -Wextra -include wolfssl/options.h \ + -I include -I lib/wolfCOSE/include -I lib/wolfCOSE/src \ + -isystem "$WOLFSSL_DIR/include" \ + tests/suit_test.c \ + src/suit/suit_parse.c src/suit/suit_verify.c src/suit/suit_process.c \ + lib/wolfCOSE/src/wolfcose.c lib/wolfCOSE/src/wolfcose_cbor.c \ + -L"$WOLFSSL_DIR/lib" -lwolfssl -o "$OUT" + +LD_LIBRARY_PATH="$WOLFSSL_DIR/lib" "$OUT" diff --git a/tests/suit_test.c b/tests/suit_test.c new file mode 100644 index 0000000000..aef9cdb362 --- /dev/null +++ b/tests/suit_test.c @@ -0,0 +1,373 @@ +/* suit_test.c + * + * Copyright (C) 2025 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ +/** + * @file suit_test.c + * @brief Host unit test for the SUIT verify + process + install paths. Authors a + * full signed SUIT envelope with wolfCOSE and exercises suit_open + + * suit_verify_auth + suit_process (identity validate, payload install via + * directive-write, image-match), with tamper cases. Build with + * -DSUIT_INSTALL_DIRECTIVES. + */ +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "suit.h" + +/* Test trust anchor: keystore_get_buffer() returns the P-256 public key X||Y. */ +static uint8_t g_pub[64]; +uint8_t* keystore_get_buffer(int id) { (void)id; return g_pub; } + +/* A RAM "component" standing in for a flash partition: directive-write stores + * the installed payload here, and condition-image-match hashes it. */ +static uint8_t g_flash[256]; +static size_t g_flashLen; +static int g_corrupt_write; /* simulate a faulty/hostile write for testing */ + +static int comp_hash(void* c, size_t idx, uint8_t* out, size_t outLen) +{ + (void)c; (void)idx; + if (outLen < 32) { return -1; } + return wc_Hash(WC_HASH_TYPE_SHA256, g_flash, (word32)g_flashLen, out, + (word32)outLen); +} + +static int comp_write(void* c, size_t idx, const uint8_t* src, size_t len) +{ + (void)c; (void)idx; + if (len > sizeof(g_flash)) { return -1; } + memcpy(g_flash, src, len); + g_flashLen = len; + if (g_corrupt_write) { g_flash[0] ^= 0xFF; } + return 0; +} + +#define CHECK(cond, msg) do { \ + if (!(cond)) { printf("FAIL: %s\n", (msg)); return -1; } } while (0) + +/* The firmware payload the manifest installs. */ +static const uint8_t FW[] = "wolfUpdate firmware payload v2 installed via SUIT"; +static const uint8_t VENDOR[16] = { + 0xfa,0x6b,0x4a,0x53,0xd5,0xad,0x5f,0xdf,0xbe,0x9d,0xe6,0x63,0xe4,0xd4,0x1f,0xfe }; +static const uint8_t CLASSID[16] = { + 0x14,0x92,0xaf,0x14,0x25,0x69,0x5e,0x48,0xbf,0x42,0x9b,0x2d,0x51,0xf2,0xab,0x45 }; + +static const uint8_t TEST_D[32] = { + 0xC9,0xAF,0xA9,0xD8,0x45,0xBA,0x75,0x16,0x6B,0x5C,0x21,0x57,0x67,0xB1,0xD6,0x93, + 0x4E,0x50,0xC3,0xDB,0x36,0xE8,0x9B,0x12,0x7B,0x8A,0x62,0x2B,0x12,0x0F,0x67,0x21 }; +static const uint8_t TEST_QX[32] = { + 0x60,0xFE,0xD4,0xBA,0x25,0x5A,0x9D,0x31,0xC9,0x61,0xEB,0x74,0xC6,0x35,0x6D,0x68, + 0xC0,0x49,0xB8,0x92,0x3B,0x61,0xFA,0x6C,0xE6,0x69,0x62,0x2E,0x60,0xF2,0x9F,0xB6 }; +static const uint8_t TEST_QY[32] = { + 0x79,0x03,0xFE,0x10,0x08,0xB8,0xBC,0x99,0xA4,0x1A,0xE9,0xE9,0x56,0x28,0xBC,0x64, + 0xF2,0xF1,0xB2,0x0C,0x2D,0x7E,0x9F,0x51,0x77,0xA3,0xC2,0x94,0xD4,0x46,0x22,0x99 }; + +#define E(call) do { if ((call) != WOLFCOSE_SUCCESS) { return -1; } } while (0) + +static void cbor_init(WOLFCOSE_CBOR_CTX* c, uint8_t* out, size_t sz) +{ + c->buf = out; c->cbuf = out; c->bufSz = sz; c->idx = 0; +} + +static int enc_suit_digest(uint8_t* out, size_t outSz, size_t* outLen, + const uint8_t* digest, size_t digestLen) +{ + WOLFCOSE_CBOR_CTX c; + cbor_init(&c, out, outSz); + E(wc_CBOR_EncodeArrayStart(&c, 2)); + E(wc_CBOR_EncodeInt(&c, SUIT_COSE_ALG_SHA_256)); + E(wc_CBOR_EncodeBstr(&c, digest, digestLen)); + *outLen = c.idx; + return 0; +} + +/* shared-sequence: select component 0, set device identity parameters. */ +static int enc_shared_seq(uint8_t* out, size_t outSz, size_t* outLen) +{ + WOLFCOSE_CBOR_CTX c; + cbor_init(&c, out, outSz); + E(wc_CBOR_EncodeArrayStart(&c, 4)); + E(wc_CBOR_EncodeInt(&c, SUIT_DIR_SET_COMPONENT_INDEX)); + E(wc_CBOR_EncodeUint(&c, 0)); + E(wc_CBOR_EncodeInt(&c, SUIT_DIR_OVERRIDE_PARAMETERS)); + E(wc_CBOR_EncodeMapStart(&c, 2)); + E(wc_CBOR_EncodeInt(&c, SUIT_PARAM_VENDOR_IDENTIFIER)); + E(wc_CBOR_EncodeBstr(&c, VENDOR, sizeof(VENDOR))); + E(wc_CBOR_EncodeInt(&c, SUIT_PARAM_CLASS_IDENTIFIER)); + E(wc_CBOR_EncodeBstr(&c, CLASSID, sizeof(CLASSID))); + *outLen = c.idx; + return 0; +} + +/* validate: assert this update is for this device. */ +static int enc_validate_seq(uint8_t* out, size_t outSz, size_t* outLen) +{ + WOLFCOSE_CBOR_CTX c; + cbor_init(&c, out, outSz); + E(wc_CBOR_EncodeArrayStart(&c, 4)); + E(wc_CBOR_EncodeInt(&c, SUIT_COND_VENDOR_IDENTIFIER)); + E(wc_CBOR_EncodeUint(&c, 15)); + E(wc_CBOR_EncodeInt(&c, SUIT_COND_CLASS_IDENTIFIER)); + E(wc_CBOR_EncodeUint(&c, 15)); + *outLen = c.idx; + return 0; +} + +/* install: set the image digest + content, write it, then image-match. */ +static int enc_install_seq(uint8_t* out, size_t outSz, size_t* outLen, + const uint8_t* sd, size_t sdLen, const uint8_t* content, size_t contentLen) +{ + WOLFCOSE_CBOR_CTX c; + cbor_init(&c, out, outSz); + E(wc_CBOR_EncodeArrayStart(&c, 6)); + E(wc_CBOR_EncodeInt(&c, SUIT_DIR_OVERRIDE_PARAMETERS)); + E(wc_CBOR_EncodeMapStart(&c, 2)); + E(wc_CBOR_EncodeInt(&c, SUIT_PARAM_IMAGE_DIGEST)); + E(wc_CBOR_EncodeBstr(&c, sd, sdLen)); + E(wc_CBOR_EncodeInt(&c, SUIT_PARAM_CONTENT)); + E(wc_CBOR_EncodeBstr(&c, content, contentLen)); + E(wc_CBOR_EncodeInt(&c, SUIT_DIR_WRITE)); + E(wc_CBOR_EncodeUint(&c, 15)); + E(wc_CBOR_EncodeInt(&c, SUIT_COND_IMAGE_MATCH)); + E(wc_CBOR_EncodeUint(&c, 15)); + *outLen = c.idx; + return 0; +} + +static int enc_common(uint8_t* out, size_t outSz, size_t* outLen, + const uint8_t* shared, size_t sharedLen) +{ + static const uint8_t COMP_ID = 0x00; + WOLFCOSE_CBOR_CTX c; + cbor_init(&c, out, outSz); + E(wc_CBOR_EncodeMapStart(&c, 2)); + E(wc_CBOR_EncodeInt(&c, SUIT_COMMON_COMPONENTS)); + E(wc_CBOR_EncodeArrayStart(&c, 1)); + E(wc_CBOR_EncodeArrayStart(&c, 1)); + E(wc_CBOR_EncodeBstr(&c, &COMP_ID, 1)); + E(wc_CBOR_EncodeInt(&c, SUIT_COMMON_SHARED_SEQUENCE)); + E(wc_CBOR_EncodeBstr(&c, shared, sharedLen)); + *outLen = c.idx; + return 0; +} + +static int enc_manifest(uint8_t* out, size_t outSz, size_t* outLen, + const uint8_t* common, size_t commonLen, + const uint8_t* validate, size_t validateLen, + const uint8_t* install, size_t installLen) +{ + WOLFCOSE_CBOR_CTX c; + cbor_init(&c, out, outSz); + E(wc_CBOR_EncodeMapStart(&c, 5)); + E(wc_CBOR_EncodeInt(&c, SUIT_MAN_VERSION)); + E(wc_CBOR_EncodeUint(&c, 1)); + E(wc_CBOR_EncodeInt(&c, SUIT_MAN_SEQUENCE_NUMBER)); + E(wc_CBOR_EncodeUint(&c, 1)); + E(wc_CBOR_EncodeInt(&c, SUIT_MAN_COMMON)); + E(wc_CBOR_EncodeBstr(&c, common, commonLen)); + E(wc_CBOR_EncodeInt(&c, SUIT_MAN_VALIDATE)); + E(wc_CBOR_EncodeBstr(&c, validate, validateLen)); + E(wc_CBOR_EncodeInt(&c, SUIT_MAN_INSTALL)); + E(wc_CBOR_EncodeBstr(&c, install, installLen)); + *outLen = c.idx; + return 0; +} + +static int enc_auth(uint8_t* out, size_t outSz, size_t* outLen, + const uint8_t* sd, size_t sdLen, const uint8_t* cose, size_t coseLen) +{ + WOLFCOSE_CBOR_CTX c; + cbor_init(&c, out, outSz); + E(wc_CBOR_EncodeArrayStart(&c, 2)); + E(wc_CBOR_EncodeBstr(&c, sd, sdLen)); + E(wc_CBOR_EncodeBstr(&c, cose, coseLen)); + *outLen = c.idx; + return 0; +} + +static int enc_envelope(uint8_t* out, size_t outSz, size_t* outLen, + const uint8_t* aw, size_t awLen, const uint8_t* man, size_t manLen) +{ + WOLFCOSE_CBOR_CTX c; + cbor_init(&c, out, outSz); + E(wc_CBOR_EncodeMapStart(&c, 2)); + E(wc_CBOR_EncodeInt(&c, SUIT_ENV_AUTHENTICATION_WRAPPER)); + E(wc_CBOR_EncodeBstr(&c, aw, awLen)); + E(wc_CBOR_EncodeInt(&c, SUIT_ENV_MANIFEST)); + E(wc_CBOR_EncodeBstr(&c, man, manLen)); + *outLen = c.idx; + return 0; +} + +/* Author a full signed SUIT envelope (identity + install of FW) into env. */ +static int author(uint8_t* env, size_t envSz, size_t* envLen, size_t* sigOff) +{ + WC_RNG rng; + ecc_key eccKey; + WOLFCOSE_KEY signKey; + uint8_t fwDigest[32], manDigest[32]; + uint8_t sdFw[64], sdMan[64]; + size_t sdFwLen = 0, sdManLen = 0; + uint8_t shared[64], validate[32], install[160], common[128], manifest[512]; + size_t sharedLen = 0, validateLen = 0, installLen = 0, commonLen = 0; + size_t manifestLen = 0; + uint8_t cose[256], aw[384]; + size_t coseLen = 0, awLen = 0; + uint8_t scratch[1024]; + size_t i; + + CHECK(wc_Hash(WC_HASH_TYPE_SHA256, FW, (word32)sizeof(FW), fwDigest, + sizeof(fwDigest)) == 0, "hash fw"); + CHECK(enc_suit_digest(sdFw, sizeof(sdFw), &sdFwLen, fwDigest, + sizeof(fwDigest)) == 0, "sd fw"); + CHECK(enc_shared_seq(shared, sizeof(shared), &sharedLen) == 0, "shared"); + CHECK(enc_validate_seq(validate, sizeof(validate), &validateLen) == 0, + "validate"); + CHECK(enc_install_seq(install, sizeof(install), &installLen, sdFw, sdFwLen, + FW, sizeof(FW)) == 0, "install"); + CHECK(enc_common(common, sizeof(common), &commonLen, shared, sharedLen) == 0, + "common"); + CHECK(enc_manifest(manifest, sizeof(manifest), &manifestLen, common, + commonLen, validate, validateLen, install, installLen) == 0, "manifest"); + + CHECK(wc_Hash(WC_HASH_TYPE_SHA256, manifest, (word32)manifestLen, manDigest, + sizeof(manDigest)) == 0, "hash manifest"); + CHECK(enc_suit_digest(sdMan, sizeof(sdMan), &sdManLen, manDigest, + sizeof(manDigest)) == 0, "sd manifest"); + + CHECK(wc_InitRng(&rng) == 0, "InitRng"); + CHECK(wc_ecc_init(&eccKey) == 0, "ecc_init"); + CHECK(wc_ecc_import_unsigned(&eccKey, TEST_QX, TEST_QY, TEST_D, + ECC_SECP256R1) == 0, "import key"); + memcpy(g_pub, TEST_QX, 32); + memcpy(g_pub + 32, TEST_QY, 32); + CHECK(wc_CoseKey_Init(&signKey) == 0, "key init"); + CHECK(wc_CoseKey_SetEcc(&signKey, WOLFCOSE_CRV_P256, &eccKey) == 0, + "set ecc"); + signKey.hasPrivate = 1; + CHECK(wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_ES256, NULL, 0, NULL, 0, + sdMan, sdManLen, NULL, 0, scratch, sizeof(scratch), cose, sizeof(cose), + &coseLen, &rng) == WOLFCOSE_SUCCESS, "sign"); + + CHECK(enc_auth(aw, sizeof(aw), &awLen, sdMan, sdManLen, cose, coseLen) == 0, + "auth"); + CHECK(enc_envelope(env, envSz, envLen, aw, awLen, manifest, manifestLen) == 0, + "envelope"); + + *sigOff = 0; + for (i = 0; i + coseLen <= *envLen; i++) { + if (memcmp(env + i, cose, coseLen) == 0) { + *sigOff = i + coseLen - 1; + } + } + + wc_CoseKey_Free(&signKey); + wc_ecc_free(&eccKey); + wc_FreeRng(&rng); + return 0; +} + +static void ctx_init(struct suit_context* c, struct suit_manifest* m, + const struct suit_component_ops* ops) +{ + memset(c, 0, sizeof(*c)); + c->m = m; + c->ops = ops; + c->deviceVendorId = VENDOR; + c->deviceVendorIdLen = sizeof(VENDOR); + c->deviceClassId = CLASSID; + c->deviceClassIdLen = sizeof(CLASSID); +} + +int main(void) +{ + uint8_t env[1024]; + size_t envLen = 0; + size_t sigOff = 0; + struct suit_manifest m; + struct suit_component_ops ops; + struct suit_context c; + FILE* f; + int ret; + + memset(&ops, 0, sizeof(ops)); + ops.hash = comp_hash; + ops.write = comp_write; + + if (author(env, sizeof(env), &envLen, &sigOff) != 0) { return 1; } + printf("authored full SUIT envelope: %zu bytes\n", envLen); + + /* Dump the envelope for the independent cross-check (cbor2 + pycose). */ + f = fopen("/tmp/suit_envelope.cbor", "wb"); + if (f != NULL) { + (void)fwrite(env, 1, envLen, f); + (void)fclose(f); + } + + CHECK(suit_open(&m, env, envLen) == SUIT_SUCCESS, "suit_open"); + CHECK(m.common != NULL && m.sharedSeq != NULL && m.validate != NULL && + m.install != NULL, "manifest sub-sequences located"); + CHECK(suit_verify_auth(&m) == SUIT_SUCCESS, "verify_auth"); + printf("PASS: parsed + authenticated\n"); + + /* Full process: identity validate, install (write FW), image-match. */ + g_flashLen = 0; + g_corrupt_write = 0; + ctx_init(&c, &m, &ops); + CHECK(suit_process(&c, &m) == SUIT_SUCCESS, "process should pass"); + CHECK(g_flashLen == sizeof(FW) && memcmp(g_flash, FW, sizeof(FW)) == 0, + "installed payload must equal FW"); + printf("PASS: validated + installed payload + image-match (%zu bytes)\n", + g_flashLen); + + /* Faulty/hostile write -> the post-install image-match must catch it. */ + g_flashLen = 0; + g_corrupt_write = 1; + ctx_init(&c, &m, &ops); + ret = suit_process(&c, &m); + CHECK(ret == SUIT_E_DIGEST_MISMATCH, "corrupt write must fail image-match"); + printf("PASS: corrupt install rejected (image-match)\n"); + g_corrupt_write = 0; + + /* Wrong device identity -> condition-vendor-identifier must fail. */ + g_flashLen = 0; + ctx_init(&c, &m, &ops); + c.deviceVendorId = CLASSID; + ret = suit_process(&c, &m); + CHECK(ret == SUIT_E_CONDITION, "wrong vendor must fail condition"); + printf("PASS: wrong vendor rejected (condition)\n"); + + /* Tampered signature -> authentication must fail. */ + env[sigOff] ^= 0xFF; + CHECK(suit_open(&m, env, envLen) == SUIT_SUCCESS, "reopen tampered"); + ret = suit_verify_auth(&m); + CHECK(ret == SUIT_E_AUTH, "tampered signature must fail auth"); + printf("PASS: tampered signature rejected (auth)\n"); + + printf("ALL SUIT INSTALL TESTS PASSED\n"); + return 0; +} diff --git a/tests/vectors/suit_envelope.cbor b/tests/vectors/suit_envelope.cbor new file mode 100644 index 0000000000000000000000000000000000000000..77ea49d40057ae056d41a4f725b0a546b8856318 GIT binary patch literal 289 zcmV++0p9+i0$6i`SR{fkSRm(S#gfA}UluJrXk-J$GgvfeQBhs_4sAdtS2gC0oOoDD z(u6~y0VbgKSU|)&<=(1?sZ|g!-5AfSMv(-tlD|vC%*$Uv&nwB4N0L2`1b51K5V>}r zR^S?8fdVy!{=)5pDr3M&|BrW5yu1Tgr=16U-2FIXV% zGrL%Ja;Fu;OY$SRUX4Qq6t?$}!mxI$d8J-?i|^eMSTc8SY-Uw(WMOn=AZBTDZFgaE nWgu{2d2DZCWFU4jAZc!MbYX04Wn>_BX<;B!RY_C;5)T6pvnY1I literal 0 HcmV?d00001 From 13e7021a2ad6e532a729e5bc114cd8bc79ddf491 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 10:59:55 -0700 Subject: [PATCH 2/6] Add SUIT CI: WOLFBOOT_SUIT sim build plus host test and cross-check --- .github/workflows/suit.yml | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/suit.yml diff --git a/.github/workflows/suit.yml b/.github/workflows/suit.yml new file mode 100644 index 0000000000..da873f25f6 --- /dev/null +++ b/.github/workflows/suit.yml @@ -0,0 +1,47 @@ +# SUIT manifest support: build the WOLFBOOT_SUIT=1 sim and run the host tests. +# A = author + verify + install + tamper (host), B/C = independent cross-check of +# the frozen vector (cbor2 + cryptography). +name: SUIT + +on: + push: + pull_request: + +jobs: + suit: + name: SUIT manifest verify + process + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential autoconf automake libtool \ + python3 python3-pip + + - name: Checkout submodules (wolfCOSE + wolfSSL) + run: git submodule update --init --depth 1 lib/wolfCOSE lib/wolfssl + + - name: Build sim with WOLFBOOT_SUIT=1 (lean verify-only wolfCOSE) + run: | + cp config/examples/sim.config .config + make WOLFBOOT_SUIT=1 SIGN=ECC256 + + - name: Build host wolfSSL (ECC) for the host test + run: | + git clone --depth 1 https://github.com/wolfSSL/wolfssl.git + cd wolfssl + ./autogen.sh + ./configure --enable-cryptonly --enable-ecc \ + --prefix=$HOME/wolfssl-install + make -j"$(nproc)" + make install + + - name: A - host SUIT test (author + verify + install + tamper) + run: WOLFSSL_DIR=$HOME/wolfssl-install ./tests/suit_host_test.sh + + - name: B/C - independent cross-check of the frozen vector + run: | + pip3 install --quiet cbor2 cryptography + python3 tests/suit_cross_check.py From 39248ce3cc340c020cee428955acedcc8c26eaf8 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 11:29:06 -0700 Subject: [PATCH 3/6] Add SUIT payload encryption: COSE_Encrypt0 decrypt on install (SUIT_HAVE_ENCRYPTION) --- docs/SUIT.md | 13 +++++--- include/suit.h | 7 +++++ include/user_settings.h | 10 +++++++ options.mk | 14 ++++++++- src/suit/suit_process.c | 66 +++++++++++++++++++++++++++++++++++++++-- tests/suit_host_test.sh | 4 +-- tests/suit_test.c | 66 +++++++++++++++++++++++++++++++++++++---- 7 files changed, 165 insertions(+), 15 deletions(-) diff --git a/docs/SUIT.md b/docs/SUIT.md index c7e8081266..d56b4e5c6c 100644 --- a/docs/SUIT.md +++ b/docs/SUIT.md @@ -34,6 +34,7 @@ verify objects (lean, `WOLFCOSE_LEAN_VERIFY`). Fine-tuning macros: | --- | --- | | `SUIT_INSTALL_HANDOFF` (default) | verify then drive the existing A/B swap | | `SUIT_INSTALL_DIRECTIVES` | SUIT copy/write directives drive flash directly | +| `SUIT_HAVE_ENCRYPTION` | decrypt COSE_Encrypt0 payloads on install (AES-GCM); enables AES-GCM in the build | | `SUIT_HAVE_FETCH` / `SUIT_HAVE_TRY_EACH` / `SUIT_HAVE_RUN_SEQUENCE` | optional commands | ## Architecture @@ -84,10 +85,14 @@ CBOR/COSE tooling (test B); it is not a full draft-34 implementation. Implemented + host-tested + interop cross-checked: parse, COSE_Sign1 verify + digest binding, the command interpreter (identity + image-match conditions, set-component-index / override-parameters / write / copy directives), default -deny, and the `wolfBoot_suit_verify()` entry point. - -Follow-ups: A/B-swap handoff wiring from `wolfBoot_verify_authenticity`, and -payload encryption (COSE_Encrypt0) for confidentiality. +deny, **payload decryption on install (COSE_Encrypt0 / AES-GCM) for +confidentiality** (`SUIT_HAVE_ENCRYPTION`), and the `wolfBoot_suit_verify()` +entry point. + +Follow-up: auto-dispatch from `wolfBoot_verify_authenticity` (detect a SUIT +envelope in a partition and route to the SUIT path), which depends on the +update partition layout (where the envelope and image sit) — a wolfUpdate +integration decision. This PR is gated on the wolfCOSE fixes in wolfSSL/wolfCOSE PR #53; the submodule is pinned to that work and should be repinned to the wolfCOSE v1.0 tag before diff --git a/include/suit.h b/include/suit.h index 63573fc8ff..1b6d687db2 100644 --- a/include/suit.h +++ b/include/suit.h @@ -194,6 +194,13 @@ struct suit_context { size_t deviceVendorIdLen; const uint8_t* deviceClassId; size_t deviceClassIdLen; + /* Content-decryption key (SUIT_HAVE_ENCRYPTION). When set, directive-write + * treats the content parameter as a COSE_Encrypt0 message and decrypts it + * into decBuf before handing the plaintext to ops->write. */ + const uint8_t* cek; + size_t cekLen; + uint8_t* decBuf; + size_t decBufLen; }; int suit_open(struct suit_manifest* m, const uint8_t* env, size_t len); diff --git a/include/user_settings.h b/include/user_settings.h index c5daa79b3e..8ba94844f9 100644 --- a/include/user_settings.h +++ b/include/user_settings.h @@ -553,9 +553,19 @@ extern int tolower(int c); #endif +/* SUIT firmware-encryption: COSE_Encrypt0 payload decrypt needs AES-GCM. */ +#if defined(SUIT_HAVE_ENCRYPTION) +# define WOLFSSL_AES_GCM +# define HAVE_AESGCM +# define GCM_TABLE_4BIT +# define WOLFSSL_AES_128 +# define WOLFSSL_AES_256 +#endif + #if !defined(ENCRYPT_WITH_AES128) && !defined(ENCRYPT_WITH_AES256) && \ !defined(WOLFBOOT_TPM_PARMENC) && !defined(WOLFCRYPT_SECURE_MODE) && \ !defined(SECURE_PKCS11) && !defined(WOLFCRYPT_TZ_PSA) && \ + !defined(SUIT_HAVE_ENCRYPTION) && \ !defined(WOLFCRYPT_TEST) && !defined(WOLFCRYPT_BENCHMARK) #define NO_AES #endif diff --git a/options.mk b/options.mk index 3770eaff28..2097302858 100644 --- a/options.mk +++ b/options.mk @@ -1579,9 +1579,21 @@ endif # Uses wolfCOSE (lib/wolfCOSE) for CBOR decode + COSE_Sign1 verify. See docs/SUIT.md. ifeq ($(WOLFBOOT_SUIT),1) CFLAGS+=-DWOLFBOOT_SUIT - CFLAGS+=-I$(WOLFBOOT_ROOT)/lib/wolfCOSE/include -DWOLFCOSE_LEAN_VERIFY + CFLAGS+=-I$(WOLFBOOT_ROOT)/lib/wolfCOSE/include OBJS+=./src/suit/suit_parse.o ./src/suit/suit_verify.o ./src/suit/suit_process.o ./src/suit/suit_wolfboot.o OBJS+=./lib/wolfCOSE/src/wolfcose.o ./lib/wolfCOSE/src/wolfcose_cbor.o + ifeq ($(SUIT_HAVE_ENCRYPTION),1) + # COSE_Sign1 verify + COSE_Encrypt0 decrypt (payload confidentiality). The + # plain LEAN_VERIFY profile drops encrypt, so select the pieces explicitly. + CFLAGS+=-DSUIT_HAVE_ENCRYPTION + CFLAGS+=-DWOLFCOSE_NO_SIGN1_SIGN -DWOLFCOSE_NO_MAC0 -DWOLFCOSE_NO_MAC \ + -DWOLFCOSE_NO_SIGN -DWOLFCOSE_NO_ENCRYPT -DWOLFCOSE_NO_RECIPIENTS \ + -DWOLFCOSE_NO_KEY_ENCODE -DWOLFCOSE_NO_KEY_DECODE \ + -DWOLFCOSE_NO_ENCRYPT0_ENCRYPT + WOLFCRYPT_OBJS+=$(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/aes.o + else + CFLAGS+=-DWOLFCOSE_LEAN_VERIFY + endif ifeq ($(SUIT_INSTALL_DIRECTIVES),1) CFLAGS+=-DSUIT_INSTALL_DIRECTIVES else diff --git a/src/suit/suit_process.c b/src/suit/suit_process.c index 69c6b29603..c695843ba4 100644 --- a/src/suit/suit_process.c +++ b/src/suit/suit_process.c @@ -192,6 +192,66 @@ static int suit_id_match(const uint8_t* paramId, size_t paramLen, return ret; } +/* Write install content for the current component. With a content-decryption + * key configured (SUIT_HAVE_ENCRYPTION) the content is a COSE_Encrypt0 message, + * decrypted into decBuf before ops->write; otherwise it is written as-is. */ +static int suit_install_write(struct suit_context* ctx, + const uint8_t* content, size_t contentLen) +{ + int ret = SUIT_SUCCESS; + int handled = 0; +#ifdef SUIT_HAVE_ENCRYPTION + WOLFCOSE_KEY ek; + WOLFCOSE_HDR hdr; + uint8_t encScratch[256]; + size_t ptLen = 0; + int ekInit = 0; + + if (ctx->cek != NULL) { + handled = 1; + if (ctx->decBuf == NULL) { + ret = SUIT_E_INSTALL; + } + if (ret == SUIT_SUCCESS) { + if (wc_CoseKey_Init(&ek) != 0) { + ret = SUIT_E_CRYPTO; + } + else { + ekInit = 1; + } + } + if (ret == SUIT_SUCCESS) { + if (wc_CoseKey_SetSymmetric(&ek, ctx->cek, ctx->cekLen) != 0) { + ret = SUIT_E_CRYPTO; + } + } + if (ret == SUIT_SUCCESS) { + if (wc_CoseEncrypt0_Decrypt(&ek, content, contentLen, NULL, 0, + NULL, 0, encScratch, sizeof(encScratch), &hdr, + ctx->decBuf, ctx->decBufLen, &ptLen) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_CRYPTO; + } + } + if (ret == SUIT_SUCCESS) { + if (ctx->ops->write(ctx->ops->ctx, ctx->componentIndex, + ctx->decBuf, ptLen) != 0) { + ret = SUIT_E_INSTALL; + } + } + if (ekInit != 0) { + wc_CoseKey_Free(&ek); + } + } +#endif + if ((handled == 0) && (ret == SUIT_SUCCESS)) { + if (ctx->ops->write(ctx->ops->ctx, ctx->componentIndex, content, + contentLen) != 0) { + ret = SUIT_E_INSTALL; + } + } + return ret; +} + /* Walk one SUIT_Command_Sequence: a flat array of (command, argument) pairs. */ static int suit_run_sequence(struct suit_context* ctx, const uint8_t* seq, size_t seqLen) @@ -237,9 +297,9 @@ static int suit_run_sequence(struct suit_context* ctx, const uint8_t* seq, else if (ctx->params.content == NULL) { ret = SUIT_E_INSTALL; } - else if (ctx->ops->write(ctx->ops->ctx, ctx->componentIndex, - ctx->params.content, ctx->params.contentLen) != 0) { - ret = SUIT_E_INSTALL; + else { + ret = suit_install_write(ctx, ctx->params.content, + ctx->params.contentLen); } } else if (cmd == (int64_t)SUIT_DIR_COPY) { diff --git a/tests/suit_host_test.sh b/tests/suit_host_test.sh index 53fe5bb91e..8f28122f4e 100755 --- a/tests/suit_host_test.sh +++ b/tests/suit_host_test.sh @@ -17,8 +17,8 @@ cd "$ROOT" # wolfCOSE is built lean for ES256 sign+verify only, matching a minimal host # wolfSSL; the verify path itself is what wolfBoot uses on-target. -"$CC" -DWOLFBOOT_SUIT -DSUIT_INSTALL_DIRECTIVES \ - -DWOLFCOSE_NO_ENCRYPT0 -DWOLFCOSE_NO_ENCRYPT -DWOLFCOSE_NO_MAC0 \ +"$CC" -DWOLFBOOT_SUIT -DSUIT_INSTALL_DIRECTIVES -DSUIT_HAVE_ENCRYPTION \ + -DWOLFCOSE_NO_ENCRYPT -DWOLFCOSE_NO_MAC0 \ -DWOLFCOSE_NO_MAC -DWOLFCOSE_NO_SIGN -DWOLFCOSE_NO_RECIPIENTS \ -DWOLFCOSE_NO_MLDSA -DWOLFCOSE_NO_KEY_ENCODE -DWOLFCOSE_NO_KEY_DECODE \ -DWOLFCOSE_NO_EDDSA -DWOLFCOSE_NO_ED448 -DWOLFCOSE_NO_RSAPSS \ diff --git a/tests/suit_test.c b/tests/suit_test.c index aef9cdb362..9b9edefdd0 100644 --- a/tests/suit_test.c +++ b/tests/suit_test.c @@ -76,6 +76,12 @@ static const uint8_t VENDOR[16] = { static const uint8_t CLASSID[16] = { 0x14,0x92,0xaf,0x14,0x25,0x69,0x5e,0x48,0xbf,0x42,0x9b,0x2d,0x51,0xf2,0xab,0x45 }; +/* Content-encryption key + IV for the encrypted-install case (A128GCM). */ +static const uint8_t CEK[16] = { + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f }; +static const uint8_t ENC_IV[12] = { + 0xa0,0xa1,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xab }; + static const uint8_t TEST_D[32] = { 0xC9,0xAF,0xA9,0xD8,0x45,0xBA,0x75,0x16,0x6B,0x5C,0x21,0x57,0x67,0xB1,0xD6,0x93, 0x4E,0x50,0xC3,0xDB,0x36,0xE8,0x9B,0x12,0x7B,0x8A,0x62,0x2B,0x12,0x0F,0x67,0x21 }; @@ -223,8 +229,11 @@ static int enc_envelope(uint8_t* out, size_t outSz, size_t* outLen, return 0; } -/* Author a full signed SUIT envelope (identity + install of FW) into env. */ -static int author(uint8_t* env, size_t envSz, size_t* envLen, size_t* sigOff) +/* Author a full signed SUIT envelope (identity + install of FW) into env. When + * encrypt is set, the install content is a COSE_Encrypt0 of FW (decrypted on + * install); the image-digest is always over the FW plaintext. */ +static int author(uint8_t* env, size_t envSz, size_t* envLen, size_t* sigOff, + int encrypt) { WC_RNG rng; ecc_key eccKey; @@ -232,13 +241,20 @@ static int author(uint8_t* env, size_t envSz, size_t* envLen, size_t* sigOff) uint8_t fwDigest[32], manDigest[32]; uint8_t sdFw[64], sdMan[64]; size_t sdFwLen = 0, sdManLen = 0; - uint8_t shared[64], validate[32], install[160], common[128], manifest[512]; + uint8_t shared[64], validate[32], install[320], common[128], manifest[640]; size_t sharedLen = 0, validateLen = 0, installLen = 0, commonLen = 0; size_t manifestLen = 0; uint8_t cose[256], aw[384]; size_t coseLen = 0, awLen = 0; uint8_t scratch[1024]; + const uint8_t* contentPtr = FW; + size_t contentLen = sizeof(FW); size_t i; +#ifdef SUIT_HAVE_ENCRYPTION + WOLFCOSE_KEY enckey; + uint8_t encFW[256]; + size_t encFWLen = 0; +#endif CHECK(wc_Hash(WC_HASH_TYPE_SHA256, FW, (word32)sizeof(FW), fwDigest, sizeof(fwDigest)) == 0, "hash fw"); @@ -247,8 +263,26 @@ static int author(uint8_t* env, size_t envSz, size_t* envLen, size_t* sigOff) CHECK(enc_shared_seq(shared, sizeof(shared), &sharedLen) == 0, "shared"); CHECK(enc_validate_seq(validate, sizeof(validate), &validateLen) == 0, "validate"); + +#ifdef SUIT_HAVE_ENCRYPTION + if (encrypt) { + CHECK(wc_CoseKey_Init(&enckey) == 0, "enc key init"); + CHECK(wc_CoseKey_SetSymmetric(&enckey, CEK, sizeof(CEK)) == 0, + "set sym key"); + CHECK(wc_CoseEncrypt0_Encrypt(&enckey, WOLFCOSE_ALG_A128GCM, + ENC_IV, sizeof(ENC_IV), FW, sizeof(FW), NULL, 0, NULL, NULL, 0, + scratch, sizeof(scratch), encFW, sizeof(encFW), &encFWLen) + == WOLFCOSE_SUCCESS, "encrypt0"); + wc_CoseKey_Free(&enckey); + contentPtr = encFW; + contentLen = encFWLen; + } +#else + (void)encrypt; +#endif + CHECK(enc_install_seq(install, sizeof(install), &installLen, sdFw, sdFwLen, - FW, sizeof(FW)) == 0, "install"); + contentPtr, contentLen) == 0, "install"); CHECK(enc_common(common, sizeof(common), &commonLen, shared, sharedLen) == 0, "common"); CHECK(enc_manifest(manifest, sizeof(manifest), &manifestLen, common, @@ -313,12 +347,15 @@ int main(void) struct suit_context c; FILE* f; int ret; +#ifdef SUIT_HAVE_ENCRYPTION + uint8_t decBuf[256]; +#endif memset(&ops, 0, sizeof(ops)); ops.hash = comp_hash; ops.write = comp_write; - if (author(env, sizeof(env), &envLen, &sigOff) != 0) { return 1; } + if (author(env, sizeof(env), &envLen, &sigOff, 0) != 0) { return 1; } printf("authored full SUIT envelope: %zu bytes\n", envLen); /* Dump the envelope for the independent cross-check (cbor2 + pycose). */ @@ -368,6 +405,25 @@ int main(void) CHECK(ret == SUIT_E_AUTH, "tampered signature must fail auth"); printf("PASS: tampered signature rejected (auth)\n"); +#ifdef SUIT_HAVE_ENCRYPTION + /* Encrypted payload: install content is a COSE_Encrypt0, decrypted with the + * device key on write. Confidentiality end to end. */ + if (author(env, sizeof(env), &envLen, &sigOff, 1) != 0) { return 1; } + CHECK(suit_open(&m, env, envLen) == SUIT_SUCCESS, "open (encrypted)"); + CHECK(suit_verify_auth(&m) == SUIT_SUCCESS, "verify_auth (encrypted)"); + g_flashLen = 0; + g_corrupt_write = 0; + ctx_init(&c, &m, &ops); + c.cek = CEK; + c.cekLen = sizeof(CEK); + c.decBuf = decBuf; + c.decBufLen = sizeof(decBuf); + CHECK(suit_process(&c, &m) == SUIT_SUCCESS, "process (encrypted) should pass"); + CHECK(g_flashLen == sizeof(FW) && memcmp(g_flash, FW, sizeof(FW)) == 0, + "decrypted install must equal FW plaintext"); + printf("PASS: encrypted payload decrypted + installed (confidentiality)\n"); +#endif + printf("ALL SUIT INSTALL TESTS PASSED\n"); return 0; } From a003260e63aee373228eaff2c0c2f2fc62cc584a Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 12:28:01 -0700 Subject: [PATCH 4/6] Add SUIT boot-time auto-dispatch from wolfBoot_verify_authenticity (concatenated envelope+image layout) --- docs/SUIT.md | 29 +++++++++++++------ include/image.h | 4 +++ include/suit.h | 1 + src/image.c | 29 +++++++++++++++++++ src/suit/suit_parse.c | 2 ++ src/suit/suit_wolfboot.c | 62 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 118 insertions(+), 9 deletions(-) diff --git a/docs/SUIT.md b/docs/SUIT.md index d56b4e5c6c..a1c2b81107 100644 --- a/docs/SUIT.md +++ b/docs/SUIT.md @@ -44,9 +44,20 @@ is via a pluggable `struct suit_component_ops` (hash/write/copy) the host supplies. In wolfBoot those wrap the flash HAL; the host unit test wraps a RAM buffer. This keeps the SUIT code reusable outside wolfBoot. -- `suit_open()` — parse the envelope + manifest (zero-copy offsets). -- `suit_verify_auth()` — COSE_Sign1 + digest binding. -- `suit_process()` — command-sequence interpreter (conditions/directives). +- `suit_open()`: parse the envelope + manifest (zero-copy offsets). +- `suit_verify_auth()`: COSE_Sign1 + digest binding. +- `suit_process()`: command-sequence interpreter (conditions/directives). + +## Boot-time dispatch + +When `WOLFBOOT_SUIT` is enabled, `wolfBoot_verify_authenticity` (and the open / +integrity path) detect whether a partition holds a wolfBoot TLV image +(`WOLFBOOT_MAGIC`) or a SUIT envelope (CBOR), and route the SUIT case to the SUIT +path automatically; the TLV path is untouched. The layout is concatenated +`[envelope][image]`: the manifest is authenticated, `image-match` hashes the +image that follows the envelope, and on success the image is exposed as +`fw_base`/`fw_size` for the existing A/B swap. The TLV path remains the default +when the macro is off. ## Test @@ -86,13 +97,13 @@ Implemented + host-tested + interop cross-checked: parse, COSE_Sign1 verify + digest binding, the command interpreter (identity + image-match conditions, set-component-index / override-parameters / write / copy directives), default deny, **payload decryption on install (COSE_Encrypt0 / AES-GCM) for -confidentiality** (`SUIT_HAVE_ENCRYPTION`), and the `wolfBoot_suit_verify()` -entry point. +confidentiality** (`SUIT_HAVE_ENCRYPTION`), the `wolfBoot_suit_verify()` entry +point, and **boot-time auto-dispatch** (format detection from +`wolfBoot_verify_authenticity`, concatenated `[envelope][image]` layout, handoff +to the A/B swap). -Follow-up: auto-dispatch from `wolfBoot_verify_authenticity` (detect a SUIT -envelope in a partition and route to the SUIT path), which depends on the -update partition layout (where the envelope and image sit) — a wolfUpdate -integration decision. +Follow-ups: `directive-fetch` (networked payload retrieval, wolfUpdate), and the +optional commands (`try-each` / `run-sequence` / `swap`). This PR is gated on the wolfCOSE fixes in wolfSSL/wolfCOSE PR #53; the submodule is pinned to that work and should be repinned to the wolfCOSE v1.0 tag before diff --git a/include/image.h b/include/image.h index 69b8dc83e1..1b4acb2e3c 100644 --- a/include/image.h +++ b/include/image.h @@ -1356,6 +1356,10 @@ int wolfBoot_open_self_address(struct wolfBoot_image *img, uint8_t *hdr, #endif int wolfBoot_verify_integrity(struct wolfBoot_image *img); int wolfBoot_verify_authenticity(struct wolfBoot_image *img); +#ifdef WOLFBOOT_SUIT +int wolfBoot_image_is_suit(const uint8_t* image); +int wolfBoot_suit_verify_authenticity(struct wolfBoot_image* img); +#endif int wolfBoot_set_partition_state(uint8_t part, uint8_t newst); int wolfBoot_get_update_sector_flag(uint16_t sector, uint8_t *flag); int wolfBoot_set_update_sector_flag(uint16_t sector, uint8_t newflag); diff --git a/include/suit.h b/include/suit.h index 1b6d687db2..374c512a97 100644 --- a/include/suit.h +++ b/include/suit.h @@ -144,6 +144,7 @@ enum suit_cose_digest_alg { struct suit_manifest { const uint8_t* env; /* envelope buffer (caller-owned) */ size_t envLen; + size_t envEncodedLen; /* bytes the envelope CBOR actually occupies */ const uint8_t* manifest; /* bstr-wrapped SUIT_Manifest */ size_t manifestLen; const uint8_t* authWrapper; /* suit-authentication-wrapper contents */ diff --git a/src/image.c b/src/image.c index 3bedbef9ea..a052ab7027 100644 --- a/src/image.c +++ b/src/image.c @@ -1383,6 +1383,17 @@ uint32_t wolfBoot_image_size(uint8_t *image) int wolfBoot_open_image_address(struct wolfBoot_image *img, uint8_t *image) { uint32_t *magic = (uint32_t *)(image); +#ifdef WOLFBOOT_SUIT + if ((*magic != WOLFBOOT_MAGIC) && wolfBoot_image_is_suit(image)) { + /* SUIT envelope: no wolfBoot TLV header. fw_base/fw_size are set when + * the manifest is authenticated (wolfBoot_suit_verify_authenticity). */ + if (!img->hdr_ok) { + img->hdr = image; + } + img->hdr_ok = 1; + return 0; + } +#endif if (*magic != WOLFBOOT_MAGIC) { wolfBoot_printf("Partition %d header magic 0x%08x invalid at %p\n", img->part, (unsigned int)*magic, img->hdr); @@ -1633,6 +1644,14 @@ int wolfBoot_verify_integrity(struct wolfBoot_image *img) { uint8_t *stored_sha; uint16_t stored_sha_len; +#ifdef WOLFBOOT_SUIT + if (wolfBoot_image_is_suit(img->hdr)) { + /* Integrity is established by the manifest image-match during SUIT + * authentication, not by a wolfBoot header SHA. */ + img->sha_ok = 1; + return 0; + } +#endif stored_sha_len = get_header(img, WOLFBOOT_SHA_HDR, &stored_sha); if (stored_sha_len != WOLFBOOT_SHA_DIGEST_SIZE) return -1; @@ -2248,6 +2267,16 @@ int wolfBoot_verify_authenticity(struct wolfBoot_image *img) g_leafKeyIdValid = 0; #endif +#ifdef WOLFBOOT_SUIT + if (wolfBoot_image_is_suit(img->hdr)) { + if (wolfBoot_suit_verify_authenticity(img) == 0) { + wolfBoot_image_confirm_signature_ok(img); + return 0; + } + return -1; + } +#endif + stored_signature_size = get_header(img, HDR_SIGNATURE, &stored_signature); pubkey_hint_size = get_header(img, HDR_PUBKEY, &pubkey_hint); if (pubkey_hint_size == WOLFBOOT_SHA_DIGEST_SIZE) { diff --git a/src/suit/suit_parse.c b/src/suit/suit_parse.c index ea71424337..1f024b15bc 100644 --- a/src/suit/suit_parse.c +++ b/src/suit/suit_parse.c @@ -154,6 +154,7 @@ int suit_open(struct suit_manifest* m, const uint8_t* env, size_t len) m->env = env; m->envLen = len; + m->envEncodedLen = 0; m->manifest = NULL; m->manifestLen = 0; m->authWrapper = NULL; @@ -222,6 +223,7 @@ int suit_open(struct suit_manifest* m, const uint8_t* env, size_t len) } if (ret == SUIT_SUCCESS) { + m->envEncodedLen = ctx.idx; if ((m->manifest == NULL) || (m->authWrapper == NULL)) { ret = SUIT_E_PARSE; } diff --git a/src/suit/suit_wolfboot.c b/src/suit/suit_wolfboot.c index 4f719aac78..00c1fa08c3 100644 --- a/src/suit/suit_wolfboot.c +++ b/src/suit/suit_wolfboot.c @@ -30,6 +30,8 @@ #ifdef WOLFBOOT_SUIT #include +#include +#include "image.h" int wolfBoot_suit_verify(const uint8_t* env, size_t envLen, const struct suit_component_ops* ops, @@ -57,4 +59,64 @@ int wolfBoot_suit_verify(const uint8_t* env, size_t envLen, return ret; } +/* Detect a SUIT envelope at a partition start: CBOR major type 5 (map) or 6 + * (tag). The wolfBoot magic byte never falls in this range. */ +int wolfBoot_image_is_suit(const uint8_t* image) +{ + uint8_t b = image[0]; + return (((b >= 0xA0u) && (b <= 0xDFu)) ? 1 : 0); +} + +/* Component I/O for the concatenated [envelope][image] layout: component 0 is + * the image that follows the envelope. imageSize comes from the manifest, set by + * override-parameters before image-match runs. */ +struct wb_suit_opsctx { + const uint8_t* imgBase; + struct suit_context* sctx; +}; + +static int wb_suit_hash(void* c, size_t idx, uint8_t* out, size_t outLen) +{ + struct wb_suit_opsctx* o = (struct wb_suit_opsctx*)c; + (void)idx; + if (outLen < 32u) { + return -1; + } + return wc_Hash(WC_HASH_TYPE_SHA256, o->imgBase, + (word32)o->sctx->params.imageSize, out, (word32)outLen); +} + +/* Boot-time dispatch: authenticate the manifest and image-match the image that + * follows it, then expose the image as fw_base/fw_size for the A/B swap. */ +int wolfBoot_suit_verify_authenticity(struct wolfBoot_image* img) +{ + int ret; + struct suit_manifest m; + struct suit_context ctx; + struct suit_component_ops ops; + struct wb_suit_opsctx opsctx; + + memset(&ctx, 0, sizeof(ctx)); + memset(&ops, 0, sizeof(ops)); + + ret = suit_open(&m, img->hdr, WOLFBOOT_PARTITION_SIZE); + if (ret == SUIT_SUCCESS) { + ret = suit_verify_auth(&m); + } + if (ret == SUIT_SUCCESS) { + opsctx.imgBase = img->hdr + m.envEncodedLen; + opsctx.sctx = &ctx; + ops.ctx = &opsctx; + ops.hash = wb_suit_hash; + ctx.m = &m; + ctx.ops = &ops; + ret = suit_process(&ctx, &m); + } + if (ret == SUIT_SUCCESS) { + img->fw_base = (uint8_t*)(img->hdr + m.envEncodedLen); + img->fw_size = (uint32_t)ctx.params.imageSize; + } + return (ret == SUIT_SUCCESS) ? 0 : -1; +} + #endif /* WOLFBOOT_SUIT */ From 61b5cc64f2f4e61afbe637738544ca456ee4b5b4 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 14:06:53 -0700 Subject: [PATCH 5/6] Add SUIT anti-rollback, image bounds checks, COSE kid key selection, and device identity --- docs/SUIT.md | 8 ++++ include/suit.h | 5 ++ src/suit/suit_parse.c | 7 +++ src/suit/suit_process.c | 17 +++++++ src/suit/suit_verify.c | 78 ++++++++++++++++++++++++++++++- src/suit/suit_wolfboot.c | 23 +++++++++ tests/suit_test.c | 41 ++++++++++++++-- tests/vectors/suit_envelope.cbor | Bin 289 -> 299 bytes 8 files changed, 174 insertions(+), 5 deletions(-) diff --git a/docs/SUIT.md b/docs/SUIT.md index a1c2b81107..1a5bc81cdf 100644 --- a/docs/SUIT.md +++ b/docs/SUIT.md @@ -35,6 +35,8 @@ verify objects (lean, `WOLFCOSE_LEAN_VERIFY`). Fine-tuning macros: | `SUIT_INSTALL_HANDOFF` (default) | verify then drive the existing A/B swap | | `SUIT_INSTALL_DIRECTIVES` | SUIT copy/write directives drive flash directly | | `SUIT_HAVE_ENCRYPTION` | decrypt COSE_Encrypt0 payloads on install (AES-GCM); enables AES-GCM in the build | +| `SUIT_DEVICE_VENDOR_ID` / `SUIT_DEVICE_CLASS_ID` | this device's identity (brace initializers) for the vendor/class conditions | +| `SUIT_KEY_SLOT` | fallback trust-anchor slot when the COSE_Sign1 carries no key id | | `SUIT_HAVE_FETCH` / `SUIT_HAVE_TRY_EACH` / `SUIT_HAVE_RUN_SEQUENCE` | optional commands | ## Architecture @@ -102,6 +104,12 @@ point, and **boot-time auto-dispatch** (format detection from `wolfBoot_verify_authenticity`, concatenated `[envelope][image]` layout, handoff to the A/B swap). +Security hardening: **anti-rollback** (rejects a manifest whose +`sequence-number` is below the running version), **image bounds** (rejects an +image or content larger than the partition space, and an out-of-range component +index), and **key-id selection** (the COSE_Sign1 `kid` picks the trust anchor +via the keystore, like the TLV path's pubkey hint). + Follow-ups: `directive-fetch` (networked payload retrieval, wolfUpdate), and the optional commands (`try-each` / `run-sequence` / `swap`). diff --git a/include/suit.h b/include/suit.h index 374c512a97..584280707b 100644 --- a/include/suit.h +++ b/include/suit.h @@ -44,6 +44,8 @@ #define SUIT_E_UNSUPPORTED (-7) #define SUIT_E_CRYPTO (-8) #define SUIT_E_INSTALL (-9) +#define SUIT_E_ROLLBACK (-10) +#define SUIT_E_BOUNDS (-11) /* SUIT_Envelope map keys (IANA SUIT Envelope Elements). */ enum suit_envelope_key { @@ -147,6 +149,7 @@ struct suit_manifest { size_t envEncodedLen; /* bytes the envelope CBOR actually occupies */ const uint8_t* manifest; /* bstr-wrapped SUIT_Manifest */ size_t manifestLen; + uint64_t sequenceNumber; /* suit-manifest-sequence-number (anti-rollback) */ const uint8_t* authWrapper; /* suit-authentication-wrapper contents */ size_t authWrapperLen; const uint8_t* common; /* suit-common (bstr-wrapped SUIT_Common) */ @@ -191,6 +194,8 @@ struct suit_context { const struct suit_component_ops* ops; struct suit_params params; size_t componentIndex; + uint64_t minSequence; /* reject seq < this (anti-rollback) */ + size_t maxImageSize; /* reject image/content larger (0 = no cap) */ const uint8_t* deviceVendorId; /* this device's identity */ size_t deviceVendorIdLen; const uint8_t* deviceClassId; diff --git a/src/suit/suit_parse.c b/src/suit/suit_parse.c index 1f024b15bc..59981a5e6c 100644 --- a/src/suit/suit_parse.c +++ b/src/suit/suit_parse.c @@ -97,6 +97,12 @@ static int suit_parse_manifest(struct suit_manifest* m) if (wc_CBOR_DecodeInt(&ctx, &key) != WOLFCOSE_SUCCESS) { ret = SUIT_E_PARSE; } + else if (key == (int64_t)SUIT_MAN_SEQUENCE_NUMBER) { + if (wc_CBOR_DecodeUint(&ctx, &m->sequenceNumber) + != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } else if (key == (int64_t)SUIT_MAN_COMMON) { if (wc_CBOR_DecodeBstr(&ctx, &data, &dataLen) != WOLFCOSE_SUCCESS) { ret = SUIT_E_PARSE; @@ -157,6 +163,7 @@ int suit_open(struct suit_manifest* m, const uint8_t* env, size_t len) m->envEncodedLen = 0; m->manifest = NULL; m->manifestLen = 0; + m->sequenceNumber = 0; m->authWrapper = NULL; m->authWrapperLen = 0; m->common = NULL; diff --git a/src/suit/suit_process.c b/src/suit/suit_process.c index c695843ba4..b3f09639e1 100644 --- a/src/suit/suit_process.c +++ b/src/suit/suit_process.c @@ -127,6 +127,10 @@ static int suit_override(struct suit_context* ctx, WOLFCOSE_CBOR_CTX* c) if (wc_CBOR_DecodeUint(c, &uval) != WOLFCOSE_SUCCESS) { ret = SUIT_E_PARSE; } + else if ((ctx->maxImageSize != 0u) && + (uval > (uint64_t)ctx->maxImageSize)) { + ret = SUIT_E_BOUNDS; + } else { ctx->params.imageSize = uval; } @@ -280,6 +284,9 @@ static int suit_run_sequence(struct suit_context* ctx, const uint8_t* seq, if (wc_CBOR_DecodeUint(&c, &uval) != WOLFCOSE_SUCCESS) { ret = SUIT_E_PARSE; } + else if (uval >= (uint64_t)SUIT_MAX_COMPONENTS) { + ret = SUIT_E_BOUNDS; + } else { ctx->componentIndex = (size_t)uval; } @@ -297,6 +304,10 @@ static int suit_run_sequence(struct suit_context* ctx, const uint8_t* seq, else if (ctx->params.content == NULL) { ret = SUIT_E_INSTALL; } + else if ((ctx->maxImageSize != 0u) && + (ctx->params.contentLen > ctx->maxImageSize)) { + ret = SUIT_E_BOUNDS; + } else { ret = suit_install_write(ctx, ctx->params.content, ctx->params.contentLen); @@ -371,6 +382,12 @@ int suit_process(struct suit_context* ctx, struct suit_manifest* m) } ctx->m = m; + /* Anti-rollback: reject a manifest older than the installed sequence + * number, so a validly-signed but outdated manifest cannot downgrade. */ + if (m->sequenceNumber < ctx->minSequence) { + return SUIT_E_ROLLBACK; + } + /* suit-shared-sequence runs first (sets up parameters and component). */ if ((ret == SUIT_SUCCESS) && (m->sharedSeq != NULL)) { ret = suit_run_sequence(ctx, m->sharedSeq, m->sharedSeqLen); diff --git a/src/suit/suit_verify.c b/src/suit/suit_verify.c index 6eb7ce25c4..768eaf96a6 100644 --- a/src/suit/suit_verify.c +++ b/src/suit/suit_verify.c @@ -37,6 +37,68 @@ * Declared locally so this file does not pull in the generated keystore sizing * header, keeping it buildable in minimal/host contexts. */ extern uint8_t* keystore_get_buffer(int id); +extern int keyslot_id_by_sha(const uint8_t* hint); + +/* Extract the COSE_Sign1 unprotected key id (label 4), zero-copy. */ +static int suit_cose_kid(const uint8_t* cose, size_t coseLen, + const uint8_t** kid, size_t* kidLen) +{ + int ret = SUIT_SUCCESS; + WOLFCOSE_CBOR_CTX ctx; + size_t count = 0; + size_t i; + int64_t hkey = 0; + uint64_t tag = 0; + const uint8_t* data = NULL; + size_t dataLen = 0; + + *kid = NULL; + *kidLen = 0; + ctx.buf = NULL; + ctx.cbuf = cose; + ctx.bufSz = coseLen; + ctx.idx = 0; + + if (wc_CBOR_PeekType(&ctx) == 6u) { /* optional COSE_Sign1 tag */ + if (wc_CBOR_DecodeTag(&ctx, &tag) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + if (ret == SUIT_SUCCESS) { /* [protected, unprot, pl, sig] */ + if (wc_CBOR_DecodeArrayStart(&ctx, &count) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else if (count < 4u) { + ret = SUIT_E_PARSE; + } + } + if (ret == SUIT_SUCCESS) { /* protected (bstr) */ + if (wc_CBOR_DecodeBstr(&ctx, &data, &dataLen) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + if (ret == SUIT_SUCCESS) { /* unprotected (map) */ + if (wc_CBOR_DecodeMapStart(&ctx, &count) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + for (i = 0; (i < count) && (ret == SUIT_SUCCESS); i++) { + if (wc_CBOR_DecodeInt(&ctx, &hkey) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else if (hkey == 4) { /* kid */ + if (wc_CBOR_DecodeBstr(&ctx, kid, kidLen) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + else { + if (wc_CBOR_Skip(&ctx) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + } + } + return ret; +} #define SUIT_P256_COORD_SZ 32 #define SUIT_SHA256_SZ 32 @@ -146,6 +208,9 @@ int suit_verify_auth(struct suit_manifest* m) size_t suitDigestLen = 0; const uint8_t* coseSign1 = NULL; size_t coseSign1Len = 0; + const uint8_t* kid = NULL; + size_t kidLen = 0; + int keySlot = SUIT_KEY_SLOT; uint8_t* pub = NULL; ecc_key eccKey; int eccInit = 0; @@ -163,8 +228,19 @@ int suit_verify_auth(struct suit_manifest* m) ret = suit_split_auth(m->authWrapper, m->authWrapperLen, &suitDigest, &suitDigestLen, &coseSign1, &coseSign1Len); + /* Select the trust anchor by the COSE_Sign1 key id (a pubkey hash, as in + * the TLV path), falling back to the configured slot when absent. */ if (ret == SUIT_SUCCESS) { - pub = keystore_get_buffer(SUIT_KEY_SLOT); + if (suit_cose_kid(coseSign1, coseSign1Len, &kid, &kidLen) + == SUIT_SUCCESS) { + if (kid != NULL) { + keySlot = keyslot_id_by_sha(kid); + if (keySlot < 0) { + keySlot = SUIT_KEY_SLOT; + } + } + } + pub = keystore_get_buffer(keySlot); if (pub == NULL) { ret = SUIT_E_AUTH; } diff --git a/src/suit/suit_wolfboot.c b/src/suit/suit_wolfboot.c index 00c1fa08c3..ec5134010f 100644 --- a/src/suit/suit_wolfboot.c +++ b/src/suit/suit_wolfboot.c @@ -33,6 +33,17 @@ #include #include "image.h" +extern uint32_t wolfBoot_get_image_version(uint8_t part); + +/* This device's identity, checked by condition-vendor/class-identifier. Define + * SUIT_DEVICE_VENDOR_ID / SUIT_DEVICE_CLASS_ID as brace initializers to enable. */ +#ifdef SUIT_DEVICE_VENDOR_ID +static const uint8_t suit_dev_vendor[] = SUIT_DEVICE_VENDOR_ID; +#endif +#ifdef SUIT_DEVICE_CLASS_ID +static const uint8_t suit_dev_class[] = SUIT_DEVICE_CLASS_ID; +#endif + int wolfBoot_suit_verify(const uint8_t* env, size_t envLen, const struct suit_component_ops* ops, const uint8_t* vendorId, size_t vendorIdLen, @@ -110,6 +121,18 @@ int wolfBoot_suit_verify_authenticity(struct wolfBoot_image* img) ops.hash = wb_suit_hash; ctx.m = &m; ctx.ops = &ops; + /* Anti-rollback against the running version; bound the image to the + * space after the envelope; apply this device's identity. */ + ctx.minSequence = wolfBoot_get_image_version(PART_BOOT); + ctx.maxImageSize = WOLFBOOT_PARTITION_SIZE - m.envEncodedLen; +#ifdef SUIT_DEVICE_VENDOR_ID + ctx.deviceVendorId = suit_dev_vendor; + ctx.deviceVendorIdLen = sizeof(suit_dev_vendor); +#endif +#ifdef SUIT_DEVICE_CLASS_ID + ctx.deviceClassId = suit_dev_class; + ctx.deviceClassIdLen = sizeof(suit_dev_class); +#endif ret = suit_process(&ctx, &m); } if (ret == SUIT_SUCCESS) { diff --git a/tests/suit_test.c b/tests/suit_test.c index 9b9edefdd0..5586457609 100644 --- a/tests/suit_test.c +++ b/tests/suit_test.c @@ -42,6 +42,20 @@ static uint8_t g_pub[64]; uint8_t* keystore_get_buffer(int id) { (void)id; return g_pub; } +/* COSE key id and a keyslot_id_by_sha() stub that records the kid the verifier + * extracted (proving kid-based key selection) and maps it to the test slot. */ +static const uint8_t KID[8] = { 0xde,0xad,0xbe,0xef,0x01,0x02,0x03,0x04 }; +static uint8_t g_seen_kid[16]; +static size_t g_seen_kidLen; +int keyslot_id_by_sha(const uint8_t* hint) +{ + if (hint != NULL) { + memcpy(g_seen_kid, hint, sizeof(KID)); + g_seen_kidLen = sizeof(KID); + } + return 0; +} + /* A RAM "component" standing in for a flash partition: directive-write stores * the installed payload here, and condition-image-match hashes it. */ static uint8_t g_flash[256]; @@ -303,9 +317,9 @@ static int author(uint8_t* env, size_t envSz, size_t* envLen, size_t* sigOff, CHECK(wc_CoseKey_SetEcc(&signKey, WOLFCOSE_CRV_P256, &eccKey) == 0, "set ecc"); signKey.hasPrivate = 1; - CHECK(wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_ES256, NULL, 0, NULL, 0, - sdMan, sdManLen, NULL, 0, scratch, sizeof(scratch), cose, sizeof(cose), - &coseLen, &rng) == WOLFCOSE_SUCCESS, "sign"); + CHECK(wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_ES256, KID, sizeof(KID), + NULL, 0, sdMan, sdManLen, NULL, 0, scratch, sizeof(scratch), cose, + sizeof(cose), &coseLen, &rng) == WOLFCOSE_SUCCESS, "sign"); CHECK(enc_auth(aw, sizeof(aw), &awLen, sdMan, sdManLen, cose, coseLen) == 0, "auth"); @@ -369,7 +383,10 @@ int main(void) CHECK(m.common != NULL && m.sharedSeq != NULL && m.validate != NULL && m.install != NULL, "manifest sub-sequences located"); CHECK(suit_verify_auth(&m) == SUIT_SUCCESS, "verify_auth"); - printf("PASS: parsed + authenticated\n"); + CHECK(g_seen_kidLen == sizeof(KID) && + memcmp(g_seen_kid, KID, sizeof(KID)) == 0, + "verifier extracted + used the COSE kid"); + printf("PASS: parsed + authenticated (key selected by COSE kid)\n"); /* Full process: identity validate, install (write FW), image-match. */ g_flashLen = 0; @@ -398,6 +415,22 @@ int main(void) CHECK(ret == SUIT_E_CONDITION, "wrong vendor must fail condition"); printf("PASS: wrong vendor rejected (condition)\n"); + /* Anti-rollback: a manifest older than the installed sequence is rejected. */ + g_flashLen = 0; + ctx_init(&c, &m, &ops); + c.minSequence = 5; /* manifest sequence-number is 1 */ + ret = suit_process(&c, &m); + CHECK(ret == SUIT_E_ROLLBACK, "older sequence must be rejected"); + printf("PASS: anti-rollback (older sequence rejected)\n"); + + /* Bounds: content larger than the allowed image size is rejected. */ + g_flashLen = 0; + ctx_init(&c, &m, &ops); + c.maxImageSize = 8; /* FW payload is larger */ + ret = suit_process(&c, &m); + CHECK(ret == SUIT_E_BOUNDS, "oversized content must be rejected"); + printf("PASS: bounds (oversized content rejected)\n"); + /* Tampered signature -> authentication must fail. */ env[sigOff] ^= 0xFF; CHECK(suit_open(&m, env, envLen) == SUIT_SUCCESS, "reopen tampered"); diff --git a/tests/vectors/suit_envelope.cbor b/tests/vectors/suit_envelope.cbor index 77ea49d40057ae056d41a4f725b0a546b8856318..36002c424400d5fbe66cbae4bbea37292df5d523 100644 GIT binary patch delta 101 zcmV-r0Gj`y0;>W9q5@cbkpw7JRMLb)p#dhL1W4YkzV8770|fS1K#Dp;iNIh1WhmnD z3u`EO)_--3i-?Tli5TP@5}v3_gY7XplaTAW{i?x_4JpP%mKOJeJ6Iw8h{WQR^@GJo Hnvr*^T!Aj! delta 91 zcmV-h0Hpt`0-*u~q5@cRkpw79O45Wwp#dhK_E?uG From a5d7e4bb299826a04ae962e150f94b572fc14b75 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Fri, 12 Jun 2026 14:33:41 -0700 Subject: [PATCH 6/6] Add SUIT directive-fetch (host callback) and minimal status report for networked update --- docs/SUIT.md | 25 ++++++-- include/suit.h | 22 +++++++ options.mk | 4 ++ src/suit/suit_process.c | 26 +++++++++ src/suit/suit_report.c | 81 ++++++++++++++++++++++++++ tests/suit_host_test.sh | 2 + tests/suit_test.c | 125 +++++++++++++++++++++++++++++++++++++++- 7 files changed, 277 insertions(+), 8 deletions(-) create mode 100644 src/suit/suit_report.c diff --git a/docs/SUIT.md b/docs/SUIT.md index 1a5bc81cdf..41755941c1 100644 --- a/docs/SUIT.md +++ b/docs/SUIT.md @@ -37,7 +37,9 @@ verify objects (lean, `WOLFCOSE_LEAN_VERIFY`). Fine-tuning macros: | `SUIT_HAVE_ENCRYPTION` | decrypt COSE_Encrypt0 payloads on install (AES-GCM); enables AES-GCM in the build | | `SUIT_DEVICE_VENDOR_ID` / `SUIT_DEVICE_CLASS_ID` | this device's identity (brace initializers) for the vendor/class conditions | | `SUIT_KEY_SLOT` | fallback trust-anchor slot when the COSE_Sign1 carries no key id | -| `SUIT_HAVE_FETCH` / `SUIT_HAVE_TRY_EACH` / `SUIT_HAVE_RUN_SEQUENCE` | optional commands | +| `SUIT_HAVE_FETCH` | enable directive-fetch; the host supplies an `ops->fetch` callback that retrieves the payload by uri (e.g. wolfUpdate transport) | +| `SUIT_HAVE_REPORT` | build `suit_report_encode()`, a compact `{ result, sequence }` status record an update server reads to learn the outcome | +| `SUIT_HAVE_TRY_EACH` / `SUIT_HAVE_RUN_SEQUENCE` | optional commands | ## Architecture @@ -90,8 +92,11 @@ CBOR/COSE tooling (test B); it is not a full draft-34 implementation. - Unrecognized (or known-but-unsupported) commands are **default-denied** (the sequence fails), as a SUIT processor must, rather than silently skipped. -- Not implemented (and rejected if present): directive-fetch, severable members, - try-each / run-sequence / swap, dependencies/trust-domains, SUIT Reports. +- Optional, built only when their macro is set: directive-fetch + (`SUIT_HAVE_FETCH`, via a host callback) and a compact status report + (`SUIT_HAVE_REPORT`, not the full draft-suit-report COSE attestation). +- Not implemented (and rejected if present): severable members, + try-each / run-sequence / swap, dependencies/trust-domains. ## Status @@ -110,8 +115,18 @@ image or content larger than the partition space, and an out-of-range component index), and **key-id selection** (the COSE_Sign1 `kid` picks the trust anchor via the keystore, like the TLV path's pubkey hint). -Follow-ups: `directive-fetch` (networked payload retrieval, wolfUpdate), and the -optional commands (`try-each` / `run-sequence` / `swap`). +Networked update support: `directive-fetch` (`SUIT_HAVE_FETCH`) retrieves the +payload by uri through a host callback, and `suit_report_encode()` +(`SUIT_HAVE_REPORT`) emits a compact status record, so a server (e.g. wolfUpdate) +can pull images and learn outcomes. Remaining optional commands: `try-each` / +`run-sequence` / `swap`. + +Production readiness: this feature is experimental and off by default. Before +enabling it in a shipping product the gate is, at minimum: fuzz the manifest +parser and complete a security review (the manifest is attacker-controlled), +hardware-test the boot/swap path, and provision the content-encryption key by +key-wrap rather than handing it in raw. Encryption (`SUIT_HAVE_ENCRYPTION`) is +not production-ready until the key-wrap step exists. This PR is gated on the wolfCOSE fixes in wolfSSL/wolfCOSE PR #53; the submodule is pinned to that work and should be repinned to the wolfCOSE v1.0 tag before diff --git a/include/suit.h b/include/suit.h index 584280707b..32d73a61bc 100644 --- a/include/suit.h +++ b/include/suit.h @@ -46,6 +46,7 @@ #define SUIT_E_INSTALL (-9) #define SUIT_E_ROLLBACK (-10) #define SUIT_E_BOUNDS (-11) +#define SUIT_E_FETCH (-12) /* SUIT_Envelope map keys (IANA SUIT Envelope Elements). */ enum suit_envelope_key { @@ -173,6 +174,8 @@ struct suit_params { size_t classIdLen; const uint8_t* content; /* directive-write content */ size_t contentLen; + const uint8_t* uri; /* directive-fetch source (tstr bytes) */ + size_t uriLen; uint64_t imageSize; size_t sourceComponent; int componentSlot; @@ -186,6 +189,9 @@ struct suit_component_ops { int (*hash)(void* ctx, size_t idx, uint8_t* out, size_t outLen); int (*write)(void* ctx, size_t idx, const uint8_t* src, size_t len); int (*copy)(void* ctx, size_t idx, size_t srcIdx); + /* Retrieve the payload at uri into component idx (SUIT_HAVE_FETCH). The host + * (e.g. wolfUpdate transport) owns the network/storage; NULL if unsupported. */ + int (*fetch)(void* ctx, size_t idx, const uint8_t* uri, size_t uriLen); }; /* Command-sequence interpreter state. Fixed-size, no heap. */ @@ -213,6 +219,22 @@ int suit_open(struct suit_manifest* m, const uint8_t* env, size_t len); int suit_verify_auth(struct suit_manifest* m); int suit_process(struct suit_context* ctx, struct suit_manifest* m); +#ifdef SUIT_HAVE_REPORT +/* Minimal SUIT status report keys: a compact outcome record for an update server + * (e.g. wolfUpdate), not the full draft-suit-report COSE attestation. */ +enum suit_report_key { + SUIT_REPORT_RESULT = 1, /* int: 0 on success, else the SUIT_E_* code */ + SUIT_REPORT_SEQUENCE_NUMBER = 2 /* uint: the manifest sequence number */ +}; + +/* Encode a status report for a finished suit_process into the caller's buffer as + * CBOR. result is the suit_process return code. Writes the encoded length to + * written. Zero allocation: out is caller-owned. */ +int suit_report_encode(const struct suit_context* ctx, + const struct suit_manifest* m, int result, + uint8_t* out, size_t outLen, size_t* written); +#endif /* SUIT_HAVE_REPORT */ + /* wolfBoot entry point: open + authenticate + process a staged SUIT envelope. * The caller supplies the component I/O ops and this device's identity. */ int wolfBoot_suit_verify(const uint8_t* env, size_t envLen, diff --git a/options.mk b/options.mk index 2097302858..fad9bd705f 100644 --- a/options.mk +++ b/options.mk @@ -1602,6 +1602,10 @@ ifeq ($(WOLFBOOT_SUIT),1) ifeq ($(SUIT_HAVE_FETCH),1) CFLAGS+=-DSUIT_HAVE_FETCH endif + ifeq ($(SUIT_HAVE_REPORT),1) + CFLAGS+=-DSUIT_HAVE_REPORT + OBJS+=./src/suit/suit_report.o + endif ifeq ($(SUIT_HAVE_TRY_EACH),1) CFLAGS+=-DSUIT_HAVE_TRY_EACH endif diff --git a/src/suit/suit_process.c b/src/suit/suit_process.c index b3f09639e1..b1f187f319 100644 --- a/src/suit/suit_process.c +++ b/src/suit/suit_process.c @@ -144,6 +144,15 @@ static int suit_override(struct suit_context* ctx, WOLFCOSE_CBOR_CTX* c) ctx->params.contentLen = dataLen; } } + else if (pkey == (int64_t)SUIT_PARAM_URI) { + if (wc_CBOR_DecodeTstr(c, &data, &dataLen) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else { + ctx->params.uri = data; + ctx->params.uriLen = dataLen; + } + } else if (pkey == (int64_t)SUIT_PARAM_SOURCE_COMPONENT) { if (wc_CBOR_DecodeUint(c, &uval) != WOLFCOSE_SUCCESS) { ret = SUIT_E_PARSE; @@ -313,6 +322,23 @@ static int suit_run_sequence(struct suit_context* ctx, const uint8_t* seq, ctx->params.contentLen); } } +#ifdef SUIT_HAVE_FETCH + else if (cmd == (int64_t)SUIT_DIR_FETCH) { + if (wc_CBOR_Skip(&c) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_PARSE; + } + else if ((ctx->ops == NULL) || (ctx->ops->fetch == NULL)) { + ret = SUIT_E_UNSUPPORTED; + } + else if (ctx->params.uri == NULL) { + ret = SUIT_E_FETCH; + } + else if (ctx->ops->fetch(ctx->ops->ctx, ctx->componentIndex, + ctx->params.uri, ctx->params.uriLen) != 0) { + ret = SUIT_E_FETCH; + } + } +#endif else if (cmd == (int64_t)SUIT_DIR_COPY) { if (wc_CBOR_Skip(&c) != WOLFCOSE_SUCCESS) { ret = SUIT_E_PARSE; diff --git a/src/suit/suit_report.c b/src/suit/suit_report.c new file mode 100644 index 0000000000..199b95807c --- /dev/null +++ b/src/suit/suit_report.c @@ -0,0 +1,81 @@ +/* suit_report.c + * + * Copyright (C) 2025 wolfSSL Inc. + * + * This file is part of wolfBoot. + * + * wolfBoot is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfBoot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + */ +/** + * @file suit_report.c + * @brief Minimal SUIT status report: a compact CBOR { result, sequence-number } + * record an update server (e.g. wolfUpdate) reads to learn an update's outcome + * without a second boot. Not the full draft-suit-report COSE attestation. + */ +#include "suit.h" + +#if defined(WOLFBOOT_SUIT) && defined(SUIT_HAVE_REPORT) + +#include + +int suit_report_encode(const struct suit_context* ctx, + const struct suit_manifest* m, int result, + uint8_t* out, size_t outLen, size_t* written) +{ + int ret = SUIT_SUCCESS; + WOLFCOSE_CBOR_CTX c; + + (void)ctx; + if ((m == NULL) || (out == NULL) || (written == NULL)) { + return SUIT_E_INVALID_ARG; + } + + c.buf = out; + c.cbuf = NULL; + c.bufSz = outLen; + c.idx = 0; + + if (wc_CBOR_EncodeMapStart(&c, 2) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_BOUNDS; + } + if (ret == SUIT_SUCCESS) { + if (wc_CBOR_EncodeInt(&c, (int64_t)SUIT_REPORT_RESULT) + != WOLFCOSE_SUCCESS) { + ret = SUIT_E_BOUNDS; + } + } + if (ret == SUIT_SUCCESS) { + if (wc_CBOR_EncodeInt(&c, (int64_t)result) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_BOUNDS; + } + } + if (ret == SUIT_SUCCESS) { + if (wc_CBOR_EncodeInt(&c, (int64_t)SUIT_REPORT_SEQUENCE_NUMBER) + != WOLFCOSE_SUCCESS) { + ret = SUIT_E_BOUNDS; + } + } + if (ret == SUIT_SUCCESS) { + if (wc_CBOR_EncodeUint(&c, m->sequenceNumber) != WOLFCOSE_SUCCESS) { + ret = SUIT_E_BOUNDS; + } + } + if (ret == SUIT_SUCCESS) { + *written = c.idx; + } + return ret; +} + +#endif /* WOLFBOOT_SUIT && SUIT_HAVE_REPORT */ diff --git a/tests/suit_host_test.sh b/tests/suit_host_test.sh index 8f28122f4e..b2f66c028a 100755 --- a/tests/suit_host_test.sh +++ b/tests/suit_host_test.sh @@ -18,6 +18,7 @@ cd "$ROOT" # wolfCOSE is built lean for ES256 sign+verify only, matching a minimal host # wolfSSL; the verify path itself is what wolfBoot uses on-target. "$CC" -DWOLFBOOT_SUIT -DSUIT_INSTALL_DIRECTIVES -DSUIT_HAVE_ENCRYPTION \ + -DSUIT_HAVE_FETCH -DSUIT_HAVE_REPORT \ -DWOLFCOSE_NO_ENCRYPT -DWOLFCOSE_NO_MAC0 \ -DWOLFCOSE_NO_MAC -DWOLFCOSE_NO_SIGN -DWOLFCOSE_NO_RECIPIENTS \ -DWOLFCOSE_NO_MLDSA -DWOLFCOSE_NO_KEY_ENCODE -DWOLFCOSE_NO_KEY_DECODE \ @@ -27,6 +28,7 @@ cd "$ROOT" -isystem "$WOLFSSL_DIR/include" \ tests/suit_test.c \ src/suit/suit_parse.c src/suit/suit_verify.c src/suit/suit_process.c \ + src/suit/suit_report.c \ lib/wolfCOSE/src/wolfcose.c lib/wolfCOSE/src/wolfcose_cbor.c \ -L"$WOLFSSL_DIR/lib" -lwolfssl -o "$OUT" diff --git a/tests/suit_test.c b/tests/suit_test.c index 5586457609..3da71f53b5 100644 --- a/tests/suit_test.c +++ b/tests/suit_test.c @@ -106,6 +106,27 @@ static const uint8_t TEST_QY[32] = { 0x79,0x03,0xFE,0x10,0x08,0xB8,0xBC,0x99,0xA4,0x1A,0xE9,0xE9,0x56,0x28,0xBC,0x64, 0xF2,0xF1,0xB2,0x0C,0x2D,0x7E,0x9F,0x51,0x77,0xA3,0xC2,0x94,0xD4,0x46,0x22,0x99 }; +#ifdef SUIT_HAVE_FETCH +/* directive-fetch source, and a fetch op standing in for wolfUpdate transport: + * it records the uri it was handed and stages the payload into the component. */ +static const char URI[] = "coaps://wolfupdate.example/fw"; +static int g_fetch_called; +static uint8_t g_fetch_uri[64]; +static size_t g_fetch_uriLen; +static int comp_fetch(void* c, size_t idx, const uint8_t* uri, size_t uriLen) +{ + (void)c; (void)idx; + g_fetch_called = 1; + if (uriLen > sizeof(g_fetch_uri)) { return -1; } + memcpy(g_fetch_uri, uri, uriLen); + g_fetch_uriLen = uriLen; + if (sizeof(FW) > sizeof(g_flash)) { return -1; } + memcpy(g_flash, FW, sizeof(FW)); + g_flashLen = sizeof(FW); + return 0; +} +#endif + #define E(call) do { if ((call) != WOLFCOSE_SUCCESS) { return -1; } } while (0) static void cbor_init(WOLFCOSE_CBOR_CTX* c, uint8_t* out, size_t sz) @@ -178,6 +199,29 @@ static int enc_install_seq(uint8_t* out, size_t outSz, size_t* outLen, return 0; } +#ifdef SUIT_HAVE_FETCH +/* install via fetch: set the image digest + uri, fetch the payload, image-match. */ +static int enc_install_fetch_seq(uint8_t* out, size_t outSz, size_t* outLen, + const uint8_t* sd, size_t sdLen) +{ + WOLFCOSE_CBOR_CTX c; + cbor_init(&c, out, outSz); + E(wc_CBOR_EncodeArrayStart(&c, 6)); + E(wc_CBOR_EncodeInt(&c, SUIT_DIR_OVERRIDE_PARAMETERS)); + E(wc_CBOR_EncodeMapStart(&c, 2)); + E(wc_CBOR_EncodeInt(&c, SUIT_PARAM_IMAGE_DIGEST)); + E(wc_CBOR_EncodeBstr(&c, sd, sdLen)); + E(wc_CBOR_EncodeInt(&c, SUIT_PARAM_URI)); + E(wc_CBOR_EncodeTstr(&c, (const uint8_t*)URI, strlen(URI))); + E(wc_CBOR_EncodeInt(&c, SUIT_DIR_FETCH)); + E(wc_CBOR_EncodeUint(&c, 15)); + E(wc_CBOR_EncodeInt(&c, SUIT_COND_IMAGE_MATCH)); + E(wc_CBOR_EncodeUint(&c, 15)); + *outLen = c.idx; + return 0; +} +#endif + static int enc_common(uint8_t* out, size_t outSz, size_t* outLen, const uint8_t* shared, size_t sharedLen) { @@ -247,7 +291,7 @@ static int enc_envelope(uint8_t* out, size_t outSz, size_t* outLen, * encrypt is set, the install content is a COSE_Encrypt0 of FW (decrypted on * install); the image-digest is always over the FW plaintext. */ static int author(uint8_t* env, size_t envSz, size_t* envLen, size_t* sigOff, - int encrypt) + int encrypt, int useFetch) { WC_RNG rng; ecc_key eccKey; @@ -295,8 +339,20 @@ static int author(uint8_t* env, size_t envSz, size_t* envLen, size_t* sigOff, (void)encrypt; #endif +#ifdef SUIT_HAVE_FETCH + if (useFetch) { + CHECK(enc_install_fetch_seq(install, sizeof(install), &installLen, sdFw, + sdFwLen) == 0, "install-fetch"); + } + else { + CHECK(enc_install_seq(install, sizeof(install), &installLen, sdFw, + sdFwLen, contentPtr, contentLen) == 0, "install"); + } +#else + (void)useFetch; CHECK(enc_install_seq(install, sizeof(install), &installLen, sdFw, sdFwLen, contentPtr, contentLen) == 0, "install"); +#endif CHECK(enc_common(common, sizeof(common), &commonLen, shared, sharedLen) == 0, "common"); CHECK(enc_manifest(manifest, sizeof(manifest), &manifestLen, common, @@ -364,12 +420,20 @@ int main(void) #ifdef SUIT_HAVE_ENCRYPTION uint8_t decBuf[256]; #endif +#ifdef SUIT_HAVE_REPORT + uint8_t report[64]; + size_t reportLen = 0; + WOLFCOSE_CBOR_CTX rc; + size_t rcount = 0; + int64_t rkey = 0, rresult = 0; + uint64_t rseq = 0; +#endif memset(&ops, 0, sizeof(ops)); ops.hash = comp_hash; ops.write = comp_write; - if (author(env, sizeof(env), &envLen, &sigOff, 0) != 0) { return 1; } + if (author(env, sizeof(env), &envLen, &sigOff, 0, 0) != 0) { return 1; } printf("authored full SUIT envelope: %zu bytes\n", envLen); /* Dump the envelope for the independent cross-check (cbor2 + pycose). */ @@ -441,7 +505,7 @@ int main(void) #ifdef SUIT_HAVE_ENCRYPTION /* Encrypted payload: install content is a COSE_Encrypt0, decrypted with the * device key on write. Confidentiality end to end. */ - if (author(env, sizeof(env), &envLen, &sigOff, 1) != 0) { return 1; } + if (author(env, sizeof(env), &envLen, &sigOff, 1, 0) != 0) { return 1; } CHECK(suit_open(&m, env, envLen) == SUIT_SUCCESS, "open (encrypted)"); CHECK(suit_verify_auth(&m) == SUIT_SUCCESS, "verify_auth (encrypted)"); g_flashLen = 0; @@ -457,6 +521,61 @@ int main(void) printf("PASS: encrypted payload decrypted + installed (confidentiality)\n"); #endif +#ifdef SUIT_HAVE_FETCH + /* directive-fetch: the host retrieves the payload by uri instead of having it + * embedded, then image-match validates what was fetched. */ + if (author(env, sizeof(env), &envLen, &sigOff, 0, 1) != 0) { return 1; } + CHECK(suit_open(&m, env, envLen) == SUIT_SUCCESS, "open (fetch)"); + CHECK(suit_verify_auth(&m) == SUIT_SUCCESS, "verify_auth (fetch)"); + g_flashLen = 0; + g_fetch_called = 0; + memset(&ops, 0, sizeof(ops)); + ops.hash = comp_hash; + ops.fetch = comp_fetch; + ctx_init(&c, &m, &ops); + CHECK(suit_process(&c, &m) == SUIT_SUCCESS, "process (fetch) should pass"); + CHECK(g_fetch_called == 1, "fetch op must be invoked"); + CHECK(g_fetch_uriLen == strlen(URI) && + memcmp(g_fetch_uri, URI, g_fetch_uriLen) == 0, "fetch uri must match"); + CHECK(g_flashLen == sizeof(FW) && memcmp(g_flash, FW, sizeof(FW)) == 0, + "fetched payload must equal FW"); + printf("PASS: directive-fetch retrieved + image-matched payload\n"); + memset(&ops, 0, sizeof(ops)); + ops.hash = comp_hash; + ops.write = comp_write; +#endif + +#ifdef SUIT_HAVE_REPORT + /* status report: a finished process emits a compact { result, sequence } + * record an update server consumes to learn the outcome. */ + if (author(env, sizeof(env), &envLen, &sigOff, 0, 0) != 0) { return 1; } + CHECK(suit_open(&m, env, envLen) == SUIT_SUCCESS, "open (report)"); + CHECK(suit_verify_auth(&m) == SUIT_SUCCESS, "verify_auth (report)"); + g_flashLen = 0; + g_corrupt_write = 0; + ctx_init(&c, &m, &ops); + ret = suit_process(&c, &m); + CHECK(ret == SUIT_SUCCESS, "process (report) should pass"); + CHECK(suit_report_encode(&c, &m, ret, report, sizeof(report), &reportLen) + == SUIT_SUCCESS, "report encode"); + rc.buf = NULL; + rc.cbuf = report; + rc.bufSz = reportLen; + rc.idx = 0; + CHECK(wc_CBOR_DecodeMapStart(&rc, &rcount) == WOLFCOSE_SUCCESS && + rcount == 2, "report is a 2-entry map"); + CHECK(wc_CBOR_DecodeInt(&rc, &rkey) == WOLFCOSE_SUCCESS && + rkey == SUIT_REPORT_RESULT, "report result key"); + CHECK(wc_CBOR_DecodeInt(&rc, &rresult) == WOLFCOSE_SUCCESS && + rresult == 0, "report result is success"); + CHECK(wc_CBOR_DecodeInt(&rc, &rkey) == WOLFCOSE_SUCCESS && + rkey == SUIT_REPORT_SEQUENCE_NUMBER, "report sequence key"); + CHECK(wc_CBOR_DecodeUint(&rc, &rseq) == WOLFCOSE_SUCCESS && + rseq == 1, "report sequence is the manifest sequence"); + printf("PASS: status report encodes result + sequence (%zu bytes)\n", + reportLen); +#endif + printf("ALL SUIT INSTALL TESTS PASSED\n"); return 0; }