Merge pull request #1586 from JonathonHall-Purism/root-file-hash-qubes

Root file hashing: support Qubes default partition layout (+ tracing helpers)
This commit is contained in:
tlaurion 2024-02-01 14:25:48 -05:00 committed by GitHub
commit 5a75e6bffa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 266 additions and 44 deletions

View File

@ -39,7 +39,7 @@ while true; do
'r' ' Clear GPG key(s) and reset all user settings'
'R' ' Change the root device for hashing'
'D' ' Change the root directories to hash'
'B' ' Check root hashes at boot'
'B' " $(get_config_display_action "$CONFIG_ROOT_CHECK_AT_BOOT") root check at boot"
'L' " $(get_config_display_action "$CONFIG_RESTRICTED_BOOT") Restricted Boot"
)
@ -265,10 +265,9 @@ while true; do
--msgbox "The root directories to hash was successfully changed to:\n$NEW_CONFIG_ROOT_DIRLIST" 0 80
;;
"B" )
CURRENT_OPTION="$(load_config_value CONFIG_ROOT_CHECK_AT_BOOT)"
if [ "$CURRENT_OPTION" != "y" ]; then
if [ "$CONFIG_ROOT_CHECK_AT_BOOT" != "y" ]; then
# Root device and directories must be set to enable this
if [ -z "$(load_config_value CONFIG_ROOT_DEV)" ] || [ -z "$(load_config_value CONFIG_ROOT_DIRLIST)" ]; then
if [ -z "$CONFIG_ROOT_DEV" ] || [ -z "$CONFIG_ROOT_DIRLIST" ]; then
whiptail $BG_COLOR_ERROR --title 'Root Check Not Configured' \
--msgbox "Set the root device and directories to hash before enabling this feature." 0 80
elif (whiptail --title 'Enable Root Hash Check at Boot?' \
@ -277,8 +276,7 @@ while true; do
\na minute or more to the boot time.
\n\nDo you want to proceed?" 0 80) then
set_config /etc/config.user "CONFIG_ROOT_CHECK_AT_BOOT" "y"
combine_configs
set_user_config "CONFIG_ROOT_CHECK_AT_BOOT" "y"
# check that root hash file exists
if [ ! -f ${ROOT_HASH_FILE} ]; then
@ -298,8 +296,7 @@ while true; do
--yesno "This will disable checking root hashes each time you boot.
\n\nDo you want to proceed?" 0 80) then
set_config /etc/config.user "CONFIG_ROOT_CHECK_AT_BOOT" "n"
combine_configs
set_user_config "CONFIG_ROOT_CHECK_AT_BOOT" "n"
whiptail --title 'Config change successful' \
--msgbox "The root device will not be checked at each boot." 0 80

View File

@ -32,7 +32,9 @@ update_root_checksums() {
fi
echo "+++ Calculating hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY "
cd $ROOT_MOUNT && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*' -print0 | xargs -0 sha256sum | tee ${HASH_FILE}
# Intentional wordsplit
# shellcheck disable=SC2086
(cd "$ROOT_MOUNT" && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*' -print0 | xargs -0 sha256sum) >"${HASH_FILE}"
# switch back to ro mode
mount -o ro,remount /boot
@ -86,7 +88,7 @@ check_root_checksums() {
fi
echo "+++ Checking for new files in $CONFIG_ROOT_DIRLIST_PRETTY "
find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*' | sort > /tmp/new_file_list
(cd "$ROOT_MOUNT" && find ${CONFIG_ROOT_DIRLIST} -type f ! -name '*kexec*') | sort > /tmp/new_file_list
cut -d' ' -f3- ${HASH_FILE} | sort | diff -U0 - /tmp/new_file_list > /tmp/new_file_diff || new_files_found=y
if [ "$new_files_found" == "y" ]; then
grep -E -v '^[+-]{3}|[@]{2} ' /tmp/new_file_diff > /tmp/new_file_diff2 # strip any output that's not a file
@ -102,7 +104,7 @@ check_root_checksums() {
fi
echo "+++ Checking hashes for all files in $CONFIG_ROOT_DIRLIST_PRETTY (this might take a while) "
if cd $ROOT_MOUNT && sha256sum -c ${HASH_FILE} > /tmp/hash_output 2>/dev/null; then
if (cd $ROOT_MOUNT && sha256sum -c ${HASH_FILE} > /tmp/hash_output 2>/dev/null); then
echo "+++ Verified root hashes "
valid_hash='y'
unmount_root_device
@ -156,37 +158,242 @@ check_root_checksums() {
fi
fi
}
# Check if a device is an LVM2 PV, and if so print the VG name
find_lvm_vg_name() {
TRACE_FUNC
local DEVICE VG
DEVICE="$1"
mkdir -p /tmp/root-hashes-gui
if ! lvm pvs "$DEVICE" >/tmp/root-hashes-gui/lvm_vg 2>/dev/null; then
# It's not an LVM PV
return 1
fi
VG="$(tail -n +2 /tmp/root-hashes-gui/lvm_vg | awk '{print $2}')"
if [ -z "$VG" ]; then
DEBUG "Could not find LVM2 VG from lvm pvs output:"
DEBUG "$(cat /tmp/root-hashes-gui/lvm_vg)"
return 1
fi
echo "$VG"
}
# Open an LVM volume group, then continue looking for more layers in the 'root'
# logical volume.
open_block_device_lvm() {
TRACE_FUNC
local VG="$1"
if ! lvm vgchange -ay "$VG"; then
DEBUG "Can't open LVM VG: $VG"
return 1
fi
# Use the LV 'root'. This is the default name used by Qubes. There's no
# way to configure this at the moment.
if ! [ -e "/dev/mapper/$VG-root" ]; then
DEBUG "LVM volume group does not have 'root' logical volume"
return 1
fi
# Use the root LV now
open_block_device_layers "/dev/mapper/$VG-root"
}
# Open a LUKS device, then continue looking for more layers.
open_block_device_luks() {
TRACE_FUNC
local DEVICE="$1"
local LUKSDEV
LUKSDEV="$(basename "$DEVICE")_crypt"
# Open the LUKS device. This may prompt interactively for the passphrase, so
# hook it up to the console even if stdout/stdin have been redirected.
if ! cryptsetup open "$DEVICE" "$LUKSDEV"; then
DEBUG "Can't open LUKS volume: $DEVICE"
return 1
fi
open_block_device_layers "/dev/mapper/$LUKSDEV"
}
# Open block device layers to access /root recursively. If another layer (LUKS
# or LVM) can be identified, open it and recurse into the new device. When all
# recognized layers are opened, print the final block device and exit
# successfully (open_root_device will try to mount it).
#
# This only fails if we can recognize another LUKS or LVM layer, but cannot open
# it. It succeeds otherwise, even if no layers are recognized, because we
# should try to mount the block device directly in that case.
open_block_device_layers() {
TRACE_FUNC
local DEVICE="$1"
local VG
if ! [ -e "$DEVICE" ]; then
DEBUG "Block device doesn't exit: $DEVICE"
# This shouldn't really happen, we thought we opened the last layer
# successfully. The call stack reveals what LUKS/LVM2 layers have been
# opened so far.
DEBUG_STACK
return 1
fi
# Try to open a LUKS layer
if cryptsetup isLuks "$DEVICE" &>/dev/null; then
open_block_device_luks "$DEVICE" || return 1
# Try to open an LVM layer
elif VG="$(find_lvm_vg_name "$DEVICE")"; then
open_block_device_lvm "$VG" || return 1
else
# The given block device exists but is not any layer we understand. Stop
# opening layers and try to mount it.
echo "$DEVICE"
fi
}
# Try to open a block device as /root. open_block_device_layers() is used to
# open LUKS and LVM layers before mounting the filesystem.
#
# This function does not clean up anything if it is unsuccessful. Use
# try_open_root_device() to also clean up when unsuccessful.
open_root_device_no_clean_up() {
TRACE_FUNC
local DEVICE="$1"
local FS_DEVICE
# Open LUKS/LVM and get the name of the block device that should contain the
# filesystem. If there are no LUKS/LVM layers, FS_DEVICE is just DEVICE.
FS_DEVICE="$(open_block_device_layers "$DEVICE")" || return 1
# Mount the device
if ! mount -o ro "$FS_DEVICE" "$ROOT_MOUNT" &>/dev/null; then
DEBUG "Can't mount filesystem on $FS_DEVICE from $DEVICE"
return 1
fi
# The filesystem must have all of the directories configured. (Intentional
# word-split)
# shellcheck disable=SC2086
if ! (cd "$ROOT_MOUNT" && ls -d $CONFIG_ROOT_DIRLIST &>/dev/null); then
DEBUG "Root filesystem on $DEVICE lacks one of the configured directories: $CONFIG_ROOT_DIRLIST"
return 1
fi
# Root is mounted now and the directories are present
return 0
}
# If an LVM VG is open, close any layers within it, then close the LVM VG.
close_block_device_lvm() {
TRACE_FUNC
local VG="$1"
# We always use the LV 'root' currently
local LV="/dev/mapper/$VG-root"
if [ -e "$LV" ]; then
close_block_device_layers "$LV"
fi
# The LVM VG might be open even if no 'root' LV exists, still try to close it.
lvm vgchange -an "$VG" || \
DEBUG "Can't close LVM VG: $VG"
}
# If a LUKS device is open, close any layers within the LUKS device, then close
# the LUKS device.
close_block_device_luks() {
TRACE_FUNC
local DEVICE="$1"
local LUKSDEV
LUKSDEV="$(basename "$DEVICE")_crypt"
if [ -e "/dev/mapper/$LUKSDEV" ]; then
# Close inner layers before trying to close LUKS
close_block_device_layers "/dev/mapper/$LUKSDEV"
cryptsetup close "$LUKSDEV" || \
DEBUG "Can't close LUKS volume: $LUKSDEV"
fi
}
# Close the root device, including unmounting the filesystem and closing all
# layers. This can close a partially-opened device if an error occurs.
close_block_device_layers() {
TRACE_FUNC
local DEVICE="$1"
local VG
if ! [ -e "$DEVICE" ]; then
DEBUG "Block device doesn't exit: $DEVICE"
# Like in open_root_device(), this shouldn't really happen, show the layers
# up to this point via the call stack.
DEBUG_STACK
return 1
fi
if cryptsetup isLuks "$DEVICE"; then
close_block_device_luks "$DEVICE"
elif VG="$(find_lvm_vg_name "$DEVICE")"; then
close_block_device_lvm "$VG"
fi
# Otherwise, we've handled all the layers we understood, there's nothing left
# to do.
}
# Try to open the root device, and clean up if unsuccessful.
open_root_device() {
TRACE_FUNC
if ! open_root_device_no_clean_up "$1"; then
unmount_root_device
return 1
fi
return 0
}
# Close the root device, including unmounting the filesystem and closing all
# layers. This can close a partially-opened device if an error occurs. This
# never fails, if an error occurs it still tries to close anything it can.
close_root_device() {
TRACE_FUNC
local DEVICE="$1"
# Unmount the filesystem if it is mounted. If it is not mounted, ignore the
# failure. If it is mounted but can't be unmounted, this will fail and we
# will fail to close any LUKS/LVM layers too.
umount "$ROOT_MOUNT" &>/dev/null || true
close_block_device_layers "$DEVICE" || true
}
# detect and set /root device
# mount /root if successful
detect_root_device()
{
TRACE_FUNC
echo "+++ Detecting root device "
if [ ! -e $ROOT_MOUNT ]; then
mkdir -p $ROOT_MOUNT
fi
# unmount $ROOT_MOUNT to be safe
cd / && umount $ROOT_MOUNT 2>/dev/null
# Ensure nothing is opened/mounted
unmount_root_device
# check $CONFIG_ROOT_DEV if set/valid
if [ -e "$CONFIG_ROOT_DEV" ]; then
if cryptsetup isLuks $CONFIG_ROOT_DEV >/dev/null 2>&1; then
if cryptsetup open $CONFIG_ROOT_DEV rootdisk; then
if mount -o ro /dev/mapper/rootdisk $ROOT_MOUNT >/dev/null 2>&1; then
if cd $ROOT_MOUNT && ls -d $CONFIG_ROOT_DIRLIST >/dev/null 2>&1; then # CONFIG_ROOT_DEV is valid device and contains an installed OS
if [ -e "$CONFIG_ROOT_DEV" ] && open_root_device "$CONFIG_ROOT_DEV"; then
return 0
fi
fi
fi
fi
fi
# generate list of possible boot devices
fdisk -l | grep "Disk /dev/" | cut -f2 -d " " | cut -f1 -d ":" > /tmp/disklist
# filter out extraneous options
> /tmp_root_device_list
for i in `cat /tmp/disklist`; do
while IFS= read -r -u 10 i; do
# remove block device from list if numeric partitions exist
DEV_NUM_PARTITIONS=$((`ls -1 $i* | wc -l`-1))
if [ ${DEV_NUM_PARTITIONS} -eq 0 ]; then
@ -194,33 +401,25 @@ detect_root_device()
else
ls $i* | tail -${DEV_NUM_PARTITIONS} >> /tmp_root_device_list
fi
done
done 10</tmp/disklist
# iterate thru possible options and check for LUKS
for i in `cat /tmp_root_device_list`; do
if cryptsetup isLuks $i >/dev/null 2>&1; then
if cryptsetup open $i rootdisk; then
if mount -o ro /dev/mapper/rootdisk $ROOT_MOUNT >/dev/null 2>&1; then
if cd $ROOT_MOUNT && ls -d $CONFIG_ROOT_DIRLIST >/dev/null 2>&1; then
# iterate through possible options
while IFS= read -r -u 10 i; do
if open_root_device "$i"; then
# CONFIG_ROOT_DEV is valid device and contains an installed OS
CONFIG_ROOT_DEV="$i"
return 0
fi
fi
fi
fi
done
done 10</tmp_root_device_list
# no valid root device found
echo "Unable to locate $ROOT_MOUNT files on any mounted disk"
unmount_root_device
return 1
}
unmount_root_device()
{
cd /
umount $ROOT_MOUNT 2>/dev/null
cryptsetup close rootdisk
[ -e "$CONFIG_ROOT_DEV" ] && close_root_device "$CONFIG_ROOT_DEV"
}
checkonly="n"

View File

@ -33,6 +33,28 @@ DO_WITH_DEBUG() {
"$@"
}
# Trace the current script and function.
TRACE_FUNC() {
# Index [1] for BASH_SOURCE and FUNCNAME give us the caller location.
# FUNCNAME is 'main' if called from a script outside any function.
# BASH_LINENO is offset by 1, it provides the line that the
# corresponding FUNCNAME was _called from_, so BASH_LINENO[0] is the
# location of the caller.
TRACE "${BASH_SOURCE[1]}(${BASH_LINENO[0]}): ${FUNCNAME[1]}"
}
# Show the entire current call stack in debug output - useful if a catastrophic
# error or something very unexpected occurs, like totally invalid parameters.
DEBUG_STACK() {
local FRAMES
FRAMES="${#FUNCNAME[@]}"
DEBUG "call stack: ($((FRAMES-1)) frames)"
# Don't print DEBUG_STACK itself, start from 1
for i in $(seq 1 "$((FRAMES-1))"); do
DEBUG "- $((i-1)) - ${BASH_SOURCE[$i]}(${BASH_LINENO[$((i-1))]}): ${FUNCNAME[$i]}"
done
}
pcrs() {
if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then
tpm2 pcrread sha256

View File

@ -7,6 +7,9 @@ bash_tar := bash-$(bash_version).tar.gz
bash_url := https://ftpmirror.gnu.org/bash/$(bash_tar)
bash_hash := 5bac17218d3911834520dad13cd1f85ab944e1c09ae1aba55906be1f8192f558
# --enable-debugger: Enables BASH_SOURCE tracing through functions as well as
# BASH_ARGV/BASH_ARGC. (Otherwise BASH_SOURCE[0] is empty when calling a
# function, it's only set in top level script code.)
bash_configure := CFLAGS="-g0 -Os" LDFLAGS="-s" ./configure \
$(CROSS_TOOLS) \
--host $(target) \
@ -16,11 +19,11 @@ bash_configure := CFLAGS="-g0 -Os" LDFLAGS="-s" ./configure \
--mandir=/usr/share/man \
--without-bash-malloc \
--disable-coprocesses \
--disable-debugger \
--enable-debugger \
--disable-net-redirections \
--enable-single-help-strings \
--disable-nls \
--disable-readline \
--enable-readline \
bash_target := $(MAKE_JOBS) \
&& $(MAKE) -C $(build)/$(bash_dir) \

View File

@ -33,6 +33,7 @@ lvm2_configure := \
--disable-cache_check_needs_check \
--disable-thin_check_needs_check \
--with-cluster=none \
--with-thin-check= \
# not sure why LIB_SUFFIX is not defined in the cross build
lvm2_target := \