关于你的应用今天所传输的数据,有一个值得思考的问题:这些数据的敏感性会持续多久?一份病历、一份合同、一份工资单、一组凭据。如果答案是超过几年,那么真正值得关注的威胁并不是现在就能破解你的加密的攻击者,而是那种现在就记录流量、将其存储起来、等工具跟上后再解密的攻击者。这种做法有一个名字,叫harvest now, decrypt later(现在收集,以后解密),它并不需要任何人今天就造出一台量子计算机,只需要有人相信将来会有人造出来。
这正是标准率先行动的原因。NIST 已在 2024 年发布了相关算法,浏览器和 CDN 在 2025 年启用了混合密钥交换,淘汰 RSA 和椭圆曲线密钥协商的截止日期现已写入公开指南。sgcWebSockets 2026.10 将这一整套算法带到了 Delphi,完全在 sgcCrypto 包内用 Object Pascal 编写,无需安装或部署任何外部库。
三种算法,一个包
这三项 NIST 标准各司其职,通常你只会用到前两项:
- ML-KEM(FIPS 203)用于协商共享密钥,取代了今天 Diffie-Hellman 所承担的工作。
- ML-DSA(FIPS 204)用于签名,取代 RSA 和 ECDSA 签名。
- SLH-DSA(FIPS 205)同样用于签名,完全建立在哈希函数之上,适用于你希望采用最保守的安全假设、并且能够承受较大签名尺寸的场景。
使用 ML-KEM 协商密钥
密钥封装机制(key encapsulation mechanism)比 Diffie-Hellman 更易于使用。发送方获取接收方的公钥,并生成两样东西:一个全新的共享密钥,以及承载该密钥的密文。接收方再将密文还原为相同的密钥。
uses
sgcCrypto_MLKEM;
var
oPublicKey, oPrivateKey, oCiphertext, oSecret, oSame: TBytes;
begin
sgcMLKEM_GenerateKeyPair(mlkem768, oPublicKey, oPrivateKey);
// the sender, who only ever sees the public key
sgcMLKEM_Encapsulate(mlkem768, oPublicKey, oCiphertext, oSecret);
// the recipient, who holds the private key, gets the same 32 bytes
oSame := sgcMLKEM_Decapsulate(mlkem768, oPrivateKey, oCiphertext);
end;
参数集包括 mlkem512、mlkem768 和 mlkem1024。FIPS 203 所要求的输入检查会先于其他一切操作运行,因此格式错误的公钥会引发异常,而不是悄悄生成一个密钥;无法解密的密文会返回一个伪随机密钥而不是错误,这正是该标准所要求的隐式拒绝(implicit rejection)。
X-Wing:当你不想做选择时
采用一种新算法就意味着要信任它。混合构造消除了这一决策:它们将经典算法与后量子算法结合起来,只要其中任意一半仍然安全,结果就是安全的。X-Wing 将 X25519 与 ML-KEM-768 配对,并只暴露一对密钥。
uses
sgcCrypto_MLKEM_Hybrid;
var
oPublicKey, oPrivateKey, oCiphertext, oSecret: TBytes;
begin
sgcXWing_GenerateKeyPair(oPublicKey, oPrivateKey);
sgcXWing_Encapsulate(oPublicKey, oCiphertext, oSecret);
oSecret := sgcXWing_Decapsulate(oPrivateKey, oCiphertext);
end;
使用 ML-DSA 签名
签名操作需要参数集、私钥、消息以及一个上下文字符串,该字符串通常为空:
uses
sgcCrypto_MLDSA;
var
oSeed, oPublicKey, oPrivateKey, oSignature: TBytes;
begin
sgcMLDSA_GenerateKeyPairAndSeed(mldsa65, oSeed, oPublicKey, oPrivateKey);
oSignature := sgcMLDSA_Sign(mldsa65, oPrivateKey, aMessage, nil);
if sgcMLDSA_Verify(mldsa65, oPublicKey, aMessage, oSignature, nil) then
ShowMessage('signature is valid');
end;
请留意种子(seed)。ML-DSA 私钥可以以生成它所用的 32 字节种子形式存储,也可以以展开后的密钥形式存储,或者两者兼有,库对这三种形式都能读写。种子正是你希望保存在配置文件中的形式:它体积小,而展开密钥可以从中以确定性方式派生出来。
可以传输的密钥
一个没人能与之交换密钥的算法没有多大用处,因此其编码遵循已发布的规范:采用 DER 或 PEM 格式的 SubjectPublicKeyInfo 与 PKCS#8,ML-KEM 遵循 RFC 9935,ML-DSA 遵循 RFC 9881,SLH-DSA 遵循 RFC 9909。
var
vPublicPEM, vPrivatePEM: string;
begin
vPublicPEM := sgcMLDSA_ExportPublicKeyPEM(mldsa65, oPublicKey);
vPrivatePEM := sgcMLDSA_ExportPrivateKeyPEM(mldsa65, oSeed, nil);
end;
这些都是普通的 BEGIN PUBLIC KEY 和 BEGIN PRIVATE KEY 区块,可供其他实现读取。私钥在导入时会被检查:公钥部分会被重新推导并比对,因此不属于其证书的密钥会当场被拒绝,而不会生成任何人都无法验证的签名。
证书与后量子 CA
证书和证书请求都可以携带后量子密钥,证书颁发机构本身也可以持有一个,因此整条证书链都可以是后量子的:
var
vKey: TsgcX509SignKey;
vOptions: TsgcX509Options;
oCertDER: TBytes;
begin
vKey := sgcX509_MLDSAKey(mldsa65, oPrivateKey, oPublicKey);
vOptions.Subject.CommonName := 'My Post-Quantum CA';
vOptions.KeyUsage := [kuKeyCertSign, kuCRLSign];
oCertDER := sgcX509_CreateSelfSignedEx(vKey, vOptions);
end;
规范中的密钥用途规则会在任何签名操作之前被强制执行,因此当一个后量子密钥被要求执行密钥加密(key encipherment)时会引发异常,而不会生成违反自身规范的证书。证书链校验现在也会强制执行颁发证书所声明的路径长度和名称约束。
已经集成的位置
库中有两个地方已经为你用上了这一切,你无需编写任何密码学代码:
- TLS。原生 TLS 1.3 引擎会协商 RFC 10024 中的混合密钥交换组,即
X25519MLKEM768、SecP256r1MLKEM768和SecP384r1MLKEM1024,默认列表已经以一个混合组开头。参见《不依赖 OpenSSL DLL 的 Delphi TLS 1.3》。 - JWT。令牌可以按照 RFC 9964 使用 ML-DSA 进行签名和验证。参见《在 Delphi 中使用 ML-DSA 签署 JWT》。
我们如何确认它是正确的
对标准的实现,在被验证之前只是一种声明。这三种算法都已针对 NIST 已知答案测试向量进行了验证,这些向量作为可运行的测试集随库一同提供,而不仅仅是数据表中的一句话。ML-DSA 签名器采用确定性变体,因此相同的密钥和相同的消息总会得到相同的签名,这也是 RFC 中发布的示例能够逐字节复现的原因。
应该选用哪一种
对于密钥协商,请使用混合方案:单独使用 X-Wing,或者在流量为 TLS 时使用混合 TLS 组。你几乎不会失去什么,而且无论哪一种假设最终被证明有误,你都受到保护。对于签名,ML-DSA-65 是合理的默认选择,在意尺寸时用 ML-DSA-44,不在意尺寸时用 ML-DSA-87。当签名将在多年后由某个你无法更新的东西(例如固件)进行验证、且签名尺寸可以承受时,可以选用 SLH-DSA。
升级
所有这些都位于 sgcCrypto 包中,不增加任何依赖:没有 OpenSSL,没有 DLL,运行你的应用的机器上无需安装任何东西。在你调用它之前,现有代码不受任何影响。
延伸阅读
观看视频
在eSeGeCe 频道上有一个关于此内容的简短视频。
有疑问、反馈,或者需要迁移方面的帮助?与我们联系—你会收到编写这些代码的人的回复。
