#!/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 # shellcheck disable=SC2154 guard_sourcing || return "${ERR_GUARD_SRCE}" ####################################### # Print a validation error without echoing secret values. # Globals: # None # Arguments: # 1: Validation label. # 2: Error detail. # Returns: # 0: on success ####################################### secret_validation_error() { declare label="$1" detail="$2" printf "\e[91mERROR: %s: %s\e[0m\n" "${label}" "${detail}" >&2 return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f secret_validation_error ####################################### # Validate a filename-only secret argument. # Globals: # ERR_ARG_MSMTCH # Arguments: # 1: Validation label. # 2: Filename. # Returns: # 0: on success # ERR_ARG_MSMTCH: on failure ####################################### validate_secret_filename() { declare label="$1" filename="$2" declare filename_regex='^[A-Za-z0-9._@%+=:,~-]+$' if [[ -z "${filename}" ]]; then secret_validation_error "${label}" "filename MUST NOT be empty." return "${ERR_ARG_MSMTCH}" fi if [[ "${filename}" == "." || "${filename}" == ".." ]]; then secret_validation_error "${label}" "filename MUST NOT be '.' or '..'." return "${ERR_ARG_MSMTCH}" fi if [[ "${filename}" == -* ]]; then secret_validation_error "${label}" "filename MUST NOT start with '-'." return "${ERR_ARG_MSMTCH}" fi if [[ "${filename}" == */* || "${filename}" == *\\* ]]; then secret_validation_error "${label}" "filename MUST NOT contain path separators." return "${ERR_ARG_MSMTCH}" fi if [[ ! "${filename}" =~ ${filename_regex} ]]; then secret_validation_error "${label}" "filename contains unsupported characters." return "${ERR_ARG_MSMTCH}" fi return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_secret_filename ####################################### # Validate the fixed tmpfs secret root. # Globals: # ERR_INVLD_CHAR # Arguments: # 1: Secret root path. # Returns: # 0: on success # ERR_INVLD_CHAR: on failure ####################################### validate_secret_root() { declare secret_root="$1" root_owner="" root_mode="" root_mode_octal="" root_fs="" if [[ -z "${secret_root}" ]]; then secret_validation_error "secret root" "path MUST NOT be empty." return "${ERR_INVLD_CHAR}" fi if [[ -L "${secret_root}" ]]; then secret_validation_error "secret root" "path MUST NOT be a symlink." return "${ERR_INVLD_CHAR}" fi if [[ ! -d "${secret_root}" ]]; then secret_validation_error "secret root" "path MUST be an existing directory." return "${ERR_INVLD_CHAR}" fi root_owner="$(stat -c '%u:%g' -- "${secret_root}")" if [[ "${root_owner}" != "0:0" ]]; then secret_validation_error "secret root" "directory MUST be owned by root:root." return "${ERR_INVLD_CHAR}" fi root_mode="$(stat -c '%a' -- "${secret_root}")" root_mode_octal=$((8#${root_mode})) if (( (root_mode_octal & 077) != 0 || (root_mode_octal & 0700) != 0700 )); then secret_validation_error "secret root" "directory mode MUST be 0700 or stricter." return "${ERR_INVLD_CHAR}" fi root_fs="$(stat -f -c '%T' -- "${secret_root}")" if [[ "${root_fs}" != "tmpfs" && "${root_fs}" != "ramfs" ]]; then secret_validation_error "secret root" "directory MUST be backed by tmpfs or ramfs." return "${ERR_INVLD_CHAR}" fi # shellcheck disable=SC2312 if find "${secret_root}" -xdev -type l -print -quit | grep -q .; then secret_validation_error "secret root" "secret tree MUST NOT contain symlinks." return "${ERR_INVLD_CHAR}" fi # shellcheck disable=SC2312 if find "${secret_root}" -xdev \( -type b -o -type c -o -type p -o -type s \) -print -quit | grep -q .; then secret_validation_error "secret root" "secret tree MUST NOT contain special files." return "${ERR_INVLD_CHAR}" fi return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_secret_root ####################################### # Normalize ownership and mode of regular files in the secret root. # Globals: # ERR_INVLD_CHAR # Arguments: # 1: Secret root path. # Returns: # 0: on success # ERR_INVLD_CHAR: on failure ####################################### harden_secret_root_files() { declare secret_root="$1" validate_secret_root "${secret_root}" || return "$?" find "${secret_root}" -xdev -type f -exec chown root:root -- {} + find "${secret_root}" -xdev -type f -exec chmod 0400 -- {} + return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f harden_secret_root_files ####################################### # Validate that an existing absolute path is a regular non-symlink file. # Globals: # ERR_INVLD_CHAR # Arguments: # 1: Validation label. # 2: File path. # Returns: # 0: on success # ERR_INVLD_CHAR: on failure ####################################### validate_secret_absolute_file_basics() { declare label="$1" file_path="$2" if [[ -z "${file_path}" ]]; then secret_validation_error "${label}" "file path MUST NOT be empty." return "${ERR_INVLD_CHAR}" fi if [[ "${file_path}" != /* ]]; then secret_validation_error "${label}" "file path MUST be absolute." return "${ERR_INVLD_CHAR}" fi if [[ -L "${file_path}" ]]; then secret_validation_error "${label}" "file MUST NOT be a symlink." return "${ERR_INVLD_CHAR}" fi if [[ ! -f "${file_path}" ]]; then secret_validation_error "${label}" "file MUST be an existing regular file." return "${ERR_INVLD_CHAR}" fi return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_secret_absolute_file_basics ####################################### # Validate a strict secret file. # Globals: # ERR_INVLD_CHAR # Arguments: # 1: Validation label. # 2: File path. # Returns: # 0: on success # ERR_INVLD_CHAR: on failure ####################################### validate_secret_file_path() { declare label="$1" file_path="$2" file_owner="" file_mode="" file_mode_octal="" validate_secret_absolute_file_basics "${label}" "${file_path}" || return "$?" file_owner="$(stat -c '%u:%g' -- "${file_path}")" if [[ "${file_owner}" != "0:0" ]]; then secret_validation_error "${label}" "file MUST be owned by root:root." return "${ERR_INVLD_CHAR}" fi file_mode="$(stat -c '%a' -- "${file_path}")" file_mode_octal=$((8#${file_mode})) if (( (file_mode_octal & 077) != 0 || (file_mode_octal & 0400) != 0400 )); then secret_validation_error "${label}" "file mode MUST allow root read and no group/other access." return "${ERR_INVLD_CHAR}" fi return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_secret_file_path ####################################### # Validate a filename-only secret stored below the fixed secret root. # Globals: # VAR_TMP_SECRET # Arguments: # 1: Validation label. # 2: Filename. # Returns: # 0: on success # Non-zero: on failure ####################################### validate_secret_file_in_root() { declare label="$1" filename="$2" validate_secret_filename "${label}" "${filename}" || return "$?" validate_secret_file_path "${label}" "${VAR_TMP_SECRET}/${filename}" || return "$?" return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_secret_file_in_root ####################################### # Validate a public input file copied into the ISO. # Globals: # ERR_INVLD_CHAR # Arguments: # 1: Validation label. # 2: File path. # Returns: # 0: on success # ERR_INVLD_CHAR: on failure ####################################### validate_public_input_file() { declare label="$1" file_path="$2" file_owner="" file_mode="" file_mode_octal="" validate_secret_absolute_file_basics "${label}" "${file_path}" || return "$?" file_owner="$(stat -c '%u:%g' -- "${file_path}")" if [[ "${file_owner}" != "0:0" ]]; then secret_validation_error "${label}" "file MUST be owned by root:root." return "${ERR_INVLD_CHAR}" fi file_mode="$(stat -c '%a' -- "${file_path}")" file_mode_octal=$((8#${file_mode})) if (( (file_mode_octal & 022) != 0 )); then secret_validation_error "${label}" "file MUST NOT be group- or world-writable." return "${ERR_INVLD_CHAR}" fi return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_public_input_file ####################################### # Validate the authorized_keys directory. # Globals: # ERR_INVLD_CHAR # Arguments: # 1: Directory path. # Returns: # 0: on success # ERR_INVLD_CHAR: on failure ####################################### validate_ssh_pubkey_directory() { declare key_dir="$1" key_file="" dir_owner="" dir_mode="" dir_mode_octal="" if [[ -z "${key_dir}" ]]; then secret_validation_error "--ssh-pubkey" "directory path MUST NOT be empty." return "${ERR_INVLD_CHAR}" fi if [[ "${key_dir}" != /* ]]; then secret_validation_error "--ssh-pubkey" "directory path MUST be absolute." return "${ERR_INVLD_CHAR}" fi if [[ -L "${key_dir}" ]]; then secret_validation_error "--ssh-pubkey" "directory MUST NOT be a symlink." return "${ERR_INVLD_CHAR}" fi if [[ ! -d "${key_dir}" ]]; then secret_validation_error "--ssh-pubkey" "directory MUST exist." return "${ERR_INVLD_CHAR}" fi dir_owner="$(stat -c '%u:%g' -- "${key_dir}")" if [[ "${dir_owner}" != "0:0" ]]; then secret_validation_error "--ssh-pubkey" "directory MUST be owned by root:root." return "${ERR_INVLD_CHAR}" fi dir_mode="$(stat -c '%a' -- "${key_dir}")" dir_mode_octal=$((8#${dir_mode})) if (( (dir_mode_octal & 022) != 0 )); then secret_validation_error "--ssh-pubkey" "directory MUST NOT be group- or world-writable." return "${ERR_INVLD_CHAR}" fi key_file="${key_dir}/authorized_keys" validate_public_input_file "--ssh-pubkey authorized_keys" "${key_file}" || return "$?" return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_ssh_pubkey_directory ####################################### # Validate all selected secret inputs after argument parsing. # Globals: # VAR_AGE # VAR_AGE_KEY # VAR_LUKS # VAR_LUKS_KEY # VAR_SIGNER # VAR_SIGNING_CA # VAR_SIGNING_KEY # VAR_SIGNING_KEY_PASS # VAR_SSHPUBKEY # VAR_TMP_SECRET # Arguments: # None # Returns: # 0: on success # Non-zero: on failure ####################################### validate_selected_secret_inputs() { if [[ "${VAR_AGE,,}" == "true" ]]; then validate_secret_file_in_root "--key_age" "${VAR_AGE_KEY}" || return "$?" fi if [[ "${VAR_LUKS,,}" == "true" ]]; then validate_secret_file_in_root "--key_luks" "${VAR_LUKS_KEY}" || return "$?" fi if [[ "${VAR_SIGNER,,}" == "true" ]]; then validate_secret_file_in_root "--signing_key" "${VAR_SIGNING_KEY}" || return "$?" validate_secret_file_in_root "--signing_key_pass" "${VAR_SIGNING_KEY_PASS}" || return "$?" fi if [[ -n "${VAR_SIGNING_CA:-}" ]]; then validate_secret_file_in_root "--signing_ca" "${VAR_SIGNING_CA}" || return "$?" fi if [[ -n "${VAR_SSHPUBKEY:-}" ]]; then validate_ssh_pubkey_directory "${VAR_SSHPUBKEY}" || return "$?" fi return 0 } ### Prevents accidental 'unset -f'. # shellcheck disable=SC2034 readonly -f validate_selected_secret_inputs # vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh