feat: signed USB recovery (Option A)

- keys/adacam-update-public.pem: RSA-4096 public key for bundle verification
  Private key: /boot/config/adacam/adacam-update-private.pem on Lucy
- services/updater/adacam-updater.sh: reference implementation of updater
- services/updater/99-adacam-usb.rules: udev rule (USB insertion trigger)
- scripts/sign-bundle.sh: create + sign a recovery bundle on Lucy
- scripts/example-bundle/install.sh: template recovery install script
- liberate.sh: Phase 5 now installs signed updater instead of just deleting
  - Hivemapper unsigned updater still removed
  - adacam-updater installed at /usr/local/bin/adacam-updater
  - verify key installed at /etc/adacam/update-verify.pem
  - udev rule installed for automatic USB trigger
  - removed duplicate usb-updater kill in boot persistence section
- keys/README.md: full key inventory, locations, usage
This commit is contained in:
Kayos 2026-03-14 14:49:56 -07:00
parent 48d648c5f2
commit e01748422c
7 changed files with 359 additions and 23 deletions

View file

@ -1,13 +1,16 @@
# keys/
## adacam_authorized_key.pub
All public keys. Private keys live on Lucy only, never in this repo.
SSH public keys authorized on every liberated AdaCam by default.
Two keys are included:
- `cobb@adacam` — primary key, private half at `/boot/config/adacam/id_ed25519_adacam` on Lucy
---
## SSH Access Keys — `adacam_authorized_key.pub`
Injected into every liberated AdaCam during liberation. Two keys:
- `cobb@adacam` — primary access key, private half at `/boot/config/adacam/id_ed25519_adacam` on Lucy
- `kayos@openclaw` — OpenClaw agent key, private half at `~/.openclaw/id_ed25519_unraid`
To SSH into a liberated AdaCam:
**How to SSH into a liberated AdaCam:**
```bash
# On adacam AP (always works):
ssh -i /boot/config/adacam/id_ed25519_adacam root@10.77.0.1
@ -16,26 +19,47 @@ ssh -i /boot/config/adacam/id_ed25519_adacam root@10.77.0.1
ssh -i /boot/config/adacam/id_ed25519_adacam root@<device-lan-ip>
```
Private key lives at: `root@192.168.0.5:/boot/config/adacam/id_ed25519_adacam`
**Private key location on Lucy:** `/boot/config/adacam/id_ed25519_adacam`
---
## Signing Keys (future)
## Update Signing Key — `adacam-update-public.pem`
The USB updater is currently disabled — no signing infrastructure needed.
RSA-4096 public key. Used by `adacam-updater` to verify signed recovery bundles before applying.
If OTA updates are ever added, generate a signing keypair:
**Private key location on Lucy:** `/boot/config/adacam/adacam-update-private.pem`
**To create a signed recovery bundle:**
```bash
openssl genrsa -out adacam-update-private.pem 4096
openssl rsa -in adacam-update-private.pem -pubout -out adacam-update-public.pem
# On Lucy
bash scripts/sign-bundle.sh ./recovery-output ./my-bundle-dir
# Produces: adacam-recovery.tar.gz + adacam-recovery.tar.gz.sig
# Copy both to USB drive: USB:/adacam_recovery/
```
- Private key: keep offline / on Lucy only. NEVER commit to repo.
- Public key: commit to `keys/adacam-update-public.pem`, install to `/etc/adacam/update-verify.pem` via liberate.sh
**Recovery flow:**
1. Create and sign a bundle (`scripts/sign-bundle.sh`)
2. Copy `.tar.gz` + `.sig` to USB drive under `adacam_recovery/`
3. Insert USB into powered-on AdaCam
4. `adacam-updater` fires via udev, verifies sig, runs `install.sh` inside bundle
5. Device reboots automatically
6. Check `/data/adacam/recovery.log` if something went wrong
**What a recovery bundle can do:**
- Reinstall services (`/opt/adacam/`)
- Restore config (`/data/adacam/config.json`)
- Replace SSH authorized_keys (locked-out recovery)
- Run arbitrary root commands via `install.sh`
**What it cannot do (by design):**
- Flash bootloader — `adacam-updater` does not call `movisoc-fwu`
- Full OS image replacement — use Mender for that (future)
---
## What's NOT here
## Key Locations Summary
- Private keys (never)
- Per-device keys (generated on device during liberation at `/data/adacam/`)
| Key | Type | Private | Public |
|-----|------|---------|--------|
| SSH access | ed25519 | Lucy: `/boot/config/adacam/id_ed25519_adacam` | `keys/adacam_authorized_key.pub``/root/.ssh/authorized_keys` |
| Update signing | RSA-4096 | Lucy: `/boot/config/adacam/adacam-update-private.pem` | `keys/adacam-update-public.pem``/etc/adacam/update-verify.pem` |

View file

@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6PXOsUf4VqjlR+8u3zKB
sqoEDtyRpQjOctqqPpGDMdqEvFGcikt1Z0McSOPtZZoOhhuUIvLdWBwtx91elZP9
aL3WpUd5xh6WXzUrejj8Evgir7TfacGbWSe61B/VwUq06z4DNCKbQj9geWy0T2gh
1nSTI68REx8nYbR3fB+snpjZoDhlqNew7slaCY96Aah/minvzmS9cWiXtkSHOQJr
rJRx2jjKC/E3CvI9d4ElRiXBA3oMmqai9oBjEs/TAlv74Weqa9fxOEOJN0HueGn0
uQ8JWVUROf88I37GTFRP7NYSY8OZDazDj0Jry0QQHmh/WlfKG78eRH/zQno3mssa
UMExR3bBFHQGsk7lmWVdTTUUOYkN2BCxDvifGTk9vBKb4x7mRlO4bf6HyDH0ueEQ
GrQTN2owUjeRt16hhwTWlVv/2/YChkTHDwnM43bQAEGpwA9hGcNmgUXy7F+m1szb
Ti9+/n1TlMcbOFqMouB5C16obUDPOnJ7m6rxVETTCFX7lyYmRwnl7XEgSqGXMX95
s8TRblYudK9GejD846Jt1feF7MGWllbPqTT4qsHxpAFv1tMXcfpiFsTZ4JL1jdez
qT2F8qV9WTdGCctZVc0ItbYLw/A6J+UXiAp5V+/vSpATby0vBcqmTN3rQ6OU490T
G/asHZqQuJjdxaE6iNGB7ucCAwEAAQ==
-----END PUBLIC KEY-----

View file

@ -225,13 +225,95 @@ chmod 600 /root/.ssh/authorized_keys
ok "SSH authorized_keys installed ($(wc -l < /root/.ssh/authorized_keys) keys)"
systemctl restart sshd
# ── PHASE 5: KILL USB UPDATER ────────────────────────────────────────────────
# ── PHASE 5: REPLACE USB UPDATER WITH SIGNED RECOVERY ───────────────────────
log ""
log "=== Phase 5: Disabling USB updater ==="
log "=== Phase 5: Installing secure USB recovery ==="
# Kill Hivemapper's unsigned updater
systemctl mask usb-updater 2>/dev/null || true
rm -f /usr/bin/usb-updater
ok "usb-updater disabled"
ok "Hivemapper usb-updater removed"
# Install verify key for signed bundles
mkdir -p /etc/adacam
cat > /etc/adacam/update-verify.pem << 'UPDATE_VERIFY_KEY'
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6PXOsUf4VqjlR+8u3zKB
sqoEDtyRpQjOctqqPpGDMdqEvFGcikt1Z0McSOPtZZoOhhuUIvLdWBwtx91elZP9
aL3WpUd5xh6WXzUrejj8Evgir7TfacGbWSe61B/VwUq06z4DNCKbQj9geWy0T2gh
1nSTI68REx8nYbR3fB+snpjZoDhlqNew7slaCY96Aah/minvzmS9cWiXtkSHOQJr
rJRx2jjKC/E3CvI9d4ElRiXBA3oMmqai9oBjEs/TAlv74Weqa9fxOEOJN0HueGn0
uQ8JWVUROf88I37GTFRP7NYSY8OZDazDj0Jry0QQHmh/WlfKG78eRH/zQno3mssa
UMExR3bBFHQGsk7lmWVdTTUUOYkN2BCxDvifGTk9vBKb4x7mRlO4bf6HyDH0ueEQ
GrQTN2owUjeRt16hhwTWlVv/2/YChkTHDwnM43bQAEGpwA9hGcNmgUXy7F+m1szb
Ti9+/n1TlMcbOFqMouB5C16obUDPOnJ7m6rxVETTCFX7lyYmRwnl7XEgSqGXMX95
s8TRblYudK9GejD846Jt1feF7MGWllbPqTT4qsHxpAFv1tMXcfpiFsTZ4JL1jdez
qT2F8qV9WTdGCctZVc0ItbYLw/A6J+UXiAp5V+/vSpATby0vBcqmTN3rQ6OU490T
G/asHZqQuJjdxaE6iNGB7ucCAwEAAQ==
-----END PUBLIC KEY-----
UPDATE_VERIFY_KEY
chmod 644 /etc/adacam/update-verify.pem
ok "update verify key installed at /etc/adacam/update-verify.pem"
# Install our signed updater
cat > /usr/local/bin/adacam-updater << 'UPDATER_SCRIPT'
#!/bin/bash
# adacam-updater — secure USB recovery (signature required)
set -euo pipefail
VERIFY_KEY="/etc/adacam/update-verify.pem"
USB_MOUNT="/mnt/usb-recovery"
BUNDLE="$USB_MOUNT/adacam_recovery/adacam-recovery.tar.gz"
SIG="$USB_MOUNT/adacam_recovery/adacam-recovery.tar.gz.sig"
WORK_DIR="/tmp/adacam-recovery-work"
LOG="/data/adacam/recovery.log"
MARKER="/data/adacam/recovery_in_progress"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
die() { log "ERROR: $*"; rm -f "$MARKER"; umount "$USB_MOUNT" 2>/dev/null; exit 1; }
[ -f "$VERIFY_KEY" ] || die "no verify key — aborting"
[ "$(id -u)" = "0" ] || die "must run as root"
[ -f "$MARKER" ] && die "recovery already in progress"
mkdir -p /data/adacam && touch "$MARKER"
log "=== AdaCam USB Recovery Starting ==="
USB_DEV="${1:-}"
[ -z "$USB_DEV" ] && USB_DEV=$(lsblk -rno NAME,TYPE | awk '$2=="part"{print "/dev/"$1}' | grep -v mmcblk | head -n1)
[ -n "$USB_DEV" ] || die "no USB device found"
log "USB: $USB_DEV"
mkdir -p "$USB_MOUNT"
mount -o ro "$USB_DEV" "$USB_MOUNT" || die "mount failed"
trap 'umount "$USB_MOUNT" 2>/dev/null; rm -rf "$WORK_DIR"; rm -f "$MARKER"' EXIT
[ -f "$BUNDLE" ] || die "no bundle at adacam_recovery/adacam-recovery.tar.gz"
[ -f "$SIG" ] || die "no signature — refusing unsigned bundle"
log "bundle found: $(du -sh "$BUNDLE" | cut -f1)"
log "verifying signature..."
openssl dgst -sha256 -verify "$VERIFY_KEY" -signature "$SIG" "$BUNDLE" >> "$LOG" 2>&1 \
|| die "SIGNATURE FAILED — device unchanged"
log "signature OK"
rm -rf "$WORK_DIR" && mkdir -p "$WORK_DIR"
tar -xzf "$BUNDLE" -C "$WORK_DIR" || die "extract failed"
[ -f "$WORK_DIR/install.sh" ] || die "no install.sh in bundle"
chmod +x "$WORK_DIR/install.sh"
log "running install.sh..."
bash "$WORK_DIR/install.sh" >> "$LOG" 2>&1 || die "install.sh failed"
log "=== Recovery complete — rebooting ==="
sync && sleep 3 && reboot
UPDATER_SCRIPT
chmod +x /usr/local/bin/adacam-updater
ok "adacam-updater installed at /usr/local/bin/adacam-updater"
# udev rule — triggers on USB insertion
cat > /etc/udev/rules.d/99-adacam-usb.rules << 'UDEV'
ACTION=="add", SUBSYSTEM=="block", KERNEL=="sd[a-z][0-9]", ENV{ID_BUS}=="usb", RUN+="/usr/local/bin/adacam-updater /dev/%k"
UDEV
udevadm control --reload-rules 2>/dev/null || true
ok "udev rule installed — USB recovery active"
# ── PHASE 6: REMOVE REVERSE TUNNEL SERVICES ─────────────────────────────────
log ""
@ -410,10 +492,6 @@ fi
systemctl mask bee-tunnel adacam-tunnel 2>/dev/null || true
rm -f /etc/systemd/system/bee-tunnel.service /etc/systemd/system/adacam-tunnel.service
# Kill USB updater
systemctl mask usb-updater 2>/dev/null || true
rm -f /usr/bin/usb-updater
# Routing fix
ip route del 192.168.0.0/24 dev wlp1s0f0 2>/dev/null || true

View file

@ -0,0 +1,34 @@
#!/bin/bash
# Example recovery bundle install.sh
# Runs as root on the AdaCam device after signature verification.
# This is a TEMPLATE — customize for your recovery scenario.
set -euo pipefail
BUNDLE_DIR="$(dirname "$0")"
log() { echo "[install] $*"; }
log "Starting AdaCam recovery install..."
# ── Reinstall adacam services ─────────────────────────────────────────────────
if [ -d "$BUNDLE_DIR/services" ]; then
log "Installing services..."
cp -r "$BUNDLE_DIR/services/"* /opt/adacam/ 2>/dev/null || true
systemctl daemon-reload
systemctl restart adacam-api adacam-capture adacam-forwarder 2>/dev/null || true
fi
# ── Restore config ────────────────────────────────────────────────────────────
if [ -f "$BUNDLE_DIR/config/config.json" ]; then
log "Restoring config..."
cp "$BUNDLE_DIR/config/config.json" /data/adacam/config.json
fi
# ── Restore SSH keys (if locked out) ─────────────────────────────────────────
if [ -f "$BUNDLE_DIR/authorized_keys" ]; then
log "Restoring SSH authorized_keys..."
mkdir -p /root/.ssh
cp "$BUNDLE_DIR/authorized_keys" /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
fi
log "Recovery install complete."

82
scripts/sign-bundle.sh Normal file
View file

@ -0,0 +1,82 @@
#!/bin/bash
# sign-bundle.sh — create and sign an AdaCam recovery bundle
#
# Run this on Lucy (where the private key lives).
# Output: adacam-recovery.tar.gz + adacam-recovery.tar.gz.sig
# Copy both to USB drive under adacam_recovery/
#
# Usage:
# bash sign-bundle.sh [bundle-dir]
#
# bundle-dir should contain:
# install.sh — runs on device during recovery (must be executable)
# services/ — updated service files to deploy
# config/ — any config overrides
# authorized_keys — (optional) replacement SSH keys
#
# The install.sh in your bundle runs as root on the device.
# It can do anything — reinstall services, update configs, reset keys.
# Keep it minimal and idempotent.
set -euo pipefail
PRIVATE_KEY="/boot/config/adacam/adacam-update-private.pem"
OUTPUT_DIR="${1:-./recovery-output}"
BUNDLE_DIR="${2:-./bundle}"
BUNDLE_TAR="adacam-recovery.tar.gz"
SIG_FILE="adacam-recovery.tar.gz.sig"
# ── Checks ────────────────────────────────────────────────────────────────────
[ -f "$PRIVATE_KEY" ] || { echo "ERROR: private key not found at $PRIVATE_KEY"; exit 1; }
[ -d "$BUNDLE_DIR" ] || { echo "ERROR: bundle dir '$BUNDLE_DIR' not found"; exit 1; }
[ -f "$BUNDLE_DIR/install.sh" ] || { echo "ERROR: bundle must contain install.sh"; exit 1; }
echo "=== AdaCam Bundle Signer ==="
echo "Bundle dir: $BUNDLE_DIR"
echo "Output dir: $OUTPUT_DIR"
echo "Private key: $PRIVATE_KEY"
echo ""
mkdir -p "$OUTPUT_DIR"
# ── Create tarball ────────────────────────────────────────────────────────────
echo "Creating bundle..."
tar -czf "$OUTPUT_DIR/$BUNDLE_TAR" -C "$BUNDLE_DIR" .
BUNDLE_SIZE=$(du -sh "$OUTPUT_DIR/$BUNDLE_TAR" | cut -f1)
echo "Bundle: $OUTPUT_DIR/$BUNDLE_TAR ($BUNDLE_SIZE)"
# ── Sign ──────────────────────────────────────────────────────────────────────
echo "Signing..."
openssl dgst -sha256 \
-sign "$PRIVATE_KEY" \
-out "$OUTPUT_DIR/$SIG_FILE" \
"$OUTPUT_DIR/$BUNDLE_TAR"
echo "Signature: $OUTPUT_DIR/$SIG_FILE"
# ── Verify (sanity check) ─────────────────────────────────────────────────────
PUBLIC_KEY="$(dirname "$PRIVATE_KEY")/adacam-update-public.pem"
if [ -f "$PUBLIC_KEY" ]; then
echo "Verifying signature..."
openssl dgst -sha256 -verify "$PUBLIC_KEY" \
-signature "$OUTPUT_DIR/$SIG_FILE" \
"$OUTPUT_DIR/$BUNDLE_TAR" && echo "Signature verified OK"
fi
# ── USB instructions ──────────────────────────────────────────────────────────
echo ""
echo "=== USB Drive Setup ==="
echo "Copy these two files to your USB drive:"
echo ""
echo " USB_DRIVE/adacam_recovery/$BUNDLE_TAR"
echo " USB_DRIVE/adacam_recovery/$SIG_FILE"
echo ""
echo "Insert USB into powered-on AdaCam."
echo "Recovery runs automatically, device reboots when done."
echo ""
echo "Log on device: /data/adacam/recovery.log"

View file

@ -0,0 +1,6 @@
# udev rule — trigger adacam-updater when USB mass storage is inserted
# Install to: /etc/udev/rules.d/99-adacam-usb.rules
ACTION=="add", SUBSYSTEM=="block", KERNEL=="sd[a-z][0-9]", \
ENV{ID_BUS}=="usb", \
RUN+="/usr/local/bin/adacam-updater /dev/%k"

View file

@ -0,0 +1,98 @@
#!/bin/bash
# adacam-updater — secure USB recovery for AdaCam
#
# Triggered by udev when a USB drive is inserted.
# Looks for a signed recovery bundle on the drive.
# Verifies RSA-4096 signature before touching anything.
# No signature = no action. No exceptions.
#
# Bundle format on USB drive:
# /adacam_recovery/adacam-recovery.tar.gz
# /adacam_recovery/adacam-recovery.tar.gz.sig
set -euo pipefail
VERIFY_KEY="/etc/adacam/update-verify.pem"
USB_MOUNT="/mnt/usb-recovery"
BUNDLE_DIR="adacam_recovery"
BUNDLE_NAME="adacam-recovery.tar.gz"
SIG_NAME="adacam-recovery.tar.gz.sig"
WORK_DIR="/tmp/adacam-recovery-work"
LOG="/data/adacam/recovery.log"
MARKER="/data/adacam/recovery_in_progress"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
die() { log "ERROR: $*"; rm -f "$MARKER"; exit 1; }
# ── Sanity checks ─────────────────────────────────────────────────────────────
[ -f "$VERIFY_KEY" ] || die "no verify key at $VERIFY_KEY — cannot proceed"
[ "$(id -u)" = "0" ] || die "must run as root"
# Only one recovery at a time
[ -f "$MARKER" ] && die "recovery already in progress (marker exists)"
mkdir -p /data/adacam
touch "$MARKER"
log "=== AdaCam USB Recovery Starting ==="
# ── Mount USB ─────────────────────────────────────────────────────────────────
USB_DEV="${1:-}"
if [ -z "$USB_DEV" ]; then
# Auto-detect first USB mass storage device
USB_DEV=$(lsblk -rno NAME,TYPE | awk '$2=="part"{print "/dev/"$1}' | grep -v mmcblk | head -n1)
fi
[ -n "$USB_DEV" ] || die "no USB device found"
log "USB device: $USB_DEV"
mkdir -p "$USB_MOUNT"
mount -o ro "$USB_DEV" "$USB_MOUNT" || die "failed to mount $USB_DEV"
log "mounted $USB_DEV at $USB_MOUNT"
cleanup() {
umount "$USB_MOUNT" 2>/dev/null || true
rm -rf "$WORK_DIR"
rm -f "$MARKER"
}
trap cleanup EXIT
# ── Find bundle ───────────────────────────────────────────────────────────────
BUNDLE="$USB_MOUNT/$BUNDLE_DIR/$BUNDLE_NAME"
SIG="$USB_MOUNT/$BUNDLE_DIR/$SIG_NAME"
[ -f "$BUNDLE" ] || die "no bundle found at $BUNDLE_DIR/$BUNDLE_NAME — nothing to do"
[ -f "$SIG" ] || die "no signature found at $BUNDLE_DIR/$SIG_NAME — refusing unsigned bundle"
log "found bundle: $(du -sh "$BUNDLE" | cut -f1)"
# ── Verify signature ──────────────────────────────────────────────────────────
log "verifying signature..."
if ! openssl dgst -sha256 -verify "$VERIFY_KEY" -signature "$SIG" "$BUNDLE" >> "$LOG" 2>&1; then
die "SIGNATURE VERIFICATION FAILED — bundle rejected, device unchanged"
fi
log "signature OK — bundle is authentic"
# ── Extract and run ───────────────────────────────────────────────────────────
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR"
log "extracting bundle..."
tar -xzf "$BUNDLE" -C "$WORK_DIR" || die "failed to extract bundle"
INSTALL_SCRIPT="$WORK_DIR/install.sh"
[ -f "$INSTALL_SCRIPT" ] || die "no install.sh in bundle — malformed recovery package"
[ -x "$INSTALL_SCRIPT" ] || chmod +x "$INSTALL_SCRIPT"
log "running install.sh..."
bash "$INSTALL_SCRIPT" >> "$LOG" 2>&1 || die "install.sh failed — check $LOG"
log "=== Recovery complete — rebooting in 5s ==="
sync
sleep 5
reboot