使用FIDO2实现LUKS磁盘加密的技术解析与安全实践

本文详细介绍了如何利用FIDO2安全密钥增强LUKS磁盘加密的安全性,包括技术实现原理、CTAP协议扩展、密钥管理机制以及不同版本CTAP协议的安全差异和潜在风险。

LUKS磁盘加密与FIDO2

FIDO2安全密钥提供了多种用户认证选项。在ph0wn研讨会上,我们探讨了其中一些可能性。本文深入探讨了如何使用安全密钥保护LUKS磁盘加密的设置,并解释了底层机制及需要避免的陷阱。

LUKS磁盘加密

LUKS是Linux生态系统中加密块设备(如固态硬盘)的常见解决方案。其基本工作原理是在加密设备前保留未加密的头部,包含解密后续设备所需的所有信息。该头部包含用于解密二进制密钥槽区域的密钥派生信息。在密钥槽中,主密钥用于解密或加密整个设备。管理加密设备的知名工具是cryptsetup,它允许设置和管理设备加密。例如,要在文件或块设备上创建LUKS设备,可以使用luksFormat子命令。此命令会完全格式化设备,因此需谨慎使用:

1
2
3
4
5
6
7
8
9
$ 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请求一个用于设备加密和解密的密码。然后,文件disk.img被格式化并加密。我们可以使用open命令简单地打开它:

1
2
3
4
5
6
7
$ 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
...

现在,让我们看看如何用安全密钥替换密码。

使用FIDO2 hmac-secret扩展进行磁盘加密

FIDO2引入了称为客户端到认证器协议(CTAP)的标准化通信协议,定义了安全密钥(也称为认证器)与操作系统或Web浏览器之间的信息交换。当前版本为2.1,新版本2.2已作为评审草案发布。该标准通常用于用户认证并消除密码,如我们之前的博客文章所述。

对于LUKS磁盘加密,使用了一个称为“hmac-secret”的CTAP扩展。此扩展扩展了CTAP命令authenticatorMakeCredential和authenticatorGetAssertion的行为,允许获取用于LUKS密钥槽解密的对称密钥。您可以使用Yubico python-fido2库的get_info.py示例脚本验证您的认证器是否支持hmac-secret扩展。此脚本使用authenticatorGetInfo CTAP命令检索认证器信息。例如,这是使用Ledger Nano X和安全密钥应用程序时脚本的输出:

1
2
3
4
5
6
7
$ 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

这表明设备遵循CTAP 2.0标准并支持hmac-secret扩展。

在CTAP authenticatorMakeCredential命令期间,如果存在hmac-secret参数,认证器会创建凭证ID,并额外生成32字节的随机值CredRandom。要将安全密钥注册到LUKS设备,systemd提供了一个方便的工具systemd-cryptenroll。此工具可用于使用hmac-secret扩展注册认证器。同样,以下命令将擦除先前的槽,因此必须谨慎使用:

1
2
3
4
5
6
7
8
9
$ 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.

让我们看看安全密钥注册后设备的新LUKS头部:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
$ 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

我们可以看到有一个与密钥槽0关联的令牌。此令牌有一个fido2-credential,即凭证ID,允许在安全密钥端重建所有需要的内容,包括CredRandom的值。安全密钥上不存储任何内容,因此必须存储在LUKS头部中。

然后,使用CTAP authenticatorGetAssertion命令生成密钥。通过计算主机和安全密钥之间的Diffie-Hellman密钥协商,获得安全密钥和主机之间的共享密钥。共享密钥用作加密和验证主机选择的盐的密钥,该盐嵌入在LUKS头部的fido2-salt字段中。加密的盐发送到安全密钥,安全密钥将验证盐的真实性并解密它。然后,认证器生成一个秘密输出,即先前生成的CredRandom和盐的HMAC:

1
output = HMAC-SHA-256(CredRandom, salt)

它被加密返回给主机。一旦解密,cryptsetup使用此秘密解密关联的密钥槽。实际上,当您想要解密LUKS镜像时,cryptsetup需要您的认证器和PIN码来重建秘密输出:

1
2
3
4
$ sudo cryptsetup open --token-only image.img encrypted
Enter token PIN: 
Asking FIDO2 token for authentication.
👆 Please confirm presence on security token to unlock.

由于秘密的一部分CredRandom只能由安全密钥恢复,因此没有它就无法解密设备。

PIN管理

CTAP在凭证创建期间使用一个称为clientPin的参数。当设置为True时,这表明您的认证器上已配置了PIN码,并且将来生成凭证时会提示输入此PIN码。如果您没有建立PIN码并在未将clientPin设置为True的情况下注册安全密钥,则即使稍后配置了PIN码,设备解密也永远不会要求PIN码。这种安排适用于像Ledger钱包这样的设备,这些设备需要单独的PIN码来启动设备。然而,对于其他认证器,这构成了潜在问题,因为任何窃取设备和认证器的人都可以解密您的数据,而无需您的PIN码。

还有另一个问题。如果设置了clientPin但未提供PIN码就发出authenticatorGetAssertion请求,CTAP 2.0规定:

CTAP 2.0

这意味着如果省略了pinAuth参数,则“uv”位设置为0,表示认证器未进行用户验证。然而,CTAP规范未明确说明hmac-secret扩展是否应根据“uv”位返回秘密。为了调查此行为,我们在各种认证器上进行了实验。我们在运行旧固件版本(3.0.0)的Solo密钥上启动了测试:

1
2
3
4
5
6
7
$ 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!

此认证器支持带有hmac-secret扩展的CTAP 2.0,并且已设置了客户端PIN。然后,我们如前所述将密钥注册到LUKS设备,并获得了与之前相同的头部。但随后我们在LUKS头部中将值fido2-clientPin-required修补为False。由于头部有校验和机制,我们创建了一个脚本来处理所有操作,该脚本可在此处获得。修补头部后,我们可以验证一切正常:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
$ 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

字段fido2-clientPin-required设置为False。如果我们尝试打开设备:

1
2
3
$ sudo cryptsetup open --token-only test.img encrypted        
Asking FIDO2 token for authentication.
👆 Please confirm presence on security token to unlock.

惊喜!在这种情况下,没有请求PIN码,但设备正确解密。这是一个安全问题,如前所述,如果有人能够窃取您的磁盘和安全密钥。此行为并非所有认证器都通用。我们测试了固件版本为5.1.1的Yubikey,结果发现即使fido2-clientPin-required设置为False,密钥也不允许回答秘密。

CTAP更新

先前的缺陷在后续CTAP版本中得到了纠正,从CTAP 2.1开始。这些更新引入了一个变化,即安全密钥在authenticatorMakeCredential命令期间生成两个不同的秘密:CredRandomWithUV和CredRandomWithoutUV。在命令执行期间,认证器根据先前步骤是否执行了用户验证来确定是使用CredRandomWithUV还是CredRandomWithoutUV作为秘密生成的CredRandom。让我们观察较新的安全密钥在实际中的行为。我们选择较新的YubiKey 5 NFC作为示例。

1
2
3
4
5
6
7
$ 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!

字段’FIDO_2_1_PRE’表示认证器部分支持CTAP 2

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计