Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
45 changes: 33 additions & 12 deletions docs/security-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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:

Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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_<board>_fe/` via `just build-fe <board>`, 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 <board>` 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 <board>` 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.
Expand Down
7 changes: 5 additions & 2 deletions flasher/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,13 @@ <h1>Web Flasher</h1>
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 },
];
Expand Down
2 changes: 1 addition & 1 deletion partitions.csv
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
24 changes: 16 additions & 8 deletions scripts/sign_release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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=()

Expand All @@ -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" <<EOF
{
"flash_files": {
"0x2000": "bootloader.bin",
"0x8000": "partition-table.bin",
"0x1e000": "ota_data_initial.bin",
"0x20000": "firmware-signed.bin"
"${OFF_BOOTLOADER}": "bootloader.bin",
"${OFF_PARTITION_TABLE}": "partition-table.bin",
"${OFF_OTADATA}": "ota_data_initial.bin",
"${OFF_APP}": "firmware-signed.bin"
}
}
EOF
Expand Down
4 changes: 4 additions & 0 deletions sdkconfig.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ CONFIG_IDF_TARGET="esp32p4"
CONFIG_APP_REPRODUCIBLE_BUILD=y
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_PARTITION_TABLE_CUSTOM=y
# 0x10000 gives the bootloader 56KB (0x2000..0x10000): the plain bootloader was 320 bytes
# from the 24KB cap, and the Phase 5 FE / Phase 6 secure boot bootloaders need ~36KB+.
# nvs moved to 0x11000 (shrunk 84->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.
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.14
0.0.15
Loading