#!/bin/bash # SPDX-Version: 3.0 # SPDX-CreationInfo: 2026-06-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-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 guard_sourcing || return "${ERR_GUARD_SRCE}" if ! declare -p _ARY_SECRET_REDACTION_VALUES >/dev/null 2>&1; then declare -ga _ARY_SECRET_REDACTION_VALUES=() fi ####################################### # Runs GNU stat on Debian and gstat on macOS development hosts. # Arguments: # stat arguments # Returns: # stat exit status ####################################### secure_stat() { if command -v gstat >/dev/null 2>&1; then gstat "$@" else stat "$@" fi } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f secure_stat ####################################### # Prints a generic secret validation error without disclosing a path or value. # Arguments: # 1: unsafe input class # 2: quiet flag # Returns: # ERR_SECRET_PATH ####################################### secret_validation_error() { declare error_class="$1" quiet="${2:-false}" if [[ "${quiet}" != "true" ]]; then printf "\e[91m❌ Unsafe secret input rejected: %s. \e[0m\n" "${error_class}" >&2 fi return "${ERR_SECRET_PATH:-216}" } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f secret_validation_error ####################################### # Registers an exact known secret value for final log redaction. # Globals: # _ARY_SECRET_REDACTION_VALUES # Arguments: # 1: secret value # Returns: # 0: on success ####################################### register_secret_value() { declare secret_value="$1" registered_value="" was_traced="false" [[ $- == *x* ]] && was_traced="true" set +x if [[ -n "${secret_value}" ]]; then for registered_value in "${_ARY_SECRET_REDACTION_VALUES[@]}"; do if [[ "${registered_value}" == "${secret_value}" ]]; then [[ "${was_traced}" == "true" ]] && set -x return 0 fi done _ARY_SECRET_REDACTION_VALUES+=("${secret_value}") fi [[ "${was_traced}" == "true" ]] && set -x return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f register_secret_value ####################################### # Registers exact text values from a controlled secret file. # Globals: # _ARY_SECRET_REDACTION_VALUES # Arguments: # 1: secret file # Returns: # 0: on success ####################################### register_secret_file_for_redaction() { declare secret_file="$1" secret_line="" secret_text="" was_traced="false" [[ $- == *x* ]] && was_traced="true" set +x secret_text="$(cat "${secret_file}" || exit $?; printf '.')" || { [[ "${was_traced}" == "true" ]] && set -x return "${ERR_SANITIZING:-133}" } secret_text="${secret_text%.}" register_secret_value "${secret_text}" while IFS= read -r secret_line || [[ -n "${secret_line}" ]]; do register_secret_value "${secret_line}" done < "${secret_file}" [[ "${was_traced}" == "true" ]] && set -x return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f register_secret_file_for_redaction ####################################### # Validates a filename-only secret argument. # Arguments: # 1: filename # 2: input class # Returns: # 0: on success # ERR_SECRET_PATH: on failure ####################################### validate_secret_filename() { declare filename="$1" input_class="${2:-filename-only secret argument}" declare filename_regex='^[A-Za-z0-9._@%+=:,~-]+$' if [[ -z "${filename}" || "${filename}" == "." || "${filename}" == ".." || "${filename}" == */* \ || ! "${filename}" =~ ${filename_regex} ]]; then secret_validation_error "${input_class}" return "${ERR_SECRET_PATH:-216}" fi return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_secret_filename ####################################### # Validates a restrictively permissioned secret directory. # Arguments: # 1: directory path # 2: input class # 3: require tmpfs # 4: quiet flag # Returns: # 0: on success # ERR_SECRET_PATH: on failure ####################################### validate_secret_directory() { declare directory="$1" input_class="${2:-secret directory}" require_tmpfs="${3:-false}" quiet="${4:-false}" declare fs_type="" mode="" owner="" if [[ -z "${directory}" || -L "${directory}" || ! -d "${directory}" ]]; then secret_validation_error "${input_class}" "${quiet}" return "${ERR_SECRET_PATH:-216}" fi owner="$(secure_stat -c '%u' "${directory}" 2>/dev/null)" || { secret_validation_error "${input_class} ownership" "${quiet}" return "${ERR_SECRET_PATH:-216}" } mode="$(secure_stat -c '%a' "${directory}" 2>/dev/null)" || { secret_validation_error "${input_class} permissions" "${quiet}" return "${ERR_SECRET_PATH:-216}" } if [[ "${owner}" != "${EUID}" || "${mode}" != "700" ]]; then secret_validation_error "${input_class} ownership or permissions" "${quiet}" return "${ERR_SECRET_PATH:-216}" fi if [[ "${require_tmpfs}" == "true" ]]; then fs_type="$(secure_stat -f -c '%T' "${directory}" 2>/dev/null)" || { secret_validation_error "${input_class} filesystem" "${quiet}" return "${ERR_SECRET_PATH:-216}" } if [[ "${fs_type}" != "tmpfs" && "${fs_type}" != "ramfs" ]]; then secret_validation_error "${input_class} is not tmpfs-backed" "${quiet}" return "${ERR_SECRET_PATH:-216}" fi fi return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_secret_directory ####################################### # Validates and registers a secret file. # Arguments: # 1: file path # 2: input class # 3: register for redaction # 4: quiet flag # Returns: # 0: on success # ERR_SECRET_PATH: on failure ####################################### validate_secret_file() { declare secret_file="$1" input_class="${2:-secret file}" register_value="${3:-true}" quiet="${4:-false}" declare link_count="" mode="" owner="" if [[ -z "${secret_file}" || -L "${secret_file}" || ! -f "${secret_file}" ]]; then secret_validation_error "${input_class}" "${quiet}" return "${ERR_SECRET_PATH:-216}" fi owner="$(secure_stat -c '%u' "${secret_file}" 2>/dev/null)" || { secret_validation_error "${input_class} ownership" "${quiet}" return "${ERR_SECRET_PATH:-216}" } mode="$(secure_stat -c '%a' "${secret_file}" 2>/dev/null)" || { secret_validation_error "${input_class} permissions" "${quiet}" return "${ERR_SECRET_PATH:-216}" } link_count="$(secure_stat -c '%h' "${secret_file}" 2>/dev/null)" || { secret_validation_error "${input_class} link count" "${quiet}" return "${ERR_SECRET_PATH:-216}" } if [[ "${owner}" != "${EUID}" || "${link_count}" != "1" || ( "${mode}" != "400" && "${mode}" != "600" ) ]]; then secret_validation_error "${input_class} ownership, permissions, or link count" "${quiet}" return "${ERR_SECRET_PATH:-216}" fi if [[ "${register_value}" == "true" ]]; then register_secret_file_for_redaction "${secret_file}" fi return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_secret_file ####################################### # Validates an explicitly supported absolute secret file path. # Arguments: # 1: file path # 2: input class # Returns: # 0: on success # ERR_SECRET_PATH: on failure ####################################### validate_secret_absolute_file() { declare secret_file="$1" input_class="${2:-absolute secret file}" resolved_file="" if [[ -z "${secret_file}" || "${secret_file}" != /* ]]; then secret_validation_error "${input_class} must be an absolute path" return "${ERR_SECRET_PATH:-216}" fi resolved_file="$(realpath "${secret_file}" 2>/dev/null)" || { secret_validation_error "${input_class} cannot be resolved" return "${ERR_SECRET_PATH:-216}" } if [[ "${resolved_file}" != "${secret_file}" ]]; then secret_validation_error "${input_class} must be canonical and must not traverse symlinked parents" return "${ERR_SECRET_PATH:-216}" fi validate_secret_file "${secret_file}" "${input_class}" || return "${ERR_SECRET_PATH:-216}" return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_secret_absolute_file ####################################### # Validates an explicitly supported absolute secret directory path. # Arguments: # 1: directory path # 2: input class # Returns: # 0: on success # ERR_SECRET_PATH: on failure ####################################### validate_secret_absolute_directory() { declare directory="$1" input_class="${2:-absolute secret directory}" resolved_directory="" if [[ -z "${directory}" || "${directory}" != /* ]]; then secret_validation_error "${input_class} must be an absolute path" return "${ERR_SECRET_PATH:-216}" fi resolved_directory="$(realpath "${directory}" 2>/dev/null)" || { secret_validation_error "${input_class} cannot be resolved" return "${ERR_SECRET_PATH:-216}" } if [[ "${resolved_directory}" != "${directory}" ]]; then secret_validation_error "${input_class} must be canonical and must not traverse symlinked parents" return "${ERR_SECRET_PATH:-216}" fi validate_secret_directory "${directory}" "${input_class}" || return "${ERR_SECRET_PATH:-216}" return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_secret_absolute_directory ####################################### # Validates a filename-only secret file below the fixed secret root. # Globals: # VAR_TMP_SECRET # Arguments: # 1: filename # 2: input class # 3: register for redaction # Returns: # 0: on success # ERR_SECRET_PATH: on failure ####################################### validate_secret_file_in_root() { declare filename="$1" input_class="${2:-secret file}" register_value="${3:-true}" validate_secret_directory "${VAR_TMP_SECRET}" "secret root" "true" || return "${ERR_SECRET_PATH:-216}" validate_secret_filename "${filename}" "${input_class} filename" || return "${ERR_SECRET_PATH:-216}" validate_secret_file "${VAR_TMP_SECRET}/${filename}" "${input_class}" "${register_value}" || return "${ERR_SECRET_PATH:-216}" return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_secret_file_in_root ####################################### # Validates the fixed tmpfs secret staging area and all entries below it. # Globals: # VAR_TMP_SECRET # Arguments: # 1: quiet flag # Returns: # 0: on success # ERR_SECRET_PATH: on failure ####################################### validate_secret_staging_area() { declare quiet="${1:-false}" secret_entries_file="" secret_entry="" declare -a secret_entries=() validate_secret_directory "${VAR_TMP_SECRET}" "secret root" "true" "${quiet}" || return "${ERR_SECRET_PATH:-216}" secret_entries_file="$(mktemp)" || return "${ERR_SECRET_PATH:-216}" if ! find "${VAR_TMP_SECRET}" -xdev -mindepth 1 -print0 >| "${secret_entries_file}"; then rm -f "${secret_entries_file}" secret_validation_error "secret-root enumeration failed" "${quiet}" return "${ERR_SECRET_PATH:-216}" fi mapfile -d '' -t secret_entries < "${secret_entries_file}" rm -f "${secret_entries_file}" for secret_entry in "${secret_entries[@]}"; do if [[ -L "${secret_entry}" ]]; then secret_validation_error "symlink below secret root" "${quiet}" return "${ERR_SECRET_PATH:-216}" elif [[ -d "${secret_entry}" ]]; then validate_secret_directory "${secret_entry}" "directory below secret root" "false" "${quiet}" \ || return "${ERR_SECRET_PATH:-216}" elif [[ -f "${secret_entry}" ]]; then validate_secret_file "${secret_entry}" "file below secret root" "true" "${quiet}" || return "${ERR_SECRET_PATH:-216}" else secret_validation_error "non-regular entry below secret root" "${quiet}" return "${ERR_SECRET_PATH:-216}" fi done return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_secret_staging_area # vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh