Let us say you have OpenPGP card, or a javacard with the SmartPGP Applet (or hell, maybe even an implant), and you want to use it with your fancy LUKS encrypted battlestation (specifically, your root partition). It sounds like it should be possible, right? Well... yes, but it's not as straightforward as you might think. A quick web search will lead you to a lot of information, most of it not really getting anywhere concrete (much like this introduction).

TL;DR - Just give me a bunch of commands to copy!

Fine! A longer explanation/rant/recollection of my pain can be found further down this page...

Pre-requisites:

  • dracut
  • a smartcard of some kind with the private key
  • working LUKS/dracut setup with a password key
  • gpg
  • CCID compliant card reader

Adjust the [TEXT] tags to match your setup.
First we want to export our GPG public key:

gpg --armor --export-options export-minimal --export [RECIPIENT] > crypt-public-key.gpg

Then we generate a keyfile that we will add to our LUKS:

dd if=/dev/urandom of=keyfile bs=1 count=245

Add it to LUKS:

sudo cryptsetup luksAddKey [DEVICE] keyfile

Encrypt it with our public key:

gpg -e -r [RECIPIENT] --cipher-algo aes256 --armor --output keyfile.gpg keyfile

Test decrypting your keyfile:

gpg -d keyfile.gpg

Securely delete the cleartext keyfile:

shred -uvzn 5 keyfile

Copy your public key to this HARDCODED filename/path:

sudo cp crypt-public-key.gpg /etc/dracut.conf.d/crypt-public-key.gpg

Edit your dracut conf with the following:

add_dracutmodules+=" crypt rootfs-block crypt-gpg "
omit_dracutmodules+=" dracut-systemd systemd systemd-networkd systemd-initrd systemd-udevd "
install_items+=" [path/to/keyfile.gpg] /usr/bin/stty "
kernel_cmdline+=" rd.luks.uuid=[LUKS UUID] rd.luks.key=[path/to/keyfile.gpg]:/ root=/dev/mapper/[ROOT] rootfstype=ext4 rootflags=rw,noatime rd.luks.smartcard=1  "

Do note that that kernel_cmdline should match your kernel options coming from your bootloader (otherwise dracut won't know about the disks) and your LUKS, LVM and root/rootfs options.
You can use sudo dracut --print-cmdline to help you generate (part of) the appropriate options. Do note that at the time of writing you have to add stty manually with install_items because of a bug.

Finally, we just have to take care of a small bug (that exists at the time of writing), we have to edit the crypt-gpg module at /usr/lib/dracut/modules.d/91crypt-gpg/crypt-gpg-lib.sh. Find the following section:

useSmartcard="1"
echo "allow-loopback-pinentry" >> "$gpghome/gpg-agent.conf"
GNUPGHOME="$gpghome" gpg-agent --quiet --daemon
GNUPGHOME="$gpghome" gpg --quiet --no-tty --import < /root/crypt-public-key.gpg
local smartcardSerialNumber
smartcardSerialNumber="$(GNUPGHOME=$gpghome gpg --no-tty --card-status \
    | sed -n -r -e 's|Serial number.*: ([0-9]*)|\1|p' | tr -d '\n')"
if [ -n "${smartcardSerialNumber}" ]; then
    inputPrompt="PIN (OpenPGP card ${smartcardSerialNumber})"
fi
GNUPGHOME="$gpghome" gpg-connect-agent 1> /dev/null learn /bye
opts="$opts --pinentry-mode=loopback"
cmd="GNUPGHOME=$gpghome gpg --card-status --no-tty > /dev/null 2>&1; gpg $opts --decrypt $mntp/$keypath"

Move GNUPGHOME="$gpghome" gpg-connect-agent 1> /dev/null learn /bye to just before local smartcardSerialNumber, like this:

useSmartcard="1"
echo "allow-loopback-pinentry" >> "$gpghome/gpg-agent.conf"
GNUPGHOME="$gpghome" gpg-agent --quiet --daemon
GNUPGHOME="$gpghome" gpg --quiet --no-tty --import < /root/crypt-public-key.gpg
GNUPGHOME="$gpghome" gpg-connect-agent 1> /dev/null learn /bye
local smartcardSerialNumber
smartcardSerialNumber="$(GNUPGHOME=$gpghome gpg --no-tty --card-status \
    | sed -n -r -e 's|Serial number.*: ([0-9]*)|\1|p' | tr -d '\n')"
if [ -n "${smartcardSerialNumber}" ]; then
    inputPrompt="PIN (OpenPGP card ${smartcardSerialNumber})"
fi
opts="$opts --pinentry-mode=loopback"
cmd="GNUPGHOME=$gpghome gpg --card-status --no-tty > /dev/null 2>&1; gpg $opts --decrypt $mntp/$keypath"

Technically it will work without this small patch but the prompt won't be as clear (as it will use the wrong text). That's it. That's the setup.
After rebuilding dracut and rebooting, you should get the PIN (OpenPGP card) prompt. If there is no card or if you input the wrong pin 3 times, it will fallback to using a password.




RANT (or: what I learned)

Here's a small quick breakdown of what I have learned (after waaay too much time spent getting this working):

  • card reader - GET A CCID COMPLIANT ONE trust me (more on this later, here's a list)
  • use dracut for your initramfs (you could stick with mkinitcpio but then you would basically need to roll your own hook and FUCK THAT) with crypt-gpg
  • when using dracut, disable all systemd modules

About card readers...

Simply put - if your card reader requires you to use the scdaemon switch disable-ccid, then nothing you do will work. By default gpg-agent uses the CCID driver, if you disable it gpg is going to fallback to pcsc, which requires pcscd to be running. If you are using dracut then you are just fucked, because pcscd requires systemd, which intercepts our LUKS partition instead of letting the correct module take care of it.
I have spent a lot of time looking into how to bypass it and, as the time of writing this, the only way is to omit all systemd modules. The only alternative is to start writing your own pcscd/gpg hook for mkinitcpio (or similar), but I say FUCK THAT and simply get a properly compliant CCID reader so you can just use gpg freely.

Initramfs, dracut and so on

I touched on this already, but you want dracut, say what you want about Red Hat but if they use something it is usually not shit.
Using dracut the setup for this is deceptively simple. I suggest following the previously linked Arch Linux wiki article to get it up and running at first. Unfortunately the systemd modules catch our LUKS before it gets to crypt-gpg and forces password use.

dracut has all the public key stuff hardcoded, so it has to be in /etc/dracut.conf.d/crypt-public-key.gpg.

How about multiple cards?

I have yet to test this as I am waiting on more smartcards, but gpg supports multi-key encryption, so it should be as easy as passing multiple -r [RECIPIENT] when encrypting your keyfile. Then you can just import multiple public keys (would require a simple patch to crypt-gpg, which I just might do myself eventually).