diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f488fa0..ef198473 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.0.15] - 2026-07-24 + +### Added +- PSBT signing: when a transaction cannot be verified because no matching descriptor is loaded, the scan flow offers to load one on the fly instead of dead-ending +- The ESP32-C6 Wi-Fi/BT co-processor is held in reset on every board, so the radio never comes up on this air-gapped signer + +### Changed +- **Partition table moved from 0x8000 to 0x10000**, enlarging the bootloader slot to 56KB — headroom the future flash-encryption and secure-boot bootloaders will need (the plain bootloader was already 320 bytes short of the old 24KB cap). NVS absorbs the shift — it moves to 0x11000 and shrinks from 84KB to 52KB; otadata, both app slots, and storage keep their offsets. **Existing devices need one serial reflash and NVS re-setup (PIN and settings); SD-card and OTA updates cannot cross this change.** +- Every build now carries a Secure Boot v2 signature block: IDF aborts at boot when the running app is unsigned, so dev builds are auto-signed with a per-clone throwaway key (generated on first build, gitignored) and releases re-sign the retained unsigned image offline with the real key +- Updated libwally + + ## [0.0.14] - 2026-07-20 ### Added diff --git a/docs/security-plan.md b/docs/security-plan.md index 7626f0c2..d11fd3f0 100644 --- a/docs/security-plan.md +++ b/docs/security-plan.md @@ -37,7 +37,7 @@ The ESP32-P4 is not a secure element; two physical attacks remain open by constr ### Usable for Kern - **Secure Boot v2** — **RSA-3072 (RSA-PSS)**, up to 3 key digest slots in eFuse. Kern burns all 3 (KEY0/1/2) to allow two key rotations. **ECDSA is not used**: ECDSA-based Secure Boot v2 is non-functional on ESP32-P4 silicon (chip errata; would require `CONFIG_SECURE_BOOT_INSECURE`). RSA-3072 is also the faster verifier here (~14.8 ms vs ~61.1 ms). See [secure-boot.md](secure-boot.md). -- **Flash Encryption** — XTS-AES-128/256, auto-encrypts PSRAM too. On ESP32-P4 the **Key Manager** can hold the XTS key outside the shared eFuse key blocks (Kconfig choice `SECURE_FLASH_ENCRYPTION_KEY_SOURCE`, option `..._KEY_MGR`), which is what lets Kern keep XTS-AES-256 while spending KEY0–KEY2 on secure boot digests. +- **Flash Encryption** — XTS-AES-128/256, auto-encrypts PSRAM too. The **Key Manager** could hold the XTS key outside the shared eFuse key blocks (Kconfig choice `SECURE_FLASH_ENCRYPTION_KEY_SOURCE`, option `..._KEY_MGR`), but IDF gates that option on ESP32-P4 silicon ≥ v3 (`depends on !(IDF_TARGET_ESP32P4 && ESP32P4_SELECTS_REV_LESS_V3)`); fielded Kern boards measured v1.0 (wave_4b) and v1.3 (wave_43), so Kern uses **XTS-AES-128 in eFuse KEY3** instead — see the key-block note below. - **NVS Encryption** — HMAC-based key derivation from eFuse (no separate key partition needed); independent of flash encryption - **RSA/MPI + Digital Signature peripherals** — hardware-accelerated RSA; used by Secure Boot v2 RSA-3072 verification at boot - **HMAC peripheral** — computes HMAC-SHA256 using eFuse keys without exposing them to software; used for anti-phishing word derivation (`esp_hmac_calculate()` with `HMAC_KEY5` purpose) and NVS key derivation (KEY4) @@ -66,7 +66,7 @@ The ESP32-P4 is not a secure element; two physical attacks remain open by constr All 6 key blocks allocated. Kern spends **three** blocks on secure-boot digests (KEY0–KEY2) so it can rotate a compromised key twice instead of once. Because all three digest *slots* are populated, no free slot remains for an attacker to inject a rogue signing key. Both HMAC keys (KEY4, KEY5) use the `HMAC_UP` purpose — software can request HMAC computations but never reads the raw key material. -**Flash-encryption note:** With three digest slots taking KEY0–KEY2, only one key block (KEY3) is left, but XTS-AES-256 normally needs two. Kern keeps 256-bit strength by deploying the flash-encryption key through the ESP32-P4 **Key Manager** (Kconfig choice `SECURE_FLASH_ENCRYPTION_KEY_SOURCE`, option `..._KEY_MGR`), which stores the key outside the shared eFuse blocks (leaving KEY3 spare). The fallback, if the Key Manager path is not adopted, is **XTS-AES-128** stored in KEY3 (single block). This is finalized in Phase 5. +**Flash-encryption note (decided July 2026):** With three digest slots taking KEY0–KEY2, only one key block (KEY3) is left, but XTS-AES-256 needs two. The hoped-for escape — deploying the key through the ESP32-P4 **Key Manager**, outside the eFuse blocks — turned out to be revision-gated: `SECURE_FLASH_ENCRYPTION_KEY_SOURCE_KEY_MGR` depends on `!(IDF_TARGET_ESP32P4 && ESP32P4_SELECTS_REV_LESS_V3)` (IDF 6.x), i.e. it is only offered to builds that mandate chip revision ≥ v3.0. Fielded boards measured **v1.0 (wave_4b) and v1.3 (wave_43)**, so raising the revision floor would drop existing hardware. **Plan of record: XTS-AES-128 in KEY3** — a single block, generated by the FE bootloader from TRNG on first boot and read-protected at burn time; the key never exists outside the chip. If the minimum supported revision ever moves to v3, the KM + XTS-AES-256 path can be revisited (leaving KEY3 spare); `sdkconfig.fe` documents the exact config switch. Beware: Kconfig **silently falls back** to the eFuse key source when the KM dependency is unmet — after any key-source change, verify the generated sdkconfig. ## Partition Table @@ -85,7 +85,7 @@ ota_1 app ota_1 0x820000 0x400000 (4M) storage data spiffs 0xC20000 0x3E0000 (~3.9M) ``` -### Target layout (Phase 3 migration) +### Target layout (Phase 3 migration; since revised — see the bootloader-headroom revision below) Phase 3 replaces the layout above with an OTA-only table: @@ -110,6 +110,24 @@ storage data spiffs 0xC20000 0x3E0000 (~3.9M) - NVS grows 24KB → 84KB, filling the space up to `ota_0` exactly. More pages improve wear-leveling for the failure-counter write on every PIN attempt. Note that old NVS content **survives** the resize: NVS pages are self-contained 4K units, so the old pages parse fine inside the larger partition, and flashing never writes the `nvs` region. A pre-existing plaintext PIN is therefore still present after the update — handled by the firmware-side migration in 3a. - `phy_init` is dropped — the ESP32-P4 has no radio, no `CONFIG_ESP_PHY*` option is set, and the partition was never read. +### Bootloader-headroom revision (pre-Phase 5, July 2026) — current layout + +The Phase 3 layout kept the partition table at the default offset 0x8000, capping the bootloader at 24KB. The plain bootloader had grown to within 320 bytes of that cap, and the Phase 5 flash-encryption bootloader (~36KB) — let alone the Phase 6 secure-boot one — does not fit. Since the table is frozen for the life of the device once Phase 6 restricts serial to signed images, the offset was moved to **0x10000** before any Profile B device exists: + +``` +# Name Type SubType Offset Size (Human) +nvs data nvs 0x11000 0xD000 (52K) +otadata data ota 0x1E000 0x2000 (8K) +ota_0 app ota_0 0x20000 0x600000 (6M) +ota_1 app ota_1 0x620000 0x600000 (6M) +storage data spiffs 0xC20000 0x3E0000 (~3.9M) +``` + +- `CONFIG_PARTITION_TABLE_OFFSET=0x10000` (in `sdkconfig.defaults`) — the bootloader slot grows 24KB → 56KB, with headroom for the secure-boot bootloader beyond the FE one. +- The shift is absorbed entirely by `nvs` (moved to 0x11000, shrunk 84KB → 52KB — still ample for the PIN hash and settings); `otadata`, both app slots, and `storage` keep their exact offsets, so OTA app images and SPIFFS contents survive. +- Cost for already-flashed devices: one serial re-flash plus an NVS wipe (PIN/settings re-setup); apps and `storage` are untouched. +- This is a single global move for **all boards and profiles**, not an FE-only override: `CONFIG_PARTITION_TABLE_OFFSET` is compiled into the app, so a per-profile table offset would fork the firmware artifacts — an app built for one offset cannot run on a device whose table sits elsewhere, and a mixed fleet would invite bricking mismatches through the SD update path. + ## Phase Ordering Rationale The lockdown phases are ordered so that the **roadmap order is also the safe, irreversible eFuse burn order** — there is no separate "burn order" to track. Every read-protected key (KEY5, KEY4, the flash-encryption key) is burned in Phases 2, 3 and 5, all *before* secure boot (Phase 6) write-protects `RD_DIS`; Phase 4 (SD updates) burns nothing: @@ -174,7 +192,7 @@ Per Espressif's security-features workflow, **Flash Encryption must be enabled b - KEY5 (anti-phishing HMAC) — Phase 2, during PIN setup. - KEY4 (NVS HMAC) — Phase 3, during PIN setup. -- Flash-encryption XTS key — Phase 5. The ESP32-P4 **Key Manager** path may store this key outside a shared eFuse block; confirm on a dev board. +- Flash-encryption XTS key — Phase 5, KEY3 (XTS-AES-128): generated by the FE bootloader on first boot and read-protected at burn time. (The Key Manager path that would have kept KEY3 spare requires ≥ v3 silicon — see the eFuse key-block note.) ## Implementation Phases @@ -339,15 +357,18 @@ CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID=4 # BLOCK_KEY4 **Irreversible eFuse commitment — practice on a dev board first.** -#### 5a. Flash-encryption key provisioning -- Provision the **flash-encryption XTS key**: preferably via the ESP32-P4 **Key Manager** (`CONFIG_SECURE_FLASH_ENCRYPTION_KEY_SOURCE_KEY_MGR`), keeping XTS-AES-256 with the key outside the shared blocks; fallback is **XTS-AES-128** in eFuse KEY3. Decide and validate before committing. -- KEY4 (NVS, Phase 3) and KEY5 (anti-phishing, Phase 2) are already burned. -- **Key Manager caveats to validate on the dev board:** the KM key is device-internal (HUK-derived) — the host can never learn it, so `esptool`-style pre-encrypted flashing does not exist with a KM key; confirm what development-mode serial recovery actually looks like in practice. Also check whether the KM path removes the `RD_DIS` ordering constraint entirely (the key never occupies an eFuse block that needs read-protection) — if so, a later Profile A → B upgrade (flash encryption *after* secure boot) may still be possible. Document the result either way. +#### 5a. Flash-encryption key provisioning ✅ (decided July 2026) +- **XTS-AES-128, eFuse KEY3, bootloader-generated**: the FE bootloader generates the key from TRNG on first boot and burns it into KEY3 with read-protection — the key never exists outside the chip, so there is no host-side key ceremony and nothing to back up or leak. +- The preferred **Key Manager + XTS-AES-256** path is not available on current silicon: the Kconfig option is gated on chip revision ≥ v3.0 (see the eFuse key-block note), and fielded boards measured v1.0/v1.3. The KM questions from earlier drafts (what dev-mode recovery looks like without a host-known key, whether KM lifts the `RD_DIS` ordering constraint) are moot unless the revision floor is ever raised. +- KEY4 (NVS, Phase 3) and KEY5 (anti-phishing, Phase 2) are already burned; KEY3 is the last read-protected burn before Phase 6 locks `RD_DIS`. + +#### 5b. Enable flash encryption (development mode) — tooling in place ✅, **blocked on IDF ≥ 6.1** (see status note) -#### 5b. Enable flash encryption (development mode) -- `CONFIG_SECURE_FLASH_ENC_ENABLED` with **XTS-AES-256**; key size / source per 5a. -- **Not an on-device toggle**: flash an FE-built, signed bootloader over serial; the bootloader self-encrypts flash (bootloader + app + `encrypted` partitions) *in place* on first boot (~1 min). **Do not interrupt power** — a partial pass corrupts flash. -- Development mode retains serial fallback for re-flashing/recovery. +**Status (July 2026): first on-device provisioning attempt failed — known IDF/P4 limitation.** The encryption pass itself succeeded (bootloader decrypts and boots the app), but the device WDT-bootloops ~1s into the splash animation, and the display never shows a frame (DSI underrun blue). Root cause: flash encryption auto-sets the MMU "sensitive" bit on **every PSRAM page** (`mmu_ll_write_entry`, with an anti-FI assert — no opt-out in IDF 6.0.x), pushing all cache traffic through the XTS engine, while DMA masters (MIPI DSI framebuffer refresh, camera/ISP) access PSRAM outside that path. Upstream issues with the same signature: [esp-idf#17708](https://github.com/espressif/esp-idf/issues/17708) (LVGL + DSI, WDT crash), [#17701](https://github.com/espressif/esp-idf/issues/17701) (bootloop), [#18427](https://github.com/espressif/esp-idf/issues/18427) (H264 DMA hang, open). The fix mechanism landed in **IDF v6.1-beta1**: `CONFIG_SPIRAM_ENC_EXEMPT` carves an unencrypted PSRAM region (top of PSRAM, `MALLOC_CAP_SPIRAM_NO_ENC`). Unblocking Phase 5 therefore needs: (1) IDF ≥ 6.1, (2) DSI framebuffers and camera/ISP buffers allocated from the exempt region (esp_lcd DPI / esp_lvgl_adapter / esp_video support or patches), and (3) a threat-model amendment: display and camera frames become plaintext in PSRAM at rest — this includes SeedQR display/scan moments, an accepted narrowing of the PSRAM-encryption guarantee (in-package hex PSRAM probing remains a highly sophisticated physical attack). +- `sdkconfig.fe` overlay: `CONFIG_SECURE_FLASH_ENC_ENABLED=y`, development mode, XTS-AES-128 per 5a. FE variants build into `build__fe/` via `just build-fe `, leaving normal builds untouched; images inherit the Phase 4 signed-app config as usual. +- Partition-table prerequisite done: the 0x10000 table offset gives the ~36KB FE bootloader room in its 56KB slot (see [Partition Table](#partition-table)). +- **Not an on-device toggle**: `just flash-fe ` performs the one-time plaintext provisioning flash (with confirmation prompt); the bootloader self-encrypts flash (bootloader + app + `encrypted` partitions) *in place* on first boot (~1 min). **Do not interrupt power** — a partial pass corrupts flash. Follow with `just monitor-fe ` to watch the pass complete. +- Development-mode serial recovery: the ROM encrypts during download (`SECURE_FLASH_UART_BOOTLOADER_ALLOW_ENC`), so already-encrypted devices re-flash with `just flash-fe-enc` (full) / `just flash-fe-app` (app only). A plain plaintext flash leaves an encrypted device unbootable until re-flashed encrypted. - PSRAM encryption is enabled automatically — protects runtime key material in external RAM. - The `storage` (SPIFFS) partition stays **plaintext**: SPIFFS does not support flash encryption, so the first-boot pass skips it (no `encrypted` flag). Mnemonics there are always KEF envelopes, so nothing key-critical depends on this — see [Cross-Phase Security Dependencies](#cross-phase-security-dependencies) and [Accepted Residual Risks](#accepted-residual-risks). - Verify encrypted flash contents are unreadable via physical extraction. diff --git a/flasher/index.html b/flasher/index.html index bf04cd46..6cbc1afd 100644 --- a/flasher/index.html +++ b/flasher/index.html @@ -227,10 +227,13 @@

Web Flasher

import { ESPLoader, Transport } from './vendor/esptool-js-0.6.0.js'; // Fallback flash layout, only used if flasher_args.json (the canonical source -// of offsets generated by the build) is missing or malformed. +// of offsets generated by the build) is missing or malformed. Tracks the +// current layout: the partition table moved 0x8000 -> 0x10000 in v0.0.15. +// Bundles for <= v0.0.14 carry their own flasher_args.json with the old +// offset, so they keep flashing correctly without a fallback. const FLASH_FILES_FALLBACK = [ { name: 'bootloader.bin', offset: 0x2000 }, - { name: 'partition-table.bin', offset: 0x8000 }, + { name: 'partition-table.bin', offset: 0x10000 }, { name: 'ota_data_initial.bin',offset: 0x1e000 }, { name: 'kern.bin', offset: 0x20000 }, ]; diff --git a/partitions.csv b/partitions.csv index 4c852328..4f0121d5 100644 --- a/partitions.csv +++ b/partitions.csv @@ -1,5 +1,5 @@ # Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x15000, +nvs, data, nvs, 0x11000, 0xD000, otadata, data, ota, 0x1E000, 0x2000, ota_0, app, ota_0, 0x20000, 0x600000, ota_1, app, ota_1, 0x620000, 0x600000, diff --git a/scripts/sign_release.sh b/scripts/sign_release.sh index 64f89d8d..a268c172 100755 --- a/scripts/sign_release.sh +++ b/scripts/sign_release.sh @@ -66,6 +66,14 @@ else exit 1 fi +# Flash layout — must match CONFIG_PARTITION_TABLE_OFFSET and partitions.csv. +# The partition table moved 0x8000 -> 0x10000 in v0.0.15 (56KB bootloader slot); +# images produced here are not flashable on top of a <= v0.0.14 layout. +OFF_BOOTLOADER=0x2000 +OFF_PARTITION_TABLE=0x10000 +OFF_OTADATA=0x1e000 +OFF_APP=0x20000 + FOUND=0 ZIPS=() @@ -87,20 +95,20 @@ for UNSIGNED in "$RELEASE_DIR"/*/firmware.bin; do $ESPTOOL --chip esp32p4 merge-bin \ --format hex \ -o "$HEX" \ - 0x2000 "$DEVICE_DIR/bootloader.bin" \ - 0x8000 "$DEVICE_DIR/partition-table.bin" \ - 0x1e000 "$DEVICE_DIR/ota_data_initial.bin" \ - 0x20000 "$SIGNED" + "$OFF_BOOTLOADER" "$DEVICE_DIR/bootloader.bin" \ + "$OFF_PARTITION_TABLE" "$DEVICE_DIR/partition-table.bin" \ + "$OFF_OTADATA" "$DEVICE_DIR/ota_data_initial.bin" \ + "$OFF_APP" "$SIGNED" # flasher_args.json lets the web flasher's custom-ZIP mode find the signed # app; offsets must match the merge-bin layout above cat > "$DEVICE_DIR/flasher_args.json" <52KB) so otadata/ota_0/ota_1/storage keep their offsets. +CONFIG_PARTITION_TABLE_OFFSET=0x10000 # NVS encryption, HMAC scheme (keys derived from eFuse KEY4, no nvs_keys partition). # The app never calls nvs_flash_init(): its keygen path would burn KEY4 without # consent. Boot branches on KEY4 presence in core/nvs_secure.c instead. diff --git a/version.txt b/version.txt index 9789c4cc..ceddfb28 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.0.14 +0.0.15