From 76db63f13ef4252953de0227d9cd9202e124e170 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 01:48:41 +0200 Subject: [PATCH 01/26] F-7063: add missing break in arg2num case 4 case 4 masked to 32 bits then fell through into case 8, whose only statement is break. Harmless today, but indistinguishable from a genuine missing break; add the explicit break. --- tools/keytools/sign.c | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index 221f142e92..68572f686f 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -2973,6 +2973,7 @@ uint64_t arg2num(const char *arg, size_t len) break; case 4: ret &= 0xFFFFFFFF; + break; case 8: break; default: From bfda601a053ba9b1855af85ba58439344c1eac26 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 01:48:41 +0200 Subject: [PATCH 02/26] F-7059: detect short reads in the image hashing loops fread() returns size_t, so the (io_sz < 0) guard could never trigger, and a short read silently folded stale buffer bytes into the image digest. Compare the read count against the requested size in all three loops (SHA-256, SHA-384, SHA3-384), matching the existing io_sz != expected pattern used elsewhere in this file. --- tools/keytools/sign.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index 68572f686f..402fa99af8 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -1968,7 +1968,7 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz, if (read_sz > 32) read_sz = 32; io_sz = (int)fread(buf, 1, read_sz, f); - if ((io_sz < 0) && !feof(f)) { + if (io_sz != (int)read_sz) { ret = -1; break; } @@ -2045,7 +2045,7 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz, if (read_sz > 32) read_sz = 32; io_sz = (int)fread(buf, 1, read_sz, f); - if ((io_sz < 0) && !feof(f)) { + if (io_sz != (int)read_sz) { ret = -1; break; } @@ -2120,7 +2120,7 @@ static int make_header_ex(int is_diff, uint8_t *pubkey, uint32_t pubkey_sz, if (read_sz > 128) read_sz = 128; io_sz = (int)fread(buf, 1, read_sz, f); - if ((io_sz < 0) && !feof(f)) { + if (io_sz != (int)read_sz) { ret = -1; break; } From 96f507e0f9a6c9e21c5c19f2df289224d8eae321 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 01:48:41 +0200 Subject: [PATCH 03/26] F-7058: bound-check positional and option arguments argv[argc] is NULL, so a missing trailing argument (e.g. 'sign --ecc256 img.bin key.der') yielded a NULL fw_version that crashed strtoul, and --id/--encrypt/--delta/--policy as the last token dereferenced NULL directly. Guard the four value-taking options like --cert-chain already does, and validate the positional argument count against the selected signing mode before indexing. --- tools/keytools/sign.c | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index 402fa99af8..d87dbb39ed 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -3358,6 +3358,8 @@ int main(int argc, char** argv) { int ret = 0; int i; + int pos_args; + int need; char* tmpstr; const char* sign_str = "AUTO"; const char* hash_str = "SHA256"; @@ -3582,6 +3584,10 @@ int main(int argc, char** argv) CMD.header_only = 1; } else if (strcmp(argv[i], "--id") == 0) { + if (argc <= (i + 1)) { + fprintf(stderr, "Missing --id argument\n"); + exit(16); + } long id = strtol(argv[++i], NULL, 10); if ((id < 0 || id > 15) || ((id == 0) && (argv[i][0] != '0'))) { fprintf(stderr, "Invalid partition id: %s\n", argv[i]); @@ -3598,6 +3604,10 @@ int main(int argc, char** argv) CMD.manual_sign = 1; } else if (strcmp(argv[i], "--encrypt") == 0) { + if (argc <= (i + 1)) { + fprintf(stderr, "Missing --encrypt key file argument\n"); + exit(16); + } if (CMD.encrypt == ENC_OFF) CMD.encrypt = ENC_CHACHA; CMD.encrypt_key_file = argv[++i]; @@ -3612,6 +3622,10 @@ int main(int argc, char** argv) CMD.encrypt = ENC_CHACHA; } else if (strcmp(argv[i], "--delta") == 0) { + if (argc <= (i + 1)) { + fprintf(stderr, "Missing --delta base file argument\n"); + exit(16); + } CMD.delta = 1; CMD.delta_base_file = argv[++i]; } else if (strcmp(argv[i], "--no-base-sha") == 0) { @@ -3621,6 +3635,10 @@ int main(int argc, char** argv) CMD.no_ts = 1; } else if (strcmp(argv[i], "--policy") == 0) { + if (argc <= (i + 1)) { + fprintf(stderr, "Missing --policy file argument\n"); + exit(16); + } CMD.policy_sign = 1; CMD.policy_file = argv[++i]; } @@ -3897,6 +3915,24 @@ int main(int argc, char** argv) CMD.secondary_signature_sz = 0; } + /* Validate the positional argument count for the selected mode: image + + * version, plus key (and secondary key when hybrid) when signing, plus + * the precomputed signature file with --manual-sign. */ + pos_args = argc - (i + 1); + need = 2; /* image file + version */ + if (CMD.sign != NO_SIGN) { + need += 1; /* key file */ + if (CMD.hybrid) + need += 1; /* secondary key file */ + if (CMD.manual_sign) + need += 1; /* precomputed signature file */ + } + if (pos_args < need) { + fprintf(stderr, "Missing positional arguments: need %d, got %d " + "(image key version)\n", need, pos_args); + exit(1); + } + if (CMD.sign != NO_SIGN) { if (CMD.hybrid) { From 16ab1e086f1645cfece56cbd8a29e5dca18ade16 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 01:48:41 +0200 Subject: [PATCH 04/26] F-7057: use the size of the destination buffer The snprintf building output_diff_file passed sizeof(CMD.output_image_file); both are char[PATH_MAX] today, so the guarantee was accidental. Use sizeof of the destination. --- tools/keytools/sign.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index d87dbb39ed..39573d7af3 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -4013,7 +4013,7 @@ int main(int argc, char** argv) } if (CMD.delta) { printf("Delta Base file: %s\n", CMD.delta_base_file); - snprintf(CMD.output_diff_file, sizeof(CMD.output_image_file), + snprintf(CMD.output_diff_file, sizeof(CMD.output_diff_file), "%s_v%s_signed_diff.bin", (char*)buf, CMD.fw_version); snprintf(CMD.output_encrypted_image_file, From 53f1d4e056db2b50915fe3a4655d3e0a0ac16516 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 01:48:54 +0200 Subject: [PATCH 05/26] F-7060: fill the swap area with erased bytes, not the update flags The loop wrote the 5-byte 'pBOOT' trailer SWAP_SIZE times, emitting 5x SWAP_SIZE bytes past the 4KB swap region (the file grew 16KB past the mmap'd device) and leaving the swap filled with 'pBOOT' instead of the erased 0xFF state real flash has. Use the 0xFF pad loop the non-WOLF path already uses. --- tools/uart-flash-server/ufserver.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/uart-flash-server/ufserver.c b/tools/uart-flash-server/ufserver.c index 0c47a6db58..f8aa9377fa 100644 --- a/tools/uart-flash-server/ufserver.c +++ b/tools/uart-flash-server/ufserver.c @@ -217,11 +217,12 @@ uint8_t *mmap_firmware(const char *fname) valid_update = 0; } else { int i; + uint8_t pad = 0xFF; const char update_flags[] = "pBOOT"; lseek(fd, FIRMWARE_PARTITION_SIZE - 5, SEEK_SET); write(fd, update_flags, 5); for (i = 0; i < SWAP_SIZE; i++) - write(fd, update_flags, 5); + write(fd, &pad, 1); } base_fw = mmap(NULL, FIRMWARE_PARTITION_SIZE + SWAP_SIZE, (PROT_READ | PROT_WRITE), MAP_SHARED, fd, 0); From 2f9a8ea15dcdebd6b0eb9f28fa47a3df8e7db85e Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 01:48:54 +0200 Subject: [PATCH 06/26] F-7064: give the erase-completion ACK its long timeout ERASE_TIMEOUT was defined but never used; the final erase ACK used the plain short WAIT_CYCLES budget, so a slow remote flash made ext_flash_erase() fail even when the erase succeeded. Factor wait_ack_cycles() out of wait_ack() and use WAIT_CYCLES * ERASE_TIMEOUT for the erase ACK, like uart_rx_timeout() does with READ_TIMEOUT. Add unit tests for the success and timeout paths. --- src/uart_flash.c | 13 ++++++++---- tools/unit-tests/unit-uart-flash.c | 33 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/uart_flash.c b/src/uart_flash.c index 3623c9c9eb..7e7b3b98f8 100644 --- a/src/uart_flash.c +++ b/src/uart_flash.c @@ -47,17 +47,22 @@ int uart_tx(const uint8_t c); int uart_rx(uint8_t *c); -static int wait_ack(void) +static int wait_ack_cycles(int cycles) { + uint8_t c; volatile int count = 0; - while(++count < WAIT_CYCLES) { - uint8_t c; + while(++count < cycles) { if ((uart_rx(&c) == 1) && (c == CMD_ACK)) return 0; } return -1; } +static int wait_ack(void) +{ + return wait_ack_cycles(WAIT_CYCLES); +} + static int uart_rx_timeout(uint8_t *c) { volatile int count = 0; @@ -144,7 +149,7 @@ int ext_flash_erase(uintptr_t address, int len) return -1; } /* Wait for extra ack at the end of Erase */ - if (wait_ack() == 0) + if (wait_ack_cycles(WAIT_CYCLES * ERASE_TIMEOUT) == 0) return 0; return -1; } diff --git a/tools/unit-tests/unit-uart-flash.c b/tools/unit-tests/unit-uart-flash.c index c26e11e128..51b75b1c34 100644 --- a/tools/unit-tests/unit-uart-flash.c +++ b/tools/unit-tests/unit-uart-flash.c @@ -69,12 +69,45 @@ START_TEST(test_ext_flash_read_timeout_returns_error) } END_TEST +START_TEST(test_ext_flash_erase_success) +{ + uint8_t script[11]; + int ret; + + /* 10 command ACKs + the erase-completion ACK */ + memset(script, CMD_ACK, sizeof(script)); + reset_uart_script(script, sizeof(script)); + + ret = ext_flash_erase(0x1000, 0x1000); + + ck_assert_int_eq(ret, 0); + ck_assert_int_eq(rx_script_pos, 11); +} +END_TEST + +START_TEST(test_ext_flash_erase_timeout_returns_error) +{ + uint8_t script[10]; + int ret; + + /* Command ACKs only: the erase-completion ACK never arrives */ + memset(script, CMD_ACK, sizeof(script)); + reset_uart_script(script, sizeof(script)); + + ret = ext_flash_erase(0x1000, 0x1000); + + ck_assert_int_eq(ret, -1); +} +END_TEST + Suite *wolfboot_suite(void) { Suite *s = suite_create("wolfBoot"); TCase *uart_flash = tcase_create("UART flash"); tcase_add_test(uart_flash, test_ext_flash_read_timeout_returns_error); + tcase_add_test(uart_flash, test_ext_flash_erase_success); + tcase_add_test(uart_flash, test_ext_flash_erase_timeout_returns_error); tcase_set_timeout(uart_flash, 20); suite_add_tcase(s, uart_flash); From 769c3885f0099f1f15e165f7919c244bd9561a3d Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 01:48:54 +0200 Subject: [PATCH 07/26] Remove the obsolete python keytools, convert remaining users to C sign.py and keygen.py are superseded by the C tools/keytools/sign and keygen (built by the same Makefile, no python/wolfcrypt-py dependency). Rewrite unit-sign-delta-tlv.py to sign a real delta image with the C sign tool instead of the python bmdiff/sign.py pipeline, and update README.md and include/delta.h references. --- README.md | 46 +- include/delta.h | 2 +- tools/keytools/keygen.py | 399 ----------- tools/keytools/sign.py | 837 ------------------------ tools/unit-tests/unit-sign-delta-tlv.py | 83 +-- 5 files changed, 42 insertions(+), 1325 deletions(-) delete mode 100644 tools/keytools/keygen.py delete mode 100755 tools/keytools/sign.py diff --git a/README.md b/README.md index 27a8db953d..d7e0f41dfa 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Design based on [RFC 9019](https://datatracker.ietf.org/doc/rfc9019/) - A Firmwa This repository contains the following components: - the wolfBoot bootloader - - key generator and image signing tools (requires python 3.x and wolfcrypt-py https://github.com/wolfSSL/wolfcrypt-py) + - key generator and image signing tools - Baremetal test applications ### wolfBoot bootloader @@ -63,17 +63,17 @@ Additional examples available on our GitHub wolfBoot-examples repository [here]( The following steps are automated in the default `Makefile` target, using the baremetal test application as an example to create the factory image. By running `make`, the build system will: - - Create a Ed25519 Key-pair using the `ed25519_keygen` tool + - Create a Ed25519 Key-pair using the `keygen` tool - Compile the bootloader. The public key generated in the step above is included in the build - Compile the firmware image from the test application in [test\_app](test-app/) - Re-link the firmware to change the entry-point to the start address of the primary partition - - Sign the firmware image using the `ed25519_sign` tool + - Sign the firmware image using the `sign` tool - Create a factory image by concatenating the bootloader and the firmware image The factory image can be flashed to the target device. It contains the bootloader and the signed initial firmware at the specified address on the flash. -The `sign.py` tool transforms a bootable firmware image to comply with the firmware image format required by the bootloader. +The `sign` tool transforms a bootable firmware image to comply with the firmware image format required by the bootloader. For detailed information about the firmware image format, see [Firmware image](docs/firmware_image.md) @@ -82,7 +82,7 @@ For detailed information about the configuration options for the target system, ### Upgrading the firmware - Compile the new firmware image, and link it so that its entry point is at the start address of the primary partition - - Sign the firmware using the `sign.py` tool and the private key generated for the factory image + - Sign the firmware using the `sign` tool and the private key generated for the factory image - Transfer the image using a secure connection, and store it to the secondary firmware slot - Trigger the image swap using libwolfboot `wolfBoot_update_trigger()` function. See [wolfBoot library API](docs/API.md) for a description of the operation - Reboot to let the bootloader begin the image swap @@ -171,45 +171,13 @@ guidance and worked SBOM examples, see the ## Troubleshooting -1. Python errors when signing a key: - -``` -Traceback (most recent call last): - File "tools/keytools/keygen.py", line 135, in - rsa = ciphers.RsaPrivate.make_key(2048) -AttributeError: type object 'RsaPrivate' has no attribute 'make_key' -``` - -``` -Traceback (most recent call last): - File "tools/keytools/sign.py", line 189, in - r, s = ecc.sign_raw(digest) -AttributeError: 'EccPrivate' object has no attribute 'sign_raw' -``` - -You need to install the latest wolfcrypt-py here: https://github.com/wolfSSL/wolfcrypt-py - -Use `pip3 install wolfcrypt`. - -Or to install based on a local wolfSSL installation use: - -```sh -cd wolfssl -./configure --enable-keygen --enable-rsa --enable-ecc --enable-ed25519 --enable-des3 CFLAGS="-DFP_MAX_BITS=8192 -DWOLFSSL_PUBLIC_MP" -make -sudo make install - -cd wolfcrypt-py -USE_LOCAL_WOLFSSL=/usr/local pip3 install . -``` - -2. Key algorithm mismatch: +1. Key algorithm mismatch: The error `Key algorithm mismatch. Remove old keys via 'make keysclean'` indicates the current `.config` `SIGN` algorithm does not match what is in the generated `src/keystore.c` file. Use `make keysclean` to delete keys and regenerate. -3. Cannot open compiler generated file ... Permission denied +2. Cannot open compiler generated file ... Permission denied This may occur due to multiple environments being opened concurrently, or anti-virus software. Try manually deleting the respective build directories and/or restarting your IDE. diff --git a/include/delta.h b/include/delta.h index 9066cc8d5a..1625cbe566 100644 --- a/include/delta.h +++ b/include/delta.h @@ -9,7 +9,7 @@ * * Compile with DELTA_UPDATES=1 * - * Use tools/sign.py or tool/sign.c on the host to provide small + * Use the sign tool (tools/keytools/sign.c) on the host to provide small * secure update packages containing only binary difference, using the * --delta option. * diff --git a/tools/keytools/keygen.py b/tools/keytools/keygen.py deleted file mode 100644 index fc250d77c3..0000000000 --- a/tools/keytools/keygen.py +++ /dev/null @@ -1,399 +0,0 @@ -#!/usr/bin/python3 -''' - * keygen.py - * - * 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. - * - * 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 -''' - -import sys,os,struct -from wolfcrypt import ciphers - -AUTH_KEY_ED25519 = 0x01 -AUTH_KEY_ECC256 = 0x02 -AUTH_KEY_RSA2048 = 0x03 -AUTH_KEY_RSA4096 = 0x04 -AUTH_KEY_ED448 = 0x05 -AUTH_KEY_ECC384 = 0x06 -AUTH_KEY_ECC521 = 0x07 -AUTH_KEY_RSA3072 = 0x08 - -#default sign algorithm value -sign="ed25519" - - -def usage(): - print("Usage: %s [--ed25519 | --ed448 | --ecc256 | --ecc384 | --ecc521 | --rsa2048| --rsa3072 | --rsa4096] [ --force ] [-i pubkey0.der [-i pubkey1.der -i pubkey2.der ... -i pubkeyN.der]] [-i pubkey0.der [-i pubkey1.der -i pubkey2.der ... -i pubkeyN.der] [-keystoreDir dir]]n" % sys.argv[0]) - parser.print_help() - sys.exit(1) - -def dupsign(): - print("") - print("Error: only one algorithm must be specified.") - print("") - usage() - -def sign_key_type(name): - if name == 'ed25519': - return 'AUTH_KEY_ED25519' - elif name == 'ed448': - return 'AUTH_KEY_ED448' - elif name == 'ecc256': - return 'AUTH_KEY_ECC256' - elif name == 'ecc384': - return 'AUTH_KEY_ECC384' - elif name == 'ecc521': - return 'AUTH_KEY_ECC521' - elif name == 'rsa2048': - return 'AUTH_KEY_RSA2048' - elif name == 'rsa3072': - return 'AUTH_KEY_RSA3072' - elif name == 'rsa4096': - return 'AUTH_KEY_RSA4096' - else: - return 0 - -def sign_key_size(name): - if name == 'ed25519': - return 'KEYSTORE_PUBKEY_SIZE_ED25519' - elif name == 'ed448': - return 'KEYSTORE_PUBKEY_SIZE_ED448' - elif name == 'ecc256': - return 'KEYSTORE_PUBKEY_SIZE_ECC256' - elif name == 'ecc384': - return 'KEYSTORE_PUBKEY_SIZE_ECC384' - elif name == 'ecc521': - return 'KEYSTORE_PUBKEY_SIZE_ECC521' - elif name == 'rsa2048': - return 'KEYSTORE_PUBKEY_SIZE_RSA2048' - elif name == 'rsa3072': - return 'KEYSTORE_PUBKEY_SIZE_RSA3072' - elif name == 'rsa4096': - return 'KEYSTORE_PUBKEY_SIZE_RSA4096' - else: - return 0 - -def sign_key_size_literal(name): - if name == 'ed25519': - return 32 - elif name == 'ed448': - return 57 - elif name == 'ecc256': - return 64 - elif name == 'ecc384': - return 96 - elif name == 'ecc521': - return 132 - elif name == 'rsa2048': - return 320 - elif name == 'rsa3072': - return 448 - elif name == 'rsa4096': - return 576 - else: - return 0 - -def keystore_add(slot, pub, sz = 0): - ktype = sign_key_type(sign) - if (sz == 0): - ksize = sign_key_size(sign) - else: - ksize = str(sz) - pfile.write(Slot_hdr % (key_file, slot, ktype, ksize)) - i = 0 - for c in bytes(pub[0:-1]): - pfile.write("0x%02X, " % c) - i += 1 - if (i % 8 == 0): - pfile.write('\n\t\t\t') - pfile.write("0x%02X" % pub[-1]) - pfile.write(Pubkey_footer) - pfile.write(Slot_footer) - t = 0x8A8A8A8A - m = 0xFFFFFFFF - ks_struct = struct.pack("\n#include \"wolfboot/wolfboot.h\"\n#include \"keystore.h\"\n" \ - "#ifdef WOLFBOOT_NO_SIGN\n\t#define NUM_PUBKEYS 0\n#else\n\n" \ - "#if !defined(KEYSTORE_ANY) && (KEYSTORE_PUBKEY_SIZE != KEYSTORE_PUBKEY_SIZE_%s)\n\t" \ - "#error Key algorithm mismatch. Remove old keys via 'make keysclean'\n" \ - "#else\n" - - -Store_hdr = "#define NUM_PUBKEYS %d\nconst struct keystore_slot PubKeys[NUM_PUBKEYS] = {\n\n" -Slot_hdr = "\t /* Key associated to file '%s' */\n" -Slot_hdr += "\t{\n\t\t.slot_id = %d,\n\t\t.key_type = %s,\n" -Slot_hdr += "\t\t.part_id_mask = KEY_VERIFY_ALL,\n\t\t.pubkey_size = %s,\n" -Slot_hdr += "\t\t.pubkey = {\n\t\t\t" -Pubkey_footer = "\n\t\t}," -Slot_footer = "\n\t},\n\n" -Store_footer = '\n};\n\n' - -Keystore_API = "int keystore_num_pubkeys(void)\n" -Keystore_API += "{\n" -Keystore_API += " return NUM_PUBKEYS;\n" -Keystore_API += "}\n\n" -Keystore_API += "uint8_t *keystore_get_buffer(int id)\n" -Keystore_API += "{\n" -Keystore_API += " if (id >= keystore_num_pubkeys())\n" -Keystore_API += " return (uint8_t *)0;\n" -Keystore_API += " return (uint8_t *)PubKeys[id].pubkey;\n" -Keystore_API += "}\n\n" -Keystore_API += "int keystore_get_size(int id)\n" -Keystore_API += "{\n" -Keystore_API += " if (id >= keystore_num_pubkeys())\n" -Keystore_API += " return -1;\n" -Keystore_API += " return (int)PubKeys[id].pubkey_size;\n" -Keystore_API += "}\n\n" -Keystore_API += "uint32_t keystore_get_mask(int id)\n" -Keystore_API += "{\n" -Keystore_API += " if (id >= keystore_num_pubkeys())\n" -Keystore_API += " return -1;\n" -Keystore_API += " return PubKeys[id].part_id_mask;\n" -Keystore_API += "}\n\n" -Keystore_API += "#endif /* Keystore public key size check */\n" -Keystore_API += "#endif /* WOLFBOOT_NO_SIGN */\n" - - -import argparse as ap - -parser = ap.ArgumentParser(prog='keygen.py', description='wolfBoot key generation tool') -parser.add_argument('--ed25519', dest='ed25519', action='store_true') -parser.add_argument('--ed448', dest='ed448', action='store_true') -parser.add_argument('--ecc256', dest='ecc256', action='store_true') -parser.add_argument('--ecc384', dest='ecc384', action='store_true') -parser.add_argument('--ecc521', dest='ecc521', action='store_true') -parser.add_argument('--rsa2048', dest='rsa2048', action='store_true') -parser.add_argument('--rsa3072', dest='rsa3072', action='store_true') -parser.add_argument('--rsa4096', dest='rsa4096', action='store_true') -parser.add_argument('--force', dest='force', action='store_true') -parser.add_argument('-i', dest='pubfile', nargs='+', action='extend') -parser.add_argument('-g', dest='keyfile', nargs='+', action='extend') -parser.add_argument('-keystoreDir', dest='storeDir', nargs='+', action='extend') - -print(" *** WARNING ***") -print("Python key tools are now deprecated") -print("and will be removed in future versions.") -print("Please ensure that your scripts are using") -print("the compiled C version of these tools") -print("(e.g. by running 'make keytools').") -print(" *** ******* ***") -print("") - -args=parser.parse_args() - -if (type(args.storeDir) == list): - pubkey_cfile = "".join(args.storeDir)+"/keystore.c" - keystore_imgfile = "".join(args.storeDir)+"/keystore.der" -else: - pubkey_cfile = "src/keystore.c" - keystore_imgfile = "keystore.der" - -key_files = args.keyfile -pubkey_files = args.pubfile - -if pubkey_files == None: - pubkey_files = [] - -if key_files == None: - key_files = [] - -print("keys to import:") -print(pubkey_files) -print("keys to generate:") -print(key_files) - - -sign=None -force=False -if (args.ed25519): - sign='ed25519' -if (args.ed448): - if sign is not None: - dupsign() - sign='ed448' -if (args.ecc256): - if sign is not None: - dupsign() - sign='ecc256' -if (args.ecc384): - if sign is not None: - dupsign() - sign='ecc384' -if (args.ecc521): - if sign is not None: - dupsign() - sign='ecc521' - print("ecc521 keys are not yet supported!") - sys.exit(1) -if (args.rsa2048): - if sign is not None: - dupsign() - sign='rsa2048' -if (args.rsa3072): - if sign is not None: - dupsign() - sign='rsa3072' -if (args.rsa4096): - if sign is not None: - dupsign() - sign='rsa4096' - -if sign is None: - usage() - -force = args.force - - -if pubkey_cfile[-2:] != '.c': - print("** Warning: generated public key cfile does not have a '.c' extension") - -# Create/open public key c file -print ("Output C file: " + pubkey_cfile) -pfile = open(pubkey_cfile, "w") -pfile.write(Cfile_Banner % sign.upper()) -pfile.write(Store_hdr % (len(key_files) + len(pubkey_files))) -ksfile = open(keystore_imgfile, "wb") - -pub_slot_index = 0 - - -if pubkey_files != None: - for pub_slot_index, key_file in enumerate(pubkey_files): - print ("Public key slot: " + str(pub_slot_index)) - print ("Selected cipher: " + sign) - print ("Input public key: " + key_file) - with open(key_file, 'rb') as f: - key = f.read(4096) - # if it's an ecc key and it's length is longer than the raw key we - # need to parse it - if (sign == 'ecc256' or sign == 'ecc384' or sign == 'ecc521') and len(key) > sign_key_size_literal(sign): - eccKey = ciphers.EccPublic(key) - key = eccKey.encode_key_raw() - key = key[0] + key[1] - keystore_add(pub_slot_index, key) - pub_slot_index = len(pubkey_files) - -for slot_index_off, key_file in enumerate(key_files): - slot_index = slot_index_off + pub_slot_index - print ("Public key slot: " + str(slot_index)) - print ("Selected cipher: " + sign) - print ("Output Private key: " + key_file) - print() - if os.path.exists(key_file) and not force: - choice = input("** Warning: key file already exist! Are you sure you want to "+ - "generate a new key and overwrite the existing key? [Type 'Yes']: ") - if (choice != "Yes"): - print("Operation canceled.") - sys.exit(2) - - if (sign == "ed25519"): - ed = ciphers.Ed25519Private.make_key(32) - priv,pub = ed.encode_key() - - print() - print("Creating file " + key_file) - with open(key_file, "wb") as f: - f.write(priv) - f.write(pub) - f.close() - keystore_add(slot_index, pub) - - if (sign == "ed448"): - ed = ciphers.Ed448Private.make_key(57) - priv,pub = ed.encode_key() - print() - print("Creating file " + key_file) - with open(key_file, "wb") as f: - f.write(priv) - f.write(pub) - f.close() - keystore_add(slot_index, pub) - - if (sign[0:3] == 'ecc'): - if (sign == "ecc256"): - ec = ciphers.EccPrivate.make_key(32) - ecc_pub_key_len = 64 - qx,qy,d = ec.encode_key_raw() - - if (sign == "ecc384"): - ec = ciphers.EccPrivate.make_key(48) - ecc_pub_key_len = 96 - qx,qy,d = ec.encode_key_raw() - - if (sign == "ecc521"): - ec = ciphers.EccPrivate.make_key(66) - ecc_pub_key_len = 132 - qx,qy,d = ec.encode_key_raw() - print() - print("Creating file " + key_file) - keystore_add(slot_index, bytes(qx) + bytes(qy)) - with open(key_file, "wb") as f: - f.write(qx) - f.write(qy) - f.write(d) - f.close() - - if (sign == "rsa2048"): - rsa = ciphers.RsaPrivate.make_key(2048) - priv,pub = rsa.encode_key() - print() - print("Creating file " + key_file) - with open(key_file, "wb") as f: - f.write(priv) - f.close() - print("Creating file " + pubkey_cfile) - keystore_add(slot_index, pub, len(pub)) - - if (sign == "rsa3072"): - rsa = ciphers.RsaPrivate.make_key(3072) - priv,pub = rsa.encode_key() - print() - print("Creating file " + key_file) - with open(key_file, "wb") as f: - f.write(priv) - f.close() - keystore_add(slot_index, pub, len(pub)) - - if (sign == "rsa4096"): - rsa = ciphers.RsaPrivate.make_key(4096) - if os.path.exists(key_file) and not force: - choice = input("** Warning: key file already exist! Are you sure you want to "+ - "generate a new key and overwrite the existing key? [Type 'Yes']: ") - if (choice != "Yes"): - print("Operation canceled.") - sys.exit(2) - priv,pub = rsa.encode_key() - print() - print("Creating file " + key_file) - with open(key_file, "wb") as f: - f.write(priv) - f.close() - keystore_add(slot_index, pub, len(pub)) - -pfile.write(Store_footer) -pfile.write(Keystore_API) -pfile.close() diff --git a/tools/keytools/sign.py b/tools/keytools/sign.py deleted file mode 100755 index 75d57a100d..0000000000 --- a/tools/keytools/sign.py +++ /dev/null @@ -1,837 +0,0 @@ -#!/usr/bin/python3 -''' - * sign.py - * - * 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. - * - * 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 -''' - -import sys, os, struct, time, re - -try: - import wolfcrypt -except: - print ("No wolfcrypt support found. Try 'pip install wolfcrypt'") - sys.exit(1) - -from wolfcrypt import ciphers, hashes - - -WOLFBOOT_MAGIC = 0x464C4F57 -HDR_END = 0x00 -HDR_VERSION = 0x01 -HDR_TIMESTAMP = 0x02 -HDR_SHA256 = 0x03 -HDR_IMG_DELTA_BASE = 0x05 -HDR_IMG_DELTA_SIZE = 0x06 -HDR_SHA3_384 = 0x13 -HDR_SHA384 = 0x14 -HDR_IMG_DELTA_INVERSE = 0x15 -HDR_IMG_DELTA_INVERSE_SIZE = 0x16 -HDR_IMG_TYPE = 0x04 -HDR_PUBKEY = 0x10 -HDR_SIGNATURE = 0x20 -HDR_PADDING = 0xFF - - -HDR_VERSION_LEN = 4 -HDR_TIMESTAMP_LEN = 8 -HDR_SHA256_LEN = 32 -HDR_SHA384_LEN = 48 -HDR_SHA3_384_LEN = 48 -HDR_IMG_TYPE_LEN = 2 -HDR_SIGNATURE_LEN = 64 - -HDR_IMG_TYPE_AUTH_NONE = 0xFF00 -HDR_IMG_TYPE_AUTH_ED25519 = 0x0100 -HDR_IMG_TYPE_AUTH_ECC256 = 0x0200 -HDR_IMG_TYPE_AUTH_RSA2048 = 0x0300 -HDR_IMG_TYPE_AUTH_RSA4096 = 0x0400 -HDR_IMG_TYPE_AUTH_ED448 = 0x0500 -HDR_IMG_TYPE_AUTH_ECC384 = 0x0600 -HDR_IMG_TYPE_AUTH_ECC521 = 0x0700 -HDR_IMG_TYPE_AUTH_RSA3072 = 0x0800 -HDR_IMG_TYPE_DIFF = 0x00D0 - -HDR_IMG_TYPE_WOLFBOOT = 0x0000 -HDR_IMG_TYPE_APP = 0x0001 - -WOLFBOOT_HEADER_SIZE = 256 -WOLFBOOT_PARTITION_SIZE = 0 -WOLFBOOT_SECTOR_SIZE = 0 - -sign="auto" -self_update=False -sha_only=False -manual_sign=False -encrypt=False -chacha=True -aes128=False -aes256=False -delta=False -encrypt_key_file=None -delta_base_file=None -partition_id = HDR_IMG_TYPE_APP - - -argc = len(sys.argv) -argv = sys.argv -hash_algo='sha256' - - -def make_header(image_file, fw_version, extra_fields=[]): - img_size = os.path.getsize(image_file) - # Magic header (spells 'WOLF') - header = struct.pack(' 0): - img_type |= HDR_IMG_TYPE_DIFF - - header += struct.pack(' 12): - print("Usage: "+argv[0]+" [options] image key version"); - print("For full usage manual, see 'docs/Signing.md'"); - sys.exit(1) - -i = 1 -while (i < len(argv)): - if (argv[i] == '--no-sign'): - sign='none' - elif (argv[i] == '--ed25519'): - sign='ed25519' - elif (argv[i] == '--ed448'): - sign='ed448' - elif (argv[i] == '--ecc256'): - sign='ecc256' - elif (argv[i] == '--ecc384'): - sign='ecc384' - elif (argv[i] == '--ecc521'): - sign='ecc521' - elif (argv[i] == '--rsa2048'): - sign='rsa2048' - elif (argv[i] == '--rsa3072'): - sign='rsa3072' - elif (argv[i] == '--rsa4096'): - sign='rsa4096' - elif (argv[i] == '--sha256'): - hash_algo='sha256' - elif (argv[i] == '--sha384'): - hash_algo='sha384' - elif (argv[i] == '--sha3'): - hash_algo='sha3' - elif (argv[i] == '--wolfboot-update'): - self_update = True - partition_id = HDR_IMG_TYPE_WOLFBOOT - elif (argv[i] == '--id'): - i+=1 - partition_id = int(argv[i]) - if partition_id < 0 or partition_id > 15: - print("Invalid partition id: " + argv[i]) - sys.exit(16) - if partition_id == 0: - self_update = True - elif (argv[i] == '--sha-only'): - sha_only = True - elif (argv[i] == '--manual-sign'): - manual_sign = True - elif (argv[i] == '--encrypt'): - encrypt = True - i += 1 - encrypt_key_file = argv[i] - elif (argv[i] == '--chacha'): - encrypt = True - elif (argv[i] == '--aes128'): - encrypt = True - chacha = False - aes128 = True - elif (argv[i] == '--aes256'): - encrypt = True - chacha = False - aes256 = True - elif (argv[i] == '--delta'): - delta = True - i += 1 - delta_base_file = argv[i] - else: - i-=1 - break - i += 1 - - -if (encrypt and delta): - print("Encryption of delta image") - -try: - cfile = open(".config", "r") -except: - cfile = None - pass - -if cfile: - l = cfile.readline() - while l != '': - if "IMAGE_HEADER_SIZE" in l: - val=l.split('=')[1].rstrip('\n') - WOLFBOOT_HEADER_SIZE = int(val,0) - print("IMAGE_HEADER_SIZE (from .config): " + str(WOLFBOOT_HEADER_SIZE)) - if "WOLFBOOT_PARTITION_SIZE" in l and "ADDRESS" not in l: - val=l.split('=')[1].rstrip('\n') - WOLFBOOT_PARTITION_SIZE = int(val,0) - if "WOLFBOOT_SECTOR_SIZE" in l: - val=l.split('=')[1].rstrip('\n') - WOLFBOOT_SECTOR_SIZE = int(val,0) - - l = cfile.readline() - cfile.close() - - -image_file = argv[i+1] -if sign != 'none': - key_file = argv[i+2] - fw_version = int(argv[i+3]) -else: - key_file = '' - fw_version = int(argv[i+2]) - -if manual_sign: - signature_file = argv[i+4] - -if not sha_only: - if '.' in image_file: - tokens = image_file.split('.') - output_image_file = '' - for x in tokens[0:-1]: - output_image_file+=x - output_image_file += "_v" + str(fw_version) + "_signed.bin" - else: - output_image_file = image_file + "_v" + str(fw_version) + "_signed.bin" -else: - if '.' in image_file: - tokens = image_file.split('.') - output_image_file = '' - for x in tokens[0:-1]: - output_image_file+=x - output_image_file += "_v" + str(fw_version) + "_digest.bin" - else: - output_image_file = image_file + "_v" + str(fw_version) + "_digest.bin" - -if delta and encrypt: - if '.' in image_file: - tokens = image_file.split('.') - encrypted_output_image_file = '' - for x in tokens[0:-1]: - encrypted_output_image_file += x - encrypted_output_image_file += "_v" + str(fw_version) + "_signed_diff_encrypted.bin" - else: - encrypted_output_image_file = image_file + "_v" + str(fw_version) + "_signed_diff_encrypted.bin" - -elif encrypt: - if '.' in image_file: - tokens = image_file.split('.') - encrypted_output_image_file = '' - for x in tokens[0:-1]: - encrypted_output_image_file += x - encrypted_output_image_file += "_v" + str(fw_version) + "_signed_and_encrypted.bin" - else: - encrypted_output_image_file = image_file + "_v" + str(fw_version) + "_signed_and_encrypted.bin" - -if delta: - if '.' in image_file: - tokens = image_file.split('.') - delta_output_image_file = '' - for x in tokens[0:-1]: - delta_output_image_file += x - delta_output_image_file += "_v" + str(fw_version) + "_signed_diff.bin" - else: - delta_output_image_file = image_file + "_v" + str(fw_version) + "_signed_diff.bin" - -if (self_update): - print("Update type: wolfBoot") -else: - print("Update type: Firmware") - -print ("Input image: " + image_file) - -print ("Selected cipher: " + sign) -print ("Private key: " + key_file) - -if not sha_only: - print ("Output image: " + output_image_file) -else: - print ("Output digest: " + output_image_file) - -if not encrypt: - print ("Not Encrypted") -else: - print ("Encrypted using: " + encrypt_key_file) -nickname = "" -if partition_id == 0: - nickname = "(bootloader)" -print ("Target partition id: " + str(partition_id) +" "+ nickname) - -if sign == 'none': - kf = None - wolfboot_key_buffer='' - wolfboot_key_buffer_len = 0 -else: - kf = open(key_file, "rb") - wolfboot_key_buffer = kf.read(4096) - wolfboot_key_buffer_len = len(wolfboot_key_buffer) - -if wolfboot_key_buffer_len == 0: - if (sign != 'none'): - print("Error. Key size is zero but cipher is " + sign) - sys.exit(3) - print("*** WARNING: cipher 'none' selected.") - print("*** Image will not be authenticated!") - print("*** SECURE BOOT DISABLED.") - -elif wolfboot_key_buffer_len == 32: - if (sign != 'ed25519' and not manual_sign and not sha_only): - print("Error: key too short for cipher") - sys.exit(1) - elif sign == 'auto' and (manual_sign or sha_only): - sign = 'ed25519' - print("'ed25519' public key autodetected.") -elif wolfboot_key_buffer_len == 64: - if (sign == 'ecc256'): - if not manual_sign and not sha_only: - print("Error: key size does not match the cipher selected") - sys.exit(1) - else: - print("Ecc256 public key detected") - if sign == 'auto': - if (manual_sign or sha_only): - sign = 'ecc256' - print("'ecc256' public key autodetected.") - else: - sign = 'ed25519' - print("'ed25519' key autodetected.") -elif wolfboot_key_buffer_len == 114: - if (sign != 'ed448' and not manual_sign and not sha_only): - print("Error: key size incorrect for cipher") - sys.exit(1) - elif sign == 'auto' and (manual_sign or sha_only): - sign = 'ed448' - print("'ed448' public key autodetected.") -elif wolfboot_key_buffer_len == 96: - if (sign == 'ed25519'): - print("Error: key size does not match the cipher selected") - sys.exit(1) - if sign == 'auto': - sign = 'ecc256' - print("'ecc256' key autodetected.") -elif wolfboot_key_buffer_len == 144: - if (sign != 'auto' and sign != 'ecc384'): - print("Error: key size does not match the cipher selected") - sys.exit(1) - if sign == 'auto': - sign = 'ecc384' - print("'ecc384' key autodetected.") -elif wolfboot_key_buffer_len == 198: - if (sign != 'auto' and sign != 'ecc521'): - print("Error: key size does not match the cipher selected") - sys.exit(1) - if sign == 'auto': - sign = 'ecc521' - print("'ecc521' key autodetected.") -elif (wolfboot_key_buffer_len > 512): - if (sign == 'auto'): - sign = 'rsa4096' - print("'rsa4096' key autodetected.") -elif (wolfboot_key_buffer_len > 256): - if (sign == 'auto'): - sign = 'rsa3072' - print("'rsa3072' key autodetected.") -elif (wolfboot_key_buffer_len > 128): - if (sign == 'auto'): - sign = 'rsa2048' - print("'rsa2048' key autodetected.") - elif (sign != 'rsa2048'): - print ("Error: key size %d too large for the selected cipher" % wolfboot_key_buffer_len) -else: - if sign[0:3] == 'ecc': - # if this decode doesn't raise an error we have a valid ecc key - # public only - if manual_sign or sha_only: - tmpEcc = ciphers.EccPublic(wolfboot_key_buffer) - #private - else: - tmpEcc = ciphers.EccPrivate() - tmpEcc.decode_key(wolfboot_key_buffer) - else: - print ("Error: key size does not match any cipher") - sys.exit(2) - -if sign == 'none': - privkey = None - pubkey = None -elif not sha_only and not manual_sign: - ''' import (decode) private key for signing ''' - if sign == 'ed25519': - ed = ciphers.Ed25519Private(key = wolfboot_key_buffer) - privkey, pubkey = ed.encode_key() - - if sign == 'ed448': - HDR_SIGNATURE_LEN = 114 - if WOLFBOOT_HEADER_SIZE < 512: - print("Ed448: header size increased to 512") - WOLFBOOT_HEADER_SIZE = 512 - ed = ciphers.Ed448Private(key = wolfboot_key_buffer) - privkey, pubkey = ed.encode_key() - - if sign == 'ecc256': - ecc = ciphers.EccPrivate() - - if (wolfboot_key_buffer_len == 96): - ecc.decode_key_raw(wolfboot_key_buffer[0:32], - wolfboot_key_buffer[32:64], wolfboot_key_buffer[64:]) - pubkey = wolfboot_key_buffer[0:64] - else: - ecc.decode_key(wolfboot_key_buffer) - pubkey = ecc.encode_key_raw() - pubkey = pubkey[0] + pubkey[1] - - if sign == 'ecc384': - HDR_SIGNATURE_LEN = 96 - if WOLFBOOT_HEADER_SIZE < 512: - print("Ecc384: header size increased to 512") - WOLFBOOT_HEADER_SIZE = 512 - ecc = ciphers.EccPrivate() - - if (wolfboot_key_buffer_len == 144): - ecc.decode_key_raw(wolfboot_key_buffer[0:48], - wolfboot_key_buffer[48:96], wolfboot_key_buffer[96:], - curve_id = ciphers.ECC_SECP384R1) - pubkey = wolfboot_key_buffer[0:96] - else: - ecc.decode_key(wolfboot_key_buffer) - pubkey = ecc.encode_key_raw() - pubkey = pubkey[0] + pubkey[1] - - - if sign == 'ecc521': - HDR_SIGNATURE_LEN = 132 - - ecc = ciphers.EccPrivate() - - if (wolfboot_key_buffer_len == 198): - ecc.decode_key_raw(wolfboot_key_buffer[0:66], - wolfboot_key_buffer[66:132], wolfboot_key_buffer[132:], - curve_id = ciphers.ECC_SECP521R1) - pubkey = wolfboot_key_buffer[0:132] - else: - ecc.decode_key(wolfboot_key_buffer) - pubkey = ecc.encode_key_raw() - pubkey = pubkey[0] + pubkey[1] - - if WOLFBOOT_HEADER_SIZE < 512: - print("Ecc521: header size increased to 512") - WOLFBOOT_HEADER_SIZE = 512 - - if sign == 'rsa2048': - if WOLFBOOT_HEADER_SIZE < 512: - print("Rsa2048: header size increased to 512") - WOLFBOOT_HEADER_SIZE = 512 - HDR_SIGNATURE_LEN = 256 - rsa = ciphers.RsaPrivate(wolfboot_key_buffer) - privkey,pubkey = rsa.encode_key() - - if sign == 'rsa3072': - if hash_algo != 'sha256': - if WOLFBOOT_HEADER_SIZE < 1024: - print("Rsa3072: header size increased to 1024") - WOLFBOOT_HEADER_SIZE = 1024 - if WOLFBOOT_HEADER_SIZE < 512: - print("Rsa3072: header size increased to 512") - WOLFBOOT_HEADER_SIZE = 512 - HDR_SIGNATURE_LEN = 384 - rsa = ciphers.RsaPrivate(wolfboot_key_buffer) - privkey,pubkey = rsa.encode_key() - - if sign == 'rsa4096': - if WOLFBOOT_HEADER_SIZE < 1024: - print("Rsa4096: header size increased to 1024") - WOLFBOOT_HEADER_SIZE = 1024 - HDR_SIGNATURE_LEN = 512 - rsa = ciphers.RsaPrivate(wolfboot_key_buffer) - privkey,pubkey = rsa.encode_key() - -else: - if sign == 'rsa2048': - if WOLFBOOT_HEADER_SIZE < 512: - WOLFBOOT_HEADER_SIZE = 512 - HDR_SIGNATURE_LEN = 256 - if sign == 'rsa3072': - if WOLFBOOT_HEADER_SIZE < 512: - WOLFBOOT_HEADER_SIZE = 512 - HDR_SIGNATURE_LEN = 384 - if sign == 'rsa4096': - if WOLFBOOT_HEADER_SIZE < 1024: - WOLFBOOT_HEADER_SIZE = 1024 - HDR_SIGNATURE_LEN = 512 - - # if it's an ecc key, check if it is encoded - if (sign == 'ecc256' and wolfboot_key_buffer_len != 64) or (sign == 'ecc384' and wolfboot_key_buffer_len != 96) or (sign == 'ecc384' and wolfboot_key_buffer_len != 132): - eccKey = ciphers.EccPublic(wolfboot_key_buffer) - pubkey = eccKey.encode_key_raw() - pubkey = pubkey[0] + pubkey[1] - else: - pubkey = wolfboot_key_buffer - -header = make_header(image_file, fw_version) - -# Create output image. Add padded header in front -outfile = open(output_image_file, 'wb') -outfile.write(header) -sz = len(header) -while sz < WOLFBOOT_HEADER_SIZE: - outfile.write(struct.pack('B',HDR_PADDING)) - sz += 1 -infile = open(image_file, 'rb') -while True: - buf = infile.read(1024) - if len(buf) == 0: - break - outfile.write(buf) - -infile.close() -outfile.close() - -# Check if signed image fits in partition -if WOLFBOOT_PARTITION_SIZE > 0: - img_size = os.path.getsize(image_file) - total_img_sz = WOLFBOOT_HEADER_SIZE + img_size - # Only subtract sector for trailer when sector < partition. - # When sector >= partition (e.g. update_ram targets), the - # entire partition is available for the image. - if WOLFBOOT_SECTOR_SIZE < WOLFBOOT_PARTITION_SIZE: - max_img_sz = WOLFBOOT_PARTITION_SIZE - WOLFBOOT_SECTOR_SIZE - else: - max_img_sz = WOLFBOOT_PARTITION_SIZE - if total_img_sz > max_img_sz: - if WOLFBOOT_SECTOR_SIZE < WOLFBOOT_PARTITION_SIZE: - print("Error: Image size %d (header %d + firmware %d) " - "exceeds max %d (partition %d - sector %d)" % - (total_img_sz, WOLFBOOT_HEADER_SIZE, img_size, - max_img_sz, WOLFBOOT_PARTITION_SIZE, WOLFBOOT_SECTOR_SIZE)) - else: - print("Error: Image size %d (header %d + firmware %d) " - "exceeds max %d (partition %d)" % - (total_img_sz, WOLFBOOT_HEADER_SIZE, img_size, - max_img_sz, WOLFBOOT_PARTITION_SIZE)) - sys.exit(1) - -if (encrypt): - delta_align=64 -else: - delta_align=16 - -if (delta): - tmp_outfile='/tmp/delta.bin' - tmp_inv_outfile='/tmp/delta-1.bin' - os.system('tools/delta/bmdiff ' + delta_base_file + ' ' + output_image_file + ' ' + tmp_outfile) - os.system('tools/delta/bmdiff ' + output_image_file + ' ' + delta_base_file + ' ' + tmp_inv_outfile) - - delta_size = os.path.getsize(tmp_outfile) - delta_inv_size = os.path.getsize(tmp_inv_outfile) - delta_file = open(tmp_outfile, 'ab+') - delta_inv_file = open(tmp_inv_outfile, 'rb') - while delta_file.tell() % delta_align != 0: - delta_file.write(struct.pack('B', 0x00)) - inv_off = delta_file.tell() - while True: - cpbuf = delta_inv_file.read(1024) - if len(cpbuf) == 0: - break - delta_file.write(cpbuf) - delta_file.close() - delta_inv_file.close() - base_version = re.split("_", (re.split("_v", delta_base_file)[1]))[0] - header = make_header(tmp_outfile, fw_version, - [[HDR_IMG_DELTA_BASE, 4, struct.pack(" Date: Wed, 23 Sep 2026 02:46:15 +0200 Subject: [PATCH 08/26] F-6758: hal_flash_write: derive DW base from address+i in else branch The 64-bit partial write indexed a fixed base with i>>2, so an unaligned start crossing a double-word boundary programmed the wrong, misaligned DW pair. Adopt the stm32g4/stm32c0 form: recompute unit_addr = (address+i) & ~0x07 and use dst[0]/dst[1]. --- hal/stm32g0.c | 15 +++++++-------- hal/stm32l4.c | 15 +++++++-------- hal/stm32wb.c | 15 +++++++-------- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/hal/stm32g0.c b/hal/stm32g0.c index 8becc90f97..aea0fc8bc4 100644 --- a/hal/stm32g0.c +++ b/hal/stm32g0.c @@ -150,18 +150,17 @@ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len) flash_wait_complete(); i+=8; } else { + uint32_t unit_addr = (address + i) & (~0x07); + int off = (address + i) - unit_addr; uint32_t val[2]; uint8_t *vbytes = (uint8_t *)(val); - int off = (address + i) - (((address + i) >> 3) << 3); - uint32_t base_addr = address & (~0x07); /* aligned to 64 bit */ - int u32_idx = (i >> 2); - dst = (uint32_t *)(base_addr); - val[0] = dst[u32_idx]; - val[1] = dst[u32_idx + 1]; + dst = (uint32_t *)unit_addr; + val[0] = dst[0]; + val[1] = dst[1]; while ((off < 8) && (i < len)) vbytes[off++] = data[i++]; - dst[u32_idx] = val[0]; - dst[u32_idx + 1] = val[1]; + dst[0] = val[0]; + dst[1] = val[1]; flash_wait_complete(); } } diff --git a/hal/stm32l4.c b/hal/stm32l4.c index 84bcc85842..10bc6f2917 100644 --- a/hal/stm32l4.c +++ b/hal/stm32l4.c @@ -146,18 +146,17 @@ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len) flash_wait_complete(); i+=8; } else { + uint32_t unit_addr = (address + i) & (~0x07); + int off = (address + i) - unit_addr; uint32_t val[2]; uint8_t *vbytes = (uint8_t *)(val); - int off = (address + i) - (((address + i) >> 3) << 3); - uint32_t base_addr = address & (~0x07); /* aligned to 64 bit */ - int u32_idx = (i >> 2); - dst = (uint32_t *)(base_addr); - val[0] = dst[u32_idx]; - val[1] = dst[u32_idx + 1]; + dst = (uint32_t *)unit_addr; + val[0] = dst[0]; + val[1] = dst[1]; while ((off < 8) && (i < len)) vbytes[off++] = data[i++]; - dst[u32_idx] = val[0]; - dst[u32_idx + 1] = val[1]; + dst[0] = val[0]; + dst[1] = val[1]; flash_wait_complete(); } } diff --git a/hal/stm32wb.c b/hal/stm32wb.c index 805f900482..2cccf81434 100644 --- a/hal/stm32wb.c +++ b/hal/stm32wb.c @@ -204,18 +204,17 @@ int RAMFUNCTION hal_flash_write(uint32_t address, const uint8_t *data, int len) flash_wait_complete(); i+=8; } else { + uint32_t unit_addr = (address + i) & (~0x07); + int off = (address + i) - unit_addr; uint32_t val[2]; uint8_t *vbytes = (uint8_t *)(val); - int off = (address + i) - (((address + i) >> 3) << 3); - uint32_t base_addr = address & (~0x07); /* aligned to 64 bit */ - int u32_idx = (i >> 2); - dst = (uint32_t *)(base_addr); - val[0] = dst[u32_idx]; - val[1] = dst[u32_idx + 1]; + dst = (uint32_t *)unit_addr; + val[0] = dst[0]; + val[1] = dst[1]; while ((off < 8) && (i < len)) vbytes[off++] = data[i++]; - dst[u32_idx] = val[0]; - dst[u32_idx + 1] = val[1]; + dst[0] = val[0]; + dst[1] = val[1]; flash_wait_complete(); } } From bb99d7d95c941c63ef51d4749bcfdcf5092a70e7 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 02:46:16 +0200 Subject: [PATCH 09/26] F-7061: sim: reject powerfail without an address argument powerfail as the last argv made strtol(NULL) run - undefined behaviour and a segfault. Print a usage error and exit instead. --- hal/sim.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hal/sim.c b/hal/sim.c index 6393e07e25..ee0395efba 100644 --- a/hal/sim.c +++ b/hal/sim.c @@ -539,6 +539,10 @@ void hal_init(void) for (i = 1; i < main_argc; i++) { if (strcmp(main_argv[i], "powerfail") == 0) { + if ((i + 1) >= main_argc) { + wolfBoot_printf( "powerfail requires a hex address argument\n"); + exit(-1); + } erasefail_address = strtol(main_argv[++i], NULL, 16); wolfBoot_printf( "Set power fail to erase at address %x\n", erasefail_address); From b0929687f2569596e13dd1ed0cbd7213afaec0d0 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 02:46:16 +0200 Subject: [PATCH 10/26] F-7384: hal_flash_erase: erase to the end of the requested range Rounding the start address down to a sector/page without extending len shifted the erase window back and left the tail unerased. Capture end = address+len before aligning and loop on address 0) { + while (address < end) { flc_base = flc_base_for_addr(address); ret = flc_page_erase(address, flc_base); @@ -439,7 +444,6 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len) } address += FLASH_PAGE_SIZE; - len -= FLASH_PAGE_SIZE; } icc_enable(); diff --git a/hal/mcxn.c b/hal/mcxn.c index da96b747d6..6b30362926 100644 --- a/hal/mcxn.c +++ b/hal/mcxn.c @@ -310,16 +310,20 @@ void RAMFUNCTION hal_flash_lock(void) int RAMFUNCTION hal_flash_erase(uint32_t address, int len) { uint32_t sector_size = pflash_sector_size; + uint32_t end; if (sector_size == 0U) { sector_size = WOLFBOOT_SECTOR_SIZE; } + /* Drive the loop from the end of the requested range so the tail is + * erased when the start address is rounded back to a sector. */ + end = address + (uint32_t)len; if ((address % sector_size) != 0U) { address -= address % sector_size; } - while (len > 0) { + while (address < end) { if (FLASH_Erase(&pflash, address, sector_size, kFLASH_ApiEraseKey) != kStatus_FLASH_Success) { return -1; @@ -329,7 +333,6 @@ int RAMFUNCTION hal_flash_erase(uint32_t address, int len) return -1; } address += sector_size; - len -= (int)sector_size; } return 0; From 00005053cc92fb7d357f2b7ec82fb1f9c1d73886 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 02:46:16 +0200 Subject: [PATCH 11/26] F-7385: nrf54l spi: unblock spi_read after a DMA bus error spi_write left spi_rx_ready at 0 on RX/TX bus error, so the unconditional spin in spi_read hung the bootloader. Force a defined 0xFF byte and set the ready flag on the error path. --- hal/spi/spi_drv_nrf54l.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/hal/spi/spi_drv_nrf54l.c b/hal/spi/spi_drv_nrf54l.c index 4bf58583ba..2a09d0897c 100644 --- a/hal/spi/spi_drv_nrf54l.c +++ b/hal/spi/spi_drv_nrf54l.c @@ -96,8 +96,14 @@ void RAMFUNCTION spi_write(const char byte) ; SPI_EVENTS_STOPPED = 0; - if (SPI_EVENTS_DMA_RX_BUSERROR == 0 && SPI_EVENTS_DMA_TX_BUSERROR == 0) + if (SPI_EVENTS_DMA_RX_BUSERROR == 0 && SPI_EVENTS_DMA_TX_BUSERROR == 0) { spi_rx_ready = 1; + } else { + /* DMA bus error: force a defined byte and unblock the caller, or + * spi_read() would spin forever on spi_rx_ready == 0. */ + spi_rx_byte = 0xFF; + spi_rx_ready = 1; + } } From c94453b083502620623e6435101586fb3acae6d0 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 02:46:16 +0200 Subject: [PATCH 12/26] F-7972: disk_open_mbr: return -1 when no usable partition n_parts is unsigned, so the caller's < 0 guard was dead code and an empty MBR reported success with zero partitions. Fail explicitly. --- src/disk.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/disk.c b/src/disk.c index 79114a6ca6..b0b9a21a01 100644 --- a/src/disk.c +++ b/src/disk.c @@ -104,6 +104,10 @@ static int disk_open_mbr(struct disk_drive *drive, const uint8_t *mbr_sector) } } + if (drive->n_parts == 0) { + return -1; /* no usable partition entries */ + } + return drive->n_parts; } From 0f266e6407c715e1560a697bc3e4d2b5c0628a11 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 02:46:16 +0200 Subject: [PATCH 13/26] F-7973: qspi test_ext_flash: return -1, not -i, on mismatch A mismatch at index 0 evaluated to 0, which spi_flash_probe reads as success. Use the fixed -1 like src/spi_flash.c does. --- src/qspi_flash.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qspi_flash.c b/src/qspi_flash.c index 64638904ff..5ca906d559 100644 --- a/src/qspi_flash.c +++ b/src/qspi_flash.c @@ -533,7 +533,7 @@ static int test_ext_flash(void) #endif if (pageData[i] != (i & 0xff)) { wolfBoot_printf("Check Data @ %d failed\n", i); - return -i; + return -1; } } From 9b73957a85c90ffda0ae2d95ba175e12ad9ea894 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 02:46:25 +0200 Subject: [PATCH 14/26] F-7387: sdcard_send_switch_function: reject group 0 Groups are numbered 1..6; group 0 wrapped (group-1)*4 into a 0xFFFFFFFC shift count - undefined behaviour. Tighten the guard to group_number < 1. --- src/sdhci.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sdhci.c b/src/sdhci.c index 51a9c179a9..73a57d53f7 100644 --- a/src/sdhci.c +++ b/src/sdhci.c @@ -1235,7 +1235,7 @@ static int sdcard_send_switch_function(uint32_t mode, uint32_t function_number, uint32_t func_status[64/sizeof(uint32_t)]; /* fixed 512 bits */ uint8_t* p_func_status = (uint8_t*)func_status; - if (group_number > 6 || function_number > 15) { + if (group_number < 1 || group_number > 6 || function_number > 15) { return -1; /* Invalid group or function number */ } From ab3b6f6f9dd10e28100e7d9eda3e16b468e91a38 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 02:46:25 +0200 Subject: [PATCH 15/26] F-7386: sdcard_send_switch_function: fail when busy budget runs out The do/while fell out with status==0 when the card stayed busy until timeout, indistinguishable from supported, so the SWITCH command went out for a function the card never cleared. Set status=-1 when timeout reaches 0. --- src/sdhci.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sdhci.c b/src/sdhci.c index 73a57d53f7..9b8c6539a4 100644 --- a/src/sdhci.c +++ b/src/sdhci.c @@ -1268,6 +1268,11 @@ static int sdcard_send_switch_function(uint32_t mode, uint32_t function_number, break; } } while (status == 0 && --timeout > 0); /* retry until function not busy */ + + if (timeout == 0) { + /* Card stayed busy until the retry budget ran out. */ + status = -1; + } return status; } From 02f15efd0f8db085a855c8dfb254db65ce343c05 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 02:46:25 +0200 Subject: [PATCH 16/26] F-7062: sdhci: check sdhci_set_clock results at all three call sites A 0 return (no base clock / ICS never stabilized) proceeded with an unconfigured card clock. Fail sdhci_init, emmc_card_full_init and the UHS-I 50MHz step when the clock is not programmed. --- src/sdhci.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/sdhci.c b/src/sdhci.c index 9b8c6539a4..c078882da3 100644 --- a/src/sdhci.c +++ b/src/sdhci.c @@ -1187,7 +1187,10 @@ static int sdcard_card_full_init(void) } if (status == 0) { - sdhci_set_clock(SDHCI_CLK_50MHZ); + if (sdhci_set_clock(SDHCI_CLK_50MHZ) == 0) { + wolfBoot_printf("UHS-I: failed to set 50MHz clock\n"); + status = -1; + } } SDHCI_REG_SET(SDHCI_SRS13, irq_restore); /* re-enable interrupt */ @@ -1500,7 +1503,10 @@ static int emmc_card_full_init(void) } /* Set clock to 25MHz for legacy mode */ - sdhci_set_clock(SDHCI_CLK_25MHZ); + if (sdhci_set_clock(SDHCI_CLK_25MHZ) == 0) { + wolfBoot_printf("eMMC: failed to set 25MHz clock\n"); + return -1; + } /* Enable high speed if desired (optional for legacy mode) */ sdhci_reg_or(SDHCI_SRS10, SDHCI_SRS10_HSE); @@ -2005,7 +2011,10 @@ int sdhci_init(void) SDHCI_REG_SET(SDHCI_SRS10, reg); /* Setup 400khz starting clock */ - sdhci_set_clock(SDHCI_CLK_400KHZ); + if (sdhci_set_clock(SDHCI_CLK_400KHZ) == 0) { + wolfBoot_printf("Failed to set 400kHz starting clock\n"); + return -1; + } /* Allow clock to stabilize before issuing first command */ udelay(1000); /* 1ms */ From 6b505817eddb98b509ff54ea73f2f334b7a88bd5 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 03:26:31 +0200 Subject: [PATCH 17/26] F-13660: zeroize the nonce-derived IV copy in disk_crypto_set_iv The CTR counter block built on the stack is a full copy of the secret disk-encryption nonce plus counter; both in-tree sibling IV helpers already scrub their stack copy, this was the only one that did not. --- src/update_disk.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/update_disk.c b/src/update_disk.c index 8a78a64c79..e74bf986d4 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -226,6 +226,9 @@ static void disk_crypto_set_iv(uint32_t block_offset) iv[15] = (uint8_t)(ctr); wc_AesSetIV(&aes_dec, iv); + /* Scrub the stack copy: the counter bytes are derived from the + * secret disk-encryption nonce (matches aes_set_iv in libwolfboot.c). */ + wc_ForceZero(iv, sizeof(iv)); #endif } From 217dbe8f9ee467ab0b5831d8bf18625c46c690e9 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 03:29:14 +0200 Subject: [PATCH 18/26] F-11039: zeroize XFREE'd slots in the static malloc pool The small-stack crypto pool hands out workspace for hash blocks and signature verification state; the slot was released back to the pool still holding that data. Scrub it in XFREE before marking the slot free (wc_ForceZero, same helper the rest of the crypto path uses). --- src/xmalloc.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/xmalloc.c b/src/xmalloc.c index a270c2fffc..46930e8b7f 100644 --- a/src/xmalloc.c +++ b/src/xmalloc.c @@ -26,6 +26,7 @@ #include #include #include +#include /* wc_ForceZero */ #ifndef USE_FAST_MATH #include #include @@ -514,6 +515,9 @@ void XFREE(void *ptr, void *heap, int type) #endif while (xmalloc_pool[i].addr) { if ((ptr == (void *)(xmalloc_pool[i].addr)) && xmalloc_pool[i].in_use) { + /* Scrub the slot before releasing it: it may hold crypto + * workspace (hash blocks, signature verification state). */ + wc_ForceZero(xmalloc_pool[i].addr, xmalloc_pool[i].size); xmalloc_pool[i].in_use = 0; return; } From 60803ad3eb19abb8c5648572c5454abc66fe6aec Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 03:29:49 +0200 Subject: [PATCH 19/26] F-14156: mp_clear the ECDSA r/s scalars after verify wolfBoot_verify_signature_ecc() imports the raw signature into mp_int r/s and passes them to wc_ecc_verify_hash_ex() without clearing them; mp_clear() scrubs the digit memory before the stack frame retires. --- src/image.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/image.c b/src/image.c index dc48fc860c..ee23840c33 100644 --- a/src/image.c +++ b/src/image.c @@ -417,6 +417,9 @@ static void wolfBoot_verify_signature_ecc(uint8_t key_slot, mp_read_unsigned_bin(&s, sig + point_sz, point_sz); VERIFY_FN(img, &verify_res, wc_ecc_verify_hash_ex, &r, &s, img->sha_hash, WOLFBOOT_SHA_DIGEST_SIZE, &verify_res, &ecc); + /* Signature scalars: scrub before the stack frame retires. */ + mp_clear(&r); + mp_clear(&s); } #endif } From 8cec76e1061ec601f752888cd233f3041df28aee Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 03:31:09 +0200 Subject: [PATCH 20/26] F-14155: scrub the decoded key objects in sign.c cleanup The key/key2 structs in main() hold the decoded private key material for the rest of the process lifetime, and free_key() only releases what its dispatch knows about. Scrub both objects unconditionally in cleanup so no key residue survives, for any algorithm. --- tools/keytools/sign.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/keytools/sign.c b/tools/keytools/sign.c index 39573d7af3..7f6e804927 100644 --- a/tools/keytools/sign.c +++ b/tools/keytools/sign.c @@ -4134,5 +4134,11 @@ int main(int argc, char** argv) if (CMD.hybrid) { free_key(CMD.secondary_sign, 1); } + /* Defence in depth: scrub the decoded key objects regardless of the + * algorithm dispatch above, so no key residue survives. */ + wc_ForceZero(&key, sizeof(key)); + if (CMD.hybrid) { + wc_ForceZero(&key2, sizeof(key2)); + } return ret; } From e1c7fb1f8247349a9f6211b2c9ac786ac01a64c0 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 03:33:44 +0200 Subject: [PATCH 21/26] F-7393: zeroize the TPM session/SRK globals at deinit wolfBoot_tpm2_deinit() unloads the session and SRK handles and cleans up the device, but the file-scope wolftpm_session (HMAC/parameter- encryption session key) and wolftpm_srk (SRK authValue) stayed in .bss SRAM for the booted OS to read. ForceZero both after Cleanup. --- src/tpm.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tpm.c b/src/tpm.c index e240946e86..01c0fe3898 100644 --- a/src/tpm.c +++ b/src/tpm.c @@ -1652,6 +1652,14 @@ void wolfBoot_tpm2_deinit(void) #endif /* WOLFBOOT_TPM_KEYSTORE */ wolfTPM2_Cleanup(&wolftpm_dev); + +#if defined(WOLFBOOT_TPM_KEYSTORE) || defined(WOLFBOOT_TPM_SEAL) + /* The OS takes over from here: leave no session key or SRK auth in + * SRAM. UnloadHandle flushes the TPM-side context but is not + * documented to clear handle->auth. */ + TPM2_ForceZero(&wolftpm_session, sizeof(wolftpm_session)); + TPM2_ForceZero(&wolftpm_srk, sizeof(wolftpm_srk)); +#endif } /** From 525fd7639829997e779ed8382de0b864c3a968c5 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 03:35:23 +0200 Subject: [PATCH 22/26] F-7395: zeroize the stack auth blobs in the TPM seal/unseal and NV paths wolfBoot_seal_auth()/wolfBoot_unseal_auth() copy the caller authValue into a stack WOLFTPM2_KEYBLOB and the NV helpers copy it into a stack WOLFTPM2_NV; none were cleared before return, inconsistent with the existing TPM2_ForceZero(&unsealOut) in the same file. ForceZero each stack object at the single exit. --- src/tpm.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/tpm.c b/src/tpm.c index 01c0fe3898..32c8773d4a 100644 --- a/src/tpm.c +++ b/src/tpm.c @@ -720,6 +720,8 @@ int wolfBoot_store_blob(TPMI_RH_NV_AUTH authHandle, uint32_t nvIndex, wolfBoot_printf("Error %d writing blob to NV index %x (error %s)\n", rc, nv.handle.hndl, wolfTPM2_GetRCString(rc)); } + /* Scrub the stack NV handle: it carries the authValue copy. */ + TPM2_ForceZero(&nv, sizeof(nv)); return rc; } @@ -792,6 +794,7 @@ int wolfBoot_read_blob(uint32_t nvIndex, WOLFTPM2_KEYBLOB* blob, wolfBoot_printf("Error %d reading blob from NV index %x (error %s)\n", rc, nv.handle.hndl, wolfTPM2_GetRCString(rc)); } + TPM2_ForceZero(&nv, sizeof(nv)); return rc; } @@ -825,6 +828,7 @@ int wolfBoot_delete_blob(TPMI_RH_NV_AUTH authHandle, uint32_t nvIndex, wolfBoot_printf("Error %d deleting blob from NV index %x (error %s)\n", rc, nv.handle.hndl, wolfTPM2_GetRCString(rc)); } + TPM2_ForceZero(&nv, sizeof(nv)); return rc; } @@ -973,6 +977,8 @@ int wolfBoot_seal_auth(const uint8_t* pubkey_hint, wolfBoot_printf("Error %d sealing secret! (%s)\n", rc, wolfTPM2_GetRCString(rc)); } + /* The blob holds the plaintext authValue copy used for the seal. */ + TPM2_ForceZero(&seal_blob, sizeof(seal_blob)); return rc; } int wolfBoot_seal(const uint8_t* pubkey_hint, @@ -1202,6 +1208,7 @@ int wolfBoot_unseal_auth(const uint8_t* pubkey_hint, wolfBoot_printf("Error %d unsealing secret! (%s)\n", rc, wolfTPM2_GetRCString(rc)); } + TPM2_ForceZero(&seal_blob, sizeof(seal_blob)); return rc; } int wolfBoot_unseal(const uint8_t* pubkey_hint, @@ -1729,6 +1736,7 @@ int wolfBoot_check_rot(int key_slot, uint8_t* pubkey_hint) } wolfTPM2_UnsetAuthSession(&wolftpm_dev, 1, &wolftpm_session); + TPM2_ForceZero(&nv, sizeof(nv)); return rc; } #endif From 07301f972c4015824ebee6aac1ec6572964ecd0f Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 03:35:38 +0200 Subject: [PATCH 23/26] F-12943: zeroize the policy session in seal/unseal blob cleanup wolfBoot_seal_blob() and wolfBoot_unseal_blob() unload the policy_session TPM handle on cleanup but leave the stack session object (SRK-derived session key material) in the frame; ForceZero it in the common cleanup of both. --- src/tpm.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tpm.c b/src/tpm.c index 32c8773d4a..5679cdd75c 100644 --- a/src/tpm.c +++ b/src/tpm.c @@ -912,6 +912,8 @@ int wolfBoot_seal_blob(const uint8_t* pubkey_hint, wolfTPM2_UnloadHandle(&wolftpm_dev, &policy_session.handle); wolfTPM2_UnsetAuthSession(&wolftpm_dev, 1, &wolftpm_session); + /* Scrub the session object: it holds the SRK-derived session key. */ + TPM2_ForceZero(&policy_session, sizeof(policy_session)); return rc; } @@ -1175,6 +1177,7 @@ int wolfBoot_unseal_blob(const uint8_t* pubkey_hint, wolfTPM2_UnloadHandle(&wolftpm_dev, &seal_blob->handle); wolfTPM2_UnloadHandle(&wolftpm_dev, &policy_session.handle); wolfTPM2_UnsetAuthSession(&wolftpm_dev, 1, &wolftpm_session); + TPM2_ForceZero(&policy_session, sizeof(policy_session)); return rc; } From 3e1f4a1295b4be3f2b4969718bd107b608ba5e35 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 03:45:48 +0200 Subject: [PATCH 24/26] F-7395/F-12943: update the tpm unit tests for the new scrubs The tests #include the real src/tpm.c with a mocked wolfTPM surface: add the missing TPM2_ForceZero mock (already present in three sibling tests) and bump unit-tpm-blob's call-count assertion from 1 to 2 now that wolfBoot_unseal_blob() also scrubs policy_session in its exit path. --- tools/unit-tests/unit-tpm-api-names.c | 9 +++++++++ tools/unit-tests/unit-tpm-blob.c | 10 ++++++---- tools/unit-tests/unit-tpm-check-rot-auth.c | 9 +++++++++ tools/unit-tests/unit-tpm-nsc-cert.c | 9 +++++++++ tools/unit-tests/unit-tpm-rsa-exp.c | 9 +++++++++ 5 files changed, 42 insertions(+), 4 deletions(-) diff --git a/tools/unit-tests/unit-tpm-api-names.c b/tools/unit-tests/unit-tpm-api-names.c index 6a61abb976..6f9b0648c9 100644 --- a/tools/unit-tests/unit-tpm-api-names.c +++ b/tools/unit-tests/unit-tpm-api-names.c @@ -37,6 +37,15 @@ const char* TPM2_GetRCString(int rc) return NULL; } +void TPM2_ForceZero(void* mem, word32 len) +{ + volatile uint8_t* p = (volatile uint8_t*)mem; + word32 i; + + for (i = 0; i < len; i++) + p[i] = 0; +} + #include "../../src/tpm.c" static void setup_small_buf(struct small_buf* buf) diff --git a/tools/unit-tests/unit-tpm-blob.c b/tools/unit-tests/unit-tpm-blob.c index 84683f11e1..20e3479f47 100644 --- a/tools/unit-tests/unit-tpm-blob.c +++ b/tools/unit-tests/unit-tpm-blob.c @@ -637,8 +637,9 @@ START_TEST(test_wolfBoot_unseal_blob_zeroes_unseal_output) ck_assert_int_eq(rc, 0); ck_assert_int_eq(secret_sz, 4); - ck_assert_int_eq(forcezero_calls, 1); - ck_assert_uint_eq(last_forcezero_len, sizeof(Unseal_Out)); + /* unsealOut scrub + the policy_session scrub in the exit path */ + ck_assert_int_eq(forcezero_calls, 2); + ck_assert_uint_eq(last_forcezero_len, sizeof(WOLFTPM2_SESSION)); } END_TEST @@ -736,8 +737,9 @@ START_TEST(test_wolfBoot_unseal_blob_rejects_output_larger_than_capacity) ck_assert_int_eq(rc, BUFFER_E); ck_assert_int_eq(secret_sz, 0); - ck_assert_int_eq(forcezero_calls, 1); - ck_assert_uint_eq(last_forcezero_len, sizeof(Unseal_Out)); + /* unsealOut scrub + the policy_session scrub in the exit path */ + ck_assert_int_eq(forcezero_calls, 2); + ck_assert_uint_eq(last_forcezero_len, sizeof(WOLFTPM2_SESSION)); for (i = 0; i < (int)sizeof(output.canary); i++) { ck_assert_uint_eq(output.canary[i], 0xA5); } diff --git a/tools/unit-tests/unit-tpm-check-rot-auth.c b/tools/unit-tests/unit-tpm-check-rot-auth.c index 203d3bc90a..e86bf4042c 100644 --- a/tools/unit-tests/unit-tpm-check-rot-auth.c +++ b/tools/unit-tests/unit-tpm-check-rot-auth.c @@ -168,6 +168,15 @@ int ConstantCompare(const byte* a, const byte* b, int length) return diff; } +void TPM2_ForceZero(void* mem, word32 len) +{ + volatile uint8_t* p = (volatile uint8_t*)mem; + word32 i; + + for (i = 0; i < len; i++) + p[i] = 0; +} + #include "../../src/tpm.c" static void setup(void) diff --git a/tools/unit-tests/unit-tpm-nsc-cert.c b/tools/unit-tests/unit-tpm-nsc-cert.c index 06e0970af1..2175c65d29 100644 --- a/tools/unit-tests/unit-tpm-nsc-cert.c +++ b/tools/unit-tests/unit-tpm-nsc-cert.c @@ -109,6 +109,15 @@ int wolfTPM2_NVReadCert(WOLFTPM2_DEV* dev, TPM_HANDLE handle, return 0; } +void TPM2_ForceZero(void* mem, word32 len) +{ + volatile uint8_t* p = (volatile uint8_t*)mem; + word32 i; + + for (i = 0; i < len; i++) + p[i] = 0; +} + #include "../../src/tpm.c" static void setup_ns_edge(uint32_t certSz, uint32_t race, uint32_t nvSize) diff --git a/tools/unit-tests/unit-tpm-rsa-exp.c b/tools/unit-tests/unit-tpm-rsa-exp.c index 1cf0ce5d69..814cbad7d2 100644 --- a/tools/unit-tests/unit-tpm-rsa-exp.c +++ b/tools/unit-tests/unit-tpm-rsa-exp.c @@ -167,6 +167,15 @@ static int forbidden_memcmp(const void *a, const void *b, size_t n) } #define memcmp forbidden_memcmp +void TPM2_ForceZero(void* mem, word32 len) +{ + volatile uint8_t* p = (volatile uint8_t*)mem; + word32 i; + + for (i = 0; i < len; i++) + p[i] = 0; +} + #include "../../src/tpm.c" #undef memcmp From b31a7c959f6f443e426d8384de11ff800e63fd90 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 04:09:22 +0200 Subject: [PATCH 25/26] F-7395/F-12943: assert both scrub lengths in the unseal tests The forcezero_calls bump to 2 dropped the sizeof(Unseal_Out) assertion, leaving the unsealOut wipe unguarded. Track the first scrub length and assert it alongside the last one. --- tools/unit-tests/unit-tpm-blob.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tools/unit-tests/unit-tpm-blob.c b/tools/unit-tests/unit-tpm-blob.c index 20e3479f47..611b042f2f 100644 --- a/tools/unit-tests/unit-tpm-blob.c +++ b/tools/unit-tests/unit-tpm-blob.c @@ -38,6 +38,7 @@ static int unexpected_nvdelete_calls; static int oversized_pub_read_attempted; static int oversized_priv_read_attempted; static int forcezero_calls; +static word32 first_forcezero_len; static word32 last_forcezero_len; static word32 last_pub_read_request_sz; static int unload_handle_calls; @@ -331,6 +332,9 @@ TPM_RC TPM2_Unseal(Unseal_In* in, Unseal_Out* out) void TPM2_ForceZero(void* mem, word32 len) { + if (forcezero_calls == 0) { + first_forcezero_len = len; + } forcezero_calls++; last_forcezero_len = len; memset(mem, 0, len); @@ -501,6 +505,7 @@ static void setup(void) oversized_pub_read_attempted = 0; oversized_priv_read_attempted = 0; forcezero_calls = 0; + first_forcezero_len = 0; last_forcezero_len = 0; last_pub_read_request_sz = 0; unload_handle_calls = 0; @@ -637,8 +642,10 @@ START_TEST(test_wolfBoot_unseal_blob_zeroes_unseal_output) ck_assert_int_eq(rc, 0); ck_assert_int_eq(secret_sz, 4); - /* unsealOut scrub + the policy_session scrub in the exit path */ + /* unsealOut scrub first, then the policy_session scrub in the + * exit path: assert both lengths, not just the last one. */ ck_assert_int_eq(forcezero_calls, 2); + ck_assert_uint_eq(first_forcezero_len, sizeof(Unseal_Out)); ck_assert_uint_eq(last_forcezero_len, sizeof(WOLFTPM2_SESSION)); } END_TEST @@ -737,8 +744,10 @@ START_TEST(test_wolfBoot_unseal_blob_rejects_output_larger_than_capacity) ck_assert_int_eq(rc, BUFFER_E); ck_assert_int_eq(secret_sz, 0); - /* unsealOut scrub + the policy_session scrub in the exit path */ + /* unsealOut scrub first, then the policy_session scrub in the + * exit path: assert both lengths, not just the last one. */ ck_assert_int_eq(forcezero_calls, 2); + ck_assert_uint_eq(first_forcezero_len, sizeof(Unseal_Out)); ck_assert_uint_eq(last_forcezero_len, sizeof(WOLFTPM2_SESSION)); for (i = 0; i < (int)sizeof(output.canary); i++) { ck_assert_uint_eq(output.canary[i], 0xA5); From a6eb491087e8ad8c83833e05e58e99972329c500 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Wed, 23 Sep 2026 09:53:18 +0200 Subject: [PATCH 26/26] unit-uart-flash: cover the extended erase-ACK budget The success test delivered the erase-completion ACK on the first poll, so it passed identically with the pre-PR short budget. Add a per-byte delay to the uart_rx mock and a test where the final ACK arrives after WAIT_CYCLES + 1 empty polls: the short budget times out (verified), the extended one returns 0. --- tools/unit-tests/unit-uart-flash.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tools/unit-tests/unit-uart-flash.c b/tools/unit-tests/unit-uart-flash.c index 51b75b1c34..f3fffb9c3f 100644 --- a/tools/unit-tests/unit-uart-flash.c +++ b/tools/unit-tests/unit-uart-flash.c @@ -14,6 +14,7 @@ static const uint8_t CMD_ACK = 0x06; static uint8_t rx_script[16]; static int rx_script_len; static int rx_script_pos; +static int rx_delay[16]; static uint8_t tx_log[32]; static int tx_log_len; @@ -34,6 +35,12 @@ int uart_rx(uint8_t *c) if (rx_script_pos >= rx_script_len) return 0; + /* Empty polls before this script byte is delivered */ + if (rx_delay[rx_script_pos] > 0) { + rx_delay[rx_script_pos]--; + return 0; + } + *c = rx_script[rx_script_pos++]; return 1; } @@ -47,6 +54,7 @@ static void reset_uart_script(const uint8_t *script, int len) memcpy(rx_script, script, len); rx_script_len = len; rx_script_pos = 0; + memset(rx_delay, 0, sizeof(rx_delay)); memset(tx_log, 0, sizeof(tx_log)); tx_log_len = 0; } @@ -85,6 +93,25 @@ START_TEST(test_ext_flash_erase_success) } END_TEST +START_TEST(test_ext_flash_erase_ack_late_budget) +{ + uint8_t script[11]; + int ret; + + /* 10 command ACKs + the erase-completion ACK, which arrives only + * after more than WAIT_CYCLES empty polls: the pre-PR short budget + * would time out here, the extended one must not. */ + memset(script, CMD_ACK, sizeof(script)); + reset_uart_script(script, sizeof(script)); + rx_delay[10] = WAIT_CYCLES + 1; + + ret = ext_flash_erase(0x1000, 0x1000); + + ck_assert_int_eq(ret, 0); + ck_assert_int_eq(rx_script_pos, 11); +} +END_TEST + START_TEST(test_ext_flash_erase_timeout_returns_error) { uint8_t script[10]; @@ -107,6 +134,7 @@ Suite *wolfboot_suite(void) tcase_add_test(uart_flash, test_ext_flash_read_timeout_returns_error); tcase_add_test(uart_flash, test_ext_flash_erase_success); + tcase_add_test(uart_flash, test_ext_flash_erase_ack_late_budget); tcase_add_test(uart_flash, test_ext_flash_erase_timeout_returns_error); tcase_set_timeout(uart_flash, 20); suite_add_tcase(s, uart_flash);