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:
@@ -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
|
||||||
23
overlays/base/usr/local/sbin/arcline-mok-enroll.sh
Executable file
23
overlays/base/usr/local/sbin/arcline-mok-enroll.sh
Executable 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
|
||||||
@@ -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
45
scripts/secureboot/gen-keys.sh
Executable 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"
|
||||||
51
scripts/secureboot/sign-image.sh
Executable file
51
scripts/secureboot/sign-image.sh
Executable 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"
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user