This shows you the differences between two versions of the page.
|
iothings:laboratoare:2025:lab10 [2025/12/06 19:12] dan.tudose [A2. Add the partition table file] |
iothings:laboratoare:2025:lab10 [2025/12/06 21:35] (current) dan.tudose |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| ====== Lab 10. Secure OTA ====== | ====== Lab 10. Secure OTA ====== | ||
| - | This lab shows how to implement **real remote firmware updates from a browser** for an ESP32-C6 board using **PlatformIO + pioarduino** and an **Async Web OTA** page powered by **ElegantOTA**. | + | Over-the-air (OTA) updates are a core capability of modern IoT devices, enabling firmware improvements, bug fixes, and security patches without physical access to the hardware. In real deployments, devices may be installed in hard-to-reach locations or embedded in larger systems, so requiring a USB connection for every update becomes impractical. OTA solves this by allowing a device to receive new firmware over a network, reducing maintenance costs and shortening the time between discovering an issue and delivering a fix. |
| + | |||
| + | Designing OTA for embedded systems also introduces engineering trade-offs that are less visible in traditional software. Devices must update safely despite limited flash, intermittent connectivity, and the risk of power loss mid-installation. A robust OTA approach typically uses separate firmware slots so a known-good image remains available if an update fails. Just as importantly, OTA is part of a security boundary: update mechanisms must ensure that only authentic, intact firmware can be installed, or they can become a high-impact attack path in an IoT fleet. | ||
| + | |||
| + | {{ :iothings:laboratoare:2025:ota1.jpg?800 |}} | ||
| + | |||
| + | ====== Simple OTA ====== | ||
| + | |||
| + | This first example shows you how to implement **real remote firmware updates from a browser** for an ESP32-C6 board using **PlatformIO + pioarduino**. | ||
| You will: | You will: | ||
| Line 9: | Line 17: | ||
| * verify success with a visible NeoPixel behavior change, | * verify success with a visible NeoPixel behavior change, | ||
| * document basic threat-model thinking. | * document basic threat-model thinking. | ||
| - | |||
| ===== Learning outcomes ===== | ===== Learning outcomes ===== | ||
| Line 21: | Line 28: | ||
| - Identify key security risks for OTA and apply simple mitigations. | - Identify key security risks for OTA and apply simple mitigations. | ||
| - | |||
| - | ===== Concept snapshot ===== | ||
| **What you are building:** | **What you are building:** | ||
| Line 36: | Line 41: | ||
| - | ===== Part A — Create the PlatformIO project ===== | + | ==== Part A — Create the PlatformIO project ==== |
| - | Create a new PlatformIO project and create or replace your ``platformio.ini`` with: | + | Create a new PlatformIO project and create or replace your ''platformio.ini'' with: |
| <code ini platformio.ini> | <code ini platformio.ini> | ||
| Line 71: | Line 76: | ||
| - | ==== A2. Add the partition table file ==== | + | === Add the partition table file === |
| In your project root, next to ''platformio.ini'', create ''sparrow_ota_4mb.csv''. | In your project root, next to ''platformio.ini'', create ''sparrow_ota_4mb.csv''. | ||
| Line 89: | Line 94: | ||
| This layout assumes **4MB internal flash** and gives you two OTA application slots plus a small SPIFFS area. | This layout assumes **4MB internal flash** and gives you two OTA application slots plus a small SPIFFS area. | ||
| - | ===== Part B — Baseline firmware with Async OTA web page ===== | + | ==== Part B — Baseline firmware with Async OTA web page ==== |
| - | Create ``src/main.cpp`` and paste the code from [[iothings:laboratoare:2025_code:lab10_1| here]] then build and upload via USB. | + | Create ''src/main.cpp'' and paste the code from [[iothings:laboratoare:2025_code:lab10_1| here]] then build and upload via USB. |
| You should see: | You should see: | ||
| Line 97: | Line 102: | ||
| * Wi-Fi connection dots | * Wi-Fi connection dots | ||
| * A printed IP address | * A printed IP address | ||
| - | * A message telling you to open ``/update`` | + | * A message telling you to open ''/update'' |
| In your browser: | In your browser: | ||
| - | * ``http://<device-ip>/`` | + | * ''http://<device-ip>/'' |
| - | * ``http://<device-ip>/update`` | + | * ''http://<device-ip>/update'' |
| Log in with: | Log in with: | ||
| - | * user: ``admin`` | + | * user: ''admin'' |
| - | * pass: ``change-me`` | + | * pass: ''change-me'' |
| + | |||
| + | ==== Part C — OTA proof using a NeoPixel blink change ==== | ||
| + | |||
| + | Now you will make a visible change and deliver it **without USB**. | ||
| + | |||
| + | Add a small LED pattern into main.cpp, you can get the new code from [[iothings:laboratoare:2025_code:lab10_2| here]]. | ||
| + | |||
| + | |||
| + | |||
| + | ====== Pull-Based OTA + Hash Integrity + Telemetry ====== | ||
| + | |||
| + | You will upgrade your firmware update architecture from **push OTA** (human uploads a .bin to /update) to a more production-like **pull OTA**, where the device: | ||
| + | |||
| + | * checks an **update manifest** hosted on a server, | ||
| + | * compares versions, | ||
| + | * downloads a firmware image, | ||
| + | * verifies **SHA-256 integrity**, | ||
| + | * writes the update to the inactive OTA slot, | ||
| + | * stores **update telemetry** in NVS, | ||
| + | * exposes a **/status** JSON endpoint. | ||
| + | |||
| + | {{ :iothings:laboratoare:2025:ota2.jpg?800 |}} | ||
| + | |||
| + | ===== Learning outcomes ===== | ||
| + | |||
| + | After completing this lab, you can: | ||
| + | |||
| + | - Implement a **manifest-driven update** workflow. | ||
| + | - Perform **streaming SHA-256 verification** during OTA download. | ||
| + | - Store and retrieve update metadata with **Preferences (NVS)**. | ||
| + | - Design basic fleet-friendly **status telemetry** endpoints. | ||
| + | |||
| + | |||
| + | |||
| + | ===== Architecture ===== | ||
| + | |||
| + | **Update server hosts:** | ||
| + | |||
| + | * ''manifest.json'' | ||
| + | * ''firmware-<version>.bin'' | ||
| + | |||
| + | **Device workflow:** | ||
| + | |||
| + | 1. GET manifest | ||
| + | 2. Parse JSON | ||
| + | 3. If manifest.version > current_version: | ||
| + | - download firmware | ||
| + | - compute SHA-256 while streaming | ||
| + | - compare expected vs computed | ||
| + | - write to OTA slot | ||
| + | - commit update + reboot | ||
| + | 4. Record telemetry in NVS | ||
| + | |||
| + | ===== Platformio Setup ===== | ||
| + | |||
| + | You will need to edit your platformio.ini file to this: | ||
| + | |||
| + | <code ini platformio.ini> | ||
| + | [env:sparrow_c6] | ||
| + | platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip | ||
| + | board = esp32-c6-devkitm-1 | ||
| + | framework = arduino | ||
| + | monitor_speed = 115200 | ||
| + | |||
| + | ; Use OTA-capable partition table | ||
| + | board_build.partitions = sparrow_ota_4mb.csv | ||
| + | |||
| + | ; Optional but often helpful | ||
| + | build_flags = | ||
| + | -D CORE_DEBUG_LEVEL=1 | ||
| + | -D ARDUINO_USB_MODE=1 | ||
| + | -D ARDUINO_USB_CDC_ON_BOOT=1 | ||
| + | -D ESP32_C6_env | ||
| + | |||
| + | lib_deps = | ||
| + | ESP32Async/AsyncTCP@^3.4.9 | ||
| + | ESP32Async/ESPAsyncWebServer@^3.9.2 | ||
| + | adafruit/Adafruit NeoPixel | ||
| + | bblanchon/ArduinoJson | ||
| + | |||
| + | |||
| + | lib_ignore = | ||
| + | AsyncTCP_RP2040W | ||
| + | |||
| + | </code> | ||
| + | ===== Part A — Update manifest ===== | ||
| + | |||
| + | Create a manifest file with the following structure: | ||
| + | |||
| + | <code json> | ||
| + | { | ||
| + | "project": "sparrow-c6-lab", | ||
| + | "version": "1.0.1", | ||
| + | "url": "http://YOUR_PC_IP:8000/firmware-1.0.1.bin", | ||
| + | "sha256": "REPLACE_WITH_SHA256_HEX", | ||
| + | "notes": "NeoPixel speed fix + status endpoint." | ||
| + | } | ||
| + | </code> | ||
| + | |||
| + | **Rules:** | ||
| + | * ''version'' uses semantic versioning: ''MAJOR.MINOR.PATCH'' | ||
| + | * ''url'' must be reachable by the device | ||
| + | * ''sha256'' is lowercase hex of the binary | ||
| + | |||
| + | |||
| + | ==== A1. Compute SHA-256 for a firmware binary ==== | ||
| + | |||
| + | After building in PlatformIO, your binary is typically: | ||
| + | |||
| + | * ''.pio/build/sparrow_c6/firmware.bin'' | ||
| + | |||
| + | Compute its SHA-256 on your computer: | ||
| + | |||
| + | **Linux/macOS** | ||
| + | <code> | ||
| + | shasum -a 256 firmware.bin | ||
| + | </code> | ||
| + | |||
| + | **Windows PowerShell** | ||
| + | <code> | ||
| + | Get-FileHash .\firmware.bin -Algorithm SHA256 | ||
| + | </code> | ||
| + | |||
| + | Copy the hash into ''manifest.json''. | ||
| + | |||
| + | |||
| + | ==== A2. Host a simple local update server ==== | ||
| + | |||
| + | In the directory containing: | ||
| + | |||
| + | * ''manifest.json'' | ||
| + | * ''firmware-1.0.1.bin'' | ||
| + | |||
| + | Run: | ||
| + | |||
| + | <code> | ||
| + | python3 -m http.server 8000 | ||
| + | </code> | ||
| + | |||
| + | You should be able to open from your laptop browser ''http://YOUR_PC_IP:8000/manifest.json'' | ||
| + | |||
| + | |||
| + | |||
| + | ===== Part B — Device firmware (pull OTA + hash + telemetry) ===== | ||
| + | |||
| + | Create/replace ''src/main.cpp'' with this [[iothings:laboratoare:2025_code:lab10_3|template]]. | ||
| + | |||
| + | **You must edit:** | ||
| + | * Wi-Fi credentials | ||
| + | * ''MANIFEST_URL'' to your PC/server IP | ||
| + | |||
| + | |||
| + | |||
| + | ===== Part C — Build, host, update ===== | ||
| + | |||
| + | 1) **Flash this version once over USB** (your pull-OTA baseline). | ||
| + | |||
| + | 2) Build a new firmware with: | ||
| + | |||
| + | * ''FW_VERSION = "1.0.1"'' | ||
| + | |||
| + | Also update the homepage text or the blink color of the Neopixel so the new firmware is easy to confirm. | ||
| + | |||
| + | 3) Build in PlatformIO and copy: | ||
| + | |||
| + | * ''.pio/build/sparrow_c6/firmware.bin'' | ||
| + | |||
| + | Rename it to: | ||
| + | |||
| + | * ''firmware-1.0.1.bin'' | ||
| + | |||
| + | 4) Compute SHA-256 and update ''manifest.json''. | ||
| + | |||
| + | 5) Host the files: | ||
| + | |||
| + | <code> | ||
| + | python -m http.server 8000 | ||
| + | </code> | ||
| + | |||
| + | 6) Reboot the device. | ||
| + | |||
| + | 7) Open ''http://DEVICE-IP/status'' and confirm telemetry fields exist. | ||
| + | |||
| + | |||
| + | ===== Part D — Test cases ===== | ||
| + | |||
| + | Complete these tests and record results. | ||
| + | |||
| + | |||
| + | ==== Test 1: No update needed ==== | ||
| + | |||
| + | * Manifest version equals device version. | ||
| + | |||
| + | **Expected:** | ||
| + | * Device prints "No update needed." | ||
| + | * ''last_error'' clears or remains empty. | ||
| + | |||
| + | ---- | ||
| + | |||
| + | ==== Test 2: Successful update ==== | ||
| + | |||
| + | * Manifest version is higher. | ||
| + | * Hash is correct. | ||
| + | |||
| + | **Expected:** | ||
| + | * Device updates and reboots. | ||
| + | * ''/status'' shows: | ||
| + | - ''last_result = "success"'' | ||
| + | - correct version transition values | ||
| + | |||
| + | |||
| + | ==== Test 3: Hash mismatch ==== | ||
| - | ===== Part C — OTA proof using a NeoPixel blink change ===== | + | Intentionally change the manifest hash to a wrong value. |
| - | Now you will make a visible change and deliver it **without USB**. Add a small LED pattern into main.cpp, you can get the new code from [[iothings:laboratoare:2025_code:lab10_2| here]]. | + | **Expected:** |
| + | * Device refuses update. | ||
| + | * ''last_error = "sha_mismatch"'' | ||
| + | * Old firmware continues running. | ||