Go 语言中使用 MLDSA 和 MLKEM 的实践
概述
背景
早在很久以前,量子计算机的快速运算就被认为是现有加密体系的威胁。这是因为像现有的 RSA 或 ECC 等算法,由于量子计算机的这种运算能力,有可能被破解。但是,从几年前量子计算机的概念开始具体化以来,针对其的替代方案的研究和开发也开始进行,并且 NIST 一直在进行 PQC(后量子密码学)的标准化工作。
MLDSA 和 MLKEM
最终,NIST 于 2024 年 8 月采用了基于 CRYSTALS-Kyber 和 CRYSTALS-Dilithium 的 MLKEM 和 MLDSA 作为标准。这两个算法都基于名为 MLWE(Module Learning with Errors)的问题运行。我们将这种形式称为格基密码学。
格基密码学顾名思义,是一种基于格子上数学难题的加密系统。 我也没有关于此的深入数学知识,但如果用一句话概括,就是在模块格中求解带噪声的线性方程的问题
。 尽管我不知道它有多难,但据说这个问题难到即使是量子计算机也无法解决。
MLDSA
那么,首先了解一下 MLDSA。
构成
顾名思义,MLDSA 是一种非对称签名算法,总共经过以下两个步骤。
- 签名生成:使用私钥生成消息的签名。
- 签名验证:使用公钥验证生成的签名的有效性。
并且 MLDSA 具有以下三个特性。
- strong existential unforgeability:不能用一个签名和公钥生成其他有效的签名。
- chosen message attack:即使有对某个消息的签名,也不能用公钥生成新的有效签名。
- side-channel attack:签名时持续使用新的随机值和从消息派生的伪随机值,因此安全性很高。
- domain separation:对不同的参数使用不同的 seed,以预防重复的安全问题。
代码
那么,我将展示一个简单的 Go 语言示例代码。在此示例中,我使用了 cloudflare/circl 的 mldsa。
1package main
2
3import (
4 "crypto"
5 "crypto/rand"
6 "encoding/base64"
7 "fmt"
8
9 "github.com/cloudflare/circl/sign/mldsa/mldsa44"
10)
11
12func main() {
13 // 使用 mldsa44 规范生成密钥。
14 pub, priv, err := mldsa44.GenerateKey(rand.Reader)
15 if err != nil {
16 panic(err)
17 }
18
19 message := []byte("Hello, World!")
20
21 // 生成签名。
22 // 需要注意的一点是,截至 2024 年 12 月 22 日的当前版本,如果不是 crypto.Hash(0) 则会发生错误。
23 signature, err := priv.Sign(rand.Reader, message, crypto.Hash(0))
24 if err != nil {
25 panic(err)
26 }
27
28 encodedSignature := base64.URLEncoding.EncodeToString(signature)
29 fmt.Println(len(encodedSignature), encodedSignature)
30
31 // 调用公钥的 scheme 进行验证。
32 ok := pub.Scheme().Verify(pub, message, signature, nil)
33 fmt.Println(ok)
34}
13228 oaSaOA-...
2true
签名值太长,省略了。 如果您想查看完整内容,请在playground 中运行。
即使是 base64 编码,3228 字节的大小也有些令人负担。想到不久的将来,我们可能需要以这种大小来交换应对量子计算机的签名,就感到有些负担。
MLKEM
构成
MLKEM 是一种密钥封装机制(Key Encapsulation Mechanism)。KEM 是一种允许两个参与方使用公钥加密方法生成共享密钥的算法。MLKEM 的密钥交换机制遵循以下过程。
- 密钥封装:发送方使用接收方的公钥生成加密消息(cipher text)和共享密钥(shared key)。这个加密消息最初被传递给接收方使用。
- 密钥解封装:接收方使用自己的私钥从加密消息中提取共享密钥。
MLKEM 总共有 3 个参数。存在 MLKEM-512、MLKEM-768 和 MLKEM-1024,参数越小,密钥和密文就越小,参数越大,密钥和密文就越长,安全级别也越高。
代码
MLKEM 将在 go 1.24 中添加,因此目前使用了可以使用的 go 1.24rc1。
1package main
2
3import (
4 "crypto/mlkem"
5 "encoding/base64"
6 "fmt"
7)
8
9func main() {
10 // 生成接收方的 PrivateKey。
11 receiverKey, err := mlkem.GenerateKey1024()
12 if err != nil {
13 panic(err)
14 }
15
16 // 在 MLKEM 中,我们使用术语 EncapsulationKey 而不是 PublicKey。
17 receiverPubKey := receiverKey.EncapsulationKey()
18
19 // 为了简单地展示可以使用 EncapsulationKey 的 Bytes() 和 NewEncapsulationKeyX 来提取和重用密钥,我进行了复制。
20 // 当然,在现实中,可以将其视为发送方将以文本形式公开的接收方的 EncapsulationKey 密钥创建为对象的过程。
21 clonedReceiverPubKey, err := mlkem.NewEncapsulationKey1024(receiverPubKey.Bytes())
22 if err != nil {
23 panic(err)
24 }
25
26 // 发送方使用 Encapsulate 生成密文和共享密钥。
27 cipherText, SenderSharedKey := clonedReceiverPubKey.Encapsulate()
28
29 // 我故意复制它以演示如何存储和检索接收方的私钥。
30 clonedReceiverKey, err := mlkem.NewDecapsulationKey1024(receiverKey.Bytes())
31 if err != nil {
32 panic(err)
33 }
34
35 // 接收方使用私钥 Decapsulate 密文以生成另一个共享密钥。
36 sharedKeyReceiver, err := clonedReceiverKey.Decapsulate(cipherText)
37 if err != nil {
38 panic(err)
39 }
40
41 fmt.Println(base64.StdEncoding.EncodeToString(SenderSharedKey))
42 fmt.Println(base64.StdEncoding.EncodeToString(sharedKeyReceiver))
43}
1Q1ciS818WFHTK7D4MTvsQvciMTGF+dSGqMllOxW80ew=
2Q1ciS818WFHTK7D4MTvsQvciMTGF+dSGqMllOxW80ew=
结果,我们可以确认生成了相同大小的共享密钥!
此代码也可以在playground中查看。
结论
每个算法的规范、安全级别以及私钥、公钥、签名或密文的大小可以总结如下。每个算法都名副其实地展现了 PQC 的庞大尺寸。
算法 | NIST 安全级别 | 私钥大小 | 公钥大小 | 签名/密文大小 |
---|---|---|---|---|
ML-DSA-44 | 2 | 2,560 | 1,312 | 2,420 |
ML-DSA-65 | 3 | 4,032 | 1,952 | 3,309 |
ML-DSA-87 | 5 | 4,896 | 2,592 | 4,627 |
ML-KEM-512 | 1 | 1,632 | 800 | 768 |
ML-KEM-768 | 3 | 2,400 | 1,184 | 1,088 |
ML-KEM-1024 | 5 | 3,168 | 1,568 | 1,568 |
我们期望通过这些算法,即使在量子计算机上也能使用足够安全的互联网,但是,相对增加的密钥和签名/密文的大小似乎不可避免地会导致更多的计算。
尽管如此,由于 Go 语言有效地实现了每个算法,我们期待它们在适当的位置积极地用于保护您的安全!