Created
October 7, 2018 21:16
-
-
Save danderson/6a1c8aed390a0a20092998610af5c0e7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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