1
0
Fork 0
mirror of https://github.com/imjasonh/esp32 synced 2026-07-06 23:52:24 +00:00
No description
Find a file
Jason Hall 5be27a560c Add cosign signature verification (OTA phase 4a)
Each OTA fetch is now gated on a Sigstore Bundle signature whose Fulcio
cert identifies a member of an allowlist hardcoded in src/trust.rs
(currently imjasonh@gmail.com / accounts.google.com). The allowlist
cannot be changed via OTA -- only by editing source and reflashing
over USB.

Publisher (Makefile): make publish now runs cosign sign --yes after
the publisher push. Cosign keyless OIDC pops a browser the first time;
subsequent signs reuse the cached token within ~10min.

Firmware:
- src/trust.rs: TRUSTED_IDENTITIES list + bundled Sigstore root and
  intermediate CA PEMs (trust/fulcio_root.pem, fulcio_intermediate.pem).
- src/sig.rs: parse Sigstore Bundle v0.3 (DSSE envelope), verify (a)
  Fulcio cert SAN email + OID 1.3.6.1.4.1.57264.1.1 issuer match the
  allowlist, (b) leaf cert chains to bundled Sigstore root via
  P-384 ECDSA-SHA384, (c) DSSE signature verifies via P-256 ECDSA-SHA256
  over the PAE, (d) in-toto Statement subject digest binds to our
  manifest digest.
- src/ota.rs: fetch_manifest now returns the manifest's own SHA256 (the
  digest cosign signed), not just the parsed body. New
  fetch_signature_bundle walks the OCI 1.1 referrers layout cosign uses
  (image index -> inner manifest -> bundle blob). Sig is fetched and
  verified before the firmware download starts.

Crates: p256, p384, x509-cert (with pem feature), base64. Adds
~350 KB to the firmware -- repartitioned ota slots from 1.5MB to
1.75MB to fit (USB-only migration). Bumped OTA thread stack to
48KB for cert-parsing headroom.

Verified end-to-end: Jason signed :latest with cosign keyless,
device polled, all four verification steps passed (identity, chain,
DSSE sig, in-toto binding), then downloaded and applied as before.

Phase 4b (Rekor SET / transparency log inclusion proof) and 4c
(operational hardening) remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 13:50:03 -04:00
.cargo initial commit 2026-05-02 11:43:41 -04:00
src Add cosign signature verification (OTA phase 4a) 2026-05-02 13:50:03 -04:00
tools/publisher Add OCI artifact publisher (OTA phase 1) 2026-05-02 12:29:51 -04:00
trust Add cosign signature verification (OTA phase 4a) 2026-05-02 13:50:03 -04:00
.gitignore Add OCI artifact publisher (OTA phase 1) 2026-05-02 12:29:51 -04:00
build.rs Add device-side OTA loop (OTA phase 2) 2026-05-02 12:53:19 -04:00
Cargo.toml Add cosign signature verification (OTA phase 4a) 2026-05-02 13:50:03 -04:00
eink-plan.md initial commit 2026-05-02 11:43:41 -04:00
LICENSE Initial commit 2026-05-02 11:43:06 -04:00
Makefile Add cosign signature verification (OTA phase 4a) 2026-05-02 13:50:03 -04:00
notes.txt Switch to OTA partition layout and document Wi-Fi + HTTPS demo 2026-05-02 12:14:21 -04:00
ota-plan.md Add cosign signature verification (OTA phase 4a) 2026-05-02 13:50:03 -04:00
partitions.csv Add cosign signature verification (OTA phase 4a) 2026-05-02 13:50:03 -04:00
README.md Add OCI artifact publisher (OTA phase 1) 2026-05-02 12:29:51 -04:00
rust-toolchain.toml initial commit 2026-05-02 11:43:41 -04:00
sdkconfig.defaults.in Add device-side OTA loop (OTA phase 2) 2026-05-02 12:53:19 -04:00

ESP32 Rust

Std Rust on an Inland ESP-WROOM-32 dev board. Currently a Wi-Fi + HTTPS demo; e-ink display work coming next (see eink-plan.md).

Hardware

  • Board: Inland ESP-WROOM-32 (Micro Center SKU 027466). Connects as /dev/cu.usbserial-0001 on macOS via the onboard CP210x USB-UART.
  • No user LED: this specific board variant only has a power LED ("D1", always on). There is no software-controllable LED. For visible output, attach an external LED + ~330Ω resistor to a GPIO, or watch logs via make monitor.

Prerequisites (one-time, macOS)

# Cargo tools:
#   espup     installs the Xtensa Rust toolchain (rustc fork + LLVM + GCC)
#   espflash  flashes firmware over USB serial
#   ldproxy   linker shim used by esp-idf-sys
cargo install espup espflash ldproxy

# C build deps for the ESP-IDF SDK
brew install cmake ninja dfu-util

# Xtensa Rust toolchain + GCC + esp-clang, installed under
# ~/.rustup/toolchains/esp/. The Makefile finds them via wildcards
# so future espup updates don't require any path edits.
espup install --targets esp32

# uv (the Makefile uses it to install Python 3.12 and shim it into PATH
# so the ESP-IDF Python venv is bootstrapped against 3.12 instead of
# Apple's 3.9). Skip if you already have uv.
curl -LsSf https://astral.sh/uv/install.sh | sh

First build

make wifi.env             # creates wifi.env from template
$EDITOR wifi.env          # fill in WIFI_SSID and WIFI_PASS
make run                  # build, flash, open serial monitor

The first build clones ESP-IDF v5.2.2 into .embuild/ and bootstraps a Python venv inside it. Plan on 510 minutes. Subsequent builds are fast.

Day-to-day

make build      Compile (requires wifi.env)
make flash      Build (if needed) and flash app to PORT
make flash-all  Erase + write bootloader, partition table, and app
make monitor    Open serial monitor; Ctrl+C to exit
make run        Build + flash + monitor
make clean      cargo clean

make flash PORT=/dev/cu.usbserial-XXXX   # override the port

Use make flash-all after partition-table changes, or as the recovery path if both OTA slots end up bad and the bootloader can't auto-revert (see "USB recovery" in ota-plan.md). Day-to-day use is just make flash or make run.

Publishing firmware to GHCR (OTA, phase 1)

The firmware is packaged as an OCI artifact and pushed to ghcr.io/imjasonh/esp32. The device will pull it back down (phase 2, not yet implemented).

One-time setup:

  1. Mint a classic PAT at https://github.com/settings/tokens/new with the write:packages scope. (Fine-grained PATs don't currently expose a Packages permission for user-owned packages.)
  2. echo 'export GH_TOKEN=ghp_...' > gh.env (gitignored)
  3. After the first push, set the package's visibility to public at https://github.com/users/imjasonh/packages/container/esp32/settings — the device will pull anonymously, no token on the ESP32.

Then:

make publish      # build firmware, save .bin, push :latest and :sha-<short>

The publisher tool lives at tools/publisher/ — Rust, uses oci-distribution. It's a separate cargo project so it builds for the host, not for the ESP32.

What the firmware does

src/main.rs connects to Wi-Fi using the credentials baked in at compile time from wifi.env, then issues two HTTPS GETs and logs the responses over serial:

  • https://api.ipify.org?format=json — public IP, JSON
  • https://wttr.in/?format=3 — one-line weather for your location

Project layout

src/main.rs            firmware
Cargo.toml             deps: esp-idf-svc, embedded-svc, log, anyhow
build.rs               embuild + cargo:rerun-if-env-changed for WIFI_*
.cargo/config.toml     target, linker, runner, build-std
rust-toolchain.toml    pins the `esp` rustc toolchain
sdkconfig.defaults     ESP-IDF kconfig overrides (stack, mbedtls bundle)
Makefile               canonical build / flash / monitor entrypoint
wifi.env.example       template for compile-time Wi-Fi creds
eink-plan.md           plan for the upcoming e-ink display work
ota-plan.md            plan for OCI-artifact-based OTA updates
tools/publisher/       Rust tool that pushes firmware as an OCI artifact
notes.txt              internal design notes and issues hit during setup