How to use a GPG smartcard with LUKS
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).