Linux Passphrase Transfer

Author:Hansjörg Lipp
Date:2018-04-16
URL:original document

The original problem

Dealing with an openSUSE installation with an encrypted root file system (including /boot) can be inconvenient as the user has to enter the passphrase twice: The first time when the boot loader grub needs to read its files and the kernel including the initial ramdisk, the second time when systemd mounts the root partition. Having to enter a lengthy passphrase twice (with two different keyboard layouts) every time the device is booted turned out to be not optimal.

The first idea

My first idea was allowing grub to pass the passphrase to systemd via the Linux kernel. While the proof of concept code was quite interesting to implement, it turned out not to be the right approach. Even if there might be use cases for some mechanism allowing the boot loader to pass information secretly to systemd, this is not one of them. I still leave the original approach below as I find this topic rather interesting.

The real solution

The real solution is using key files and make the system include the key files into the initial ramdisk. At first glance, it appears to by quite unsafe to store a key file on a file system where some malware could gain access to it. Having a closer look at this, it turns out, that you don't lose security:

As long as the computer is not running, the key file, including the copy inside the initial ramdisk are securely located in the encrypted root partition. So, even if your device gets stolen, there is no problem (as long as your key file does not get out of the system by careless backups).

As soon as the computer is booted, an attacker who can gain root rights cannot only read all your files on the now mounted file system anyway, he might also be able to extract the cryptographical data needed to decrypt the device from memory (where it has to be accessible as the kernel needs to decrypt your data). Just make sure that neither the key files nor the initial ramdisks are readable by an unprivileged user.

Take away message: Don't use luks encryption as a measure to protect your recent construction plans for the perpetual motion machine you just developed from a malware attack by your competitor. As soon as there is an attack vector allowing him to gain root rights (e.g. a bug in your mailer in combination with a bug in the kernel or some software running as root), your data is not safe! You need to take other measures to protect yourself in this scenario. Luks encryption is still useful, as it protects your data from someone with physical access to your drives (a thief, the "cleaner" paid by your competitor, a funny colleague, ...).

How to do this on openSUSE Leap 42.3

Creating and using key files

Using key files is straight forward and well documented. I show an example how you might do this (it's based on an example Oleksandr Natalenko made me aware of); refer to the documentation for details:

Creating the key files

mkdir /keydir
chmod go-rwx /boot /keydir #only root should be able to read those, even if included in initrd

dd bs=512 count=4 if=/dev/urandom of=/keydir/root.bin
dd bs=512 count=4 if=/dev/urandom of=/keydir/datapart.bin
#...

chmod go-rw /keydir/*

Adding the key files

cryptsetup luksAddKey  /dev/ROOTPART /keydir/root.bin
cryptsetup luksAddKey  /dev/DATAPART /keydir/datapart.bin
#...

#You might want to add the key to a specific slot instead
cryptsetup luksAddKey  /dev/ROOTPART /keydir/root.bin      -S ROOT_SLOT
cryptsetup luksAddKey  /dev/DATAPART /keydir/datapart.bin  -S DATA_SLOT
#...

#Check if everything looks okay
cryptsetup luksDump /dev/ROOTPART
#...

Telling the system to use the key files

Change your /etc/crypttab which should contain lines like

ROOTNAME   /dev/ROOTPART   none   none
DATANAME   /dev/DATAPART   none   none

to something like

ROOTNAME   /dev/ROOTPART   /keydir/root.bin       none
DATANAME   /dev/DATAPART   /keydir/datapart.bin   none

Getting the key files into the initial ramdisk

There were two reasons I didn't use this approach in the first place. Firstly, I thought using key files were not as secure, and secondly, I somehow could not think of a way systemd could access a key file (which is located on the encrypted root partition). The (thanks to Oleksandr Natalenko's hint) obvious answer is: systemd can get it from the initial ramdisk which is decrypted by the boot loader.

The only remaining question is: How can I get my distribution to automatically include the key files into the initial ramdisk? Doing it manually would be a pain as this would have to be repeated for every kernel update and the like... Ideally, the tools creating the initial ramdisk would detect from crypttab that a key file is required for the root device and include it automatically into the initial ramdisk. I unfortunately couldn't find out if that is possible.

Nevertheless, as openSUSE uses dracut, it is easy to configure the system to include files into the initial ramdisk. I simply had to create a file /etc/dracut.conf.d/50-crypt.conf like this:

install_items+="/keydir/root.bin"

After executing openSUSE's mkinird wrapper all initial ram disks did contain above key file. When doing this, you can check this using lsinitrd.

Please note, that only the key needed for decrypting the root file system has to be included into the initial ramdisk.

This seems to work fine here so far. Feel free to contact me if you think this can still be improved or if you have any remarks! And thanks again for the helpful feedback!


This is the original document:

The Problem

The boot loader grub allows booting the Linux kernel from an encrypted partition. For doing this the user has to enter a passphrase. After some initialization, the kernel executes the init process (i.e. systemd on a usual desktop system) which mounts the file systems. When mounting the encrypted partition, the user has to enter the passphrase a second time. Having to provide a long and secure passphrase twice can be rather inconvenient.

Tasks to solve

In order to avoid this, a mechanism has to be introduced which can transfer secret data

Proof of concept

Therefore, proof of concept code was developed as follows.

Data transfer

Transferring data from grub to the Linux kernel

In order to avoid introducing a new and incompatible interface (and trying to reuse existing code), the current way to pass a publicly visible command line from the boot loader to Linux was extended to allow passing also private data:

Until now, the boot loader has to store a null-terminated string in a certain memory region of fixed size. This string is then copied by the kernel to a variable for further use.

The main idea is to append the secret data directly after the NUL character of the regular command line and some signature bytes (currently "hdn ") as another null-terminated string in the form "pwd=passphrase1 pwd=passphrase2". The complete sequence therefore is:

regular command line NUL "hdn" SPACE secret command line NUL

This key-value-format is flexible enough to allow other secret data to be passed to the kernel.

Transferring data from the Linux kernel to systemd

The regular command line is accessible by user space via /proc/cmdline.

Similarly, the secret command line can be read via /proc/cmdline_hidden. For security reasons this can only be done once and only by root. It might also be a good idea to restrict access to the init process (process id 1).

Implementation

The implementation is based on openSUSE Leap 42.3 with current default kernels from http://download.opensuse.org/repositories/Kernel:/stable/standard.

grub

The code based on grub 2.02 collects all passphrases used to unlock luks devices in a list.

The linux command which is used to prepare booting the Linux kernel is extended to pass the collected pass phrases as hidden command line as described above if grub.pass_password is specified in the regular command line (mostly for preventing developers from locking themselves out).

Another small change allows the user to try again if a wrong passphrase was given.

There might to be added more code to zero out password data after use.

Linux

The code is based on the kernels provided in the openSUSE stable kernel repository but also applies cleanly to Linux 4.16.2.

Another command line variable hidden_command_line is introduced and initialized when the regular command line is retrieved. If no hidden command line is found (indicated by the signature "hdn "), the pointer is set to NULL. The hidden command line passed by the boot loader is zeroed out after storing the data.

Additionally, the proc fs entry cmdline_hidden is created. After the entry has been read, the passphrase is zeroed out for security reasons. Only root may access this entry; one could also require pid==1.

systemd

The code is based on systemd v228. An untested port to current MASTER is also provided.

The systemd changes are surprisingly tiny because there is already a key ring in use for encrypted disks. We only have to read /proc/cmdline_hidden, collect the pwd=passphraseN entries into an array of strings and store the result in the key ring used for unlocking encrypted devices.

To prevent developers from locking themselves out, this happens only if grub.pass_password is specified in the regular command line.

Memory containing passphrase data is zeroed out after use.

Code

Patches

The changes are mainly provided as patches to the openSUSE Leap 42.3 sources (using current stable kernels from the kernel repository), additionally there are untested patches to the current versions of grub and systemd.

Leap 42.3 Current
grub diff spec diff
Linux diff patches.addon
systemd diff spec diff

Before installing these, make sure to have a working backup and a way to access your computer using another boot device or the like! In case of an error you might lock yourself out of your computer otherwise!

Find out how you can install several kernels in parallel, find out how to boot from a rescue disk and restore your backup, and learn how to use the rescue console of your boot loader, before you try installing any of these!

Applying the patches

If you want to compile these from scratch follow the instructions. Just remember applying the patches by executing

patch --dry-run -p1 < /path/to/patch.diff

after extracting the source archive and changing into the extracted directory in order to check if the patch applies cleanly. After doing so, you can apply the patch using

patch -p1 < /path/to/patch.diff

Building rpm packages

It might be more convenient to build rpm packages and install these using your package manager:

When building the grub package fails for you because files are erroneously copied to /usr/lib64/grub2/i386-pc instead of /usr/lib/grub2/i386-pc, it might help to add --libdir=%{_libdir} to every configure command inside grub2.spec.

Note that it might be convenient not to use system wide rpm directories such as /usr/src/packages but an rpm directory in your home directory. You can do this by adding the line

%_topdir /path/to/your/rpm

to the file .rpmmacros in your home directory.

Building kernel packages works a bit differently:

Before installing an experimental kernel package using zypper on openSUSE, make sure to configure multiversion.kernels in /etc/zypp/zypp.conf properly in order to keep a working kernel.

Trouble shooting

Starting points for finding out why this does not work on your system: