Files
CISS.debian.live.builder/config/hooks/live/zzzz_ciss_uki_build.hook.binary
T
msw e42fdff89b
🛡️ Retrieve DNSSEC status of coresecret.dev. / 🛡️ Retrieve DNSSEC status of coresecret.dev. (push) Has been cancelled
🛡️ Shell Script Linting / 🛡️ Shell Script Linting (push) Has been cancelled
💙 Generating a PUBLIC Live ISO. / 💙 Generating a PUBLIC Live ISO. (push) Has been cancelled
🔐 Generating a Private Live ISO TRIXIE. / 🔐 Generating a Private Live ISO TRIXIE. (push) Has been cancelled
V9.14.016.2026.06.06
Signed-off-by: Marc S. Weidner <msw@coresecret.dev>
2026-06-06 14:39:12 +01:00

397 lines
13 KiB
Bash

#!/bin/bash
# SPDX-Version: 3.0
# SPDX-CreationInfo: 2026-06-04; WEIDNER, Marc S.; <msw@coresecret.dev>
# 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.; <msw@coresecret.dev>
# 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-<version>: '${kernel_path}'."
[[ -n "${initrd_version}" && "${initrd_base}" != "${initrd_version}" ]] || die "Initrd artifact name does not match initrd.img-<version>: '${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