#!/bin/bash # SPDX-Version: 3.0 # SPDX-CreationInfo: 2026-06-04; WEIDNER, Marc S.; # SPDX-ExternalRef: GIT https://git.coresecret.dev/msw/CISS.debian.live.builder.git # SPDX-FileContributor: WEIDNER, Marc S.; Centurion Intelligence Consulting Agency # SPDX-FileCopyrightText: 2024-2026; WEIDNER, Marc S.; # SPDX-FileType: SOURCE # SPDX-License-Identifier: LicenseRef-CNCL-1.1 OR LicenseRef-CCLA-1.1 # SPDX-LicenseComment: This file is part of the CISS.debian.installer.secure framework. # SPDX-PackageName: CISS.debian.live.builder # SPDX-Security-Contact: security@coresecret.eu # shellcheck disable=SC2312 set -Ceuo pipefail # Final live-build binary hook for the CISS UKI build. When the ciss-uki Secure Boot profile is active, this hook selects the # complete kernel/initrd pair, reads the live kernel command line, optionally embeds separate early microcode, creates unsigned # and signed Unified Kernel Images with ukify, verifies the signed UKI with 'sbverify', writes a manifest, and refuses private # Secure Boot key material in build artifact paths. ####################################### # Prints a fatal error message and terminates the hook. # Globals: # None # Arguments: # 1: Error message # Returns: # 42: always exits with failure ####################################### die() { declare message="${1}" printf "\e[91m❌ %s \e[0m\n" "${message}" >&2 exit 42 } ####################################### # Checks whether a required command exists. # Globals: # None # Arguments: # 1: Command name # Returns: # 0: on success # 42: if the command is missing ####################################### require_command() { declare command_name="${1}" command -v "${command_name}" >/dev/null 2>&1 || die "Required command not found: '${command_name}'." return 0 } ####################################### # Checks whether a required file exists. # Globals: # None # Arguments: # 1: File path # 2: Human-readable file description # Returns: # 0: on success # 42: if the file is missing ####################################### require_file() { declare file_path="${1}" declare description="${2}" [[ -f "${file_path}" ]] || die "Missing ${description}: '${file_path}'." return 0 } ####################################### # Reads the single LB_BOOTAPPEND_LIVE value from a live-build binary configuration file. # Globals: # None # Arguments: # 1: live-build binary configuration file # 2: Output variable name for the kernel command line # Returns: # 0: on success # 42: if the file is missing, the entry is ambiguous, or the value is empty ####################################### read_bootappend_live() { declare config_file="${1}" declare output_var="${2}" declare -a matches=() declare value="" require_file "${config_file}" "live-build binary configuration" mapfile -t matches < <(grep -E '^LB_BOOTAPPEND_LIVE=' "${config_file}" || true) if (( ${#matches[@]} != 1 )); then die "Expected exactly one LB_BOOTAPPEND_LIVE entry in '${config_file}', found '${#matches[@]}'." fi value="${matches[0]#LB_BOOTAPPEND_LIVE=}" if [[ "${value}" == \"*\" ]]; then value="${value#\"}" value="${value%\"}" fi [[ -n "${value}" ]] || die "LB_BOOTAPPEND_LIVE in '${config_file}' is empty." printf -v "${output_var}" "%s" "${value}" return 0 } ####################################### # Collects kernel and initrd candidates from one artifact directory. # Globals: # None # Arguments: # 1: Artifact directory # 2: Output variable name for the selected kernel path # 3: Output variable name for the selected initrd path # Returns: # 0: on success, including when the directory does not exist # 42: if more than one kernel or initrd candidate exists ####################################### collect_artifacts_from_dir() { declare artifact_dir="${1}" declare kernel_output_var="${2}" declare initrd_output_var="${3}" declare -a kernels=() declare -a initrds=() if [[ ! -d "${artifact_dir}" ]]; then printf -v "${kernel_output_var}" "%s" "" printf -v "${initrd_output_var}" "%s" "" return 0 fi mapfile -d '' -t kernels < <(find "${artifact_dir}" -maxdepth 1 -type f -name "vmlinuz-*" -print0 | LC_ALL=C sort -z) mapfile -d '' -t initrds < <(find "${artifact_dir}" -maxdepth 1 -type f -name "initrd.img-*" -print0 | LC_ALL=C sort -z) if (( ${#kernels[@]} > 1 )); then die "Ambiguous kernel candidates in '${artifact_dir}'. Refusing to select automatically." fi if (( ${#initrds[@]} > 1 )); then die "Ambiguous initrd candidates in '${artifact_dir}'. Refusing to select automatically." fi printf -v "${kernel_output_var}" "%s" "${kernels[0]:-}" printf -v "${initrd_output_var}" "%s" "${initrds[0]:-}" return 0 } ####################################### # Selects the kernel/initrd pair used to build the UKI. # Globals: # None # Arguments: # 1: Output variable name for the selected kernel path # 2: Output variable name for the selected initrd path # Returns: # 0: on success # 42: if no complete pair exists, the final pair is incomplete, or candidates are ambiguous ####################################### select_kernel_initrd_pair() { declare kernel_output_var="$1" declare initrd_output_var="$2" declare binary_kernel="" declare binary_initrd="" declare fallback_kernel="" declare fallback_initrd="" collect_artifacts_from_dir "binary/live" binary_kernel binary_initrd if [[ -n "${binary_kernel}" && -n "${binary_initrd}" ]]; then printf "\e[92m✅ Using final binary/live kernel and initrd artifacts. \e[0m\n" printf -v "${kernel_output_var}" "%s" "${binary_kernel}" printf -v "${initrd_output_var}" "%s" "${binary_initrd}" return 0 fi if [[ -n "${binary_kernel}" || -n "${binary_initrd}" ]]; then die "Incomplete binary/live kernel/initrd pair. Refusing to mix final and fallback artifacts." fi printf "\e[93m❌ No complete binary/live kernel/initrd pair found; checking chroot/boot fallback. \e[0m\n" collect_artifacts_from_dir "chroot/boot" fallback_kernel fallback_initrd if [[ -n "${fallback_kernel}" && -n "${fallback_initrd}" ]]; then printf "\e[93m❌ Using chroot/boot fallback artifacts because binary/live has no complete pair. \e[0m\n" printf -v "${kernel_output_var}" "%s" "${fallback_kernel}" printf -v "${initrd_output_var}" "%s" "${fallback_initrd}" return 0 fi die "No complete kernel/initrd pair found in binary/live or chroot/boot." } ####################################### # Finds an optional separate early microcode cpio next to the selected initrd. # Globals: # None # Arguments: # 1: Artifact directory # 2: Output variable name for the selected microcode cpio path # Returns: # 0: on success, including when no separate microcode cpio exists # 42: if more than one separate microcode cpio candidate exists ####################################### collect_optional_microcode() { declare artifact_dir="${1}" declare output_var="${2}" declare -a microcode_candidates=() mapfile -d '' -t microcode_candidates < <( find "${artifact_dir}" -maxdepth 1 -type f \( -name "*microcode*.cpio" -o -name "*ucode*.cpio" \) -print0 | LC_ALL=C sort -z ) if (( ${#microcode_candidates[@]} > 1 )); then die "Ambiguous separate early microcode cpio candidates in '${artifact_dir}'. Refusing to select automatically." fi printf -v "${output_var}" "%s" "${microcode_candidates[0]:-}" return 0 } ####################################### # Refuses private Secure Boot key material in generated artifact paths. # Globals: # None # Arguments: # None # Returns: # 0: on success # 42: if a private Secure Boot key is found below a guarded path ####################################### guard_private_key_leaks() { declare -a guard_roots=(binary chroot config/includes.binary config/includes.chroot config/includes.installer) declare guard_root="" declare private_file="" for guard_root in "${guard_roots[@]}"; do if [[ ! -d "${guard_root}" ]]; then continue fi while IFS= read -r -d '' private_file; do die "Refusing private Secure Boot key inside build artifact path: '${private_file}'." done < <(find "${guard_root}" -xdev -type f \( -name "ciss-efi-image.key" -o -name "ciss-module-signing.key" \) -print0) done return 0 } ####################################### # Builds unsigned and signed CISS UKIs for the ciss-uki Secure Boot profile. # Globals: # PWD # VAR_CISS_SECUREBOOT_DIR # VAR_CISS_SECUREBOOT_EFI_CERT # VAR_CISS_SECUREBOOT_EFI_KEY # VAR_CISS_SECUREBOOT_PROFILE # VAR_HANDLER_BUILD_DIR # VAR_WORKDIR # Arguments: # None # Returns: # 0: on success or when the active Secure Boot profile does not require a CISS UKI # 42: on validation, artifact selection, UKI build, signing, or verification failure ####################################### main() { declare profile="${VAR_CISS_SECUREBOOT_PROFILE:-debian-shim}" declare build_dir="${VAR_HANDLER_BUILD_DIR:-${PWD}}" declare secureboot_dir="${VAR_CISS_SECUREBOOT_DIR:-${VAR_WORKDIR:-${build_dir}}/ciss.secureboot}" declare secureboot_key="${VAR_CISS_SECUREBOOT_EFI_KEY:-${secureboot_dir}/private/ciss-efi-image.key}" declare secureboot_cert="${VAR_CISS_SECUREBOOT_EFI_CERT:-${secureboot_dir}/public/ciss-efi-image.crt}" declare stub="/usr/lib/systemd/boot/efi/linuxx64.efi.stub" declare os_release="chroot/usr/lib/os-release" declare kernel_path="" declare initrd_path="" declare kernel_base="" declare initrd_base="" declare kernel_version="" declare initrd_version="" declare cmdline="" declare microcode_initrd="" declare output_root="" declare uki_dir="" declare manifest_dir="" declare unsigned_uki="" declare signed_uki="" declare manifest="" declare -a ukify_args=() if [[ "${profile}" != "ciss-uki" ]]; then printf "\e[92m✅ Secure Boot profile '%s'; skipping CISS UKI build. \e[0m\n" "${profile}" return 0 fi printf "\e[95m🧪 Building CISS Secure Boot UKI ... \e[0m\n" cd "${build_dir}" require_command ukify require_command sbverify require_command sha512sum require_file "${stub}" "systemd EFI stub" require_file "${secureboot_key}" "CISS EFI image signing key" require_file "${secureboot_cert}" "CISS EFI image signing certificate" require_file "${os_release}" "target os-release metadata" guard_private_key_leaks select_kernel_initrd_pair kernel_path initrd_path kernel_base="${kernel_path##*/}" initrd_base="${initrd_path##*/}" kernel_version="${kernel_base#vmlinuz-}" initrd_version="${initrd_base#initrd.img-}" [[ -n "${kernel_version}" && "${kernel_base}" != "${kernel_version}" ]] || die "Kernel artifact name does not match vmlinuz-: '${kernel_path}'." [[ -n "${initrd_version}" && "${initrd_base}" != "${initrd_version}" ]] || die "Initrd artifact name does not match initrd.img-: '${initrd_path}'." if [[ "${kernel_version}" != "${initrd_version}" ]]; then die "Kernel/initrd version mismatch: kernel='${kernel_version}', initrd='${initrd_version}'." fi read_bootappend_live "config/binary" cmdline collect_optional_microcode "${initrd_path%/*}" microcode_initrd output_root="${build_dir}/ciss.secureboot" uki_dir="${output_root}/uki" manifest_dir="${output_root}/manifests" unsigned_uki="${uki_dir}/CISS-LIVE-${kernel_version}.unsigned.efi" signed_uki="${uki_dir}/CISS-LIVE-${kernel_version}.signed.efi" manifest="${manifest_dir}/CISS-LIVE-${kernel_version}.uki-build.txt" install -d -m 0755 "${uki_dir}" "${manifest_dir}" rm -f -- "${unsigned_uki}" "${signed_uki}" "${manifest}" ukify_args=( build --stub="${stub}" --linux="${kernel_path}" --cmdline="${cmdline}" --os-release="@${os_release}" --uname="${kernel_version}" ) if [[ -n "${microcode_initrd}" ]]; then printf "\e[92m✅ Embedding separate early microcode cpio before normal initrd: '%s'. \e[0m\n" "${microcode_initrd}" ukify_args+=(--initrd="${microcode_initrd}") else printf "\e[92m✅ No separate early microcode cpio found; using normal initrd only. \e[0m\n" fi ukify_args+=(--initrd="${initrd_path}") printf "\e[95m🧪 Creating unsigned UKI: '%s'. \e[0m\n" "${unsigned_uki}" ukify "${ukify_args[@]}" --output="${unsigned_uki}" printf "\e[95m🧪 Creating signed UKI: '%s'. \e[0m\n" "${signed_uki}" ukify "${ukify_args[@]}" \ --secureboot-private-key="${secureboot_key}" \ --secureboot-certificate="${secureboot_cert}" \ --output="${signed_uki}" require_file "${unsigned_uki}" "unsigned CISS UKI" require_file "${signed_uki}" "signed CISS UKI" { printf "CISS Secure Boot UKI build manifest\n" printf "Kernel: %s\n" "${kernel_path}" printf "Initrd: %s\n" "${initrd_path}" printf "Microcode initrd: %s\n" "${microcode_initrd:-none}" printf "Uname: %s\n" "${kernel_version}" printf "OS release: %s\n" "${os_release}" printf "Command line: %s\n" "${cmdline}" printf "\nSHA512:\n" sha512sum "${unsigned_uki}" "${signed_uki}" printf "\nukify inspect:\n" ukify inspect "${signed_uki}" printf "\nsbverify:\n" sbverify --cert "${secureboot_cert}" "${signed_uki}" } >| "${manifest}" 2>&1 printf "\e[92m✅ UKI inspection and signature verification written to '%s'. \e[0m\n" "${manifest}" printf "\e[92m✅ CISS Secure Boot UKI build completed. \e[0m\n" return 0 } main "$@" exit 0 # vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh