341 lines
13 KiB
Bash
341 lines
13 KiB
Bash
#!/bin/bash
|
|
# SPDX-Version: 3.0
|
|
# SPDX-CreationInfo: 2026-06-11; 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-2026; 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
|
|
|
|
guard_sourcing || return "${ERR_GUARD_SRCE}"
|
|
|
|
#######################################
|
|
# Prints a generic build-directory validation error without disclosing a path.
|
|
# Arguments:
|
|
# 1: unsafe input class
|
|
# 2: quiet flag
|
|
# Returns:
|
|
# ERR_BUILD_PATH
|
|
#######################################
|
|
build_directory_validation_error() {
|
|
declare error_class="$1" quiet="${2:-false}"
|
|
|
|
if [[ "${quiet}" != "true" ]]; then
|
|
printf "\e[91m❌ Unsafe build-directory input rejected: %s. \e[0m\n" "${error_class}" >&2
|
|
fi
|
|
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
}
|
|
### Prevents accidental 'unset -f'.
|
|
# shellcheck disable=SC2034
|
|
readonly -f build_directory_validation_error
|
|
|
|
#######################################
|
|
# Canonicalises an existing path or a path whose parent exists.
|
|
# Arguments:
|
|
# 1: candidate path
|
|
# 2: output variable name
|
|
# Returns:
|
|
# 0: on success
|
|
# ERR_BUILD_PATH: on failure
|
|
#######################################
|
|
canonicalize_build_directory() {
|
|
declare candidate="$1" output_variable="$2" basename="" parent="" resolved_path=""
|
|
|
|
[[ -n "${candidate}" && "${candidate}" == /* ]] || return "${ERR_BUILD_PATH:-217}"
|
|
[[ "${candidate}" == "/" ]] || candidate="${candidate%/}"
|
|
[[ ! -L "${candidate}" ]] || return "${ERR_BUILD_PATH:-217}"
|
|
|
|
if [[ -e "${candidate}" ]]; then
|
|
resolved_path="$(realpath "${candidate}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}"
|
|
else
|
|
basename="${candidate##*/}"
|
|
parent="${candidate%/*}"
|
|
[[ -n "${parent}" ]] || parent="/"
|
|
[[ -n "${basename}" && "${basename}" != "." && "${basename}" != ".." && -d "${parent}" ]] || return "${ERR_BUILD_PATH:-217}"
|
|
resolved_path="$(realpath "${parent}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}"
|
|
resolved_path="${resolved_path%/}/${basename}"
|
|
fi
|
|
|
|
[[ "${candidate}" == "${resolved_path}" ]] || return "${ERR_BUILD_PATH:-217}"
|
|
printf -v "${output_variable}" '%s' "${resolved_path}"
|
|
|
|
return 0
|
|
}
|
|
### Prevents accidental 'unset -f'.
|
|
# shellcheck disable=SC2034
|
|
readonly -f canonicalize_build_directory
|
|
|
|
#######################################
|
|
# Validates a build directory against the repository cleanup policy.
|
|
# Globals:
|
|
# VAR_TMP_SECRET
|
|
# VAR_WORKDIR
|
|
# Arguments:
|
|
# 1: candidate path
|
|
# 2: output variable name
|
|
# 3: quiet flag
|
|
# Returns:
|
|
# 0: on success
|
|
# ERR_BUILD_PATH: on failure
|
|
#######################################
|
|
validate_build_directory_path() {
|
|
declare candidate="$1" output_variable="${2:-}" quiet="${3:-false}" secret_root="" validated_path="" workdir=""
|
|
declare -a rejected_paths=(
|
|
"/" "/bin" "/boot" "/dev" "/etc" "/home" "/lib" "/lib64" "/media" "/mnt" "/opt" "/proc" "/root" "/run" "/sbin" "/srv"
|
|
"/sys" "/tmp" "/usr" "/usr/local" "/var" "/var/lib" "/var/tmp"
|
|
)
|
|
declare rejected_path=""
|
|
|
|
canonicalize_build_directory "${candidate}" validated_path || {
|
|
build_directory_validation_error \
|
|
"path is empty, non-absolute, non-canonical, missing its parent, or is a symlink" "${quiet}"
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
}
|
|
|
|
for rejected_path in "${rejected_paths[@]}"; do
|
|
if [[ "${validated_path}" == "${rejected_path}" ]]; then
|
|
build_directory_validation_error "broad or system parent directory" "${quiet}"
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
fi
|
|
done
|
|
|
|
workdir="$(realpath "${VAR_WORKDIR}" 2>/dev/null)" || {
|
|
build_directory_validation_error "repository work directory cannot be resolved" "${quiet}"
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
}
|
|
secret_root="$(realpath "${VAR_TMP_SECRET}" 2>/dev/null)" || {
|
|
build_directory_validation_error "secret root cannot be resolved" "${quiet}"
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
}
|
|
|
|
if [[ "${workdir}" == "${validated_path}" || "${workdir}" == "${validated_path}/"* || "${validated_path}" == "${workdir}/"* \
|
|
|| "${validated_path}" == "${secret_root}" || "${validated_path}" == "${secret_root}/"* ]]; then
|
|
build_directory_validation_error "path is outside the dedicated build-directory policy" "${quiet}"
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
fi
|
|
|
|
if [[ -n "${output_variable}" ]]; then
|
|
printf -v "${output_variable}" '%s' "${validated_path}"
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
### Prevents accidental 'unset -f'.
|
|
# shellcheck disable=SC2034
|
|
readonly -f validate_build_directory_path
|
|
|
|
#######################################
|
|
# Validates the builder-owned marker for an exact build directory.
|
|
# Globals:
|
|
# EUID
|
|
# Arguments:
|
|
# 1: candidate path
|
|
# 2: quiet flag
|
|
# Returns:
|
|
# 0: on success
|
|
# ERR_BUILD_PATH: on failure
|
|
#######################################
|
|
validate_build_directory_marker() {
|
|
declare candidate="$1" quiet="${2:-false}" directory_mode="" directory_owner="" marker="" marker_build_dir=""
|
|
declare expected_marker_value="" marker_link_count="" marker_value="" mode="" owner=""
|
|
|
|
validate_build_directory_path "${candidate}" marker_build_dir "${quiet}" || return "${ERR_BUILD_PATH:-217}"
|
|
[[ -d "${marker_build_dir}" ]] || {
|
|
build_directory_validation_error "build directory does not exist" "${quiet}"
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
}
|
|
|
|
directory_owner="$(secure_stat -c '%u' "${marker_build_dir}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}"
|
|
directory_mode="$(secure_stat -c '%a' "${marker_build_dir}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}"
|
|
if [[ "${directory_owner}" != "${EUID}" ]] || (( (8#${directory_mode} & 022) != 0 )); then
|
|
build_directory_validation_error "build directory ownership or permissions are unsafe" "${quiet}"
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
fi
|
|
|
|
marker="${marker_build_dir}/.ciss-live-builder-owned"
|
|
if [[ -L "${marker}" || ! -f "${marker}" ]]; then
|
|
build_directory_validation_error "builder-owned marker is missing or unsafe" "${quiet}"
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
fi
|
|
|
|
owner="$(secure_stat -c '%u' "${marker}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}"
|
|
mode="$(secure_stat -c '%a' "${marker}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}"
|
|
marker_link_count="$(secure_stat -c '%h' "${marker}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}"
|
|
marker_value="$(cat "${marker}" || exit $?; printf '.')" || return "${ERR_BUILD_PATH:-217}"
|
|
marker_value="${marker_value%.}"
|
|
expected_marker_value="${marker_build_dir}"$'\n'
|
|
|
|
if [[ "${owner}" != "${EUID}" || "${mode}" != "400" || "${marker_link_count}" != "1" \
|
|
|| "${marker_value}" != "${expected_marker_value}" ]]; then
|
|
build_directory_validation_error "builder-owned marker does not match the exact directory" "${quiet}"
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
### Prevents accidental 'unset -f'.
|
|
# shellcheck disable=SC2034
|
|
readonly -f validate_build_directory_marker
|
|
|
|
#######################################
|
|
# Validates an existing exact subpath below a marker-owned build directory.
|
|
# Arguments:
|
|
# 1: build directory
|
|
# 2: relative subpath
|
|
# 3: output variable name
|
|
# 4: quiet flag
|
|
# Returns:
|
|
# 0: on success
|
|
# ERR_BUILD_PATH: on failure
|
|
#######################################
|
|
validate_build_directory_subpath() {
|
|
declare build_directory="$1" relative_path="$2" output_variable="$3" quiet="${4:-false}"
|
|
declare candidate_subpath="" resolved_subpath=""
|
|
|
|
validate_build_directory_marker "${build_directory}" "${quiet}" || return "${ERR_BUILD_PATH:-217}"
|
|
|
|
if [[ -z "${relative_path}" || "${relative_path}" == /* || "${relative_path}" == "." || "${relative_path}" == ".." \
|
|
|| "${relative_path}" == ../* || "${relative_path}" == */../* || "${relative_path}" == */.. \
|
|
|| "${relative_path}" == ./* || "${relative_path}" == */./* || "${relative_path}" == */. ]]; then
|
|
build_directory_validation_error "unsafe relative cleanup subpath" "${quiet}"
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
fi
|
|
|
|
candidate_subpath="${build_directory}/${relative_path}"
|
|
if [[ -L "${candidate_subpath}" || ! -e "${candidate_subpath}" ]]; then
|
|
build_directory_validation_error "cleanup subpath is missing or is a symlink" "${quiet}"
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
fi
|
|
|
|
resolved_subpath="$(realpath "${candidate_subpath}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}"
|
|
if [[ "${resolved_subpath}" != "${candidate_subpath}" || "${resolved_subpath}" != "${build_directory}/"* ]]; then
|
|
build_directory_validation_error "cleanup subpath escapes the exact build directory" "${quiet}"
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
fi
|
|
|
|
printf -v "${output_variable}" '%s' "${resolved_subpath}"
|
|
return 0
|
|
}
|
|
### Prevents accidental 'unset -f'.
|
|
# shellcheck disable=SC2034
|
|
readonly -f validate_build_directory_subpath
|
|
|
|
#######################################
|
|
# Initialises a new or empty build directory and its ownership marker.
|
|
# Arguments:
|
|
# 1: candidate path
|
|
# 2: output variable name
|
|
# Returns:
|
|
# 0: on success
|
|
# ERR_BUILD_PATH: on failure
|
|
#######################################
|
|
initialize_build_directory() {
|
|
declare candidate="$1" output_variable="$2" directory_mode="" directory_owner="" existing_entry=""
|
|
declare initialized_build_dir="" marker=""
|
|
|
|
validate_build_directory_path "${candidate}" initialized_build_dir || return "${ERR_BUILD_PATH:-217}"
|
|
|
|
if [[ ! -e "${initialized_build_dir}" ]]; then
|
|
mkdir -m 0700 "${initialized_build_dir}" || return "${ERR_BUILD_PATH:-217}"
|
|
fi
|
|
|
|
validate_build_directory_path "${initialized_build_dir}" initialized_build_dir || return "${ERR_BUILD_PATH:-217}"
|
|
[[ -d "${initialized_build_dir}" && ! -L "${initialized_build_dir}" ]] || return "${ERR_BUILD_PATH:-217}"
|
|
|
|
directory_owner="$(secure_stat -c '%u' "${initialized_build_dir}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}"
|
|
directory_mode="$(secure_stat -c '%a' "${initialized_build_dir}" 2>/dev/null)" || return "${ERR_BUILD_PATH:-217}"
|
|
if [[ "${directory_owner}" != "${EUID}" ]] || (( (8#${directory_mode} & 022) != 0 )); then
|
|
build_directory_validation_error "build directory ownership or permissions are unsafe"
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
fi
|
|
|
|
marker="${initialized_build_dir}/.ciss-live-builder-owned"
|
|
|
|
if [[ -e "${marker}" || -L "${marker}" ]]; then
|
|
validate_build_directory_marker "${initialized_build_dir}" || return "${ERR_BUILD_PATH:-217}"
|
|
else
|
|
existing_entry="$(find "${initialized_build_dir}" -mindepth 1 -maxdepth 1 -print -quit)" || return "${ERR_BUILD_PATH:-217}"
|
|
if [[ -n "${existing_entry}" ]]; then
|
|
build_directory_validation_error "non-empty directory has no builder-owned marker"
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
fi
|
|
(umask 077; printf '%s\n' "${initialized_build_dir}" >| "${marker}") || return "${ERR_BUILD_PATH:-217}"
|
|
chmod 0400 "${marker}" || return "${ERR_BUILD_PATH:-217}"
|
|
validate_build_directory_marker "${initialized_build_dir}" || return "${ERR_BUILD_PATH:-217}"
|
|
fi
|
|
|
|
printf -v "${output_variable}" '%s' "${initialized_build_dir}"
|
|
return 0
|
|
}
|
|
### Prevents accidental 'unset -f'.
|
|
# shellcheck disable=SC2034
|
|
readonly -f initialize_build_directory
|
|
|
|
#######################################
|
|
# Removes paths with a one-filesystem boundary where supported.
|
|
# Arguments:
|
|
# paths to remove
|
|
# Returns:
|
|
# rm exit status
|
|
#######################################
|
|
remove_build_paths() {
|
|
# shellcheck disable=SC2312
|
|
if rm --help 2>&1 | grep -q -- '--one-file-system'; then
|
|
rm -rf --one-file-system -- "$@"
|
|
else
|
|
rm -rf -- "$@"
|
|
fi
|
|
}
|
|
### Prevents accidental 'unset -f'.
|
|
# shellcheck disable=SC2034
|
|
readonly -f remove_build_paths
|
|
|
|
#######################################
|
|
# Deletes all content except the ownership marker from an exact build directory.
|
|
# Arguments:
|
|
# 1: candidate path
|
|
# Returns:
|
|
# 0: on success
|
|
# ERR_BUILD_PATH: on failure
|
|
#######################################
|
|
clean_build_directory_contents() {
|
|
declare candidate="$1" build_entry=""
|
|
declare -a build_entries=()
|
|
declare -i old_dotglob=0 old_failglob=0 old_nullglob=0
|
|
|
|
validate_build_directory_marker "${candidate}" || return "${ERR_BUILD_PATH:-217}"
|
|
|
|
shopt -q dotglob && old_dotglob=1
|
|
shopt -q failglob && old_failglob=1
|
|
shopt -q nullglob && old_nullglob=1
|
|
shopt -s dotglob nullglob
|
|
shopt -u failglob
|
|
|
|
build_entries=("${candidate}"/*)
|
|
for build_entry in "${build_entries[@]}"; do
|
|
[[ "${build_entry}" == "${candidate}/.ciss-live-builder-owned" ]] && continue
|
|
remove_build_paths "${build_entry}" || {
|
|
if (( old_dotglob )); then shopt -s dotglob; else shopt -u dotglob; fi
|
|
if (( old_failglob )); then shopt -s failglob; else shopt -u failglob; fi
|
|
if (( old_nullglob )); then shopt -s nullglob; else shopt -u nullglob; fi
|
|
return "${ERR_BUILD_PATH:-217}"
|
|
}
|
|
done
|
|
|
|
if (( old_dotglob )); then shopt -s dotglob; else shopt -u dotglob; fi
|
|
if (( old_failglob )); then shopt -s failglob; else shopt -u failglob; fi
|
|
if (( old_nullglob )); then shopt -s nullglob; else shopt -u nullglob; fi
|
|
|
|
validate_build_directory_marker "${candidate}" || return "${ERR_BUILD_PATH:-217}"
|
|
return 0
|
|
}
|
|
### Prevents accidental 'unset -f'.
|
|
# shellcheck disable=SC2034
|
|
readonly -f clean_build_directory_contents
|
|
|
|
# vim: number et ts=2 sw=2 sts=2 ai tw=128 ft=sh
|