#!/bin/sh
# bashsupport disable=BP5007
# shellcheck disable=SC2249
# shellcheck shell=sh

# SPDX-Version: 3.0
# SPDX-CreationInfo: 2025-11-12; 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-2025; 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

# ToDo: Remove Debug

# Module summary:
# - Runs after the encrypted live root filesystem has been decrypted.
# - Requires the pinned public key, external rootfs attestation manifest, and detached signature to exist as readable,
#   non-empty regular files.
# - Verifies the attestation signature with gpgv against the pinned key material and expected signer fingerprint.
# - Verifies the exact final SquashFS byte stream copied into the decrypted LUKS mapper. The signed manifest provides both the
#   SHA-512 digest and the exact byte length; allocation slack after that SquashFS payload is intentionally out of scope.
# - Panics on missing, malformed, unauthentic, or mismatched evidence.

set -eu
PS4='+ 0042(): '
set -x

printf "\e[95m[INFO] Starting             : [/usr/lib/live/boot/0042_ciss_post_decrypt_attest] \n\e[0m"

ciss_debug_countdown=8
while [ "${ciss_debug_countdown}" -gt 0 ]; do

  printf "\e[93m[DEBUG] 0042() countdown    : %s seconds \n\e[0m" "${ciss_debug_countdown}"
  sleep 1
  ciss_debug_countdown=$((ciss_debug_countdown - 1))

done

### Check panic command availability -------------------------------------------------------------------------------------------
if ! command -v panic >/dev/null 2>&1; then

  panic() {
    printf '\e[91m[FATAL] %s \n\e[0m' "${*}" >&2
    exit 1
  }

fi

### Declare variables ----------------------------------------------------------------------------------------------------------

### Will be replaced at build time:
export CDLB_EXP_FPR="@EXP_FPR@"
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}"
export CDLB_MAPPER_DEV="${CDLB_MAPPER_DEV:-/dev/mapper/${CDLB_MAPPER_NAME}}"
export CDLB_MNT_MEDIUM="${CDLB_MNT_MEDIUM:-/run/live/medium}"

### Locations of the attestation file of filesystem.squashfs on the verified live medium. --------------------------------------
CDLB_ROOTFS_ATTEST_MANIFEST="${CDLB_ROOTFS_ATTEST_MANIFEST:-${CDLB_MNT_MEDIUM}/live/filesystem.squashfs.sha512sum.txt}"
CDLB_ROOTFS_ATTEST_SIGNATURE="${CDLB_ROOTFS_ATTEST_SIGNATURE:-${CDLB_ROOTFS_ATTEST_MANIFEST}.sig}"
CDLB_ROOTFS_ATTEST_CHECK="${CDLB_ROOTFS_ATTEST_CHECK:-/run/ciss-rootfs-attestation.sha512sum}"
CDLB_KEY_DIR="${CDLB_KEY_DIR:-/etc/ciss/keys}"

### Declare functions ----------------------------------------------------------------------------------------------------------

#######################################
# Helper for colored text output on stdout.
# Globals:
#   None
# Arguments:
#   *: String to print
#######################################
log_in() { printf '\e[95m[INFO] %s \n\e[0m' "$*"; }

#######################################
# Helper for colored text output on stdout.
# Globals:
#   None
# Arguments:
#   *: String to print
#######################################
log_ok() { printf '\e[92m[INFO] %s \n\e[0m' "$*"; }

#######################################
# Helper for colored text output on stdout.
# Globals:
#   None
# Arguments:
#   *: String to print
#######################################
log_er() { printf '\e[91m[FATAL] %s \n\e[0m' "$*"; }

#######################################
# Validate a boot-time attestation input file.
# Globals:
#   None
# Arguments:
#   1: Human-readable artifact label
#   2: Absolute artifact path
# Returns:
#   0: on success
#######################################
require_attestation_file() {
  artifact_label="${1}"
  artifact_path="${2}"

  if [ ! -e "${artifact_path}" ]; then

    if [ -L "${artifact_path}" ]; then

      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}]"

    fi

    log_er "0042()              : ${artifact_label} missing: [${artifact_path}]"
    panic  "0042()              : ${artifact_label} missing: [${artifact_path}]"

  fi

  if [ -L "${artifact_path}" ] || [ ! -f "${artifact_path}" ]; then

    log_er "0042()              : ${artifact_label} is not a regular file: [${artifact_path}]"
    panic  "0042()              : ${artifact_label} is not a regular file: [${artifact_path}]"

  fi

  if [ ! -s "${artifact_path}" ]; then

    log_er "0042()              : ${artifact_label} is empty: [${artifact_path}]"
    panic  "0042()              : ${artifact_label} is empty: [${artifact_path}]"

  fi

  if [ ! -r "${artifact_path}" ]; then

    log_er "0042()              : ${artifact_label} is not readable: [${artifact_path}]"
    panic  "0042()              : ${artifact_label} is not readable: [${artifact_path}]"

  fi

  return 0
}

#######################################
# Validate the decrypted rootfs payload device.
# Globals:
#   None
# Arguments:
#   1: Absolute payload device path
# Returns:
#   0: on success
#######################################
require_rootfs_payload_device() {
  artifact_path="${1}"

  if [ ! -e "${artifact_path}" ]; then

    log_er "0042()              : Rootfs payload device missing: [${artifact_path}]"
    panic  "0042()              : Rootfs payload device missing: [${artifact_path}]"

  fi

  if [ -L "${artifact_path}" ] || { [ ! -b "${artifact_path}" ] && [ ! -f "${artifact_path}" ]; }; then

    log_er "0042()              : Rootfs payload must be a block device or regular test fixture: [${artifact_path}]"
    panic  "0042()              : Rootfs payload must be a block device or regular test fixture: [${artifact_path}]"

  fi

  if [ ! -r "${artifact_path}" ]; then

    log_er "0042()              : Rootfs payload is not readable: [${artifact_path}]"
    panic  "0042()              : Rootfs payload is not readable: [${artifact_path}]"

  fi

  return 0
}

#######################################
# Stream exactly the attested SquashFS payload bytes from the decrypted mapper.
# Globals:
#   None
# Arguments:
#   1: Payload device or regular test fixture
#   2: Exact payload byte count
# Returns:
#   0: on success
#######################################
stream_rootfs_payload() {
  payload_device="${1}"
  payload_size="${2}"
  block_size=1048576
  full_blocks=$((payload_size / block_size))
  remainder=$((payload_size % block_size))

  if [ "${full_blocks}" -gt 0 ]; then

    dd if="${payload_device}" bs="${block_size}" count="${full_blocks}" 2>/dev/null || return 1

  fi

  if [ "${remainder}" -gt 0 ]; then

    dd if="${payload_device}" bs="${block_size}" skip="${full_blocks}" count=1 2>/dev/null | dd bs=1 count="${remainder}" 2>/dev/null || return 1

  fi

  return 0
}

#######################################
# Verify the attested SquashFS payload hash against the decrypted mapper bytes.
# Globals:
#   CDLB_ROOTFS_ATTEST_CHECK
# Arguments:
#   1: Manifest path
#   2: Payload device path
# Returns:
#   0: on success
#######################################
verify_rootfs_payload() {
  manifest_path="${1}"
  payload_device="${2}"
  payload_size=""
  payload_hash=""

  payload_size="$(awk '/^# Bytes[[:space:]]*:[[:space:]]Final filesystem[.]squashfs[[:space:]]+[0-9]+[[:space:]]*$/ {print $NF; exit}' "${manifest_path}")"
  payload_hash="$(awk '($0 !~ /^#/ && NF >= 2){print $1; exit}' "${manifest_path}")"

  case "${payload_size}" in

    ""|*[!0-9]*)
      log_er "0042()              : Rootfs attestation manifest has invalid payload size."
      panic  "0042()              : Rootfs attestation manifest has invalid payload size."
      ;;

  esac

  if [ "${payload_size}" -le 0 ]; then

    log_er "0042()              : Rootfs attestation manifest has empty payload size."
    panic  "0042()              : Rootfs attestation manifest has empty payload size."

  fi

  case "${payload_hash}" in

    ""|*[!0123456789abcdefABCDEF]*)
      log_er "0042()              : Rootfs attestation manifest has invalid SHA-512 payload hash."
      panic  "0042()              : Rootfs attestation manifest has invalid SHA-512 payload hash."
      ;;

  esac

  if [ "${#payload_hash}" -ne 128 ]; then

    log_er "0042()              : Rootfs attestation manifest SHA-512 payload hash has invalid length."
    panic  "0042()              : Rootfs attestation manifest SHA-512 payload hash has invalid length."

  fi

  if ! printf '%s  -\n' "${payload_hash}" > "${CDLB_ROOTFS_ATTEST_CHECK}"; then

    log_er "0042()              : Failed to prepare transient rootfs payload checksum file."
    panic  "0042()              : Failed to prepare transient rootfs payload checksum file."

  fi

  chmod 0600 "${CDLB_ROOTFS_ATTEST_CHECK}" 2>/dev/null || :

  log_in "0042()               : Verifying exact SquashFS payload bytes from: [${payload_device}]"

  # stream_rootfs_payload may be evaluated in a pipeline here; sha512sum -c is the fail-closed authority for truncated or
  # tampered payload bytes.
  # shellcheck disable=SC2310
  if ! stream_rootfs_payload "${payload_device}" "${payload_size}" | /usr/bin/sha512sum -c "${CDLB_ROOTFS_ATTEST_CHECK}"; then

    log_er "0042()              : Rootfs payload SHA-512 verification failed."
    panic  "0042()              : Rootfs payload SHA-512 verification failed."

  fi

  log_ok "0042()               : Rootfs payload SHA-512 verification successful."

  return 0
}

HASH_FILE="${CDLB_ROOTFS_ATTEST_MANIFEST}"
SIGN_FILE="${CDLB_ROOTFS_ATTEST_SIGNATURE}"
KEYFILE="${CDLB_KEY_DIR}/${CDLB_EXP_FPR}.gpg"

log_in "0042()               : Validating [${KEYFILE}]"
require_attestation_file "Public key"                   "${KEYFILE}"
log_in "0042()               : Validating [${HASH_FILE}]"
require_attestation_file "Rootfs attestation manifest"  "${HASH_FILE}"
log_in "0042()               : Validating [${SIGN_FILE}]"
require_attestation_file "Rootfs attestation signature" "${SIGN_FILE}"
log_in "0042()               : Validating [${CDLB_MAPPER_DEV}]"
require_rootfs_payload_device "${CDLB_MAPPER_DEV}"

log_ok "0042()               : Rootfs attestation inputs are present and readable."

log_in "0042()               : Verifying rootfs attestation manifest with 'gpgv' and pinned GPG FPR."

if ! _STATUS="$(/usr/bin/gpgv --keyring "${KEYFILE}" --status-fd 1 "${SIGN_FILE}" "${HASH_FILE}" 2>&1)"; then

  log_er "0042()              : gpgv verification failed for signature: [${SIGN_FILE}]"

  if [ -n "${_STATUS}" ]; then

    printf '%s\n' "${_STATUS}" >&2

  fi

  sleep 8
  panic  "0042()              : gpgv verification failed for signature: [${SIGN_FILE}]"

fi

_CDLB_SIG_FILE_FPR="$(printf '%s\n' "${_STATUS}" | awk '/^\[GNUPG:\] VALIDSIG /{print $3; exit}')"

### Compare against pinned and expected fingerprint. ---------------------------------------------------------------------------
if [ "${_CDLB_SIG_FILE_FPR}" = "${CDLB_EXP_FPR}" ]; then

  log_ok "0042()               : Signature FPR valid: got: [${_CDLB_SIG_FILE_FPR}] expected: [${CDLB_EXP_FPR}]"

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}]."

fi

verify_rootfs_payload "${HASH_FILE}" "${CDLB_MAPPER_DEV}"

printf "\e[92m[INFO] Successfully applied : [/usr/lib/live/boot/0042_ciss_post_decrypt_attest] \n\e[0m"

# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh
