mirror of
https://github.com/linuxboot/heads.git
synced 2025-01-21 03:55:19 +00:00
efc49c7425
Currently Heads will check files in /boot for tampering before booting into a system. It would be nice if you could use the trusted environment within Heads and extend this to check files in / itself. This new script adds that functionality, however due to the length of time it takes to perform these kinds of checks, it doesn't run automatically (yet). This feature can be configured from the config GUI - the root device/ directories to check can be set, and it can be configured to run during boot. To make this a bit easier to use, I added a feature to detect whether the hash file exists and if not, to display a more limited menu to the user guiding them to create the initial hash file. Otherwise it will display the date the file was last modified, which can be useful to determine how stale it is.
432 lines
11 KiB
Bash
Executable File
432 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
# Generic configurable boot script via kexec
|
|
set -e -o pipefail
|
|
. /tmp/config
|
|
. /etc/functions
|
|
|
|
TRACE "Under /bin/kexec-select-boot"
|
|
|
|
add=""
|
|
remove=""
|
|
config="*.cfg"
|
|
unique="n"
|
|
valid_hash="n"
|
|
valid_global_hash="n"
|
|
valid_rollback="n"
|
|
force_menu="n"
|
|
gui_menu="n"
|
|
force_boot="n"
|
|
skip_confirm="n"
|
|
while getopts "b:d:p:a:r:c:uimgfs" arg; do
|
|
case $arg in
|
|
b) bootdir="$OPTARG" ;;
|
|
d) paramsdev="$OPTARG" ;;
|
|
p) paramsdir="$OPTARG" ;;
|
|
a) add="$OPTARG" ;;
|
|
r) remove="$OPTARG" ;;
|
|
c) config="$OPTARG" ;;
|
|
u) unique="y" ;;
|
|
m) force_menu="y" ;;
|
|
i) valid_hash="y"; valid_rollback="y" ;;
|
|
g) gui_menu="y" ;;
|
|
f) force_boot="y"; valid_hash="y"; valid_rollback="y" ;;
|
|
s) skip_confirm="y" ;;
|
|
esac
|
|
done
|
|
|
|
if [ -z "$bootdir" ]; then
|
|
die "Usage: $0 -b /boot"
|
|
fi
|
|
|
|
if [ -z "$paramsdev" ]; then
|
|
paramsdev="$bootdir"
|
|
fi
|
|
|
|
if [ -z "$paramsdir" ]; then
|
|
paramsdir="$bootdir"
|
|
fi
|
|
|
|
bootdir="${bootdir%%/}"
|
|
paramsdev="${paramsdev%%/}"
|
|
paramsdir="${paramsdir%%/}"
|
|
|
|
PRIMHASH_FILE="$paramsdir/kexec_primhdl_hash.txt"
|
|
if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then
|
|
if [ -r "$PRIMHASH_FILE" ]; then
|
|
sha256sum -c "$PRIMHASH_FILE" \
|
|
|| {
|
|
echo "FATAL: Hash of TPM2 primary key handle mismatch!";
|
|
echo "If you have not intentionally regenerated TPM2 primary key,";
|
|
warn "your system may have been compromised!";
|
|
}
|
|
else
|
|
echo "WARNING: Hash of TPM2 primary key handle does not exist!"
|
|
echo "Please rebuild the boot hash tree."
|
|
default_failed="y"
|
|
fi
|
|
fi
|
|
|
|
verify_global_hashes()
|
|
{
|
|
echo "+++ Checking verified boot hash file "
|
|
# Check the hashes of all the files
|
|
if verify_checksums "$bootdir" "$gui_menu" ; then
|
|
echo "+++ Verified boot hashes "
|
|
valid_hash='y'
|
|
valid_global_hash='y'
|
|
else
|
|
if [ "$gui_menu" = "y" ]; then
|
|
CHANGED_FILES=$(grep -v 'OK$' /tmp/hash_output | cut -f1 -d ':')
|
|
whiptail $BG_COLOR_ERROR --title 'ERROR: Boot Hash Mismatch' \
|
|
--msgbox "The following files failed the verification process:\n${CHANGED_FILES}\nExiting to a recovery shell" 0 80
|
|
fi
|
|
die "$TMP_HASH_FILE: boot hash mismatch"
|
|
fi
|
|
# If user enables it, check root hashes before boot as well
|
|
if [[ "$CONFIG_ROOT_CHECK_AT_BOOT" = "y" && "$force_menu" == "n" ]]; then
|
|
if root-hashes-gui.sh -c; then
|
|
echo "+++ Verified root hashes, continuing boot "
|
|
# if user re-signs, it wipes out saved options, so scan the boot directory and generate
|
|
if [ ! -r "$TMP_MENU_FILE" ]; then
|
|
scan_options
|
|
fi
|
|
else
|
|
# root-hashes-gui.sh handles the GUI error menu, just die here
|
|
if [ "$gui_menu" = "y" ]; then
|
|
whiptail $BG_COLOR_ERROR --title 'ERROR: Root Hash Mismatch' \
|
|
--msgbox "The root hash check failed!\nExiting to a recovery shell" 16 60
|
|
fi
|
|
die "root hash mismatch, see /tmp/hash_output_mismatches for details"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
verify_rollback_counter()
|
|
{
|
|
TPM_COUNTER=`grep counter $TMP_ROLLBACK_FILE | cut -d- -f2`
|
|
if [ -z "$TPM_COUNTER" ]; then
|
|
die "$TMP_ROLLBACK_FILE: TPM counter not found?"
|
|
fi
|
|
|
|
read_tpm_counter $TPM_COUNTER \
|
|
|| die "Failed to read TPM counter"
|
|
|
|
sha256sum -c $TMP_ROLLBACK_FILE \
|
|
|| die "Invalid TPM counter state"
|
|
|
|
valid_rollback="y"
|
|
}
|
|
|
|
first_menu="y"
|
|
get_menu_option() {
|
|
num_options=`cat $TMP_MENU_FILE | wc -l`
|
|
if [ $num_options -eq 0 ]; then
|
|
die "No boot options"
|
|
fi
|
|
|
|
if [ $num_options -eq 1 -a $first_menu = "y" ]; then
|
|
option_index=1
|
|
elif [ "$gui_menu" = "y" ]; then
|
|
MENU_OPTIONS=""
|
|
n=0
|
|
while read option
|
|
do
|
|
parse_option
|
|
n=`expr $n + 1`
|
|
name=$(echo $name | tr " " "_")
|
|
MENU_OPTIONS="$MENU_OPTIONS $n ${name} "
|
|
done < $TMP_MENU_FILE
|
|
|
|
whiptail --title "Select your boot option" \
|
|
--menu "Choose the boot option [1-$n, a to abort]:" 0 80 8 \
|
|
-- $MENU_OPTIONS \
|
|
2>/tmp/whiptail || die "Aborting boot attempt"
|
|
|
|
option_index=$(cat /tmp/whiptail)
|
|
else
|
|
echo "+++ Select your boot option:"
|
|
n=0
|
|
while read option
|
|
do
|
|
parse_option
|
|
n=`expr $n + 1`
|
|
echo "$n. $name [$kernel]"
|
|
done < $TMP_MENU_FILE
|
|
|
|
read \
|
|
-p "Choose the boot option [1-$n, a to abort]: " \
|
|
option_index
|
|
|
|
if [ "$option_index" = "a" ]; then
|
|
die "Aborting boot attempt"
|
|
fi
|
|
fi
|
|
first_menu="n"
|
|
|
|
option=`head -n $option_index $TMP_MENU_FILE | tail -1`
|
|
parse_option
|
|
}
|
|
|
|
confirm_menu_option() {
|
|
if [ "$gui_menu" = "y" ]; then
|
|
default_text="Make default"
|
|
[[ "$CONFIG_TPM_NO_LUKS_DISK_UNLOCK" = "y" ]] && default_text="${default_text} and boot"
|
|
whiptail $BG_COLOR_WARNING --title "Confirm boot details" \
|
|
--menu "Confirm the boot details for $name:\n\n$(echo $kernel| fold -s -w 80) \n\n" 0 80 8 \
|
|
-- 'd' "${default_text}" 'y' "Boot one time" \
|
|
2>/tmp/whiptail || die "Aborting boot attempt"
|
|
|
|
option_confirm=$(cat /tmp/whiptail)
|
|
else
|
|
echo "+++ Please confirm the boot details for $name:"
|
|
echo $option
|
|
|
|
read \
|
|
-n 1 \
|
|
-p "Confirm selection by pressing 'y', make default with 'd': " \
|
|
option_confirm
|
|
echo
|
|
fi
|
|
}
|
|
|
|
parse_option() {
|
|
name=`echo $option | cut -d\| -f1`
|
|
kernel=`echo $option | cut -d\| -f3`
|
|
}
|
|
|
|
scan_options() {
|
|
echo "+++ Scanning for unsigned boot options"
|
|
option_file="/tmp/kexec_options.txt"
|
|
if [ -r $option_file ]; then rm $option_file; fi
|
|
for i in `find $bootdir -name "$config"`; do
|
|
DO_WITH_DEBUG kexec-parse-boot "$bootdir" "$i" >> $option_file
|
|
done
|
|
# FC29/30+ may use BLS format grub config files
|
|
# https://fedoraproject.org/wiki/Changes/BootLoaderSpecByDefault
|
|
# only parse these if $option_file is still empty
|
|
if [ ! -s $option_file ] && [ -d "$bootdir/loader/entries" ]; then
|
|
for i in `find $bootdir -name "$config"`; do
|
|
kexec-parse-bls "$bootdir" "$i" "$bootdir/loader/entries" >> $option_file
|
|
done
|
|
fi
|
|
if [ ! -s $option_file ]; then
|
|
die "Failed to parse any boot options"
|
|
fi
|
|
if [ "$unique" = 'y' ]; then
|
|
sort -r $option_file | uniq > $TMP_MENU_FILE
|
|
else
|
|
cp $option_file $TMP_MENU_FILE
|
|
fi
|
|
}
|
|
|
|
save_default_option() {
|
|
if [ "$gui_menu" != "y" ]; then
|
|
read \
|
|
-n 1 \
|
|
-p "Saving a default will modify the disk. Proceed? (Y/n): " \
|
|
default_confirm
|
|
echo
|
|
fi
|
|
|
|
[ "$default_confirm" = "" ] && default_confirm="y"
|
|
if [[ "$default_confirm" = "y" || "$default_confirm" = "Y" ]]; then
|
|
if kexec-save-default \
|
|
-b "$bootdir" \
|
|
-d "$paramsdev" \
|
|
-p "$paramsdir" \
|
|
-i "$option_index" \
|
|
; then
|
|
echo "+++ Saved defaults to device"
|
|
sleep 2
|
|
default_failed="n"
|
|
force_menu="n"
|
|
return
|
|
else
|
|
echo "Failed to save defaults"
|
|
fi
|
|
fi
|
|
|
|
option_confirm="n"
|
|
}
|
|
|
|
default_select() {
|
|
# Attempt boot with expected parameters
|
|
|
|
# Check that entry matches that which is expected from menu
|
|
default_index=`basename "$TMP_DEFAULT_FILE" | cut -d. -f 2`
|
|
|
|
# Check to see if entries have changed - useful for detecting grub update
|
|
expectedoption=`cat $TMP_DEFAULT_FILE`
|
|
option=`head -n $default_index $TMP_MENU_FILE | tail -1`
|
|
if [ "$option" != "$expectedoption" ]; then
|
|
if [ "$gui_menu" = "y" ]; then
|
|
whiptail $BG_COLOR_ERROR --title 'ERROR: Boot Entry Has Changed' \
|
|
--msgbox "The list of boot entries has changed\n\nPlease set a new default" 16 60
|
|
fi
|
|
warn "!!! Boot entry has changed - please set a new default"
|
|
return
|
|
fi
|
|
parse_option
|
|
|
|
# Enforce that default option hashes are valid
|
|
echo "+++ Checking verified default boot hash file "
|
|
# Check the hashes of all the files
|
|
if ( cd $bootdir && sha256sum -c "$TMP_DEFAULT_HASH_FILE" > /tmp/hash_output ); then
|
|
echo "+++ Verified default boot hashes "
|
|
valid_hash='y'
|
|
else
|
|
if [ "$gui_menu" = "y" ]; then
|
|
CHANGED_FILES=$(grep -v 'OK$' /tmp/hash_output | cut -f1 -d ':')
|
|
whiptail $BG_COLOR_ERROR --title 'ERROR: Default Boot Hash Mismatch' \
|
|
--msgbox "The following files failed the verification process:\n${CHANGED_FILES}\nExiting to a recovery shell" 0 80
|
|
fi
|
|
die "!!! $TMP_DEFAULT_HASH_FILE: default boot hash mismatch"
|
|
fi
|
|
|
|
echo "+++ Executing default boot for $name:"
|
|
do_boot
|
|
warn "Failed to boot default option"
|
|
}
|
|
|
|
user_select() {
|
|
# No default expected boot parameters, ask user
|
|
|
|
option_confirm=""
|
|
while [ "$option_confirm" != "y" -a "$option_confirm" != "d" ]
|
|
do
|
|
get_menu_option
|
|
# In force boot mode, no need offer the option to set a default, just boot
|
|
if [[ "$force_boot" = "y" || "$skip_confirm" = "y" ]]; then
|
|
do_boot
|
|
else
|
|
confirm_menu_option
|
|
fi
|
|
|
|
if [ "$option_confirm" = 'd' ]; then
|
|
save_default_option
|
|
fi
|
|
done
|
|
|
|
if [ "$option_confirm" = "d" ]; then
|
|
if [ ! -r "$TMP_KEY_DEVICES" ]; then
|
|
# continue below to boot the new default option
|
|
true
|
|
else
|
|
echo "+++ Rebooting to start the new default option"
|
|
sleep 2
|
|
if [ "$CONFIG_DEBUG_OUTPUT" != "y" ]; then
|
|
reboot \
|
|
|| die "!!! Failed to reboot system"
|
|
else
|
|
DEBUG "Rebooting is required prior of booting default boot entry"
|
|
# Instead of rebooting, drop to a recovery shell
|
|
# for a chance to inspect debug output
|
|
recovery "Entering recovery to permit inspection of /tmp/debug.log output, reboot to continue"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
do_boot
|
|
}
|
|
|
|
do_boot()
|
|
{
|
|
if [ "$CONFIG_BOOT_REQ_ROLLBACK" = "y" -a "$valid_rollback" = "n" ]; then
|
|
die "!!! Missing required rollback counter state"
|
|
fi
|
|
|
|
if [ "$CONFIG_BOOT_REQ_HASH" = "y" -a "$valid_hash" = "n" ]; then
|
|
die "!!! Missing required boot hashes"
|
|
fi
|
|
|
|
if [ "$CONFIG_TPM" = "y" ] && [ -r "$TMP_KEY_DEVICES" ]; then
|
|
INITRD=`kexec-boot -b "$bootdir" -e "$option" -i` \
|
|
|| die "!!! Failed to extract the initrd from boot option"
|
|
if [ -z "$INITRD" ]; then
|
|
die "!!! No initrd file found in boot option"
|
|
fi
|
|
|
|
kexec-insert-key $INITRD \
|
|
|| die "!!! Failed to insert disk key into a new initrd"
|
|
|
|
kexec-boot -b "$bootdir" -e "$option" \
|
|
-a "$add" -r "$remove" -o "/tmp/secret/initrd.cpio" \
|
|
|| die "!!! Failed to boot w/ options: $option"
|
|
else
|
|
kexec-boot -b "$bootdir" -e "$option" -a "$add" -r "$remove" \
|
|
|| die "!!! Failed to boot w/ options: $option"
|
|
fi
|
|
}
|
|
|
|
while true; do
|
|
if [ "$force_boot" = "y" ]; then
|
|
check_config $paramsdir force
|
|
else
|
|
check_config $paramsdir
|
|
fi
|
|
TMP_DEFAULT_FILE=`find /tmp/kexec/kexec_default.*.txt 2>/dev/null | head -1` || true
|
|
TMP_MENU_FILE="/tmp/kexec/kexec_menu.txt"
|
|
TMP_HASH_FILE="/tmp/kexec/kexec_hashes.txt"
|
|
TMP_TREE_FILE="/tmp/kexec/kexec_tree.txt"
|
|
TMP_DEFAULT_HASH_FILE="/tmp/kexec/kexec_default_hashes.txt"
|
|
TMP_ROLLBACK_FILE="/tmp/kexec/kexec_rollback.txt"
|
|
TMP_KEY_DEVICES="/tmp/kexec/kexec_key_devices.txt"
|
|
TMP_KEY_LVM="/tmp/kexec/kexec_key_lvm.txt"
|
|
|
|
# Allow a way for users to ignore warnings and boot into their systems
|
|
# even if hashes don't match
|
|
if [ "$force_boot" = "y" ]; then
|
|
scan_options
|
|
# Remove boot splash and make background red in the event of a forced boot
|
|
add="$add vt.default_red=0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff"
|
|
remove="$remove splash quiet"
|
|
user_select
|
|
fi
|
|
|
|
if [ "$CONFIG_TPM" = "y" ]; then
|
|
if [ ! -r "$TMP_KEY_DEVICES" ]; then
|
|
# Extend PCR4 as soon as possible
|
|
tpmr extend -ix 4 -ic generic \
|
|
|| die "Failed to extend PCR 4"
|
|
fi
|
|
fi
|
|
|
|
# if no saved options, scan the boot directory and generate
|
|
if [ ! -r "$TMP_MENU_FILE" ]; then
|
|
scan_options
|
|
fi
|
|
|
|
if [ "$CONFIG_TPM" = "y" ]; then
|
|
# Optionally enforce device file hashes
|
|
if [ -r "$TMP_HASH_FILE" ]; then
|
|
valid_global_hash="n"
|
|
|
|
verify_global_hashes
|
|
|
|
if [ "$valid_global_hash" = "n" ]; then
|
|
die "Failed to verify global hashes"
|
|
fi
|
|
fi
|
|
|
|
if [ "$CONFIG_IGNORE_ROLLBACK" != "y" -a -r "$TMP_ROLLBACK_FILE" ]; then
|
|
# in the case of iso boot with a rollback file, do not assume valid
|
|
valid_rollback="n"
|
|
|
|
verify_rollback_counter
|
|
fi
|
|
fi
|
|
|
|
if [ "$default_failed" != "y" \
|
|
-a "$force_menu" = "n" \
|
|
-a -r "$TMP_DEFAULT_FILE" \
|
|
-a -r "$TMP_DEFAULT_HASH_FILE" ] \
|
|
; then
|
|
default_select
|
|
default_failed="y"
|
|
else
|
|
user_select
|
|
fi
|
|
done
|
|
|
|
die "!!! Shouldn't get here"
|