mirror of https://github.com/wolfSSL/wolfBoot.git
x86 FSP: add TPM-NV anti-rollback with a persistent version floor and boot audit counters
parent
2cb873bd8e
commit
aa6fdb8fa9
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
4
arch.mk
4
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 <stdint.h>
|
||||
|
||||
#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 */
|
||||
|
|
@ -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
|
||||
|
|
|
|||
14
options.mk
14
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)
|
||||
|
|
|
|||
|
|
@ -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 <string.h>
|
||||
|
||||
#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 */
|
||||
|
|
@ -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 <wolftpm/tpm2_wrap.h>
|
||||
#include <string.h>
|
||||
|
||||
#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 */
|
||||
11
src/tpm.c
11
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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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/)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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' $< >> $@
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <setjmp.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
Loading…
Reference in New Issue