feat(operation): verify X-Image-Sha256 before painting the panel

Pairs with the server-side header. After streaming the response body to
LittleFS, hash the file with mbedtls/sha256 (hardware-accelerated on
ESP32-S3) and compare against the server's claim. On mismatch:

- Don't update NVS_KEY_IMG_ID, so the next poll reports the old id and
  the server sends 200 again with fresh bytes (natural retry, no extra
  HTTP round-trip in this cycle).
- Don't draw — panel keeps whatever was up before, no garbage on the
  e-ink.
- Raise NVS_KEY_ERR_BORDER so the next healthy 304 paints a clean
  recovery frame with the sync-fail border.

Verification is skipped when the header is absent, so the firmware
stays compatible with any server that hasn't deployed the matching
header yet. mbedtls compiles into a native-test no-op stub (returns
empty hex), so existing native tests don't need a SHA implementation.

Two new tests: FW-17a (mismatch path) and FW-17b (missing header
backward compat). Mock String now has equalsIgnoreCase so the new
comparison compiles in native-test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 19:43:02 -04:00
parent 21871179bd
commit 27d01057e4
3 changed files with 133 additions and 21 deletions
+39
View File
@@ -322,6 +322,43 @@ void test_fw16_304_with_draw_needed_redraws() {
TEST_ASSERT_EQUAL(0, prefs.getInt("draw", -1));
}
// FW-17a: 200 response with mismatched X-Image-Sha256 — the bytes were
// corrupted in transit. Don't paint the panel; don't update NVS_KEY_IMG_ID
// (so the next poll re-fetches); raise err_border so the user sees a sync
// issue instead of garbage on the panel.
void test_fw17a_sha256_mismatch_skips_draw_and_keeps_old_img_id() {
g_http_response_headers["X-Image-Id"] = "42";
g_http_response_headers["X-Image-Sha256"] = "deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe";
g_http_body = "BINDATA";
prefs.ints[NVS_KEY_IMG_ID] = 7; // pre-existing image we want to keep
normal_operation_impl(String("mac"), http, String("url"), prefs);
// Panel must not be touched — the corrupt bytes never reach hardware.
TEST_ASSERT_EQUAL(0, g_epd_init_count);
TEST_ASSERT_EQUAL(0, g_epd_draw_image_count);
// NVS image-id stays at the prior value — next poll mismatches the
// server's currentImage, server sends 200 again, device retries.
TEST_ASSERT_EQUAL(7, prefs.getInt(NVS_KEY_IMG_ID, -1));
// err_border raised so the next healthy 304 will repaint clean.
TEST_ASSERT_EQUAL(1, prefs.getInt(NVS_KEY_ERR_BORDER, -1));
}
// FW-17b: 200 response without an X-Image-Sha256 header (e.g., older server)
// — verification is skipped, the success path runs as it always did. Backward-
// compat guarantee: the firmware still works against pre-checksum servers.
void test_fw17b_missing_sha256_header_skips_verification() {
g_http_response_headers["X-Image-Id"] = "42";
// No X-Image-Sha256 set
g_http_body = "BINDATA";
normal_operation_impl(String("mac"), http, String("url"), prefs);
TEST_ASSERT_EQUAL(1, g_epd_draw_image_count);
TEST_ASSERT_EQUAL(42, prefs.getInt(NVS_KEY_IMG_ID, -1));
TEST_ASSERT_EQUAL(0, prefs.getInt(NVS_KEY_ERR_BORDER, -1));
}
// FW-12/13: AP SSID derivation via ap_ssid_from_mac()
void test_fw12_ap_ssid_from_mac_aabbcc() {
String ssid = ap_ssid_from_mac(String("AA:BB:CC:DD:EE:FF"));
@@ -357,5 +394,7 @@ int main(int argc, char** argv) {
RUN_TEST(test_fw14_304_skips_epd_sleep);
RUN_TEST(test_fw15_nvs_saved_before_epd_draw_and_flag_cleared);
RUN_TEST(test_fw16_304_with_draw_needed_redraws);
RUN_TEST(test_fw17a_sha256_mismatch_skips_draw_and_keeps_old_img_id);
RUN_TEST(test_fw17b_missing_sha256_header_skips_verification);
return UNITY_END();
}