#!/bin/bash # SPDX-Version: 3.0 # SPDX-CreationInfo: 2025-10-11; 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-2025; 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=SC2154 set -Ceuo pipefail # Final live-build binary hook for encrypted root filesystem packaging. Preallocate a LUKS2 container, format it with the # generated build secret, copy the generated filesystem.squashfs into the opened encrypted mapping, generate and sign a # SHA-512 attestation manifest for the complete decrypted mapper, then close the container, shred the temporary LUKS secret, # and remove the plaintext SquashFS from the ISO payload. printf "\e[95m🧪 '%s' starting ... \e[0m\n" "${0}" __umask=$(umask) umask 0077 ####################################### # 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 and is readable. # Globals: # None # Arguments: # 1: File path # 2: Human-readable file description # Returns: # 0: on success # 42: if the file is missing or unreadable ####################################### require_file() { declare file_path="${1}" declare description="${2}" [[ -f "${file_path}" && -r "${file_path}" ]] || die "Missing or unreadable ${description}: '${file_path}'." return 0 } ####################################### # Checks whether a required environment variable is non-empty. # Globals: # None # Arguments: # 1: Variable name # Returns: # 0: on success # 42: if the variable is empty or unset ####################################### require_variable() { declare variable_name="${1}" [[ -n "${!variable_name:-}" ]] || die "Required environment variable is empty or unset: '${variable_name}'." return 0 } ####################################### # Pre allocates space for LUKS container. # Globals: # None # Arguments: # 1: LUKS Container # 2: LUKS Container Size # Returns: # 0: on success # 42: on failure ####################################### preallocate() { declare file="$1" size="$2" declare -i blocksize=$((8*1024*1024)) declare -i blockcounter=$(( (size + blocksize - 1) / blocksize )) if fallocate -l "${size}" -- "${file}" 2>/dev/null; then printf "\e[92m✅ [fallocate -l %s -- %s] successful. \e[0m\n" "${size}" "${file}" return 0 else printf "\e[91m❌ [fallocate -l %s -- %s] NOT successful. \e[0m\n" "${size}" "${file}" fi if dd if=/dev/zero of="${file}" bs="${blocksize}" count="${blockcounter}" status=progress conv=fsync; then printf "\e[92m✅ [dd if=/dev/zero of=%s bs=%s count=%s status=progress conv=fsync] successful. \e[0m\n" "${file}" "${blocksize}" "${blockcounter}" return 0 else printf "\e[91m❌ [dd if=/dev/zero of=%s bs=%s count=%s status=progress conv=fsync] NOT successful. \e[0m\n" "${file}" "${blocksize}" "${blockcounter}" return 42 fi } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f preallocate declare ROOTFS="${VAR_HANDLER_BUILD_DIR}/binary/live/filesystem.squashfs" declare LUKSFS="${VAR_HANDLER_BUILD_DIR}/binary/live/ciss_rootfs.crypt" declare MAPPER_DEV="/dev/mapper/crypt_liveiso" declare ROOTFS_ATTESTATION="${LUKSFS}.decrypted.sha512sum.txt" declare ROOTFS_ATTESTATION_SIG="${ROOTFS_ATTESTATION}.sig" declare KEYFD="" require_command gpg require_command gpgv require_command sha512sum require_file "${ROOTFS}" "final SquashFS payload" require_variable VAR_SIGNING_KEY_FPR require_variable VAR_SIGNING_KEY_PASSFILE require_variable VAR_VERIFY_KEYRING require_file "${VAR_SIGNING_KEY_PASSFILE}" "GPG signing passphrase file" require_file "${VAR_VERIFY_KEYRING}" "GPG verification keyring" [[ "${VAR_SIGNER:-false}" == "true" ]] || die "Rootfs attestation requires an enabled artifact signer." rm -f -- "${ROOTFS_ATTESTATION}" "${ROOTFS_ATTESTATION_SIG}" # shellcheck disable=SC2155 declare -i VAR_ROOTFS_SIZE=$(stat -c%s -- "${ROOTFS}") ### Safety margin: # - LUKS2-Header and Metadata # - dm-integrity Overhead (Tags and Journal) # - Filesystem-Slack declare -i OVERHEAD_FIXED=$((64 * 1024 * 1024)) declare -i OVERHEAD_PCT=2 declare -i ALIGN_BYTES=$(( 4096 * 1024 )) declare -i BASE_SIZE=$(( VAR_ROOTFS_SIZE + OVERHEAD_FIXED + (VAR_ROOTFS_SIZE * OVERHEAD_PCT / 100) )) declare -i VAR_LUKSFS_SIZE=$(( ( (BASE_SIZE + ALIGN_BYTES - 1) / ALIGN_BYTES ) * ALIGN_BYTES )) preallocate "${LUKSFS}" "${VAR_LUKSFS_SIZE}" exec {KEYFD}<"${VAR_TMP_SECRET}/luks.txt" if [[ "${VAR_CDLB_INSIDE_RUNNER}" == "false" ]]; then cryptsetup luksFormat \ --batch-mode \ --cipher aes-xts-plain64 \ --integrity hmac-sha512 \ --iter-time 1000 \ --key-file "/proc/$$/fd/${KEYFD}" \ --key-size 512 \ --label crypt_liveiso \ --luks2-keyslots-size 16777216 \ --luks2-metadata-size 4194304 \ --pbkdf argon2id \ --sector-size 4096 \ --type luks2 \ --use-random \ --verbose \ "${LUKSFS}" elif [[ "${VAR_CDLB_INSIDE_RUNNER}" == "true" ]]; then cryptsetup luksFormat \ --batch-mode \ --cipher aes-xts-plain64 \ --iter-time 1000 \ --key-file "/proc/$$/fd/${KEYFD}" \ --key-size 512 \ --label crypt_liveiso \ --luks2-keyslots-size 16777216 \ --luks2-metadata-size 4194304 \ --pbkdf argon2id \ --sector-size 4096 \ --type luks2 \ --use-random \ --verbose \ "${LUKSFS}" fi cryptsetup open --key-file "/proc/$$/fd/${KEYFD}" "${LUKSFS}" crypt_liveiso # shellcheck disable=SC2155 declare -i LUKS_FREE=$(blockdev --getsize64 "${MAPPER_DEV}") declare -i SQUASH_FS="${VAR_ROOTFS_SIZE}" if (( LUKS_FREE >= SQUASH_FS )); then printf "\e[92m✅ LUKS_FREE '%s' >= SQUASH_FS '%s' \e[0m\n" "${LUKS_FREE}" "${SQUASH_FS}" else printf "\e[91m❌ LUKS_FREE '%s' <= SQUASH_FS '%s' \e[0m\n" "${LUKS_FREE}" "${SQUASH_FS}" >&2 exit 42 fi dd if="${ROOTFS}" of="${MAPPER_DEV}" bs=8M status=progress conv=fsync sync # The selected boot root is the complete decrypted mapper. Hashing this exact block payload binds the signed manifest to the # bytes later mounted as SquashFS, including the mapper padding after the SquashFS image. LC_ALL=C sha512sum "${MAPPER_DEV}" >| "${ROOTFS_ATTESTATION}" gpg --batch --yes --pinentry-mode loopback --passphrase-file "${VAR_SIGNING_KEY_PASSFILE}" --local-user "${VAR_SIGNING_KEY_FPR}" \ --detach-sign --output "${ROOTFS_ATTESTATION_SIG}" "${ROOTFS_ATTESTATION}" gpgv --keyring "${VAR_VERIFY_KEYRING}" "${ROOTFS_ATTESTATION_SIG}" "${ROOTFS_ATTESTATION}" (cd / && LC_ALL=C sha512sum -c --strict --quiet "${ROOTFS_ATTESTATION}") chmod 0444 "${ROOTFS_ATTESTATION}" "${ROOTFS_ATTESTATION_SIG}" cryptsetup close crypt_liveiso exec {KEYFD}<&- shred -fzu -n 5 -- "${VAR_TMP_SECRET}/luks.txt" rm -f -- "${ROOTFS}" umask "${__umask}" __umask="" printf "\e[92m✅ '%s' applied successfully. \e[0m\n" "${0}" exit 0 # vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh