diff --git a/.github/workflows/test-custom-tlv-simulator.yml b/.github/workflows/test-custom-tlv-simulator.yml index be94d6d000..712d498479 100644 --- a/.github/workflows/test-custom-tlv-simulator.yml +++ b/.github/workflows/test-custom-tlv-simulator.yml @@ -48,3 +48,51 @@ jobs: - name: Run get_tlv simulator test run: | [ x`./wolfboot.elf get_tlv 2>/dev/null| tail -1` = xAABBCCDDEEFF0011223344 ] + + custom_tlv_large_simulator_tests: + runs-on: ubuntu-latest + container: + image: ghcr.io/wolfssl/wolfboot-ci-sim:v1.0 + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Trust workspace + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: make clean + run: | + make distclean + + - name: Select config + run: | + cp config/examples/sim.config .config + + - name: Build tools + run: | + make -C tools/keytools && make -C tools/bin-assemble + + - name: Build wolfboot.elf and test-app/image.elf with a 2KB header + run: | + make clean && make IMAGE_HEADER_SIZE=2048 + + - name: Sign the image with 300-byte buffer and file custom TLVs + run: | + VAL=`printf 'A5%.0s' $(seq 1 300)` + head -c 300 /dev/zero | tr '\0' '\245' > large_tlv.bin + IMAGE_HEADER_SIZE=2048 tools/keytools/sign --ed25519 --custom-tlv-buffer 0x0034 $VAL --custom-tlv-file 0x0035 large_tlv.bin test-app/image.elf wolfboot_signing_private_key.der 1 + + - name: Re-assemble the internal_flash.dd image file + run: | + make assemble_internal_flash.dd IMAGE_HEADER_SIZE=2048 + + - name: Run get_tlv simulator test on the 300-byte buffer TLV + run: | + [ x`./wolfboot.elf get_tlv 2>/dev/null| tail -1` = x`printf 'A5%.0s' $(seq 1 300)` ] + + - name: Run get_tlv simulator test on the 300-byte file TLV (tag 0x35) + run: | + [ x`./wolfboot.elf get_tlv=53 2>/dev/null| tail -1` = x`printf 'A5%.0s' $(seq 1 300)` ] diff --git a/.gitignore b/.gitignore index b1bc7fb6ad..3139c130af 100644 --- a/.gitignore +++ b/.gitignore @@ -225,6 +225,7 @@ tools/unit-tests/unit-update-ram-enc tools/unit-tests/unit-update-ram-enc-nopart tools/unit-tests/unit-va416x0-fram tools/unit-tests/unit-wolfhsm_flash_hal +tools/unit-tests/__pycache__/* diff --git a/docs/Signing.md b/docs/Signing.md index af21fa6855..3e3377b6ae 100644 --- a/docs/Signing.md +++ b/docs/Signing.md @@ -255,17 +255,33 @@ Provides a value to be set with a custom tag * `--custom-tlv-buffer tag value`: Adds a TLV entry with arbitrary length to the manifest header, corresponding to the type identified by `tag`, and assigns the value `value`. The tag is a 16-bit number. Valid tags are in the range between 0x0030 and 0xFEFE. The length - is implicit, and is the length of the value. + is implicit, and is the length of the value. The maximum length is 65524 bytes. Value argument is in the form of a hex string, e.g. `--custom-tlv-buffer 0x0030 AABBCCDDEE` will add a TLV entry with tag 0x0030, length 5 and value 0xAABBCCDDEE. * `--custom-tlv-string tag ascii-string`: Adds a TLV entry with arbitrary length to the manifest header, corresponding to the type identified by `tag`, and assigns the value of `ascii-string`. The tag is a 16-bit number. Valid tags are in the range between 0x0030 and 0xFEFE. The length - is implicit, and is the length of the `ascii-string`. `ascii-string` argument is in the form of a string, + is implicit, and is the length of the `ascii-string`. The maximum length is 65524 bytes. + `ascii-string` argument is in the form of a string, e.g. `--custom-tlv-string 0x0030 "Version-1"` will add a TLV entry with tag 0x0030, length 9 and value Version-1. + * `--custom-tlv-file tag filename`: Adds a TLV entry with arbitrary length to the manifest + header, corresponding to the type identified by `tag`, with the value read as raw bytes + from the file `filename`. The tag is a 16-bit number. Valid tags are in the range between + 0x0030 and 0xFEFE. The length is implicit, and is the size of the file. The maximum length + is 65524 bytes. Unlike `--custom-tlv-buffer`, the value is not passed on the command line, + so large binary values are not subject to the OS argument length limits. + + The 65524-byte maximum is the largest TLV value the wolfBoot header parser can walk + past when locating the fields that follow it, such as the signature. + + If the custom TLVs do not fit in the configured header size, the sign tool automatically + increases the size of the manifest header, rounding up to the next power of two. wolfBoot + must be built with a matching `IMAGE_HEADER_SIZE`, or it will fail to locate the firmware + image at boot. + #### Three-steps signing using external provisioning tools If the private key is not accessible, while it's possible to sign payloads using diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index 77b3c5389b..cae6099b64 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -78,6 +78,12 @@ static inline int fp_truncate(FILE *f, size_t len) #define MAX_CUSTOM_TLVS (16) #endif +/* wolfBoot and this tool stop parsing at any header field larger than + * (uint16_t)(header size - IMAGE_HEADER_OFFSET), at most 65528 including the + * 4-byte tag and length, so the largest usable value is 65524. A longer + * field would hide every field after it, including the signature. */ +#define MAX_TLV_LEN (65524) + #include #include #include @@ -1351,7 +1357,7 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz, uint8_t *base_hash, uint32_t base_hash_sz) { uint32_t header_idx; - uint8_t *header; + uint8_t *header = NULL; FILE *f = NULL, *f2 = NULL, *fek = NULL, *fef = NULL; uint32_t fw_version32; struct stat attrib; @@ -1377,42 +1383,56 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz, /* Check certificate chain file size before allocating header, and adjust * header size if needed */ - if (CMD.cert_chain_file != NULL) { - struct stat file_stat; - - /* Get the file size */ - if (stat(CMD.cert_chain_file, &file_stat) == 0) { - off_t chain_file_sz = file_stat.st_size; - uint32_t required_space; - - if ((chain_file_sz < 0) || - ((uintmax_t)chain_file_sz > (uintmax_t)UINT32_MAX)) { - printf("Warning: certificate chain file size is invalid (%jd)\n", - (intmax_t)chain_file_sz); + if ((CMD.cert_chain_file != NULL) || (CMD.custom_tlvs > 0)) { + uint32_t hdr_cert_chain_sz = 0; + uint32_t required_space; + + if (CMD.cert_chain_file != NULL) { + struct stat file_stat; + if (stat(CMD.cert_chain_file, &file_stat) == 0) { + off_t chain_file_sz = file_stat.st_size; + if (chain_file_sz < 0) { + printf("Warning: certificate chain file size is invalid " + "(%jd)\n", (intmax_t)chain_file_sz); + } + else if ((uintmax_t)chain_file_sz > (uintmax_t)MAX_TLV_LEN) { + printf("Error: Certificate chain too large for TLV encoding " + "(%ju > %u)\n", (uintmax_t)chain_file_sz, MAX_TLV_LEN); + goto failure; + } + else { + hdr_cert_chain_sz = (uint32_t)chain_file_sz; + } } else { - required_space = header_required_size(is_diff, - (uint32_t)chain_file_sz, secondary_key_sz); - - /* If the current header size is too small, increase it */ - if (CMD.header_sz < required_space) { - /* Round up to nearest power of 2 that can hold the chain */ - const uint32_t min_header_size = 256; - uint32_t new_size = min_header_size; - while (new_size < required_space) { - new_size *= 2; - } + printf("Warning: Could not stat certificate chain file %s: %s\n", + CMD.cert_chain_file, strerror(errno)); + } + } + + required_space = + header_required_size(is_diff, hdr_cert_chain_sz, secondary_key_sz); - printf("Increasing header size from %u to %u bytes to fit " - "certificate chain\n", - CMD.header_sz, new_size); - CMD.header_sz = new_size; + /* If the current header size is too small, increase it */ + if (CMD.header_sz < required_space) { + /* Round up to nearest power of 2 that can hold all fields */ + const uint32_t min_header_size = 256; + uint32_t new_size = min_header_size; + while (new_size < required_space) { + if (new_size > (UINT32_MAX / 2U)) { + printf("Error: Header size overflow while sizing " + "manifest header\n"); + goto failure; } + new_size *= 2; } - } - else { - printf("Warning: Could not stat certificate chain file %s: %s\n", - CMD.cert_chain_file, strerror(errno)); + + fprintf(stderr, "Warning: increasing header size from %u to %u " + "bytes to fit manifest header fields.\n" + "Warning: wolfBoot must be built with IMAGE_HEADER_SIZE=%u " + "or it will not find the firmware image.\n", + CMD.header_sz, new_size, new_size); + CMD.header_sz = new_size; } } @@ -1572,10 +1592,10 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz, } cert_chain_sz = (uint32_t)file_stat.st_size; - if (cert_chain_sz > (uint32_t)UINT16_MAX) { + if (cert_chain_sz > (uint32_t)MAX_TLV_LEN) { printf("Error: Certificate chain too large for TLV encoding " "(%u > %u)\n", - cert_chain_sz, (unsigned int)UINT16_MAX); + cert_chain_sz, (unsigned int)MAX_TLV_LEN); fclose(f); f = NULL; goto failure; @@ -2530,35 +2550,42 @@ static int base_diff(const char *f_base, uint8_t *pubkey, uint32_t pubkey_sz, in len3++; } /* make_header_delta() below calls make_header_ex(is_diff=1), which may grow - * CMD.header_sz to fit the delta TLVs plus certificate chain. Resolve that - * expansion here, using the same logic, so patch_inv_off reflects the header - * size actually written; otherwise HDR_IMG_DELTA_INVERSE would encode a - * stale, too-small offset and break inverse-patch rollback. */ - if (CMD.cert_chain_file != NULL) { - struct stat cc_stat; - if ((stat(CMD.cert_chain_file, &cc_stat) == 0) && - (cc_stat.st_size >= 0)) { - if ((uintmax_t)cc_stat.st_size > (uintmax_t)UINT16_MAX) { - printf("Error: Certificate chain too large for TLV encoding " - "(%ju > %u)\n", (uintmax_t)cc_stat.st_size, UINT16_MAX); - goto cleanup; + * CMD.header_sz to fit the delta TLVs, custom TLVs and certificate chain. + * Resolve that expansion here, using the same logic, so patch_inv_off + * reflects the header size actually written; otherwise HDR_IMG_DELTA_INVERSE + * would encode a stale, too-small offset and break inverse-patch rollback. */ + if ((CMD.cert_chain_file != NULL) || (CMD.custom_tlvs > 0)) { + uint32_t cert_chain_sz = 0; + uint32_t required_space; + if (CMD.cert_chain_file != NULL) { + struct stat cc_stat; + if ((stat(CMD.cert_chain_file, &cc_stat) == 0) && + (cc_stat.st_size >= 0)) { + if ((uintmax_t)cc_stat.st_size > (uintmax_t)MAX_TLV_LEN) { + printf("Error: Certificate chain too large for TLV encoding " + "(%ju > %u)\n", (uintmax_t)cc_stat.st_size, MAX_TLV_LEN); + goto cleanup; + } + cert_chain_sz = (uint32_t)cc_stat.st_size; } - else { - uint32_t required_space = header_required_size(1, - (uint32_t)cc_stat.st_size, 0); - if (CMD.header_sz < required_space) { - uint32_t new_size = 256; - while (new_size < required_space) { - if (new_size > (UINT32_MAX / 2U)) { - printf("Error: Header size overflow while sizing " - "certificate chain\n"); - goto cleanup; - } - new_size *= 2; - } - CMD.header_sz = new_size; + } + required_space = header_required_size(1, cert_chain_sz, 0); + if (CMD.header_sz < required_space) { + uint32_t new_size = 256; + while (new_size < required_space) { + if (new_size > (UINT32_MAX / 2U)) { + printf("Error: Header size overflow while sizing " + "manifest header\n"); + goto cleanup; } + new_size *= 2; } + fprintf(stderr, "Warning: increasing header size from %u to %u " + "bytes to fit manifest header fields.\n" + "Warning: wolfBoot must be built with IMAGE_HEADER_SIZE=%u " + "or it will not find the firmware image.\n", + CMD.header_sz, new_size, new_size); + CMD.header_sz = new_size; } } patch_inv_off = (uint32_t)len3 + CMD.header_sz; @@ -3171,7 +3198,7 @@ int main(int argc, char** argv) fprintf(stderr, "Too many custom TLVs.\n"); exit(16); } - if (argc < (i + 3)) { + if (argc < (i + 4)) { fprintf(stderr, "Invalid custom TLV fields. \n"); exit(16); } @@ -3202,17 +3229,18 @@ int main(int argc, char** argv) } else if (strcmp(argv[i], "--custom-tlv-buffer") == 0) { int p = CMD.custom_tlvs; uint16_t tag, len; + size_t slen; uint32_t j; if (p >= MAX_CUSTOM_TLVS) { fprintf(stderr, "Too many custom TLVs.\n"); exit(16); } - if (argc < (i + 2)) { + if (argc < (i + 3)) { fprintf(stderr, "Invalid custom TLV fields. \n"); exit(16); } tag = (uint16_t)arg2num(argv[i + 1], 2); - len = (uint16_t)strlen(argv[i + 2]) / 2; + slen = strlen(argv[i + 2]); if (tag < 0x0030) { fprintf(stderr, "Invalid custom tag: %s\n", argv[i + 1]); exit(16); @@ -3221,10 +3249,18 @@ int main(int argc, char** argv) fprintf(stderr, "Invalid custom tag: %s\n", argv[i + 1]); exit(16); } - if (len > 255) { - fprintf(stderr, "custom tlv buffer size too big: %s\n", argv[i + 2]); + if ((slen / 2) > MAX_TLV_LEN) { + fprintf(stderr, "custom tlv buffer size too big: " + "%lu bytes (max %u)\n", (unsigned long)(slen / 2), + MAX_TLV_LEN); exit(16); } + if ((slen % 2) != 0) { + fprintf(stderr, "custom tlv buffer hex string must have an " + "even number of digits: %s\n", argv[i + 2]); + exit(16); + } + len = (uint16_t)(slen / 2); CMD.custom_tlv[p].tag = tag; CMD.custom_tlv[p].len = len; CMD.custom_tlv[p].buffer = malloc(len); @@ -3241,17 +3277,18 @@ int main(int argc, char** argv) } else if (strcmp(argv[i], "--custom-tlv-string") == 0) { int p = CMD.custom_tlvs; uint16_t tag, len; + size_t slen; uint32_t j; if (p >= MAX_CUSTOM_TLVS) { fprintf(stderr, "Too many custom TLVs.\n"); exit(16); } - if (argc < (i + 2)) { + if (argc < (i + 3)) { fprintf(stderr, "Invalid custom TLV fields. \n"); exit(16); } tag = (uint16_t)arg2num(argv[i + 1], 2); - len = (uint16_t)strlen(argv[i + 2]); + slen = strlen(argv[i + 2]); if (tag < 0x0030) { fprintf(stderr, "Invalid custom tag: %s\n", argv[i + 1]); exit(16); @@ -3260,10 +3297,12 @@ int main(int argc, char** argv) fprintf(stderr, "Invalid custom tag: %s\n", argv[i + 1]); exit(16); } - if (len > 255) { - fprintf(stderr, "custom tlv buffer size too big: %s\n", argv[i + 2]); + if (slen > MAX_TLV_LEN) { + fprintf(stderr, "custom tlv string size too big: " + "%lu bytes (max %u)\n", (unsigned long)slen, MAX_TLV_LEN); exit(16); } + len = (uint16_t)slen; CMD.custom_tlv[p].tag = tag; CMD.custom_tlv[p].len = len; CMD.custom_tlv[p].buffer = malloc(len); @@ -3276,6 +3315,68 @@ int main(int argc, char** argv) } CMD.custom_tlvs++; i += 2; + } else if (strcmp(argv[i], "--custom-tlv-file") == 0) { + int p = CMD.custom_tlvs; + uint16_t tag; + FILE *f; + long fsz; + size_t rd; + if (p >= MAX_CUSTOM_TLVS) { + fprintf(stderr, "Too many custom TLVs.\n"); + exit(16); + } + if (argc < (i + 3)) { + fprintf(stderr, "Invalid custom TLV fields. \n"); + exit(16); + } + tag = (uint16_t)arg2num(argv[i + 1], 2); + if (tag < 0x0030) { + fprintf(stderr, "Invalid custom tag: %s\n", argv[i + 1]); + exit(16); + } + if ( ((tag & 0xFF00) == 0xFF00) || ((tag & 0xFF) == 0xFF) ) { + fprintf(stderr, "Invalid custom tag: %s\n", argv[i + 1]); + exit(16); + } + f = fopen(argv[i + 2], "rb"); + if (f == NULL) { + fprintf(stderr, "Cannot open custom tlv file %s: %s\n", + argv[i + 2], strerror(errno)); + exit(16); + } + fseek(f, 0, SEEK_END); + fsz = ftell(f); + fseek(f, 0, SEEK_SET); + if (fsz <= 0) { + fprintf(stderr, "custom tlv file is empty or unreadable: %s\n", + argv[i + 2]); + fclose(f); + exit(16); + } + if (fsz > (long)MAX_TLV_LEN) { + fprintf(stderr, "custom tlv file too big: %ld bytes " + "(max %u): %s\n", fsz, MAX_TLV_LEN, argv[i + 2]); + fclose(f); + exit(16); + } + CMD.custom_tlv[p].tag = tag; + CMD.custom_tlv[p].len = (uint16_t)fsz; + CMD.custom_tlv[p].buffer = malloc((size_t)fsz); + if (CMD.custom_tlv[p].buffer == NULL) { + fprintf(stderr, "Error malloc for custom tlv buffer %ld\n", + fsz); + fclose(f); + exit(16); + } + rd = fread(CMD.custom_tlv[p].buffer, 1, (size_t)fsz, f); + fclose(f); + if (rd != (size_t)fsz) { + fprintf(stderr, "Error reading custom tlv file %s\n", + argv[i + 2]); + exit(16); + } + CMD.custom_tlvs++; + i += 2; } else if (strcmp(argv[i], "--cert-chain") == 0) { if (argc <= (i + 1)) { @@ -3395,11 +3496,19 @@ int main(int argc, char** argv) printf("TLV %u\n", i); printf("----\n"); if (CMD.custom_tlv[i].buffer) { + uint16_t print_len = CMD.custom_tlv[i].len; + if (print_len > 256) { + print_len = 256; + } printf("Tag: %04X Len: %hu Val: ", CMD.custom_tlv[i].tag, CMD.custom_tlv[i].len); - for (j = 0; j < CMD.custom_tlv[i].len; j++) { + for (j = 0; j < print_len; j++) { printf("%02X", CMD.custom_tlv[i].buffer[j]); } + if (print_len < CMD.custom_tlv[i].len) { + printf("... (truncated, %hu bytes total)", + CMD.custom_tlv[i].len); + } printf("\n"); } else { diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 3a1b466086..5374d28a80 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -129,6 +129,7 @@ run: $(TESTS) python3 unit-sign-delta-tlv.py || exit 1 python3 unit-sign-delta-cert-inv-off.py || exit 1 python3 unit-sign-custom-tlv-le.py || exit 1 + python3 unit-sign-custom-tlv-large.py || exit 1 WOLFCRYPT_SRC:=$(WOLFBOOT_LIB_WOLFSSL)/wolfcrypt/src/sha.c \ diff --git a/tools/unit-tests/unit-sign-custom-tlv-large.py b/tools/unit-tests/unit-sign-custom-tlv-large.py new file mode 100644 index 0000000000..e3b3536eba --- /dev/null +++ b/tools/unit-tests/unit-sign-custom-tlv-large.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python3 +# unit-sign-custom-tlv-large.py +# +# Tests large custom TLVs in the C sign tool (tools/keytools/sign.c): +# values up to the 65524-byte maximum via --custom-tlv-buffer, +# --custom-tlv-string and --custom-tlv-file, automatic power-of-two growth +# of the manifest header, rejection of oversized, empty or missing values, +# and delta images keeping HDR_IMG_DELTA_INVERSE consistent with the grown +# header. +# +# Copyright (C) 2026 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. + +import os +import struct +import subprocess +import sys +import tempfile + +HDR_PADDING = 0xFF +HDR_IMG_DELTA_SIZE = 0x06 +HDR_IMG_DELTA_INVERSE = 0x15 +HDR_IMG_DELTA_INVERSE_SIZE = 0x16 + +TAG_BUFFER = 0x0030 +TAG_STRING = 0x0031 +TAG_FILE = 0x0032 + +SECTOR_SIZE = 0x1000 + +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(THIS_DIR, "..", "..")) +SIGN = os.path.join(ROOT, "tools", "keytools", "sign") + +failures = [] + + +def skip(msg): + print("SKIP unit-sign-custom-tlv-large: " + msg) + sys.exit(0) + + +def fail(msg): + failures.append(msg) + + +def parse_tlvs(data, scan_end): + """Walk the header like wolfBoot_find_header(): {tag: value bytes}.""" + tlvs = {} + p = 8 # skip 4-byte magic + 4-byte image size + while p + 4 <= scan_end: + htype = data[p] | (data[p + 1] << 8) + if htype == 0: + break + if data[p] == HDR_PADDING or (p & 1) != 0: + p += 1 + continue + length = data[p + 2] | (data[p + 3] << 8) + # wolfBoot_find_header() stops at any field larger than the header + # capacity truncated to uint16_t, so this walk must stop too or the + # test would accept images wolfBoot cannot read + if 4 + length > (scan_end - 8) & 0xFFFF: + break + if p + 4 + length > scan_end: + break + if htype not in tlvs: + tlvs[htype] = bytes(data[p + 4:p + 4 + length]) + p += 4 + length + return tlvs + + +def tlv_u32(tlvs, tag): + val = tlvs.get(tag) + if val is None or len(val) != 4: + return None + return struct.unpack("