I have completed my work to add support for the GELI disk encryption system to the FreeBSD EFI boot loader. This work started off intending to be a “simple” patch, but it became a much larger undertaking that ended up refactoring a significant portion of the EFI boot loader.
Regardless, the changeset is now usable and ready for testing. It can be accessed on my GitHub. I will be merging this periodically with the FreeBSD master in order to keep it up to date.
I am not recommending this for inclusion in the 11 release; it’s too big a change to incorporate this late in the game.
This work breaks down into roughly four different components: EFI refactoring, boot crypto framework, GELI support, and kernel key injection. I’ll cover each of these in turn.
I have already written extensively about my EFI refactoring here. The reason for undertaking this effort, however, was driven by GELI support. Early in my work on this, I had implemented a non-EFI “providers” framework in boot1 in order to support the notion of disk partitions that may contain sub-partitions. This was deeply unsatisfying to me for several reasons:
- It implemented a lot of the same functionality that exists in the EFI framework.
- It involved implementing a GPT partition driver to deal with partition tables inside GELI partitions (GPT detection and support is guaranteed by the EFI spec).
- The interface between the EFI framework and the custom “providers” framework was awkward.
- The driver was completely boot1-specific, and exporting it to something like GRUB probably involved a total rewrite.
- Implementing it within loader was going to involve a lot of code duplication.
- There was no obvious was to pass keys between boot1, loader, and the kernel.
Doing the EFIzation work eliminated these problems, and in my opinion, cleaned things up in boot1 as well. The results were pleasing:
- The GELI driver can be extracted from the FreeBSD codebase without too much trouble.
- While I was unable to go all the way to the EFI driver model, the only blocker is the bcache code, and once that is resolved, we can have hotplug support in the boot loader!
- The boot1 and loader codebases are now sharing all the backend drivers, and boot1 has been reduced to one very small source file.
As I previously mentioned, my only reservation about this is increased dependence on (historically flaky) vendor-specific BIOS code. However, with things like CoreBoot on the rise, I’m less concerned about this.
There are a couple of open questions and future work items coming out of this refactoring:
- Might it be a good idea to move the backend drivers out of boot and loader completely, and have them be wholly-separate EFI drivers that get installed by boot/loader?
- Provide EFI drivers for GELI and FreeBSD filesystems to projects like CoreBoot, TianoCore, and GRUB.
- Refactor the bcache code to support dynamic device detection and complete the transition to the EFI driver model.
- Play with possibilities that arise from hot-plugging, like pluggable USB dongles containing access credentials.
The boot crypto refactoring was a small effort to pull the crypto code out of the BIOS GELI code and put it in a common place for all boot utilities. My hope is that boot_crypto becomes the go-to place for boot-time crypto code.
However, I’m a bit displeased with the state of crypto code in general. I deemed it too ambitious to take that on in addition to everything else, but it seems that the crypto framework could profit from some work. The problems I see are as follows:
- The codebase seems rather disorganized. There are crypto and opencrypto in the kernel, and there doesn’t seem to be a common interface for all ciphers.
- There are insecure ciphers and algorithms (RC4, SHA1, MD5, DES) as well as missing modern ciphers (ChaCha20-Poly1305, stronger RipeMD variants, etc).
- The procedure for linking against the crypto codebase is harder than it could be.
The core crypto code should be usable in userland, kernel, and boot environments, with the only difference being the ABI for which it is compiled (native for user and kernel, MSABI for EFI, 32-bit for x86 BIOS, etc). It should be possible to have a single codebase that gets turned into static libraries for each ABI and a shared library for userland.
It might be worth an experiment to replace the current crypto code with something like NaCL (libsodium) and use the scheme I describe above.
The GELI EFI driver is a straightforward EFI bus driver that detects the presence of a GELI volume on a device handle and creates a new device handle bearing an EFI_BLOCK_IO_PROTOCOL interface that provides access to the encrypted data. This involves getting ahold of a password or a key, which I manage through the KMS driver (actually, the GELI driver currently contains the code to ask for a password directly, though this could and probably should be moved into the KMS driver).
: EFI considers any driver that creates new handles to be a “bus driver”, even if it is something like a partition driver.
Kernel Key Injection and Boot KMS
Passing keys between boot1, loader, and the kernel was one of the key challenges of this work. I also wanted to do this in a way that could be extended to support hardware security modules with a minimum of effort. The EFI_KMS_PROTOCOL interface provided a means to accomplish this.
I provided another EFI driver that implements a software-based EFI_KMS_PROTOCOL interface. This allows boot services to register keys, look them up later, and ultimately have them injected into the kernel. This code is a fairly simple key table manager internally, and uses the file metadata functionality in loader to communicate keys to the kernel.
The current implementation leaves the task of asking for passwords up to the individual drivers, but I am seriously considering bringing that into the KMS driver as well, under the auspices of the CreateKey function.
On the kernel side, I added an interface to the crypto framework that provides access to the key buffer provided to the kernel by the KMS driver. This allows keys to be passed from boot to kernel in a safer manner than the environment variable currently used by the BIOS GELI implementation. It also provides a generic interface going forward for accomplishing this task.
This also puts things in such a state that a hardware-based key management system could be integrated. Because boot1 and loader use EFI_KMS_PROTOCOL, which was specifically designed for key management systems, it is easy to add support for a device that supports this. On the kernel side, we would simply expect the hardware KMS to be initialized with the keys we need.
Using and Testing EFI GELI Support
The changeset should be usable for most people. I have tested the core functionality and have successfully loaded and booted kernels from ZFS volumes inside a GELI partition. I am not actively using this, however, as I want to participate in the tests of the drm2 4.6 update, and I don’t want to assume the risks involved in merging two experimental branches.
If you want to use or test the changeset, I strongly recommend the following procedure:
- Be aware of the risks: this is full-disk encryption, so a problem may result in your files becoming inaccessible by any means.
- Read and understand the basics of EFI and the FreeBSD boot architecture.
- Install an EFI Shell program on your EFI System Partition, know how to run it from your BIOS menu.
- Install boot1 to an alternate location (like /efi/boot/bootx64.tst).
- Install loader to /boot/loader.tst on your main partition (I have left the LOADER_PATH variable set to /boot/loader.tst to facilitate this kind of testing)
- Start the EFI shell and run /efi/boot/bootx64.tst
- Try creating an empty GELI partition, and verify that boot1 and loader detect it
- Try creating a filesystem in the GELI partition, copy your /boot/ directory from your main partition, see if boot1 and loader can read files from it
- Try loading and booting a kernel from your GELI partition
- Once you have succeeded at these, back up all your files, try converting your main disk into a GELI partition, and see if you can boot from it.
: The EFIzed loader (with GELI support) should work fine even when loaded by the old boot1 (or by a tool such as GRUB)
Looking Forward: Tamper-Resilience
This changeset adds the first of three major capabilities that are needed to realize a longer-term tamper-resilience that I would very much like to see be a part of FreeBSD. The steps toward this are as follows:
- Support full-disk encryption from boot, with the ESP being the only non-encrypted partition on the system
- Support EFI secure boot of boot1, combined with signature checking for loader, the kernel, and device drivers, with the ability to designate a machine-specific signing key to be used to generate the signatures
- Support secure suspend-to-disk
These three features when combined and implemented on a ZFS-based system create a powerful tamper-resilience scheme. Because secure boot is used with a machine-specific platform key, this key can be stored on the encrypted disk. This ensures that only someone with the ability to decrypt the disk is able to create a boot1 program that will run on the machine. This prevents anyone from tampering with boot1 and hijacking the boot process. Moreover, with suspend-to-disk, the machine is only vulnerable to data exfiltration when it is in active use. When suspended or powered-off, everything is encrypted and protected by the secure boot scheme.
Obviously, this is not perfect; anyone able to overwrite the firmware or mess with the hardware can hijack the secure boot process and tamper with boot1. This is why I call it tamper-resilience as opposed to tamper-proofing. However, this scheme guarantees that the OS does not open any vulnerabilities to an attacker who wants to tamper with the system.
: With projects like CoreBoot, it may become possible to have the firmware itself load programs from an encrypted volume