719 lines
36 KiB
Bash
719 lines
36 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
PATH=/usr/sbin:/usr/bin:/sbin:/bin
|
|
export PATH
|
|
LC_ALL=C
|
|
export LC_ALL
|
|
umask 077
|
|
set -Eeuo pipefail
|
|
shopt -s extglob
|
|
|
|
_k3slrv_initial_guard() {
|
|
local candidate_euid="${1-}" shell_flags="${2-}"
|
|
[[ "$candidate_euid" =~ ^[0-9]+$ ]] || return 1
|
|
(( candidate_euid != 0 )) || return 1
|
|
[[ "$shell_flags" != *x* ]]
|
|
}
|
|
|
|
if ! _k3slrv_initial_guard "$EUID" "$-"; then
|
|
printf 'Recovery validation refused\n' >&2
|
|
if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then return 1; else exit 1; fi
|
|
fi
|
|
|
|
readonly K3SLRV_SECURE_PATH='/usr/sbin:/usr/bin:/sbin:/bin'
|
|
readonly K3SLRV_TRUSTED_BINARIES=(
|
|
/usr/bin/findmnt /usr/bin/lsblk /usr/bin/stat /usr/bin/readlink
|
|
/usr/sbin/blkid /usr/bin/ntfs-3g.probe /usr/sbin/smartctl
|
|
/usr/sbin/losetup /usr/sbin/cryptsetup /usr/bin/du
|
|
/usr/bin/sha256sum /usr/bin/test /usr/bin/sudo /usr/bin/mawk
|
|
)
|
|
readonly K3SLRV_SCRIPT_DIR="$(cd -P -- "${BASH_SOURCE[0]%/*}" && pwd -P)"
|
|
readonly K3SLRV_REPOSITORY_ROOT="$(cd -P -- "${K3SLRV_SCRIPT_DIR}/../.." && pwd -P)"
|
|
readonly K3SLRV_LIBRARY_PATH="${K3SLRV_REPOSITORY_ROOT}/scripts/lib/k3s-local-recovery.sh"
|
|
readonly K3SLRV_CONTRACT_PATH="${K3SLRV_REPOSITORY_ROOT}/infrastructure/security/k3s/local-recovery.env"
|
|
|
|
_k3slrv_usage() {
|
|
printf 'Usage: bash scripts/validate/k3s-local-recovery.sh [--expect-device-ready|--expect-closed|--expect-open] [--check-latest-bundle]\n'
|
|
}
|
|
|
|
_k3slrv_fail() {
|
|
printf 'Recovery validation failed\n' >&2
|
|
return 1
|
|
}
|
|
|
|
_k3slrv_exec() {
|
|
local scope="$1"
|
|
shift
|
|
case "$scope" in
|
|
user) "$@" ;;
|
|
root) /usr/bin/sudo --non-interactive -- "$@" ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
# Bash command substitution normally discards every trailing newline. Append a
|
|
# non-newline sentinel inside the substitution, then remove only that sentinel
|
|
# so record parsers can distinguish one record from a record plus blank lines.
|
|
_k3slrv_capture() {
|
|
local destination="$1" captured command_rc sentinel=$'\036'
|
|
shift
|
|
if captured="$(
|
|
set +e
|
|
"$@"
|
|
command_rc=$?
|
|
printf '\036'
|
|
exit "$command_rc"
|
|
)"; then
|
|
command_rc=0
|
|
else
|
|
command_rc=$?
|
|
fi
|
|
[[ "$captured" == *"$sentinel" ]] || return 125
|
|
captured="${captured%"$sentinel"}"
|
|
[[ "$captured" != *"$sentinel"* ]] || return 125
|
|
printf -v "$destination" '%s' "$captured"
|
|
return "$command_rc"
|
|
}
|
|
|
|
_k3slrv_normalize_one_record() {
|
|
local destination="$1" value="$2"
|
|
[[ -n "$value" && "$value" != *$'\r'* ]] || return 1
|
|
value="${value%$'\n'}"
|
|
[[ -n "$value" && "$value" != *$'\n'* ]] || return 1
|
|
printf -v "$destination" '%s' "$value"
|
|
}
|
|
|
|
_k3slrv_verify_binary() {
|
|
local binary="$1" metadata uid mode kind group_digit other_digit
|
|
[[ "$binary" == /* ]] || return 1
|
|
_k3slrv_exec user /usr/bin/test -f "$binary" >/dev/null 2>&1 || return 1
|
|
_k3slrv_exec user /usr/bin/test ! -L "$binary" >/dev/null 2>&1 || return 1
|
|
_k3slrv_capture metadata _k3slrv_exec user /usr/bin/stat --format='%u|%a|%F' -- "$binary" 2>/dev/null || return 1
|
|
_k3slrv_normalize_one_record metadata "$metadata" || return 1
|
|
IFS='|' read -r uid mode kind <<<"$metadata"
|
|
[[ "$uid" == 0 && "$mode" =~ ^[0-7]{3,4}$ && "$kind" == 'regular file' ]] || return 1
|
|
group_digit="${mode: -2:1}"
|
|
other_digit="${mode: -1}"
|
|
(( (10#$group_digit & 2) == 0 && (10#$other_digit & 2) == 0 ))
|
|
}
|
|
|
|
_k3slrv_verify_trusted_binaries() {
|
|
local binary
|
|
[[ "$PATH" == "$K3SLRV_SECURE_PATH" ]] || return 1
|
|
for binary in "${K3SLRV_TRUSTED_BINARIES[@]}"; do
|
|
_k3slrv_verify_binary "$binary" || return 1
|
|
done
|
|
}
|
|
|
|
_k3slrv_require_cached_sudo() {
|
|
_k3slrv_exec user /usr/bin/sudo --non-interactive --validate >/dev/null 2>&1
|
|
}
|
|
|
|
_k3slrv_root() {
|
|
_k3slrv_exec root "$@"
|
|
}
|
|
|
|
_k3slrv_trim() {
|
|
local value="$1"
|
|
value="${value##+([[:space:]])}"
|
|
value="${value%%+([[:space:]])}"
|
|
printf '%s\n' "$value"
|
|
}
|
|
|
|
_k3slrv_one_line() {
|
|
local output
|
|
_k3slrv_capture output _k3slrv_exec user "$@" 2>/dev/null || return 1
|
|
_k3slrv_normalize_one_record output "$output" || return 1
|
|
_k3slrv_trim "$output"
|
|
}
|
|
|
|
_k3slrv_one_line_root() {
|
|
local output
|
|
_k3slrv_capture output _k3slrv_root "$@" 2>/dev/null || return 1
|
|
_k3slrv_normalize_one_record output "$output" || return 1
|
|
_k3slrv_trim "$output"
|
|
}
|
|
|
|
_k3slrv_blkid() {
|
|
_k3slrv_one_line_root /usr/sbin/blkid --output value --match-tag "$1" -- "$2"
|
|
}
|
|
|
|
_k3slrv_lsblk() {
|
|
_k3slrv_one_line /usr/bin/lsblk --noheadings --nodeps --paths --output "$1" -- "$2"
|
|
}
|
|
|
|
_k3slrv_smart_evidence() {
|
|
local disk="$1" output line id raw
|
|
local -a fields=() lines=()
|
|
local health='' reallocated='' pending='' uncorrectable=''
|
|
_k3slrv_capture output _k3slrv_root /usr/sbin/smartctl -H -A -- "$disk" 2>/dev/null || return 1
|
|
mapfile -t lines < <(printf '%s' "$output")
|
|
(( ${#lines[@]} > 0 )) || return 1
|
|
for line in "${lines[@]}"; do
|
|
[[ "$line" != *$'\r'* ]] || return 1
|
|
if [[ "$line" =~ ^SMART[[:space:]]overall-health[[:space:]]self-assessment[[:space:]]test[[:space:]]result:[[:space:]]*PASSED[[:space:]]*$ ]]; then
|
|
[[ -z "$health" ]] || return 1
|
|
health=PASSED
|
|
continue
|
|
fi
|
|
[[ "$line" =~ ^[[:space:]]*(5|197|198)[[:space:]] ]] || continue
|
|
id="${BASH_REMATCH[1]}"
|
|
read -r -a fields <<<"$line"
|
|
(( ${#fields[@]} >= 2 )) || return 1
|
|
raw="${fields[${#fields[@]} - 1]}"
|
|
[[ "$raw" =~ ^[0-9]+$ ]] || return 1
|
|
case "$id" in
|
|
5) [[ -z "$reallocated" ]] || return 1; reallocated="$raw" ;;
|
|
197) [[ -z "$pending" ]] || return 1; pending="$raw" ;;
|
|
198) [[ -z "$uncorrectable" ]] || return 1; uncorrectable="$raw" ;;
|
|
esac
|
|
done
|
|
[[ -n "$health" && -n "$reallocated" && -n "$pending" && -n "$uncorrectable" ]] || return 1
|
|
printf 'smart_health=%s\nsmart_reallocated=%s\nsmart_pending=%s\nsmart_uncorrectable=%s\n' \
|
|
"$health" "$reallocated" "$pending" "$uncorrectable"
|
|
}
|
|
|
|
_k3slrv_findmnt_line() {
|
|
local output
|
|
_k3slrv_capture output _k3slrv_root /usr/bin/findmnt --noheadings --raw --output SOURCE,FSTYPE,OPTIONS,ID,MAJ:MIN --mountpoint "$1" 2>/dev/null || return 1
|
|
_k3slrv_normalize_one_record output "$output" || return 1
|
|
output="$(_k3slrv_trim "$output")"
|
|
[[ -n "$output" ]] || return 1
|
|
printf '%s\n' "$output"
|
|
}
|
|
|
|
_k3slrv_findmnt_optional() {
|
|
local output rc=0
|
|
_k3slrv_capture output _k3slrv_root /usr/bin/findmnt --noheadings --raw --output SOURCE,FSTYPE,OPTIONS,ID,MAJ:MIN --mountpoint "$1" 2>/dev/null || rc=$?
|
|
case "$rc" in
|
|
0)
|
|
_k3slrv_normalize_one_record output "$output" || return 1
|
|
output="$(_k3slrv_trim "$output")"
|
|
[[ -n "$output" ]] || return 1
|
|
printf '%s\n' "$output"
|
|
;;
|
|
1) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
_k3slrv_findmnt_source_optional() {
|
|
local output rc=0
|
|
_k3slrv_capture output _k3slrv_root /usr/bin/findmnt --noheadings --raw --output TARGET --source "$1" 2>/dev/null || rc=$?
|
|
case "$rc" in
|
|
0)
|
|
_k3slrv_normalize_one_record output "$output" || return 1
|
|
output="$(_k3slrv_trim "$output")"
|
|
[[ -n "$output" ]] || return 1
|
|
printf '%s\n' "$output"
|
|
;;
|
|
1) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
_k3slrv_collect_device_evidence() {
|
|
local mode="$1" recovery_partition recovery_disk recovery_parent k3s_partition k3s_disk k3s_parent
|
|
local recovery_model recovery_serial recovery_wwn recovery_mm k3s_model k3s_serial k3s_wwn k3s_mm
|
|
local recovery_uuid recovery_partuuid recovery_type k3s_uuid k3s_partuuid k3s_type outer_mount source_mounts
|
|
recovery_partition="$(_k3slrv_one_line /usr/bin/readlink -f -- "$K3SLR_RECOVERY_PARTITION_BY_ID")" || return 1
|
|
recovery_disk="$(_k3slrv_one_line /usr/bin/readlink -f -- "$K3SLR_RECOVERY_DISK_BY_ID")" || return 1
|
|
recovery_parent="$(_k3slrv_lsblk PKNAME "$recovery_partition")" || return 1
|
|
k3s_partition="$(_k3slrv_one_line /usr/bin/readlink -f -- "$K3SLR_K3S_PARTITION_BY_ID")" || return 1
|
|
k3s_disk="$(_k3slrv_one_line /usr/bin/readlink -f -- "$K3SLR_K3S_DISK_BY_ID")" || return 1
|
|
k3s_parent="$(_k3slrv_lsblk PKNAME "$k3s_partition")" || return 1
|
|
recovery_model="$(_k3slrv_lsblk MODEL "$recovery_disk")" || return 1
|
|
recovery_serial="$(_k3slrv_lsblk SERIAL "$recovery_disk")" || return 1
|
|
recovery_wwn="$(_k3slrv_lsblk WWN "$recovery_disk")" || return 1
|
|
recovery_mm="$(_k3slrv_lsblk MAJ:MIN "$recovery_disk")" || return 1
|
|
k3s_model="$(_k3slrv_lsblk MODEL "$k3s_disk")" || return 1
|
|
k3s_serial="$(_k3slrv_lsblk SERIAL "$k3s_disk")" || return 1
|
|
k3s_wwn="$(_k3slrv_lsblk WWN "$k3s_disk")" || return 1
|
|
k3s_mm="$(_k3slrv_lsblk MAJ:MIN "$k3s_disk")" || return 1
|
|
recovery_uuid="$(_k3slrv_blkid UUID "$recovery_partition")" || return 1
|
|
recovery_partuuid="$(_k3slrv_blkid PARTUUID "$recovery_partition")" || return 1
|
|
recovery_type="$(_k3slrv_blkid TYPE "$recovery_partition")" || return 1
|
|
k3s_uuid="$(_k3slrv_blkid UUID "$k3s_partition")" || return 1
|
|
k3s_partuuid="$(_k3slrv_blkid PARTUUID "$k3s_partition")" || return 1
|
|
k3s_type="$(_k3slrv_blkid TYPE "$k3s_partition")" || return 1
|
|
printf 'recovery_partition=%s\nrecovery_disk=%s\nrecovery_partition_parent=%s\n' \
|
|
"$recovery_partition" "$recovery_disk" "$recovery_parent"
|
|
printf 'recovery_fs_uuid=%s\nrecovery_partuuid=%s\nrecovery_type=%s\n' \
|
|
"$recovery_uuid" "$recovery_partuuid" "$recovery_type"
|
|
printf 'recovery_model=%s\nrecovery_serial=%s\nrecovery_wwn=%s\nrecovery_major_minor=%s\n' \
|
|
"$recovery_model" "$recovery_serial" "$recovery_wwn" "$recovery_mm"
|
|
printf 'k3s_partition=%s\nk3s_disk=%s\nk3s_partition_parent=%s\n' \
|
|
"$k3s_partition" "$k3s_disk" "$k3s_parent"
|
|
printf 'k3s_fs_uuid=%s\nk3s_partuuid=%s\nk3s_type=%s\n' \
|
|
"$k3s_uuid" "$k3s_partuuid" "$k3s_type"
|
|
printf 'k3s_model=%s\nk3s_serial=%s\nk3s_wwn=%s\nk3s_major_minor=%s\n' \
|
|
"$k3s_model" "$k3s_serial" "$k3s_wwn" "$k3s_mm"
|
|
_k3slrv_smart_evidence "$recovery_disk" || return 1
|
|
if [[ "$mode" == open ]]; then
|
|
printf 'ntfs_probe=not_applicable\n'
|
|
else
|
|
outer_mount="$(_k3slrv_findmnt_optional "$K3SLR_OUTER_MOUNT")" || return 1
|
|
[[ -z "$outer_mount" ]] || return 1
|
|
source_mounts="$(_k3slrv_findmnt_source_optional "$recovery_partition")" || return 1
|
|
[[ -z "$source_mounts" ]] || return 1
|
|
if _k3slrv_root /usr/bin/ntfs-3g.probe --readwrite "$recovery_partition" >/dev/null 2>&1; then
|
|
printf 'ntfs_probe=pass\n'
|
|
else
|
|
printf 'ntfs_probe=fail\n'
|
|
fi
|
|
fi
|
|
}
|
|
|
|
_k3slrv_mapping_evidence() {
|
|
local name="$1" prefix="$2" output type='' device='' rc=0
|
|
local line
|
|
local -a lines=()
|
|
_k3slrv_capture output _k3slrv_root /usr/sbin/cryptsetup status -- "$name" 2>/dev/null || rc=$?
|
|
if (( rc == 4 )); then
|
|
printf '%s_present=0\n' "$prefix"
|
|
return 0
|
|
fi
|
|
(( rc == 0 )) || return 1
|
|
mapfile -t lines < <(printf '%s' "$output")
|
|
(( ${#lines[@]} > 0 )) || return 1
|
|
for line in "${lines[@]}"; do
|
|
[[ "$line" != *$'\r'* ]] || return 1
|
|
line="$(_k3slrv_trim "$line")"
|
|
case "$line" in
|
|
type:*) [[ -z "$type" ]] || return 1; type="$(_k3slrv_trim "${line#type:}")" ;;
|
|
device:*) [[ -z "$device" ]] || return 1; device="$(_k3slrv_trim "${line#device:}")" ;;
|
|
esac
|
|
done
|
|
[[ -n "$type" && -n "$device" ]] || return 1
|
|
printf '%s_present=1\n%s_type=%s\n%s_device=%s\n' "$prefix" "$prefix" "$type" "$prefix" "$device"
|
|
}
|
|
|
|
_k3slrv_pin_container_chain() {
|
|
local container="$1" current="$K3SLR_OUTER_MOUNT" canonical metadata component index=0 snapshot=''
|
|
local -a components=()
|
|
IFS='/' read -r -a components <<<"$K3SLR_CONTAINER_RELATIVE"
|
|
for component in '' "${components[@]}"; do
|
|
if (( index > 0 )); then current="${current}/${component}"; fi
|
|
_k3slrv_root /usr/bin/test ! -L "$current" >/dev/null 2>&1 || return 1
|
|
canonical="$(_k3slrv_one_line_root /usr/bin/readlink -e -- "$current")" || return 1
|
|
[[ "$canonical" == "$current" ]] || return 1
|
|
metadata="$(_k3slrv_one_line_root /usr/bin/stat --format='%d:%i|%F' -- "$current")" || return 1
|
|
if (( index < ${#components[@]} )); then
|
|
[[ "$metadata" == *'|directory' ]] || return 1
|
|
else
|
|
[[ "$current" == "$container" && "$metadata" == *'|regular file' ]] || return 1
|
|
fi
|
|
snapshot+="${metadata%%|*},"
|
|
index=$((index + 1))
|
|
done
|
|
printf '%s\n' "${snapshot%,}"
|
|
}
|
|
|
|
_k3slrv_mapping_value() {
|
|
local evidence="$1" key="$2" line found=''
|
|
while IFS= read -r line; do
|
|
[[ "$line" == "$key="* ]] || continue
|
|
[[ -z "$found" ]] || return 1
|
|
found="${line#*=}"
|
|
done <<<"$evidence"
|
|
[[ -n "$found" ]] || return 1
|
|
printf '%s\n' "$found"
|
|
}
|
|
|
|
_k3slrv_collect_state_evidence() {
|
|
local mode="$1" outer='' inner='' loops='' outer_source outer_fstype outer_options outer_mount_id outer_major_minor
|
|
local inner_source inner_fstype inner_options inner_mount_id inner_major_minor container metadata kind uid gid file_mode size blocks allocated inode container_device
|
|
local inner_type inner_label recovery_partition source_mounts chain_snapshot loop_device loop_back_inode loop_back_major_minor
|
|
local loop_major_minor loop_offset loop_sizelimit mapping_snapshot proof_snapshot mapping_device mapping_loop_major_minor mapping_major_minor
|
|
local outer_after inner_after source_after chain_after loops_after mapping_after proof_after mapping_mm_after
|
|
local container_metadata_after inner_type_after inner_label_after inner_root_metadata inner_root_after
|
|
local mapping_device_after mapping_loop_mm_after
|
|
local -a loop_records=()
|
|
outer="$(_k3slrv_findmnt_optional "$K3SLR_OUTER_MOUNT")" || return 1
|
|
inner="$(_k3slrv_findmnt_optional "$K3SLR_INNER_MOUNT")" || return 1
|
|
container="${K3SLR_OUTER_MOUNT}/${K3SLR_CONTAINER_RELATIVE}"
|
|
if [[ "$mode" != open ]]; then
|
|
_k3slrv_capture loops _k3slrv_root /usr/sbin/losetup --associated "$container" --noheadings --output NAME 2>/dev/null || return 1
|
|
printf 'outer_mounted=%s\ninner_mounted=%s\n' "$([[ -n "$outer" ]] && printf 1 || printf 0)" "$([[ -n "$inner" ]] && printf 1 || printf 0)"
|
|
if [[ -z "$loops" ]]; then
|
|
printf 'loop_count=0\n'
|
|
else
|
|
mapfile -t loop_records < <(printf '%s' "$loops")
|
|
if (( ${#loop_records[@]} == 1 )) && [[ -n "${loop_records[0]}" && "${loop_records[0]}" != *$'\r'* ]]; then
|
|
printf 'loop_count=1\n'
|
|
else
|
|
printf 'loop_count=2\n'
|
|
fi
|
|
fi
|
|
_k3slrv_mapping_evidence "$K3SLR_MAPPING_NAME" mapping || return 1
|
|
_k3slrv_mapping_evidence "$K3SLR_PROOF_MAPPING_NAME" proof_mapping || return 1
|
|
return 0
|
|
fi
|
|
_k3slrv_capture loops _k3slrv_root /usr/sbin/losetup --list --associated "$container" --noheadings --raw \
|
|
--output NAME,BACK-INO,BACK-MAJ:MIN,MAJ:MIN,OFFSET,SIZELIMIT 2>/dev/null || return 1
|
|
_k3slrv_normalize_one_record loops "$loops" || return 1
|
|
[[ -n "$outer" && -n "$inner" ]] || return 1
|
|
read -r outer_source outer_fstype outer_options outer_mount_id outer_major_minor <<<"$outer"
|
|
read -r inner_source inner_fstype inner_options inner_mount_id inner_major_minor <<<"$inner"
|
|
read -r loop_device loop_back_inode loop_back_major_minor loop_major_minor loop_offset loop_sizelimit <<<"$loops"
|
|
[[ -n "$outer_mount_id" && -n "$outer_major_minor" && -n "$inner_mount_id" && -n "$inner_major_minor" &&
|
|
-n "$loop_device" && -n "$loop_back_inode" && -n "$loop_back_major_minor" && -n "$loop_major_minor" &&
|
|
-n "$loop_offset" && -n "$loop_sizelimit" ]] || return 1
|
|
outer_source="$(_k3slrv_one_line /usr/bin/readlink -f -- "$outer_source")" || return 1
|
|
recovery_partition="$(_k3slrv_one_line /usr/bin/readlink -f -- "$K3SLR_RECOVERY_PARTITION_BY_ID")" || return 1
|
|
source_mounts="$(_k3slrv_findmnt_source_optional "$recovery_partition")" || return 1
|
|
[[ "$source_mounts" == "$K3SLR_OUTER_MOUNT" ]] || return 1
|
|
chain_snapshot="$(_k3slrv_pin_container_chain "$container")" || return 1
|
|
metadata="$(_k3slrv_one_line_root /usr/bin/stat --format='%F|%u|%g|%a|%s|%b|%i|%Hd:%Ld' -- "$container")" || return 1
|
|
IFS='|' read -r kind uid gid file_mode size blocks inode container_device <<<"$metadata"
|
|
[[ "$container_device" =~ ^[0-9]+:[0-9]+$ ]] || return 1
|
|
[[ "$blocks" =~ ^[0-9]+$ && "$blocks" -le 18014398509481983 ]] || return 1
|
|
allocated=$((blocks * 512))
|
|
printf 'outer_mounted=1\nouter_source=%s\nouter_fstype=%s\nouter_options=%s\n' "$outer_source" "$outer_fstype" "$outer_options"
|
|
printf 'inner_mounted=1\ninner_source=%s\ninner_fstype=%s\ninner_options=%s\n' "$inner_source" "$inner_fstype" "$inner_options"
|
|
printf 'container_kind=%s\ncontainer_symlink=%s\ncontainer_uid=%s\ncontainer_gid=%s\ncontainer_mode=%s\n' \
|
|
"$([[ "$kind" == 'regular file' ]] && printf regular || printf other)" \
|
|
"$(_k3slrv_root /usr/bin/test -L "$container" >/dev/null 2>&1 && printf 1 || printf 0)" "$uid" "$gid" "$file_mode"
|
|
printf 'container_size=%s\ncontainer_allocated=%s\nloop_count=1\nloop_device=%s\n' "$size" "$allocated" "$loop_device"
|
|
_k3slrv_capture mapping_snapshot _k3slrv_mapping_evidence "$K3SLR_MAPPING_NAME" mapping || return 1
|
|
_k3slrv_capture proof_snapshot _k3slrv_mapping_evidence "$K3SLR_PROOF_MAPPING_NAME" proof_mapping || return 1
|
|
printf '%s%s' "$mapping_snapshot" "$proof_snapshot"
|
|
mapping_device="$(_k3slrv_mapping_value "$mapping_snapshot" mapping_device)" || return 1
|
|
mapping_loop_major_minor="$(_k3slrv_lsblk MAJ:MIN "$mapping_device")" || return 1
|
|
mapping_major_minor="$(_k3slrv_lsblk MAJ:MIN "/dev/mapper/${K3SLR_MAPPING_NAME}")" || return 1
|
|
inner_type="$(_k3slrv_blkid TYPE "/dev/mapper/${K3SLR_MAPPING_NAME}")" || return 1
|
|
inner_label="$(_k3slrv_blkid LABEL "/dev/mapper/${K3SLR_MAPPING_NAME}")" || return 1
|
|
printf 'inner_type=%s\ninner_label=%s\n' "$inner_type" "$inner_label"
|
|
inner_root_metadata="$(_k3slrv_one_line_root /usr/bin/stat --format='%u|%g|%a|%F' -- "$K3SLR_INNER_MOUNT")" || return 1
|
|
IFS='|' read -r uid gid file_mode kind <<<"$inner_root_metadata"
|
|
printf 'inner_root_uid=%s\ninner_root_gid=%s\ninner_root_mode=%s\ninner_root_kind=%s\n' \
|
|
"$uid" "$gid" "$file_mode" "$kind"
|
|
outer_after="$(_k3slrv_findmnt_optional "$K3SLR_OUTER_MOUNT")" || return 1
|
|
inner_after="$(_k3slrv_findmnt_optional "$K3SLR_INNER_MOUNT")" || return 1
|
|
source_after="$(_k3slrv_findmnt_source_optional "$recovery_partition")" || return 1
|
|
chain_after="$(_k3slrv_pin_container_chain "$container")" || return 1
|
|
_k3slrv_capture loops_after _k3slrv_root /usr/sbin/losetup --list --associated "$container" --noheadings --raw \
|
|
--output NAME,BACK-INO,BACK-MAJ:MIN,MAJ:MIN,OFFSET,SIZELIMIT 2>/dev/null || return 1
|
|
_k3slrv_normalize_one_record loops_after "$loops_after" || return 1
|
|
_k3slrv_capture mapping_after _k3slrv_mapping_evidence "$K3SLR_MAPPING_NAME" mapping || return 1
|
|
_k3slrv_capture proof_after _k3slrv_mapping_evidence "$K3SLR_PROOF_MAPPING_NAME" proof_mapping || return 1
|
|
container_metadata_after="$(_k3slrv_one_line_root /usr/bin/stat --format='%F|%u|%g|%a|%s|%b|%i|%Hd:%Ld' -- "$container")" || return 1
|
|
inner_type_after="$(_k3slrv_blkid TYPE "/dev/mapper/${K3SLR_MAPPING_NAME}")" || return 1
|
|
inner_label_after="$(_k3slrv_blkid LABEL "/dev/mapper/${K3SLR_MAPPING_NAME}")" || return 1
|
|
inner_root_after="$(_k3slrv_one_line_root /usr/bin/stat --format='%u|%g|%a|%F' -- "$K3SLR_INNER_MOUNT")" || return 1
|
|
mapping_device_after="$(_k3slrv_mapping_value "$mapping_after" mapping_device)" || return 1
|
|
mapping_loop_mm_after="$(_k3slrv_lsblk MAJ:MIN "$mapping_device_after")" || return 1
|
|
mapping_mm_after="$(_k3slrv_lsblk MAJ:MIN "/dev/mapper/${K3SLR_MAPPING_NAME}")" || return 1
|
|
[[ "$outer_after" == "$outer" && "$inner_after" == "$inner" && "$source_after" == "$source_mounts" &&
|
|
"$chain_after" == "$chain_snapshot" && "$loops_after" == "$loops" && "$mapping_after" == "$mapping_snapshot" &&
|
|
"$proof_after" == "$proof_snapshot" && "$container_metadata_after" == "$metadata" &&
|
|
"$inner_type_after" == "$inner_type" && "$inner_label_after" == "$inner_label" &&
|
|
"$inner_root_after" == "$inner_root_metadata" && "$mapping_loop_mm_after" == "$mapping_loop_major_minor" &&
|
|
"$mapping_mm_after" == "$mapping_major_minor" ]] || return 1
|
|
printf '%s\n' \
|
|
'container_chain_pinned=1' \
|
|
"container_inode=${inode}" \
|
|
"container_device=${container_device}" \
|
|
"loop_back_inode=${loop_back_inode}" \
|
|
"loop_back_major_minor=${loop_back_major_minor}" \
|
|
"outer_major_minor=${outer_major_minor}" \
|
|
"loop_major_minor=${loop_major_minor}" \
|
|
"mapping_loop_major_minor=${mapping_loop_major_minor}" \
|
|
"mapping_major_minor=${mapping_major_minor}" \
|
|
"inner_major_minor=${inner_major_minor}" \
|
|
"loop_offset=${loop_offset}" \
|
|
"loop_sizelimit=${loop_sizelimit}" \
|
|
'snapshot_stable=1'
|
|
}
|
|
|
|
_k3slrv_parse_evidence() {
|
|
local input="$1" destination_name="$2"
|
|
shift 2
|
|
local -n destination="$destination_name"
|
|
local line key value expected
|
|
local -A allowed=() seen=()
|
|
local -a lines=()
|
|
destination=()
|
|
for expected in "$@"; do allowed["$expected"]=1; done
|
|
mapfile -t lines < <(printf '%s' "$input")
|
|
(( ${#lines[@]} > 0 )) || return 1
|
|
for line in "${lines[@]}"; do
|
|
[[ "$line" =~ ^([a-z][a-z0-9_]*)=([A-Za-z0-9._:/,+=-]*)$ ]] || return 1
|
|
key="${BASH_REMATCH[1]}"; value="${BASH_REMATCH[2]}"
|
|
[[ "${allowed[$key]+yes}" == yes && "${seen[$key]+yes}" != yes ]] || return 1
|
|
seen["$key"]=1; destination["$key"]="$value"
|
|
done
|
|
for expected in "${!allowed[@]}"; do [[ "${seen[$expected]+yes}" == yes ]] || return 1; done
|
|
}
|
|
|
|
_k3slrv_validate_device() {
|
|
local mode="$1" input="$2"
|
|
local -A e=()
|
|
local keys=(recovery_partition recovery_disk recovery_partition_parent recovery_fs_uuid recovery_partuuid recovery_type
|
|
recovery_model recovery_serial recovery_wwn recovery_major_minor k3s_partition k3s_disk k3s_partition_parent
|
|
k3s_fs_uuid k3s_partuuid k3s_type k3s_model k3s_serial k3s_wwn k3s_major_minor smart_health
|
|
smart_reallocated smart_pending smart_uncorrectable ntfs_probe)
|
|
_k3slrv_parse_evidence "$input" e "${keys[@]}" || return 1
|
|
[[ "${e[recovery_partition_parent]}" == "${e[recovery_disk]}" && "${e[k3s_partition_parent]}" == "${e[k3s_disk]}" ]] || return 1
|
|
[[ "${e[recovery_fs_uuid]}" == "$K3SLR_RECOVERY_FS_UUID" && "${e[recovery_partuuid]}" == "$K3SLR_RECOVERY_PARTUUID" && "${e[recovery_type]}" == ntfs ]] || return 1
|
|
[[ "${e[recovery_model]}" == "$K3SLR_RECOVERY_MODEL" && "${e[recovery_serial]}" == "$K3SLR_RECOVERY_SERIAL" && "${e[recovery_wwn]}" == "$K3SLR_RECOVERY_WWN" ]] || return 1
|
|
[[ "${e[k3s_fs_uuid]}" == "$K3SLR_K3S_FS_UUID" && "${e[k3s_partuuid]}" == "$K3SLR_K3S_PARTUUID" && "${e[k3s_type]}" == ext4 ]] || return 1
|
|
[[ "${e[k3s_model]}" == "$K3SLR_K3S_MODEL" && "${e[k3s_serial]}" == "$K3SLR_K3S_SERIAL" && "${e[k3s_wwn]}" == "$K3SLR_K3S_WWN" ]] || return 1
|
|
[[ "${e[recovery_major_minor]}" != "${e[k3s_major_minor]}" && "${e[smart_health]}" == PASSED ]] || return 1
|
|
[[ "${e[smart_reallocated]}" == 0 && "${e[smart_pending]}" == 0 && "${e[smart_uncorrectable]}" == 0 ]] || return 1
|
|
if [[ "$mode" == open ]]; then [[ "${e[ntfs_probe]}" == not_applicable || "${e[ntfs_probe]}" == pass ]];
|
|
else [[ "${e[ntfs_probe]}" == pass ]]; fi
|
|
}
|
|
|
|
_k3slrv_option_present() {
|
|
[[ ",${1}," == *",${2},"* ]]
|
|
}
|
|
|
|
_k3slrv_outer_masks_secure() {
|
|
local options="$1" option umask_count=0 dmask_count=0 fmask_count=0 invalid=false
|
|
local -a option_list=()
|
|
IFS=',' read -r -a option_list <<<"$options"
|
|
for option in "${option_list[@]}"; do
|
|
case "$option" in
|
|
umask=077|umask=0077) umask_count=$((umask_count + 1)) ;;
|
|
dmask=077|dmask=0077) dmask_count=$((dmask_count + 1)) ;;
|
|
fmask=077|fmask=0077|fmask=177|fmask=0177) fmask_count=$((fmask_count + 1)) ;;
|
|
umask=*|dmask=*|fmask=*) invalid=true ;;
|
|
esac
|
|
done
|
|
"$invalid" && return 1
|
|
if (( umask_count == 1 && dmask_count == 0 && fmask_count == 0 )); then return 0; fi
|
|
(( umask_count == 0 && dmask_count == 1 && fmask_count == 1 ))
|
|
}
|
|
|
|
_k3slrv_validate_open_snapshot() {
|
|
local input="$1"
|
|
local -A snapshot=()
|
|
_k3slrv_parse_evidence "$input" snapshot \
|
|
container_chain_pinned container_inode container_device loop_back_inode loop_back_major_minor outer_major_minor \
|
|
loop_major_minor mapping_loop_major_minor mapping_major_minor inner_major_minor \
|
|
loop_offset loop_sizelimit snapshot_stable || return 1
|
|
[[ "${snapshot[container_chain_pinned]}" == 1 && "${snapshot[snapshot_stable]}" == 1 ]] || return 1
|
|
[[ "${snapshot[container_inode]}" =~ ^[0-9]+$ &&
|
|
"${snapshot[loop_back_inode]}" == "${snapshot[container_inode]}" ]] || return 1
|
|
[[ "${snapshot[container_device]}" =~ ^[0-9]+:[0-9]+$ &&
|
|
"${snapshot[loop_back_major_minor]}" == "${snapshot[container_device]}" &&
|
|
"${snapshot[container_device]}" == "${snapshot[outer_major_minor]}" ]] || return 1
|
|
[[ "${snapshot[mapping_loop_major_minor]}" == "${snapshot[loop_major_minor]}" ]] || return 1
|
|
[[ "${snapshot[mapping_major_minor]}" == "${snapshot[inner_major_minor]}" ]] || return 1
|
|
[[ "${snapshot[loop_offset]}" == 0 && "${snapshot[loop_sizelimit]}" == 0 ]]
|
|
}
|
|
|
|
_k3slrv_validate_state() {
|
|
local mode="$1" input="$2" device_input="$3" approved_partition='' line snapshot_raw
|
|
local -A e=()
|
|
if [[ "$mode" != open ]]; then
|
|
_k3slrv_parse_evidence "$input" e outer_mounted inner_mounted loop_count mapping_present proof_mapping_present || return 1
|
|
[[ "${e[outer_mounted]}" == 0 && "${e[inner_mounted]}" == 0 && "${e[loop_count]}" == 0 &&
|
|
"${e[mapping_present]}" == 0 && "${e[proof_mapping_present]}" == 0 ]]
|
|
return
|
|
fi
|
|
local keys=(outer_mounted outer_source outer_fstype outer_options inner_mounted inner_source inner_fstype inner_options
|
|
container_kind container_symlink container_uid container_gid container_mode container_size container_allocated
|
|
loop_count loop_device mapping_present mapping_type mapping_device proof_mapping_present inner_type inner_label
|
|
inner_root_uid inner_root_gid inner_root_mode inner_root_kind container_chain_pinned container_inode
|
|
container_device loop_back_inode loop_back_major_minor outer_major_minor loop_major_minor mapping_loop_major_minor
|
|
mapping_major_minor inner_major_minor loop_offset loop_sizelimit snapshot_stable)
|
|
_k3slrv_parse_evidence "$input" e "${keys[@]}" || return 1
|
|
while IFS= read -r line; do
|
|
[[ "$line" == recovery_partition=* ]] || continue
|
|
[[ -z "$approved_partition" ]] || return 1
|
|
approved_partition="${line#recovery_partition=}"
|
|
done <<<"$device_input"
|
|
[[ -n "$approved_partition" && "${e[outer_mounted]}" == 1 &&
|
|
"${e[outer_source]}" == "$approved_partition" && "${e[outer_fstype]}" == ntfs3 ]] || return 1
|
|
for option in rw nodev nosuid noexec "uid=${K3SLR_OWNER_UID}" "gid=${K3SLR_OWNER_GID}"; do
|
|
_k3slrv_option_present "${e[outer_options]}" "$option" || return 1
|
|
done
|
|
_k3slrv_outer_masks_secure "${e[outer_options]}" || return 1
|
|
[[ "${e[inner_mounted]}" == 1 && "${e[inner_source]}" == "/dev/mapper/${K3SLR_MAPPING_NAME}" && "${e[inner_fstype]}" == ext4 ]] || return 1
|
|
for option in rw nodev nosuid noexec; do _k3slrv_option_present "${e[inner_options]}" "$option" || return 1; done
|
|
[[ "${e[container_kind]}" == regular && "${e[container_symlink]}" == 0 &&
|
|
"${e[container_uid]}" == "$K3SLR_OWNER_UID" && "${e[container_gid]}" == "$K3SLR_OWNER_GID" ]] || return 1
|
|
[[ "${e[container_mode]}" =~ ^[0-7]{3,4}$ ]] || return 1
|
|
(( (10#${e[container_mode]: -2:1} & 7) == 0 && (10#${e[container_mode]: -1:1} & 7) == 0 )) || return 1
|
|
[[ "${e[container_size]}" == "$K3SLR_CONTAINER_SIZE_BYTES" && "${e[container_allocated]}" =~ ^[0-9]+$ ]] || return 1
|
|
(( e[container_allocated] >= K3SLR_CONTAINER_SIZE_BYTES )) || return 1
|
|
[[ "${e[loop_count]}" == 1 && "${e[mapping_present]}" == 1 && "${e[mapping_type]}" == LUKS2 &&
|
|
"${e[mapping_device]}" == "${e[loop_device]}" && "${e[proof_mapping_present]}" == 0 ]] || return 1
|
|
[[ "${e[inner_type]}" == ext4 && "${e[inner_label]}" == "$K3SLR_INNER_LABEL" ]] || return 1
|
|
[[ "${e[inner_root_uid]}" == 0 && "${e[inner_root_gid]}" == 0 && "${e[inner_root_mode]}" == 700 && "${e[inner_root_kind]}" == directory ]] || return 1
|
|
printf -v snapshot_raw '%s\n' \
|
|
"container_chain_pinned=${e[container_chain_pinned]}" \
|
|
"container_inode=${e[container_inode]}" \
|
|
"container_device=${e[container_device]}" \
|
|
"loop_back_inode=${e[loop_back_inode]}" \
|
|
"loop_back_major_minor=${e[loop_back_major_minor]}" \
|
|
"outer_major_minor=${e[outer_major_minor]}" \
|
|
"loop_major_minor=${e[loop_major_minor]}" \
|
|
"mapping_loop_major_minor=${e[mapping_loop_major_minor]}" \
|
|
"mapping_major_minor=${e[mapping_major_minor]}" \
|
|
"inner_major_minor=${e[inner_major_minor]}" \
|
|
"loop_offset=${e[loop_offset]}" \
|
|
"loop_sizelimit=${e[loop_sizelimit]}" \
|
|
"snapshot_stable=${e[snapshot_stable]}"
|
|
_k3slrv_validate_open_snapshot "${snapshot_raw%$'\n'}"
|
|
}
|
|
|
|
_k3slrv_verify_manifest_targets() {
|
|
local bundle="$1" manifest="$2" records record expected_hash relative target component current canonical
|
|
local before after hash_line actual_hash count=0
|
|
local -a components=() manifest_records=()
|
|
_k3slrv_capture records _k3slrv_root /usr/bin/mawk '
|
|
NF!=2 || length($1)!=64 || $1 !~ /^[0-9a-f]+$/ || $2 !~ /^\.\/[A-Za-z0-9._\/-]+$/ ||
|
|
$2 ~ /\.\./ || $2 ~ /\/\// || $2 ~ /\/\.\// || $2 ~ /\/\.$/ || $2 ~ /\/$/ { bad=1; exit }
|
|
{ print $1 "|" $2 }
|
|
END { exit (bad || NR==0) }
|
|
' "$manifest" 2>/dev/null || return 1
|
|
mapfile -t manifest_records < <(printf '%s' "$records")
|
|
(( ${#manifest_records[@]} > 0 )) || return 1
|
|
for record in "${manifest_records[@]}"; do
|
|
[[ "$record" == *'|'* ]] || return 1
|
|
expected_hash="${record%%|*}"
|
|
relative="${record#*|}"
|
|
[[ "$expected_hash" =~ ^[0-9a-f]{64}$ && "$relative" =~ ^\./[A-Za-z0-9._/-]+$ ]] || return 1
|
|
target="${bundle}/${relative#./}"
|
|
current="$bundle"
|
|
IFS='/' read -r -a components <<<"${relative#./}"
|
|
for component in "${components[@]}"; do
|
|
[[ -n "$component" && "$component" != . && "$component" != .. ]] || return 1
|
|
current="${current}/${component}"
|
|
_k3slrv_root /usr/bin/test ! -L "$current" >/dev/null 2>&1 || return 1
|
|
canonical="$(_k3slrv_one_line_root /usr/bin/readlink -e -- "$current")" || return 1
|
|
[[ "$canonical" == "$current" ]] || return 1
|
|
done
|
|
_k3slrv_root /usr/bin/test -f "$target" >/dev/null 2>&1 || return 1
|
|
before="$(_k3slrv_one_line_root /usr/bin/stat --format='%d:%i|%F' -- "$target")" || return 1
|
|
[[ "$before" == *'|regular file' ]] || return 1
|
|
hash_line="$(_k3slrv_one_line_root /usr/bin/sha256sum -- "$target")" || return 1
|
|
actual_hash="${hash_line%% *}"
|
|
[[ "$actual_hash" == "$expected_hash" ]] || return 1
|
|
after="$(_k3slrv_one_line_root /usr/bin/stat --format='%d:%i|%F' -- "$target")" || return 1
|
|
[[ "$after" == "$before" ]] || return 1
|
|
count=$((count + 1))
|
|
done
|
|
(( count > 0 ))
|
|
}
|
|
|
|
_k3slrv_collect_latest_bundle_evidence() {
|
|
local metadata="${K3SLR_INNER_MOUNT}/.latest-post-bundle.env" parsed relative expected_identity bundle canonical_bundle actual_identity after_identity
|
|
local bundle_metadata manifest stat_value
|
|
_k3slrv_root /usr/bin/test -f "$metadata" >/dev/null 2>&1 || return 1
|
|
_k3slrv_root /usr/bin/test ! -L "$metadata" >/dev/null 2>&1 || return 1
|
|
stat_value="$(_k3slrv_one_line_root /usr/bin/stat --format='%u:%g:%a:%F' -- "$metadata")" || return 1
|
|
[[ "$stat_value" == '0:0:600:regular file' ]] || return 1
|
|
_k3slrv_capture parsed _k3slrv_root /usr/bin/mawk -F= '
|
|
BEGIN { ok=1 }
|
|
!/^[a-z_]+=[A-Za-z0-9._:\/-]+$/ { ok=0; next }
|
|
$1=="schema" && !s++ && $2=="k3slr-latest-post-bundle-v1" { schema=$2; next }
|
|
$1=="relative_path" && !p++ && $2 ~ /^k3s-secrets-encryption-[0-9]{8}T[0-9]{6}Z\/post$/ { path=$2; next }
|
|
$1=="directory_identity" && !i++ && $2 ~ /^[0-9]+:[0-9]+$/ { identity=$2; next }
|
|
{ ok=0 }
|
|
END { if (ok && NR==3 && s==1 && p==1 && i==1) print path "|" identity; else exit 1 }
|
|
' "$metadata" 2>/dev/null || return 1
|
|
_k3slrv_normalize_one_record parsed "$parsed" || return 1
|
|
[[ "$parsed" == *'|'* ]] || return 1
|
|
relative="${parsed%%|*}"; expected_identity="${parsed#*|}"
|
|
[[ "$relative" =~ ^k3s-secrets-encryption-[0-9]{8}T[0-9]{6}Z/post$ && "$relative" != *..* ]] || return 1
|
|
bundle="${K3SLR_INNER_MOUNT}/${relative}"
|
|
canonical_bundle="$(_k3slrv_one_line_root /usr/bin/readlink -f -- "$bundle")" || return 1
|
|
[[ "$canonical_bundle" == "$bundle" ]] || return 1
|
|
actual_identity="$(_k3slrv_one_line_root /usr/bin/stat --dereference --format='%d:%i' -- "$bundle")" || return 1
|
|
[[ "$actual_identity" == "$expected_identity" ]] || return 1
|
|
stat_value="$(_k3slrv_one_line_root /usr/bin/stat --dereference --format='%u:%g:%a:%F' -- "$bundle")" || return 1
|
|
[[ "$stat_value" == '0:0:700:directory' ]] || return 1
|
|
bundle_metadata="${bundle}/bundle.env"; manifest="${bundle}/verification.manifest"
|
|
for file in "$bundle_metadata" "$manifest"; do
|
|
_k3slrv_root /usr/bin/test -f "$file" >/dev/null 2>&1 || return 1
|
|
_k3slrv_root /usr/bin/test ! -L "$file" >/dev/null 2>&1 || return 1
|
|
stat_value="$(_k3slrv_one_line_root /usr/bin/stat --format='%u:%g:%a:%F' -- "$file")" || return 1
|
|
[[ "$stat_value" == '0:0:600:regular file' ]] || return 1
|
|
done
|
|
_k3slrv_root /usr/bin/mawk -F= '
|
|
BEGIN { ok=1 }
|
|
!/^[a-z][a-z0-9_]*=[A-Za-z0-9:+._-]+$/ { ok=0; next }
|
|
$1=="schema" && !a++ && $2=="platform-k3s-bundle-v1" { next }
|
|
$1=="bundle_id" && !b++ && $2 ~ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/ { next }
|
|
$1=="phase" && !c++ && $2=="post" { next }
|
|
$1=="k3s_version" && !d++ && $2=="v1.36.2+k3s1" { next }
|
|
$1=="datastore" && !e++ && ($2=="sqlite" || $2=="embedded-etcd") { next }
|
|
$1=="created_at_utc" && !f++ && $2 ~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$/ { next }
|
|
$1=="secret_count" && !g++ && $2 ~ /^[0-9]+$/ { next }
|
|
{ ok=0 }
|
|
END { exit !(ok && NR==7 && a==1 && b==1 && c==1 && d==1 && e==1 && f==1 && g==1) }
|
|
' "$bundle_metadata" >/dev/null 2>&1 || return 1
|
|
_k3slrv_verify_manifest_targets "$bundle" "$manifest" || return 1
|
|
after_identity="$(_k3slrv_one_line_root /usr/bin/stat --dereference --format='%d:%i' -- "$bundle")" || return 1
|
|
[[ "$after_identity" == "$expected_identity" ]] || return 1
|
|
printf '%s\n' \
|
|
'metadata_schema=k3slr-latest-post-bundle-v1' \
|
|
'relative_path_valid=1' \
|
|
'directory_identity_match=1' \
|
|
'directory_secure=1' \
|
|
'bundle_schema=platform-k3s-bundle-v1' \
|
|
'bundle_phase=post' \
|
|
'manifest_safe=1' \
|
|
'manifest_verified=1' \
|
|
'directory_identity_stable=1'
|
|
}
|
|
|
|
_k3slrv_verify_latest_bundle() {
|
|
local raw
|
|
local -A evidence=()
|
|
_k3slrv_capture raw _k3slrv_collect_latest_bundle_evidence || return 1
|
|
_k3slrv_parse_evidence "$raw" evidence \
|
|
metadata_schema relative_path_valid directory_identity_match directory_secure \
|
|
bundle_schema bundle_phase manifest_safe manifest_verified directory_identity_stable || return 1
|
|
[[ "${evidence[metadata_schema]}" == k3slr-latest-post-bundle-v1 &&
|
|
"${evidence[relative_path_valid]}" == 1 &&
|
|
"${evidence[directory_identity_match]}" == 1 &&
|
|
"${evidence[directory_secure]}" == 1 &&
|
|
"${evidence[bundle_schema]}" == platform-k3s-bundle-v1 &&
|
|
"${evidence[bundle_phase]}" == post &&
|
|
"${evidence[manifest_safe]}" == 1 &&
|
|
"${evidence[manifest_verified]}" == 1 &&
|
|
"${evidence[directory_identity_stable]}" == 1 ]]
|
|
}
|
|
|
|
k3slr_local_recovery_main() {
|
|
local mode='' check_latest=false argument device_raw state_raw latest=not_checked
|
|
for argument in "$@"; do
|
|
case "$argument" in
|
|
--expect-device-ready) [[ -z "$mode" ]] || { _k3slrv_usage >&2; return 2; }; mode=device_ready ;;
|
|
--expect-closed) [[ -z "$mode" ]] || { _k3slrv_usage >&2; return 2; }; mode=closed ;;
|
|
--expect-open) [[ -z "$mode" ]] || { _k3slrv_usage >&2; return 2; }; mode=open ;;
|
|
--check-latest-bundle) "$check_latest" && { _k3slrv_usage >&2; return 2; }; check_latest=true ;;
|
|
--help|-h) _k3slrv_usage; return 0 ;;
|
|
*) _k3slrv_usage >&2; return 2 ;;
|
|
esac
|
|
done
|
|
[[ -n "$mode" ]] || { _k3slrv_usage >&2; return 2; }
|
|
if "$check_latest" && [[ "$mode" != open ]]; then _k3slrv_usage >&2; return 2; fi
|
|
PATH="$K3SLRV_SECURE_PATH"; export PATH
|
|
_k3slrv_verify_trusted_binaries || { _k3slrv_fail; return 1; }
|
|
# Direct execution always replaces any inherited/predefined parser with the
|
|
# repository library in this unprivileged process.
|
|
# shellcheck source=/dev/null
|
|
builtin source -- "$K3SLRV_LIBRARY_PATH" || { _k3slrv_fail; return 1; }
|
|
_k3slr_load_contract "$K3SLRV_CONTRACT_PATH" || { _k3slrv_fail; return 1; }
|
|
_k3slrv_require_cached_sudo || { _k3slrv_fail; return 1; }
|
|
_k3slrv_capture device_raw _k3slrv_collect_device_evidence "$mode" || { _k3slrv_fail; return 1; }
|
|
_k3slrv_validate_device "$mode" "$device_raw" || { _k3slrv_fail; return 1; }
|
|
_k3slrv_capture state_raw _k3slrv_collect_state_evidence "$mode" || { _k3slrv_fail; return 1; }
|
|
_k3slrv_validate_state "$mode" "$state_raw" "$device_raw" || { _k3slrv_fail; return 1; }
|
|
if "$check_latest"; then
|
|
_k3slrv_verify_latest_bundle || { _k3slrv_fail; return 1; }
|
|
latest=verified
|
|
fi
|
|
printf 'Recovery device: match\nRecovery state: %s\nLineage: match\nLatest bundle: %s\n' "$mode" "$latest"
|
|
}
|
|
|
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
k3slr_local_recovery_main "$@"
|
|
fi
|