diff --git a/AGENTS.md b/AGENTS.md index 6ebcdf2..49fa0ff 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,101 +1,43 @@ -# Beacon CM4 — Quick Reference +# Beacon CM4 — Agent Quick Reference > Full docs: `beacon-buildroot/README.md` +## Device +- **Login**: `user` / `beacon` (sudo passwordless; root login disabled) +- **MAC**: `d8:3a:dd:a8:9a:40` — IP is DHCP, changes on reboot +- **Find IP**: `CM4=$(ip neigh show dev enp0s31f6 | awk '/d8:3a:dd:a8:9a:40/{print $1; exit}')` +- **SSH**: `sshpass -p beacon ssh user@$CM4` — use `-tt` for `rauc` commands +- **File transfer**: `scp` broken on Dropbear → use `ssh 'sudo tee /upload/file >/dev/null' < localfile` +- **UART**: `picocom -b 115200 /dev/ttyUSB0` — user handles power cycling manually + ## Build ```bash cd ~/repos/buildroot-beacon make -C rpi-buildroot-fork O=$(pwd)/output BR2_EXTERNAL=$(pwd)/beacon-buildroot -j$(nproc) -# outputs: output/images/rootfs.raucb update.raucb sdcard.img.xz ``` -## Flash (initial, EMMC_DISABLE jumper bridged) +## Flash (secure-boot CM4) +Bridge EMMC_DISABLE jumper + connect USB, then: ```bash -./beacon-buildroot/scripts/flash-cm4.sh # auto-detect -./beacon-buildroot/scripts/flash-cm4.sh /dev/sda # explicit device -``` - -## SSH / find IP -```bash -# Non-secure-boot CM4 (MAC e4:5f:01:e9:13:96): -CM4=$(ip neigh show dev enp0s31f6 | awk '/e4:5f:01:e9:13:96/{print $1}') -# Secure-boot CM4 (MAC 2c:cf:67:fd:93:1a): -CM4=$(ip neigh show dev enp0s31f6 | awk '/2c:cf:67:fd:93:1a/{print $1}') -sshpass -p beacon ssh user@$CM4 # login: user / beacon +bash beacon-buildroot/scripts/flash-cm4-sb.sh # auto-detect +bash beacon-buildroot/scripts/flash-cm4-sb.sh /dev/sda # explicit ``` +Script auto-signs `usbboot/secure-boot-msd/boot.img` with `private.pem` if needed. ## OTA Update ```bash -# transfer (scp broken on Dropbear — use tee pipe): +# Transfer bundle sshpass -p beacon ssh user@$CM4 'sudo tee /upload/rootfs.raucb >/dev/null' \ < output/images/rootfs.raucb -# install + reboot: -sshpass -p beacon ssh -tt user@$CM4 'rauc install /upload/rootfs.raucb && sudo reboot' -# after reboot — find new IP, then mark-good (REQUIRED on every new boot to confirm slot): -sshpass -p beacon ssh -tt user@$CM4 'rauc status mark-good && rauc status' -# NOTE: rauc commands need -tt (PTY) on Dropbear SSH or output is silently dropped -# NOTE: mark-good MUST be called after each OTA reboot — without it RAUC falls back to previous slot +# Install + reboot +sshpass -p beacon ssh -tt user@$CM4 'sudo rauc install /upload/rootfs.raucb && sudo reboot' +# After reboot: find new IP, then MUST mark-good or slot rolls back +sshpass -p beacon ssh -tt user@$CM4 'sudo rauc status mark-good && rauc status' ``` -## UART -```bash -picocom -b 115200 /dev/ttyUSB1 # interactive (GPIO14/15) -socat -u /dev/ttyUSB1,b115200,rawer,crnl OPEN:/tmp/uart.log,creat,trunc & # headless capture -``` - -## Rescue -Short GPIO4 (pin 7) → GND (pin 9) during power-on → boots `/dev/mmcblk0p2`. - -## Secure Boot — Unlock as MSD -```bash -# Sign the MSD boot image with private.pem (once, or after rpi-eeprom submodule init): -cd usbboot/secure-boot-msd -../tools/rpi-eeprom-digest -i boot.img -o boot.sig -k ../../private.pem -# Expose eMMC as USB mass storage (user must bridge EMMC_DISABLE jumper first): -sudo ./usbboot/rpiboot -d usbboot/secure-boot-msd -# Flash: -sudo bmaptool copy output/images/sdcard.img.xz /dev/sda -``` - -## UART / Power Cycle -> **The user handles power cycling and UART logging manually.** -> Ask user to: remove EMMC_DISABLE jumper → power-cycle → connect picocom. -```bash -picocom -b 115200 /dev/ttyUSB1 # user runs this to see boot log -``` -Expected secure boot log lines: `secure-boot`, `rsa-verify pass (0x0)`, then U-Boot. - -## Secure Boot — Provision (burn OTP) -```bash -update-pieeprom.sh -k private.pem && rpiboot -d secure-boot-recovery -``` -> Use existing `private.pem` — never regenerate it. - -## Disk Encryption (/data partition) ✅ COMPLETE -`/dev/mmcblk0p3` is LUKS2-encrypted using the device-unique 256-bit OTP private key. -Key is read from OTP at every boot via `/dev/vcio` (VideoCore mailbox). -No key ever touches disk — tmpfs only. - -**Self-healing:** If service finds a LUKS header that can't be opened (e.g. stale header -surviving a sparse bmaptool flash), it wipes the first 4 MB and re-formats automatically. - -**Note:** `lsblk` is not installed on target — use `cryptsetup status` instead. - -```bash -# Check /data is mounted + encrypted: -sshpass -p beacon ssh user@$CM4 'sudo cryptsetup status data' -# Expected: type: LUKS2, cipher: aes-xts-plain64, keysize: 512 bits, mode: read/write - -# Read OTP key (hex, stable across reboots) — requires root: -sshpass -p beacon ssh user@$CM4 'sudo beacon-otp-key' - -# Verify LUKS2 header + keyslots: -sshpass -p beacon ssh user@$CM4 'sudo cryptsetup luksDump /dev/mmcblk0p3' -``` - -**OTA update behavior:** RAUC only writes to rootfs.0/1 — data partition is untouched. -On reboot into new slot, service opens LUKS with same OTP key → same data accessible. -Verified: A→B and B→A OTA both maintain encrypted /data correctly. - -**Security model:** OTP key is protected by secure boot (only signed boot.img runs). -Root processes within the signed OS can still read OTP via `/dev/vcio` (RPi hardware limitation). \ No newline at end of file +## Key gotchas +- `lsblk` not on target — use `cryptsetup status data` to check `/data` +- `rauc` needs `-tt` on Dropbear or output is silently dropped +- `mark-good` is required after every OTA reboot +- `/data` is LUKS2 (AES-XTS-256, OTP key) — untouched by RAUC, self-heals on bad header +- `private.pem` at repo root — **never regenerate** it \ No newline at end of file diff --git a/README.md b/README.md index 3465f59..d1a9055 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ # Beacon CM4 — Buildroot + RAUC -Buildroot BR2_EXTERNAL for a Raspberry Pi CM4 with A/B OTA updates via [RAUC](https://rauc.io/). +Buildroot BR2_EXTERNAL for Raspberry Pi CM4 with: +- **RAUC A/B OTA updates** (rootfs.0 / rootfs.1) +- **Secure boot** (RPi EEPROM OTP, signed `boot.img`) +- **Disk encryption** (`/data` on LUKS2, key derived from device OTP) ## Partition layout -| # | Label | Size | Content | -|---|-------|------|---------| -| p1 | boot_a | 64 MiB | FAT32 — firmware + U-Boot (slot A) | -| p2 | boot_b | 64 MiB | FAT32 — firmware + U-Boot (slot B) | -| p3 | data | 256 MiB | ext4 — persistent data (`/data`) | -| p4 | (extended) | — | — | -| p5 | rootfs0 | 250 MiB | ext4 — rootfs slot A | -| p6 | rootfs1 | 250 MiB | ext4 — rootfs slot B | -| p7 | upload | 256 MiB | ext4 — staging area for bundles (`/upload`) | - -U-Boot reads `BOOT_ORDER`/`BOOT_x_LEFT` env vars from eMMC and selects the active slot before loading the kernel. RAUC uses `boot-mbr-switch` to toggle between slots. +| Dev | Size | Mount | Content | +|-----|------|-------|---------| +| p1 | 256 MiB | — | FAT32 outer boot (firmware, boot.img, boot.sig, boot.scr) | +| p2 | 64 MiB | — | Rescue rootfs (ext4) | +| p3 | 128 MiB | `/data` | LUKS2 encrypted persistent data | +| p5 | 900 MiB | `/` | Rootfs slot A (ext4) | +| p6 | 900 MiB | — | Rootfs slot B (ext4) | +| p7 | 900 MiB | `/upload` | OTA bundle staging (ext4) | --- @@ -22,241 +22,119 @@ U-Boot reads `BOOT_ORDER`/`BOOT_x_LEFT` env vars from eMMC and selects the activ ### 1. Generate RAUC signing certificates -Run **once** from the `beacon-buildroot/` directory: - ```bash -cd ~/repos/buildroot-beacon/beacon-buildroot +cd beacon-buildroot ./openssl-ca.sh "Beacon" "Beacon RAUC CA" ``` -This creates `openssl-ca/dev/` with: +Creates `openssl-ca/dev/`: `ca.cert.pem` (installed on device as keyring), `development-1.cert.pem` + `private/development-1.key.pem` (signing, host-only). -``` -openssl-ca/dev/ - ca.cert.pem ← keyring installed into target /etc/rauc/keyring.pem - development-1.cert.pem ← signing cert (build host) - private/ - development-1.key.pem ← signing key (build host, keep secret) -``` +> **Never regenerate** the CA after devices are flashed — new CA = rejected bundles. -> **Do not regenerate** the CA once devices are flashed — bundles signed with -> a new CA will be rejected by devices that have the old keyring. - -### 2. Initial full build +### 2. Build ```bash cd ~/repos/buildroot-beacon -make -C rpi-buildroot-fork \ - O=$(pwd)/output \ - BR2_EXTERNAL=$(pwd)/beacon-buildroot \ - beacon_cm4_rauc_defconfig +# First time: load defconfig +make -C rpi-buildroot-fork O=$(pwd)/output BR2_EXTERNAL=$(pwd)/beacon-buildroot beacon_cm4_rauc_defconfig -make -C rpi-buildroot-fork \ - O=$(pwd)/output \ - BR2_EXTERNAL=$(pwd)/beacon-buildroot \ - -j$(nproc) +# Build (incremental on subsequent runs) +make -C rpi-buildroot-fork O=$(pwd)/output BR2_EXTERNAL=$(pwd)/beacon-buildroot -j$(nproc) ``` -Output artifacts in `output/images/`: - -| File | Purpose | -|------|---------| -| `sdcard.img.xz` | Full eMMC image for initial flash | -| `sdcard.img.bmap` | Block map for fast flash with bmaptool | -| `rootfs.raucb` | **OTA bundle** — rootfs only | -| `update.raucb` | OTA bundle — bootfs + rootfs (full system) | - -### 3. Initial flash (EMMC_DISABLE jumper bridged) - -```bash -./beacon-buildroot/scripts/flash-cm4.sh -``` - -Remove the jumper and power-cycle after the script completes. +Outputs in `output/images/`: +- `sdcard.img.xz` + `.bmap` — full eMMC image for initial flash +- `rootfs.raucb` — OTA bundle (rootfs only) +- `update.raucb` — OTA bundle (bootfs + rootfs) --- -## Creating an OTA update +## Initial flash -### What triggers a new bundle - -Any source change that results in a different `rootfs.ext4` or `boot.vfat` will produce a new bundle on the next build. Typical triggers: - -- Package version bump / new package in defconfig -- File added/changed under `rootfs-overlay/` -- `post-build.sh` changes -- Kernel or U-Boot update - -### Build the update - -Incremental build — only changed packages and the rootfs/image stage are rebuilt: +The CM4 uses **secure boot** — the eMMC can only be exposed as MSD using a `boot.img` signed with `private.pem`. +1. Bridge the **EMMC_DISABLE** jumper and connect USB +2. Run: ```bash -cd ~/repos/buildroot-beacon - -# Optional: set a human-readable version string -export VERSION="1.2.0" - -make -C rpi-buildroot-fork \ - O=$(pwd)/output \ - BR2_EXTERNAL=$(pwd)/beacon-buildroot \ - -j$(nproc) +bash beacon-buildroot/scripts/flash-cm4-sb.sh # auto-detect +bash beacon-buildroot/scripts/flash-cm4-sb.sh /dev/sda # explicit ``` +The script auto-signs `usbboot/secure-boot-msd/boot.img` if needed, exposes eMMC via `rpiboot`, then flashes with `bmaptool`. -`post-image.sh` runs automatically at the end and: -1. Builds `boot.vfat` from U-Boot + firmware blobs -2. Creates `rootfs.raucb` (rootfs-only bundle) -3. Creates `update.raucb` (full bootfs+rootfs bundle) -4. Signs both bundles with `development-1.key.pem` -5. Assembles `sdcard.img.xz` + `.bmap` +3. Remove jumper → power-cycle -### Bundle contents +--- -`rootfs.raucb` manifest (`format=verity`): -```ini -[update] -compatible=beacon-cm4 -version= -[bundle] -format=verity -[image.rootfs] -filename=rootfs.ext4 -``` +## OTA update -`update.raucb` additionally contains `[image.bootloader]` → `boot.vfat`. +### Build the bundle -### Signing details - -Signing is done by the host `rauc` binary during `post-image.sh`: +Any change to packages, `rootfs-overlay/`, `post-build.sh`, or kernel triggers a new bundle on the next incremental build (same build command as above). +To inspect a bundle: ```bash -rauc bundle \ - --cert openssl-ca/dev/development-1.cert.pem \ - --key openssl-ca/dev/private/development-1.key.pem \ - --keyring openssl-ca/dev/ca.cert.pem \ - / .raucb -``` - -The target verifies the bundle signature against `/etc/rauc/keyring.pem` -(= `ca.cert.pem` installed during build by `post-build.sh`). - -To inspect a bundle without installing it: -```bash -output/host/bin/rauc \ - --keyring beacon-buildroot/openssl-ca/dev/ca.cert.pem \ +output/host/bin/rauc --keyring beacon-buildroot/openssl-ca/dev/ca.cert.pem \ info output/images/rootfs.raucb ``` ---- - -## Deploying the update to the CM4 - -### Find the CM4's IP - -The CM4 gets a DHCP address on eth0 (changes on each reboot): +### Find the CM4 ```bash -# By MAC address: -ip neigh show dev enp0s31f6 | grep e4:5f:01:e9:13:96 +# Secure-boot CM4 (MAC d8:3a:dd:a8:9a:40): +CM4=$(ip neigh show dev enp0s31f6 | awk '/d8:3a:dd:a8:9a:40/{print $1; exit}') ``` -### Transfer the bundle +### Transfer, install, reboot -Dropbear has no sftp-server — standard `scp` does **not** work. -Use stdin pipe instead: +`scp` does **not** work on Dropbear — use stdin pipe. `rauc` needs `-tt`. ```bash -CM4=10.11.0.xx # replace with actual IP - -sshpass -p beacon ssh user@$CM4 \ - 'sudo tee /upload/rootfs.raucb > /dev/null' \ +# 1. Transfer +sshpass -p beacon ssh user@$CM4 'sudo tee /upload/rootfs.raucb >/dev/null' \ < output/images/rootfs.raucb + +# 2. Install + reboot +sshpass -p beacon ssh -tt user@$CM4 'sudo rauc install /upload/rootfs.raucb && sudo reboot' ``` -~51 MiB transfers in ~5 s on LAN. +### Mark-good after reboot (required) -### Install +After reboot the new slot is in **trial mode** — it will roll back after 3 boots without `mark-good`. ```bash -sshpass -p beacon ssh user@$CM4 'rauc install /upload/rootfs.raucb' -``` +# Find new IP (DHCP changes on reboot): +CM4=$(ip neigh show dev enp0s31f6 | awk '/d8:3a:dd:a8:9a:40/{print $1; exit}') -Expected output: -``` - 0% Installing - 20% Checking bundle done. - 40% Determining target install group done. - 46% Checking slot rootfs.1 done. - 99% Copying image to rootfs.1 done. -100% Installing done. -Installing `/upload/rootfs.raucb` succeeded -``` - -RAUC automatically selects the **inactive** slot as the target. - -### Reboot into the new slot - -```bash -sshpass -p beacon ssh user@$CM4 'rauc status && sudo reboot' -``` - -`rauc status` should show `Activated: rootfs.1 (B)` before the reboot. - -### Confirm and mark-good - -After reboot the system boots into the new slot in **trial mode** -(U-Boot decrements `BOOT_x_LEFT`). You must mark it good or it will -roll back on the next reboot. - -```bash -# Find new IP (DHCP address changes): -CM4_NEW=$(ip neigh show dev enp0s31f6 | awk '/e4:5f:01:e9:13:96/{print $1}') - -sshpass -p beacon ssh user@$CM4_NEW 'rauc status mark-good && rauc status' -``` - -Expected final `rauc status`: -``` -Booted from: rootfs.1 (B) -Activated: rootfs.1 (B) -x [rootfs.1] boot status: good ← currently running, committed -o [rootfs.0] boot status: good ← fallback +sshpass -p beacon ssh -tt user@$CM4 'sudo rauc status mark-good && rauc status' ``` --- -## Rollback behaviour +## Disk encryption -If `mark-good` is **not** called after a reboot, U-Boot decrements -`BOOT_x_LEFT`. After 3 failed attempts it switches back to the previous -slot automatically — no manual intervention needed. +`/data` (`/dev/mmcblk0p3`) is LUKS2-encrypted at rest. The key is derived from the device-unique OTP private key via `/dev/vcio` (VideoCore mailbox) — never stored on disk. -To force an immediate rollback: +- **First boot / stale header**: the service automatically wipes and reformats the partition. +- **OTA updates**: RAUC only writes to rootfs slots — `/data` is untouched and re-opens with the same OTP key. + +Verify encryption on device: ```bash -# On the CM4: -sudo fw_setenv BOOT_ORDER "A B" # or "B A" depending on current slot -sudo reboot +sshpass -p beacon ssh user@$CM4 'sudo cryptsetup status data' +# type: LUKS2, cipher: aes-xts-plain64, keysize: 512 bits ``` --- -## Rescue partition - -Short **GPIO4** (pin 7) to **GND** (pin 9) on the 40-pin header during -power-on. U-Boot detects this and boots the read-only rescue rootfs from -`/dev/mmcblk0p2`. - ---- - ## Reference | Path | Purpose | |------|---------| | `configs/beacon_cm4_rauc_defconfig` | Buildroot defconfig | | `board/beacon-cm4/genimage.cfg` | Partition layout | -| `board/beacon-cm4/post-image.sh` | Bundle creation + signing | +| `board/beacon-cm4/post-image.sh` | Bundle signing + image assembly | | `board/beacon-cm4/post-build.sh` | Target rootfs customisation | | `board/beacon-cm4/rootfs-overlay/etc/rauc/system.conf` | RAUC slot config | -| `openssl-ca/dev/` | Signing certificates (generated once) | -| `scripts/flash-cm4.sh` | Automated initial flash script | +| `openssl-ca/dev/` | RAUC signing certificates | +| `scripts/flash-cm4-sb.sh` | Initial flash (secure-boot CM4) | +| `package/beacon-otp/` | OTP key reader (`beacon-otp-key`) |