From 23587c41e2b1befeeebb22feae13628629f1b1ba Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sun, 12 Apr 2026 23:47:00 +0900 Subject: [PATCH] SHA-256 hash verification of flash slots, pico_hash_binary, flash:: namespace, ring_buffer iterator, non-destructive log --- firmware/CMakeLists.txt | 5 +- firmware/include/debug_log.h | 12 --- firmware/include/flash.h | 20 ++++ firmware/include/ring_buffer.h | 11 +++ firmware/include/wire.h | 13 +-- firmware/lib/flash.cpp | 163 +++++++++++++++++++++++++++++++++ firmware/lib/handlers.cpp | 50 ++-------- 7 files changed, 210 insertions(+), 64 deletions(-) create mode 100644 firmware/include/flash.h create mode 100644 firmware/lib/flash.cpp diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt index c8ba148..ca00014 100644 --- a/firmware/CMakeLists.txt +++ b/firmware/CMakeLists.txt @@ -12,6 +12,7 @@ pico_sdk_init() set(LIB_SOURCES lib/arp.cpp lib/dispatch.cpp + lib/flash.cpp lib/handlers.cpp lib/icmp.cpp lib/igmp.cpp @@ -21,7 +22,7 @@ set(LIB_SOURCES w6300/w6300.cpp ) -set(LIB_DEPS pico_stdlib tinyusb_device tinyusb_board hardware_pio hardware_spi hardware_dma hardware_clocks) +set(LIB_DEPS pico_stdlib pico_sha256 tinyusb_device tinyusb_board hardware_pio hardware_spi hardware_dma hardware_clocks) add_executable(picomap firmware.cpp ${LIB_SOURCES}) target_include_directories(picomap PRIVATE include w6300) @@ -35,6 +36,7 @@ math(EXPR VERSION_MAJOR "${BUILD_EPOCH} >> 16") math(EXPR VERSION_MINOR "${BUILD_EPOCH} & 65535") pico_set_binary_version(picomap MAJOR ${VERSION_MAJOR} MINOR ${VERSION_MINOR}) target_compile_definitions(picomap PRIVATE BUILD_EPOCH=${BUILD_EPOCH}) +pico_hash_binary(picomap) pico_embed_pt_in_binary(picomap ${CMAKE_CURRENT_LIST_DIR}/partition_table.json) pico_add_extra_outputs(picomap) target_link_libraries(picomap ${LIB_DEPS}) @@ -48,6 +50,7 @@ pico_enable_stdio_uart(picomap_test 0) pico_set_binary_type(picomap_test copy_to_ram) pico_set_binary_version(picomap_test MAJOR ${VERSION_MAJOR} MINOR ${VERSION_MINOR}) target_compile_definitions(picomap_test PRIVATE BUILD_EPOCH=${BUILD_EPOCH}) +pico_hash_binary(picomap_test) pico_embed_pt_in_binary(picomap_test ${CMAKE_CURRENT_LIST_DIR}/partition_table.json) pico_add_extra_outputs(picomap_test) target_link_libraries(picomap_test ${LIB_DEPS}) diff --git a/firmware/include/debug_log.h b/firmware/include/debug_log.h index 68be752..f0da1d6 100644 --- a/firmware/include/debug_log.h +++ b/firmware/include/debug_log.h @@ -37,15 +37,3 @@ inline void dlog_if_slow(std::string_view label, uint32_t threshold_us, std::fun dlogf("%.*s %luus", static_cast(label.size()), label.data(), static_cast(elapsed)); } -inline std::vector dlog_drain() { - std::vector result; - uint16_t n = g_debug_log.used(); - result.reserve(n); - for (uint16_t i = 0; i < n; i++) { - log_entry e; - g_debug_log.peek(std::span{&e, 1}); - result.push_back(std::move(e)); - g_debug_log.consume(1); - } - return result; -} diff --git a/firmware/include/flash.h b/firmware/include/flash.h new file mode 100644 index 0000000..f9a166e --- /dev/null +++ b/firmware/include/flash.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include + +namespace flash { + +constexpr uint32_t FLASH_BASE = 0x10000000; +constexpr uint32_t FLASH_SIZE = 2 * 1024 * 1024; + +struct slot { + bool valid; + uint32_t version; + bool hash_ok; + auto as_tuple() const { return std::tie(valid, version, hash_ok); } + auto as_tuple() { return std::tie(valid, version, hash_ok); } +}; + +slot scan(uint32_t flash_offset); + +} diff --git a/firmware/include/ring_buffer.h b/firmware/include/ring_buffer.h index 01e820c..5220335 100644 --- a/firmware/include/ring_buffer.h +++ b/firmware/include/ring_buffer.h @@ -53,4 +53,15 @@ struct ring_buffer { uint16_t len = pending < contig ? pending : contig; return {data.data() + offset, len}; } + + struct iterator { + const ring_buffer* rb; + uint16_t index; + const T& operator*() const { return rb->data[(rb->head + index) % N]; } + iterator& operator++() { index++; return *this; } + bool operator!=(const iterator& o) const { return index != o.index; } + }; + + iterator begin() const { return {this, 0}; } + iterator end() const { return {this, used()}; } }; diff --git a/firmware/include/wire.h b/firmware/include/wire.h index aebfa3d..e8b5503 100644 --- a/firmware/include/wire.h +++ b/firmware/include/wire.h @@ -8,6 +8,7 @@ #include "msgpack.h" #include "halfsiphash.h" #include "static_vector.h" +#include "flash.h" struct Envelope { static constexpr int8_t ext_id = 0; @@ -128,19 +129,11 @@ struct RequestFlashStatus { auto as_tuple() { return std::tie(); } }; -struct SlotInfo { - bool valid; - uint32_t version; - bool hash_ok; - auto as_tuple() const { return std::tie(valid, version, hash_ok); } - auto as_tuple() { return std::tie(valid, version, hash_ok); } -}; - struct ResponseFlashStatus { static constexpr int8_t ext_id = 15; int8_t boot_partition; - SlotInfo slot_a; - SlotInfo slot_b; + flash::slot slot_a; + flash::slot slot_b; auto as_tuple() const { return std::tie(boot_partition, slot_a, slot_b); } auto as_tuple() { return std::tie(boot_partition, slot_a, slot_b); } }; diff --git a/firmware/lib/flash.cpp b/firmware/lib/flash.cpp new file mode 100644 index 0000000..fb90b01 --- /dev/null +++ b/firmware/lib/flash.cpp @@ -0,0 +1,163 @@ +#include "flash.h" +#include +#include "pico/sha256.h" +#include "boot/picobin.h" + +namespace flash { +namespace { + +constexpr uint32_t PICOBIN_MARKER_END = 0xab123579; + +struct __attribute__((packed)) last_item { + uint8_t type; + uint16_t block_item_words; + uint8_t pad; + int32_t next_block_offset; + uint32_t marker_end; +}; + +struct __attribute__((packed)) hash_def_header { + uint8_t type; + uint8_t size_words; + uint8_t reserved; + uint8_t hash_type; + uint16_t block_words_to_hash; + uint16_t pad; +}; + +struct __attribute__((packed)) load_map_header { + uint8_t type; + uint8_t size_words; + uint8_t reserved; + uint8_t flags_and_count; + + uint8_t count() const { return flags_and_count & 0x7f; } + bool absolute() const { return flags_and_count & 0x80; } +}; + +struct load_map_entry { + uint32_t storage_addr; + uint32_t runtime_addr; + uint32_t size; +}; + +struct parsed_item { + uint8_t type; + uint8_t size_words; + const uint32_t* words; +}; + +struct parsed_block { + const uint32_t* base; + parsed_item items[16]; + uint8_t item_count; + int32_t next_block_offset; +}; + +bool parse_block(const uint32_t* start, const uint32_t* limit, parsed_block& out) { + if (start >= limit || *start != PICOBIN_BLOCK_MARKER_START) return false; + out.base = start; + out.item_count = 0; + out.next_block_offset = 0; + + auto* w = start + 1; + while (w + 3 < limit && out.item_count < 16) { + uint8_t type = *w & 0xff; + if (type == PICOBIN_BLOCK_ITEM_2BS_LAST) { + auto* last = reinterpret_cast(w); + out.next_block_offset = last->next_block_offset; + return last->marker_end == PICOBIN_MARKER_END; + } + uint8_t size_words = (*w >> 8) & 0xff; + if (w + size_words > limit) return false; + out.items[out.item_count++] = {type, size_words, w}; + w += size_words; + } + return false; +} + +const parsed_item* find_item(const parsed_block& blk, uint8_t type) { + for (uint8_t i = 0; i < blk.item_count; i++) + if (blk.items[i].type == type) return &blk.items[i]; + return nullptr; +} + +bool verify_hash(const parsed_block& last) { + auto* lm_item = find_item(last, PICOBIN_BLOCK_ITEM_LOAD_MAP); + auto* hd_item = find_item(last, PICOBIN_BLOCK_ITEM_1BS_HASH_DEF); + auto* hv_item = find_item(last, PICOBIN_BLOCK_ITEM_HASH_VALUE); + if (!lm_item || !hd_item || !hv_item) return false; + if (hd_item->size_words < 2 || hv_item->size_words < 9) return false; + + auto* hd = reinterpret_cast(hd_item->words); + if (hd->hash_type != PICOBIN_HASH_SHA256) return false; + + auto* lm = reinterpret_cast(lm_item->words); + auto* entries = reinterpret_cast(lm_item->words + 1); + uint32_t lm_xip_addr = reinterpret_cast(lm_item->words); + + pico_sha256_state_t sha; + if (pico_sha256_try_start(&sha, SHA256_BIG_ENDIAN, false) != PICO_OK) return false; + + for (uint8_t i = 0; i < lm->count(); i++) { + uint32_t storage_addr = entries[i].storage_addr; + uint32_t size = entries[i].size; + if (lm->absolute()) size -= entries[i].runtime_addr; + if (storage_addr == 0) { + pico_sha256_update_blocking(&sha, reinterpret_cast(&size), 4); + } else { + if (!lm->absolute()) storage_addr += lm_xip_addr; + pico_sha256_update_blocking(&sha, reinterpret_cast(storage_addr), size); + } + } + + pico_sha256_update_blocking(&sha, + reinterpret_cast(last.base), + static_cast(hd->block_words_to_hash) * 4); + + sha256_result_t result; + pico_sha256_finish(&sha, &result); + + auto* expected = reinterpret_cast(hv_item->words + 1); + return memcmp(result.bytes, expected, 32) == 0; +} + +} + +slot scan(uint32_t flash_offset) { + slot info{}; + constexpr uint32_t scan_limit = 4096; + constexpr uint32_t slot_size = 512 * 1024; + auto* slot_base = reinterpret_cast(FLASH_BASE + flash_offset); + auto* slot_end = reinterpret_cast(FLASH_BASE + flash_offset + slot_size); + + auto* s = slot_base; + auto* s_end = slot_base + scan_limit / 4; + while (s < s_end && *s != PICOBIN_BLOCK_MARKER_START) s++; + if (s >= s_end) return info; + + parsed_block start; + if (!parse_block(s, slot_end, start)) return info; + + for (uint8_t i = 0; i < start.item_count; i++) { + if (start.items[i].type == PICOBIN_BLOCK_ITEM_1BS_VERSION && start.items[i].size_words >= 2) + info.version = start.items[i].words[1]; + } + + auto* cur = &start; + parsed_block next; + while (cur->next_block_offset != 0) { + auto* np = reinterpret_cast( + reinterpret_cast(cur->base) + cur->next_block_offset); + if (np <= slot_base || np >= slot_end) return info; + if (np == start.base) break; + if (!parse_block(np, slot_end, next)) return info; + cur = &next; + } + + info.valid = true; + info.hash_ok = verify_hash(*cur); + return info; +} + +} diff --git a/firmware/lib/handlers.cpp b/firmware/lib/handlers.cpp index 29e242f..97e524e 100644 --- a/firmware/lib/handlers.cpp +++ b/firmware/lib/handlers.cpp @@ -3,15 +3,11 @@ #include "pico/bootrom.h" #include "hardware/flash.h" #include "hardware/watchdog.h" -#include "boot/picobin.h" +#include "flash.h" #include "dispatch.h" #include "net.h" #include "debug_log.h" -static constexpr uint32_t XIP_BASE_ADDR = 0x10000000; -static constexpr uint32_t FLASH_SIZE = 2 * 1024 * 1024; -static constexpr uint32_t SLOT_A_OFFSET = 0x00000; -static constexpr uint32_t SLOT_B_OFFSET = 0x80000; static boot_reason detected_boot_reason; static void poke_watchdog() { @@ -54,18 +50,18 @@ std::optional handle_info(const responder&, const RequestInfo&) { std::optional handle_log(const responder&, const RequestLog&) { ResponseLog resp; - for (auto& e : dlog_drain()) - resp.entries.push_back(LogEntry{e.timestamp_us, std::move(e.message)}); + for (auto& e : g_debug_log) + resp.entries.push_back(LogEntry{e.timestamp_us, e.message}); return resp; } std::optional handle_flash_erase(const responder&, const RequestFlashErase& req) { - if (req.addr < XIP_BASE_ADDR || req.addr + req.len > XIP_BASE_ADDR + FLASH_SIZE) { + if (req.addr < flash::FLASH_BASE || req.addr + req.len > flash::FLASH_BASE + flash::FLASH_SIZE) { dlogf("flash erase: out of range %08lx+%lu", static_cast(req.addr), static_cast(req.len)); return std::nullopt; } - uint32_t offset = req.addr - XIP_BASE_ADDR; + uint32_t offset = req.addr - flash::FLASH_BASE; if (offset % FLASH_SECTOR_SIZE != 0 || req.len % FLASH_SECTOR_SIZE != 0 || req.len == 0) { dlogf("flash erase: bad alignment %08lx+%lu", static_cast(req.addr), static_cast(req.len)); @@ -76,12 +72,12 @@ std::optional handle_flash_erase(const responder&, const Req } std::optional handle_flash_write(const responder&, const RequestFlashWrite& req) { - if (req.addr < XIP_BASE_ADDR || req.addr + req.data.size() > XIP_BASE_ADDR + FLASH_SIZE) { + if (req.addr < flash::FLASH_BASE || req.addr + req.data.size() > flash::FLASH_BASE + flash::FLASH_SIZE) { dlogf("flash write: out of range %08lx+%zu", static_cast(req.addr), req.data.size()); return std::nullopt; } - uint32_t offset = req.addr - XIP_BASE_ADDR; + uint32_t offset = req.addr - flash::FLASH_BASE; if (offset % FLASH_PAGE_SIZE != 0 || req.data.size() % FLASH_PAGE_SIZE != 0 || req.data.empty()) { dlogf("flash write: bad alignment %08lx+%zu", static_cast(req.addr), req.data.size()); @@ -91,34 +87,6 @@ std::optional handle_flash_write(const responder&, const Req return ResponseFlashWrite{}; } -static SlotInfo scan_slot(uint32_t flash_offset) { - SlotInfo info{}; - constexpr uint32_t scan_limit = 4096; - auto* scan = reinterpret_cast(XIP_BASE_ADDR + flash_offset); - auto* scan_end = scan + scan_limit / 4; - while (scan < scan_end && *scan != PICOBIN_BLOCK_MARKER_START) scan++; - if (scan >= scan_end) return info; - info.valid = true; - - auto* p = reinterpret_cast(scan + 1); - auto* end = reinterpret_cast(scan + 64); - while (p + 2 <= end) { - uint8_t type = p[0]; - uint8_t size_words = p[1]; - if (type == PICOBIN_BLOCK_ITEM_2BS_LAST) break; - uint32_t item_bytes = static_cast(size_words) * 4; - if (p + item_bytes > end) break; - if (type == PICOBIN_BLOCK_ITEM_1BS_VERSION && size_words >= 2) { - auto* words = reinterpret_cast(p + 4); - uint16_t minor = words[0]; - uint16_t major = words[1]; - info.version = (static_cast(major) << 16) | minor; - } - p += item_bytes; - } - return info; -} - std::optional handle_flash_status(const responder&, const RequestFlashStatus&) { ResponseFlashStatus resp; boot_info_t bi; @@ -126,8 +94,8 @@ std::optional handle_flash_status(const responder&, const R resp.boot_partition = bi.partition; else resp.boot_partition = -1; - resp.slot_a = scan_slot(SLOT_A_OFFSET); - resp.slot_b = scan_slot(SLOT_B_OFFSET); + resp.slot_a = flash::scan(0x00000); + resp.slot_b = flash::scan(0x80000); return resp; }