mirror of
https://github.com/linuxboot/heads.git
synced 2025-02-21 01:31:26 +00:00
We just set the TPM owner password, so there's no need to make the user enter it again. Eliminates some failure modes if the user mistypes it or enters the wrong password. Allow optionally passing in the TPM owner password in tpmr seal, check_tpm_counter(), seal-totp, and generate_totp_htop(). The user is still prompted if the password is needed but was not provided, so existing uses in other contexts continue to work unchanged. Prompt for the password in reset_tpm() and pass it down to each of the above. Signed-off-by: Jonathon Hall <jonathon.hall@puri.sm>
579 lines
15 KiB
Bash
Executable File
579 lines
15 KiB
Bash
Executable File
#!/bin/bash
|
|
# Shell functions for most initialization scripts
|
|
. /etc/ash_functions
|
|
|
|
# Print <hidden> or <empty> depending on whether $1 is empty. Useful to mask an
|
|
# optional password parameter.
|
|
mask_param() {
|
|
if [ -z "$1" ]; then
|
|
echo "<empty>"
|
|
else
|
|
echo "<hidden>"
|
|
fi
|
|
}
|
|
|
|
# Trace a command with DEBUG, then execute it.
|
|
# A password parameter can be masked by passing --mask-position N before the
|
|
# command to execute, the debug trace will just indicate whether the password
|
|
# was empty or nonempty (which is important when use of a password is optional).
|
|
# N=0 is the name of the command to be executed, N=1 is its first parameter,
|
|
# etc.
|
|
DO_WITH_DEBUG() {
|
|
if [ "$1" == "--mask-position" ]; then
|
|
mask_position="$2"
|
|
shift
|
|
shift
|
|
DEBUG_ARGS=("$@")
|
|
|
|
DEBUG_ARGS[$mask_position]="$(mask_param "${DEBUG_ARGS[$mask_position]}")"
|
|
DEBUG "${DEBUG_ARGS[@]}"
|
|
else
|
|
DEBUG "$@"
|
|
fi
|
|
"$@"
|
|
}
|
|
|
|
recovery() {
|
|
TRACE "Under /etc/functions:recovery"
|
|
echo >&2 "!!!!! $*"
|
|
|
|
# Remove any temporary secret files that might be hanging around
|
|
# but recreate the directory so that new tools can use it.
|
|
|
|
#safe to always be true. Otherwise "set -e" would make it exit here
|
|
shred -n 10 -z -u /tmp/secret/* 2> /dev/null || true
|
|
rm -rf /tmp/secret
|
|
mkdir -p /tmp/secret
|
|
|
|
# ensure /tmp/config exists for recovery scripts that depend on it
|
|
touch /tmp/config
|
|
|
|
if [ "$CONFIG_TPM" = "y" ]; then
|
|
tpmr extend -ix 4 -ic recovery
|
|
fi
|
|
|
|
while [ true ]
|
|
do
|
|
echo >&2 "!!!!! Starting recovery shell"
|
|
sleep 1
|
|
|
|
if [ -x /bin/setsid ]; then
|
|
/bin/setsid -c /bin/sh
|
|
else
|
|
/bin/sh
|
|
fi
|
|
done
|
|
}
|
|
|
|
pause_recovery() {
|
|
TRACE "Under /etc/functions:pause_recovery"
|
|
read -p $'!!! Hit enter to proceed to recovery shell !!!\n'
|
|
recovery $*
|
|
}
|
|
|
|
pcrs() {
|
|
if [ "$CONFIG_TPM2_TOOLS" = "y" ]; then
|
|
tpm2 pcrread sha256
|
|
elif [ "$CONFIG_TPM" = "y" ]; then
|
|
head -8 /sys/class/tpm/tpm0/pcrs
|
|
fi
|
|
}
|
|
|
|
confirm_totp()
|
|
{
|
|
TRACE "Under /etc/functions:confirm_totp"
|
|
prompt="$1"
|
|
last_half=X
|
|
unset totp_confirm
|
|
|
|
while true; do
|
|
|
|
# update the TOTP code every thirty seconds
|
|
date=`date "+%Y-%m-%d %H:%M:%S"`
|
|
seconds=`date "+%s"`
|
|
half=`expr \( $seconds % 60 \) / 30`
|
|
if [ "$CONFIG_TPM" != "y" ]; then
|
|
TOTP="NO TPM"
|
|
elif [ "$half" != "$last_half" ]; then
|
|
last_half=$half;
|
|
TOTP=`unseal-totp` \
|
|
|| recovery "TOTP code generation failed"
|
|
fi
|
|
|
|
echo -n "$date $TOTP: "
|
|
|
|
# read the first character, non-blocking
|
|
read \
|
|
-t 1 \
|
|
-n 1 \
|
|
-s \
|
|
-p "$prompt" \
|
|
totp_confirm \
|
|
&& break
|
|
|
|
# nothing typed, redraw the line
|
|
echo -ne '\r'
|
|
done
|
|
|
|
# clean up with a newline
|
|
echo
|
|
}
|
|
|
|
enable_usb()
|
|
{
|
|
TRACE "Under /etc/functions:enable_usb"
|
|
#insmod ehci_hcd prior of uhdc_hcd and ohci_hcd to suppress dmesg warning
|
|
if ! lsmod | grep -q ehci_hcd; then
|
|
insmod /lib/modules/ehci-hcd.ko \
|
|
|| die "ehci_hcd: module load failed"
|
|
fi
|
|
if [ "$CONFIG_LINUX_USB_COMPANION_CONTROLLER" = y ]; then
|
|
if ! lsmod | grep -q uhci_hcd; then
|
|
insmod /lib/modules/uhci-hcd.ko \
|
|
|| die "uhci_hcd: module load failed"
|
|
fi
|
|
if ! lsmod | grep -q ohci_hcd; then
|
|
insmod /lib/modules/ohci-hcd.ko \
|
|
|| die "ohci_hcd: module load failed"
|
|
fi
|
|
if ! lsmod | grep -q ohci_pci; then
|
|
insmod /lib/modules/ohci-pci.ko \
|
|
|| die "ohci_pci: module load failed"
|
|
fi
|
|
fi
|
|
if ! lsmod | grep -q ehci_pci; then
|
|
insmod /lib/modules/ehci-pci.ko \
|
|
|| die "ehci_pci: module load failed"
|
|
fi
|
|
if ! lsmod | grep -q xhci_hcd; then
|
|
insmod /lib/modules/xhci-hcd.ko \
|
|
|| die "xhci_hcd: module load failed"
|
|
fi
|
|
if ! lsmod | grep -q xhci_pci; then
|
|
insmod /lib/modules/xhci-pci.ko \
|
|
|| die "xhci_pci: module load failed"
|
|
sleep 2
|
|
fi
|
|
|
|
if [ "$CONFIG_USB_KEYBOARD" = y ]; then
|
|
if ! lsmod | grep -q usbhid; then
|
|
insmod /lib/modules/usbhid.ko \
|
|
|| die "usbhid: module load failed"
|
|
fi
|
|
fi
|
|
|
|
}
|
|
|
|
list_usb_storage()
|
|
{
|
|
TRACE "Under /etc/functions:list_usb_storage"
|
|
stat -c %N /sys/block/sd* 2>/dev/null | grep usb |
|
|
cut -f1 -d ' ' |
|
|
sed "s/[']//g" |
|
|
while read b; do
|
|
# Ignore devices of size 0, such as empty SD card
|
|
# readers on laptops attached via USB.
|
|
if [ "$(cat "$b/size")" -gt 0 ]; then
|
|
echo "$b"
|
|
fi
|
|
done |
|
|
sed "s|/sys/block|/dev|" |
|
|
while read b; do
|
|
# If the device has a partition table, ignore it and
|
|
# include the partitions instead - even if the kernel
|
|
# hasn't detected the partitions yet. Such a device is
|
|
# never usable directly, and this allows the "wait for
|
|
# disks" loop in mount-usb to correctly wait for the
|
|
# partitions.
|
|
# This check: [ $(fdisk -l "$b" | wc -l) -eq 5 ]
|
|
# covers the case of a device without partition table but
|
|
# formatted as fat32, which contains a sortof partition table.
|
|
# this causes fdisk to not print the invalid partition table
|
|
# message and instead it'll print an empty table with header.
|
|
# In both cases the output is 5 lines: 3 about device info,
|
|
# 1 empty line and the 5th will be the table header or the
|
|
# unvalid message.
|
|
DISK_DATA=$(fdisk -l "$b")
|
|
if echo "$DISK_DATA" | grep -q "doesn't contain a valid partition table" || [ $(echo "$DISK_DATA" | wc -l) -eq 5 ]; then
|
|
# No partition table, include this device
|
|
echo "$b"
|
|
else
|
|
# Has a partition table, include partitions
|
|
ls -1 "$b"* | awk 'NR!=1 {print $0}'
|
|
fi
|
|
done
|
|
}
|
|
|
|
confirm_gpg_card()
|
|
{
|
|
TRACE "Under /etc/functions:confirm_gpg_card"
|
|
read \
|
|
-n 1 \
|
|
-p "Please confirm that your GPG card is inserted [Y/n]: " \
|
|
card_confirm
|
|
echo
|
|
|
|
if [ "$card_confirm" != "y" \
|
|
-a "$card_confirm" != "Y" \
|
|
-a -n "$card_confirm" ] \
|
|
; then
|
|
die "gpg card not confirmed"
|
|
fi
|
|
|
|
# setup the USB so we can reach the GPG card
|
|
enable_usb
|
|
|
|
echo -e "\nVerifying presence of GPG card...\n"
|
|
# ensure we don't exit without retrying
|
|
errexit=$(set -o | grep errexit | awk '{print $2}')
|
|
set +e
|
|
gpg --card-status > /dev/null
|
|
if [ $? -ne 0 ]; then
|
|
# prompt for reinsertion and try a second time
|
|
read -n1 -r -p \
|
|
"Can't access GPG key; remove and reinsert, then press Enter to retry. " \
|
|
ignored
|
|
# restore prev errexit state
|
|
if [ "$errexit" = "on" ]; then
|
|
set -e
|
|
fi
|
|
# retry card status
|
|
gpg --card-status > /dev/null \
|
|
|| die "gpg card read failed"
|
|
fi
|
|
# restore prev errexit state
|
|
if [ "$errexit" = "on" ]; then
|
|
set -e
|
|
fi
|
|
}
|
|
|
|
# Prompt for an owner password if it is not already set in tpm_password. Sets
|
|
# tpm_password. Tools should optionally accept a TPM password on the command
|
|
# line, since some flows need it multiple times and only one prompt is ideal.
|
|
prompt_tpm_password() {
|
|
if [ -n "$tpm_password" ]; then
|
|
return 0;
|
|
fi
|
|
|
|
read -s -p "TPM Owner password: " tpm_password
|
|
echo # new line after password prompt
|
|
}
|
|
|
|
check_tpm_counter()
|
|
{
|
|
TRACE "Under /etc/functions:check_tpm_counter"
|
|
LABEL=${2:-3135106223}
|
|
tpm_password="$3"
|
|
# if the /boot.hashes file already exists, read the TPM counter ID
|
|
# from it.
|
|
if [ -r "$1" ]; then
|
|
TPM_COUNTER=`grep counter- "$1" | cut -d- -f2`
|
|
else
|
|
warn "$1 does not exist; creating new TPM counter"
|
|
prompt_tpm_password
|
|
tpmr counter_create \
|
|
-pwdo "$tpm_password" \
|
|
-pwdc '' \
|
|
-la $LABEL \
|
|
| tee /tmp/counter \
|
|
|| die "Unable to create TPM counter"
|
|
TPM_COUNTER=`cut -d: -f1 < /tmp/counter`
|
|
fi
|
|
|
|
if [ -z "$TPM_COUNTER" ]; then
|
|
die "$1: TPM Counter not found?"
|
|
fi
|
|
}
|
|
|
|
read_tpm_counter()
|
|
{
|
|
TRACE "Under /etc/functions:read_tpm_counter"
|
|
tpmr counter_read -ix "$1" | tee "/tmp/counter-$1" \
|
|
|| die "Counter read failed"
|
|
}
|
|
|
|
increment_tpm_counter()
|
|
{
|
|
TRACE "Under /etc/functions:increment_tpm_counter"
|
|
tpmr counter_increment -ix "$1" -pwdc '' \
|
|
| tee /tmp/counter-$1 \
|
|
|| die "Counter increment failed"
|
|
}
|
|
|
|
check_config() {
|
|
TRACE "Under /etc/functions:check_config"
|
|
if [ ! -d /tmp/kexec ]; then
|
|
mkdir /tmp/kexec \
|
|
|| die 'Failed to make kexec tmp dir'
|
|
else
|
|
rm -rf /tmp/kexec/* \
|
|
|| die 'Failed to empty kexec tmp dir'
|
|
fi
|
|
|
|
if [ ! -r $1/kexec.sig ]; then
|
|
return
|
|
fi
|
|
|
|
if [ `find $1/kexec*.txt | wc -l` -eq 0 ]; then
|
|
return
|
|
fi
|
|
|
|
if [ "$2" != "force" ]; then
|
|
if ! sha256sum `find $1/kexec*.txt` | gpgv $1/kexec.sig - ; then
|
|
die 'Invalid signature on kexec boot params'
|
|
fi
|
|
fi
|
|
|
|
echo "+++ Found verified kexec boot params"
|
|
cp $1/kexec*.txt /tmp/kexec \
|
|
|| die "Failed to copy kexec boot params to tmp"
|
|
}
|
|
|
|
replace_config() {
|
|
TRACE "Under /etc/functions:replace_config"
|
|
CONFIG_FILE=$1
|
|
CONFIG_OPTION=$2
|
|
NEW_SETTING=$3
|
|
|
|
touch $CONFIG_FILE
|
|
# first pull out the existing option from the global config and place in a tmp file
|
|
awk "gsub(\"^export ${CONFIG_OPTION}=.*\",\"export ${CONFIG_OPTION}=\\\"${NEW_SETTING}\\\"\")" /tmp/config > ${CONFIG_FILE}.tmp
|
|
awk "gsub(\"^${CONFIG_OPTION}=.*\",\"${CONFIG_OPTION}=\\\"${NEW_SETTING}\\\"\")" /tmp/config >> ${CONFIG_FILE}.tmp
|
|
|
|
# then copy any remaining settings from the existing config file, minus the option you changed
|
|
grep -v "^export ${CONFIG_OPTION}=" ${CONFIG_FILE} | grep -v "^${CONFIG_OPTION}=" >> ${CONFIG_FILE}.tmp || true
|
|
sort ${CONFIG_FILE}.tmp | uniq > ${CONFIG_FILE}
|
|
rm -f ${CONFIG_FILE}.tmp
|
|
}
|
|
combine_configs() {
|
|
TRACE "Under /etc/functions:combine_configs"
|
|
cat /etc/config* > /tmp/config
|
|
}
|
|
|
|
update_checksums()
|
|
{
|
|
TRACE "Under /etc/functions:update_checksums"
|
|
# ensure /boot mounted
|
|
if ! grep -q /boot /proc/mounts ; then
|
|
mount -o ro /boot \
|
|
|| recovery "Unable to mount /boot"
|
|
fi
|
|
|
|
# remount RW
|
|
mount -o rw,remount /boot
|
|
|
|
# sign and auto-roll config counter
|
|
extparam=
|
|
if [ "$CONFIG_TPM" = "y" ];then
|
|
if [ "$CONFIG_IGNORE_ROLLBACK" != "y" ]; then
|
|
extparam=-r
|
|
fi
|
|
fi
|
|
if ! kexec-sign-config -p /boot -u $extparam ; then
|
|
rv=1
|
|
else
|
|
rv=0
|
|
fi
|
|
|
|
# switch back to ro mode
|
|
mount -o ro,remount /boot
|
|
|
|
return $rv
|
|
}
|
|
|
|
print_tree() {
|
|
TRACE "Under /etc/functions:print_tree"
|
|
find ./ ! -path './kexec*' -print0 | sort -z
|
|
}
|
|
|
|
# Escape zero-delimited standard input to safely display it to the user in e.g.
|
|
# `whiptail`, `less`, `echo`, `cat`. Doesn't produce shell-escaped output.
|
|
# Most printable characters are passed verbatim (exception: \).
|
|
# These escapes are used to replace their corresponding characters: #n#r#t#v#b
|
|
# Other characters are rendered as hexadecimal escapes.
|
|
# escape_zero [prefix] [escape character]
|
|
# prefix: \0 in the input will result in \n[prefix]
|
|
# escape character: character to use for escapes (default: #); \ may be interpreted by `whiptail`
|
|
escape_zero() {
|
|
local prefix="$1"
|
|
local echar="${2:-#}"
|
|
local todo=""
|
|
local echar_hex="$(echo -n "$echar" | xxd -p -c1)"
|
|
[ ${#echar_hex} -eq 2 ] || die "Invalid escape character $echar passed to escape_zero(). Programming error?!"
|
|
|
|
echo -e -n "$prefix"
|
|
xxd -p -c1 | tr -d '\n' |
|
|
{
|
|
while IFS= read -r -n2 -d '' ; do
|
|
if [ -n "$todo" ] ; then
|
|
#REPLY == " " is EOF
|
|
[[ "$REPLY" == " " ]] && echo '' || echo -e -n "$todo"
|
|
todo=""
|
|
fi
|
|
|
|
case "$REPLY" in
|
|
00)
|
|
todo="\n$prefix"
|
|
;;
|
|
08)
|
|
echo -n "${echar}b"
|
|
;;
|
|
09)
|
|
echo -n "${echar}t"
|
|
;;
|
|
0a)
|
|
echo -n "${echar}n"
|
|
;;
|
|
0b)
|
|
echo -n "${echar}v"
|
|
;;
|
|
0d)
|
|
echo -n "${echar}r"
|
|
;;
|
|
"$echar_hex")
|
|
echo -n "$echar$echar"
|
|
;;
|
|
#interpreted characters:
|
|
2[0-9a-f]|3[0-9a-f]|4[0-9a-f]|5[0-9abd-f]|6[0-9a-f]|7[0-9a-e])
|
|
echo -e -n '\x'"$REPLY"
|
|
;;
|
|
# All others are escaped
|
|
*)
|
|
echo -n "${echar}x$REPLY"
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
}
|
|
|
|
# Currently heads doesn't support signing file names with certain characters
|
|
# due to https://bugs.busybox.net/show_bug.cgi?id=14226. Also, certain characters
|
|
# may be intepreted by `whiptail`, `less` et al (e.g. \n, \b, ...).
|
|
assert_signable() {
|
|
TRACE "Under /etc/functions:assert_signable"
|
|
# ensure /boot mounted
|
|
if ! grep -q /boot /proc/mounts ; then
|
|
mount -o ro /boot || die "Unable to mount /boot"
|
|
fi
|
|
|
|
find /boot -print0 > /tmp/signable.ref
|
|
local del='\001-\037\134\177-\377'
|
|
LC_ALL=C tr -d "$del" < /tmp/signable.ref > /tmp/signable.del || die "Failed to execute tr."
|
|
if ! cmp -s "/tmp/signable.ref" "/tmp/signable.del" &> /dev/null ; then
|
|
local user_out="/tmp/hash_output_mismatches"
|
|
local add="Please investigate!"
|
|
[ -f "$user_out" ] && add="Please investigate the following relative paths to /boot (where # are sanitized invalid characters):"$'\n'"$(cat "$user_out")"
|
|
recovery "Some /boot file names contain characters that are currently not supported by heads: $del"$'\n'"$add"
|
|
fi
|
|
rm -f /tmp/signable.*
|
|
}
|
|
|
|
verify_checksums()
|
|
{
|
|
TRACE "Under /etc/functions:verify_checksums"
|
|
local boot_dir="$1"
|
|
local gui="${2:-y}"
|
|
|
|
(
|
|
set +e -o pipefail
|
|
local ret=0
|
|
cd "$boot_dir" || ret=1
|
|
sha256sum -c "$TMP_HASH_FILE" > /tmp/hash_output || ret=1
|
|
|
|
# also make sure that the file & directory structure didn't change
|
|
# (sha256sum won't detect added files)
|
|
print_tree > /tmp/tree_output || ret=1
|
|
if ! cmp -s "$TMP_TREE_FILE" /tmp/tree_output &> /dev/null ; then
|
|
ret=1
|
|
[[ "$gui" != "y" ]] && exit "$ret"
|
|
# produce a diff that can safely be presented to the user
|
|
# this is relatively hard as file names may e.g. contain backslashes etc.,
|
|
# which are interpreted by whiptail, less, ...
|
|
escape_zero "(new) " < "$TMP_TREE_FILE" > "${TMP_TREE_FILE}.user"
|
|
escape_zero "(new) " < /tmp/tree_output > /tmp/tree_output.user
|
|
diff "${TMP_TREE_FILE}.user" /tmp/tree_output.user | grep -E '^\+\(new\).*$' | sed -r 's/^\+\(new\)/(new)/g' >> /tmp/hash_output
|
|
rm -f "${TMP_TREE_FILE}.user"
|
|
rm -f /tmp/tree_output.user
|
|
fi
|
|
exit $ret
|
|
)
|
|
return $?
|
|
}
|
|
|
|
# detect and set /boot device
|
|
# mount /boot if successful
|
|
detect_boot_device()
|
|
{
|
|
TRACE "Under /etc/functions:detect_boot_device"
|
|
# unmount /boot to be safe
|
|
cd / && umount /boot 2>/dev/null
|
|
|
|
# check $CONFIG_BOOT_DEV if set/valid
|
|
if [ -e "$CONFIG_BOOT_DEV" ]; then
|
|
if mount -o ro $CONFIG_BOOT_DEV /boot >/dev/null 2>&1; then
|
|
if ls -d /boot/grub* >/dev/null 2>&1; then
|
|
# CONFIG_BOOT_DEV is valid device and contains an installed OS
|
|
return 0
|
|
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/boot_device_list
|
|
for i in `cat /tmp/disklist`; do
|
|
# remove block device from list if numeric partitions exist, since not bootable
|
|
DEV_NUM_PARTITIONS=$((`ls -1 $i* | wc -l`-1))
|
|
if [ ${DEV_NUM_PARTITIONS} -eq 0 ]; then
|
|
echo $i >> /tmp/boot_device_list
|
|
else
|
|
ls $i* | tail -${DEV_NUM_PARTITIONS} >> /tmp/boot_device_list
|
|
fi
|
|
done
|
|
|
|
# iterate thru possible options and check for grub dir
|
|
for i in `cat /tmp/boot_device_list`; do
|
|
umount /boot 2>/dev/null
|
|
if mount -o ro $i /boot >/dev/null 2>&1; then
|
|
if ls -d /boot/grub* >/dev/null 2>&1; then
|
|
CONFIG_BOOT_DEV="$i"
|
|
return 0
|
|
fi
|
|
fi
|
|
done
|
|
|
|
# no valid boot device found
|
|
echo "Unable to locate /boot files on any mounted disk"
|
|
umount /boot 2>/dev/null
|
|
return 1
|
|
}
|
|
|
|
calc()
|
|
{
|
|
awk "BEGIN { print "$*" }";
|
|
}
|
|
|
|
print_battery_health()
|
|
{
|
|
if [ -d /sys/class/power_supply/BAT* ]; then
|
|
battery_health=$(calc $(cat /sys/class/power_supply/BAT*/charge_full)/$(cat /sys/class/power_supply/BAT*/charge_full_design)*100 | awk -F "." {'print $1'})
|
|
echo "$battery_health"
|
|
fi
|
|
}
|
|
|
|
print_battery_charge()
|
|
{
|
|
if [ -d /sys/class/power_supply/BAT* ]; then
|
|
battery_charge=$(calc $(cat /sys/class/power_supply/BAT*/charge_now)/$(cat /sys/class/power_supply/BAT*/charge_full)*100 | awk -F "." {'print $1'})
|
|
echo "$battery_charge"
|
|
fi
|
|
}
|
|
|
|
generate_random_mac_address()
|
|
{
|
|
#Borrowed from https://stackoverflow.com/questions/42660218/bash-generate-random-mac-address-unicast
|
|
hexdump -n 6 -ve '1/1 "%.2x "' /dev/urandom | awk -v a="2,6,a,e" -v r="$RANDOM" 'BEGIN{srand(r);}NR==1{split(a,b,",");r=int(rand()*4+1);printf "%s%s:%s:%s:%s:%s:%s\n",substr($1,0,1),b[r],$2,$3,$4,$5,$6}'
|
|
}
|