From 3a17504dd01c40b6799ebf589a3da8eaffe9da28 Mon Sep 17 00:00:00 2001 From: Blake Ridgway Date: Fri, 21 Aug 2026 13:33:17 -0500 Subject: [PATCH] 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. --- .../systemd/system/arcline-mok-enroll.service | 13 +++++ .../base/usr/local/sbin/arcline-mok-enroll.sh | 23 +++++++++ scripts/build-iso.sh | 10 ++++ scripts/secureboot/gen-keys.sh | 45 ++++++++++++++++ scripts/secureboot/sign-image.sh | 51 +++++++++++++++++++ tests/smoke/verify-rootfs.sh | 2 + 6 files changed, 144 insertions(+) create mode 100644 overlays/base/usr/lib/systemd/system/arcline-mok-enroll.service create mode 100755 overlays/base/usr/local/sbin/arcline-mok-enroll.sh create mode 100755 scripts/secureboot/gen-keys.sh create mode 100755 scripts/secureboot/sign-image.sh diff --git a/overlays/base/usr/lib/systemd/system/arcline-mok-enroll.service b/overlays/base/usr/lib/systemd/system/arcline-mok-enroll.service new file mode 100644 index 0000000..f7bd1b0 --- /dev/null +++ b/overlays/base/usr/lib/systemd/system/arcline-mok-enroll.service @@ -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 diff --git a/overlays/base/usr/local/sbin/arcline-mok-enroll.sh b/overlays/base/usr/local/sbin/arcline-mok-enroll.sh new file mode 100755 index 0000000..8ab185c --- /dev/null +++ b/overlays/base/usr/local/sbin/arcline-mok-enroll.sh @@ -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 diff --git a/scripts/build-iso.sh b/scripts/build-iso.sh index 5dee636..5bde7e4 100755 --- a/scripts/build-iso.sh +++ b/scripts/build-iso.sh @@ -45,6 +45,16 @@ INITRD="$(find "$ROOTFS/boot" -maxdepth 1 -name 'initrd.img-*' | sort -V | tail cp -L "$KERNEL" "$ISOFILES/live/vmlinuz" 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)" # 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. diff --git a/scripts/secureboot/gen-keys.sh b/scripts/secureboot/gen-keys.sh new file mode 100755 index 0000000..2202ba2 --- /dev/null +++ b/scripts/secureboot/gen-keys.sh @@ -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 ]}" + +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" diff --git a/scripts/secureboot/sign-image.sh b/scripts/secureboot/sign-image.sh new file mode 100755 index 0000000..e3aea2d --- /dev/null +++ b/scripts/secureboot/sign-image.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# ───────────────────────────────────────────────────────────────────────────── +# Arcline OS — secure boot: sign an image with the MOK +# +# scripts/secureboot/sign-image.sh [--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 [--keydir ]}" +KEYDIR="$BUILD_DIR/keys" +[[ "${2:-}" == "--keydir" ]] && KEYDIR="${3:?usage: sign-image.sh [--keydir ]}" + +[[ -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" diff --git a/tests/smoke/verify-rootfs.sh b/tests/smoke/verify-rootfs.sh index 1ac6531..f8a3896 100755 --- a/tests/smoke/verify-rootfs.sh +++ b/tests/smoke/verify-rootfs.sh @@ -69,6 +69,8 @@ have_file "usr/local/sbin/arcline-rollback" \ && ok "btrfs rollback tooling present" || bad "missing arcline-rollback" have_file "usr/lib/systemd/system/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 \ && ok "arcline-release present" || bad "missing /etc/arcline-release"