feat: add MOK-based secure boot signing

- scripts/secureboot/gen-keys.sh: generates a Machine Owner Key pair
  (MOK.priv / MOK.pem / MOK.der) for self-signing the boot chain.
- scripts/secureboot/sign-image.sh: signs kernels and EFI binaries
  (already-signed files skipped) with sbsign.
- arcline-mok-enroll.service (+ script): one-time MOK enrollment at first
  boot via mokutil; no-ops when no key was shipped.
- build-iso.sh: ARCLINE_SIGN=1 signs the live boot chain and ships the
  public MOK in the image. Smoke test now asserts the enroll unit exists.
This commit is contained in:
Blake Ridgway
2026-08-21 13:33:17 -05:00
parent 18167fc70e
commit 3a17504dd0
6 changed files with 144 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
[Unit]
Description=Enroll Arcline secure boot MOK (one-time)
Documentation=docs/secureboot.md
DefaultDependencies=no
Before=sysinit.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/arcline-mok-enroll.sh
RemainAfterExit=yes
[Install]
WantedBy=sysinit.target

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Arcline OS — one-time MOK enrollment (runs at first boot, then disables itself)
# No-op when no key was shipped in the image (secure boot off by default).
set -euo pipefail
KEY=/etc/arcline/MOK.der
UNIT=arcline-mok-enroll.service
if [[ ! -f "$KEY" ]]; then
echo "arcline: no MOK key shipped — secure boot enrollment skipped"
systemctl disable --now "$UNIT" 2>/dev/null || true
exit 0
fi
if mokutil --test-key "$KEY" 2>/dev/null | grep -q enabled; then
echo "arcline: MOK already enrolled"
else
echo "arcline: importing MOK — you will be prompted to confirm it on the next reboot"
mokutil --import "$KEY" || true
fi
# enrollment is one-time; disable ourselves so this never runs again
systemctl disable --now "$UNIT" 2>/dev/null || true

View File

@@ -45,6 +45,16 @@ INITRD="$(find "$ROOTFS/boot" -maxdepth 1 -name 'initrd.img-*' | sort -V | tail
cp -L "$KERNEL" "$ISOFILES/live/vmlinuz" cp -L "$KERNEL" "$ISOFILES/live/vmlinuz"
cp -L "$INITRD" "$ISOFILES/live/initrd.img" cp -L "$INITRD" "$ISOFILES/live/initrd.img"
# ── 2b. secure boot (optional: ARCLINE_SIGN=1 + a MOK keypair) ──────────────
if [[ "${ARCLINE_SIGN:-0}" == "1" ]]; then
log "secure boot: signing boot chain and shipping MOK in the live image"
[[ -f "$BUILD_DIR/keys/MOK.der" ]] || die "ARCLINE_SIGN=1 but no MOK keypair — run: scripts/secureboot/gen-keys.sh"
mkdir -p "$ROOTFS/etc/arcline"
cp "$BUILD_DIR/keys/MOK.der" "$ROOTFS/etc/arcline/MOK.der"
"$ROOT/scripts/secureboot/sign-image.sh" "$ROOTFS" --keydir "$BUILD_DIR/keys"
"$ROOT/scripts/secureboot/sign-image.sh" "$ISOFILES" --keydir "$BUILD_DIR/keys"
fi
log "compressing rootfs → squashfs (this takes a while)" log "compressing rootfs → squashfs (this takes a while)"
# The squashfs is the live root. Keep it complete — offline man pages and # The squashfs is the live root. Keep it complete — offline man pages and
# docs are a product promise (see the landing page), so nothing is excluded. # docs are a product promise (see the landing page), so nothing is excluded.

45
scripts/secureboot/gen-keys.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — secure boot: MOK key generation
#
# scripts/secureboot/gen-keys.sh [--keydir build/keys]
#
# Generates a Machine Owner Key (MOK) signing keypair for self-signing the
# kernel and EFI boot chain. MOK is the pragmatic secure-boot path for a
# self-hosted distro: you enroll the key once (one-time prompt at first boot)
# and then every Arcline update is verified against it. No third-party CA.
#
# Outputs (default build/keys/):
# MOK.priv — private signing key (keep this secret, back it up)
# MOK.pem — certificate in PEM form (for sbsign)
# MOK.der — certificate in DER form (for mokutil enrollment)
#
# Requires openssl. Enrollment on a target system is handled by
# arcline-mok-enroll.service (see overlays/base).
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/../../scripts/common.sh"
KEYDIR="${1:-$BUILD_DIR/keys}"
[[ "${1:-}" == "--keydir" ]] && KEYDIR="${2:?usage: gen-keys.sh [--keydir <dir>]}"
command -v openssl >/dev/null || die "openssl not found"
if [[ -f "$KEYDIR/MOK.priv" && -f "$KEYDIR/MOK.pem" && -f "$KEYDIR/MOK.der" ]]; then
log "MOK keypair already exists in $KEYDIR — keeping it (delete to regenerate)"
exit 0
fi
mkdir -p "$KEYDIR"
log "generating MOK signing keypair in $KEYDIR"
openssl req -new -x509 -newkey rsa:3072 -sha256 -nodes \
-keyout "$KEYDIR/MOK.priv" \
-outform DER -out "$KEYDIR/MOK.der" \
-subj "/CN=Arcline OS MOK/OU=Secure Boot/O=Arcline IT LLC" \
-days 3650
openssl x509 -in "$KEYDIR/MOK.der" -inform DER -out "$KEYDIR/MOK.pem" -outform PEM
chmod 600 "$KEYDIR/MOK.priv"
log "generated: MOK.priv (keep secret), MOK.pem, MOK.der"
log "to enroll on a running system:"
log " sudo mokutil --import $KEYDIR/MOK.der"

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Arcline OS — secure boot: sign an image with the MOK
#
# scripts/secureboot/sign-image.sh <rootfs-or-isofiles-dir> [--keydir build/keys]
#
# Signs every PE binary in the boot chain with the MOK:
# - kernels (vmlinuz*) in /boot and /live
# - EFI binaries (*.efi) in /boot (grubx64, shimx64, mmx64, ...)
#
# sbsign replaces files in place (already-signed files are skipped). GRUB
# .mod modules are not PE and are out of scope here — see docs/secureboot.md.
#
# Requires sbsigntool. Keypair from scripts/secureboot/gen-keys.sh.
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/../../scripts/common.sh"
TARGET="${1:?usage: sign-image.sh <rootfs-or-isofiles-dir> [--keydir <dir>]}"
KEYDIR="$BUILD_DIR/keys"
[[ "${2:-}" == "--keydir" ]] && KEYDIR="${3:?usage: sign-image.sh <dir> [--keydir <dir>]}"
[[ -d "$TARGET" ]] || die "target '$TARGET' does not exist"
[[ -f "$KEYDIR/MOK.priv" && -f "$KEYDIR/MOK.pem" ]] || die "MOK keypair not found in $KEYDIR (run: scripts/secureboot/gen-keys.sh)"
command -v sbsign >/dev/null || die "sbsign not found (package: sbsigntool)"
command -v sbverify >/dev/null || die "sbverify not found (package: sbsigntool)"
signed=0; skipped=0
sign_file() {
local f="$1"
if sbverify --cert "$KEYDIR/MOK.pem" "$f" >/dev/null 2>&1; then
skipped=$((skipped+1))
return
fi
local tmp="$f.arcline-signed"
if sbsign --key "$KEYDIR/MOK.priv" --cert "$KEYDIR/MOK.pem" --output "$tmp" "$f" >/dev/null 2>&1; then
mv "$tmp" "$f"
log "signed: $f"
signed=$((signed+1))
else
warn "could not sign: $f"
rm -f "$tmp"
fi
}
log "signing boot chain in $TARGET"
while IFS= read -r -d '' f; do sign_file "$f"; done < <(find "$TARGET" -type f \( -name 'vmlinuz*' -o -name '*.efi' \) -print0)
# also sign any live-boot kernel staged by the ISO build
while IFS= read -r -d '' f; do sign_file "$f"; done < <(find "$TARGET/live" -maxdepth 1 -type f -name 'vmlinuz*' -print0 2>/dev/null)
log "secure boot signing done: $signed signed, $skipped already signed"

View File

@@ -69,6 +69,8 @@ have_file "usr/local/sbin/arcline-rollback" \
&& ok "btrfs rollback tooling present" || bad "missing arcline-rollback" && ok "btrfs rollback tooling present" || bad "missing arcline-rollback"
have_file "usr/lib/systemd/system/arcline-snapshot.timer" \ have_file "usr/lib/systemd/system/arcline-snapshot.timer" \
&& ok "snapshot timer unit present" || bad "missing arcline-snapshot.timer" && ok "snapshot timer unit present" || bad "missing arcline-snapshot.timer"
have_file "usr/lib/systemd/system/arcline-mok-enroll.service" \
&& ok "secure-boot MOK enrollment unit present" || bad "missing arcline-mok-enroll.service"
grep -qi 'arcline' "$ROOTFS/etc/arcline-release" 2>/dev/null \ grep -qi 'arcline' "$ROOTFS/etc/arcline-release" 2>/dev/null \
&& ok "arcline-release present" || bad "missing /etc/arcline-release" && ok "arcline-release present" || bad "missing /etc/arcline-release"