From 85b3ee39ddc1e4949dcd6735b7c580554b67f3a5 Mon Sep 17 00:00:00 2001 From: Kayos Date: Sat, 14 Mar 2026 15:07:32 -0700 Subject: [PATCH] feat: signed USB recovery system - keys/adacam-update-public.pem: RSA-4096 public key (private on Lucy) - services/updater/adacam-updater.sh: standalone updater (also inlined in liberate.sh) - services/updater/99-adacam-usb.rules: udev rule for auto-trigger on USB insert - scripts/sign-bundle.sh: create + sign recovery bundles on Lucy - keys/README.md: updated with signing key docs and bundle creation instructions Private key at: /boot/config/adacam/adacam-update-private.pem (Lucy, boot-persistent) --- keys/README.md | 18 +++ scripts/sign-bundle.sh | 203 +++++++++++++++++++++-------- services/updater/adacam-updater.sh | 0 3 files changed, 166 insertions(+), 55 deletions(-) mode change 100644 => 100755 scripts/sign-bundle.sh mode change 100644 => 100755 services/updater/adacam-updater.sh diff --git a/keys/README.md b/keys/README.md index 50dcd38..f968d2a 100644 --- a/keys/README.md +++ b/keys/README.md @@ -63,3 +63,21 @@ bash scripts/sign-bundle.sh ./recovery-output ./my-bundle-dir |-----|------|---------|--------| | 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` | + +## adacam-update-public.pem + +RSA-4096 public key for verifying signed USB recovery bundles. +Installed to `/etc/adacam/update-verify.pem` on every liberated device by liberate.sh. + +Private key: `/boot/config/adacam/adacam-update-private.pem` on Lucy — NEVER commit this. + +### Creating a recovery bundle + +From Lucy, inside the cloned adacam repo: +```bash +bash scripts/sign-bundle.sh [output-dir] +# Default output: /tmp/adacam-recovery-bundle/ +``` + +Copy the `adacam_recovery/` folder to the root of a USB drive. +Insert into a liberated AdaCam — recovery runs automatically. diff --git a/scripts/sign-bundle.sh b/scripts/sign-bundle.sh old mode 100644 new mode 100755 index 7d5a3c6..dda5919 --- a/scripts/sign-bundle.sh +++ b/scripts/sign-bundle.sh @@ -1,82 +1,175 @@ #!/bin/bash -# sign-bundle.sh — create and sign an AdaCam recovery bundle +# sign-bundle.sh — create a signed 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/ +# Run this on Lucy to package and sign a recovery bundle. +# The signed bundle goes on a USB drive for emergency device recovery. # # Usage: -# bash sign-bundle.sh [bundle-dir] +# ./scripts/sign-bundle.sh [output-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 +# Output: +# adacam_recovery/adacam-recovery.tar.gz +# adacam_recovery/adacam-recovery.tar.gz.sig # -# 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. +# Requires: +# - /boot/config/adacam/adacam-update-private.pem (RSA-4096 private key on Lucy) +# - The adacam repo checked out (source of services/ and configs/) 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" +REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" +OUTPUT_DIR="${1:-/tmp/adacam-recovery-bundle}" +BUNDLE_DIR="$OUTPUT_DIR/adacam_recovery" +BUNDLE="$BUNDLE_DIR/adacam-recovery.tar.gz" +SIG="$BUNDLE_DIR/adacam-recovery.tar.gz.sig" +STAGING="/tmp/adacam-recovery-staging" -# ── Checks ──────────────────────────────────────────────────────────────────── +log() { echo "[$(date '+%H:%M:%S')] $*"; } +die() { echo "ERROR: $*" >&2; exit 1; } -[ -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; } +[ -f "$PRIVATE_KEY" ] || die "private key not found at $PRIVATE_KEY" +[ -d "$REPO_DIR/services" ] || die "run from inside the adacam repo (services/ not found)" -echo "=== AdaCam Bundle Signer ===" -echo "Bundle dir: $BUNDLE_DIR" -echo "Output dir: $OUTPUT_DIR" -echo "Private key: $PRIVATE_KEY" -echo "" +log "Building AdaCam recovery bundle" +log "Source: $REPO_DIR" +log "Output: $BUNDLE_DIR" -mkdir -p "$OUTPUT_DIR" +# ── Stage recovery contents ─────────────────────────────────────────────────── + +rm -rf "$STAGING" && mkdir -p "$STAGING" + +# Core services +cp -r "$REPO_DIR/services/capture/" "$STAGING/capture/" 2>/dev/null || true +cp -r "$REPO_DIR/services/updater/" "$STAGING/updater/" 2>/dev/null || true + +# adacam-api (if present locally — otherwise pull from Gitea in install.sh) +if [ -d "$REPO_DIR/../adacam-api" ]; then + cp -r "$REPO_DIR/../adacam-api" "$STAGING/adacam-api/" +fi + +# Keys +mkdir -p "$STAGING/keys" +cp "$REPO_DIR/keys/adacam_authorized_key.pub" "$STAGING/keys/" +cp "$REPO_DIR/keys/adacam-update-public.pem" "$STAGING/keys/" + +# Write install.sh — runs on device after extraction +cat > "$STAGING/install.sh" << 'INSTALL' +#!/bin/bash +# AdaCam recovery install — runs on device after bundle is verified and extracted +# Working directory is the extracted bundle root + +set -euo pipefail +BUNDLE_ROOT="$(cd "$(dirname "$0")" && pwd)" +GITEA="http://192.168.0.5:3001" +GITEA_TOKEN="33a9eb57b58c262f4434c12028bc3a30b1ff7021" + +log() { echo "[recovery] $*"; } +die() { echo "[recovery] ERROR: $*" >&2; exit 1; } + +log "=== AdaCam Recovery Install ===" + +# 1. Restore authorized SSH keys +log "restoring SSH keys..." +mkdir -p /root/.ssh && chmod 700 /root/.ssh +cat "$BUNDLE_ROOT/keys/adacam_authorized_key.pub" >> /root/.ssh/authorized_keys +sort -u /root/.ssh/authorized_keys -o /root/.ssh/authorized_keys +chmod 600 /root/.ssh/authorized_keys + +# 2. Re-install update verify key +log "restoring update verify key..." +mkdir -p /etc/adacam +cp "$BUNDLE_ROOT/keys/adacam-update-public.pem" /etc/adacam/update-verify.pem + +# 3. Re-install updater itself (self-healing) +log "restoring updater..." +cp "$BUNDLE_ROOT/updater/adacam-updater.sh" /usr/local/bin/adacam-updater +chmod +x /usr/local/bin/adacam-updater +cp "$BUNDLE_ROOT/updater/99-adacam-usb.rules" /etc/udev/rules.d/ +udevadm control --reload-rules 2>/dev/null || true + +# 4. Re-install capture services +if [ -d "$BUNDLE_ROOT/capture" ]; then + log "restoring capture services..." + mkdir -p /opt/adacam/capture + cp -r "$BUNDLE_ROOT/capture/"* /opt/adacam/capture/ + if [ -f "$BUNDLE_ROOT/capture/adacam-capture.service" ]; then + cp "$BUNDLE_ROOT/capture/adacam-capture.service" /etc/systemd/system/ + cp "$BUNDLE_ROOT/capture/adacam-forwarder.service" /etc/systemd/system/ 2>/dev/null || true + systemctl daemon-reload + systemctl enable adacam-capture adacam-forwarder 2>/dev/null || true + fi +fi + +# 5. Pull latest adacam-api from Gitea and reinstall +log "pulling adacam-api from Gitea..." +if curl -sf "$GITEA/Sulkta-Coop/adacam-api/archive/main.tar.gz" \ + -H "Authorization: token $GITEA_TOKEN" \ + -o /tmp/adacam-api.tar.gz 2>/dev/null; then + mkdir -p /opt/adacam/api + tar -xzf /tmp/adacam-api.tar.gz -C /opt/adacam/api --strip-components=1 + if [ -f /opt/adacam/api/install.sh ]; then + bash /opt/adacam/api/install.sh + fi + rm /tmp/adacam-api.tar.gz + log "adacam-api restored from Gitea" +else + log "WARNING: could not reach Gitea — adacam-api not restored (LAN required)" +fi + +# 6. Re-apply firewall +if [ -f /data/persist/firewall.sh ]; then + log "reapplying firewall..." + bash /data/persist/firewall.sh +fi + +# 7. Restart services +log "restarting services..." +systemctl restart sshd 2>/dev/null || true +systemctl restart adacam-api 2>/dev/null || true +systemctl restart adacam-capture 2>/dev/null || true + +log "=== Recovery install complete ===" +INSTALL + +chmod +x "$STAGING/install.sh" # ── 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)" +mkdir -p "$BUNDLE_DIR" +log "creating bundle..." +tar -czf "$BUNDLE" -C "$STAGING" . +BUNDLE_SIZE=$(du -sh "$BUNDLE" | cut -f1) +log "bundle: $BUNDLE ($BUNDLE_SIZE)" -# ── Sign ────────────────────────────────────────────────────────────────────── +# ── Sign it ─────────────────────────────────────────────────────────────────── -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) ───────────────────────────────────────────────────── +log "signing with RSA-4096..." +openssl dgst -sha256 -sign "$PRIVATE_KEY" -out "$SIG" "$BUNDLE" +log "signature: $SIG" +# Verify our own signature (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" +if openssl dgst -sha256 -verify "$PUBLIC_KEY" -signature "$SIG" "$BUNDLE" > /dev/null 2>&1; then + log "self-verify: OK" +else + die "self-verify FAILED — bundle may be corrupt" fi -# ── USB instructions ────────────────────────────────────────────────────────── +# ── Cleanup and report ──────────────────────────────────────────────────────── + +rm -rf "$STAGING" echo "" -echo "=== USB Drive Setup ===" -echo "Copy these two files to your USB drive:" +echo "======================================" +echo " Recovery Bundle Ready" +echo "======================================" +echo " Bundle: $BUNDLE" +echo " Signature: $SIG" +echo " Size: $BUNDLE_SIZE" echo "" -echo " USB_DRIVE/adacam_recovery/$BUNDLE_TAR" -echo " USB_DRIVE/adacam_recovery/$SIG_FILE" +echo " Copy the adacam_recovery/ folder to a USB drive:" +echo " cp -r $BUNDLE_DIR /path/to/usb/adacam_recovery" 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" +echo " The USB drive root must contain: adacam_recovery/" +echo "======================================" diff --git a/services/updater/adacam-updater.sh b/services/updater/adacam-updater.sh old mode 100644 new mode 100755