Skip to content

Commit a5d7e4b

Browse files
committed
Add SUIT directive-fetch (host callback) and minimal status report for networked update
1 parent 61b5cc6 commit a5d7e4b

7 files changed

Lines changed: 277 additions & 8 deletions

File tree

docs/SUIT.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ verify objects (lean, `WOLFCOSE_LEAN_VERIFY`). Fine-tuning macros:
3737
| `SUIT_HAVE_ENCRYPTION` | decrypt COSE_Encrypt0 payloads on install (AES-GCM); enables AES-GCM in the build |
3838
| `SUIT_DEVICE_VENDOR_ID` / `SUIT_DEVICE_CLASS_ID` | this device's identity (brace initializers) for the vendor/class conditions |
3939
| `SUIT_KEY_SLOT` | fallback trust-anchor slot when the COSE_Sign1 carries no key id |
40-
| `SUIT_HAVE_FETCH` / `SUIT_HAVE_TRY_EACH` / `SUIT_HAVE_RUN_SEQUENCE` | optional commands |
40+
| `SUIT_HAVE_FETCH` | enable directive-fetch; the host supplies an `ops->fetch` callback that retrieves the payload by uri (e.g. wolfUpdate transport) |
41+
| `SUIT_HAVE_REPORT` | build `suit_report_encode()`, a compact `{ result, sequence }` status record an update server reads to learn the outcome |
42+
| `SUIT_HAVE_TRY_EACH` / `SUIT_HAVE_RUN_SEQUENCE` | optional commands |
4143

4244
## Architecture
4345

@@ -90,8 +92,11 @@ CBOR/COSE tooling (test B); it is not a full draft-34 implementation.
9092

9193
- Unrecognized (or known-but-unsupported) commands are **default-denied** (the
9294
sequence fails), as a SUIT processor must, rather than silently skipped.
93-
- Not implemented (and rejected if present): directive-fetch, severable members,
94-
try-each / run-sequence / swap, dependencies/trust-domains, SUIT Reports.
95+
- Optional, built only when their macro is set: directive-fetch
96+
(`SUIT_HAVE_FETCH`, via a host callback) and a compact status report
97+
(`SUIT_HAVE_REPORT`, not the full draft-suit-report COSE attestation).
98+
- Not implemented (and rejected if present): severable members,
99+
try-each / run-sequence / swap, dependencies/trust-domains.
95100

96101
## Status
97102

@@ -110,8 +115,18 @@ image or content larger than the partition space, and an out-of-range component
110115
index), and **key-id selection** (the COSE_Sign1 `kid` picks the trust anchor
111116
via the keystore, like the TLV path's pubkey hint).
112117

113-
Follow-ups: `directive-fetch` (networked payload retrieval, wolfUpdate), and the
114-
optional commands (`try-each` / `run-sequence` / `swap`).
118+
Networked update support: `directive-fetch` (`SUIT_HAVE_FETCH`) retrieves the
119+
payload by uri through a host callback, and `suit_report_encode()`
120+
(`SUIT_HAVE_REPORT`) emits a compact status record, so a server (e.g. wolfUpdate)
121+
can pull images and learn outcomes. Remaining optional commands: `try-each` /
122+
`run-sequence` / `swap`.
123+
124+
Production readiness: this feature is experimental and off by default. Before
125+
enabling it in a shipping product the gate is, at minimum: fuzz the manifest
126+
parser and complete a security review (the manifest is attacker-controlled),
127+
hardware-test the boot/swap path, and provision the content-encryption key by
128+
key-wrap rather than handing it in raw. Encryption (`SUIT_HAVE_ENCRYPTION`) is
129+
not production-ready until the key-wrap step exists.
115130

116131
This PR is gated on the wolfCOSE fixes in wolfSSL/wolfCOSE PR #53; the submodule
117132
is pinned to that work and should be repinned to the wolfCOSE v1.0 tag before

include/suit.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
#define SUIT_E_INSTALL (-9)
4747
#define SUIT_E_ROLLBACK (-10)
4848
#define SUIT_E_BOUNDS (-11)
49+
#define SUIT_E_FETCH (-12)
4950

5051
/* SUIT_Envelope map keys (IANA SUIT Envelope Elements). */
5152
enum suit_envelope_key {
@@ -173,6 +174,8 @@ struct suit_params {
173174
size_t classIdLen;
174175
const uint8_t* content; /* directive-write content */
175176
size_t contentLen;
177+
const uint8_t* uri; /* directive-fetch source (tstr bytes) */
178+
size_t uriLen;
176179
uint64_t imageSize;
177180
size_t sourceComponent;
178181
int componentSlot;
@@ -186,6 +189,9 @@ struct suit_component_ops {
186189
int (*hash)(void* ctx, size_t idx, uint8_t* out, size_t outLen);
187190
int (*write)(void* ctx, size_t idx, const uint8_t* src, size_t len);
188191
int (*copy)(void* ctx, size_t idx, size_t srcIdx);
192+
/* Retrieve the payload at uri into component idx (SUIT_HAVE_FETCH). The host
193+
* (e.g. wolfUpdate transport) owns the network/storage; NULL if unsupported. */
194+
int (*fetch)(void* ctx, size_t idx, const uint8_t* uri, size_t uriLen);
189195
};
190196

191197
/* 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);
213219
int suit_verify_auth(struct suit_manifest* m);
214220
int suit_process(struct suit_context* ctx, struct suit_manifest* m);
215221

222+
#ifdef SUIT_HAVE_REPORT
223+
/* Minimal SUIT status report keys: a compact outcome record for an update server
224+
* (e.g. wolfUpdate), not the full draft-suit-report COSE attestation. */
225+
enum suit_report_key {
226+
SUIT_REPORT_RESULT = 1, /* int: 0 on success, else the SUIT_E_* code */
227+
SUIT_REPORT_SEQUENCE_NUMBER = 2 /* uint: the manifest sequence number */
228+
};
229+
230+
/* Encode a status report for a finished suit_process into the caller's buffer as
231+
* CBOR. result is the suit_process return code. Writes the encoded length to
232+
* written. Zero allocation: out is caller-owned. */
233+
int suit_report_encode(const struct suit_context* ctx,
234+
const struct suit_manifest* m, int result,
235+
uint8_t* out, size_t outLen, size_t* written);
236+
#endif /* SUIT_HAVE_REPORT */
237+
216238
/* wolfBoot entry point: open + authenticate + process a staged SUIT envelope.
217239
* The caller supplies the component I/O ops and this device's identity. */
218240
int wolfBoot_suit_verify(const uint8_t* env, size_t envLen,

options.mk

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1602,6 +1602,10 @@ ifeq ($(WOLFBOOT_SUIT),1)
16021602
ifeq ($(SUIT_HAVE_FETCH),1)
16031603
CFLAGS+=-DSUIT_HAVE_FETCH
16041604
endif
1605+
ifeq ($(SUIT_HAVE_REPORT),1)
1606+
CFLAGS+=-DSUIT_HAVE_REPORT
1607+
OBJS+=./src/suit/suit_report.o
1608+
endif
16051609
ifeq ($(SUIT_HAVE_TRY_EACH),1)
16061610
CFLAGS+=-DSUIT_HAVE_TRY_EACH
16071611
endif

src/suit/suit_process.c

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,15 @@ static int suit_override(struct suit_context* ctx, WOLFCOSE_CBOR_CTX* c)
144144
ctx->params.contentLen = dataLen;
145145
}
146146
}
147+
else if (pkey == (int64_t)SUIT_PARAM_URI) {
148+
if (wc_CBOR_DecodeTstr(c, &data, &dataLen) != WOLFCOSE_SUCCESS) {
149+
ret = SUIT_E_PARSE;
150+
}
151+
else {
152+
ctx->params.uri = data;
153+
ctx->params.uriLen = dataLen;
154+
}
155+
}
147156
else if (pkey == (int64_t)SUIT_PARAM_SOURCE_COMPONENT) {
148157
if (wc_CBOR_DecodeUint(c, &uval) != WOLFCOSE_SUCCESS) {
149158
ret = SUIT_E_PARSE;
@@ -313,6 +322,23 @@ static int suit_run_sequence(struct suit_context* ctx, const uint8_t* seq,
313322
ctx->params.contentLen);
314323
}
315324
}
325+
#ifdef SUIT_HAVE_FETCH
326+
else if (cmd == (int64_t)SUIT_DIR_FETCH) {
327+
if (wc_CBOR_Skip(&c) != WOLFCOSE_SUCCESS) {
328+
ret = SUIT_E_PARSE;
329+
}
330+
else if ((ctx->ops == NULL) || (ctx->ops->fetch == NULL)) {
331+
ret = SUIT_E_UNSUPPORTED;
332+
}
333+
else if (ctx->params.uri == NULL) {
334+
ret = SUIT_E_FETCH;
335+
}
336+
else if (ctx->ops->fetch(ctx->ops->ctx, ctx->componentIndex,
337+
ctx->params.uri, ctx->params.uriLen) != 0) {
338+
ret = SUIT_E_FETCH;
339+
}
340+
}
341+
#endif
316342
else if (cmd == (int64_t)SUIT_DIR_COPY) {
317343
if (wc_CBOR_Skip(&c) != WOLFCOSE_SUCCESS) {
318344
ret = SUIT_E_PARSE;

src/suit/suit_report.c

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/* suit_report.c
2+
*
3+
* Copyright (C) 2025 wolfSSL Inc.
4+
*
5+
* This file is part of wolfBoot.
6+
*
7+
* wolfBoot is free software; you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation; either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* wolfBoot is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program; if not, write to the Free Software
19+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
20+
*/
21+
/**
22+
* @file suit_report.c
23+
* @brief Minimal SUIT status report: a compact CBOR { result, sequence-number }
24+
* record an update server (e.g. wolfUpdate) reads to learn an update's outcome
25+
* without a second boot. Not the full draft-suit-report COSE attestation.
26+
*/
27+
#include "suit.h"
28+
29+
#if defined(WOLFBOOT_SUIT) && defined(SUIT_HAVE_REPORT)
30+
31+
#include <wolfcose/wolfcose.h>
32+
33+
int suit_report_encode(const struct suit_context* ctx,
34+
const struct suit_manifest* m, int result,
35+
uint8_t* out, size_t outLen, size_t* written)
36+
{
37+
int ret = SUIT_SUCCESS;
38+
WOLFCOSE_CBOR_CTX c;
39+
40+
(void)ctx;
41+
if ((m == NULL) || (out == NULL) || (written == NULL)) {
42+
return SUIT_E_INVALID_ARG;
43+
}
44+
45+
c.buf = out;
46+
c.cbuf = NULL;
47+
c.bufSz = outLen;
48+
c.idx = 0;
49+
50+
if (wc_CBOR_EncodeMapStart(&c, 2) != WOLFCOSE_SUCCESS) {
51+
ret = SUIT_E_BOUNDS;
52+
}
53+
if (ret == SUIT_SUCCESS) {
54+
if (wc_CBOR_EncodeInt(&c, (int64_t)SUIT_REPORT_RESULT)
55+
!= WOLFCOSE_SUCCESS) {
56+
ret = SUIT_E_BOUNDS;
57+
}
58+
}
59+
if (ret == SUIT_SUCCESS) {
60+
if (wc_CBOR_EncodeInt(&c, (int64_t)result) != WOLFCOSE_SUCCESS) {
61+
ret = SUIT_E_BOUNDS;
62+
}
63+
}
64+
if (ret == SUIT_SUCCESS) {
65+
if (wc_CBOR_EncodeInt(&c, (int64_t)SUIT_REPORT_SEQUENCE_NUMBER)
66+
!= WOLFCOSE_SUCCESS) {
67+
ret = SUIT_E_BOUNDS;
68+
}
69+
}
70+
if (ret == SUIT_SUCCESS) {
71+
if (wc_CBOR_EncodeUint(&c, m->sequenceNumber) != WOLFCOSE_SUCCESS) {
72+
ret = SUIT_E_BOUNDS;
73+
}
74+
}
75+
if (ret == SUIT_SUCCESS) {
76+
*written = c.idx;
77+
}
78+
return ret;
79+
}
80+
81+
#endif /* WOLFBOOT_SUIT && SUIT_HAVE_REPORT */

tests/suit_host_test.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ cd "$ROOT"
1818
# wolfCOSE is built lean for ES256 sign+verify only, matching a minimal host
1919
# wolfSSL; the verify path itself is what wolfBoot uses on-target.
2020
"$CC" -DWOLFBOOT_SUIT -DSUIT_INSTALL_DIRECTIVES -DSUIT_HAVE_ENCRYPTION \
21+
-DSUIT_HAVE_FETCH -DSUIT_HAVE_REPORT \
2122
-DWOLFCOSE_NO_ENCRYPT -DWOLFCOSE_NO_MAC0 \
2223
-DWOLFCOSE_NO_MAC -DWOLFCOSE_NO_SIGN -DWOLFCOSE_NO_RECIPIENTS \
2324
-DWOLFCOSE_NO_MLDSA -DWOLFCOSE_NO_KEY_ENCODE -DWOLFCOSE_NO_KEY_DECODE \
@@ -27,6 +28,7 @@ cd "$ROOT"
2728
-isystem "$WOLFSSL_DIR/include" \
2829
tests/suit_test.c \
2930
src/suit/suit_parse.c src/suit/suit_verify.c src/suit/suit_process.c \
31+
src/suit/suit_report.c \
3032
lib/wolfCOSE/src/wolfcose.c lib/wolfCOSE/src/wolfcose_cbor.c \
3133
-L"$WOLFSSL_DIR/lib" -lwolfssl -o "$OUT"
3234

0 commit comments

Comments
 (0)