Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initrdscripts: Make cryptsetup fail hard in unexpected conditions #3486

Merged
merged 1 commit into from
Sep 13, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 123 additions & 20 deletions meta-balena-common/recipes-core/initrdscripts/files/cryptsetup-efi-tpm
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
#!/bin/sh

# This script unlocks encrypted partitions on UEFI systems
# with secure boot enabled and encryption key protected by a TPM2.0
#
# The partition layout of such system is:
# sda // Boot device
# |-sda1 // Unencrypted EFI partition, contains the bootloader and encrypted disk passphrase
# |-sda2 // Encrypted boot partition
# | `-dm-0 // Unlocked boot partition, contains balena-specifics, e.g. config.json
# |-sda3 // Encrypted rootA partition
# | `-dm-1 // Unlocked rootA partition
# |-sda4 // Encrypted rootB partition
# | `-dm-2 // Unlocked rootB partition
# |-sda5 // Encrypted state partition
# | `-dm-3 // Unlocked state partition
# `-sda6 // Encrypted data partition
# `-dm-4 // Unlocked data partition
#
# When this script executes, it should only find the encrypted partitions.
# The script unlocks the partitions using the metadata stored in the EFI
# partition in combination with secrets stored in the TPM.
# As a result the dm-0..dm-5 devices are created.
#
# After successfully unlocking a partition, the default udev rules will
# in fact create two instances of the unlocked device:
# * /dev/dm-X - the "dm-X" part is referred to as "KNAME".
# This is what the kernel uses internally to identify the device
# * /dev/mapper/luks-$UUID - the "luks-$UUID" part is referred to as "NAME".
# This is a user-defined name, we chose "luks-$UUID", the same as Fedora.
# The two devices are identical and can be used interchangeably, but this
# script tries to prefer KNAME as it seems more practical for scripting.

# shellcheck disable=SC1091
. /usr/libexec/os-helpers-logging
. /usr/libexec/os-helpers-fs
. /usr/libexec/os-helpers-tpm2
. /usr/libexec/os-helpers-efi
. /usr/sbin/balena-config-defaults

# Give a chance to the by-state directory to appear
# We do not expect any particular device or partition to come up
# but if balenaOS is correctly configured on the device the by-state
# directory will eventually be created by the custom udev rule.
# This is useful if the rootfs is on a device that takes a while
# to initialize such as a USB disk.
wait4file "/dev/disk/by-state" "50"
EFI_DEV=$(get_state_path_from_label "${BALENA_NONENC_BOOT_LABEL}")
ENCRYPTED_PARTITIONS="boot rootA rootB state data"

ensure_luks2() {
LUKS_DEVICE="$1"
Expand All @@ -35,16 +59,7 @@ cryptsetup_enabled() {

# Ensure that secure boot is enabled and in user mode before unlocking
if ! user_mode_enabled; then
return 1
fi

# Only run if the EFI partition is split
if [ ! -e "$EFI_DEV" ]; then
return 1
fi

# Check whether there are any LUKS partitions
if ! lsblk -nlo fstype | grep -q crypto_LUKS; then
info "Won't attempt to decrypt drives because secure boot is not enabled"
return 1
fi

Expand All @@ -55,6 +70,28 @@ cryptsetup_run() {
# Die if anything fails here
set -e

# Make sure there is only a single unencrypted partition.
# We will blindly mount it here with no further authentication,
# so if there are ambiguities, we prefer to bail out.
NONENC_PARTS="$(lsblk -nlo label | grep "${BALENA_NONENC_BOOT_LABEL}")"
if [ -z "${NONENC_PARTS}" ]; then
fail "Partition '${BALENA_NONENC_BOOT_LABEL}' not found"
fi

if [ "$(echo "${NONENC_PARTS}" | wc -l)" -gt 1 ]; then
fail "Multiple '${BALENA_NONENC_BOOT_LABEL}' partitions found"
fi

EFI_DEV=$(get_state_path_from_label "${BALENA_NONENC_BOOT_LABEL}")
if [ ! -e "$EFI_DEV" ]; then
fail "Won't attempt to decrypt drives because the EFI partition is not split"
fi

# Check whether there are any LUKS partitions
if ! lsblk -nlo fstype | grep -q crypto_LUKS; then
fail "There are no encrypted partitions"
fi

EFI_MOUNT_DIR="/efi"
mkdir "$EFI_MOUNT_DIR"
mount "$EFI_DEV" "$EFI_MOUNT_DIR"
Expand Down Expand Up @@ -86,14 +123,30 @@ cryptsetup_run() {

BOOT_DEVICE=$(lsblk -nlo pkname "${EFI_DEV}")

# Check that we have the expected amount of encrypted partitions on the boot device
LUKS_PARTITIONS=$(lsblk -nlo "kname,uuid,fstype,partlabel" "/dev/${BOOT_DEVICE}" | grep "crypto_LUKS")
if [ "$(echo "${LUKS_PARTITIONS}" | wc -l)" != "$(echo "${ENCRYPTED_PARTITIONS}" | wc -w)" ]; then
fail "An unexpected amount of encrypted partitions was found"
fi

# Unlock all the partitions - cryptsetup luksOpen does not wait for udev processing
# of the DM device to complete, it just kicks off the process and returns.
# Since this is async, we can perform all the luksOpens here, note the device names
# and wait for them in a separate loop later
LUKS_UNLOCKED=""
for LUKS_UUID in $(lsblk -nlo uuid,fstype "/dev/${BOOT_DEVICE}" | grep crypto_LUKS | cut -d " " -f 1); do
for PART_NAME in ${ENCRYPTED_PARTITIONS}; do
LUKS_UUID=$(echo "${LUKS_PARTITIONS}" | grep " \(balena\|resin\)-${PART_NAME}$" | cut -d " " -f 2)

if [ -z "${LUKS_UUID}" ]; then
fail "Partition '${PART_NAME}' not found"
fi

if [ "$(echo "${LUKS_UUID}" | wc -l)" -gt 1 ]; then
fail "More than one '${PART_NAME}' partition found"
fi

ensure_luks2 "/dev/disk/by-uuid/${LUKS_UUID}"
cryptsetup luksOpen --key-file $PASSPHRASE_FILE UUID="${LUKS_UUID}" luks-"${LUKS_UUID}"
cryptsetup luksOpen --key-file "${PASSPHRASE_FILE}" "UUID=${LUKS_UUID}" "luks-${LUKS_UUID}"
LUKS_UNLOCKED="${LUKS_UNLOCKED} luks-${LUKS_UUID}"
done

Expand All @@ -102,6 +155,56 @@ cryptsetup_run() {
wait4udev "/dev/mapper/${DM_NAME}"
done

# Perform sanity checks after unlocking.
# We know what the system should look like after the partitions are unlocked.
# We want to make sure that the newly unlocked partitions are the ones
# we are going to actually use, there is nothing missing and nothing extra.
for PART_NAME in ${ENCRYPTED_PARTITIONS}; do
LUKS_LINE=$(echo "${LUKS_PARTITIONS}" | grep " \(balena\|resin\)-${PART_NAME}$")

# Effectively the same checks have been done in the previous loop
# We want to be defensive, so we don't mind double-checking
if [ -z "${LUKS_LINE}" ]; then
fail "Partition '${PART_NAME}' not found"
fi

if [ "$(echo "${LUKS_LINE}" | wc -l)" -gt 1 ]; then
fail "More than one '${PART_NAME}' partition found"
fi

LUKS_KNAME=$(echo "${PART_LINE}" | cut -d " " -f 1)
LUKS_LABEL=$(echo "${PART_LINE}" | cut -d " " -f 4)

# Even though the following lsblk could be restricted to just /dev/${LUKS_KNAME}
# we intentionally do not want to do that. If there are multiple filesystems with
# the correct label, even though we know which one to use, we prefer to bail out,
# as this should never happen.
FS_LINE=$(lsblk -nlo "kname,pkname,label" | grep "${LUKS_LABEL}")
if [ -z "${FS_LINE}" ]; then
fail "Filesystem '${LUKS_LABEL}' not found after decryption"
fi

if [ "$(echo "${FS_LINE}" | wc -l)" -gt 1 ]; then
fail "More than one '${LUKS_LABEL}' filesystems found after decryption"
fi

FS_KNAME=$(echo "${FS_LINE}" | cut -d " " -f 1)
FS_PKNAME=$(echo "${FS_LINE}" | cut -d " " -f 2)

# We have found an encrypted partition with partlabel=${LUKS_LABEL} and a filesystem
# with label=${LUKS_LABEL}. Check that these are parent/child.
if [ "${FS_PKNAME}" != "${LUKS_KNAME}" ]; then
fail "LUKS partition '${LUKS_LABEL}' does not provide the expected filesystem"
fi

# The original idea was to check the /dev/disk/by-state symlinks here,
# but they do not all exist at this point. Because resin_update_state_probe
# only creates the symlinks for partitions that are on the same physical drive
# as the root partition, no by-state symlinks are created until rootA or rootB
# is unlocked. This is not an issue here and all further scripts that actually
# want to use the symlinks will trigger udev and get them created.
done

rm -f "$PASSPHRASE_FILE"
umount "$EFI_MOUNT_DIR"
rmdir "$EFI_MOUNT_DIR"
Expand Down
Loading