From aa6fdb8fa94b12fa1eda280284012c288d200d0a Mon Sep 17 00:00:00 2001 From: David Garske Date: Wed, 7 Oct 2026 13:47:32 -0700 Subject: [PATCH] x86 FSP: add TPM-NV anti-rollback with a persistent version floor and boot audit counters --- .../test-build-kontron-vx3060-s2.yml | 4 - .github/workflows/test-x86-fsp-qemu.yml | 27 +- .gitignore | 2 + arch.mk | 4 + .../examples/x86_fsp_qemu_antirollback.config | 45 +++ include/boot_state.h | 85 +++++ include/tpm.h | 3 + options.mk | 14 + src/boot_state.c | 264 ++++++++++++++++ src/boot_state_tpm.c | 156 +++++++++ src/tpm.c | 11 +- src/update_disk.c | 33 ++ tools/scripts/x86_fsp/qemu/qemu.sh | 2 +- tools/scripts/x86_fsp/qemu/qemu_build_fsp.sh | 9 +- tools/scripts/x86_fsp/qemu/test_qemu.sh | 71 ++++- tools/scripts/x86_fsp/tgl/tgl_download_fsp.sh | 4 +- tools/unit-tests/Makefile | 22 +- tools/unit-tests/stubs-boot-state/loader.h | 27 ++ tools/unit-tests/stubs-boot-state/printf.h | 27 ++ .../stubs-boot-state/user_settings.h | 26 ++ tools/unit-tests/unit-boot-state.c | 296 ++++++++++++++++++ 21 files changed, 1102 insertions(+), 30 deletions(-) create mode 100644 config/examples/x86_fsp_qemu_antirollback.config create mode 100644 include/boot_state.h create mode 100644 src/boot_state.c create mode 100644 src/boot_state_tpm.c create mode 100644 tools/unit-tests/stubs-boot-state/loader.h create mode 100644 tools/unit-tests/stubs-boot-state/printf.h create mode 100644 tools/unit-tests/stubs-boot-state/user_settings.h create mode 100644 tools/unit-tests/unit-boot-state.c diff --git a/.github/workflows/test-build-kontron-vx3060-s2.yml b/.github/workflows/test-build-kontron-vx3060-s2.yml index a4547e69..3eff7df9 100644 --- a/.github/workflows/test-build-kontron-vx3060-s2.yml +++ b/.github/workflows/test-build-kontron-vx3060-s2.yml @@ -46,10 +46,6 @@ jobs: export DEBIAN_FRONTEND=noninteractive sudo apt-get update -o Acquire::Retries=3 sudo apt-get install --no-install-recommends -y -q -o Acquire::Retries=3 nasm gcc-multilib - - name: setup git - run: | - git config --global user.email "you@example.com" - git config --global user.name "Your Name" - name: run test run: | cp config/examples/kontron_vx3060_s2.config .config diff --git a/.github/workflows/test-x86-fsp-qemu.yml b/.github/workflows/test-x86-fsp-qemu.yml index 543ea82f..9691dca4 100644 --- a/.github/workflows/test-x86-fsp-qemu.yml +++ b/.github/workflows/test-x86-fsp-qemu.yml @@ -46,10 +46,6 @@ jobs: export DEBIAN_FRONTEND=noninteractive sudo apt-get update -o Acquire::Retries=3 sudo apt-get install --no-install-recommends -y -q -o Acquire::Retries=3 nasm gcc-multilib qemu-system-x86 swtpm uuid-dev - - name: setup git - run: | - git config --global user.email "you@example.com" - git config --global user.name "Your Name" - name: run test run: | ./tools/scripts/x86_fsp/qemu/test_qemu.sh @@ -70,10 +66,25 @@ jobs: export DEBIAN_FRONTEND=noninteractive sudo apt-get update -o Acquire::Retries=3 sudo apt-get install --no-install-recommends -y -q -o Acquire::Retries=3 nasm gcc-multilib qemu-system-x86 uuid-dev dosfstools mtools e2fsprogs - - name: setup git - run: | - git config --global user.email "you@example.com" - git config --global user.name "Your Name" - name: run test run: | ./tools/scripts/x86_fsp/qemu/test_qemu.sh -f + + # TPM-NV anti-rollback: the floor anchors on a fresh swtpm, survives a TPM + # restart, and a validly signed downgrade boots and is counted. + fsp_qemu_antirollback_test: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: install req + run: | + export DEBIAN_FRONTEND=noninteractive + sudo apt-get update -o Acquire::Retries=3 + sudo apt-get install --no-install-recommends -y -q -o Acquire::Retries=3 nasm gcc-multilib qemu-system-x86 swtpm uuid-dev + - name: run test + run: | + ./tools/scripts/x86_fsp/qemu/test_qemu.sh -r diff --git a/.gitignore b/.gitignore index c909ebd0..9fa8b7a7 100644 --- a/.gitignore +++ b/.gitignore @@ -181,6 +181,8 @@ tools/unit-tests/unit-update-flash-enc tools/unit-tests/unit-update-flash-db tools/unit-tests/unit-update-flash-enc-full tools/unit-tests/unit-update-ram +tools/unit-tests/unit-boot-state +tools/unit-tests/unit-boot-state-halt tools/unit-tests/unit-boot-x86-fsp tools/unit-tests/unit-image-rsa tools/unit-tests/unit-multiboot diff --git a/arch.mk b/arch.mk index c5fc2a83..84eac55d 100644 --- a/arch.mk +++ b/arch.mk @@ -2312,6 +2312,10 @@ ifeq ("${FSP}", "1") OBJS += src/x86/fsp_s.o UPDATE_OBJS := src/update_disk.o CFLAGS+=-DWOLFBOOT_UPDATE_DISK + ifeq ($(ANTI_ROLLBACK),1) + OBJS += src/boot_state.o + OBJS += src/boot_state_tpm.o + endif ifeq ($(64BIT),1) LDFLAGS += -m elf_x86_64 --oformat elf64-x86-64 CFLAGS += -m64 diff --git a/config/examples/x86_fsp_qemu_antirollback.config b/config/examples/x86_fsp_qemu_antirollback.config new file mode 100644 index 00000000..e246cf65 --- /dev/null +++ b/config/examples/x86_fsp_qemu_antirollback.config @@ -0,0 +1,45 @@ +ARCH=x86_64 +TARGET=x86_fsp_qemu +WOLFBOOT_SMALL_STACK=1 +SIGN=ECC256 +HASH=SHA256 +DEBUG=1 +SPMATH=1 +FORCE_32BIT=1 +ENCRYPTION=0 +WOLFBOOT_NO_PARTITIONS=1 + +# TPM Keystore options +#WOLFBOOT_TPM_KEYSTORE?=1 +#WOLFBOOT_TPM_KEYSTORE_NV_BASE?=0x01800200 +#WOLFBOOT_TPM_POLICY_NV_INDEX?=0x01800201 + +# 4gb - 8mb +WOLFBOOT_LOAD_BASE=0x2000000 +WOLFBOOT_LOAD_ADDRESS=0x1000000 + +# required for keytools +WOLFBOOT_SECTOR_SIZE?=0x1000 +WOLFBOOT_DATA_ADDRESS=0x1000000 + +FSP_M_BASE=0xffe30000 +FSP_S_BASE=0xffed6000 +FSP_T_BASE=0xfffe0000 +FSP_S_LOAD_BASE=0x0FED5F00 +WOLFBOOT_ORIGIN=0xfff80000 + +BOOTLOADER_PARTITION_SIZE=0xe0000 +BIOS_REGION_SIZE=0x800000 +MACHINE_OBJ=src/x86/qemu_fsp.o +FSP_T_BIN=./src/x86/fsp_t.bin +FSP_M_BIN=./src/x86/fsp_m.bin +FSP_S_BIN=./src/x86/fsp_s.bin +STAGE1_AUTH=1 +64BIT=1 +ELF=1 +MULTIBOOT2=1 + +# TPM-NV anti-rollback (version floor and audit counters), tested by +# tools/scripts/x86_fsp/qemu/test_qemu.sh -r +ANTI_ROLLBACK=1 +ANTI_ROLLBACK_HALT_ON_FAILURE=1 diff --git a/include/boot_state.h b/include/boot_state.h new file mode 100644 index 00000000..593e7618 --- /dev/null +++ b/include/boot_state.h @@ -0,0 +1,85 @@ +/* boot_state.h + * + * Persistent anti-rollback reference and boot audit counters. + * + * 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 2 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 + */ + +/* Anti-rollback state: anchor = highest version ever booted (trust on first + * use), record = last booted. See the Anti-rollback section of Targets.md. */ + +#ifndef BOOT_STATE_H +#define BOOT_STATE_H + +#include + +#define BOOT_STATE_MAGIC 0x42535431UL /* "BST1" */ +#define BOOT_STATE_VERSION 1 + +/* Result of loading the persisted state. */ +#define BOOT_STATE_OK 0 /* loaded and trustworthy */ +#define BOOT_STATE_UNPROVISIONED 1 /* nothing stored yet; not an error */ +#define BOOT_STATE_CORRUPT (-1) /* stored but unusable -- fatal */ +#define BOOT_STATE_UNAVAILABLE (-2) /* backend could not be reached */ + +/* Failure codes recorded in last_fail_code. */ +#define BOOT_FAIL_NONE 0 +#define BOOT_FAIL_NO_IMAGE 1 +#define BOOT_FAIL_VERIFY_PRIMARY 2 +#define BOOT_FAIL_VERIFY_BOTH 3 +#define BOOT_FAIL_ROLLBACK 4 + +/* On-storage layout. Packed and explicitly sized so the representation does + * not shift between the bootloader and any provisioning tool. */ +struct boot_state { + uint32_t magic; + uint32_t anchor; + uint32_t record; + uint32_t rollback_count; + uint32_t last_rollback_from; + uint32_t last_rollback_to; + uint32_t boot_fail_count; + uint32_t last_fail_code; + uint16_t struct_version; + uint16_t reserved; + uint32_t crc; +} __attribute__((packed)); + +/* Backend, implemented per platform. v1 uses TPM NV (src/boot_state_tpm.c). */ +int boot_state_backend_read(uint8_t *buf, uint32_t len); +int boot_state_backend_write(const uint8_t *buf, uint32_t len); +/* Write-locks the record until the next TPM reset, so the booted OS cannot + * rewrite it. */ +int boot_state_backend_lock(void); + +/* Load the persisted state. Returns one of the BOOT_STATE_* codes above. */ +int boot_state_load(struct boot_state *st); + +/* Called once an image has verified, immediately before handing off. + * Applies the rollback check, updates anchor and record, and persists. */ +void boot_state_on_boot(uint32_t booted_version); +/* Lock the stored state against the OS without recording a boot (golden + * slot). Halts on failure with ANTI_ROLLBACK_HALT_ON_FAILURE. */ +void boot_state_lock(void); + +/* Called when the boot policy has decided no image may be booted. Records the + * failure so the evidence survives the power cycle. */ +void boot_state_on_failure(uint32_t fail_code); + +#endif /* BOOT_STATE_H */ diff --git a/include/tpm.h b/include/tpm.h index 96847a50..7b3a2f2e 100644 --- a/include/tpm.h +++ b/include/tpm.h @@ -40,6 +40,9 @@ extern WOLFTPM2_KEY wolftpm_srk; #ifndef WOLFBOOT_TPM_SEAL_NV_BASE #define WOLFBOOT_TPM_SEAL_NV_BASE 0x01400300 #endif +#ifndef WOLFBOOT_TPM_BOOT_STATE_NV_BASE + #define WOLFBOOT_TPM_BOOT_STATE_NV_BASE 0x01400400 +#endif /* The PCR bank algorithm and its digest size must always agree. They used to * be defined together under a single #ifndef on the algorithm, which meant * overriding only WOLFBOOT_TPM_PCR_ALG left WOLFBOOT_TPM_PCR_DIG_SZ undefined diff --git a/options.mk b/options.mk index 2f854a9a..350620f2 100644 --- a/options.mk +++ b/options.mk @@ -57,6 +57,20 @@ ifeq ($(ACPI),1) CFLAGS+=-D"WOLFBOOT_ACPI" endif +# TPM NV version floor and boot audit counters (see docs/Targets.md). +# HALT_ON_FAILURE also halts when the TPM state cannot be read or locked. +ifeq ($(ANTI_ROLLBACK),1) + ifeq ($(filter $(TARGET),x86_fsp_qemu kontron_vx3060_s2 nai_68int6),) + $(error ANTI_ROLLBACK=1 is only wired into the x86 FSP disk boot) + endif + CFLAGS+=-D"WOLFBOOT_ANTI_ROLLBACK" + # The backend is TPM NV (src/boot_state_tpm.c), so wolfTPM is required. + WOLFTPM:=1 + ifeq ($(ANTI_ROLLBACK_HALT_ON_FAILURE),1) + CFLAGS+=-D"WOLFBOOT_ANTI_ROLLBACK_HALT_ON_FAILURE" + endif +endif + ifeq ($(USE_CLANG),1) ifeq ($(USE_GCC),1) $(error USE_CLANG=1 is incompatible with USE_GCC=1; set USE_GCC=0) diff --git a/src/boot_state.c b/src/boot_state.c new file mode 100644 index 00000000..9c44dafb --- /dev/null +++ b/src/boot_state.c @@ -0,0 +1,264 @@ +/* boot_state.c + * + * Persistent anti-rollback reference and boot audit counters. + * + * 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 2 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 + */ + +#include "user_settings.h" +#include "boot_state.h" +#include "printf.h" +#include "loader.h" +#include + +#ifdef WOLFBOOT_ANTI_ROLLBACK + +#define CRC32_POLY_REVERSED 0xEDB88320UL +#define CRC32_INIT 0xFFFFFFFFUL +#define CRC32_BITS_PER_BYTE 8 + +/* Bitwise CRC-32 (IEEE 802.3). Table-free: the record is 40 bytes and this + * runs once or twice per boot, so the table is not worth the footprint. */ +static uint32_t boot_state_crc32(const uint8_t *buf, uint32_t len) +{ + uint32_t crc = CRC32_INIT; + uint32_t i; + int bit; + + for (i = 0; i < len; i++) { + crc ^= (uint32_t)buf[i]; + for (bit = 0; bit < CRC32_BITS_PER_BYTE; bit++) { + if (crc & 1U) { + crc = (crc >> 1) ^ CRC32_POLY_REVERSED; + } + else { + crc >>= 1; + } + } + } + return crc ^ CRC32_INIT; +} + +/* The CRC covers every byte of the record except the trailing crc field. */ +static uint32_t boot_state_compute_crc(const struct boot_state *st) +{ + return boot_state_crc32((const uint8_t *)st, + (uint32_t)(sizeof(struct boot_state) + - sizeof(st->crc))); +} + +static void boot_state_log(const char *msg) +{ + wolfBoot_printf("%s\r\n", msg); +} + +static void boot_state_log_val(const char *msg, uint32_t value) +{ + wolfBoot_printf("%s%u\r\n", msg, value); +} + +int boot_state_load(struct boot_state *st) +{ + int ret; + + if (st == NULL) { + return BOOT_STATE_UNAVAILABLE; + } + + memset(st, 0, sizeof(struct boot_state)); + + ret = boot_state_backend_read((uint8_t *)st, sizeof(struct boot_state)); + if (ret == BOOT_STATE_UNPROVISIONED) { + return BOOT_STATE_UNPROVISIONED; + } + /* A present but malformed record cannot be trusted: corrupt, which halts, + * not unavailable. */ + if (ret == BOOT_STATE_CORRUPT) { + return BOOT_STATE_CORRUPT; + } + if (ret != 0) { + return BOOT_STATE_UNAVAILABLE; + } + + /* Storage exists. From here on anything unexpected means the reference + * cannot be trusted, which is the one condition that must stop the boot. */ + if (st->magic != BOOT_STATE_MAGIC) { + return BOOT_STATE_CORRUPT; + } + if (st->struct_version != BOOT_STATE_VERSION) { + return BOOT_STATE_CORRUPT; + } + if (st->crc != boot_state_compute_crc(st)) { + return BOOT_STATE_CORRUPT; + } + return BOOT_STATE_OK; +} + +/* Persist, refreshing the CRC first. Failures are logged but not fatal: losing + * an audit write must not turn a good image into an unbootable unit. */ +static void boot_state_save(struct boot_state *st) +{ + st->magic = BOOT_STATE_MAGIC; + st->struct_version = BOOT_STATE_VERSION; + st->reserved = 0; + st->crc = boot_state_compute_crc(st); + + if (boot_state_backend_write((const uint8_t *)st, + sizeof(struct boot_state)) != 0) { +#if defined(WOLFBOOT_ANTI_ROLLBACK_HALT_ON_FAILURE) + boot_state_log("could not persist anti-rollback state, refusing to boot"); + /* Never returns. */ + wolfBoot_panic(); +#else + boot_state_log("WARNING: could not persist boot state"); +#endif + } +} + +/* Load for update. A corrupt reference halts here, which is the whole point + * of the check. Unprovisioned is a clean slate, not a fault. */ +static int boot_state_load_for_update(struct boot_state *st) +{ + int status = boot_state_load(st); + + if (status == BOOT_STATE_CORRUPT) { + boot_state_log("Version reference is corrupt, refusing to boot"); + /* Never returns. */ + wolfBoot_panic(); + } + if (status == BOOT_STATE_UNAVAILABLE) { +#if defined(WOLFBOOT_ANTI_ROLLBACK_HALT_ON_FAILURE) + boot_state_log("anti-rollback state unavailable, refusing to boot"); + /* Never returns. */ + wolfBoot_panic(); +#else + boot_state_log("WARNING: boot state backend unavailable"); +#endif + } + return status; +} + +static int boot_state_update(uint32_t booted_version) +{ + struct boot_state st; + struct boot_state before; + int status; + + status = boot_state_load_for_update(&st); + if (status == BOOT_STATE_UNAVAILABLE) { + return status; + } + + if (status == BOOT_STATE_UNPROVISIONED) { + /* Trust on first use: this image defines the floor. */ + memset(&st, 0, sizeof(st)); + st.anchor = booted_version; + st.record = booted_version; + boot_state_log_val("Anti-rollback anchor initialised at version ", + booted_version); + boot_state_save(&st); + return status; + } + + /* Never persist over anything but a trustworthy record (a corrupt one + * already halted above). */ + if (status != BOOT_STATE_OK) { + return status; + } + + /* Snapshot so the write can be skipped when nothing actually changes. */ + memcpy(&before, &st, sizeof(before)); + + if (booted_version < st.anchor) { + /* Explicitly allowed: an older but validly signed image boots. The + * audit event is the deliverable, not a veto. */ + st.rollback_count++; + st.last_rollback_from = st.anchor; + st.last_rollback_to = booted_version; + boot_state_log_val("ROLLBACK: booting version ", booted_version); + boot_state_log_val("ROLLBACK: highest version previously booted was ", + st.anchor); + boot_state_log_val("ROLLBACK: total rollbacks recorded ", + st.rollback_count); + } + else if (booted_version > st.anchor) { + st.anchor = booted_version; + } + + st.record = booted_version; + + /* Skip the NV write when nothing changed (the CRC is recomputed on + * save). */ + if (memcmp(&before, &st, sizeof(st) - sizeof(st.crc)) == 0) { + return status; + } + + boot_state_save(&st); + return status; +} + +void boot_state_on_boot(uint32_t booted_version) +{ + (void)boot_state_update(booted_version); + boot_state_lock(); +} + +void boot_state_lock(void) +{ + /* On every boot, even with nothing written or the state unreadable, so + * the OS can never move the floor. */ + if (boot_state_backend_lock() != 0) { +#if defined(WOLFBOOT_ANTI_ROLLBACK_HALT_ON_FAILURE) + boot_state_log("could not lock anti-rollback state, refusing to boot"); + /* Never returns. */ + wolfBoot_panic(); +#else + boot_state_log("WARNING: could not lock boot state"); +#endif + } +} + +void boot_state_on_failure(uint32_t fail_code) +{ + struct boot_state st; + int status; + + status = boot_state_load(&st); + if (status == BOOT_STATE_UNAVAILABLE) { + return; + } + if (status == BOOT_STATE_CORRUPT) { + /* Do not persist: a fresh record over a corrupt one would reset the + * floor to zero on the next boot. */ + boot_state_log("Boot failure with a corrupt version reference; " + "not persisting over it"); + return; + } + if (status == BOOT_STATE_UNPROVISIONED) { + /* Nothing stored yet: start a record to capture the failure. */ + memset(&st, 0, sizeof(st)); + } + + st.boot_fail_count++; + st.last_fail_code = fail_code; + boot_state_log_val("Recording boot failure, code ", fail_code); + boot_state_save(&st); +} + +#endif /* WOLFBOOT_ANTI_ROLLBACK */ diff --git a/src/boot_state_tpm.c b/src/boot_state_tpm.c new file mode 100644 index 00000000..12920561 --- /dev/null +++ b/src/boot_state_tpm.c @@ -0,0 +1,156 @@ +/* boot_state_tpm.c + * + * TPM NV backend for the anti-rollback reference and boot audit counters. + * + * 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 2 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 + */ + +/* TPM NV storage backend for src/boot_state.c; another root of trust can + * replace this file without touching the policy or src/tpm.c. */ + +#include "user_settings.h" + +#ifdef WOLFBOOT_ANTI_ROLLBACK + +#include "boot_state.h" +#include "tpm.h" +#include +#include + +#define BOOT_STATE_NV_WRITE_MASK (TPMA_NV_PLATFORMCREATE | TPMA_NV_PPWRITE | \ + TPMA_NV_OWNERWRITE | TPMA_NV_AUTHWRITE | TPMA_NV_POLICYWRITE | \ + TPMA_NV_TPM_NT) +#define BOOT_STATE_NV_WRITE_ATTR (TPMA_NV_PLATFORMCREATE | TPMA_NV_PPWRITE) + +int boot_state_backend_read(uint8_t *buf, uint32_t len) +{ + WOLFTPM2_DEV *dev = &wolftpm_dev; + WOLFTPM2_NV nv; + word32 readSz; + int rc; + + memset(&nv, 0, sizeof(nv)); + nv.handle.hndl = WOLFBOOT_TPM_BOOT_STATE_NV_BASE; + + /* Only an undefined index (format-1 TPM_RC_HANDLE) is unprovisioned; any + * other error must not reset the floor through trust-on-first-use. */ + rc = wolfTPM2_NVOpen(dev, &nv, WOLFBOOT_TPM_BOOT_STATE_NV_BASE, NULL, 0); + if (rc != 0) { + if ((rc & RC_FMT1) && ((rc & RC_MAX_FMT1) == TPM_RC_HANDLE)) { + return BOOT_STATE_UNPROVISIONED; + } + return BOOT_STATE_UNAVAILABLE; + } + /* Only an index this backend created can hold the floor: platform-created + * and writable by the platform hierarchy alone. */ + if ((nv.attributes & BOOT_STATE_NV_WRITE_MASK) != BOOT_STATE_NV_WRITE_ATTR) { + return BOOT_STATE_CORRUPT; + } + + wolfTPM2_SetAuthHandle(dev, 0, &nv.handle); + readSz = len; + rc = wolfTPM2_NVReadAuth(dev, &nv, WOLFBOOT_TPM_BOOT_STATE_NV_BASE, + buf, &readSz, 0); + /* Clear the auth slot the read set, so it does not carry into later ops. */ + wolfTPM2_UnsetAuth(dev, 0); + /* An index smaller than the record (TPM_RC_NV_RANGE) is malformed: + * corrupt, which halts, not unavailable. */ + if (rc == TPM_RC_NV_RANGE) { + return BOOT_STATE_CORRUPT; + } + if (rc != 0) { + return BOOT_STATE_UNAVAILABLE; + } + if (readSz != len) { + return BOOT_STATE_CORRUPT; + } + return 0; +} + +int boot_state_backend_write(const uint8_t *buf, uint32_t len) +{ + WOLFTPM2_DEV *dev = &wolftpm_dev; + WOLFTPM2_HANDLE parent; + WOLFTPM2_NV nv; + NV_Write_In in; + word32 nvAttributes = 0; + int rc; + + if (len > sizeof(in.data.buffer)) { + return -1; + } + memset(&parent, 0, sizeof(parent)); + memset(&nv, 0, sizeof(nv)); + parent.hndl = TPM_RH_PLATFORM; + nv.handle.hndl = WOLFBOOT_TPM_BOOT_STATE_NV_BASE; + + wolfTPM2_GetNvAttributesTemplate(TPM_RH_PLATFORM, &nvAttributes); + nvAttributes &= ~TPMA_NV_AUTHWRITE; + + /* Created on first write (trust-on-first-use). No TPMA_NV_WRITEDEFINE: + * the record is rewritten as versions change. */ + rc = wolfTPM2_NVCreateAuth(dev, &parent, &nv, + WOLFBOOT_TPM_BOOT_STATE_NV_BASE, + nvAttributes, len, NULL, 0); + if (rc == TPM_RC_NV_DEFINED) { + rc = 0; + } + wolfTPM2_UnsetAuth(dev, 1); + if (rc != 0) { + return -1; + } + + /* TPMA_NV_PPWRITE: the platform authorizes the write. Its auth is empty + * until boot_state_backend_lock() disables the hierarchy. */ + memset(&in, 0, sizeof(in)); + in.authHandle = TPM_RH_PLATFORM; + in.nvIndex = WOLFBOOT_TPM_BOOT_STATE_NV_BASE; + in.data.size = (UINT16)len; + memcpy(in.data.buffer, buf, len); + wolfTPM2_SetAuthPassword(dev, 0, NULL); + rc = TPM2_NV_Write(&in); + wolfTPM2_UnsetAuth(dev, 0); + if (rc != 0) { + return -1; + } + return 0; +} + +int boot_state_backend_lock(void) +{ + WOLFTPM2_DEV *dev = &wolftpm_dev; + HierarchyControl_In in; + int rc; + + /* Disable the platform hierarchy until the next Startup(CLEAR) (TCG PC + * Client Platform spec, section 10): the OS cannot write or undefine. */ + memset(&in, 0, sizeof(in)); + in.authHandle = TPM_RH_PLATFORM; + in.enable = TPM_RH_PLATFORM; + in.state = 0; + wolfTPM2_SetAuthPassword(dev, 0, NULL); + rc = TPM2_HierarchyControl(&in); + wolfTPM2_UnsetAuth(dev, 0); + if (rc != 0) { + return -1; + } + return 0; +} + +#endif /* WOLFBOOT_ANTI_ROLLBACK */ diff --git a/src/tpm.c b/src/tpm.c index 047b80c0..c44c1767 100644 --- a/src/tpm.c +++ b/src/tpm.c @@ -1781,11 +1781,12 @@ int wolfBoot_tpm2_init(void) void wolfBoot_tpm2_deinit(void) { #ifdef WOLFBOOT_TPM_KEYSTORE - #if !defined(ARCH_SIM) && !defined(WOLFBOOT_TPM_NO_CHG_PLAT_AUTH) - /* Enable parameter encryption for session */ - int rc = wolfTPM2_SetAuthSession(&wolftpm_dev, 0, &wolftpm_session, - (TPMA_SESSION_decrypt | TPMA_SESSION_encrypt | - TPMA_SESSION_continueSession)); + /* Anti-rollback has already disabled the platform hierarchy. */ + #if !defined(ARCH_SIM) && !defined(WOLFBOOT_TPM_NO_CHG_PLAT_AUTH) && \ + !defined(WOLFBOOT_ANTI_ROLLBACK) + /* Password (empty platform auth) in slot 0; ChangePlatformAuth puts the + * parameter-encryption session in slot 1. */ + int rc = wolfTPM2_SetAuthPassword(&wolftpm_dev, 0, NULL); if (rc == 0) { /* Change platform auth to random value, to prevent application * from being able to use platform hierarchy. This is defined in diff --git a/src/update_disk.c b/src/update_disk.c index 8cdacb7a..d0343aaa 100644 --- a/src/update_disk.c +++ b/src/update_disk.c @@ -83,6 +83,10 @@ static uint8_t disk_encrypt_nonce[ENCRYPT_NONCE_SIZE]; #include "pci.h" #include "x86/tgl_fsp.h" +#ifdef WOLFBOOT_ANTI_ROLLBACK +#include "boot_state.h" +#endif + #ifdef TARGET_kontron_vx3060_s2 #define BOOT_PART_A 5 #define BOOT_PART_B 6 @@ -829,6 +833,9 @@ void RAMFUNCTION wolfBoot_start(void) #ifdef DISK_ENCRYPT disk_decrypted_header_clear(dec_hdr); disk_crypto_clear(); +#endif +#if defined(WOLFBOOT_ANTI_ROLLBACK) && defined(WOLFBOOT_FSP) + boot_state_on_failure(BOOT_FAIL_NO_IMAGE); #endif wolfBoot_panic(); #endif @@ -898,6 +905,9 @@ void RAMFUNCTION wolfBoot_start(void) #ifdef DISK_ENCRYPT disk_decrypted_header_clear(dec_hdr); disk_crypto_clear(); +#endif +#if defined(WOLFBOOT_ANTI_ROLLBACK) && defined(WOLFBOOT_FSP) + boot_state_on_failure(BOOT_FAIL_ROLLBACK); #endif wolfBoot_panic(); return; @@ -1075,6 +1085,9 @@ void RAMFUNCTION wolfBoot_start(void) disk_crypto_clear(); #endif wolfBoot_printf("Unable to find a valid partition!\r\n"); +#if defined(WOLFBOOT_ANTI_ROLLBACK) && defined(WOLFBOOT_FSP) + boot_state_on_failure(BOOT_FAIL_VERIFY_BOTH); +#endif wolfBoot_panic(); return; } @@ -1307,6 +1320,26 @@ void RAMFUNCTION wolfBoot_start(void) #ifndef WOLFBOOT_SKIP_BOOT_VERIFY PART_SANITY_CHECK(&os_image); #endif +#if defined(WOLFBOOT_ANTI_ROLLBACK) && defined(WOLFBOOT_FSP) + /* Last step before handoff, so a failure above never moves the floor. + * The golden slot only locks. */ +#ifdef DISK_GOLDEN_SLOT + if (SLOT_IS_GOLDEN(selected)) + boot_state_lock(); + else +#endif + { + /* Use the version from the verified header, not the early + * pre-verification reads. */ + uint32_t booted_ver; +#ifdef DISK_ENCRYPT + booted_ver = get_decrypted_blob_version(dec_hdr); +#else + booted_ver = wolfBoot_get_blob_version((uint8_t *)hdr_ptr); +#endif + boot_state_on_boot(booted_ver); + } +#endif #ifdef DISK_ENCRYPT disk_decrypted_header_clear(dec_hdr); disk_crypto_clear(); diff --git a/tools/scripts/x86_fsp/qemu/qemu.sh b/tools/scripts/x86_fsp/qemu/qemu.sh index d803fbad..b3624cc2 100755 --- a/tools/scripts/x86_fsp/qemu/qemu.sh +++ b/tools/scripts/x86_fsp/qemu/qemu.sh @@ -40,7 +40,7 @@ set -e set -x # Parse command line options -while getopts "d:wpt" opt; do +while getopts "d:wptc" opt; do case "$opt" in d) DEBUG_STAGE="$OPTARG" diff --git a/tools/scripts/x86_fsp/qemu/qemu_build_fsp.sh b/tools/scripts/x86_fsp/qemu/qemu_build_fsp.sh index ac1c6e2e..d3ee84b7 100755 --- a/tools/scripts/x86_fsp/qemu/qemu_build_fsp.sh +++ b/tools/scripts/x86_fsp/qemu/qemu_build_fsp.sh @@ -12,6 +12,9 @@ CONFIG_FILE=${CONFIG_FILE:-"${WOLFBOOT_DIR}/.config"} set -e +# Identity for the patch commits in the scratch edk2 clone only +GIT_ID="-c user.name=wolfBoot -c user.email=wolfboot@wolfssl.com" + if [ ! -f "${WOLFBOOT_DIR}/arch.mk" ]; then echo "Error: Current directory is not the root directory of the source tree" exit 1 @@ -37,7 +40,7 @@ download_edkii() { git clone "${EDKII_REPO}" edk2) else (cd "$WORK_DIR/edk2" && - git stash save --include-untracked "Auto-stashed on $(date)" && + git $GIT_ID stash save --include-untracked "Auto-stashed on $(date)" && git clean -fd) # This will remove untracked files/directories fi @@ -49,9 +52,9 @@ download_edkii() { download_sbl_patch_and_patch_edkii() { (cd "$WORK_DIR/edk2" && curl -L -o 0001-Build-QEMU-FSP-2.0-binaries.patch ${SBL_PATCH_URL}; - git am --keep-cr --whitespace=nowarn 0001-Build-QEMU-FSP-2.0-binaries.patch) + git $GIT_ID am --keep-cr --whitespace=nowarn 0001-Build-QEMU-FSP-2.0-binaries.patch) (cd "$WORK_DIR/edk2" && - git am --keep-cr --whitespace=nowarn "${SCRIPT_DIR}"/0002-add-W-no-warnings-to-compile-with-gcc-12.patch "${SCRIPT_DIR}"/0003-disable-optmizaion-patch-for-edk2.patch "${SCRIPT_DIR}"/0004-fix-PatchFv-make-regex-match-both-8-and-16-length-ad.patch; + git $GIT_ID am --keep-cr --whitespace=nowarn "${SCRIPT_DIR}"/0002-add-W-no-warnings-to-compile-with-gcc-12.patch "${SCRIPT_DIR}"/0003-disable-optmizaion-patch-for-edk2.patch "${SCRIPT_DIR}"/0004-fix-PatchFv-make-regex-match-both-8-and-16-length-ad.patch; mkdir -p MdeModulePkg/Library/BrotliCustomDecompressLib/brotli/c/include/) } diff --git a/tools/scripts/x86_fsp/qemu/test_qemu.sh b/tools/scripts/x86_fsp/qemu/test_qemu.sh index ff29bf45..63545daa 100755 --- a/tools/scripts/x86_fsp/qemu/test_qemu.sh +++ b/tools/scripts/x86_fsp/qemu/test_qemu.sh @@ -4,7 +4,7 @@ set -e WOLFBOOT_DIR=$(pwd) # Parse command line options -while getopts "bf" opt; do +while getopts "bfr" opt; do case "$opt" in b) REBUILD_ONLY=true @@ -12,10 +12,14 @@ while getopts "bf" opt; do f) FS_BOOT=true ;; + r) + ANTI_ROLLBACK=true + ;; *) - echo "Usage: $0 [-b] [-f]" + echo "Usage: $0 [-b] [-f] [-r]" echo "-b : rebuild only (skip FSP build, key gen, ecc)" echo "-f : boot from a file on a read-only filesystem (DISK_FS)" + echo "-r : TPM-NV anti-rollback across three boots" exit 1 ;; esac @@ -25,6 +29,8 @@ done if [ "$REBUILD_ONLY" != true ]; then if [ "$FS_BOOT" = true ]; then CONFIG=x86_fsp_qemu_fs.config + elif [ "$ANTI_ROLLBACK" = true ]; then + CONFIG=x86_fsp_qemu_antirollback.config else CONFIG=x86_fsp_qemu_seal.config fi @@ -33,7 +39,7 @@ if [ "$REBUILD_ONLY" != true ]; then cp "config/examples/${CONFIG}" .config ./tools/scripts/x86_fsp/qemu/qemu_build_fsp.sh make keytools - if [ "$FS_BOOT" = true ]; then + if [ "$FS_BOOT" = true ] || [ "$ANTI_ROLLBACK" = true ]; then # The filesystem config does not use the TPM, so only the image key is # needed here. It must match SIGN= in x86_fsp_qemu_fs.config (ECC256), # or the generated src/keystore.c raises a key-algorithm mismatch. @@ -46,7 +52,7 @@ if [ "$REBUILD_ONLY" != true ]; then fi fi -if [ "$FS_BOOT" = true ]; then +if [ "$FS_BOOT" = true ] || [ "$ANTI_ROLLBACK" = true ]; then make else # manual add ECC256 for TPM @@ -56,7 +62,7 @@ fi # test-app make test-app/image.elf -if [ "$FS_BOOT" != true ]; then +if [ "$FS_BOOT" != true ] && [ "$ANTI_ROLLBACK" != true ]; then # compute pcr0 value PCR0=$(python ./tools/scripts/x86_fsp/compute_pcr.py --target qemu wolfboot_stage1.bin | tail -n 1) echo $PCR0 @@ -67,11 +73,66 @@ fi if [ "$FS_BOOT" = true ]; then FS=1 IMAGE=test-app/image.elf SIGN=--ecc256 \ ./tools/scripts/x86_fsp/qemu/make_hd.sh +elif [ "$ANTI_ROLLBACK" = true ]; then + IMAGE=test-app/image.elf SIGN=--ecc256 \ + ./tools/scripts/x86_fsp/qemu/make_hd.sh else IMAGE=test-app/image.elf SIGN=--ecc384 \ ./tools/scripts/x86_fsp/qemu/make_hd.sh fi +# Boot once with the swtpm state kept from the previous boot, wait for the +# test app, and leave the console log in /tmp/qemu_output. +run_qemu_tpm() { + ./tools/scripts/x86_fsp/qemu/qemu.sh -t -p $1 > /tmp/qemu_output 2>&1 & + for i in $(seq 1 30); do + sleep 1 + grep -q "wolfBoot QEMU x86 FSP test app" /tmp/qemu_output && break + grep -q "PANIC" /tmp/qemu_output && break + done + timeout 5 echo 'quit' > /tmp/qemu_mon.in || true + wait + cat /tmp/qemu_output +} + +expect_log() { + if ! grep -q "$1" /tmp/qemu_output; then + echo -e "\e[31mTEST FAILED: missing '$1'\e[0m" + exit 255 + fi +} + +reject_log() { + if grep -q "$1" /tmp/qemu_output; then + echo -e "\e[31mTEST FAILED: unexpected '$1'\e[0m" + exit 255 + fi +} + +if [ "$ANTI_ROLLBACK" = true ]; then + # Boot 1, fresh TPM: slot B (version 2) boots and anchors the floor. + run_qemu_tpm -c + expect_log "Anti-rollback anchor initialised at version 2" + expect_log "wolfBoot QEMU x86 FSP test app" + + # Boot 2: both slots v1. It boots, the floor survived the TPM restart and + # the downgrade is written (platform hierarchy enabled again at startup). + dd if=image_v1_signed.bin of=app.bin bs=512 seek=34816 conv=notrunc + run_qemu_tpm + expect_log "ROLLBACK: booting version 1" + expect_log "ROLLBACK: total rollbacks recorded 1" + expect_log "wolfBoot QEMU x86 FSP test app" + + # Boot 3: the counter keeps counting across boots. + run_qemu_tpm + expect_log "ROLLBACK: total rollbacks recorded 2" + expect_log "wolfBoot QEMU x86 FSP test app" + reject_log "WARNING: could not" + reject_log "PANIC" + echo -e "\e[32mTEST OK\e[0m" + exit 0 +fi + echo "RUNNING QEMU" # launch qemu in background # -t starts an emulated TPM. The seal path needs it; the filesystem config diff --git a/tools/scripts/x86_fsp/tgl/tgl_download_fsp.sh b/tools/scripts/x86_fsp/tgl/tgl_download_fsp.sh index ad6191f6..12d3f00f 100755 --- a/tools/scripts/x86_fsp/tgl/tgl_download_fsp.sh +++ b/tools/scripts/x86_fsp/tgl/tgl_download_fsp.sh @@ -54,7 +54,9 @@ download_split_tool() { } patch_tgl_fsp() { - (cd "$WORK_DIR"/FSP && git am --keep-cr "${PATCH}") + # Identity for the patch commit in the scratch FSP clone only + (cd "$WORK_DIR"/FSP && git -c user.name=wolfBoot \ + -c user.email=wolfboot@wolfssl.com am --keep-cr "${PATCH}") } rebase_fsp_component() { diff --git a/tools/unit-tests/Makefile b/tools/unit-tests/Makefile index 5ccc3509..5a4a9c43 100644 --- a/tools/unit-tests/Makefile +++ b/tools/unit-tests/Makefile @@ -240,6 +240,8 @@ TESTS+=unit-xspi-tfd-index TESTS+=unit-pic32-pfswap TESTS+=unit-secure-handoff TESTS+=unit-imx-rt700 +TESTS+=unit-boot-state +TESTS+=unit-boot-state-halt # linux_loader.c is x86 32-bit only, so its unit tests need a working 32-bit # (multilib) toolchain. Probe whether "gcc -m32" can link, and only add the @@ -868,6 +870,20 @@ unit-linux-loader-initrd: ../../include/target.h unit-linux-loader-initrd.c -DWOLFBOOT_LOAD_BASE=0x100000 \ -Wl,--defsym,_end_wb=_start_wolfboot+0x100000 +# unit-boot-state exercises the anti-rollback state machine in src/boot_state.c +# through a mock storage backend. Built standalone (not via the common CFLAGS) +# so the stubs-boot-state/ headers shadow user_settings.h/printf.h/loader.h and the +# host build needs no target configuration. boot_state.c has no inline asm, so +# no -m32 is required. +unit-boot-state: unit-boot-state.c ../../src/boot_state.c + gcc -o $@ unit-boot-state.c -Istubs-boot-state -I../../src -I../../include \ + -g -Wall -DUNIT_TEST -DWOLFBOOT_ANTI_ROLLBACK + +unit-boot-state-halt: unit-boot-state.c ../../src/boot_state.c + gcc -o $@ unit-boot-state.c -Istubs-boot-state -I../../src -I../../include \ + -g -Wall -DUNIT_TEST -DWOLFBOOT_ANTI_ROLLBACK \ + -DWOLFBOOT_ANTI_ROLLBACK_HALT_ON_FAILURE + unit-boot-x86-fsp: ../../include/target.h unit-boot-x86_fsp.c gcc -o $@ $^ $(CFLAGS) -DWOLFBOOT_LOAD_BASE=0x100000 -DWOLFBOOT_FSP \ -DUCODE0_ADDRESS=0 -ffunction-sections -fdata-sections $(LDFLAGS) \ @@ -1700,14 +1716,14 @@ unit-p1021-read-badblock: unit-p1021-read-badblock.c p1021_read_extract.h \ gcc -o $@ unit-p1021-read-badblock.c $(CFLAGS) $(LDFLAGS) # unit-kontron-tgl-spi runs the real tgl_lock_bios_region() from -# hal/kontron_vx3060_s2.c against mocked PCI config space and an +# hal/x86_fsp_tgl.c against mocked PCI config space and an # MMIO array at the SPI BAR address (F-12104: the lock was never # applied - no hal_flash_protect() override - and targeted the # wrong register space). -kontron_spi_extract.h: ../../hal/kontron_vx3060_s2.c +kontron_spi_extract.h: ../../hal/x86_fsp_tgl.c sed -n '/^#define SPI_PCI_DEV/,/^#define SPI_FLOCKDN/p' $< > $@ -kontron_spi_fn_extract.h: ../../hal/kontron_vx3060_s2.c +kontron_spi_fn_extract.h: ../../hal/x86_fsp_tgl.c sed -n '/^int tgl_lock_bios_region/,/^}/p' $< > $@ sed -n '/^int hal_flash_protect/,/^}/p' $< >> $@ diff --git a/tools/unit-tests/stubs-boot-state/loader.h b/tools/unit-tests/stubs-boot-state/loader.h new file mode 100644 index 00000000..76c8e4a6 --- /dev/null +++ b/tools/unit-tests/stubs-boot-state/loader.h @@ -0,0 +1,27 @@ +/* hal.h (host test stub) + * + * 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 2 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 + */ + +/* The build's loader.h pulls in hooks.h and the platform panic; + * boot_state.c only needs the declaration of the panic it calls. */ +#ifndef UNIT_BOOT_STATE_LOADER_H +#define UNIT_BOOT_STATE_LOADER_H +void wolfBoot_panic(void); +#endif diff --git a/tools/unit-tests/stubs-boot-state/printf.h b/tools/unit-tests/stubs-boot-state/printf.h new file mode 100644 index 00000000..956a8877 --- /dev/null +++ b/tools/unit-tests/stubs-boot-state/printf.h @@ -0,0 +1,27 @@ +/* printf.h (host test stub) + * + * 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 2 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 + */ + +/* boot_state.c logs its decisions with wolfBoot_printf; the host test asserts + * on return values, so the log is dropped to keep PASS/FAIL output clean. */ +#ifndef UNIT_BOOT_STATE_PRINTF_H +#define UNIT_BOOT_STATE_PRINTF_H +#define wolfBoot_printf(...) do {} while (0) +#endif diff --git a/tools/unit-tests/stubs-boot-state/user_settings.h b/tools/unit-tests/stubs-boot-state/user_settings.h new file mode 100644 index 00000000..9a332b3b --- /dev/null +++ b/tools/unit-tests/stubs-boot-state/user_settings.h @@ -0,0 +1,26 @@ +/* user_settings.h (host test stub) + * + * 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 2 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 + */ + +/* Shadows the build's user_settings.h so boot_state.c builds on the host + * without a target configuration. */ +#ifndef UNIT_BOOT_STATE_USER_SETTINGS_H +#define UNIT_BOOT_STATE_USER_SETTINGS_H +#endif diff --git a/tools/unit-tests/unit-boot-state.c b/tools/unit-tests/unit-boot-state.c new file mode 100644 index 00000000..df09133e --- /dev/null +++ b/tools/unit-tests/unit-boot-state.c @@ -0,0 +1,296 @@ +/* unit-boot-state.c + * + * Host unit tests for the anti-rollback state machine in src/boot_state.c: + * the bitwise CRC-32, the load/validate path and its BOOT_STATE_* codes, + * trust-on-first-use anchoring, the rollback-audit path and the + * skip-write-when-unchanged comparison. The storage backend is isolated behind + * boot_state_backend_{read,write}(), so a mock backend exercises the whole + * policy on the host. Built with plain gcc; boot_state.c has no inline asm. + * + * 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 2 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 + */ + +#include +#include +#include +#include + +#include "boot_state.h" + +/* Mock backend and halt stub. A nonzero mock_read_code is returned by the + * read; zero serves the record from mock_nv. */ +static uint8_t mock_nv[sizeof(struct boot_state)]; +static int mock_read_code; +static int mock_write_fail; +static int mock_write_called; +static uint8_t mock_write_buf[sizeof(struct boot_state)]; +static jmp_buf halt_env; +static int halted; +#if defined(WOLFBOOT_ANTI_ROLLBACK_HALT_ON_FAILURE) +static const int halt_on_failure = 1; +#else +static const int halt_on_failure = 0; +#endif +static int mock_lock_called; +static int mock_lock_fail; + +int boot_state_backend_read(uint8_t *buf, uint32_t len) +{ + if (mock_read_code != 0) + return mock_read_code; + memcpy(buf, mock_nv, len); + return 0; +} + +int boot_state_backend_write(const uint8_t *buf, uint32_t len) +{ + mock_write_called++; + if (len <= sizeof(mock_write_buf)) { + memcpy(mock_write_buf, buf, len); + memcpy(mock_nv, buf, len); + } + return mock_write_fail ? -1 : 0; +} + +int boot_state_backend_lock(void) +{ + mock_lock_called++; + return mock_lock_fail ? -1 : 0; +} + +void wolfBoot_panic(void) +{ + halted = 1; + longjmp(halt_env, 1); +} + +#include "../../src/boot_state.c" + +static int fails; +#define CHECK(cond, msg) \ + do { if (!(cond)) { printf("FAIL: %s\n", (msg)); fails++; } } while (0) + +static void make_valid(struct boot_state *st, uint32_t anchor, uint32_t record) +{ + memset(st, 0, sizeof(*st)); + st->magic = BOOT_STATE_MAGIC; + st->struct_version = BOOT_STATE_VERSION; + st->anchor = anchor; + st->record = record; + st->crc = boot_state_compute_crc(st); +} + +static void reset_mock(void) +{ + memset(mock_nv, 0, sizeof(mock_nv)); + mock_read_code = 0; + mock_write_fail = 0; + mock_write_called = 0; + memset(mock_write_buf, 0, sizeof(mock_write_buf)); + halted = 0; + mock_lock_called = 0; + mock_lock_fail = 0; +} + +int main(void) +{ + struct boot_state st, rec; + + /* CRC-32 known-answer: the IEEE 802.3 check value of "123456789". */ + CHECK(boot_state_crc32((const uint8_t *)"123456789", 9) == 0xCBF43926u, + "crc32 KAT"); + + /* Unprovisioned passes through unchanged. */ + reset_mock(); + mock_read_code = BOOT_STATE_UNPROVISIONED; + CHECK(boot_state_load(&st) == BOOT_STATE_UNPROVISIONED, + "load unprovisioned"); + + /* A well-formed record loads OK with its fields intact. */ + reset_mock(); + make_valid(&rec, 5, 5); + memcpy(mock_nv, &rec, sizeof(rec)); + CHECK(boot_state_load(&st) == BOOT_STATE_OK, "load ok"); + CHECK(st.anchor == 5 && st.record == 5, "load ok fields"); + + /* Wrong magic is corrupt (checked before the CRC). */ + reset_mock(); + make_valid(&rec, 5, 5); + rec.magic ^= 0xFFu; + memcpy(mock_nv, &rec, sizeof(rec)); + CHECK(boot_state_load(&st) == BOOT_STATE_CORRUPT, "load bad magic"); + + /* A CRC mismatch is corrupt. */ + reset_mock(); + make_valid(&rec, 7, 7); + rec.crc ^= 0xA5A5A5A5u; + memcpy(mock_nv, &rec, sizeof(rec)); + CHECK(boot_state_load(&st) == BOOT_STATE_CORRUPT, "load bad crc"); + + /* A present but malformed record (short read) is CORRUPT, not + * UNAVAILABLE. */ + reset_mock(); + mock_read_code = BOOT_STATE_CORRUPT; + CHECK(boot_state_load(&st) == BOOT_STATE_CORRUPT, "load backend corrupt"); + + /* An unreachable backend is UNAVAILABLE. */ + reset_mock(); + mock_read_code = BOOT_STATE_UNAVAILABLE; + CHECK(boot_state_load(&st) == BOOT_STATE_UNAVAILABLE, + "load backend unavailable"); + + /* An unreachable backend writes nothing but is still locked. + * HALT_ON_FAILURE refuses to boot without it. */ + reset_mock(); + mock_read_code = BOOT_STATE_UNAVAILABLE; + if (setjmp(halt_env) == 0) { + boot_state_on_boot(7); + CHECK(!halt_on_failure, "unavailable did not halt with HALT_ON_FAILURE"); + } + else { + CHECK(halt_on_failure, "unavailable halted without HALT_ON_FAILURE"); + } + CHECK(mock_write_called == 0, "unavailable does not write"); + CHECK(mock_lock_called == (halt_on_failure ? 0 : 1), + "unavailable still locks unless it halted"); + + /* boot_state_lock (golden slot) locks without reading or writing. */ + reset_mock(); + boot_state_lock(); + CHECK(mock_write_called == 0 && mock_lock_called == 1, "lock only"); + + /* A failed write warns and still locks, or halts with HALT_ON_FAILURE. */ + reset_mock(); + mock_read_code = BOOT_STATE_UNPROVISIONED; + mock_write_fail = 1; + if (setjmp(halt_env) == 0) { + boot_state_on_boot(7); + CHECK(!halt_on_failure, "write failure did not halt with HALT_ON_FAILURE"); + CHECK(mock_lock_called == 1, "write failure still locks"); + } + else { + CHECK(halt_on_failure, "write failure halted without HALT_ON_FAILURE"); + CHECK(mock_lock_called == 0, "HALT_ON_FAILURE write failure halts before lock"); + } + + /* A failed lock warns and boots; HALT_ON_FAILURE halts. */ + reset_mock(); + make_valid(&rec, 10, 10); + memcpy(mock_nv, &rec, sizeof(rec)); + mock_lock_fail = 1; + if (setjmp(halt_env) == 0) { + boot_state_on_boot(10); + CHECK(!halt_on_failure, "lock failure did not halt with HALT_ON_FAILURE"); + } + else { + CHECK(halt_on_failure, "lock failure halted without HALT_ON_FAILURE"); + } + CHECK(mock_lock_called == 1, "lock attempted"); + + /* Trust on first use: the first booted image sets anchor and record. */ + reset_mock(); + mock_read_code = BOOT_STATE_UNPROVISIONED; + boot_state_on_boot(42); + CHECK(mock_write_called == 1, "tofu writes"); + memcpy(&st, mock_write_buf, sizeof(st)); + CHECK(st.anchor == 42 && st.record == 42, "tofu anchor/record"); + CHECK(mock_lock_called == 1, "tofu locks"); + + /* A higher version advances the anchor. */ + reset_mock(); + make_valid(&rec, 10, 10); + memcpy(mock_nv, &rec, sizeof(rec)); + boot_state_on_boot(20); + CHECK(mock_write_called == 1, "advance writes"); + memcpy(&st, mock_write_buf, sizeof(st)); + CHECK(st.anchor == 20 && st.record == 20, "advance fields"); + + /* Re-booting the same version changes nothing and skips the write. */ + reset_mock(); + make_valid(&rec, 10, 10); + memcpy(mock_nv, &rec, sizeof(rec)); + boot_state_on_boot(10); + CHECK(mock_write_called == 0, "same version skips write"); + CHECK(mock_lock_called == 1, "same version still locks"); + + /* A validly signed older image still boots, keeps the anchor, and leaves + * an audit event behind. */ + reset_mock(); + make_valid(&rec, 10, 10); + memcpy(mock_nv, &rec, sizeof(rec)); + boot_state_on_boot(4); + CHECK(mock_write_called == 1, "rollback writes audit"); + memcpy(&st, mock_write_buf, sizeof(st)); + CHECK(st.anchor == 10, "rollback keeps anchor"); + CHECK(st.record == 4, "rollback records booted"); + CHECK(st.rollback_count == 1, "rollback counted"); + CHECK(mock_lock_called == 1, "rollback locks"); + CHECK(st.last_rollback_from == 10 && st.last_rollback_to == 4, + "rollback from/to"); + + /* A corrupt reference halts the boot and is not overwritten. */ + reset_mock(); + make_valid(&rec, 9, 9); + rec.crc ^= 1u; + memcpy(mock_nv, &rec, sizeof(rec)); + if (setjmp(halt_env) == 0) { + boot_state_on_boot(9); + CHECK(0, "corrupt reference did not halt"); + } + else { + CHECK(halted == 1, "corrupt reference halted"); + CHECK(mock_write_called == 0, "corrupt reference not overwritten"); + } + + /* on_failure leaves a corrupt reference untouched and does not halt. */ + reset_mock(); + make_valid(&rec, 9, 9); + rec.crc ^= 1u; + memcpy(mock_nv, &rec, sizeof(rec)); + boot_state_on_failure(BOOT_FAIL_VERIFY_BOTH); + CHECK(mock_write_called == 0, "failure keeps corrupt reference"); + CHECK(halted == 0, "failure on corrupt does not halt"); + + /* on_failure when unprovisioned starts a fresh record with the code. */ + reset_mock(); + mock_read_code = BOOT_STATE_UNPROVISIONED; + boot_state_on_failure(BOOT_FAIL_NO_IMAGE); + CHECK(mock_write_called == 1, "failure unprovisioned writes"); + memcpy(&st, mock_write_buf, sizeof(st)); + CHECK(st.boot_fail_count == 1, "failure count set"); + CHECK(st.last_fail_code == BOOT_FAIL_NO_IMAGE, "failure code recorded"); + + /* on_failure on a valid record bumps the counter, keeps anchor/record. */ + reset_mock(); + make_valid(&rec, 10, 8); + memcpy(mock_nv, &rec, sizeof(rec)); + boot_state_on_failure(BOOT_FAIL_VERIFY_BOTH); + CHECK(mock_write_called == 1, "failure valid writes"); + memcpy(&st, mock_write_buf, sizeof(st)); + CHECK(st.boot_fail_count == 1, "failure valid count"); + CHECK(st.last_fail_code == BOOT_FAIL_VERIFY_BOTH, "failure valid code"); + CHECK(st.anchor == 10 && st.record == 8, "failure preserves anchor/record"); + + if (fails == 0) { + printf("PASS\n"); + return 0; + } + printf("%d check(s) failed\n", fails); + return 1; +}