Skip to content

Instantly share code, notes, and snippets.

@danderson
Created October 7, 2018 21:16
Show Gist options
  • Save danderson/6a1c8aed390a0a20092998610af5c0e7 to your computer and use it in GitHub Desktop.
Save danderson/6a1c8aed390a0a20092998610af5c0e7 to your computer and use it in GitHub Desktop.
#!/bin/bash
# Temporary directory for assembling the unified kernel image.
WORKDIR=$(mktemp -d)
trap "rm -rf ${WORKDIR}" EXIT
# Two directories on the EFI partition: Arch for the bootloaders, and
# Linux for the unified kernel images.
#
# We want a directory for the bootloaders because Redhat's shim binary
# looks for its next stage bootloaders in its own directory, so we
# want to group everything in there.
#
# I *think* we could stuff everything under Linux/, which is where
# systemd-boot looks for unified linux kernel images, but I decided to
# not muddy the waters, in case non-linux images confuses systemd-boot
# somehow.
mkdir -p /boot/efi/EFI/Linux /boot/efi/EFI/Arch
# Unified kernel images combine the kernel, init ramdisks, kernel
# commandline, and OS metadata into a single EFI binary. systemd-boot
# knows how to read those unified images and start them up.
#
# Why do we do this instead of having individual files? Because by
# unifying everything up, we have a single file that we can sign for
# secure boot. This gives us the guarantee that all the code that runs
# up to and including the init ramdisk is trusted. So, we can trust
# the initrd to decrypt the root partition and pass control to that.
cp /boot/vmlinuz-linux ${WORKDIR}/linux
# Linux initrds are cool. The kernel will take a binary blob
# consisting of concatenated cpio archives, and unpack all of them
# into its tmpfs, overlaying each on top of the previous. The intel
# microcode is a cpio archive with one firmware file in it (systemd
# knows how to find this file and give it to the CPU), so we just
# concat it with the "real" ramdisk that contains all the disk
# decryption and rootfs mounting stuff.
cp /boot/intel-ucode.img ${WORKDIR}/initrd
cat /boot/initramfs-linux.img >>${WORKDIR}/initrd
cp /usr/lib/os-release ${WORKDIR}/os-release
# Arch's os-release doesn't include a version, because it's a rolling
# distro. But, systemd-boot only generates boot entries for unified
# binaries that have a VERSION_ID or BUILD_ID in its OS metadata. So,
# we just add some random version number.
echo "VERSION_ID=42" >>${WORKDIR}/os-release
# This commandline is specific to my system. It tells the
# systemd-based initrd which partition needs decrypting.
echo "root=/dev/mapper/root rw rd.luks.name=09d853a6-c65f-49a1-b467-6566d5ca711f=root" >${WORKDIR}/cmdline
# Smash all the components into a unified image. In virtual memory
# address order, the binary consists of:
# - EFI stub loader. Not sure exactly what it does, but roughly sets
# up the kernel, initrd and commandlines in a way that the kernel
# likes, and chainloads to the kernel's entrypoint.
# - OS release metadata. AFAIK, only systemd-boot cares about this to
# generate the bootloader menu entry.
# - Kernel commandline.
# - Linux kernel.
# - Init ramdisks.
#
# I don't think the specific virtual memory addresses are important,
# because systemd-boot and its EFI stub don't have hardcoded
# addresses. I believe the only requirement is that these sections
# don't overlap in virtual memory space, and these numbers just happen
# to pretty much ensure that.
objcopy \
--add-section .osrel="${WORKDIR}/os-release" --change-section-vma .osrel=0x20000 \
--add-section .cmdline="${WORKDIR}/cmdline" --change-section-vma .cmdline=0x30000 \
--add-section .linux="${WORKDIR}/linux" --change-section-vma .linux=0x40000 \
--add-section .initrd="${WORKDIR}/initrd" --change-section-vma .initrd=0x3000000 \
/usr/lib/systemd/boot/efi/linuxx64.efi.stub \
/boot/linux.efi
# Finally, sign the unified kernel image with our Machine Owner
# Key. Note that we keep the kernel in /boot until it's signed, so
# that the cleartext EFI partition only ever contains signed
# binaries. Unsigned binaries never leave the "trusted" encrypted
# root, to reduce the risk of them getting tampered with before
# they're signed.
#
# (Yes, this piece of the threat model is wonky, because if you have
# runtime access to the EFI partition, you can probably also get my
# root decryption key out of RAM and modify stuff there. Still, it
# doesn't hurt to make the attacker's job a bit harder and force them
# to develop specialized malware, rather than let them get away with
# just reading the standard EFI partition)
sbsign --key /root/secure-boot/MOK.key --cert /root/secure-boot/MOK.crt --output /boot/efi/EFI/Linux/linux.efi /boot/linux.efi
# Install the bootloader stages. shim and MokManager are both signed
# by Microsoft's UEFI key (courtesy of Fedora), so we can just copy
# them to the EFI partition directly.
#
# systemd-boot is not signed by MS, so we need to sign it with our MOK
# before placing it in the EFI partition.
cp /usr/share/shim-signed/shimx64.efi /boot/efi/EFI/Arch/shim.efi
cp /usr/share/shim-signed/mmx64.efi /boot/efi/EFI/Arch/mmx64.efi
sbsign --key=/root/secure-boot/MOK.key --cert /root/secure-boot/MOK.crt \
--output /boot/efi/EFI/Arch/grubx64.efi /usr/lib/systemd/boot/efi/systemd-bootx64.efi
# You need to run this script each time you have a new kernel, initrd,
# cmdline, or intel microcode. Additionally, you need to create a UEFI
# boot entry once, with:
#
# efibootmgr -c -d /dev/nvme0n1 -l '\EFI\Arch\shim.efi' -L "Linux Secure Boot"
#
# where /dev/nvme0n1 is the disk you're booting from (the one that has
# the EFI partition you've been writing all this stuff to).
#
# Finally, again as a one-time thing, you need to boot in secure mode
# and add your MOK certificate to the MOKlist, to tell shim that it's
# safe to boot your signed stuff. The first time you boot without your
# MOK installed, shim will fail to verify systemd-boot and will
# automatically launch MokManager to let you add your MOK cert.
# All done! The secure boot process will now be:
# - UEFI has a boot entry for shim.efi. shim.efi is signed by MS, so
# UEFI is happy and passes execution to shim.
# - shim looks for a 2nd-stage bootloader called 'grubx64.efi' in its
# own directory (it doesn't have to be grub, the filename s just -
# hardcoded in the signed binary). In our case, that's -
# systemd-boot. That binary is *not* signed by MS, but it *is* -
# signed by the MOK, so shim is happy and passes execution to -
# systemd-boot. Shim also installs a UEFI service (think of it as a
# helper RPC call) that allows the 2nd stage bootloader to check if
# other stuff has been signed by the MOK.
# - systemd-boot scans the EFI partition, finds \EFI\Linux\linux.efi
# which is a well-formed unified kernel binary. It's the only
# available boot entry, so it runs a signature check. The unified
# kernel is *not* signed by MS, but it *is* signed by the MOK
# (checked via shim's UEFI service), so systemd-boot is happy and
# passes execution to linux.efi.
# - The EFI stub at the start of linux.efi takes control, sets up the
# kernel, initrd and cmdline in a way that pleases linux, and -
# passes execution to the kernel code. No signature verifications
# at this stage, the entire blob has been verified by systemd-boot
# already, so the very fact that we're running this code is proof
# that it's safe to run.
# - The kernel boots, and passes control to the init ramdisk. The
# kernel doesn't do any signature verifications, but the initrd was
# in the EFI blob that systemd-boot verified, so at this point
# we're still running signed, trusted code. The initrd prompts for
# the decryption key for the rootfs, decrypts the root, and passes
# execution to the main systemd.
# - This is where verification ends, once we leave the ramdisk we are
# no longer running explicitly signed code. However, we are running
# from an encrypted root, which gives us some integrity/anti-tamper
# guarantees, and we have assurance that we decrypted root and
# chainloaded to it using non-malicious code. Which is pretty
# good. The next step in increasing security would be to use
# dm-verity or linux's IMA subsystem to explicitly verify that all
# code that runs from the root partition is signed, but that's hard
# to do outside of a distro engineered from the ground up to
# support that (e.g. ChromeOS).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment