diff --git a/config/hooks/live/zzzz_ciss_crypt_squash.hook.binary b/config/hooks/live/zzzz_ciss_crypt_squash.hook.binary index be5a0da..6acd38d 100644 --- a/config/hooks/live/zzzz_ciss_crypt_squash.hook.binary +++ b/config/hooks/live/zzzz_ciss_crypt_squash.hook.binary @@ -9,17 +9,90 @@ # 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, formats it with the -# generated build secret, copies the generated filesystem.squashfs into the opened encrypted mapping, then closes the container, -# shreds the temporary LUKS secret, and removes the plaintext SquashFS from the ISO payload. +# 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: @@ -65,8 +138,25 @@ 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}") @@ -126,7 +216,7 @@ fi cryptsetup open --key-file "/proc/$$/fd/${KEYFD}" "${LUKSFS}" crypt_liveiso # shellcheck disable=SC2155 -declare -i LUKS_FREE=$(blockdev --getsize64 /dev/mapper/crypt_liveiso) +declare -i LUKS_FREE=$(blockdev --getsize64 "${MAPPER_DEV}") declare -i SQUASH_FS="${VAR_ROOTFS_SIZE}" if (( LUKS_FREE >= SQUASH_FS )); then @@ -140,8 +230,22 @@ else fi -dd if="${ROOTFS}" of=/dev/mapper/crypt_liveiso bs=8M status=progress conv=fsync +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}<&- diff --git a/config/includes.chroot/usr/lib/live/boot/0042_ciss_post_decrypt_attest b/config/includes.chroot/usr/lib/live/boot/0042_ciss_post_decrypt_attest index 878f1a7..fa3f54f 100644 --- a/config/includes.chroot/usr/lib/live/boot/0042_ciss_post_decrypt_attest +++ b/config/includes.chroot/usr/lib/live/boot/0042_ciss_post_decrypt_attest @@ -15,14 +15,11 @@ # SPDX-Security-Contact: security@coresecret.eu # Module summary: -# - Runs after the encrypted live root filesystem has been decrypted. -# - Requires the pinned public key, attestation hash file, and detached signature to exist as readable, non-empty regular files -# inside the decrypted rootfs. -# - Verifies the attestation signature with gpgv against the pinned key material. -# - Confirms that the signature fingerprint matches the build-time expected rootfs fingerprint and panics on missing, malformed, -# or mismatched evidence. - -_SAVED_SET_OPTS="$(set +o)" +# - Runs after the encrypted live root filesystem has been decrypted and selected for the SquashFS root mount. +# - Requires the pinned public key and the signed decrypted-mapper SHA-512 manifest from the mounted live medium. +# - Verifies the manifest signature and pinned signer fingerprint, then verifies the complete selected decrypted mapper against +# the manifest. +# - Panics on missing, malformed, mismatched, or unverifiable evidence. set -eu @@ -37,9 +34,12 @@ export CDLB_EXP_CA_FPR="@EXP_CA_FPR@" ### Name of the top-level dm-crypt mapping (e.g., cryptsetup --label): zzzz_ciss_crypt_squash.hook.binary ---------------------- export CDLB_MAPPER_NAME="${CDLB_MAPPER_NAME:-crypt_liveiso}" -### Attestation file locations inside decrypted rootfs. ------------------------------------------------------------------------ -CDLB_ATTEST_FPR_SHA="${CDLB_ATTEST_FPR_SHA:-/root/root/.ciss/attestation/${CDLB_EXP_FPR}.gpg.sha512sum.txt}" -CDLB_ATTEST_FPR_SIG="${CDLB_ATTEST_FPR_SIG:-/root/root/.ciss/attestation/${CDLB_EXP_FPR}.gpg.sha512sum.txt.sig}" +### Rootfs selection and attestation file locations. --------------------------------------------------------------------------- +CDLB_LUKS_FS="${CDLB_LUKS_FS:-/live/ciss_rootfs.crypt}" +CDLB_MAPPER_DEV="${CDLB_MAPPER_DEV:-/dev/mapper/${CDLB_MAPPER_NAME}}" +CDLB_MNT_MEDIUM="${CDLB_MNT_MEDIUM:-/run/live/medium}" +CDLB_ATTEST_ROOTFS_SHA="${CDLB_ATTEST_ROOTFS_SHA:-${CDLB_MNT_MEDIUM}${CDLB_LUKS_FS}.decrypted.sha512sum.txt}" +CDLB_ATTEST_ROOTFS_SIG="${CDLB_ATTEST_ROOTFS_SIG:-${CDLB_ATTEST_ROOTFS_SHA}.sig}" CDLB_KEY_DIR="${CDLB_KEY_DIR:-/etc/ciss/keys}" ### Declare functions ---------------------------------------------------------------------------------------------------------- @@ -91,11 +91,13 @@ require_attestation_file() { log_er "0042() : ${artifact_label} is a broken symlink, not a regular file: [${artifact_path}]" panic "0042() : ${artifact_label} is a broken symlink, not a regular file: [${artifact_path}]" + return 1 fi log_er "0042() : ${artifact_label} missing: [${artifact_path}]" panic "0042() : ${artifact_label} missing: [${artifact_path}]" + return 1 fi @@ -103,6 +105,7 @@ require_attestation_file() { log_er "0042() : ${artifact_label} is not a regular file: [${artifact_path}]" panic "0042() : ${artifact_label} is not a regular file: [${artifact_path}]" + return 1 fi @@ -110,6 +113,7 @@ require_attestation_file() { log_er "0042() : ${artifact_label} is empty: [${artifact_path}]" panic "0042() : ${artifact_label} is empty: [${artifact_path}]" + return 1 fi @@ -117,23 +121,56 @@ require_attestation_file() { log_er "0042() : ${artifact_label} is not readable: [${artifact_path}]" panic "0042() : ${artifact_label} is not readable: [${artifact_path}]" + return 1 fi return 0 } -HASH_FILE="${CDLB_ATTEST_FPR_SHA}" -SIGN_FILE="${CDLB_ATTEST_FPR_SIG}" +####################################### +# Validate the selected decrypted rootfs payload. +# Globals: +# None +# Arguments: +# 1: Absolute payload path +# Returns: +# 0: on success +####################################### +require_rootfs_payload() { + payload_path="${1}" + + if [ ! -b "${payload_path}" ]; then + + log_er "0042() : Selected rootfs payload is not a block device: [${payload_path}]" + panic "0042() : Selected rootfs payload is not a block device: [${payload_path}]" + return 1 + + fi + + if [ ! -r "${payload_path}" ]; then + + log_er "0042() : Selected rootfs payload is not readable: [${payload_path}]" + panic "0042() : Selected rootfs payload is not readable: [${payload_path}]" + return 1 + + fi + + return 0 +} + +HASH_FILE="${CDLB_ATTEST_ROOTFS_SHA}" +SIGN_FILE="${CDLB_ATTEST_ROOTFS_SIG}" KEYFILE="${CDLB_KEY_DIR}/${CDLB_EXP_FPR}.gpg" require_attestation_file "Public key" "${KEYFILE}" -require_attestation_file "Attestation data" "${HASH_FILE}" -require_attestation_file "Attestation signature" "${SIGN_FILE}" +require_attestation_file "Rootfs attestation manifest" "${HASH_FILE}" +require_attestation_file "Rootfs attestation signature" "${SIGN_FILE}" +require_rootfs_payload "${CDLB_MAPPER_DEV}" -log_in "0042() : Verifying rootfs attestation with 'gpgv' and inside LUKS encrypted rootfs pinned GPG FPR." +log_in "0042() : Verifying signed rootfs attestation manifest with pinned GPG FPR." -if ! _STATUS="$(/usr/bin/gpgv --keyring "${KEYFILE}" --status-fd 1 "${SIGN_FILE}" "${HASH_FILE}" 2>&1)"; then +if ! _STATUS="$(/usr/bin/gpgv --no-default-keyring --keyring "${KEYFILE}" --status-fd 1 "${SIGN_FILE}" "${HASH_FILE}" 2>&1)"; then log_er "0042() : gpgv verification failed for signature: [${SIGN_FILE}]" @@ -145,6 +182,7 @@ if ! _STATUS="$(/usr/bin/gpgv --keyring "${KEYFILE}" --status-fd 1 "${SIGN_FILE} sleep 8 panic "0042() : gpgv verification failed for signature: [${SIGN_FILE}]" + exit 1 fi @@ -160,10 +198,51 @@ else log_er "0042() : Signature FPR mismatch: got: [${_CDLB_SIG_FILE_FPR}] expected: [${CDLB_EXP_FPR}]" sleep 8 panic "[FATAL] Signature FPR mismatch: got: [${_CDLB_SIG_FILE_FPR}] expected: [${CDLB_EXP_FPR}]." + exit 1 fi -eval "${_SAVED_SET_OPTS}" +_ATTEST_RECORD_COUNT="$(awk 'NF && $1 !~ /^#/ { count++ } END { print count + 0 }' "${HASH_FILE}")" + +if [ "${_ATTEST_RECORD_COUNT}" -ne 1 ]; then + + log_er "0042() : Rootfs attestation manifest must contain exactly one checksum record: [${HASH_FILE}]" + sleep 8 + panic "0042() : Rootfs attestation manifest must contain exactly one checksum record: [${HASH_FILE}]" + exit 1 + +fi + +_ATTESTED_PAYLOAD="$(awk 'NF && $1 !~ /^#/ { print $2; exit }' "${HASH_FILE}")" + +if [ "${_ATTESTED_PAYLOAD}" != "${CDLB_MAPPER_DEV}" ]; then + + log_er "0042() : Rootfs attestation manifest targets [${_ATTESTED_PAYLOAD}], expected selected payload [${CDLB_MAPPER_DEV}]" + sleep 8 + panic "0042() : Rootfs attestation manifest does not target the selected rootfs payload." + exit 1 + +fi + +log_in "0042() : Verifying selected decrypted rootfs mapper content: [${CDLB_MAPPER_DEV}]" + +if ! _CHECKSUM_STATUS="$(cd / && LC_ALL=C /usr/bin/sha512sum -c --strict --quiet "${HASH_FILE}" 2>&1)"; then + + log_er "0042() : Rootfs payload checksum verification failed: [${CDLB_MAPPER_DEV}]" + + if [ -n "${_CHECKSUM_STATUS}" ]; then + + printf '%s\n' "${_CHECKSUM_STATUS}" >&2 + + fi + + sleep 8 + panic "0042() : Rootfs payload checksum verification failed: [${CDLB_MAPPER_DEV}]" + exit 1 + +fi + +log_ok "0042() : Rootfs payload checksum verification successful: [${CDLB_MAPPER_DEV}]" printf "\e[92m[INFO] Successfully applied : [/usr/lib/live/boot/0042_ciss_post_decrypt_attest] \n\e[0m" diff --git a/lib/lib_ciss_upgrades_boot.sh b/lib/lib_ciss_upgrades_boot.sh index d60442e..765d9f4 100644 --- a/lib/lib_ciss_upgrades_boot.sh +++ b/lib/lib_ciss_upgrades_boot.sh @@ -9,12 +9,12 @@ # 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 guard_sourcing || return "${ERR_GUARD_SRCE}" ####################################### # Integrates and generates sha512sum and GPG signatures on CISS specific LIVE boot artifacts: -# - /root/.ciss/attestation/VAR_SIGNING_KEY_FPR.* # - /etc/initramfs-tools/files/unlock_wrapper.sh # - /usr/lib/live/boot/0030-ciss-verify-checksums # Globals: @@ -31,10 +31,7 @@ guard_sourcing || return "${ERR_GUARD_SRCE}" ciss_upgrades_boot() { printf "\e[95m🧪 %s starting ... \e[0m\n" "${BASH_SOURCE[0]}" - gpg --batch --yes --export "${VAR_SIGNING_KEY_FPR}" >| "${VAR_HANDLER_BUILD_DIR}/config/includes.chroot/root/.ciss/attestation/${VAR_SIGNING_KEY_FPR}.gpg" - declare -ar _ary_target=( - "/root/.ciss/attestation/${VAR_SIGNING_KEY_FPR}.gpg" "/etc/initramfs-tools/files/unlock_wrapper.sh" "/usr/lib/live/boot/0030-ciss-verify-checksums" )