From 6a2c251daceeda580a56f05e0fa5ae4f76fe548f Mon Sep 17 00:00:00 2001 From: Ashrafahmed9 Date: Sun, 30 Aug 2026 21:24:36 +0530 Subject: [PATCH 1/2] fix(secure-boot): build the secure boot module, and stop it booting plaintext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit core/secure_boot.c is not in eboot_core's source list, so it has never been compiled, has no callers, and has no tests. It is the module that implements the verification chain the bootloader exists to perform. Wiring it in required core/rollback.c too, which it calls and which is also missing from the list. Compiling it turned up a policy that does the opposite of its name. require_encryption is documented as "Enforce AES-GCM decryption", and step 6 read: if (cfg->require_encryption && (hdr.flags & EOS_IMG_FLAG_ENCRYPTED)) An encrypted image reaches the body and is refused, because decryption is not implemented yet. A plaintext image fails the second half of the condition, falls past the gate, and boots. So the single image the policy exists to reject was the one case that skipped the check. Now require_encryption alone decides whether the gate applies, and an image without the flag is refused. That path was also the only failure return in the function that did not call attest_record(), so a refusal left no measurement behind. It records one now, like every other branch. Adds tests/unit/test_secure_boot.c, the first coverage this module has had: a plaintext image under require_encryption is rejected, an encrypted one is rejected while decryption is unimplemented, a plaintext image boots when the policy does not ask for encryption, and the refusal is attested. Against the current secure_boot.c the first case fails — the image boots. With the fix all four pass, and the suite goes from 17 to 18. core/fdt_loader.c is orphaned from the build in the same way. Left alone here; it is a separate module and a separate question. --- CMakeLists.txt | 1 + core/secure_boot.c | 19 +++- tests/CMakeLists.txt | 5 + tests/unit/test_secure_boot.c | 208 ++++++++++++++++++++++++++++++++++ 4 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_secure_boot.c 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..a9154cf 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(test_secure_boot unit/test_secure_boot.c) +target_link_libraries(test_secure_boot PRIVATE eboot_core) +add_test(NAME test_secure_boot COMMAND 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; +} From c26c1d8921bf3e15fc812a1c2083fd7f85ccbcfd Mon Sep 17 00:00:00 2001 From: Ashrafahmed9 Date: Mon, 31 Aug 2026 02:13:30 +0530 Subject: [PATCH 2/2] test(secure-boot): namespace the test target as eboot_test_secure_boot eos and eBoot both define test_crypto and test_multicore, and ebuild composes them into one CMake project, so #71 namespaces every eBoot test target as eboot_*. Adopt that convention here now rather than after #71 lands, so the two merge in either order. The add_test() name stays test_secure_boot: the collision is between targets, not test names, so ctest output is unchanged. --- tests/CMakeLists.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a9154cf..1c28a10 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -17,9 +17,9 @@ 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(test_secure_boot unit/test_secure_boot.c) -target_link_libraries(test_secure_boot PRIVATE eboot_core) -add_test(NAME test_secure_boot COMMAND test_secure_boot) +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)