Compare commits

...

2 Commits

Author SHA1 Message Date
cb6f94770a update readme 2026-03-05 13:56:30 +01:00
8e3cafbd5b add encryption - sb and encryption now working with rauc ota updates 2026-03-05 13:56:18 +01:00
14 changed files with 432 additions and 328 deletions

View File

@@ -1,72 +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.
## 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

View File

@@ -1,2 +1 @@
# Nothing to see here (yet)
#source "$BR2_EXTERNAL_BEACON_PATH/package/blah/Config.in"
source "$BR2_EXTERNAL_BEACON_PATH/package/beacon-otp/Config.in"

252
README.md
View File

@@ -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=<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 \
<bundle-dir>/ <output>.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`) |

View File

@@ -1,14 +1,3 @@
image data.ext4 {
name = "Data"
mountpoint = /data
ext4 {
use-mke2fs = true
label = "Data"
features = "^64bit"
}
size = 128M
}
image upload.ext4 {
name = "Upload"
empty = true
@@ -61,7 +50,6 @@ image sdcard.img {
partition data {
partition-type = 0x83
image = "data.ext4"
size = 128M
}

View File

@@ -4,6 +4,9 @@ CONFIG_BLK_DEV_LOOP=y
CONFIG_DM_VERITY=y
CONFIG_SQUASHFS=y
CONFIG_CRYPTO_SHA256=y
CONFIG_CRYPTO_SHA512=y
CONFIG_DM_CRYPT=y
CONFIG_CRYPTO_AES=y
CONFIG_CRYPTO_XTS=y
CONFIG_CRYPTO_USER_API_HASH=y
CONFIG_CRYPTO_USER_API_SKCIPHER=y

View File

@@ -24,17 +24,23 @@ fi
# Mount persistent data partitions
# /data is handled by beacon-encrypt-data.service (LUKS2 encrypted, key from OTP)
if [ -e ${TARGET_DIR}/etc/fstab ]; then
# For configuration data
# WARNING: data=journal is safest, but potentially slow!
grep -qE 'LABEL=Data' ${TARGET_DIR}/etc/fstab || \
echo "LABEL=Data /data ext4 defaults,data=journal,noatime 0 0" >> ${TARGET_DIR}/etc/fstab
# For bulk data (eg: firmware updates)
# Remove any stale LABEL=Data entry left from previous builds
sed -i '/LABEL=Data/d' ${TARGET_DIR}/etc/fstab
# For bulk data (eg: firmware updates) — unencrypted
grep -qE 'LABEL=Upload' ${TARGET_DIR}/etc/fstab || \
echo "LABEL=Upload /upload ext4 defaults,noatime 0 0" >> ${TARGET_DIR}/etc/fstab
fi
# Enable beacon-encrypt-data.service (runs before local-fs.target to mount /data)
mkdir -p "${TARGET_DIR}/etc/systemd/system/local-fs.target.wants"
ln -sf ../beacon-encrypt-data.service \
"${TARGET_DIR}/etc/systemd/system/local-fs.target.wants/beacon-encrypt-data.service"
# Ensure the service script is executable
chmod 0755 "${TARGET_DIR}/usr/sbin/beacon-encrypt-data.sh" 2>/dev/null || true
# Copy custom cmdline.txt file
install -D -m 0644 ${BR2_EXTERNAL_BEACON_PATH}/board/beacon-cm4/cmdline.txt ${BINARIES_DIR}/custom/cmdline.txt

View File

@@ -0,0 +1,18 @@
[Unit]
Description=LUKS2 encrypted data partition setup
Documentation=man:cryptsetup(8)
DefaultDependencies=no
Conflicts=umount.target
After=systemd-udevd.service
Before=local-fs.target umount.target
# Wait for the block device to appear
After=dev-mmcblk0p3.device
Wants=dev-mmcblk0p3.device
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/sbin/beacon-encrypt-data.sh
[Install]
WantedBy=local-fs.target

View File

@@ -0,0 +1,98 @@
#!/bin/sh
#
# beacon-encrypt-data.sh
#
# On first boot: LUKS2-format /dev/mmcblk0p3 with device OTP key, create ext4, mount /data
# On later boots: open LUKS2 container with device OTP key, mount /data
#
# If the OTP key is all-zeros (not programmed) the partition is mounted unencrypted
# so the system is still usable on a non-secure-boot device during development.
set -e
DATA_DEV="/dev/mmcblk0p3"
MAPPER_NAME="data"
MAPPER_DEV="/dev/mapper/${MAPPER_NAME}"
MOUNT_POINT="/data"
OTP_TOOL="/usr/sbin/beacon-otp-key"
log() { echo "beacon-encrypt-data: $*"; }
die() { echo "beacon-encrypt-data: ERROR: $*" >&2; exit 1; }
# Block device must exist
[ -b "${DATA_DEV}" ] || { log "${DATA_DEV} not found, skipping"; exit 0; }
# OTP tool must exist
[ -x "${OTP_TOOL}" ] || die "OTP tool not found at ${OTP_TOOL}"
# Write OTP key into a tmpfs file so it never touches disk
KEY_FILE="$(mktemp /dev/shm/otp-XXXXXX 2>/dev/null || mktemp /tmp/otp-XXXXXX)"
trap 'rm -f "${KEY_FILE}"' EXIT INT TERM
OTP_READ_OK=1
if ! ${OTP_TOOL} -b > "${KEY_FILE}"; then
log "WARNING: OTP key read failed — falling back to unencrypted /data"
OTP_READ_OK=0
fi
# Check if key is all zeros (OTP not programmed — dev/test device)
KEY_HEX=""
if [ "${OTP_READ_OK}" = "1" ]; then
KEY_HEX="$(${OTP_TOOL} 2>/dev/null || true)"
fi
mount_unencrypted() {
if ! blkid "${DATA_DEV}" >/dev/null 2>&1; then
log "No filesystem on ${DATA_DEV} — creating ext4 (unencrypted)"
mkfs.ext4 -q -L "Data" "${DATA_DEV}" || die "mkfs.ext4 failed"
fi
mount -t ext4 -o defaults,noatime "${DATA_DEV}" "${MOUNT_POINT}" \
|| die "mount ${MOUNT_POINT} failed"
log "${MOUNT_POINT} is ready (unencrypted)"
exit 0
}
if [ "${OTP_READ_OK}" = "0" ] || [ -z "$(echo "${KEY_HEX}" | tr -d '0')" ]; then
log "OTP key is all-zeros or unreadable — mounting ${DATA_DEV} unencrypted"
mount_unencrypted
fi
# --- Encrypted path ---
luks_format() {
log "Formatting /dev/mmcblk0p3 with LUKS2"
dd if=/dev/zero of="${DATA_DEV}" bs=1M count=4 status=none 2>/dev/null || true
cryptsetup luksFormat \
--batch-mode \
--type luks2 \
--key-file "${KEY_FILE}" \
--key-size 512 \
--cipher aes-xts-plain64 \
--hash sha256 \
--pbkdf pbkdf2 \
"${DATA_DEV}" \
|| die "luksFormat failed"
cryptsetup luksOpen "${DATA_DEV}" "${MAPPER_NAME}" \
--key-file "${KEY_FILE}" \
|| die "luksOpen after format failed"
log "Creating ext4 filesystem inside encrypted container"
mkfs.ext4 -q -L "DataEnc" "${MAPPER_DEV}" \
|| die "mkfs.ext4 failed"
}
if cryptsetup isLuks "${DATA_DEV}" 2>/dev/null; then
log "Opening existing LUKS2 container on ${DATA_DEV}"
if ! cryptsetup luksOpen "${DATA_DEV}" "${MAPPER_NAME}" \
--key-file "${KEY_FILE}" 2>/dev/null; then
log "WARNING: luksOpen failed (stale header or wrong key) — re-formatting"
luks_format
fi
else
log "No LUKS header on ${DATA_DEV} — formatting with LUKS2 (first boot)"
luks_format
fi
log "Mounting ${MAPPER_DEV} at ${MOUNT_POINT}"
mount -t ext4 -o defaults,noatime "${MAPPER_DEV}" "${MOUNT_POINT}" \
|| die "mount ${MOUNT_POINT} failed"
log "${MOUNT_POINT} is ready (encrypted)"

View File

@@ -40,6 +40,8 @@ BR2_PACKAGE_RAUC_NETWORK=y
BR2_PACKAGE_RAUC_JSON=y
BR2_PACKAGE_DROPBEAR=y
BR2_PACKAGE_CRYPTSETUP=y
BR2_PACKAGE_E2FSPROGS=y
BR2_PACKAGE_BEACON_OTP=y
BR2_PACKAGE_UTIL_LINUX_WDCTL=y
BR2_TARGET_ROOTFS_EXT2=y
BR2_TARGET_ROOTFS_EXT2_4=y

View File

@@ -0,0 +1,6 @@
config BR2_PACKAGE_BEACON_OTP
bool "beacon-otp"
help
Reads the device-specific 256-bit private key from RPi OTP
via the VideoCore mailbox (/dev/vcio). Used by the
beacon-encrypt-data service to unlock the LUKS2 data partition.

View File

@@ -0,0 +1,22 @@
################################################################################
#
# beacon-otp
#
################################################################################
BEACON_OTP_VERSION = local
BEACON_OTP_SITE = $(BR2_EXTERNAL_BEACON_PATH)/package/beacon-otp/src
BEACON_OTP_SITE_METHOD = local
BEACON_OTP_LICENSE = MIT
define BEACON_OTP_BUILD_CMDS
$(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_LDFLAGS) \
-o $(@D)/beacon-otp-key $(@D)/beacon-otp-key.c
endef
define BEACON_OTP_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0750 $(@D)/beacon-otp-key \
$(TARGET_DIR)/usr/sbin/beacon-otp-key
endef
$(eval $(generic-package))

View File

@@ -0,0 +1,84 @@
/* beacon-otp-key.c
* Read the device-specific private key from RPi OTP via the VideoCore mailbox.
* Usage: beacon-otp-key [-b]
* (no args) print 64-char hex string + newline
* -b write 32 raw bytes to stdout (for use as a key-file)
*/
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <sys/ioctl.h>
#include <unistd.h>
#define IOCTL_MBOX_PROPERTY _IOWR(100, 0, char *)
#define TAG_GET_PRIVATE_KEY 0x00030081u
#define KEY_WORDS 8 /* 8 x 32-bit = 256-bit key */
int main(int argc, char *argv[])
{
int binary = (argc > 1 && strcmp(argv[1], "-b") == 0);
/*
* Mailbox property buffer layout (uint32_t words):
* [0] total message size in bytes
* [1] process-request code (0)
* [2] tag id
* [3] value-buffer size in bytes = (2 + KEY_WORDS) * 4
* [4] request/response indicator (0 = request)
* [5] offset into OTP keystore (0)
* [6] number of words to read
* [7 .. 6+KEY_WORDS] key data (output)
* [7+KEY_WORDS] end tag (0)
*/
uint32_t buf[7 + KEY_WORDS + 1];
memset(buf, 0, sizeof(buf));
buf[0] = (uint32_t)sizeof(buf);
buf[1] = 0x00000000;
buf[2] = TAG_GET_PRIVATE_KEY;
buf[3] = (2 + KEY_WORDS) * 4;
buf[4] = 0;
buf[5] = 0;
buf[6] = KEY_WORDS;
buf[7 + KEY_WORDS] = 0;
int fd = open("/dev/vcio", O_RDWR);
if (fd < 0) {
perror("beacon-otp-key: open /dev/vcio");
return 1;
}
if (ioctl(fd, IOCTL_MBOX_PROPERTY, buf) < 0) {
perror("beacon-otp-key: ioctl MBOX_PROPERTY");
close(fd);
return 1;
}
close(fd);
if (buf[1] != 0x80000000u) {
fprintf(stderr, "beacon-otp-key: mailbox error 0x%08x\n", buf[1]);
return 1;
}
for (int i = 0; i < KEY_WORDS; i++) {
uint32_t w = buf[7 + i];
if (binary) {
uint8_t b[4] = {
(uint8_t)(w & 0xff),
(uint8_t)((w >> 8) & 0xff),
(uint8_t)((w >>16) & 0xff),
(uint8_t)((w >>24) & 0xff)
};
if (fwrite(b, 1, 4, stdout) != 4) {
perror("beacon-otp-key: fwrite");
return 1;
}
} else {
printf("%08x", w);
}
}
if (!binary)
printf("\n");
fflush(stdout);
return 0;
}

96
scripts/flash-cm4-sb.sh Executable file
View File

@@ -0,0 +1,96 @@
#!/bin/bash
# flash-cm4.sh - Flash CM4 eMMC while EMMC_DISABLE jumper is bridged
# Usage: ./scripts/flash-cm4.sh [/dev/sdX]
# If no device given, auto-detects the CM4 USB mass storage device.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BEACON_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd "$BEACON_DIR/.." && pwd)"
USBBOOT_DIR="$REPO_ROOT/usbboot"
MSD_DIR="$USBBOOT_DIR/secure-boot-msd"
IMAGE="$REPO_ROOT/output/images/sdcard.img.xz"
PRIVATE_KEY="$REPO_ROOT/private.pem"
# Build rpiboot from source if not already compiled
if [ ! -x "$USBBOOT_DIR/rpiboot" ]; then
echo "==> Building rpiboot from source..."
make -C "$USBBOOT_DIR"
fi
# Ensure secure-boot-msd boot.img is signed (required for secure-boot-locked CM4)
if [ ! -f "$MSD_DIR/boot.sig" ] || [ "$MSD_DIR/boot.img" -nt "$MSD_DIR/boot.sig" ]; then
echo "==> Signing secure-boot-msd/boot.img with private.pem..."
"$USBBOOT_DIR/tools/rpi-eeprom-digest" -i "$MSD_DIR/boot.img" \
-o "$MSD_DIR/boot.sig" -k "$PRIVATE_KEY"
fi
find_removable_sd() {
lsblk -dno NAME,RM | awk '$2==1{print $1}' | grep '^sd' | head -1
}
# Step 1: Expose CM4 eMMC as USB mass storage (skip if device already present)
if [ -n "${1:-}" ] && [ -b "${1}" ]; then
echo "==> $1 already present — skipping rpiboot."
elif [ -z "${1:-}" ] && [ -n "$(find_removable_sd)" ]; then
echo "==> Removable block device already present — skipping rpiboot."
else
# NOTE: mass-storage-gadget64 is rejected by secure-boot-locked CM4s.
# Use secure-boot-msd (signed boot.img) instead.
echo "==> Running rpiboot to expose CM4 eMMC (EMMC_DISABLE jumper must be bridged)..."
sudo "$USBBOOT_DIR/rpiboot" -d "$MSD_DIR"
echo "==> rpiboot done."
fi
# Step 2: Find the device (explicit arg or auto-detect)
if [ -n "${1:-}" ]; then
DEVICE="$1"
echo "==> Using device: $DEVICE — waiting for it to appear..."
for i in $(seq 1 30); do
[ -b "$DEVICE" ] && break
sleep 1
printf " waiting for %s... (%ds)\r" "$DEVICE" "$i"
done
if [ ! -b "$DEVICE" ]; then
echo "ERROR: $DEVICE did not appear as a block device within 30s."
exit 1
fi
else
DEVICE=""
echo "==> Waiting for removable block device..."
for i in $(seq 1 30); do
DEV=$(find_removable_sd)
if [ -n "$DEV" ]; then
DEVICE="/dev/$DEV"
break
fi
sleep 1
printf " waiting... (%ds)\r" "$i"
done
if [ -z "$DEVICE" ]; then
echo "ERROR: No removable block device found within 30s."
echo " Run 'lsblk' to find it, then re-run: $0 /dev/sdX"
exit 1
fi
echo "==> Auto-detected CM4 eMMC at $DEVICE"
fi
# Step 3: Safety check - refuse to flash the host NVMe disk
if echo "$DEVICE" | grep -qE '^/dev/nvme'; then
echo "ERROR: $DEVICE looks like the host NVMe. Aborting."
exit 1
fi
# Step 4: Unmount any auto-mounted partitions
echo "==> Unmounting $DEVICE partitions..."
sudo umount "${DEVICE}"?* 2>/dev/null || true
sudo umount "${DEVICE}"[0-9]* 2>/dev/null || true
# Step 5: Flash via bmaptool
echo "==> Flashing $IMAGE -> $DEVICE ..."
sudo bmaptool copy "$IMAGE" "$DEVICE"
sudo sync
echo ""
echo "==> Flash complete!"
echo " Remove the EMMC_DISABLE jumper, then power-cycle the CM4."

View File

@@ -1,67 +0,0 @@
#!/bin/bash
# flash-cm4.sh - Flash CM4 eMMC while EMMC_DISABLE jumper is bridged
# Usage: ./scripts/flash-cm4.sh [/dev/sdX]
# If no device given, auto-detects the CM4 USB mass storage device.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BEACON_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd "$BEACON_DIR/.." && pwd)"
USBBOOT_DIR="$REPO_ROOT/usbboot"
IMAGE="$REPO_ROOT/output/images/sdcard.img.xz"
# Build rpiboot from source if not already compiled
if [ ! -x "$USBBOOT_DIR/rpiboot" ]; then
echo "==> Building rpiboot from source..."
make -C "$USBBOOT_DIR"
fi
# Step 1: Expose CM4 eMMC as USB mass storage
echo "==> Running rpiboot to expose CM4 eMMC (EMMC_DISABLE jumper must be bridged)..."
sudo "$USBBOOT_DIR/rpiboot" -d "$USBBOOT_DIR/mass-storage-gadget64"
echo "==> rpiboot done, waiting for block device..."
# Step 2: Find the device (explicit arg or auto-detect USB disk ~8 GiB)
if [ -n "${1:-}" ]; then
DEVICE="$1"
echo "==> Using specified device: $DEVICE"
else
DEVICE=""
for i in $(seq 1 30); do
sleep 1
# Detect USB block device of 7-8 GiB (CM4 eMMC)
DEVICE=$(lsblk -dno NAME,TRAN,SIZE \
| awk '$2=="usb" && ($3~/^7\.[0-9]+G$/ || $3~/^8\.[0-9]+G$/) {print "/dev/"$1}' \
| head -1)
[ -n "$DEVICE" ] && break
printf " waiting... (%ds)\r" "$i"
done
if [ -z "$DEVICE" ]; then
echo "ERROR: CM4 eMMC did not appear as a USB block device within 30s."
echo " Run 'lsblk' manually and re-run with explicit device: $0 /dev/sdX"
exit 1
fi
echo "==> Auto-detected CM4 eMMC at $DEVICE"
fi
# Step 3: Safety check - refuse to flash the host nvme/sata disk
if echo "$DEVICE" | grep -qE '^/dev/(nvme|sd[a-z]{2,}|sda$)'; then
lsblk -dno TRAN "$DEVICE" | grep -qx usb || {
echo "ERROR: $DEVICE does not appear to be a USB device. Aborting."
exit 1
}
fi
# Step 4: Unmount any auto-mounted partitions
echo "==> Unmounting $DEVICE partitions..."
sudo umount "${DEVICE}"?* 2>/dev/null || true
sudo umount "${DEVICE}"[0-9]* 2>/dev/null || true
# Step 5: Flash via bmaptool
echo "==> Flashing $IMAGE -> $DEVICE ..."
sudo bmaptool copy "$IMAGE" "$DEVICE"
sudo sync
echo ""
echo "==> Flash complete!"
echo " Remove the EMMC_DISABLE jumper, then power-cycle the CM4."