Yubikey based Full Disk Encryption (FDE) on NixOS

From NixOS Wiki
Revision as of 06:57, 2 July 2019 by imported>Ryanorendorff (Tested the instructions.)

This page is a minimalistic guide for setting up LUKS-based full disk encryption with YubiKey pre-boot authentication on a UEFI system. The YubiKey Pre Boot Authentication (PBA) in NixOS currently features two-factor authentication using a (secret) user passphrase and a YubiKey in challenge-response mode. The described method also works without a user password, although this is not preferred.

This guide was tested to work on NixOS 19.03 as of July 2019.

Requirements

  • A NixOS live system booted in UEFI mode on the target machine.
  • A YubiKey Standard plugged into the target machine with a free configuration slot (that will be overwritten).


Partitioning

Create a GPT partition table and two partitions on the target disk.

  • Partition 1: This will be the EFI system partition: 100MB-300MB
  • Partition 2: This will be the Luks-encrypted partition, aka the "luks device": Rest of your disk

In the following we will use variables for identification, so set them to match your setup, e.g. like this:

EFI_PART=/dev/sda1
LUKS_PART=/dev/sda2


Setup the luks device

Step 1: Create the necessary filesystem on the efi system partition, which will store the current salt for the PBA, and mount it.

EFI_MNT=/root/boot
mkdir "$EFI_MNT"
mkfs.vfat -F 32 -n uefi "$EFI_PART"
mount "$EFI_PART" "$EFI_MNT"


Step 2: Decide where on the efi system partition to store the salt and prepare the directory layout accordingly.

STORAGE=/crypt-storage/default
mkdir -p "$(dirname $EFI_MNT$STORAGE)"


Step 3: Install the packages required by the next steps to the live system and make two bash helper functions available.

Packages:

  • A C compiler, e.g. gcc
  • The YubiKey Personalization command line tool
  • OpenSSL
nix-env -i gcc-wrapper
nix-env -i yubikey-personalization
nix-env -i openssl

Helper functions:

  • Convert a raw binary string to a hexadecimal string
  • Convert a hexadecimal string to a raw binary string

Note that you can copy and paste these functions into the bash shell directly to define them.

rbtohex() {
    ( od -An -vtx1 | tr -d ' \n' )
}

hextorb() {
    ( tr '[:lower:]' '[:upper:]' | sed -e 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/gI'| xargs printf )
}


Step 4: Compile a small C program (shipped with NixOS) that allows for direct access to OpenSSL's PBKDF2 implementation.

cc -O3 -I$(find / | grep "openssl/evp\.h" | head -1 | sed -e 's|/openssl/evp\.h$||g' | tr -d '\n') \ 
  -L$(find / | grep "lib/libcrypto" | head -1 | sed -e 's|/libcrypto\..*$||g' | tr -d '\n') \ 
  $(find / | grep "pbkdf2-sha512\.c" | head -1 | tr -d '\n') -o ./pbkdf2-sha512 -lcrypto


Step 5: Gather the initial salt for the PBA (set its length to what you find time-feasible on your machine).

SALT_LENGTH=16
salt="$(dd if=/dev/random bs=1 count=$SALT_LENGTH 2>/dev/null | rbtohex)"


Step 6: Gather the random secret key for the YubiKey (Important: This key should not be stored anywhere other than the YubiKey, as that poses a security risk).

k_yubi="$(dd if=/dev/random bs=1 count=20 2>/dev/null | rbtohex)"


Step 7: Get the user passphrase used as the second factor in the PBA.

read -s k_user


Step 7.5: Make very sure, that $k_user contains the correct user passphrase, or you will not be able to access your system after shutting down the live system.


Step 8: Calculate the initial challenge to the YubiKey.

challenge="$(echo -n $salt | openssl dgst -binary -sha512 | rbtohex)"


Step 9: Calculate the response the YubiKey should give to that challenge (Only possible because right now we still know the secret key for the YubiKey).

response="$(echo -n $challenge | hextorb | openssl dgst -binary -sha1 -mac HMAC -macopt hexkey:$k_yubi | rbtohex)"


Step 10: Derive the Luks slot key from the two factors.

Set the length of the Luks slot key and the ciphter appropriately.

As an example, we will use AES-256, so we set the Luks device slot key length to 512 bit.

Set the iteration count used for PBKDF2 to a high value still time-feasible for your machine.

KEY_LENGTH=512
ITERATIONS=1000000
k_luks="$(echo -n $k_user | ./pbkdf2-sha512 $(($KEY_LENGTH / 8)) $ITERATIONS $response | rbtohex)"

If you choose to authenticate without a user passphrase (not recommended), use this instead of the line above

k_luks="$(echo | ./pbkdf2-sha512 $(($KEY_LENGTH / 8)) $ITERATIONS $response | rbtohex)"


Step 11: Create the LUKS device.

  • Set the cipher used by LUKS appropriately
  • Set the has used by LUKS appropriately
CIPHER=aes-xts-plain64
HASH=sha512
echo -n "$k_luks" | hextorb | cryptsetup luksFormat --cipher="$CIPHER" \ 
  --key-size="$KEY_LENGTH" --hash="$HASH" --key-file=- "$LUKS_PART"


Step 12: Store the salt and iteration count to the efi systems partition.

echo -ne "$salt\n$ITERATIONS" > $EFI_MNT$STORAGE


Step 13: Store the secret key for the YubiKey on the YubiKey and configure it accordingly.

SLOT=2
ykpersonalize -"$SLOT" -ochal-resp -ochal-hmac -a"$k_yubi"

To test if the key is programmed correctly, you can challenge the yubikey and check that the response is the expected response previously generated.

ykchalresp -2 "$(echo -n $challenge | hextorb)"


LVM setup

The following is one of many methods for setting up the LVM partition. Another popular guide can be found here: https://qfpl.io/posts/installing-nixos/

Step 1: Setup the LUKS device as a physical volume.

The LUKS device first needs to be unlocked.

LUKSROOT=nixos-enc
echo -n "$k_luks" | hextorb | cryptsetup luksOpen $LUKS_PART $LUKSROOT --key-file=-

The physical volumje map can then be created.

pvcreate "/dev/mapper/$LUKSROOT"


Step 2: Setup a volume group on the LUKS device.

Set the name for the volume group appropriately

VGNAME=partitions
vgcreate "$VGNAME" "/dev/mapper/$LUKSROOT"


Step 3: Setup two logical volumes on the Luks device.

  • Volume 1: This will be the swap partition: choose appropriate size, 2GB for example
  • Volume 2: This will be the main btrfs volume, of which all filesystem partitions will be subvolumes: Rest of the free space
lvcreate -L 2G -n swap "$VGNAME"
FSROOT=fsroot
lvcreate -l 100%FREE -n "$FSROOT" "$VGNAME"

vgchange -ay


Step 4: Create the swap filesystem.

mkswap -L swap /dev/partitions/swap


Btrfs setup

These steps can mostly be followed the same for other filesystem types except calls to the brtfs command can be ignored.

Step 1: Create the main btrfs volume's filesystem.

mkfs.btrfs -L "$FSROOT" "/dev/partitions/$FSROOT"

Should the above fail, you might have encountered a bug that can be solved with doing the following, then attempting the above again:

mkdir /mnt-root
touch /mnt-root/nix-store.squashfs


Step 2: Mount the main btrfs volume.

mount "/dev/partitions/$FSROOT" /mnt


Step 3: Create the subvolumes, for example "root" and "home".

cd /mnt
btrfs subvolume create root
btrfs subvolume create home


Step 4: Create mountpoints on the root subvolume and finalise things for NixOS installation.

umount /mnt
mount -o subvol=root "/dev/partitions/$FSROOT" /mnt

mkdir /mnt/home
mount -o subvol=home "/dev/partitions/$FSROOT" /mnt/home

mkdir /mnt/boot
mount "EFI_PART" /mnt/boot

swapon /dev/partitions/swap


NixOS installation

Configure and install NixOS as you normally would, with these changes:

Remove all detected filesystems from hardware-configuration.nix.

Add the following to your configuration.nix (Replace anything that looks like a Bash variable with the value that it currently holds for in your shell and modify as needed)

# Minimal list of modules to use the efi system partition and the YubiKey
boot.initrd.kernelModules = [ "vfat" "nls_cp437" "nls_iso8859-1" "usbhid" ];

# Crypto setup, set modules accordingly
boot.initrd.luks.cryptoModules = [ "aes" "xts" "sha512" ];

# Enable support for the YubiKey PBA
boot.initrd.luks.yubikeySupport = true;

# You may want to enable the network interface. During 

# Configuration to use your Luks device
boot.initrd.luks.devices = [ {
  name = "$LUKSROOT";
  device = "$LUKS_PART";
  preLVM = true; # You may want to set this to false if you need to start a network service first
  yubikey = {
    twoFactor = true; # Set to false if you did not set up a user password.
    storage = {
      device = "$EFI_PART";
    };
  };
} ];

Headless setup note: If you have set up your system to not use a user password and attempt to boot the system, you may find the system stalls with the following message:

"Gathering entropy for new salt (please enter random keys to generate entropy if this blocks for long)..."

If you see this message and no more dots appear after a while, you have run into a situation where the random number generator does not have enough entropy stored up. You can partially mitigate this by starting a network interface (assuming the device is on a network), which should fill the entropy pool and allow the computer to boot headless. Below is an example configuration that has been tested to work in a headless configuration.

boot = {
    # Used to make this device dhcp enabled during boot.
    kernelParams = [
      "ip=:::::eth0:dhcp" # Change to the appropriate IP kernel command.
    ];

    initrd = {

      network.enable = true;

      # This is the driver for a particular ethernet card. See `boot.network.enable` for more details.
      availableKernelModules = ["alx"]; 

      kernelModules = ["vfat" "nls_cp437" "nls_iso8859-1" "usbhid" "alx"];

      luks = {
        cryptoModules = [ "aes" "xts" "sha512" ];
        yubikeySupport = true;

        devices = [ {
          name = "$LUKSROOT";
          device = "$LUKS_PART";
          preLVM = false;
          yubikey = {
            twoFactor = false;
            storage = {
              device = "$EFI_PART";
            };
          };
        } ];

      };
    };
  };

Finally, clean up and you should be ready to reboot into your new system.

This is a copy of the page from the original wiki. The original page can be found here: https://web.archive.org/web/20160911070220/https://nixos.org/wiki/Luks-based_FDE_with_Yubikey_PBA_and_btrfs_on_UEFI_NixOS