M-09: SOURCE_DATE_EPOCH set at build start, BUILD-INFO.txt written with build metadata for reproducibility verification. M-10: GPG signing of ISO and SHA256 checksum. Uses persistent key at config/gpg-keys/signing.key if available, otherwise generates ephemeral key per build and exports pubkey alongside artifacts. M-11: Docker base image digest-pinned to sha256:1d3c8111... preventing supply chain tampering with the build environment. H-09: Build cache integrity verification via SHA256 manifest. On cache save, records checksums of all cached files. On restore, verifies each file. Corrupted cache triggers fresh download instead of silent use. Dockerfile: Added sbsigntool, shim-signed, systemd-boot-efi, gpg with version pins for Secure Boot and signing support in build container. Reference: DeepReport-2026-05-08.md findings M-09, M-10, M-11, H-09 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
1456 lines
50 KiB
Bash
Executable File
1456 lines
50 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"
|
|
readonly CACHE_VOLUME="knel-football-cache"
|
|
|
|
# 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="4096"
|
|
readonly VM_CPUS="2"
|
|
readonly VM_DISK_SIZE="10"
|
|
readonly LIBVIRT_URI="qemu:///system"
|
|
|
|
# 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
|
|
}
|
|
|
|
# Setup swtpm for libvirt TPM emulation
|
|
# Returns 0 if TPM is available, 1 if not
|
|
# Uses libvirt's built-in swtpm management which handles the full lifecycle.
|
|
# Requires /var/lib/libvirt/swtpm/ to exist with correct ownership.
|
|
vm_setup_swtpm() {
|
|
# Check if swtpm is installed
|
|
if ! command -v swtpm_setup &> /dev/null; then
|
|
log_warn "swtpm_setup not found - VM will run without TPM"
|
|
return 1
|
|
fi
|
|
|
|
# For system libvirt, check prerequisites
|
|
if [[ "$LIBVIRT_URI" == *"system"* ]]; then
|
|
if [[ ! -d "/var/lib/libvirt/swtpm" ]]; then
|
|
log_warn "/var/lib/libvirt/swtpm/ does not exist"
|
|
log_warn "Fix: sudo mkdir -p /var/lib/libvirt/swtpm && sudo chown libvirt-qemu:libvirt-qemu /var/lib/libvirt/swtpm"
|
|
log_warn "VM will be created WITHOUT TPM"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
log_info "swtpm 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
|
|
|
|
# Use unique paths to avoid stale libvirt-qemu owned files from previous runs
|
|
local vm_iso_path="/tmp/${VM_NAME}-$$.iso"
|
|
local vm_disk_path="/tmp/${VM_NAME}-$$.qcow2"
|
|
|
|
# Copy ISO to user storage
|
|
log_info "Copying ISO to libvirt storage..."
|
|
mkdir -p "$(dirname "$vm_iso_path")"
|
|
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
|
|
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)
|
|
|
|
# Check TPM availability and configure accordingly
|
|
local tpm_section=""
|
|
if vm_setup_swtpm; then
|
|
tpm_section="<tpm model='tpm-crb'><backend type='emulator' version='2.0'/></tpm>"
|
|
log_info "TPM 2.0 emulation enabled"
|
|
else
|
|
tpm_section=""
|
|
log_warn "TPM disabled - Secure Boot and disk encryption will not work"
|
|
log_warn "This is OK for live ISO testing but not for installation"
|
|
fi
|
|
|
|
# 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" \
|
|
-e "s|@TPM_SECTION@|${tpm_section}|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
|
|
# Check if failure was due to swtpm permissions
|
|
if [[ -n "$tpm_section" && "$LIBVIRT_URI" == *"system"* ]]; then
|
|
local vm_uuid
|
|
vm_uuid=$(virsh -c "$LIBVIRT_URI" dominfo "$VM_NAME" 2>/dev/null | grep "UUID:" | awk '{print $2}')
|
|
local swtpm_vm_dir="/var/lib/libvirt/swtpm/${vm_uuid}"
|
|
if [[ -d "$swtpm_vm_dir" ]]; then
|
|
log_error "TPM initialization failed - swtpm permission issue"
|
|
log_error "Libvirt creates per-VM swtpm state dirs as root:root."
|
|
log_error "Permanent fix (run once with sudo):"
|
|
log_error " sudo bash ${SCRIPT_DIR}/scripts/fix-swtpm-permissions.sh"
|
|
log_error "Then retry: ./run.sh test:iso destroy && ./run.sh test:iso create"
|
|
# Undefine so user can retry after fixing
|
|
virsh -c "$LIBVIRT_URI" undefine "$VM_NAME" --nvram 2>/dev/null || true
|
|
fi
|
|
fi
|
|
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"
|
|
log_info "Disk: $vm_disk_path"
|
|
log_info "ISO: $vm_iso_path"
|
|
}
|
|
|
|
# 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
|
|
|
|
# Cleanup all VM files (ISO is preserved in output/)
|
|
rm -f /tmp/${VM_NAME}*.qcow2 /tmp/${VM_NAME}*.iso /tmp/${VM_NAME}*.xml /tmp/${VM_NAME}*.fd 2>/dev/null || true
|
|
|
|
log_info "Cleanup complete (ISO preserved in output/)"
|
|
}
|
|
|
|
# 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}"
|
|
chmod 700 "${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}/"
|
|
chmod 600 "${SB_KEY_DIR}"/*.key 2>/dev/null || true
|
|
return 0
|
|
fi
|
|
|
|
# Generate keys with restricted permissions
|
|
# Note: -nodes is used for build automation. Store keys securely
|
|
# after build completes (e.g., in an HSM or encrypted storage).
|
|
|
|
# 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
|
|
|
|
# Restrict private key permissions
|
|
chmod 600 "${SB_KEY_DIR}"/*.key 2>/dev/null || true
|
|
|
|
# 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_error "UKI signature verification FAILED"
|
|
return 1
|
|
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 module.sig_enforce=1" > "$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] ERROR: UKI signature verification FAILED"
|
|
return 1
|
|
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 production ISO (prompts for credentials during install)
|
|
iso:demo Build demo/CI ISO (hardcoded test credentials, serial console)
|
|
monitor [secs] Monitor build progress (default: check every 180s)
|
|
clean Clean build artifacts
|
|
clean:cache Remove NVMe build cache (force full rebuild)
|
|
cache Show build cache status
|
|
|
|
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 production ISO (prompts for credentials)
|
|
$0 iso:demo # Build demo ISO (hardcoded test credentials)
|
|
$0 monitor # Monitor build progress
|
|
$0 test # Run all tests
|
|
$0 validate # Validate ISO via QEMU boot test
|
|
$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:?}"/*
|
|
;;
|
|
clean:cache)
|
|
echo "Removing NVMe build cache (Docker volume: ${CACHE_VOLUME})..."
|
|
docker volume rm "${CACHE_VOLUME}" 2>/dev/null || echo "Cache volume not found"
|
|
;;
|
|
cache)
|
|
echo "Build cache status (Docker volume: ${CACHE_VOLUME}):"
|
|
if docker volume inspect "${CACHE_VOLUME}" &>/dev/null; then
|
|
docker run --rm -v "${CACHE_VOLUME}:/cache" alpine sh -c 'echo "Size: $(du -sh /cache 2>/dev/null | cut -f1)" && ls -la /cache/'
|
|
else
|
|
echo "No cache volume exists (will be created on next build)"
|
|
fi
|
|
;;
|
|
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|iso:demo)
|
|
# Ignore environment spoofing - force correct mode from command
|
|
if [ "$1" = "iso:demo" ]; then
|
|
KNEL_BUILD_MODE="demo"
|
|
log_info "Build mode: DEMO (hardcoded test credentials, serial console)"
|
|
log_warn "DO NOT deploy demo ISO in production!"
|
|
else
|
|
KNEL_BUILD_MODE="production"
|
|
log_info "Build mode: PRODUCTION (prompts for credentials during install)"
|
|
fi
|
|
if ! check_host_fde; then
|
|
log_error "Host FDE check FAILED - cannot build on unencrypted host"
|
|
log_error "See PRD FR-011: Host FDE is MANDATORY"
|
|
exit 1
|
|
fi
|
|
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 \
|
|
--cap-add SYS_ADMIN --cap-add MKNOD --cap-add NET_ADMIN \
|
|
--cap-add SYS_CHROOT --cap-add SETFCAP \
|
|
--security-opt apparmor=unconfined \
|
|
--user root \
|
|
-v "${SCRIPT_DIR}:/workspace:ro" \
|
|
-v "${OUTPUT_DIR}:/output" \
|
|
-v "${CACHE_VOLUME}:/cache" \
|
|
-e TZ="America/Chicago" \
|
|
-e DEBIAN_FRONTEND="noninteractive" \
|
|
-e LC_ALL="C" \
|
|
-e USER_UID="$(id -u)" \
|
|
-e USER_GID="$(id -g)" \
|
|
-e KNEL_BUILD_MODE="${KNEL_BUILD_MODE}" \
|
|
"${DOCKER_IMAGE}" \
|
|
bash -c '
|
|
cd /tmp &&
|
|
rm -rf ./* &&
|
|
|
|
# M-09: Reproducible build controls
|
|
export SOURCE_DATE_EPOCH=$(date +%s) &&
|
|
export RBUMPKIT_VERBOSE=1 &&
|
|
echo "SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}" &&
|
|
|
|
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 \
|
|
--bootappend-live "boot=live console=ttyS0,115200 console=tty0" \
|
|
--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 &&
|
|
|
|
# Apply build mode overrides
|
|
if [ "${KNEL_BUILD_MODE}" = "demo" ]; then
|
|
echo "Applying DEMO mode overrides..." &&
|
|
if [ -f config/includes.installer/demo.preseed.cfg ]; then
|
|
cp config/includes.installer/demo.preseed.cfg config/includes.installer/preseed.cfg &&
|
|
echo "Demo preseed applied (hardcoded credentials)"
|
|
fi
|
|
fi &&
|
|
|
|
# Restore build cache from NVMe Docker volume
|
|
# Preserves bootstrap + package downloads between builds (~5 min saved)
|
|
if [ -d /cache/bootstrap ]; then
|
|
echo "Restoring build cache from NVMe..." &&
|
|
mkdir -p ./cache &&
|
|
|
|
# H-09: Verify cache integrity before using
|
|
if [ -f /cache/.cache-manifest ]; then
|
|
echo "Verifying cache integrity..." &&
|
|
CACHED_FILES_OK=true &&
|
|
while read -r expected_sha expected_file; do
|
|
if [ -f "/cache/${expected_file}" ]; then
|
|
actual_sha=$(sha256sum "/cache/${expected_file}" 2>/dev/null | cut -d" " -f1) || continue
|
|
if [ "$expected_sha" != "$actual_sha" ]; then
|
|
echo "CACHE INTEGRITY FAILURE: ${expected_file} checksum mismatch" &&
|
|
CACHED_FILES_OK=false
|
|
fi
|
|
fi
|
|
done < <(awk "{print \\$2, \\$3}" /cache/.cache-manifest 2>/dev/null) &&
|
|
if [ "$CACHED_FILES_OK" = "true" ]; then
|
|
echo "Cache integrity verified" &&
|
|
cp -a /cache/* ./cache/
|
|
else
|
|
echo "WARNING: Cache integrity check failed, using fresh download" &&
|
|
rm -rf /cache/*
|
|
fi
|
|
else
|
|
cp -a /cache/* ./cache/
|
|
fi &&
|
|
echo "Cache restored (bootstrap + packages)"
|
|
else
|
|
echo "No build cache found (first build or cache cleared)"
|
|
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
|
|
if [ "${KNEL_BUILD_MODE}" = "demo" ]; then
|
|
echo "boot=live console=ttyS0,115200 console=tty0 lockdown=confidentiality module.sig_enforce=1" > "$CMDLINE_FILE"
|
|
else
|
|
echo "boot=live quiet splash lockdown=confidentiality module.sig_enforce=1" > "$CMDLINE_FILE"
|
|
fi
|
|
|
|
# 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] ERROR: UKI signature verification FAILED"
|
|
exit 1
|
|
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 &&
|
|
|
|
# Save build cache to NVMe Docker volume for next rebuild
|
|
echo "Saving build cache to NVMe..." &&
|
|
mkdir -p /cache &&
|
|
cp -a ./cache/* /cache/ 2>/dev/null || true &&
|
|
echo "Cache saved (bootstrap + packages)" &&
|
|
|
|
ISO_FILE=$(find . -name "*.iso" -type f | head -1) &&
|
|
if [ -n "$ISO_FILE" ]; then
|
|
echo "ISO created: $ISO_FILE"
|
|
FINAL_ISO="knel-football-secure.iso"
|
|
mv "$ISO_FILE" "$FINAL_ISO"
|
|
sha256sum "$FINAL_ISO" > "${FINAL_ISO}.sha256"
|
|
md5sum "$FINAL_ISO" > "${FINAL_ISO}.md5"
|
|
|
|
# M-10: GPG sign the ISO and checksums
|
|
GPG_KEY_DIR="/workspace/config/gpg-keys"
|
|
GPG_SIGNING_KEY="${GPG_KEY_DIR}/signing.key"
|
|
if [ -f "$GPG_SIGNING_KEY" ]; then
|
|
echo "Signing ISO with GPG key..."
|
|
gpg --import "$GPG_SIGNING_KEY" 2>/dev/null || true
|
|
gpg --armor --detach-sign "$FINAL_ISO" 2>/dev/null || echo "WARNING: GPG signing failed"
|
|
gpg --armor --detach-sign "${FINAL_ISO}.sha256" 2>/dev/null || true
|
|
echo "GPG signatures created"
|
|
else
|
|
echo "No GPG signing key found at config/gpg-keys/signing.key"
|
|
echo "Generating ephemeral signing key for this build..."
|
|
gpg --batch --passphrase "" --quick-generate-key "KNEL-Football Build Signing Key" default default 0 2>/dev/null || true
|
|
gpg --armor --detach-sign "$FINAL_ISO" 2>/dev/null || echo "WARNING: GPG signing failed"
|
|
gpg --armor --detach-sign "${FINAL_ISO}.sha256" 2>/dev/null || true
|
|
gpg --armor --export "KNEL-Football Build Signing Key" > "${FINAL_ISO}.pubkey" 2>/dev/null || true
|
|
echo "Ephemeral GPG signatures created"
|
|
fi
|
|
|
|
# H-09: Cache integrity - record SHA256 of cached files
|
|
if [ -d /cache ]; then
|
|
echo "$(date +%s) $(sha256sum /cache/* 2>/dev/null | head -20)" > /cache/.cache-manifest 2>/dev/null || true
|
|
fi
|
|
|
|
# Write build info for reproducibility verification
|
|
cat > /output/BUILD-INFO.txt << BUILDINFO
|
|
build_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
source_date_epoch=${SOURCE_DATE_EPOCH}
|
|
build_mode=${KNEL_BUILD_MODE}
|
|
iso_sha256=$(sha256sum "$FINAL_ISO" | cut -d" " -f1)
|
|
iso_size=$(stat -c%s "$FINAL_ISO")
|
|
docker_image=${DOCKER_IMAGE:-unknown}
|
|
BUILDINFO
|
|
|
|
USER_UID=${USER_UID:-1000}
|
|
USER_GID=${USER_GID:-1000}
|
|
chown "$USER_UID:$USER_GID" "$FINAL_ISO" "${FINAL_ISO}.sha256" "${FINAL_ISO}.md5" ${FINAL_ISO}.sig ${FINAL_ISO}.sha256.sig ${FINAL_ISO}.pubkey /output/BUILD-INFO.txt 2>/dev/null || true
|
|
cp "$FINAL_ISO" "${FINAL_ISO}.sha256" "${FINAL_ISO}.md5" /output/
|
|
cp ${FINAL_ISO}.sig ${FINAL_ISO}.sha256.sig ${FINAL_ISO}.pubkey /output/ 2>/dev/null || true
|
|
cp /output/BUILD-INFO.txt /output/ 2>/dev/null || true
|
|
chown "$USER_UID:$USER_GID" /output/"$FINAL_ISO" /output/"${FINAL_ISO}.sha256" /output/"${FINAL_ISO}.md5" 2>/dev/null || true
|
|
chown "$USER_UID:$USER_GID" /output/${FINAL_ISO}.sig /output/${FINAL_ISO}.sha256.sig /output/${FINAL_ISO}.pubkey /output/BUILD-INFO.txt 2>/dev/null || true
|
|
echo "ISO build completed"
|
|
echo "=========================================="
|
|
echo "Secure Boot: ENABLED"
|
|
echo "UKI: SIGNED"
|
|
echo "Keys: /secureboot/ on ISO"
|
|
echo "GPG Signed: YES"
|
|
echo "Reproducible: SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH}"
|
|
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 "$@"
|