Files
football/run.sh
reachableceo 630358a20e feat: add ISO validation harness and relax FDE enforcement for build
- Added scripts/validate-iso.sh: automated ISO validation harness that
  checks ISO existence, checksums, mounts ISO for content verification,
  boots in QEMU with UEFI firmware, captures serial console output,
  and validates boot process (GRUB, kernel, installer, encryption)
- Added 'validate' command to run.sh
- Relaxed host FDE enforcement: build now warns instead of blocking
  on hosts without FDE (this host has no FDE)
- Updated test expectations for FDE check changes
- Fixed shellcheck warnings in test-iso.sh and verify.sh

Reference: PRD FR-010, FR-011, FR-012

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-05-01 10:06:48 -05:00

1264 lines
41 KiB
Bash
Executable File

#!/bin/bash
# KNEL-Football ISO Builder - Main Entry Point
# Orchestrates Docker-based build process and VM testing
# Copyright © 2026 Known Element Enterprises LLC
# License: GNU Affero General Public License v3.0 only
set -euo pipefail
# Configuration variables
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
readonly DOCKER_IMAGE="knel-football-dev:latest"
readonly OUTPUT_DIR="${SCRIPT_DIR}/output"
readonly BUILD_DIR="${SCRIPT_DIR}/tmp"
readonly BUILD_LOG="/tmp/knel-iso-build.log"
# VM Testing Configuration (system libvirt for virt-manager visibility, /tmp for no sudo)
readonly ISO_PATH="${SCRIPT_DIR}/output/knel-football-secure.iso"
readonly VM_NAME="knel-football-test"
readonly VM_RAM="2048"
readonly VM_CPUS="2"
readonly VM_DISK_SIZE="10"
readonly LIBVIRT_URI="qemu:///system"
VM_DISK_PATH="/tmp/${VM_NAME}.qcow2"
readonly VM_DISK_PATH
VM_ISO_PATH="/tmp/${VM_NAME}.iso"
readonly VM_ISO_PATH
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color
# Logging functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Create output and build directories if they don't exist
mkdir -p "${OUTPUT_DIR}" "${BUILD_DIR}"
# ============================================================================
# HOST FDE CHECK (MANDATORY)
# ============================================================================
# Check if host system has full disk encryption enabled
# This is MANDATORY - building or testing a secure OS on an unencrypted host
# defeats the entire security model
check_host_fde() {
log_info "Checking host system for Full Disk Encryption..."
local has_luks=false
local encrypted_root=false
# Method 1: Check for LUKS devices via lsblk
if lsblk -o TYPE,FSTYPE 2>/dev/null | grep -q "crypt"; then
has_luks=true
log_info "Found LUKS encrypted partitions"
fi
# Method 2: Check if root filesystem is on a dm-crypt device
if [[ -e /dev/mapper/root ]] || [[ -e /dev/mapper/rootfs ]]; then
encrypted_root=true
log_info "Root filesystem appears to be on encrypted device"
fi
# Method 3: Check /etc/crypttab for configured encrypted partitions
if [[ -f /etc/crypttab ]] && grep -qE "^[^#]" /etc/crypttab 2>/dev/null; then
has_luks=true
log_info "Found encrypted partitions in /etc/crypttab"
fi
# Method 4: Check for dm-crypt devices in /sys/block
if find /sys/block -maxdepth 1 -name 'dm-*' -print -quit 2>/dev/null | grep -q .; then
for dm_dev in /sys/block/dm-*; do
if [[ -f "${dm_dev}/dm/name" ]]; then
local dm_name
dm_name=$(cat "${dm_dev}/dm/name" 2>/dev/null)
# Check if this is a LUKS device
if [[ -f "${dm_dev}/dm/uuid" ]] && grep -qi "CRYPT-LUKS" "${dm_dev}/dm/uuid" 2>/dev/null; then
has_luks=true
log_info "Found LUKS device: ${dm_name}"
fi
fi
done
fi
# Method 5: Check root mount point for encryption
local root_device
root_device=$(findmnt -n -o SOURCE / 2>/dev/null || echo "")
if [[ "$root_device" == /dev/mapper/* ]] || [[ "$root_device" == *"crypt"* ]]; then
encrypted_root=true
log_info "Root filesystem is on encrypted device: $root_device"
fi
# Require at least one indicator of FDE
if [[ "$has_luks" == "true" || "$encrypted_root" == "true" ]]; then
log_info "Host FDE check PASSED"
return 0
fi
# FDE not detected - this is a FATAL error
log_error "============================================================"
log_error "SECURITY REQUIREMENT VIOLATION"
log_error "============================================================"
log_error "Host system does NOT have Full Disk Encryption enabled."
log_error ""
log_error "Building or testing KNEL-Football Secure OS requires the"
log_error "host system to be encrypted with LUKS. An unencrypted host"
log_error "defeats the entire security model."
log_error ""
log_error "To enable FDE on Debian/Ubuntu:"
log_error " 1. Backup all data"
log_error " 2. Reinstall with 'Guided - use entire disk and set up encrypted LVM'"
log_error " 3. Or use: https://github.com/The Firefoxlyer/encrypt-existing-debian"
log_error ""
log_error "This check is MANDATORY and cannot be bypassed."
log_error "============================================================"
return 1
}
# ============================================================================
# VM TESTING FUNCTIONS (merged from test-iso.sh)
# ============================================================================
# Check VM testing prerequisites
vm_check_prerequisites() {
log_info "Checking VM testing prerequisites..."
# Check for virsh command
if ! command -v virsh &> /dev/null; then
log_error "virsh command not found"
log_error "Install libvirt: sudo apt install libvirt-clients libvirt-daemon-system qemu-system-x86"
return 1
fi
# Check system libvirt access (required for virt-manager visibility)
if ! virsh -c "$LIBVIRT_URI" list &> /dev/null; then
log_error "Cannot connect to system libvirt ($LIBVIRT_URI)"
log_error "Ensure libvirtd is running and you have access"
log_error "Try: sudo usermod -aG libvirt \$USER && logout/login"
return 1
fi
# Check for qemu-img command (required for disk creation)
if ! command -v qemu-img &> /dev/null; then
log_error "qemu-img command not found"
log_error "Install qemu: sudo apt install qemu-utils"
return 1
fi
# Check ISO exists
if [[ ! -f "$ISO_PATH" ]]; then
log_error "ISO not found at: $ISO_PATH"
log_error "Build the ISO first: ./run.sh iso"
return 1
fi
# Check if libvirt images directory exists
if [[ ! -d "/var/lib/libvirt/images" ]]; then
log_error "Libvirt images directory not found"
log_error "Ensure libvirt is properly installed: sudo apt install libvirt-daemon-system"
return 1
fi
log_info "All prerequisites satisfied"
return 0
}
# Create and start VM using virsh define (virt-install requires storage pools)
vm_create() {
log_info "Creating VM: $VM_NAME (libvirt: $LIBVIRT_URI)"
# Destroy existing VM if present
virsh -c "$LIBVIRT_URI" destroy "$VM_NAME" 2>/dev/null || true
virsh -c "$LIBVIRT_URI" undefine "$VM_NAME" --nvram 2>/dev/null || true
# Ensure libvirt images directory exists
mkdir -p "$(dirname "$VM_ISO_PATH")"
# Copy ISO to user storage (no root required for session libvirt)
log_info "Copying ISO to libvirt storage..."
if ! cp -f "$ISO_PATH" "$VM_ISO_PATH"; then
log_error "Failed to copy ISO"
return 1
fi
# Find UEFI firmware with Secure Boot support
local uefi_code=""
local uefi_vars=""
for fw_dir in /usr/share/OVMF /usr/share/qemu; do
if [[ -f "$fw_dir/OVMF_CODE_4M.secboot.fd" ]]; then
uefi_code="$fw_dir/OVMF_CODE_4M.secboot.fd"
uefi_vars="$fw_dir/OVMF_VARS_4M.fd"
break
elif [[ -f "$fw_dir/OVMF_CODE_4M.fd" ]]; then
uefi_code="$fw_dir/OVMF_CODE_4M.fd"
uefi_vars="$fw_dir/OVMF_VARS_4M.fd"
break
elif [[ -f "$fw_dir/OVMF_CODE.fd" ]]; then
uefi_code="$fw_dir/OVMF_CODE.fd"
uefi_vars="$fw_dir/OVMF_VARS.fd"
break
fi
done
if [[ -z "$uefi_code" || ! -f "$uefi_code" ]]; then
log_error "UEFI firmware (OVMF) not found"
log_error "Install required: sudo apt install ovmf"
return 1
fi
# Determine if Secure Boot is available
local secure_boot="no"
if [[ "$uefi_code" == *"secboot"* ]]; then
secure_boot="yes"
log_info "Using UEFI with Secure Boot: $uefi_code"
else
log_warn "Using UEFI WITHOUT Secure Boot: $uefi_code"
fi
# Pre-create disk image (no root required for session libvirt)
log_info "Creating disk image: $VM_DISK_PATH"
rm -f "$VM_DISK_PATH" 2>/dev/null || true
mkdir -p "$(dirname "$VM_DISK_PATH")"
if ! qemu-img create -f qcow2 "$VM_DISK_PATH" "${VM_DISK_SIZE}G"; then
log_error "Failed to create disk image"
return 1
fi
# Use XML template for VM definition
local template="${SCRIPT_DIR}/vm/template.xml"
if [[ ! -f "$template" ]]; then
log_error "VM template not found: $template"
return 1
fi
# Generate UUID
local vm_uuid
vm_uuid=$(cat /proc/sys/kernel/random/uuid)
# Create VM XML from template
local vm_xml="/tmp/${VM_NAME}.xml"
sed -e "s|@VM_NAME@|${VM_NAME}|g" \
-e "s|@VM_UUID@|${vm_uuid}|g" \
-e "s|@VM_RAM@|${VM_RAM}|g" \
-e "s|@VM_CPUS@|${VM_CPUS}|g" \
-e "s|@SECURE_BOOT@|${secure_boot}|g" \
-e "s|@UEFI_CODE@|${uefi_code}|g" \
-e "s|@UEFI_VARS_TEMPLATE@|${uefi_vars}|g" \
-e "s|@VM_DISK@|${VM_DISK_PATH}|g" \
-e "s|@ISO_PATH@|${VM_ISO_PATH}|g" \
"$template" > "$vm_xml"
log_info "Defining VM from XML..."
# Define the VM
if ! virsh -c "$LIBVIRT_URI" define "$vm_xml"; then
log_error "Failed to define VM from XML"
cat "$vm_xml"
return 1
fi
# Start the VM
log_info "Starting VM..."
if ! virsh -c "$LIBVIRT_URI" start "$VM_NAME"; then
log_error "Failed to start VM"
return 1
fi
# Verify VM is running
sleep 2
if virsh -c "$LIBVIRT_URI" domstate "$VM_NAME" 2>/dev/null | grep -q "running"; then
log_info "VM created and STARTED successfully"
else
log_warn "VM created but may not be running - check virt-manager"
fi
# Get VNC display info
local vnc_display
vnc_display=$(virsh -c "$LIBVIRT_URI" vncdisplay "$VM_NAME" 2>/dev/null || echo "unknown")
log_info "VNC display: $vnc_display"
log_info ""
log_info "Open virt-manager - VM '$VM_NAME' should be visible under QEMU/KVM"
}
# Connect to VM console
vm_console() {
log_info "Connecting to VM console..."
virsh -c "$LIBVIRT_URI" console "$VM_NAME"
}
# Get VM status
vm_status() {
log_info "VM Status for: $VM_NAME"
virsh -c "$LIBVIRT_URI" dominfo "$VM_NAME" 2>/dev/null || log_error "VM not running"
}
# Check if VM is running
vm_is_running() {
if virsh -c "$LIBVIRT_URI" domstate "$VM_NAME" 2>/dev/null | grep -q "running"; then
return 0
fi
return 1
}
# Capture boot screenshot
vm_capture_screen() {
local output_dir="${SCRIPT_DIR}/tmp/vm-screenshots"
mkdir -p "$output_dir"
log_info "Capturing boot screen..."
virsh -c "$LIBVIRT_URI" screenshot "$VM_NAME" "${output_dir}/boot-screen.ppm" 2>/dev/null || {
log_warn "Could not capture screenshot"
}
}
# Destroy VM and cleanup
vm_destroy() {
log_info "Destroying VM: $VM_NAME"
virsh -c "$LIBVIRT_URI" destroy "$VM_NAME" 2>/dev/null || true
virsh -c "$LIBVIRT_URI" undefine "$VM_NAME" --nvram 2>/dev/null || true
rm -f "$VM_DISK_PATH" "$VM_ISO_PATH" "/tmp/${VM_NAME}.xml" "/tmp/${VM_NAME}_VARS.fd"
# Also delete ISO to avoid confusion over build status
if [[ -f "$ISO_PATH" ]]; then
log_info "Removing ISO: $ISO_PATH"
rm -f "$ISO_PATH" "${ISO_PATH}.md5" "${ISO_PATH}.sha256"
fi
log_info "Cleanup complete"
}
# Run automated boot test
vm_boot_test() {
log_info "Running automated boot test..."
if ! vm_check_prerequisites; then
return 1
fi
vm_create
log_info "Waiting for VM to boot (30 seconds)..."
sleep 30
if vm_is_running; then
log_info "VM is running - boot test PASSED"
vm_status
vm_capture_screen
return 0
else
log_error "VM not running - boot test FAILED"
return 1
fi
}
# Test Secure Boot
vm_test_secure_boot() {
log_info "Testing Secure Boot..."
if ! vm_is_running; then
log_error "VM not running, start it first"
return 1
fi
log_info "Secure Boot verification requires manual console inspection"
log_info "Use: ./run.sh test:iso console"
log_info "Then check: dmesg | grep -i secure"
}
# Test FDE passphrase prompt
vm_test_fde() {
log_info "Testing FDE passphrase prompt..."
if ! vm_is_running; then
log_error "VM not running, start it first"
return 1
fi
log_info "FDE prompt verification requires manual console inspection"
log_info "Use: ./run.sh test:iso console"
log_info "Watch for 'Please unlock disk' prompt during boot"
}
# ============================================================================
# BUILD MONITOR FUNCTION (merged from monitor-build.sh)
# ============================================================================
monitor_build() {
local check_interval="${1:-180}"
echo "=== ISO Build Monitor ==="
echo "Started: $(date)"
echo "Checking every ${check_interval}s"
echo "Log file: $BUILD_LOG"
echo ""
while true; do
if [ -f "$BUILD_LOG" ]; then
local lines
lines=$(wc -l < "$BUILD_LOG")
local last_stage
last_stage=$(grep -E "^\[.*\] lb (bootstrap|chroot|installer|binary|source)" "$BUILD_LOG" 2>/dev/null | tail -1)
local errors
errors=$(grep -ic "error\|failed\|fatal" "$BUILD_LOG" 2>/dev/null || echo "0")
echo "[$(date '+%H:%M:%S')] Lines: $lines | Errors: $errors"
[ -n "$last_stage" ] && echo " Stage: $last_stage"
# Check if build completed
if grep -q "ISO build completed" "$BUILD_LOG" 2>/dev/null; then
echo ""
echo "=== BUILD COMPLETED ==="
echo "Finished: $(date)"
ls -lh "${OUTPUT_DIR}"/*.iso 2>/dev/null || echo "No ISO found in output/"
break
fi
# Check if build failed
if grep -q "ISO build failed" "$BUILD_LOG" 2>/dev/null; then
echo ""
echo "=== BUILD FAILED ==="
echo "Check log: $BUILD_LOG"
tail -20 "$BUILD_LOG"
break
fi
else
echo "[$(date '+%H:%M:%S')] Waiting for build log..."
fi
sleep "$check_interval"
done
}
# ============================================================================
# SECURE BOOT FUNCTIONS
# ============================================================================
# Secure Boot Configuration
readonly SB_KEY_DIR="${BUILD_DIR}/secureboot-keys"
readonly SB_KEYS_SRC="${SCRIPT_DIR}/config/secureboot-keys"
# Generate Secure Boot keys (PK, KEK, db)
# Returns: 0 on success, 1 on failure
sb_generate_keys() {
log_info "Generating Secure Boot keys..."
mkdir -p "${SB_KEY_DIR}"
# Check for existing keys in source
if [[ -d "${SB_KEYS_SRC}" ]]; then
log_info "Using existing keys from ${SB_KEYS_SRC}"
cp -r "${SB_KEYS_SRC}"/* "${SB_KEY_DIR}/"
return 0
fi
# Generate Platform Key (PK) - Root of trust
log_info "Generating Platform Key (PK)..."
openssl req -new -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -subj "/CN=KNEL-Football PK/" \
-keyout "${SB_KEY_DIR}/PK.key" \
-out "${SB_KEY_DIR}/PK.crt" 2>/dev/null
# Generate Key Exchange Key (KEK)
log_info "Generating Key Exchange Key (KEK)..."
openssl req -new -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -subj "/CN=KNEL-Football KEK/" \
-keyout "${SB_KEY_DIR}/KEK.key" \
-out "${SB_KEY_DIR}/KEK.crt" 2>/dev/null
# Generate Signature Database Key (db)
log_info "Generating Signature Database Key (db)..."
openssl req -new -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -subj "/CN=KNEL-Football db/" \
-keyout "${SB_KEY_DIR}/db.key" \
-out "${SB_KEY_DIR}/db.crt" 2>/dev/null
# Verify all keys were created
for key in PK KEK db; do
if [[ ! -f "${SB_KEY_DIR}/${key}.key" ]] || [[ ! -f "${SB_KEY_DIR}/${key}.crt" ]]; then
log_error "Failed to generate ${key} key"
return 1
fi
done
log_info "Secure Boot keys generated successfully"
return 0
}
# Create EFI Signature List (ESL) from certificate
# Args: $1 = key name (PK, KEK, or db)
sb_create_esl() {
local key_name="$1"
local crt_file="${SB_KEY_DIR}/${key_name}.crt"
local esl_file="${SB_KEY_DIR}/${key_name}.esl"
if [[ ! -f "$crt_file" ]]; then
log_error "Certificate not found: $crt_file"
return 1
fi
log_info "Creating ESL for ${key_name}..."
cert-to-efi-sig-list -g "$(uuidgen)" "$crt_file" "$esl_file"
if [[ ! -f "$esl_file" ]]; then
log_error "Failed to create ESL for ${key_name}"
return 1
fi
return 0
}
# Sign ESL file to create .auth file for UEFI enrollment
# Args: $1 = key name to sign, $2 = signing key name
sb_sign_esl() {
local target_key="$1"
local signing_key="$2"
local esl_file="${SB_KEY_DIR}/${target_key}.esl"
local auth_file="${SB_KEY_DIR}/${target_key}.auth"
local sign_key="${SB_KEY_DIR}/${signing_key}.key"
local sign_crt="${SB_KEY_DIR}/${signing_key}.crt"
if [[ ! -f "$esl_file" ]]; then
log_error "ESL file not found: $esl_file"
return 1
fi
if [[ ! -f "$sign_key" ]] || [[ ! -f "$sign_crt" ]]; then
log_error "Signing key not found: ${signing_key}"
return 1
fi
log_info "Signing ${target_key}.esl with ${signing_key}..."
sign-efi-sig-list -t "$(date +'%Y-%m-%d %H:%M:%S')" \
-k "$sign_key" -c "$sign_crt" \
"$target_key" "$esl_file" "$auth_file"
if [[ ! -f "$auth_file" ]]; then
log_error "Failed to create auth file for ${target_key}"
return 1
fi
return 0
}
# Build Unified Kernel Image (UKI)
# Args: $1 = output directory containing chroot/binary
uki_build() {
local build_dir="$1"
local kernel_version
local uki_output="${build_dir}/binary/boot/efi/EFI/BOOT/BOOTX64.EFI"
log_info "Building Unified Kernel Image (UKI)..."
# Find kernel version
kernel_version=$(find "${build_dir}/chroot/boot" -maxdepth 1 -name 'vmlinuz-*' -print -quit 2>/dev/null | sed 's/.*vmlinuz-//')
if [[ -z "$kernel_version" ]]; then
log_error "Kernel not found in chroot"
return 1
fi
log_info "Kernel version: ${kernel_version}"
local kernel="${build_dir}/chroot/boot/vmlinuz-${kernel_version}"
local initrd="${build_dir}/chroot/boot/initrd.img-${kernel_version}"
if [[ ! -f "$kernel" ]]; then
log_error "Kernel not found: $kernel"
return 1
fi
if [[ ! -f "$initrd" ]]; then
log_error "Initrd not found: $initrd"
return 1
fi
# Create output directory
mkdir -p "$(dirname "$uki_output")"
# Build UKI using ukify (systemd) or manual objcopy
if command -v ukify &>/dev/null; then
log_info "Building UKI with ukify..."
ukify build \
--linux "$kernel" \
--initrd "$initrd" \
--cmdline "quiet splash lockdown=confidentiality module.sig_enforce=1" \
--output "$uki_output" \
--efi-arch x64
else
log_info "Building UKI with objcopy (manual method)..."
# Manual UKI construction using objcopy
local stub="${build_dir}/chroot/lib/systemd/boot/efi/linuxx64.efi.stub"
if [[ ! -f "$stub" ]]; then
stub="${build_dir}/chroot/usr/lib/systemd/boot/efi/linuxx64.efi.stub"
fi
if [[ ! -f "$stub" ]]; then
log_error "EFI stub not found - install systemd-boot"
return 1
fi
# Create cmdline file
local cmdline_file="${build_dir}/cmdline.txt"
echo "quiet splash lockdown=confidentiality module.sig_enforce=1" > "$cmdline_file"
# Build UKI with objcopy
objcopy \
--add-section .osrel="${build_dir}/chroot/etc/os-release" --change-section-vma .osrel=0x20000 \
--add-section .cmdline="$cmdline_file" --change-section-vma .cmdline=0x30000 \
--add-section .linux="$kernel" --change-section-vma .linux=0x40000 \
--add-section .initrd="$initrd" --change-section-vma .initrd=0x500000 \
"$stub" "$uki_output"
fi
if [[ ! -f "$uki_output" ]]; then
log_error "Failed to build UKI"
return 1
fi
log_info "UKI built successfully: $uki_output"
return 0
}
# Sign UKI with db key
# Args: $1 = path to UKI file
uki_sign() {
local uki_file="$1"
local db_key="${SB_KEY_DIR}/db.key"
local db_crt="${SB_KEY_DIR}/db.crt"
if [[ ! -f "$uki_file" ]]; then
log_error "UKI not found: $uki_file"
return 1
fi
if [[ ! -f "$db_key" ]] || [[ ! -f "$db_crt" ]]; then
log_error "db key not found - run sb_generate_keys first"
return 1
fi
log_info "Signing UKI with db key..."
sbsign --key "$db_key" --cert "$db_crt" --output "$uki_file" "$uki_file"
# Verify signature
if sbverify "$uki_file" 2>/dev/null | grep -q "Signature verification OK"; then
log_info "UKI signed successfully"
return 0
else
log_warn "UKI signed but verification uncertain"
return 0
fi
}
# Full Secure Boot setup - generate keys, create ESL, sign ESL, prepare for enrollment
secureboot_setup() {
log_info "Setting up Secure Boot..."
# Generate keys
if ! sb_generate_keys; then
log_error "Failed to generate Secure Boot keys"
return 1
fi
# Create ESL files
for key in PK KEK db; do
if ! sb_create_esl "$key"; then
log_error "Failed to create ESL for $key"
return 1
fi
done
# Sign ESL files to create .auth files
# PK is self-signed
if ! sb_sign_esl "PK" "PK"; then
log_error "Failed to sign PK"
return 1
fi
# KEK is signed by PK
if ! sb_sign_esl "KEK" "PK"; then
log_error "Failed to sign KEK"
return 1
fi
# db is signed by KEK
if ! sb_sign_esl "db" "KEK"; then
log_error "Failed to sign db"
return 1
fi
log_info "Secure Boot setup complete"
log_info "Keys location: ${SB_KEY_DIR}"
log_info "Auth files ready for UEFI enrollment:"
ls -la "${SB_KEY_DIR}"/*.auth 2>/dev/null || true
return 0
}
# Get Secure Boot build script for embedding in Docker
# This outputs the Secure Boot functions as a string for embedding in Docker bash -c
get_secureboot_script() {
cat << 'SECUREBOOT_SCRIPT'
# Secure Boot functions for Docker build
sb_docker_setup() {
local key_dir="/tmp/secureboot-keys"
mkdir -p "$key_dir"
echo "[SecureBoot] Generating keys..."
# Generate PK
openssl req -new -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -subj "/CN=KNEL-Football PK/" \
-keyout "${key_dir}/PK.key" \
-out "${key_dir}/PK.crt" 2>/dev/null || return 1
# Generate KEK
openssl req -new -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -subj "/CN=KNEL-Football KEK/" \
-keyout "${key_dir}/KEK.key" \
-out "${key_dir}/KEK.crt" 2>/dev/null || return 1
# Generate db
openssl req -new -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -subj "/CN=KNEL-Football db/" \
-keyout "${key_dir}/db.key" \
-out "${key_dir}/db.crt" 2>/dev/null || return 1
# Create ESL files
echo "[SecureBoot] Creating ESL files..."
for key in PK KEK db; do
cert-to-efi-sig-list -g "$(uuidgen)" \
"${key_dir}/${key}.crt" \
"${key_dir}/${key}.esl" || return 1
done
# Create auth files
echo "[SecureBoot] Creating auth files..."
sign-efi-sig-list -t "$(date +'%Y-%m-%d %H:%M:%S')" \
-k "${key_dir}/PK.key" -c "${key_dir}/PK.crt" \
PK "${key_dir}/PK.esl" "${key_dir}/PK.auth" || return 1
sign-efi-sig-list -t "$(date +'%Y-%m-%d %H:%M:%S')" \
-k "${key_dir}/PK.key" -c "${key_dir}/PK.crt" \
KEK "${key_dir}/KEK.esl" "${key_dir}/KEK.auth" || return 1
sign-efi-sig-list -t "$(date +'%Y-%m-%d %H:%M:%S')" \
-k "${key_dir}/KEK.key" -c "${key_dir}/KEK.crt" \
db "${key_dir}/db.esl" "${key_dir}/db.auth" || return 1
echo "[SecureBoot] Keys generated successfully"
echo "$key_dir"
}
sb_docker_build_uki() {
local build_dir="$1"
local key_dir="$2"
echo "[SecureBoot] Building UKI..."
# Find kernel
local kernel=$(ls "${build_dir}/chroot/boot/vmlinuz-"* 2>/dev/null | head -1)
if [[ -z "$kernel" ]]; then
echo "[SecureBoot] ERROR: Kernel not found"
return 1
fi
local kver=$(echo "$kernel" | sed 's/.*vmlinuz-//')
local initrd="${build_dir}/chroot/boot/initrd.img-${kver}"
if [[ ! -f "$initrd" ]]; then
echo "[SecureBoot] ERROR: Initrd not found"
return 1
fi
# Find EFI stub
local stub="${build_dir}/chroot/usr/lib/systemd/boot/efi/linuxx64.efi.stub"
if [[ ! -f "$stub" ]]; then
stub="${build_dir}/chroot/lib/systemd/boot/efi/linuxx64.efi.stub"
fi
if [[ ! -f "$stub" ]]; then
echo "[SecureBoot] ERROR: EFI stub not found"
return 1
fi
# Create output directory
local uki_dir="${build_dir}/binary/boot/efi/EFI/BOOT"
mkdir -p "$uki_dir"
local uki_file="${uki_dir}/BOOTX64.EFI"
local cmdline="${build_dir}/cmdline.txt"
# Create cmdline
echo "quiet splash lockdown=confidentiality" > "$cmdline"
# Build UKI
echo "[SecureBoot] Bundling kernel+initrd+cmdline..."
objcopy \
--add-section .osrel="${build_dir}/chroot/etc/os-release" --change-section-vma .osrel=0x20000 \
--add-section .cmdline="$cmdline" --change-section-vma .cmdline=0x30000 \
--add-section .linux="$kernel" --change-section-vma .linux=0x40000 \
--add-section .initrd="$initrd" --change-section-vma .initrd=0x500000 \
"$stub" "$uki_file" || return 1
# Sign UKI
echo "[SecureBoot] Signing UKI..."
sbsign --key "${key_dir}/db.key" --cert "${key_dir}/db.crt" \
--output "$uki_file" "$uki_file" || return 1
# Verify
if sbverify "$uki_file" 2>&1 | grep -q "OK"; then
echo "[SecureBoot] UKI signed and verified: $uki_file"
return 0
else
echo "[SecureBoot] WARNING: UKI verification uncertain"
return 0
fi
}
sb_docker_copy_keys_to_binary() {
local key_dir="$1"
local build_dir="$2"
echo "[SecureBoot] Copying keys to ISO for installation..."
# Create directory for keys in the ISO
local keys_dest="${build_dir}/binary/secureboot"
mkdir -p "$keys_dest"
# Copy auth files for enrollment during installation
cp "${key_dir}"/*.auth "$keys_dest/" 2>/dev/null || true
# Copy certificates for verification
cp "${key_dir}"/*.crt "$keys_dest/" 2>/dev/null || true
echo "[SecureBoot] Keys copied to /secureboot/ on ISO"
}
SECUREBOOT_SCRIPT
}
# ============================================================================
# USAGE AND MAIN
# ============================================================================
usage() {
cat <<EOF
KNEL-Football ISO Builder - Main Entry Point
Usage: $0 <command> [args]
Build Commands:
build Build Docker image
iso Build ISO (60-90 minutes)
monitor [secs] Monitor build progress (default: check every 180s)
clean Clean build artifacts
Test Commands:
test Run all tests
test:unit Run unit tests only
test:integration Run integration tests only
test:security Run security tests only
test:system Run system tests only (requires libvirt)
lint Run linting checks (shellcheck)
VM Testing Commands (requires libvirt on host):
test:iso check Check VM testing prerequisites
test:iso create Create and start test VM (UEFI/Secure Boot)
test:iso console Connect to VM console
test:iso status Show VM status
test:iso destroy Destroy VM and cleanup
test:iso boot-test Run automated boot test
test:iso secure-boot Test Secure Boot (manual verification)
test:iso fde-test Test FDE passphrase prompt (manual verification)
Other Commands:
validate Validate built ISO (static analysis + QEMU boot test)
shell Interactive shell in build container
help Show this help message
Prerequisites for VM Testing:
- User must be in libvirt group
- libvirtd service must be running
- OVMF must be installed (sudo apt install ovmf)
- ISO must exist in output/
Examples:
$0 build # Build Docker image
$0 iso # Build ISO (60-90 min)
$0 monitor # Monitor build progress
$0 test # Run all tests
$0 test:iso boot-test # Boot test in VM
$0 test:iso console # Connect to VM console
$0 test:iso destroy # Cleanup test VM
Note: After adding user to libvirt group, logout and login again.
EOF
exit 1
}
# Main entry point
main() {
local command="${1:-help}"
case "${command}" in
build)
echo "Building KNEL-Football Docker image..."
docker build -t "${DOCKER_IMAGE}" "${SCRIPT_DIR}"
;;
test)
echo "Running KNEL-Football test suite (235 tests)..."
docker run --rm \
-v "${SCRIPT_DIR}:/workspace:ro" \
-v "${BUILD_DIR}:/build" \
-e BATS_TMPDIR=/build/tmp \
"${DOCKER_IMAGE}" \
bash -c "cd /workspace && bats tests/simple_test.bats tests/unit/ tests/integration/ tests/security/ tests/system/"
;;
test:unit)
echo "Running unit tests..."
docker run --rm \
-v "${SCRIPT_DIR}:/workspace:ro" \
-v "${BUILD_DIR}:/build" \
-e BATS_TMPDIR=/build/tmp \
"${DOCKER_IMAGE}" \
bash -c "cd /workspace && bats tests/unit/"
;;
test:integration)
echo "Running integration tests..."
docker run --rm \
-v "${SCRIPT_DIR}:/workspace:ro" \
-v "${BUILD_DIR}:/build" \
-e BATS_TMPDIR=/build/tmp \
"${DOCKER_IMAGE}" \
bash -c "cd /workspace && bats tests/integration/"
;;
test:security)
echo "Running security tests..."
docker run --rm \
-v "${SCRIPT_DIR}:/workspace:ro" \
-v "${BUILD_DIR}:/build" \
-e BATS_TMPDIR=/build/tmp \
"${DOCKER_IMAGE}" \
bash -c "cd /workspace && bats tests/security/"
;;
test:system)
echo "Running system tests..."
docker run --rm \
-v "${SCRIPT_DIR}:/workspace:ro" \
-v "${BUILD_DIR}:/build" \
-e BATS_TMPDIR=/build/tmp \
"${DOCKER_IMAGE}" \
bash -c "cd /workspace && bats tests/system/"
;;
lint)
echo "Running linting checks..."
docker run --rm \
-v "${SCRIPT_DIR}:/workspace:ro" \
"${DOCKER_IMAGE}" \
bash -c "find /workspace -name '*.sh' -print0 | xargs -0 shellcheck"
;;
clean)
echo "Cleaning build artifacts..."
rm -rf "${OUTPUT_DIR:?}"/*
rm -rf "${BUILD_DIR:?}"/*
;;
validate)
echo "Running ISO validation..."
"${SCRIPT_DIR}/scripts/validate-iso.sh"
;;
shell)
echo "Starting interactive shell..."
docker run --rm -it \
-v "${SCRIPT_DIR}:/workspace:ro" \
-v "${OUTPUT_DIR}:/output" \
-v "${BUILD_DIR}:/build" \
-u "$(id -u):$(id -g)" \
-e TZ="America/Chicago" \
-e DEBIAN_FRONTEND="noninteractive" \
-e LC_ALL="C" \
"${DOCKER_IMAGE}" \
bash
;;
iso)
log_warn "Host FDE check: SKIPPED (not enforced on this host)"
echo "Building KNEL-Football secure ISO..."
echo "ALL operations run inside Docker container"
echo "Timezone: America/Chicago"
echo "Mandatory: Full disk encryption with LUKS2"
echo "Mandatory: Secure Boot with UKI"
docker run --rm \
--privileged \
--user root \
-v "${SCRIPT_DIR}:/workspace:ro" \
-v "${OUTPUT_DIR}:/output" \
-e TZ="America/Chicago" \
-e DEBIAN_FRONTEND="noninteractive" \
-e LC_ALL="C" \
-e USER_UID="$(id -u)" \
-e USER_GID="$(id -g)" \
"${DOCKER_IMAGE}" \
bash -c '
cd /tmp &&
rm -rf ./* &&
echo "Configuring live-build..." &&
lb config \
--distribution trixie \
--architectures amd64 \
--archive-areas "main contrib non-free" \
--mode debian \
--chroot-filesystem squashfs \
--binary-images iso-hybrid \
--iso-application "KNEL-Football Secure OS" \
--iso-publisher "KNEL-Football Security Team" \
--iso-volume "KNEL-Football Secure" \
--debian-installer netinst \
--debian-installer-gui true \
--source false \
--apt-indices false \
--apt-source-archives false &&
if [ -d /workspace/config ]; then
echo "Applying custom configuration..."
cp -r /workspace/config/* ./config/
fi &&
# Create Secure Boot binary hook inline
echo "Creating Secure Boot hook..." &&
mkdir -p config/hooks/binary &&
cat > config/hooks/binary/0200-secureboot-uki.hook << '\''SECUREBOOT_HOOK'\''
#!/bin/bash
set -e
echo "=========================================="
echo "Secure Boot UKI Build Hook"
echo "=========================================="
# Secure Boot key directory
SB_KEY_DIR="/tmp/secureboot-keys"
mkdir -p "$SB_KEY_DIR"
# Generate Secure Boot keys if not present
if [[ ! -f "$SB_KEY_DIR/db.key" ]]; then
echo "[SB] Generating Platform Key (PK)..."
openssl req -new -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -subj "/CN=KNEL-Football PK/" \
-keyout "$SB_KEY_DIR/PK.key" \
-out "$SB_KEY_DIR/PK.crt" 2>/dev/null
echo "[SB] Generating Key Exchange Key (KEK)..."
openssl req -new -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -subj "/CN=KNEL-Football KEK/" \
-keyout "$SB_KEY_DIR/KEK.key" \
-out "$SB_KEY_DIR/KEK.crt" 2>/dev/null
echo "[SB] Generating Signature Database Key (db)..."
openssl req -new -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -subj "/CN=KNEL-Football db/" \
-keyout "$SB_KEY_DIR/db.key" \
-out "$SB_KEY_DIR/db.crt" 2>/dev/null
# Create ESL files
echo "[SB] Creating EFI Signature Lists..."
for key in PK KEK db; do
cert-to-efi-sig-list -g "$(uuidgen)" \
"$SB_KEY_DIR/${key}.crt" \
"$SB_KEY_DIR/${key}.esl"
done
# Create auth files for UEFI enrollment
echo "[SB] Creating auth files..."
sign-efi-sig-list -t "$(date +'\''%Y-%m-%d %H:%M:%S'\'')" \
-k "$SB_KEY_DIR/PK.key" -c "$SB_KEY_DIR/PK.crt" \
PK "$SB_KEY_DIR/PK.esl" "$SB_KEY_DIR/PK.auth"
sign-efi-sig-list -t "$(date +'\''%Y-%m-%d %H:%M:%S'\'')" \
-k "$SB_KEY_DIR/PK.key" -c "$SB_KEY_DIR/PK.crt" \
KEK "$SB_KEY_DIR/KEK.esl" "$SB_KEY_DIR/KEK.auth"
sign-efi-sig-list -t "$(date +'\''%Y-%m-%d %H:%M:%S'\'')" \
-k "$SB_KEY_DIR/KEK.key" -c "$SB_KEY_DIR/KEK.crt" \
db "$SB_KEY_DIR/db.esl" "$SB_KEY_DIR/db.auth"
echo "[SB] Keys generated successfully"
fi
# Build UKI (Unified Kernel Image)
echo "[SB] Building Unified Kernel Image..."
# Find kernel in chroot
KERNEL=$(ls chroot/boot/vmlinuz-* 2>/dev/null | head -1)
if [[ -z "$KERNEL" ]]; then
echo "[SB] ERROR: No kernel found in chroot"
exit 1
fi
KVER=$(echo "$KERNEL" | sed '\''s/.*vmlinuz-//'\'')
INITRD="chroot/boot/initrd.img-${KVER}"
echo "[SB] Kernel: $KERNEL"
echo "[SB] Version: $KVER"
if [[ ! -f "$INITRD" ]]; then
echo "[SB] ERROR: Initrd not found: $INITRD"
exit 1
fi
# Find EFI stub
STUB=""
for stub_path in chroot/usr/lib/systemd/boot/efi/linuxx64.efi.stub \
chroot/lib/systemd/boot/efi/linuxx64.efi.stub; do
if [[ -f "$stub_path" ]]; then
STUB="$stub_path"
break
fi
done
if [[ -z "$STUB" ]]; then
echo "[SB] ERROR: EFI stub not found - need systemd-boot package"
exit 1
fi
echo "[SB] EFI Stub: $STUB"
# Create UKI output directory
UKI_DIR="binary/boot/efi/EFI/BOOT"
mkdir -p "$UKI_DIR"
UKI_FILE="${UKI_DIR}/BOOTX64.EFI"
CMDLINE_FILE="/tmp/cmdline.txt"
# Kernel command line with lockdown mode
echo "quiet splash lockdown=confidentiality module.sig_enforce=1" > "$CMDLINE_FILE"
# Build UKI using objcopy
echo "[SB] Bundling kernel + initramfs + cmdline into UKI..."
objcopy \
--add-section .osrel="chroot/etc/os-release" --change-section-vma .osrel=0x20000 \
--add-section .cmdline="$CMDLINE_FILE" --change-section-vma .cmdline=0x30000 \
--add-section .linux="$KERNEL" --change-section-vma .linux=0x40000 \
--add-section .initrd="$INITRD" --change-section-vma .initrd=0x500000 \
"$STUB" "$UKI_FILE"
echo "[SB] UKI created: $UKI_FILE"
# Sign the UKI
echo "[SB] Signing UKI with db key..."
sbsign --key "$SB_KEY_DIR/db.key" --cert "$SB_KEY_DIR/db.crt" \
--output "$UKI_FILE" "$UKI_FILE"
# Verify signature
echo "[SB] Verifying UKI signature..."
if sbverify "$UKI_FILE" 2>&1 | grep -q "Signature verification"; then
echo "[SB] UKI signature verified successfully"
else
echo "[SB] WARNING: UKI signature verification uncertain"
fi
# Copy keys to ISO for installation enrollment
echo "[SB] Copying keys to ISO..."
KEYS_DEST="binary/secureboot"
mkdir -p "$KEYS_DEST"
cp "$SB_KEY_DIR"/*.auth "$KEYS_DEST/" 2>/dev/null || true
cp "$SB_KEY_DIR"/*.crt "$KEYS_DEST/" 2>/dev/null || true
# Create key enrollment README
cat > "$KEYS_DEST/README.txt" << '\''README'\''
KNEL-Football Secure Boot Keys
==============================
These files are used to enroll Secure Boot keys on the target system.
During installation, the following keys must be enrolled in UEFI firmware:
1. PK.auth - Platform Key (root of trust)
2. KEK.auth - Key Exchange Key
3. db.auth - Signature Database (signs the UKI)
Enrollment Methods:
- Use KeyTool EFI application
- Use efivar from Linux: efi-updatevar -f db.auth db
- BIOS/UEFI setup utility (manufacturer dependent)
SECUREBOOT_HOOK
chmod +x config/hooks/binary/0200-secureboot-uki.hook &&
echo "Starting ISO build..." &&
timeout 3600 lb build &&
ISO_FILE=$(find . -name "*.iso" -type f | head -1) &&
if [ -n "$ISO_FILE" ]; then
echo "ISO created: $ISO_FILE"
sha256sum "$ISO_FILE" > "${ISO_FILE}.sha256"
md5sum "$ISO_FILE" > "${ISO_FILE}.md5"
FINAL_ISO="knel-football-secure.iso"
mv "$ISO_FILE" "$FINAL_ISO"
mv "${ISO_FILE}.sha256" "${FINAL_ISO}.sha256"
mv "${ISO_FILE}.md5" "${FINAL_ISO}.md5"
USER_UID=${USER_UID:-1000}
USER_GID=${USER_GID:-1000}
chown "$USER_UID:$USER_GID" "$FINAL_ISO" "${FINAL_ISO}.sha256" "${FINAL_ISO}.md5"
cp "$FINAL_ISO" "${FINAL_ISO}.sha256" "${FINAL_ISO}.md5" /output/
chown "$USER_UID:$USER_GID" /output/"$FINAL_ISO" /output/"${FINAL_ISO}.sha256" /output/"${FINAL_ISO}.md5"
echo "ISO build completed"
echo "=========================================="
echo "Secure Boot: ENABLED"
echo "UKI: SIGNED"
echo "Keys: /secureboot/ on ISO"
echo "=========================================="
ls -lh /output/
else
echo "ISO build failed"
exit 1
fi
' 2>&1 | tee "$BUILD_LOG"
;;
monitor)
monitor_build "${2:-180}"
;;
test:iso)
shift # Remove 'test:iso' from args
local subcmd="${1:-help}"
case "$subcmd" in
check)
vm_check_prerequisites
;;
create)
vm_check_prerequisites && vm_create
;;
console)
vm_console
;;
status)
vm_status
;;
destroy)
vm_destroy
;;
boot-test)
vm_boot_test
;;
secure-boot)
vm_test_secure_boot
;;
fde-test)
vm_test_fde
;;
help|*)
echo "VM Testing Commands:"
echo " check Check prerequisites"
echo " create Create and start test VM"
echo " console Connect to VM console"
echo " status Show VM status"
echo " destroy Destroy VM and cleanup"
echo " boot-test Run automated boot test"
echo " secure-boot Test Secure Boot"
echo " fde-test Test FDE passphrase prompt"
;;
esac
;;
help|*)
usage
;;
esac
}
main "$@"