Files
platform-core/scripts/lib/k3s-local-recovery-header-proof.sh

345 lines
15 KiB
Bash

#!/usr/bin/env bash
# Source-only foundation for the standalone K3s LUKS header proof.
readonly K3SLRH_PROOF_RELATIVE='HyeonworksRecovery/containers/k3s-recovery.header-proof.luks'
readonly K3SLRH_PRE_KDBX_PREFIX='hyeonworks-recovery.header-proof-pre.'
readonly K3SLRH_POST_KDBX_PREFIX='hyeonworks-recovery.header-proof-post.'
readonly K3SLRH_HEADER_BACKUP_NAME='header-original.bin'
readonly K3SLRH_HEADER_EXPORT_NAME='header-exported.bin'
readonly K3SLRH_SQLITE_PACKAGE_VERSION='3.45.1-1ubuntu2.7'
readonly K3SLRH_SHARED_LIBRARY_SHA256='a206d81539e6c37fa085457ba01cff9de57de50f7235e3a4085989780e99585d'
readonly K3SLRH_A1_LIBRARY_SHA256='8b39fc46229ebd4c77ff20040f942c4ef7b756d564d344a8ecdd11a7428e2968'
readonly K3SLRH_A1_HELPER_SHA256='06831fa35943a328ecd183d3c6f721b24c4607b2f349abf6201a12e690d08ebe'
readonly K3SLRH_CONTRACT_SHA256='c045f42c2dc3eda09c37a123bcc05d88d94f275e61504a2d5816948237e516df'
readonly K3SLRH_VALIDATOR_SHA256='6b869032fc325759ee25b219fad4ea6190a3389a561c496f0664a51b802f73e6'
_k3slrh_command() {
"$@"
}
_k3slrh_output_name_is_safe() {
local _k3slrh_output_candidate="${1-}"
(( $# == 1 )) || return 1
[[ "$_k3slrh_output_candidate" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || return 1
[[ "$_k3slrh_output_candidate" != _k3slrh_* ]]
}
_k3slrh_stderr_marker() {
local _k3slrh_stderr_chunk='' _k3slrh_stderr_rc=0
if IFS= read -r -d '' -n 1 _k3slrh_stderr_chunk; then
_k3slrh_stderr_rc=0
else
_k3slrh_stderr_rc=$?
fi
if (( _k3slrh_stderr_rc == 0 )); then
printf '\0STDERR\0'
while IFS= read -r -d '' -n 4096 _k3slrh_stderr_chunk; do
:
done
elif [[ -n "$_k3slrh_stderr_chunk" ]]; then
printf '\0STDERR\0'
fi
}
_k3slrh_command_packet() {
local _k3slrh_packet_rc=0
(( $# > 0 )) || return 1
if _k3slrh_command "$@" 2> >(_k3slrh_stderr_marker); then
_k3slrh_packet_rc=0
else
_k3slrh_packet_rc=$?
fi
printf '\0RC=%s\0' "$_k3slrh_packet_rc"
}
_k3slrh_capture_command() {
local _k3slrh_capture_output="${1-}" _k3slrh_capture_expected_rc="${2-}"
local -a _k3slrh_capture_records=()
shift 2 || return 1
(( $# > 0 )) || return 1
_k3slrh_output_name_is_safe "$_k3slrh_capture_output" || return 1
[[ "$_k3slrh_capture_expected_rc" =~ ^(0|[1-9][0-9]{0,2})$ ]] || return 1
(( 10#$_k3slrh_capture_expected_rc <= 255 )) || return 1
mapfile -d '' -t _k3slrh_capture_records < <(_k3slrh_command_packet "$@")
(( ${#_k3slrh_capture_records[@]} == 2 )) || return 1
[[ "${_k3slrh_capture_records[1]}" == "RC=${_k3slrh_capture_expected_rc}" ]] || return 1
printf -v "$_k3slrh_capture_output" '%s' "${_k3slrh_capture_records[0]}"
}
_k3slrh_capture_one_line() {
local _k3slrh_line_output="${1-}" _k3slrh_line_value=''
shift || return 1
_k3slrh_output_name_is_safe "$_k3slrh_line_output" || return 1
_k3slrh_capture_command _k3slrh_line_value 0 "$@" || return 1
[[ "$_k3slrh_line_value" == *$'\n' ]] || return 1
[[ "$_k3slrh_line_value" != *$'\r'* ]] || return 1
_k3slrh_line_value="${_k3slrh_line_value%$'\n'}"
[[ -n "$_k3slrh_line_value" && "$_k3slrh_line_value" != *$'\n'* ]] || return 1
printf -v "$_k3slrh_line_output" '%s' "$_k3slrh_line_value"
}
_k3slrh_parse_cli() {
local _k3slrh_cli_output="${1-}" _k3slrh_cli_mode=''
shift || return 1
_k3slrh_output_name_is_safe "$_k3slrh_cli_output" || return 1
case "$#" in
0) _k3slrh_cli_mode=dry-run ;;
1)
[[ "$1" == --execute ]] || return 1
_k3slrh_cli_mode=execute
;;
*) return 1 ;;
esac
printf -v "$_k3slrh_cli_output" '%s' "$_k3slrh_cli_mode"
}
_k3slrh_target_state_is_consistent() {
local _k3slrh_target_utc="${1-}" _k3slrh_target_pre="${2-}"
local _k3slrh_target_post="${3-}" _k3slrh_target_expected_pre=''
local _k3slrh_target_expected_post=''
(( $# == 3 )) || return 1
[[ "$_k3slrh_target_utc" =~ ^[0-9]{8}T[0-9]{6}Z$ ]] || return 1
_k3slrh_target_expected_pre="HyeonworksRecovery/vault/backups/${K3SLRH_PRE_KDBX_PREFIX}${_k3slrh_target_utc}.kdbx"
_k3slrh_target_expected_post="HyeonworksRecovery/vault/backups/${K3SLRH_POST_KDBX_PREFIX}${_k3slrh_target_utc}.kdbx"
[[ "$_k3slrh_target_pre" == "$_k3slrh_target_expected_pre" ]] || return 1
[[ "$_k3slrh_target_post" == "$_k3slrh_target_expected_post" ]] || return 1
[[ "$_k3slrh_target_pre" != "$_k3slrh_target_post" ]]
}
_k3slrh_derive_targets() {
local _k3slrh_derive_utc="${1-}" _k3slrh_derive_pre='' _k3slrh_derive_post=''
(( $# == 1 )) || return 1
[[ "$_k3slrh_derive_utc" =~ ^[0-9]{8}T[0-9]{6}Z$ ]] || return 1
[[ "${K3SLRH_RUN_UTC+x}" != x && "${K3SLRH_PRE_KDBX_RELATIVE+x}" != x &&
"${K3SLRH_POST_KDBX_RELATIVE+x}" != x ]] || return 1
_k3slrh_derive_pre="HyeonworksRecovery/vault/backups/${K3SLRH_PRE_KDBX_PREFIX}${_k3slrh_derive_utc}.kdbx"
_k3slrh_derive_post="HyeonworksRecovery/vault/backups/${K3SLRH_POST_KDBX_PREFIX}${_k3slrh_derive_utc}.kdbx"
_k3slrh_target_state_is_consistent "$_k3slrh_derive_utc" "$_k3slrh_derive_pre" "$_k3slrh_derive_post" || return 1
declare -gr K3SLRH_RUN_UTC="$_k3slrh_derive_utc"
declare -gr K3SLRH_PRE_KDBX_RELATIVE="$_k3slrh_derive_pre"
declare -gr K3SLRH_POST_KDBX_RELATIVE="$_k3slrh_derive_post"
}
_k3slrh_source_file_is_exact() {
local _k3slrh_source_path="${1-}" _k3slrh_source_hash="${2-}"
local _k3slrh_source_canonical='' _k3slrh_source_metadata='' _k3slrh_source_digest=''
(( $# == 2 )) || return 1
[[ "$_k3slrh_source_path" == /* && "$_k3slrh_source_hash" =~ ^[0-9a-f]{64}$ ]] || return 1
_k3slrh_command /usr/bin/test -f "$_k3slrh_source_path" >/dev/null 2>&1 || return 1
_k3slrh_command /usr/bin/test ! -L "$_k3slrh_source_path" >/dev/null 2>&1 || return 1
_k3slrh_capture_one_line _k3slrh_source_canonical /usr/bin/readlink -e -- "$_k3slrh_source_path" || return 1
[[ "$_k3slrh_source_canonical" == "$_k3slrh_source_path" ]] || return 1
_k3slrh_capture_one_line _k3slrh_source_metadata /usr/bin/stat '--format=%F|%u|%g|%a' -- "$_k3slrh_source_path" || return 1
[[ "$_k3slrh_source_metadata" == 'regular file|'* || "$_k3slrh_source_metadata" == 'regular empty file|'* ]] || return 1
_k3slrh_capture_one_line _k3slrh_source_digest /usr/bin/sha256sum -- "$_k3slrh_source_path" || return 1
[[ "$_k3slrh_source_digest" == "${_k3slrh_source_hash} ${_k3slrh_source_path}" ]]
}
_k3slrh_required_sources_are_exact() {
local _k3slrh_source_root="${K3SLRH_REPOSITORY_ROOT:-}"
(( $# == 0 )) || return 1
[[ "$_k3slrh_source_root" == /* ]] || return 1
_k3slrh_source_file_is_exact "${_k3slrh_source_root}/scripts/lib/k3s-local-recovery.sh" "$K3SLRH_SHARED_LIBRARY_SHA256" || return 1
_k3slrh_source_file_is_exact "${_k3slrh_source_root}/scripts/lib/k3s-local-recovery-a1.sh" "$K3SLRH_A1_LIBRARY_SHA256" || return 1
_k3slrh_source_file_is_exact "${_k3slrh_source_root}/scripts/libexec/k3s-local-recovery-a1-check.sh" "$K3SLRH_A1_HELPER_SHA256" || return 1
_k3slrh_source_file_is_exact "${_k3slrh_source_root}/infrastructure/security/k3s/local-recovery.env" "$K3SLRH_CONTRACT_SHA256"
}
_k3slrh_required_binaries_are_trusted() {
local _k3slrh_binary=''
local -a _k3slrh_binaries=(
/usr/bin/bash
/usr/bin/date
/usr/bin/dpkg-query
/usr/bin/env
/usr/bin/find
/usr/bin/findmnt
/usr/bin/keepassxc-cli
/usr/bin/readlink
/usr/bin/sha256sum
/usr/bin/sqlite3
/usr/bin/stat
/usr/bin/sudo
/usr/bin/systemctl
/usr/bin/test
/usr/bin/timeout
/usr/local/bin/k3s
/usr/sbin/cryptsetup
/usr/sbin/losetup
)
(( $# == 0 )) || return 1
for _k3slrh_binary in "${_k3slrh_binaries[@]}"; do
_k3slr_trusted_root_executable "$_k3slrh_binary" || return 1
done
}
_k3slrh_package_is_exact() {
local _k3slrh_package_name="${1-}" _k3slrh_package_expected="${2-}"
local _k3slrh_package_record=''
(( $# == 2 )) || return 1
case "$_k3slrh_package_name|$_k3slrh_package_expected" in
'keepassxc|2.7.6+dfsg.1-1build3'|'cryptsetup-bin|2:2.7.0-1ubuntu4.2'|"sqlite3|${K3SLRH_SQLITE_PACKAGE_VERSION}") ;;
*) return 1 ;;
esac
_k3slrh_capture_command _k3slrh_package_record 0 /usr/bin/dpkg-query --show '--showformat=${Status}|${Version}\n' "$_k3slrh_package_name" || return 1
[[ "$_k3slrh_package_record" == "install ok installed|${_k3slrh_package_expected}"$'\n' ]]
}
_k3slrh_packages_are_exact() {
(( $# == 0 )) || return 1
_k3slrh_package_is_exact keepassxc '2.7.6+dfsg.1-1build3' || return 1
_k3slrh_package_is_exact cryptsetup-bin '2:2.7.0-1ubuntu4.2' || return 1
_k3slrh_package_is_exact sqlite3 "$K3SLRH_SQLITE_PACKAGE_VERSION"
}
_k3slrh_cached_sudo_is_available() {
(( $# == 0 )) || return 1
_k3slrh_command /usr/bin/sudo --non-interactive --validate >/dev/null 2>&1
}
_k3slrh_k3s_health() {
local _k3slrh_health_systemctl='' _k3slrh_health_readyz=''
(( $# == 0 )) || return 1
_k3slrh_capture_command _k3slrh_health_systemctl 0 /usr/bin/systemctl is-active --quiet k3s || return 1
[[ -z "$_k3slrh_health_systemctl" ]] || return 1
_k3slrh_capture_command _k3slrh_health_readyz 0 \
/usr/bin/sudo --non-interactive -- \
/usr/bin/env -i 'PATH=/usr/sbin:/usr/bin:/sbin:/bin' LC_ALL=C \
/usr/bin/timeout --signal=TERM --kill-after=1s 30s \
/usr/local/bin/k3s kubectl get --raw=/readyz || return 1
[[ "$_k3slrh_health_readyz" == $'ok\n' ]]
}
_k3slrh_run_closed_validator() {
local _k3slrh_validator_path='' _k3slrh_validator_record=''
local _k3slrh_validator_command_rc=0 _k3slrh_validator_post_rc=0
(( $# == 0 )) || return 1
[[ "${K3SLRH_REPOSITORY_ROOT:-}" == /* ]] || return 1
_k3slrh_validator_path="${K3SLRH_REPOSITORY_ROOT}/scripts/validate/k3s-local-recovery.sh"
_k3slrh_source_file_is_exact "$_k3slrh_validator_path" "$K3SLRH_VALIDATOR_SHA256" || return 1
_k3slrh_capture_command _k3slrh_validator_record 0 \
/usr/bin/env -i 'PATH=/usr/sbin:/usr/bin:/sbin:/bin' LC_ALL=C \
/usr/bin/bash --noprofile --norc -- "$_k3slrh_validator_path" --expect-closed ||
_k3slrh_validator_command_rc=1
_k3slrh_source_file_is_exact "$_k3slrh_validator_path" "$K3SLRH_VALIDATOR_SHA256" ||
_k3slrh_validator_post_rc=1
(( _k3slrh_validator_command_rc == 0 && _k3slrh_validator_post_rc == 0 )) || return 1
[[ "$_k3slrh_validator_record" == $'Recovery device: match\nRecovery state: closed\nLineage: match\nLatest bundle: not_checked\n' ]]
}
_k3slrh_mount_is_absent() {
local _k3slrh_mount_path="${1-}" _k3slrh_mount_record=''
(( $# == 1 )) || return 1
[[ "$_k3slrh_mount_path" == /* ]] || return 1
_k3slrh_capture_command _k3slrh_mount_record 1 /usr/bin/findmnt -rn -M "$_k3slrh_mount_path" -o TARGET || return 1
[[ -z "$_k3slrh_mount_record" ]]
}
_k3slrh_mapping_is_absent() {
local _k3slrh_mapping_path="${1-}"
(( $# == 1 )) || return 1
case "$_k3slrh_mapping_path" in
"/dev/mapper/${K3SLR_MAPPING_NAME}"|"/dev/mapper/${K3SLR_PROOF_MAPPING_NAME}") ;;
*) return 1 ;;
esac
_k3slrh_command /usr/bin/test ! -e "$_k3slrh_mapping_path" >/dev/null 2>&1 || return 1
_k3slrh_command /usr/bin/test ! -L "$_k3slrh_mapping_path" >/dev/null 2>&1
}
_k3slrh_related_loops_are_absent() {
local _k3slrh_loop_record='' _k3slrh_loop_line=''
local _k3slrh_loop_original="${K3SLR_OUTER_MOUNT}/${K3SLR_CONTAINER_RELATIVE}"
local _k3slrh_loop_proof="${K3SLR_OUTER_MOUNT}/${K3SLRH_PROOF_RELATIVE}"
(( $# == 0 )) || return 1
_k3slrh_capture_command _k3slrh_loop_record 0 /usr/sbin/losetup --list --noheadings --output NAME,BACK-FILE || return 1
while IFS= read -r _k3slrh_loop_line; do
[[ -n "$_k3slrh_loop_line" ]] || continue
case "$_k3slrh_loop_line" in
*" ${_k3slrh_loop_original}"|*" ${_k3slrh_loop_proof}") return 1 ;;
esac
done <<<"$_k3slrh_loop_record"
}
_k3slrh_outer_mountpoint_is_clean() {
local _k3slrh_outer_canonical='' _k3slrh_outer_metadata='' _k3slrh_outer_children=''
(( $# == 0 )) || return 1
_k3slrh_command /usr/bin/test -d "$K3SLR_OUTER_MOUNT" >/dev/null 2>&1 || return 1
_k3slrh_command /usr/bin/test ! -L "$K3SLR_OUTER_MOUNT" >/dev/null 2>&1 || return 1
_k3slrh_capture_one_line _k3slrh_outer_canonical /usr/bin/readlink -e -- "$K3SLR_OUTER_MOUNT" || return 1
[[ "$_k3slrh_outer_canonical" == "$K3SLR_OUTER_MOUNT" ]] || return 1
_k3slrh_capture_one_line _k3slrh_outer_metadata /usr/bin/stat '--format=%F|%u|%g|%a' -- "$K3SLR_OUTER_MOUNT" || return 1
[[ "$_k3slrh_outer_metadata" == 'directory|0|0|755' ]] || return 1
_k3slrh_capture_command _k3slrh_outer_children 0 /usr/bin/find "$K3SLR_OUTER_MOUNT" -mindepth 1 -maxdepth 1 -print -quit || return 1
[[ -z "$_k3slrh_outer_children" ]]
}
_k3slrh_runtime_residue_is_absent() {
local _k3slrh_runtime_record=''
(( $# == 0 )) || return 1
_k3slrh_capture_command _k3slrh_runtime_record 0 /usr/bin/find /run -mindepth 1 -maxdepth 1 -name 'hyeonworks-k3s-recovery.*' -print -quit || return 1
[[ -z "$_k3slrh_runtime_record" ]]
}
_k3slrh_closed_resources_are_absent() {
(( $# == 0 )) || return 1
_k3slrh_mount_is_absent "$K3SLR_OUTER_MOUNT" || return 1
_k3slrh_mount_is_absent "$K3SLR_INNER_MOUNT" || return 1
_k3slrh_mapping_is_absent "/dev/mapper/${K3SLR_MAPPING_NAME}" || return 1
_k3slrh_mapping_is_absent "/dev/mapper/${K3SLR_PROOF_MAPPING_NAME}" || return 1
_k3slrh_related_loops_are_absent || return 1
_k3slrh_outer_mountpoint_is_clean || return 1
_k3slrh_runtime_residue_is_absent
}
_k3slrh_pre_mount_checks() {
(( $# == 0 )) || return 1
_k3slrh_required_sources_are_exact || return 1
_k3slrh_required_binaries_are_trusted || return 1
_k3slrh_packages_are_exact || return 1
_k3slrh_cached_sudo_is_available || return 1
_k3slrh_k3s_health || return 1
_k3slrh_run_closed_validator || return 1
_k3slrh_closed_resources_are_absent
}
_k3slrh_pre_mount_capability() {
(( $# == 0 )) || return 1
_k3slrh_pre_mount_checks || return 1
printf 'Header proof capability: pass\n'
}
_k3slrh_current_utc() {
local _k3slrh_utc_output="${1-}" _k3slrh_utc_value=''
(( $# == 1 )) || return 1
_k3slrh_output_name_is_safe "$_k3slrh_utc_output" || return 1
_k3slrh_capture_one_line _k3slrh_utc_value /usr/bin/date --utc +%Y%m%dT%H%M%SZ || return 1
[[ "$_k3slrh_utc_value" =~ ^[0-9]{8}T[0-9]{6}Z$ ]] || return 1
printf -v "$_k3slrh_utc_output" '%s' "$_k3slrh_utc_value"
}
_k3slrh_header_proof_main() {
local _k3slrh_main_mode='' _k3slrh_main_utc='' _k3slrh_main_parse_rc=0
if _k3slrh_parse_cli _k3slrh_main_mode "$@"; then
_k3slrh_main_parse_rc=0
else
_k3slrh_main_parse_rc=$?
fi
(( _k3slrh_main_parse_rc == 0 )) || return 2
[[ "${EUID:-}" =~ ^[0-9]+$ && "${EUID:-}" != 0 ]] || return 1
_k3slr_xtrace_is_disabled || return 1
if [[ "$_k3slrh_main_mode" == execute ]]; then
_k3slr_require_execute_tty || return 1
declare -F _k3slrh_prove_header_restore_a1 >/dev/null || return 1
return 1
fi
_k3slrh_current_utc _k3slrh_main_utc || return 1
_k3slrh_derive_targets "$_k3slrh_main_utc" || return 1
_k3slrh_pre_mount_checks || return 1
printf '%s\n' \
'Header proof state: closed' \
'Header proof capability: pass' \
'Execute-time checks: pending' \
'DRY RUN PASS: no changes'
}