mirror of https://github.com/wolfSSL/wolfBoot.git
297 lines
10 KiB
C
297 lines
10 KiB
C
/* 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;
|
|
}
|