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)
This commit is contained in:
parent
e01748422c
commit
85b3ee39dd
3 changed files with 166 additions and 55 deletions
|
|
@ -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.
|
||||
|
|
|
|||
203
scripts/sign-bundle.sh
Normal file → Executable file
203
scripts/sign-bundle.sh
Normal file → Executable file
|
|
@ -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 "======================================"
|
||||
|
|
|
|||
0
services/updater/adacam-updater.sh
Normal file → Executable file
0
services/updater/adacam-updater.sh
Normal file → Executable file
Loading…
Add table
Add a link
Reference in a new issue