Add admin firmware upload support

Implement authenticated HTTPS OTA uploads with bounded streaming, image
validation, reboot coordination, and lifecycle exclusion. Add the admin
UI,
regression tests, and Phase 10 acceptance documentation.
This commit is contained in:
2026-09-18 22:22:11 +02:00
parent 4f628a4098
commit 31a22eba06
31 changed files with 1442 additions and 88 deletions
+36 -13
View File
@@ -40,7 +40,7 @@ These constraints apply across all phases:
| 7 | Local display and button interface | **Complete** |
| 8 | Role-based users and administrative access | **Complete** |
| 9 | Small intermittent-use security baseline | **Complete (user signoff 2026-09-18; new hardware check waived)** |
| 10 | Simple admin web firmware upload | **Planned** |
| 10 | Simple admin web firmware upload | **Complete (explicit user acceptance 2026-09-18; upload and normal operation verified)** |
| 11 | BLE serial transport and provisioning evaluation | **Planned** |
| 12 | Advanced network integration | **Under evaluation** |
| 13 | Optional filesystem-backed features | **Under evaluation** |
@@ -251,26 +251,49 @@ Small implementation:
**Acceptance (2026-09-18):** the user signed off this small Phase 9 and explicitly waived a new whole-phase hardware check of boot, UART0 recovery, native USB serial, HTTPS/SSH login and normal serial sharing. Application code is unchanged, **but crash-diagnostic defaults changed**; this is a waiver, not evidence of target or panic-path validation. The prior build/configuration evidence above remains the validation record; no new hardware test was performed. A separately controlled panic check with synthetic secrets remains optional, not a completion gate; do not add a production panic endpoint or erase flash. Disabling dumps does not erase old contents.
## Current and planned phases
**Phases 8 and 9 are complete** for their accepted scopes; Phase 9 completion includes the explicit new-hardware-check waiver above. Phase 10 is planned, not implemented; later work remains planned or under evaluation. Optional features must not weaken completed serial and recovery paths. General release gates below guide future work, not claims that every fault, soak or reserve measurement was performed for Phase 8 or that Phase 9 received new target validation.
### Phase 10 — Simple admin web firmware upload
**Plan only.** Add a file picker and upload button in the existing admin web interface. The owner builds the firmware locally and chooses the application `firmware.bin`; there is no release server, automatic download or update service. Treat an authenticated administrator as authorized to replace the application, as the physical owner can through wired flashing.
**Complete — explicit user acceptance on 2026-09-18: firmware upload works and normal operation is verified.** The admin **Settings → HTTPS / Reboot** firmware card uploads the locally built `.pio/build/esp32-s3-devkitc-1-n16r8/firmware.bin`. Install this OTA-enabled firmware by wire first, then use web application uploads. There is no release server, automatic download or update service. [Firmware update and recovery](#phase10-update-and-recovery) below covers operation; [regression guidance](#phase10-regression-guidance) covers future checks.
Planned work:
Implemented scope:
- Reuse existing HTTPS admin authentication, same-origin and CSRF protections; ordinary `user` accounts cannot upload firmware. Require an explicit upload/reboot confirmation and show progress, success or an actionable error. Never automatically retry a possibly completed update.
- Use standard ESP-IDF OTA APIs (`esp_ota_begin`, `esp_ota_write`, `esp_ota_end`, `esp_ota_set_boot_partition`) to stream one upload at a time through a bounded buffer into the inactive application slot. Use the existing two 4 MiB slots and `otadata`; no custom flash protocol or whole-image RAM buffer.
- Accept an ESP32-S3 application binary for this board/layout, not an ELF, ZIP, merged full-flash image, bootloader or partition-table image. Check nonempty/complete upload, target/header compatibility and actual destination capacity; use SDK image validation before selecting the new boot partition. A `.bin` filename alone is not validation. Basic format/integrity checks do not prove authenticity or that the application will work.
- On interrupted, invalid or failed uploads, abort the OTA operation and leave the current boot selection unchanged. Select the new slot only after successful final validation, report completion and reboot in a controlled way. Upload may disrupt network/serial activity; reboot interrupts every transport, so do not promise uninterrupted operation.
- Raw `POST /api/firmware` requires exact `Content-Type: application/octet-stream`, known exact `Content-Length`, cookie, same-origin Origin and `X-CSRF-Token`; no multipart/JSON, Basic authentication or ordinary `user` access. Reject queries, ambiguous headers, chunked transfer and `Expect`; UI code leaves browser-managed headers alone. Admission precedes body/flash work; session/principal currentness is rechecked before boot selection. The card confirms upload/reboot and shows progress/results without automatic retries. HTTPD capacity is 40 method/path handlers.
- Standard ESP-IDF OTA APIs (`esp_ota_begin`, `esp_ota_write`, `esp_ota_end`, `esp_ota_set_boot_partition`) stream one upload through a 4 KiB buffer into the inactive slot (two 4 MiB app slots). A reboot task with a 2 KiB stack is preallocated before erase. Lifecycle/identity reservations and ordinary software-reboot gating exclude competing work; no custom flash protocol or whole-image RAM buffer.
- Accepts an ESP32-S3 application binary for this board/layout, not an ELF, ZIP, merged full-flash image, bootloader or partition-table image. Browser hints require a nonempty `.bin` of at most 4 MiB; the server bounds raw length against actual destination capacity before trusting HTTPD's narrowed length. Checks completeness, target/header and exact SDK image length/integrity, including mandatory appended SHA-256, before selection. The digest is not publisher authentication; neither it nor filename checks prove board compatibility or a working application.
- Before selection, rejected/incomplete uploads and receive/write/validation failures abort any live OTA handle without selecting the candidate; the inactive slot may be erased/partially written. Boot-metadata failure (`firmware_commit_failed`) needs inspection and carries SDK transactional uncertainty. `200 {"ok":true,"rebooting":true}` means validation/selection succeeded; successful synchronous send schedules restart after 500 ms, retaining reservations, but proves neither browser receipt nor boot success. Response failure after selection schedules no restart, releases reservations for manual reboot and latches further admissible uploads to 409 `firmware_selected_reboot_required`; the latch survives HTTPS stop/start until device reset.
- Synchronous HTTPD receive/flash blocks other HTTPD work: browser sessions can stall/drop. The 120-second total receive-loop and ten-second stall checks use the existing one-second socket timeout, not an absolute deadline: synchronous SDK erase/write/validation and scheduling are not preempted. The browser's 180-second timeout cannot cancel committed work. UART0/native USB remain independent paths, not guarantees of uninterrupted serial timing during flash; reboot interrupts all transports. Software exclusion cannot prevent physical reset, power loss or panic.
- **Preserve NVS:** write only the inactive application slot and the OTA selection metadata. Do not erase the chip, rewrite the partition table/bootloader, or touch `nvs`, `nvs_key`, PHY, storage or other data partitions. Existing users, passwords, Wi-Fi/serial settings and HTTPS/SSH identities remain stored, as with an application-only wired update without erase. This preserves stored bytes; the uploaded firmware must still understand the existing schemas and must not itself erase/migrate them incompatibly.
- Keep wired USB-to-UART flashing documented as recovery if the uploaded application does not boot or no longer serves the web UI. Basic image validation cannot prevent a valid but broken application from requiring wired recovery.
**Not in scope:** secure boot, image signatures/signing-key management, anti-rollback/version-downgrade enforcement, automatic rollback/post-boot health-confirmation machinery, remote release discovery, partition migration or NVS backup/restore. Use upstream OTA support without dependency patches. The administrator is responsible for selecting trusted, compatible firmware, including when deliberately installing an older build.
**Acceptance:** test a successful application upload/reboot, invalid/wrong-target/oversized and interrupted uploads, authorization rejection, and retained configuration/identities after update. Confirm wired recovery remains possible. These are future tests, not execution claims; no new cryptographic certification or exhaustive fault campaign is required.
**Historical integration build and host evidence (parent-reported):** `pio run` **PASS**, **94,220 B linked RAM / 1,847,645 B flash**. Against Phase 9's 94,196 B / 1,828,565 B, this is **+24 B RAM / +19,080 B flash**. Final backend 88 cases plus the pinned SDK begin/abort failure-contract test, UI 169 groups, server lifecycle 44, admin transport 25, console lifecycle, SSH runtime and cookie lifecycle checks passed. Additional cookie base/admin/display/lifecycle, HTTPD idle cleanup 18, and SSH management/runtime/security checks passed; the extra cookie `--admin` run initially lacked a reboot symbol in its fixture, corrected in test-only code before passing. Host doubles are not target evidence. The later concise-UI fix was copy-only; its latest reported UI regression passed all **169 groups**, with no rebuild after the text change. The build figures above remain historical; this documentation update ran no build or tests.
**Acceptance (2026-09-18):** after the firmware upload implementation and concise-UI fix, the user confirmed: “That works perfectly. And the usual operation is also verified.” Phase 10 is complete by this explicit acceptance of working upload and verified normal operation. Do not infer specific fault-injection, NVS before/after comparisons, power-loss or wired-recovery passes. The [regression guidance](#phase10-regression-guidance) below is reusable future guidance, not an acceptance blocker.
#### Phase10 update and recovery
1. Keep **USB-to-UART** available for UART0 administration/flashing; native USB CDC is network-independent UART1 access, not administration. Install the updater by wire first: older firmware without the route cannot install its own first web updater. The custom OTA layout is required; [one-time old-layout migration](../README.md#one-time-migration-from-the-default-partition-table) is separate and destructive.
2. Build trusted, schema-compatible `esp32-s3-devkitc-1-n16r8` firmware with `pio run`; choose **`.pio/build/esp32-s3-devkitc-1-n16r8/firmware.bin`**, never a merged image or a renamed non-application file. Observe the [downgrade warning](../README.md#legacy-credential-removal).
3. Save desired RAM-only settings and record nonsecret configuration/public HTTPS/SSH fingerprints. Arrange a maintenance window and stable power/network; sign in as `admin` over trusted HTTPS, open **Settings → HTTPS / Reboot → Firmware update**, choose the file and confirm **Upload and reboot…**.
4. Wait for validation/reboot, not just 100% transmitted bytes. Restore network reachability, reload and explicitly sign in; verify the running application, saved configuration and identities before another upload.
5. A lost response, timeout, page close, sign-out or browser abort proves neither cancellation nor failure. The UI locks uncertain outcomes without replay; reload clears only browser locks, not the device latch. Inspect through UART0 and deliberately reboot if appropriate: any later reset can boot an already selected image.
6. For initial wired installation or recovery when boot/HTTPS fails, connect USB-to-UART, select a known-good compatible checkout, run `pio run --target upload`, then `pio device monitor -b 115200`. **Do not erase for ordinary updates or recovery**; there is no automatic recovery flashing/rollback.
Replies: **400** malformed/incompatible/incomplete input; **401/403** authentication/Origin/CSRF or `admin_required`; **408** `firmware_timeout`; **409** selected-image latch; **413/415** destination capacity/content type; **500** `firmware_write_failed`/`firmware_commit_failed`; **503** busy/unavailable/resources. Unread rejected bodies close rather than drain; bounded secret-free JSON may never reach a disconnected browser.
#### Phase10 regression guidance
These are reusable checks, **not recorded passes or outstanding acceptance gates**. Record only performed outcomes; host flash/network/scheduling doubles are not hardware evidence. Focused host suites: `python3 tests/web_firmware_update/run.py` and `python3 tests/web_ui_session/run.py` (the SDK begin/abort case covers a live handle published on erase failure).
- Exercise wired-first install, known-good upload/reboot/new build and explicit reconnect; compare saved users/roles, password/key login, Wi-Fi/serial/display/hostname settings and public HTTPS/SSH fingerprints without recording secrets.
- Interrupt/cancel receive; reject corrupt, truncated, wrong-target, non-application and oversized inputs without unintended selection/restart, then deliberately perform a valid update. Reject unauthenticated/ordinary-user, wrong-Origin and missing/wrong-CSRF requests before flash. Never attempt power interruption at commit without wired recovery ready.
- Exercise competing upload, HTTPS lifecycle/identity and ordinary reboot exclusion; HTTPD requests may wait rather than promptly return busy. With controlled post-selection response-failure injection, verify no automatic restart, subsequent 409 (also after HTTPS restart), then manual reboot; an arbitrary disconnect does not establish this fault case.
- Observe web stalls/drops and serial/network recovery, single-writer isolation, UART0 recovery and native USB UART1 access without networking; demonstrate known-good wired recovery without erase and recheck configuration/identities. Do not claim uninterrupted traffic.
## Current and planned phases
**Phases 8, 9 and 10 are complete** for their accepted scopes. Phase 9 includes the explicit new-hardware-check waiver above; Phase 10 includes explicit user acceptance of upload and normal operation. Phase 11 remains planned; later work is under evaluation. Optional features must not weaken completed serial and recovery paths. General release gates below guide future work, not claims that every fault, soak, recovery or reserve measurement was performed for completed phases.
### Phase 11 — BLE
@@ -332,7 +355,7 @@ The following are not implemented merely because flash partitions or library sup
- NVS, flash, or PSRAM encryption.
- Secure boot or production eFuse provisioning.
- Automatic OTA downloads, image signing, post-boot health confirmation and automatic rollback. The simple admin upload is planned in Phase 10.
- Automatic OTA downloads, image signing, post-boot health confirmation and automatic rollback. The simple admin upload is complete in Phase 10; these advanced update features remain deferred.
- Core-dump collection or secret-safe core-dump processing.
- Filesystem mounting.