The Debian cryptsetup package supports a setup for decrypting LUKS partitions using an OpenPGP smartcard. Additionally, the official Nitrokey website offers helpful instructions to simplify the configuration of this same package.

In this post, I aim to provide further explanations about this configuration and how it works, making it easier to replicate and adapt on non-Debian-based systems as well.

Assuming you already have a disk encrypted with LUKS, a typical configuration for your /etc/crypttab file might resemble the following:

sda5_crypt UUID=a766ec9b-1eba-423b-9181-d7ad033d8063 none luks

Let’s recap the meaning of these parameters:

The cryptsetup option that is needed to perform the unlocking using an OpenPGP smartcard is the keyscript option. A keyscript is a script whose output is provided to cryptsetup as the decryption key. It should contain only the key itself, without any trailing newline.

When a keyscript is used, the third option (which was set to none in the previous example) can be set to a path of a file that is passed as argument to the keyscript.

So, to enable the LUKS-GPG setup we can modify /etc/crypttab as follow:

sda5_crypt UUID=a766ec9b-1eba-423b-9181-d7ad033d8063 /etc/cryptsetup-initramfs/cryptkey.gpg luks,keyscript=decrypt_gnupg-sc

The file /etc/cryptsetup-initramfs/cryptkey.gpg contains a disk encryption key that has been encrypted using our GPG public key (the one associated with the smartcard), while decrypt_gnupg-sc is the keyscript provided by the Debian cryptsetup package, located in /lib/cryptsetup/scripts/decrypt_gnupg-sc. To be able to decrypt the partition on boot the cryptkey.gpg file must be added to the initramfs. This inclusion is automatically handled by the update-initramfs command, which we will execute at the end of the process.

The cryptkey file can be generated directly reading from /dev/urandom:

dd if=/dev/urandom bs=1 count=256 > cryptkey

The file can then be encrypted using your public GPG key and copied to /etc/cryptsetup-initramfs/cryptkey.gpg:

gpg --recipient yourname@example.com --output cryptkey.gpg --encrypt cryptkey

The /etc/cryptsetup-initramfs directory must also include your public key. This is necessary for GPG to set up the smartcard stubs during boot. A GPG stub acts as a placeholder for the private key, which resides on the smartcard.

To copy the public key to the appropriate location, run the following command:

gpg --export yourname@example.com > /etc/cryptsetup-initramfs/pubring.gpg

Now we can encrypt the disk using the new key. If you are working with a new disk, you could directly run luksFormat with the new key. However, I recommend using luksAddKey on a LUKS setup that is already configured with a standard passphrase. This approach minimizes the risk of being locked out of your files, as you will still be able to unlock the disk using the original passphrase.

If this sounds weird consider that LUKS can work with multiple keys, stored in different “keyslots”. Each keyslot contains a copy of a master key encrypted with a different key. The master key is the one actually used for filesystem encryption. LUKS may attempt to decrypt the disk with the prompt you provided many times, trying all the keyslots until it finds a keyslot that works for your input (more info).

To add the keyfile to a new keyslot, you can use the following command, which will prompt you for the previous passphrase:

cryptsetup luksAddKey --new-keyfile=cryptkey /dev/sda5

We can then check the keyslots using the command cryptsetup luksDump /dev/sda5. The output will show 2 LUKS keyslots:

Keyslots:
  0: luks2
     ...
  1: luks2
     ...

It is now a good idea to remove the original decrypted cryptkey file. Consider using shred to delete it in a safer way.

At the end we need to update the initramfs to apply the new configuration:

update-initramfs -u

This command will execute the script located at /usr/share/initramfs-tools/hooks/cryptgnupg-sc, which adds to the initramfs the files cryptkey.gpg and pubring.gpg, along with all the necessary executables to run GPG with the smartcard. It also selects the appropriate pinentry program (the application that prompts for your smartcard PIN), choosing between pinentry-curses or pinentry-tty as the second option.

Grub splash option (which provides a visually appealing loading screen) may cause issues with keyboard input for the pinentry. Therefore, it is advisable to change the splash option to nosplash in the /etc/default/grub file:

GRUB_CMDLINE_LINUX_DEFAULT="nosplash"

Then run the update-grub command to apply the changes.

Reboot and unlock the disk with your smartcard!

Minimal working example

A minimal working example for education purposes can be achieved with a much simpler keyscript:

#!/bin/sh
gpg --quiet --import /etc/public-key.pgp >&2
# Needed to "wake up" the card
gpg --quiet --card-status >&2
gpg --quiet --decrypt /etc/cryptkey.gpg

This script doesn’t handle the possibility to retry inserting the PIN after a failed attempt.

The following initramfs hook has to be added to /etc/initramfs-tools/hooks/gpg before rebuilding the initramfs:

#!/bin/sh

. /usr/share/initramfs-tools/hook-functions

cp /etc/gpg-luks/cryptkey.gpg "$DESTDIR/etc/"
cp /etc/gpg-luks/public-key.pgp "$DESTDIR/etc/"

copy_exec "/usr/bin/gpg"
copy_exec "/usr/bin/gpg-agent"
copy_exec "/usr/bin/pinentry-tty" "/bin/pinentry"
copy_exec "/usr/bin/gpg-connect-agent"
copy_exec "/usr/lib/gnupg/scdaemon"

exit 0