A few years ago I attempted to get the cryptsetup project to include a patch I made that integrated support for smartcards with LUKS. I encrypted a luks key with the smartcard and instead of storing it on the hard drive where it may not be available at boot. The encrypted key is stored in a token in the LUKS header. My solution was heavily script based and the project rejected it. At the time systemd was starting to integrate support for smartcard unlock of LUKS2 using a similar technique.
It is now many years later and the systemd approach still does not work for booting encrypted root partitions on Debian--which was one of the concerns I had at the time. Debian based distros do not use systemd in the boot process which makes this approach difficult. I have also tried switching from the default initramfs-tools to dracut and failed to make that work after days of struggle. So I came back to my hack of using tokens to store and fetch the encrypted key data.
One of the key components is the ability to properly add the key material to the Luks2 header. I created a script for this. since systemd is using the term 'cryptentroll' I follow suit and name the script 'gpg-cryptenroll'. This script allows the user to specify the various components and settings but includes 'auto' for many of them to fetch the information from the current system. Having users dig up information is a waste of time. Allowing them to specify when necessary is ... necessary.
After the luks2 header is setup we need to modify the boot script. Debian uses /usr/lib/cryptsetup/scripts/decrypt_gnupg-sc. We will modify this script in these ways
allow fallback to passphrase if smartcard is not available
allow process to fetch key from luks2 header rather than from disk
The crypttab file includes the file used for the encrypted key. Since we don't want to use that file we ignore it in the boot process. While this approach is feasible it breaks when cryptsetup gets updated and lays down decrypt_gnupg-sc. I created hacks in the system to put back my version but this is not the best way to approach this which is why created luks-root-smartcard-tools.
the goal with this first approach was to have a plugin where cryptsetup would get the token, decrypt, and handle everything to minimize the external processes needed. Cryptsetup maintainers don't want to maintain extra code like smartcard integration and draw a line in the sand. The plugin system would allow cryptsetup to call out to code maintained by someone else (me). This approach is obsoleted by https://github.com/jtmoree-github-com/luks-root-smartcard.
This presumes we have a Linux system with encrypted root already booting. The /boot partition might be encrypted or not. That all depends on your setup. As long as the boot works how you like it now we dont care. If you are tying to boot with a smartcard using grub with encrypted /boot is likely not an option. In my case /boot is unencrypted and the files are signed with my smartcard so that I can detect tampering.
We also have to have cryptsetup 2.4.0 or later installed on the system. This can be tricky since most distros come with other versions at this time. Make sure the system boots after making this change and before moving to the next step.
Next the cryptsetup-smartcard and token-handle.sh scripts have to be installed. ?? does mkinitramfs copy /usr/local binaries?
I use my fork of cryptsetup to add the smartcard based binary key to the encrypted root.
cryptsetup-smartcard add /dev/sda2
If this works we can see the newly created token and new key in the header information
sudo cryptsetup luksDump /dev/sda2
Now we need to modify the boot process. This is where it gets difficult. The current smartcard boot workflow makes assumptions that are no longer valid which means we will need to build a new keyscript and/or modify the boot process to use a wrapper around cryptsetup until it supports this model natively....that is a mess and I will likely need to implement the C plugin to simplify this.
Assuming I have the C plugin working...
Boot process would start with the Linux kernel and initrd
the kernel starts mounting file systems and gets to the encrypted filesystem for / in fstab
next crypttab is checked to get the info needed to decrypt and mount /
crypttab would normally have keyscript=xyz which signifies that the key will be dumped to standard out by running the keyscript xyz
In this case cryptsetup will use the internal token to decrypt
we dont need keyscript? or we need keyscript=
cryptsetup must support the smartcard plugin
what if the smartcard is not entered? cryptsetup shoudl fall back to passphrase entry
Some models call this enrolling a smartcard. To add the smartcard feature to an existing LUKS2 container (/dev/sdb in this example) using the passphrase that already exists in keyslot 0:
cryptsetup-smartcard add /dev/sdb -p -k 0
That will interactively ask for the passphrase. Next, when I want to open the LUKS container using the smartcard I call the handle-token.sh script (this will be a C plugin in the future and work automagicly)
tokens/smartcard/handle-token.sh /dev/sdb 0
If I want a larger binary key to be added to another keyslot for use with the smartcard the command is simpler. This will interactively ask for the passphrase and create the binary key from random data. This model allows the container to be decrypted either using the passphrase or the smartcard based key:
cryptsetup-smartcard add /dev/sdb
Why would we do this? It allows us to use a complex passphrase that we don't have to type all the time--only when absolutely necessary. Some models call this the recovery passphrase. For most cases we type only the PIN associated with the smartcard and can be assured that the key is next to impossible for someone to guess or remember as it is random binary data.
Create LUKS container using typical passphrase setup
Add a large text block as the key in a new keyslot
Use gpg with a smartcard to ascii armor encrypt the text key
Add the encrypted text to a LUKS2 json token
Currently, there is a user space utility that will decrypt the container using the smartcard. The utility will extract the encrypted key and use gpg to decrypt it. Lastly, the key will be used to open the luks container. Of course gpg will prompt the user for a PIN during this process. If the smartcard cannot be found the process falls back to typical cryptsetup. Generally, that is to ask the user for a passphrase.
handle-token.sh /dev/sdX 0
I plan to create a token plugin for cryptsetup to make this automaticly work. When we run cryptsetup luksOpen the token subsystem should activate since the token information is in the keyslot.
Since we are working with json data using tools designed for it will reduce our work. I'm starting with jq for the bash implementation of the decrypt process. Once moving to a C based version I will have libjson available as it's already used by cryptsetup.
I have been looking at the 2.4.0 code base for cryptsetup. This is where the token features are most feature complete though tokens have been around since 2.0.0 release.
There is an example plugin for fetching a key over ssh. I am studying the ssh code because it is similar to what I want to do which is to use an external process to retrieve the key information needed to decrypt.
I first considered using an unbound keyslot to store the smartcard encrypted data but after discussions in the mailing list this is probably not the best approach. The unbound keyslot can hold any secret not related to LUKS but it gets encrypted the same as any other key. In my use case the encryption happens with an external smartcard based process.
LUKS2 headers include space for json data associated with keyslots. This is 'in the clear' data that is not encrypted. Since the smartcard is doing the encryption I have data that can be stored 'in the clear'. I put the encrypted key in a token in the json data.
There are cryptsetup commands to read/write the json data using the latest 2.4.0 release.
echo '{ "type" : "smartcard" , "keyslots" : [ ], "key" : "ascii armor data here" }' | cryptsetup token import /dev/sdb2 --key-slot 0
cryptsetup token export /dev/sdb2 --token-id 1
cryptsetup luksDump /dev/sdb2 --dump-json-metadata
cryptsetup luksDump $DEVICE --debug-json | tr -d '\n' | sed -E 's/^[^{]*//;s/[^}]*$//'
The first implementation for smartcard integration uses bash shell scripts with stock cryptsetup 2.4.0+ and LUKS2. There is a setup utility that can initialize a LUKS2 device or ADD the smartcard support to an existing device. The script cryptsetup-smartcard is currently in my fork of cryptsetup.
There is also a script for decrypting the LUKS2 container using the smartcard. It is currently at tokens/smartcard/handle-token.sh in my fork of the cryptsetup project.
cryptsetup-smartcard add /dev/sdb
That command will interactively add a binary key protected by gpg to the specified LUKS container on /dev/sdb. If you look at the luks header you will see that there is a token added and another keyslot filled. To decrypt and mount the LUKS container using the smartcard we pass the device and token id to the handler script.
./tokens/smartcard/handle-token.sh /dev/sdb 0
The default will add a binary key but you can elect to simply gpg encrypt your current passphrase. This is opened the same way with the handler. This option requires that the keyslot be specified so that the token header is set properly. In this example we specify keyslot 0 by passing -k 0 and we use -p to denote that we are just encrypting the passphrase:
cryptsetup-smartcard add /dev/sdb -p -k 0
There is a test suite at tests/smartcard/test.sh. It tests all functionality that I could think of.
A future implementation of this feature will use a generic C based token plugin in cryptsetup.
If every use case has to code a plugin in C it raises the barrier to entry for using LUKS tokens. Though the cleanest implementation is probably a C plugin it would make token access much easier if there were a generic plugin that calls a specified external program.