From 353a0efe6f18f540a0fa51d58dc889cfab4f8398 Mon Sep 17 00:00:00 2001 From: Trammell Hudson Date: Wed, 12 Apr 2017 06:57:58 -0400 Subject: [PATCH] Rework /init and qubes setup scripts (issue #27, #155, #32, #29, #110) This adds support for seamless booting of Qubes with a TPM disk key, as well as signing of qubes files in /boot with a Yubikey. The signed hashes also includes a TPM counter, which is incremented when new hashes are signed. This prevents rollback attacks against the /boot filesystem. The TPMTOTP value is presented to the user at the time of entering the disk encryption keys. Hitting enter will generate a new code. The LUKS headers are included in the TPM sealing of the disk encryption keys. --- initrd/bin/qubes-boot | 38 +++++++++++++ initrd/bin/qubes-init | 118 +++++++++++++++++++--------------------- initrd/bin/qubes-update | 81 +++++++++++++++++++++++++++ initrd/bin/unseal-key | 46 ++++++++++------ initrd/init | 78 +++++++++++++------------- qubes/boot.sh | 107 ------------------------------------ qubes/boot.sh.asc | 10 ---- 7 files changed, 242 insertions(+), 236 deletions(-) create mode 100755 initrd/bin/qubes-boot create mode 100755 initrd/bin/qubes-update delete mode 100644 qubes/boot.sh delete mode 100644 qubes/boot.sh.asc diff --git a/initrd/bin/qubes-boot b/initrd/bin/qubes-boot new file mode 100755 index 00000000..e6e32af6 --- /dev/null +++ b/initrd/bin/qubes-boot @@ -0,0 +1,38 @@ +#!/bin/sh +# Final stage to start qubes given a Xen, dom0 kernel and initrd +# get the UUID of the root file system +# busybox blkid doesn't have a "just the UUID" option +. /etc/functions +. /etc/config + +XEN="$1" +KERNEL="$2" +INITRD="$3" + +if [ -z "$XEN" -o -z "$KERNEL" -o -z "$INITRD" ]; then + die "Usage: $0 /boot/xen... /boot/vmlinuz... /boot/initramfs..." +fi + +# Activate the dom0 group, if it isn't already active +lvm vgchange -a y "$CONFIG_QUBES_VG" \ + || die "$CONFIG_QUBES_VG: LVM volume group activate failed" + +ROOT_UUID=`blkid /dev/$CONFIG_QUBES_VG/00 | cut -d\" -f2` +if [ -z "$ROOT_UUID" ]; then + die "$CONFIG_QUBES_VG/00: No UUID for /" +fi + +echo "$CONFIG_QUBES_VG/00: UUID=$ROOT_UUID" + +# command line arguments are include in the signature on this script, +echo '+++ Loading kernel and initrd' +kexec \ + -l \ + --module "$KERNEL root=/dev/mapper/luks-$ROOT_UUID ro rd.qubes.hide_all_usb" \ + --module "$INITRD" \ + --command-line "no-real-mode reboot=no" \ + "${XEN}" \ +|| die "kexec load failed" + +echo "+++ Starting Qubes..." +exec kexec -e diff --git a/initrd/bin/qubes-init b/initrd/bin/qubes-init index 1a564060..cf95d16b 100755 --- a/initrd/bin/qubes-init +++ b/initrd/bin/qubes-init @@ -5,85 +5,79 @@ # which is only set if the top level /init script has started # without user intervention or dropping into a recovery shell. -recovery() { - echo >&2 "!!!!! $@" - rm -f /tmp/secret.key - tpm extend -ix 4 -ic recovery +. /etc/functions +. /etc/config - echo >&2 "!!!!! Starting recovery shell" - exec /bin/ash -} - -. /config - -# TODO: Allow /boot to be encrypted? -# This would require a different TPM key or a user -# passphrase to decrypt it. -mount -o ro "$CONFIG_QUBES_BOOT_DEV" /boot \ - || recovery '$CONFIG_BOOT_DEV: Unable to mount /boot' - -BOOT_SCRIPT=/boot/boot.sh -if [ ! -x /boot/boot.sh ]; then - recovery "$BOOT_SCRIPT does not exist" +if [ "$1" = "recovery" ]; then + warn "Recovery mode boot; ignoring key failures" + RECOVERY=1 fi -# Hand control over to the user boot script -echo "+++ Checking $BOOT_SCRIPT" -gpgv "$BOOT_SCRIPT.asc" "$BOOT_SCRIPT" \ - || recovery 'boot script signature failed' +# TODO: Allow /boot to be encrypted? +# This would require a different TPM key, a user passphrase to decrypt it, +# or loading the USB modules to talk to a Yubikey to get the thing. +if ! grep -q /boot /proc/mounts ; then + mount -o ro "$CONFIG_QUBES_BOOT_DEV" /boot \ + || recovery '$CONFIG_BOOT_DEV: Unable to mount /boot' +fi -exec "$BOOT_SCRIPT" +BOOT_HASHES=/boot/boot.hashes +if [ ! -r "$BOOT_HASHES" ]; then + recovery "$BOOT_HASHES does not exist; re-run qubes-update" +fi -recovery 'Boot script exec failed?' +# Verify the signature on the hashes +gpgv "$BOOT_HASHES.asc" "$BOOT_HASHES" \ + || recovery 'boot hashes signature failed' -############################ -# For historical reference +# Retrieve the TPM counter ID and generate its current value +TPM_COUNTER=`grep counter $BOOT_HASHES | cut -d- -f2` +if [ -z "$TPM_COUNTER" ]; then + recovery "$BOOT_HASHES: TPM counter not found?" +fi -# TODO: Allow these to be specified on the /boot device -XEN=/boot/xen-4.6.3.heads -INITRD=/boot/initramfs-4.4.31-11.pvops.qubes.x86_64.img -KERNEL=/boot/vmlinuz-4.4.31-11.pvops.qubes.x86_64 +tpm counter_read -ix "$TPM_COUNTER" | tee "/tmp/counter-$TPM_COUNTER" -echo "+++ Checking $XEN" -gpgv "${XEN}.asc" "${XEN}" \ - || recovery 'Xen signature failed' +# Check the hashes of all the files +sha256sum -c "$BOOT_HASHES" \ +|| recovery "$BOOT_HASHES: hash mismatch" -echo "+++ Checking $INITRD" -gpgv "${INITRD}.asc" "${INITRD}" \ - || recovery 'Initrd signature failed' +XEN=`grep /boot/xen $BOOT_HASHES | cut -d\ -f3 | tail -1` +KERNEL=`grep /boot/vmlin $BOOT_HASHES | cut -d\ -f3 | tail -1` +INITRD=`grep /boot/initram $BOOT_HASHES | cut -d\ -f3 | tail -1` -echo "+++ Checking $KERNEL" -gpgv "${KERNEL}.asc" "${KERNEL}" \ - || recovery 'Kernel signature failed' +# Activate the dom0 group +lvm vgchange -a y "$CONFIG_QUBES_VG" \ + || recovery "$CONFIG_QUBES_VG: LVM volume group activate failed" # Measure the LUKS headers before we unseal the disk key -/bin/qubes-measure-luks $CONFIG_QUBES_DEVS \ +qubes-measure-luks /dev/$CONFIG_QUBES_VG/* \ || recovery "LUKS measure failed" +# Unpack the initrd and fixup the /etc/crypttab +# this is a hack to split it into two parts since +# we know that the first 0x3400 bytes are the microcode +INITRD_DIR=/tmp/secret/initrd +SECRET_CPIO=/tmp/secret/initrd.cpio +mkdir -p "$INITRD_DIR/etc" + # Attempt to unseal the disk key from the TPM # should we give this some number of tries? -unseal-key \ - || recovery 'Unseal disk key failed. Starting recovery shell' +if ! unseal-key "$INITRD_DIR/secret.key" ; then + warn 'Unseal disk key failed' + if [ -z "$RECOVERY" ]; then + recovery 'Starting recovery shell' + fi +fi -# command line arguments are in the hash, so they are "correct". -kexec \ - -l \ - --module "${KERNEL} root=LABEL=root ro rd.qubes.hide_all_usb rhgb" \ - --module "${INITRD}" \ - --command-line "no-real-mode reboot=no" - "${XEN}" \ -|| recovery "kexec load failed" - - -kexec -l \ - --module "${KERNEL} root=UUID=257b593f-d4ae-46ee-b499-14bc9ffd37d4 ro rd.qubes.hide_all_usb" \ - --module "/boot/initramfs-4.4.31-11.pvops.qubes.x86_64.img" \ - --command-line "no-real-mode reboot=no" \ - /boot/xen-4.6.3.heads - -# Last step is to override PCR 6 so that user can't read the key +# Override PCR 4 so that user can't read the key tpm extend -ix 4 -ic qubes \ || recovery 'Unable to scramble PCR' -echo "+++ Starting Qubes..." -exec kexec -e +echo '+++ Building initrd' +( cd "$INITRD_DIR" ; find . | cpio -H newc -o ) > "$SECRET_CPIO" +cat "$INITRD" >> "$SECRET_CPIO" + +/bin/qubes-boot "$XEN" "$KERNEL" "$SECRET_CPIO" + +recovery "Something failed..." diff --git a/initrd/bin/qubes-update b/initrd/bin/qubes-update new file mode 100755 index 00000000..0b259a0c --- /dev/null +++ b/initrd/bin/qubes-update @@ -0,0 +1,81 @@ +#!/bin/sh +# Update the /boot partition signatures +set -o pipefail +. /etc/functions + +XEN="$1" +KERNEL="$2" +INITRD="$3" +BOOT_HASHES="/boot/boot.hashes" + +if [ -z "$XEN" -o -z "$KERNEL" -o -z "$INITRD" ]; then + die "Usage: $0 /boot/xen... /boot/vmlinuz... /boot/initramfs..." +fi + +# setup the USB so we can reach the GPG card +if ! lsmod | grep -q ehci_hcd; then + insmod /lib/modules/ehci-hcd.ko \ + || die "ehci_hcd: module load failed" +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 "ehci_hcd: module load failed" +fi +if ! lsmod | grep -q xhci_pci; then + insmod /lib/modules/xhci-pci.ko \ + || die "ehci_pci: module load failed" + sleep 2 +fi + +gpg --card-status \ +|| die "gpg card read failed" + +# if the /boot.hashes file already exists, read the TPM counter ID +# from it. +if [ -r "$BOOT_HASHES" ]; then + TPM_COUNTER=`grep counter- "$BOOT_HASHES" | cut -d- -f2` +else + warn "$BOOT_HASHES does not exist; creating new TPM counter" + read -s -p "TPM Owner password: " tpm_password + echo + tpm counter_create \ + -pwdo "$tpm_password" \ + -pwdc '' \ + -la 3135106223 \ + | tee /tmp/counter \ + || die "Unable to create TPM counter" + TPM_COUNTER=`cut -d: -f1 < /tmp/counter` +fi + +if [ -z "$TPM_COUNTER" ]; then + die "$BOOT_HASHES: TPM Counter not found?" +fi + +mount -o rw,remount /boot \ +|| die "Could not remount /boot" + +tpm counter_increment -ix "$TPM_COUNTER" -pwdc '' \ + | tee /tmp/counter-$TPM_COUNTER \ +|| die "Counter increment failed" + +sha256sum \ + "$XEN" \ + "$KERNEL" \ + "$INITRD" \ + "/tmp/counter-$TPM_COUNTER" \ +| tee "$BOOT_HASHES" + +for tries in 1 2 3; do + if gpg --detach-sign -a "$BOOT_HASHES"; then + mount -o ro,remount /boot + exit 0 + fi +done + +warn "$BOOT_HASHES: Unable to sign boot hashes" +mount -o ro,remount /boot +exit 1 diff --git a/initrd/bin/unseal-key b/initrd/bin/unseal-key index 6783f5c1..f1306579 100755 --- a/initrd/bin/unseal-key +++ b/initrd/bin/unseal-key @@ -1,34 +1,48 @@ #!/bin/sh # This will unseal and unecncrypt the drive encryption key from the TPM +# The TOTP secret will be shown to the user on each encryption attempt. # It will then need to be bundled into initrd that is booted with Qubes. TPM_INDEX=3 TPM_SIZE=312 -die() { echo >&2 "$@"; exit 1; } -warn() { echo >&2 "$@"; } +. /etc/functions +mkdir -p /tmp/secret +sealed_file="/tmp/secret/sealed.key" key_file="$1" -if [ -z "$key_file" ]; then - key_file=/tmp/secret.key -fi -read -s -p "Disk encryption password: " tpm_password -echo +if [ -z "$key_file" ]; then + key_file="/tmp/secret/secret.key" +fi tpm nv_readvalue \ -in "$TPM_INDEX" \ -sz "$TPM_SIZE" \ - -of /tmp/sealed \ + -of "$sealed_file" \ || die "Unable to read key from TPM NVRAM" -tpm unsealfile \ - -if /tmp/sealed \ - -of "$key_file" \ - -pwdd "$tpm_password" \ - -hk 40000000 \ -|| die "Unable to unseal disk encryption key" +for tries in 1 2 3; do + tpm_password= + while [ -z "$tpm_password" ]; do + unseal-totp || die "TOTP code generation failed" -rm -f /tmp/sealed + read -s -p "Disk unlock password: " tpm_password + echo + done -exit 0 + if tpm unsealfile \ + -if "$sealed_file" \ + -of "$key_file" \ + -pwdd "$tpm_password" \ + -hk 40000000 \ + ; then + rm -f /tmp/secret/sealed + exit 0 + fi + + pcrs + warn "Unable to unseal disk encryption key" +done + +die "Retry count exceeded..." diff --git a/initrd/init b/initrd/init index 01b090b6..0e83b1df 100755 --- a/initrd/init +++ b/initrd/init @@ -1,10 +1,19 @@ #!/bin/ash +# This is the very first script invoked by the Linux kernel and is +# running out of the ram disk. There are no fileysstems mounted. +# It is important to have a way to invoke a recovery shell in case +# the boot scripts are messed up, but also important to modify the +# PCRs if this happens to prevent the TPM disk keys from being revealed. + # First thing it is vital to mount the /dev and other system directories mkdir /proc /sys /dev /tmp /boot /media 2>&- 1>&- mount /dev mount /proc mount /sys +# Recovery shells will erase anything from here +mkdir -p /tmp/secret + # Setup our path export PATH=/sbin:/bin @@ -17,50 +26,37 @@ fi hwclock -l -s # Read the system configuration parameters -. /config +. /etc/functions +. /etc/config -if [ -z "$CONFIG_TIMEOUT" ]; then - CONFIG_TIMEOUT=10 +if [ ! -x "$CONFIG_BOOTSCRIPT" ]; then + recovery 'Boot script missing? Entering recovery shell' + # just in case... + tpm extend -ix 4 recovery + exec /bin/ash fi -while true; do - boot_option= +# Give the user a second to enter a recovery shell +read \ + -t "1" \ + -p "Press 'r' for recovery shell: " \ + -n 1 \ + boot_option +echo - # Verify the user's TPM secret - echo "TPM TOTP:" - if ! unsealtotp.sh ; then - echo '!!!!!' - echo '!!!!! TPM TOTP secret not found.' - echo '!!!!! This firmware can not be trusted.' - echo '!!!!! Entering recovery shell' - echo '!!!!!' - tpm extend -ix 4 -ic "tpm-failure" - exec /bin/ash - fi +if [ "$boot_option" = "r" ]; then + # Start an interactive shell + recovery 'User requested recovery shell' + # just in case... + tpm extend -ix 4 recovery + exec /bin/ash +fi - # Secret decrypted ok, so prompt for a next step - read \ - -t "$CONFIG_TIMEOUT" \ - -p "Enter for normal boot or 'r' for recovery shell: " \ - -n 1 \ - boot_option +echo '***** Normal boot' +exec "$CONFIG_BOOTSCRIPT" - if [ "$boot_option" = "r" ]; then - # Start an interactive shell - echo '***** Starting recovery shell' - tpm extend -ix 4 -ic "recovery" - exec /bin/ash - fi - - if [ "$boot_option" = "" ]; then - if [ ! -x "$CONFIG_BOOTSCRIPT" ]; then - echo '!!!!! Boot script missing? Entering recovery shell' - tpm extend -ix 4 -ic "boot-failure" - exec /bin/ash - fi - - echo '***** Normal boot' - tpm extend -ix 4 -ic "normal-boot" - exec "$CONFIG_BOOTSCRIPT" - fi -done +# We should never reach here, but just in case... +recovery 'Boot script failure? Entering recovery shell' +# belts and suspenders, just in case... +tpm extend -ix 4 recovery +exec /bin/ash diff --git a/qubes/boot.sh b/qubes/boot.sh deleted file mode 100644 index 57a2c02f..00000000 --- a/qubes/boot.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/bin/sh -# /boot/boot.sh -- Startup Qubes -# -# The signature on this script will be verified by the ROM, -# and this script lives on the /boot partition to allow -# the system owner to change the specific Qubes boot parameters -# -# This depends on the PCR 4 being "normal-boot": -# f8fa3b6e32e7c6fe04c366e74636e505b28f3b0d -# which is only set if the top level /init script has started -# without user intervention or dropping into a recovery shell. -# -# To sign this script and the other bootable components: -# -# gpg -a --sign --detach-sign boot.sh -# - -XEN=/boot/xen-4.6.4.heads -INITRD=/boot/initramfs-4.4.14-11.pvops.qubes.x86_64.img -KERNEL=/boot/vmlinuz-4.4.14-11.pvops.qubes.x86_64 - - -recovery() { - echo >&2 "!!!!! $@" - rm -f /tmp/secret.key /initrd.gz - tpm extend -ix 4 -ic recovery - - echo >&2 "!!!!! Starting recovery shell" - exec /bin/ash -} - -. /config - -echo "+++ Checking $XEN" -gpgv "${XEN}.asc" "${XEN}" \ - || recovery 'Xen signature failed' - -echo "+++ Checking $INITRD" -gpgv "${INITRD}.asc" "${INITRD}" \ - || recovery 'Initrd signature failed' - -echo "+++ Checking $KERNEL" -gpgv "${KERNEL}.asc" "${KERNEL}" \ - || recovery 'Kernel signature failed' - -# Activate the dom0 group -lvm vgchange -a y "$CONFIG_QUBES_VG" \ - || recovery "$CONFIG_QUBES_VG: LVM volume group activate failed" - -# Measure the LUKS headers before we unseal the disk key -qubes-measure-luks /dev/$CONFIG_QUBES_VG/* \ - || recovery "LUKS measure failed" - -# get the UUID of the root file system -# busybox blkid doesn't have a "just the UUID" option -ROOT_UUID=`blkid /dev/$CONFIG_QUBES_VG/00 | cut -d\" -f2` -if [ -z "$ROOT_UUID" ]; then - recovery "$CONFIG_QUBES_VG/00: No UUID for /" -fi - -echo "$CONFIG_QUBES_VG/00: UUID=$ROOT_UUID" - -# Attempt to unseal the disk key from the TPM -# should we give this some number of tries? -unseal-key \ - || recovery 'Unseal disk key failed. Starting recovery shell' - -# Unpack the initrd and fixup the /etc/crypttab -# this is a hack to split it into two parts since -# we know that the first 0x3400 bytes are the microcode -INITRD_DIR=/tmp/initrd -echo '+++ Unpacking initrd' -mkdir -p $INITRD_DIR/etc -#dd if="$INITRD" bs=256 count=52 | ( cd $INITRD_DIR ; cpio -i ) -#dd if="$INITRD" bs=256 skip=52 | zcat | ( cd $INITRD_DIR ; cpio -i ) - -mv /tmp/secret.key $INITRD_DIR/ - -## Update the /etc/crypttab in the initrd and install our key -## This is no longer required, now that dom0 /etc/crypttab has -## the /secret.key specified. -#for dev in /dev/$CONFIG_QUBES_VG/*; do -# uuid=`blkid $dev | cut -d\" -f2` -# echo luks-$uuid /dev/disk/by-uuid/$uuid /secret.key -#done > $INITRD_DIR/etc/crypttab - -echo '+++ Repacking initrd' -( cd $INITRD_DIR ; find . | cpio -H newc -o ) > /initrd.cpio -cat "$INITRD" >> /initrd.cpio - -# command line arguments are include in the signature on this script, -echo '+++ Loading kernel and initrd' -kexec \ - -l \ - --module "${KERNEL} root=/dev/mapper/luks-$ROOT_UUID ro rd.qubes.hide_all_usb" \ - --module /initrd.cpio \ - --command-line "no-real-mode reboot=no" \ - "${XEN}" \ -|| recovery "kexec load failed" - -# Last step is to override PCR 4 so that user can't read the key -tpm extend -ix 4 -ic qubes \ - || recovery 'Unable to scramble PCR' - -echo "+++ Starting Qubes..." -sleep 2 -exec kexec -e diff --git a/qubes/boot.sh.asc b/qubes/boot.sh.asc deleted file mode 100644 index de389498..00000000 --- a/qubes/boot.sh.asc +++ /dev/null @@ -1,10 +0,0 @@ ------BEGIN PGP SIGNATURE----- - -iQEVAwUAWOQaag+UgFLd7L5oAQIYMQgA1W3mnxsd6Bln0ipvZtITN0cAoAdsnuG/ -Kt/2Usabu7lzdYNpBp9h+jmGDj1Jg+5wvKBXgYQXiPG0TuPNXqeih+X1NJbeXO3S -BF6PXPEHkZlU7kDXUiPHVF9Hy2T6Kw45SQ5pEctATDYjO8SL/lVuxGRSXSiBdyW0 -PLEOHmVNh5C9LNtoGZmmRf8BkVpNc7LCZIkDWj29wNypaxBzv1AQmWBWTvWTSK3D -CkFW10DbF3nJZNrPtTY4EOV2fynRsCZYN/O3ZyN5iZ9kAm8WXWcjqMBB7K/bE3dw -KUb3E0pwyT+uAknT1pXPbcyx8hq6mvX0Fp+46UYovgx5KU+yQunItw== -=0kHU ------END PGP SIGNATURE-----