FIDO2 security keys offer a versatile range of user authentication options. We have explored some of these possibilities during a workshop we presented at ph0wn. This post delves deeper into setting up disk encryption with LUKS safeguarded by a security key. We also explain the underlying mechanics and highlight pitfalls to avoid.
LUKS disk encryption
LUKS is a common solution to encrypt block devices like solid-state drives in Linux ecosystems. Basically it works by leaving a header unencrypted before the encrypted devices with all the necessary information to decrypt the following device. This header contains information to derive a key used to decrypt a binary key slot area. In the key slot, a master key is then used to decrypt or encrypt the whole device. A well known tool to manage encrypted devices is cryptsetup
. It allows to setup and manage encryption of devices. For example, to create a LUKS device the subcommand luksFormat
can be used on a file or on a block device. This command formats completely your device, thus, it has to be used carefully:
$ cryptsetup luksFormat disk.img
WARNING!
========
This will overwrite data on disk.img irrevocably.
Are you sure? (Type 'yes' in capital letters): YES
Enter passphrase for disk.img:
Verify passphrase:
cryptsetup
requests a passphrase which will be used for device encryption and decryption. Then our file disk.img
is formatted and encrypted. We can simply open it with the open
command:
$ sudo cryptsetup open disk.img encrypted
Enter passphrase for disk.img:
$ lsblk
...
loop23 7:23 0 30M 0 loop
โโencrypted 254:0 0 14M 0 crypt
...
Now, let’s see now how we can replace the passphrase by a security key.
Disk encryption with FIDO2 hmac-secret extension
FIDO2 introduces a standardized communication protocol known as Client-to-Authenticator Protocol (CTAP), which defines the exchange of information between security keys, also referred as authenticators, and the operating system or web browser. The current version is 2.1 and a new version 2.2 is published as a Review Draft. This standard is often used to authenticate users and get rid of passwords as it was already explained in one of our previous blog post.
For LUKS disk encryption, a CTAP extension called “hmac-secret” is used. This extension extends the behavior of the CTAP commands authenticatorMakeCredential
and authenticatorGetAssertion, allowing to obtain a symmetric key for LUKS key slot decryption. You can verify if your authenticator supports the hmac-secret extension with the get_info.py
example script of the Yubico python-fido2 library. This script uses the authenticatorGetInfo
CTAP command to retrieve the authenticator information. For example, this is the output of the script when using the Ledger Nano X together with the Security key application:
$ python get_info.py
CONNECT: CtapHidDevice('/dev/hidraw7')
Product name: Ledger Nano X
Serial number: 0001
CTAPHID protocol version: 2
DEVICE INFO: Info(versions=['U2F_V2', 'FIDO_2_0'], extensions=['hmac-secret', 'txAuthSimple'], aaguid=AAGUID(fcb2bcb5-f377-078c-6994-ec24d0fe3f1e), options={'rk': True, 'up': True, 'uv': True, 'clientPin': False}, max_msg_size=1024, pin_uv_protocols=[1], max_creds_in_list=None, max_cred_id_length=None, transports=[], algorithms=None, max_large_blob=None, force_pin_change=False, min_pin_length=4, firmware_version=None, max_cred_blob_length=None, max_rpids_for_min_pin=0, preferred_platform_uv_attempts=None, uv_modality=None, certifications=None, remaining_disc_creds=None, vendor_prototype_config_commands=None)
Device does not support WINK
This indicates that the device adheres to the CTAP 2.0 standard and supports the hmac-secret extension.
During the CTAP authenticatorMakeCredential
command, if a hmac-secret parameter is present, the authenticator creates a credential ID and, in addition, generates a 32-bytes of random called CredRandom
. To enroll a security key to a LUKS device, systemd
offers a convenient tool called systemd-cryptenroll
. This tool can be used to enroll the authenticator using the hmac-secret extension. Again, this following command will wipe previous slots and thus have to be used with caution:
$ systemd-cryptenroll --fido2-device=auto --wipe-slot=all test.img
๐ Please enter current passphrase for disk /home/luks/test.img: (press TAB for no echo)********
Initializing FIDO2 credential on security token.
๐ (Hint: This might require confirmation of user presence on security token.)
๐ Please enter security token PIN: ********
Generating secret key on FIDO2 security token.
๐ In order to allow secret key generation, please confirm presence on security token.
New FIDO2 token enrolled as key slot 1.
Wiped slot 0.
Lets look at the new LUKS header of our device after the security key enrollment:
$ cryptsetup luksDump test.img
LUKS header information
Version: 2
Epoch: 6
Metadata area: 16384 [bytes]
Keyslots area: 16744448 [bytes]
UUID: df8d4f98-2e1e-4342-befb-edf4bfa3e5a8
Label: (no label)
Subsystem: (no subsystem)
Flags: (no flags)
Data segments:
0: crypt
offset: 16777216 [bytes]
length: (whole device)
cipher: aes-xts-plain64
sector: 4096 [bytes]
Keyslots:
1: luks2
Key: 512 bits
Priority: normal
Cipher: aes-xts-plain64
Cipher key: 512 bits
PBKDF: pbkdf2
Hash: sha512
Iterations: 1000
Salt: 2e 93 59 1b 94 e1 df 30 2a 98 15 10 f1 5c b5 19
89 65 d4 fd 2f 58 ac 02 68 8b cc 42 07 0b 99 12
AF stripes: 4000
AF hash: sha512
Area offset:290816 [bytes]
Area length:258048 [bytes]
Digest ID: 0
Tokens:
0: systemd-fido2
fido2-credential:
db 76 b3 fc 9d d0 18 e5 1e ec f0 53 2b ed e9 8b
b7 70 73 85 fd 4f 16 d5 7c dc 21 1c 2c 8f 12 e7
29 f7 a1 22 05 5b e0 43 e4 45 23 55 33 88 6d 34
89 03 9b 2c 77 92 1a 87 6e e5 24 23 2f 40 05 e6
fido2-salt: 95 11 36 b7 b1 93 54 42 0f 4f 79 95 4e e4 77 d1
f9 e0 d7 7a f1 37 fd 49 ab 04 6c f0 cd d9 7b 8a
fido2-rp: io.systemd.cryptsetup
fido2-clientPin-required:
true
fido2-up-required:
true
fido2-uv-required:
false
Keyslot: 1
Digests:
0: pbkdf2
Hash: sha256
Iterations: 326455
Salt: ac 17 58 50 09 95 09 c8 bc e5 fd d3 03 50 8f 98
c9 76 55 2b e7 fc 45 09 d4 c8 4b ce b2 12 30 79
Digest: ab 67 ee 89 09 45 4f ba 80 35 1a f0 a1 0b e0 ae
8b e9 82 8f 72 7b 6a 54 b5 6a 43 91 aa 0a 6c fe
We can see we have a token associated with the key slot 0. This token has a fido2-credential, the credential id which allows to reconstruct everything needed at the security key side including the value of CredRandom
. Nothing is stored on the security key, thus is has to be stored in the LUKS header.
Then to generate a secret, the command CTAP authenticatorGetAssertion
is used. A shared secret between the security key and the host is obtain with computing a Diffie-Hellman key agreement between the host on the security key. The shared secret is used as a key to encrypt and authenticate a salt
chosen by the host and embedded within the fido2-salt field of the LUKS header. The encrypted salt is sent to the security key which will verify the authenticity of the salt and decrypt it. Then the autenticator generates a secret output
being the HMAC of the previously generated CredRandom
and the salt
:
output = HMAC-SHA-256(CredRandom, salt)
It is returned encrypted to the host. Once decrypted, cryptsetup
uses this secret to decrypt the key slot associated. Practically, when you want to decrypt the LUKS image cryptsetup
would need your authenticator and your PIN code to reconstruct the secret output
:
$ sudo cryptsetup open --token-only image.img encrypted
Enter token PIN:
Asking FIDO2 token for authentication.
๐ Please confirm presence on security token to unlock.
Since a part of the secret, CredRandom
is only recoverable by the security key, it is not possible to decrypt the device without it.
PIN management
CTAP employs a parameter called clientPin during the credential creation. When set to True
, this indicates that a PIN code has been configured on your authenticator, and you will be prompted for this PIN for future credential generation. If you have not established a PIN code and enroll your security key without setting clientPin to True
, the PIN code will never be required for device decryption, even if you configure one later. This arrangement is suitable for devices like the Ledger wallet, which necessitates a separate PIN code for device initiation. However, for other authenticators, this poses a potential issue, as anyone who steals both the device and the authenticator could decrypt your data without requiring your PIN code.
There is another issue as well. If the clientPin was set but a authenticatorGetAssertion
request is made without the PIN code, CTAP 2.0 specifies:
This implies that if the pinAuth parameter is omitted, a ‘uv’ bit is set to 0 meaning that the user verification was not made by the authenticator. However, the CTAP specification does not explicitly state whether the hmac-secret extension should return the secret based on the ‘uv’ bit. To investigate this behavior, we conducted experiments on various authenticators. We initiated the test on a Solo key running an outdated firmware version (3.0.0):
$ python get_info.py
CONNECT: CtapHidDevice('/dev/hidraw6')
Product name: SoloKeys Solo 3.0.0
Serial number: 2060469E55B9
CTAPHID protocol version: 2
DEVICE INFO: Info(versions=['U2F_V2', 'FIDO_2_0'], extensions=['hmac-secret'], aaguid=AAGUID(8876631b-e4a0-428f-5784-0ac71c9e0279), options={'rk': True, 'up': True, 'plat': False, 'clientPin': True}, max_msg_size=1200, pin_uv_protocols=[1], max_creds_in_list=None, max_cred_id_length=None, transports=[], algorithms=None, max_large_blob=None, force_pin_change=False, min_pin_length=4, firmware_version=None, max_cred_blob_length=None, max_rpids_for_min_pin=0, preferred_platform_uv_attempts=None, uv_modality=None, certifications=None, remaining_disc_creds=None, vendor_prototype_config_commands=None)
WINK sent!
This authenticator supports CTAP 2.0 with the hmac-secret extension and it has the client PIN already set. Then we enroll our key to a LUKS device as previously and we obtain the same header as before. But then in the LUKS header we patched the value fido2-clientPin-required
to False
. Since the header as a checksum mechanism we created a script to handle all the operation and it is available here. Then after we patched the header we can verify everything is fine:
$ cryptsetup luksDump test.img
LUKS header information
Version: 2
Epoch: 6
Metadata area: 16384 [bytes]
Keyslots area: 16744448 [bytes]
UUID: 7a8cc432-f3ef-48de-9644-74e7d6c81d6a
Label: (no label)
Subsystem: (no subsystem)
Flags: (no flags)
Data segments:
0: crypt
offset: 16777216 [bytes]
length: (whole device)
cipher: aes-xts-plain64
sector: 4096 [bytes]
Keyslots:
1: luks2
Key: 512 bits
Priority: normal
Cipher: aes-xts-plain64
Cipher key: 512 bits
PBKDF: pbkdf2
Hash: sha512
Iterations: 1000
Salt: b7 ed 7b 10 20 c4 d6 65 cc 59 5c 64 16 9c 1e b4
22 33 63 09 a1 1f fc f4 5b 77 79 02 81 47 7d 35
AF stripes: 4000
AF hash: sha512
Area offset:290816 [bytes]
Area length:258048 [bytes]
Digest ID: 0
Tokens:
0: systemd-fido2
fido2-credential:
1e 51 5b 40 ac cb 0f d0 da e3 eb 5f 20 f9 1c aa
c3 16 f6 3c a4 00 ad ca 21 ab 64 ef e5 a3 03 ac
4b 42 3b a2 a1 21 ff 04 55 14 ab e1 b8 2a 95 99
df d9 be 3c 43 64 db 0d 6c d0 10 00 d7 29 10 1a
ba 8f 87 02 00 00
fido2-salt: bc 34 af d7 bd 50 0b 9a 8a 7f 63 51 a6 fb d3 77
36 34 ce 2a c0 26 e7 bf 49 b3 1b 31 d3 3b 11 46
fido2-rp: io.systemd.cryptsetup
fido2-clientPin-required:
false
fido2-up-required:
true
fido2-uv-required:
false
Keyslot: 1
The field fido2-clientPin-required
is set to False. And if we try to open our device:
$ sudo cryptsetup open --token-only test.img encrypted
Asking FIDO2 token for authentication.
๐ Please confirm presence on security token to unlock.
Surprise! in this case, no PIN code is request but the device is decrypted correctly. This is a security issue as explained before if someone is able to steal you disk and you security key. This behavior is not generalized to all auhtenticators. We have tested a Yubikey having the firmware version 5.1.1 and it turned out that the key would not allow to answer the secret even if the fido2-clientPin-required is set to False.
CTAP update
The previous flaw was rectified in later CTAP versions, starting from CTAP 2.1. These updates introduced a change where the security key generates two distinct secrets during the authenticatorMakeCredential
command: CredRandomWithUV
and CredRandomWithoutUV
. During the command execution, the authenticator determines whether to utilize CredRandomWithUV
or CredRandomWithoutUV
as the CredRandom for secret generation based on whether user verification was performed in the preceding steps. Let’s observe how a newer security key behaves in practice. We’ve selected a newer YubiKey 5 NFC as our example.
$ python get_info.py
CONNECT: CtapHidDevice('/dev/hidraw5')
Product name: Yubico YubiKey OTP+FIDO+CCID
Serial number: None
CTAPHID protocol version: 2
DEVICE INFO: Info(versions=['U2F_V2', 'FIDO_2_0', 'FIDO_2_1_PRE'], extensions=['credProtect', 'hmac-secret'], aaguid=AAGUID(2fc0578f-8553-48ea-b11f-ba5a8fb9213a), options={'rk': True, 'up': True, 'plat': False, 'clientPin': True, 'credentialMgmtPreview': True}, max_msg_size=1200, pin_uv_protocols=[2, 1], max_creds_in_list=8, max_cred_id_length=128, transports=['nfc', 'usb'], algorithms=[{'alg': -7, 'type': 'public-key'}, {'alg': -8, 'type': 'public-key'}], max_large_blob=None, force_pin_change=False, min_pin_length=4, firmware_version=328707, max_cred_blob_length=None, max_rpids_for_min_pin=0, preferred_platform_uv_attempts=None, uv_modality=None, certifications=None, remaining_disc_creds=None, vendor_prototype_config_commands=None)
WINK sent!
The field 'FIDO_2_1_PRE
indicates that the authenticator supports partially the CTAP 2.1 protocol. Then, as previously, we enrolled our key to the disk image and we patched the LUKS header with our script and finally, we tried to open the image with cryptsetup
:
$ sudo cryptsetup open --token-only test.img encrypted
Asking FIDO2 token for authentication.
๐ Please confirm presence on security token to unlock.
It seems we have the same problem as before. However, the device is not open properly and if we ask cryptsetup
to be more verbose we can understand the problem:
$ sudo cryptsetup open --token-only test.img encrypted --debug
Asking FIDO2 token for authentication.
๐ Please confirm presence on security token to unlock.
# Trying to open keyslot 1 with token 0 (type systemd-fido2).
# Trying to open LUKS2 keyslot 1.
# Running keyslot key derivation.
# Reading keyslot area [0x47000].
# Acquiring read lock for device test.img.
# Verifying lock handle for test.img.
# Device test.img READ lock taken.
# Reusing open ro fd on device test.img
# Device test.img READ lock released.
# Verifying key from keyslot 1, digest 0.
# Digest 0 (pbkdf2) verify failed with -1.
# Releasing crypt device test.img context.
# Releasing device-mapper backend.
# Closing read only fd for test.img.
Command failed with code -2 (no permission or bad passphrase).
# Unloading systemd-fido2 token handler.
The key slot verification failed and that’s coherent with the usage of the value CredRandomWithoutUV
which differs from CredRandomWithUV
in CTAP 2.1 and would lead to a different secret generation.
This update is now implemented in the latest solo key firmware but some authenticators may not be upgradable and thus it is better to check the version of CTAP supported by your authenticator before setting up disk encryption.
Conclusion
FIDO2 security keys offer a convenient and secure method for unlocking LUKS encrypted disks. However, it’s crucial to understand the underlying mechanisms and potential pitfalls to ensure optimal protection. To safeguard your data, it’s essential to utilize FIDO2 security keys with the latest CTAP version and ensure proper credential creation procedures, including user verification when applicable.