From e3be3e84c10f9cd8898d0be097d231fd145ed105 Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Sat, 12 Sep 2026 01:16:04 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20OTA-Trailer=20bauen=20und=20pr=C3=BCfen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 1 + src/util/nl_fwup_trailer.c | 74 +++++++++++++++++++++++++++++ src/util/nl_fwup_trailer.h | 28 +++++++++++ test/CMakeLists.txt | 1 + test/test_fwup_trailer.c | 95 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 199 insertions(+) create mode 100644 src/util/nl_fwup_trailer.c create mode 100644 src/util/nl_fwup_trailer.h create mode 100644 test/test_fwup_trailer.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e0f5a1..2285a04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ set(NL_FW_SOURCES src/app_main.c src/util/nl_sha256.c src/util/nl_crc.c + src/util/nl_fwup_trailer.c src/board/nl_board.c src/light/nl_color.c src/light/nl_engine.c diff --git a/src/util/nl_fwup_trailer.c b/src/util/nl_fwup_trailer.c new file mode 100644 index 0000000..c372c5f --- /dev/null +++ b/src/util/nl_fwup_trailer.c @@ -0,0 +1,74 @@ +#include "util/nl_fwup_trailer.h" + +#include + +/* NB: This layout is mirrored in Python at tools/mkimage/nl_mkimage.py (Task 3), + since there are two languages involved. Keep both in sync. */ + +#define OFF_MAGIC 0u +#define OFF_VERSION 4u +#define OFF_DEVICE 5u +#define OFF_FW 21u +#define OFF_SHA 37u + +static bool copy_field(uint8_t *dst, const char *src, uint32_t len) +{ + uint32_t n = (uint32_t)strlen(src); + if (n >= len) + return false; + memset(dst, 0, len); + memcpy(dst, src, n); + return true; +} + +bool nl_trailer_build(uint8_t out[NL_TRAILER_SIZE], const char *device, const char *version, const uint8_t sha256[32]) +{ + memset(out, 0xFF, NL_TRAILER_SIZE); + memcpy(&out[OFF_MAGIC], "NINL", 4); + out[OFF_VERSION] = NL_TRAILER_VERSION; + if (!copy_field(&out[OFF_DEVICE], device, NL_TRAILER_DEVICE_LEN) || + !copy_field(&out[OFF_FW], version, NL_TRAILER_VERSION_LEN)) + return false; + memcpy(&out[OFF_SHA], sha256, 32); + return true; +} + +static bool terminated(const uint8_t *field, uint32_t len) +{ + for (uint32_t i = 0; i < len; i++) + if (field[i] == 0) + return true; + return false; +} + +nl_trailer_err_t nl_trailer_verify(const uint8_t *tail, uint32_t tail_len, const char *device, const uint8_t sha256[32]) +{ + if (tail == NULL || tail_len < NL_TRAILER_SIZE) + return NL_TRAILER_ERR_SIZE; + if (memcmp(&tail[OFF_MAGIC], "NINL", 4) != 0) + return NL_TRAILER_ERR_MAGIC; + if (tail[OFF_VERSION] != NL_TRAILER_VERSION) + return NL_TRAILER_ERR_VERSION; + if (!terminated(&tail[OFF_DEVICE], NL_TRAILER_DEVICE_LEN) || + strcmp((const char *)&tail[OFF_DEVICE], device) != 0) + return NL_TRAILER_ERR_DEVICE; + if (memcmp(&tail[OFF_SHA], sha256, 32) != 0) + return NL_TRAILER_ERR_HASH; + return NL_TRAILER_OK; +} + +void nl_trailer_version(const uint8_t *tail, char *dst) +{ + memcpy(dst, &tail[OFF_FW], NL_TRAILER_VERSION_LEN); + dst[NL_TRAILER_VERSION_LEN - 1] = '\0'; +} + +const char *nl_trailer_err_name(nl_trailer_err_t e) +{ + switch (e) { + case NL_TRAILER_ERR_DEVICE: return "device_mismatch"; + case NL_TRAILER_ERR_HASH: return "hash"; + case NL_TRAILER_OK: return NULL; + default: return "trailer"; + } +} diff --git a/src/util/nl_fwup_trailer.h b/src/util/nl_fwup_trailer.h new file mode 100644 index 0000000..73e4aa0 --- /dev/null +++ b/src/util/nl_fwup_trailer.h @@ -0,0 +1,28 @@ +#ifndef NL_FWUP_TRAILER_H +#define NL_FWUP_TRAILER_H + +#include +#include + +#define NL_TRAILER_SIZE 512u +#define NL_TRAILER_VERSION 1u +#define NL_TRAILER_DEVICE_LEN 16u +#define NL_TRAILER_VERSION_LEN 16u + +typedef enum { + NL_TRAILER_OK = 0, + NL_TRAILER_ERR_SIZE, + NL_TRAILER_ERR_MAGIC, + NL_TRAILER_ERR_VERSION, + NL_TRAILER_ERR_DEVICE, + NL_TRAILER_ERR_HASH +} nl_trailer_err_t; + +/* "NINL" | version | device[16] | fw_version[16] | sha256[32] | 0xFF padding */ +bool nl_trailer_build(uint8_t out[NL_TRAILER_SIZE], const char *device, const char *version, const uint8_t sha256[32]); +nl_trailer_err_t nl_trailer_verify(const uint8_t *tail, uint32_t tail_len, const char *device, const uint8_t sha256[32]); +void nl_trailer_version(const uint8_t *tail, char *dst); /* dst: NL_TRAILER_VERSION_LEN bytes */ +/* Maps to the last_ota_error vocabulary; NULL for NL_TRAILER_OK. */ +const char *nl_trailer_err_name(nl_trailer_err_t e); + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6b98238..1c4a5c8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -48,3 +48,4 @@ nl_test(test_api test_api.c fake_api_ctx.c ${NL_SRC}/net/nl_api.c ${NL_SRC}/boar ${NL_SRC}/light/nl_boot_state.c ${NL_SRC}/config/nl_settings.c ${NL_SRC}/util/nl_crc.c fake_flash.c) target_link_libraries(test_api PRIVATE nl_cjson) nl_test(test_wifi_sm test_wifi_sm.c ${NL_SRC}/net/nl_wifi_sm.c) +nl_test(test_fwup_trailer test_fwup_trailer.c ${NL_SRC}/util/nl_fwup_trailer.c) diff --git a/test/test_fwup_trailer.c b/test/test_fwup_trailer.c new file mode 100644 index 0000000..04e02a6 --- /dev/null +++ b/test/test_fwup_trailer.c @@ -0,0 +1,95 @@ +#include +#include "unity.h" +#include "util/nl_fwup_trailer.h" + +void setUp(void) {} +void tearDown(void) {} + +static uint8_t HASH[32]; +static uint8_t tail[NL_TRAILER_SIZE]; + +static void fill_hash(void) +{ + for (int i = 0; i < 32; i++) + HASH[i] = (uint8_t)(i * 3 + 1); +} + +static void test_build_layout(void) +{ + fill_hash(); + TEST_ASSERT_TRUE(nl_trailer_build(tail, "e27_rgb", "1.2.3", HASH)); + TEST_ASSERT_EQUAL_MEMORY("NINL", tail, 4); + TEST_ASSERT_EQUAL_UINT8(NL_TRAILER_VERSION, tail[4]); + TEST_ASSERT_EQUAL_STRING("e27_rgb", (const char *)&tail[5]); + TEST_ASSERT_EQUAL_STRING("1.2.3", (const char *)&tail[21]); + TEST_ASSERT_EQUAL_UINT8_ARRAY(HASH, &tail[37], 32); + for (uint32_t i = 69; i < NL_TRAILER_SIZE; i++) + TEST_ASSERT_EQUAL_HEX8(0xFF, tail[i]); +} + +static void test_build_rejects_too_long_strings(void) +{ + fill_hash(); + TEST_ASSERT_FALSE(nl_trailer_build(tail, "0123456789abcdef", "1.0", HASH)); /* 16 chars, no room for NUL */ + TEST_ASSERT_FALSE(nl_trailer_build(tail, "evk", "0123456789abcdef", HASH)); + TEST_ASSERT_TRUE(nl_trailer_build(tail, "012345678901234", "012345678901234", HASH)); /* 15 chars fit */ +} + +static void test_verify_accepts_matching(void) +{ + fill_hash(); + nl_trailer_build(tail, "evk", "1.0.0", HASH); + TEST_ASSERT_EQUAL(NL_TRAILER_OK, nl_trailer_verify(tail, NL_TRAILER_SIZE, "evk", HASH)); +} + +static void test_verify_rejects(void) +{ + char version[NL_TRAILER_VERSION_LEN]; + uint8_t other[32]; + + fill_hash(); + memcpy(other, HASH, 32); + other[5] ^= 0x01; + nl_trailer_build(tail, "evk", "1.0.0", HASH); + + TEST_ASSERT_EQUAL(NL_TRAILER_ERR_SIZE, nl_trailer_verify(tail, 256, "evk", HASH)); + TEST_ASSERT_EQUAL(NL_TRAILER_ERR_DEVICE, nl_trailer_verify(tail, NL_TRAILER_SIZE, "gu10_cw", HASH)); + TEST_ASSERT_EQUAL(NL_TRAILER_ERR_HASH, nl_trailer_verify(tail, NL_TRAILER_SIZE, "evk", other)); + nl_trailer_version(tail, version); + TEST_ASSERT_EQUAL_STRING("1.0.0", version); + + tail[4] = 2; + TEST_ASSERT_EQUAL(NL_TRAILER_ERR_VERSION, nl_trailer_verify(tail, NL_TRAILER_SIZE, "evk", HASH)); + tail[0] = 'X'; + TEST_ASSERT_EQUAL(NL_TRAILER_ERR_MAGIC, nl_trailer_verify(tail, NL_TRAILER_SIZE, "evk", HASH)); +} + +static void test_verify_needs_terminated_device_string(void) +{ + fill_hash(); + nl_trailer_build(tail, "evk", "1.0.0", HASH); + memset(&tail[5], 'a', NL_TRAILER_DEVICE_LEN); /* no NUL inside the field */ + TEST_ASSERT_EQUAL(NL_TRAILER_ERR_DEVICE, nl_trailer_verify(tail, NL_TRAILER_SIZE, "aaaaaaaaaaaaaaaa", HASH)); +} + +static void test_error_names(void) +{ + TEST_ASSERT_EQUAL_STRING("trailer", nl_trailer_err_name(NL_TRAILER_ERR_MAGIC)); + TEST_ASSERT_EQUAL_STRING("trailer", nl_trailer_err_name(NL_TRAILER_ERR_SIZE)); + TEST_ASSERT_EQUAL_STRING("trailer", nl_trailer_err_name(NL_TRAILER_ERR_VERSION)); + TEST_ASSERT_EQUAL_STRING("device_mismatch", nl_trailer_err_name(NL_TRAILER_ERR_DEVICE)); + TEST_ASSERT_EQUAL_STRING("hash", nl_trailer_err_name(NL_TRAILER_ERR_HASH)); + TEST_ASSERT_NULL(nl_trailer_err_name(NL_TRAILER_OK)); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_build_layout); + RUN_TEST(test_build_rejects_too_long_strings); + RUN_TEST(test_verify_accepts_matching); + RUN_TEST(test_verify_rejects); + RUN_TEST(test_verify_needs_terminated_device_string); + RUN_TEST(test_error_names); + return UNITY_END(); +}