diff --git a/CMakeLists.txt b/CMakeLists.txt index ff92704..1a899d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -112,6 +112,7 @@ add_library(eboot_core STATIC core/image_tlv.c core/sha512.c core/rollback.c + core/secure_boot.c ) target_include_directories(eboot_core PUBLIC ${EBLDR_INCLUDE_DIR}) target_link_libraries(eboot_core PUBLIC eboot_hal) diff --git a/core/secure_boot.c b/core/secure_boot.c index 385ad19..10bb0d3 100644 --- a/core/secure_boot.c +++ b/core/secure_boot.c @@ -148,7 +148,18 @@ eos_secure_boot_result_t eos_secure_boot(const eos_secure_boot_config_t *cfg, eos_rollback_stage(img_counter); /* ---- Step 6: Decrypt if required ---- */ - if (cfg->require_encryption && (hdr.flags & EOS_IMG_FLAG_ENCRYPTED)) { + if (cfg->require_encryption) { + /* The policy says this device only runs encrypted images. An image + * without the flag does not satisfy it, so it must not boot. The + * condition used to require the flag as well, which meant the one + * image the policy exists to reject — a plaintext one — was the one + * case that skipped the check and booted. */ + if (!(hdr.flags & EOS_IMG_FLAG_ENCRYPTED)) { + attest_record(2, hdr.image_version, hdr.hash, NULL, + EOS_SBOOT_ERR_DECRYPT); + return EOS_SBOOT_ERR_DECRYPT; + } + /* Use fw_decrypt module for AES-256-GCM decryption */ /* Key is read from OTP by fw_decrypt_init() */ extern int eos_fw_decrypt_init(void *ctx, const uint8_t *iv); @@ -162,7 +173,11 @@ eos_secure_boot_result_t eos_secure_boot(const eos_secure_boot_config_t *cfg, * 4. Verify GCM tag */ - /* Fix critical bug: Return decryption error if decryption is requested but unsupported/unimplemented */ + /* Decryption is not implemented, so an encrypted image cannot be + * verified either. Fail closed, and record it like every other + * failure path in this function does. */ + attest_record(2, hdr.image_version, hdr.hash, NULL, + EOS_SBOOT_ERR_DECRYPT); return EOS_SBOOT_ERR_DECRYPT; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3f4b679..1c28a10 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -16,6 +16,11 @@ add_executable(test_image_verify unit/test_image_verify.c) target_link_libraries(test_image_verify PRIVATE eboot_core) add_test(NAME test_image_verify COMMAND test_image_verify) +# --- test_secure_boot: Secure boot policy gates --- +add_executable(eboot_test_secure_boot unit/test_secure_boot.c) +target_link_libraries(eboot_test_secure_boot PRIVATE eboot_core) +add_test(NAME test_secure_boot COMMAND eboot_test_secure_boot) + # --- test_recovery: UART recovery write range --- add_executable(test_recovery unit/test_recovery.c) target_link_libraries(test_recovery PRIVATE eboot_core eboot_stage1) diff --git a/tests/unit/test_secure_boot.c b/tests/unit/test_secure_boot.c new file mode 100644 index 0000000..7358178 --- /dev/null +++ b/tests/unit/test_secure_boot.c @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 EoS Project +// ISO/IEC 25000 | ISO/IEC 15288:2023 + +/** + * @file test_secure_boot.c + * @brief Host tests for the secure boot policy gates. + * + * The image the require_encryption policy exists to reject is a plaintext + * one, so that is the case worth pinning. + */ + +#include "eos_secure_boot.h" +#include "eos_image.h" +#include "eos_hal.h" +#include +#include +#include + +#define SIM_FLASH_SIZE (64 * 1024) +#define IMAGE_ADDR 0x4000u +#define PAYLOAD_LEN 0x100u + +static uint8_t sim_flash[SIM_FLASH_SIZE]; + +static int sim_flash_read(uint32_t addr, void *buf, size_t len) +{ + if (addr + len > SIM_FLASH_SIZE) return EOS_ERR_FLASH; + memcpy(buf, &sim_flash[addr], len); + return EOS_OK; +} + +static int sim_flash_write(uint32_t addr, const void *buf, size_t len) +{ + if (addr + len > SIM_FLASH_SIZE) return EOS_ERR_FLASH; + memcpy(&sim_flash[addr], buf, len); + return EOS_OK; +} + +static int sim_flash_erase(uint32_t addr, size_t len) +{ + if (addr + len > SIM_FLASH_SIZE) return EOS_ERR_FLASH; + memset(&sim_flash[addr], 0xFF, len); + return EOS_OK; +} + +static uint32_t sim_tick = 0; +static uint32_t sim_get_tick(void) { return sim_tick++; } +static void sim_noop(void) {} +static void sim_noop_u32(uint32_t x) { (void)x; } +static void sim_jump(uint32_t addr) { (void)addr; } +static eos_reset_reason_t sim_reset_reason(void) { return EOS_RESET_POWER_ON; } +static bool sim_recovery_pin(void) { return false; } +static void sim_system_reset(void) {} + +static const eos_board_ops_t sim_ops = { + .flash_base = 0, + .flash_size = SIM_FLASH_SIZE, + .slot_a_addr = 0x4000, + .slot_a_size = 0x8000, + .slot_b_addr = 0xC000, + .slot_b_size = 0x8000, + .recovery_addr = 0, + .recovery_size = 0, + .bootctl_addr = 0, + .bootctl_backup_addr = 0x1000, + .log_addr = 0x2000, + .app_vector_offset = 0, + .flash_read = sim_flash_read, + .flash_write = sim_flash_write, + .flash_erase = sim_flash_erase, + .watchdog_init = sim_noop_u32, + .watchdog_feed = sim_noop, + .get_reset_reason = sim_reset_reason, + .system_reset = sim_system_reset, + .recovery_pin_asserted = sim_recovery_pin, + .jump = sim_jump, + .uart_init = NULL, + .uart_send = NULL, + .uart_recv = NULL, + .get_tick_ms = sim_get_tick, + .disable_interrupts = sim_noop, + .enable_interrupts = sim_noop, + .deinit_peripherals = sim_noop, +}; + +static int tests_passed = 0; + +#define TEST(name) \ + static void name(void); \ + static void run_##name(void) { \ + memset(sim_flash, 0xFF, sizeof(sim_flash)); \ + sim_tick = 0; \ + eos_hal_init(&sim_ops); \ + printf(" %-50s ", #name); \ + name(); \ + tests_passed++; \ + printf("[PASS]\n"); \ + } \ + static void name(void) + +#define ASSERT(cond) do { \ + if (!(cond)) { \ + printf("[FAIL] %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + exit(1); \ + } \ +} while(0) + +/* A CRC32-checksummed image the earlier gates accept, so a test reaches the + * encryption gate rather than stopping at integrity. */ +static void write_image(uint32_t flags) +{ + eos_image_header_t hdr; + memset(&hdr, 0, sizeof(hdr)); + hdr.magic = EOS_IMG_MAGIC; + hdr.hdr_version = EOS_IMAGE_HDR_VERSION; + hdr.hdr_size = (uint16_t)sizeof(eos_image_header_t); + hdr.image_size = PAYLOAD_LEN; + hdr.load_addr = 0x20000000; + hdr.entry_addr = 0x20000000; + hdr.flags = flags; + + uint32_t payload_addr = IMAGE_ADDR + hdr.hdr_size; + memset(&sim_flash[payload_addr], 0xA5, PAYLOAD_LEN); + + memcpy(&sim_flash[IMAGE_ADDR], &hdr, sizeof(hdr)); + + /* eos_crc32 needs the HAL live, and it is by the time this runs. */ + uint32_t crc = eos_crc32(payload_addr, PAYLOAD_LEN); + memcpy(((eos_image_header_t *)&sim_flash[IMAGE_ADDR])->hash, + &crc, sizeof(crc)); +} + +static eos_secure_boot_config_t base_cfg(void) +{ + eos_secure_boot_config_t cfg; + memset(&cfg, 0, sizeof(cfg)); + cfg.image_addr = IMAGE_ADDR; + cfg.require_signature = false; + cfg.lock_debug = false; + return cfg; +} + +TEST(test_plaintext_image_rejected_when_encryption_required) +{ + write_image(0); /* no EOS_IMG_FLAG_ENCRYPTED */ + eos_secure_boot_config_t cfg = base_cfg(); + cfg.require_encryption = true; + + uint32_t entry = 0; + eos_secure_boot_result_t rc = eos_secure_boot(&cfg, &entry); + + /* The policy says encrypted only. A plaintext image must not boot. */ + ASSERT(rc == EOS_SBOOT_ERR_DECRYPT); +} + +TEST(test_encrypted_image_rejected_while_decrypt_unimplemented) +{ + write_image(EOS_IMG_FLAG_ENCRYPTED); + eos_secure_boot_config_t cfg = base_cfg(); + cfg.require_encryption = true; + + uint32_t entry = 0; + eos_secure_boot_result_t rc = eos_secure_boot(&cfg, &entry); + + /* Decryption is not implemented, so this cannot be verified either. */ + ASSERT(rc == EOS_SBOOT_ERR_DECRYPT); +} + +TEST(test_plaintext_image_boots_when_encryption_not_required) +{ + write_image(0); + eos_secure_boot_config_t cfg = base_cfg(); + cfg.require_encryption = false; + + uint32_t entry = 0; + eos_secure_boot_result_t rc = eos_secure_boot(&cfg, &entry); + + ASSERT(rc == EOS_SBOOT_OK); +} + +TEST(test_decrypt_failure_is_attested) +{ + write_image(0); + eos_secure_boot_config_t cfg = base_cfg(); + cfg.require_encryption = true; + cfg.enable_attestation = true; + + uint32_t entry = 0; + (void)eos_secure_boot(&cfg, &entry); + + /* Every other failure path records the outcome; this one must too. */ + const eos_attest_log_t *log = eos_secure_boot_get_attestation(); + ASSERT(log != NULL); + ASSERT(log->count > 0); + ASSERT(log->entries[log->count - 1].verify_result == EOS_SBOOT_ERR_DECRYPT); +} + +int main(void) +{ + printf("Secure boot policy tests\n"); + run_test_plaintext_image_rejected_when_encryption_required(); + run_test_encrypted_image_rejected_while_decrypt_unimplemented(); + run_test_plaintext_image_boots_when_encryption_not_required(); + run_test_decrypt_failure_is_attested(); + printf("%d passed\n", tests_passed); + return 0; +}