后量子密码学

一台足够强大的量子计算机将能够攻破公钥密码学页面中列出的 RSA、X25519/X448 以及所有曲线上的 ECDSA/ECDH:这些数学基础没有一个能抵御 Shor 算法。 它不需要在今天就已经存在才能成为今天的问题,攻击者现在就可以记录加密流量,等到这样的机器出现后再解密, 这种策略通常被称为“先收集,后解密”。任何需要保密数年而非数天的内容,都应当已经开始迁移到后量子密钥交换。

 

NIST 标准化了三种方案,本库均已实现,且全部只基于 SHAKE 构建 (参见哈希与消息认证),因此它们的安全性仅取决于该哈希是否稳固: 不存在需要被攻破的格或数论假设。

 

方案 用途 标准 单元
ML-KEM 密钥封装,替代 Diffie-Hellman FIPS 203 sgcCrypto_MLKEM
ML-DSA 签名,基于格,默认首选方案 FIPS 204 sgcCrypto_MLDSA
SLH-DSA 签名,仅基于哈希,保守的备选方案 FIPS 205 sgcCrypto_SLHDSA

 

ML-KEM 与混合构造(推荐)

 

ML-KEM 用于替代 Diffie-Hellman 交换:发送方把一个共享密钥封装给接收方的公钥, 接收方用私钥解封装,最终双方得到相同的 32 字节结果。它提供三种参数集,参数越大, 安全余量越高,密钥也越大。

 


procedure sgcMLKEM_GenerateKeyPair(aParams: TsgcMLKEMParams; out aPublicKey, aPrivateKey: TBytes);
procedure sgcMLKEM_Encapsulate(aParams: TsgcMLKEMParams; const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcMLKEM_Decapsulate(aParams: TsgcMLKEMParams; const aPrivateKey, aCiphertext: TBytes): TBytes;

var
  oPub, oPriv, oCiphertext, oSecretA, oSecretB: TBytes;
begin
  sgcMLKEM_GenerateKeyPair(mlkem768, oPub, oPriv);
  sgcMLKEM_Encapsulate(mlkem768, oPub, oCiphertext, oSecretA);   { sender }
  oSecretB := sgcMLKEM_Decapsulate(mlkem768, oPriv, oCiphertext); { recipient }
  { oSecretA = oSecretB }
end;

 

FIPS 203 的解封装永远不会失败。

损坏或恶意构造的密文不会引发错误,而是静默返回一个不同的共享密钥, 该密钥由内置在私钥中的一个隐式拒绝值派生而来。如果改为返回错误,则相当于给攻击者提供了一个 可用来探测私钥的解密预言机;该标准正是为了避免这一点而如此设计。派生出的共享密钥所保护的对象, 必须自行检测这种不匹配,例如依赖下游某个 AEAD 标签验证失败。

 

今天真正应当部署的是混合构造:将 X25519 与 ML-KEM-768 组合使用,使共享密钥同时依赖于两者。 即使 ML-KEM 未来因某种密码分析进展而被攻破,它依然安全;即使量子计算机攻破了 X25519,它依然安全; 攻击者必须同时攻破两者才能得手。

 


procedure sgcHybrid_GenerateKeyPair(out aPublicKey, aPrivateKey: TBytes);
procedure sgcHybrid_Encapsulate(const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcHybrid_Decapsulate(const aPrivateKey, aCiphertext: TBytes; out aSharedSecret: TBytes): Boolean;

 

X-Wing 与 TLS 1.3 混合组

 

sgcCrypto_MLKEM_Hybrid 中包含三个混合系列。上面那些 sgcHybrid_ 函数是最初的 sgcCrypto 格式,保留它们是为了向后兼容。 X-Wing 是已标准化的通用混合 KEM,由 X25519 与 ML-KEM-768 组合而成,在 TLS 之外的新代码中应当使用它。 第三个系列只用于构建 TLS 1.3 的混合密钥共享,别无他用。

 

X-Wing(draft-connolly-cfrg-xwing-kem-10)只保存一个秘密值:私钥是一个 32 字节的种子, 每一次操作都会把它展开成所需的 ML-KEM-768 和 X25519 材料。公钥为 1216 字节,密文为 1120 字节, 共享密钥为 32 字节。封装过程会执行 FIPS 203 的封装密钥检查,当公钥未通过该检查时会引发异常。

 


procedure sgcXWing_GenerateKeyPair(out aPublicKey, aPrivateKey: TBytes);
procedure sgcXWing_Encapsulate(const aPublicKey: TBytes; out aCiphertext, aSharedSecret: TBytes);
function sgcXWing_Decapsulate(const aPrivateKey, aCiphertext: TBytes; out aSharedSecret: TBytes): Boolean;

var
  oPub, oPriv, oCiphertext, oSecretA, oSecretB: TBytes;
begin
  sgcXWing_GenerateKeyPair(oPub, oPriv);
  sgcXWing_Encapsulate(oPub, oCiphertext, oSecretA);      { sender }
  if sgcXWing_Decapsulate(oPriv, oCiphertext, oSecretB) then
    { oSecretA = oSecretB };
end;

 

RFC 10024 所定义的 TLS 1.3 混合密钥共享是 X25519MLKEM768、 SecP256r1MLKEM768 和 SecP384r1MLKEM1024, 对应的 IANA 码点分别为 0x11EC、0x11EB 和 0x11ED。它们并不是可以单独使用的 KEM。 共享密钥只是两部分的简单拼接,之所以安全,是因为 TLS 会把它连同握手记录一起送入自己的密钥派生流程, 因此请只用它们来构建 TLS 密钥共享,不要用于其他任何用途。 原生 TLS 引擎默认最先提供 X25519MLKEM768, 因此使用该引擎的客户端或服务器无需任何额外代码,就已经在协商后量子密钥交换。

 

混合组仅存在于 TLS 1.3 中。原生引擎同样支持 TLS 1.2,当对端无法使用 TLS 1.3 导致连接回退到 TLS 1.2 时,会使用经典组 X25519、secp256r1 或 secp384r1,不具备后量子防护。若密钥交换必须始终为后量子级别,请将客户端的 TLSOptions.Version 或服务器的 SSLOptions.Version 设置为 tls1_3,并在 NativeTLS_Options.Groups 中只列出混合组。

 

后量子 TLS 演示程序 Demos\18.Cryptography\08.PostQuantum_TLS,提供一个 TLS 版本选择器,可选择 TLS 1.3 和 TLS 1.2、仅 TLS 1.3 或仅 TLS 1.2,还有一个密码套件选择器,其状态栏会显示已协商的版本、组和密码套件。服务端演示程序可以使用 ML-DSA-44 证书签名,该证书只服务于 TLS 1.3 客户端,因为 TLS 1.2 没有 ML-DSA 签名方案。

 


type
  TsgcTLSHybridGroup = (thgX25519MLKEM768, thgSecP256r1MLKEM768, thgSecP384r1MLKEM1024);

function sgcTLSHybrid_GroupID(aGroup: TsgcTLSHybridGroup): Word;
function sgcTLSHybrid_GroupName(aGroup: TsgcTLSHybridGroup): string;
function sgcTLSHybrid_Available(aGroup: TsgcTLSHybridGroup): Boolean;

procedure sgcTLSHybrid_ClientKeyShare(aGroup: TsgcTLSHybridGroup; out aKeyShare, aPrivateState: TBytes);
function sgcTLSHybrid_ServerKeyShare(aGroup: TsgcTLSHybridGroup; const aClientKeyShare: TBytes;
  out aKeyShare, aSharedSecret: TBytes): Boolean;
function sgcTLSHybrid_ClientSharedSecret(aGroup: TsgcTLSHybridGroup;
  const aPrivateState, aServerKeyShare: TBytes; out aSharedSecret: TBytes): Boolean;

 

客户端必须对 aPrivateState 保密,原样传给 sgcTLSHybrid_ClientSharedSecret, 之后再将其清除;它的内部布局不属于 API 的一部分。只要对端的输入无效,这两个 Boolean 函数就会返回 False,并且所有输出均为空,例如密钥共享长度不正确、ML-KEM 封装密钥未通过 FIPS 203 检查、点不在曲线上,或者 X25519 的结果全为零。对于上述每一种情形,RFC 10024 都要求以 illegal_parameter 警报中止连接。

 

所有基于 X25519 构建的内容,也就是 sgcHybrid_ 系列函数、X-Wing 以及 X25519MLKEM768 组,在 SGC_CRYPTO_FIPS 构建中都会被编译排除,参见 FIPS 模式。两个 SecP 组则保留下来, 因为 P-256、P-384 和 ML-KEM 都是获批准的算法。在提供或接受某个组之前,请先调用 sgcTLSHybrid_Available, 而不要想当然地认为它一定存在。

 

ML-DSA(推荐用于签名)

 

签名过程会抽取一个掩码向量并尝试构造签名,一旦某次尝试可能泄露有关密钥的信息就会重新开始; 预期尝试次数不多,但并不固定,因此签名所需时间是可变的。这是该方案本身固有的特性, 并不是针对密钥的侧信道。签名体积较大:ML-DSA-44 为 2420 字节,而 Ed25519 只有 64 字节; 公钥为 1312 字节,而 Ed25519 只有 32 字节,因此在对体积敏感的协议中采用它之前,需要为这一差异预留空间。

 


procedure sgcMLDSA_GenerateKeyPair(aParams: TsgcMLDSAParams; out aPublicKey, aPrivateKey: TBytes);
function sgcMLDSA_Sign(aParams: TsgcMLDSAParams; const aPrivateKey, aMessage, aContext: TBytes): TBytes;
function sgcMLDSA_Verify(aParams: TsgcMLDSAParams; const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;

var
  oPub, oPriv, oSignature, oEmpty: TBytes;
begin
  sgcMLDSA_GenerateKeyPair(mldsa44, oPub, oPriv);
  oSignature := sgcMLDSA_Sign(mldsa44, oPriv, oMessage, oEmpty);
  if sgcMLDSA_Verify(mldsa44, oPub, oMessage, oSignature, oEmpty) then
    { valid };
end;

 

SLH-DSA(保守的备选方案)

 

SLH-DSA 仅依赖哈希函数本身:无论将来任何时候,都不存在需要被攻破的格问题。这一保证代价高昂。 签名大小从最小的 7856 字节(SLH-DSA-SHAKE-128s) 到最大的 49856 字节(SLH-DSA-SHAKE-256f)不等, 使用 s(小型)参数集签名需要经过数十万次哈希运算, 可能耗时数十秒;f(快速)参数集的签名耗时约为一到七秒, 但签名体积大约是前者的两倍。所有参数集的验证速度都很快,这使它更适合“很少签名、经常验证”的场景, 例如固件或代码签名,而不太适合交互式协议。

 


procedure sgcSLHDSA_GenerateKeyPair(aParams: TsgcSLHDSAParams; out aPublicKey, aPrivateKey: TBytes);
function sgcSLHDSA_Sign(aParams: TsgcSLHDSAParams; const aPrivateKey, aMessage, aContext: TBytes;
  aDeterministic: Boolean = True): TBytes;
function sgcSLHDSA_Verify(aParams: TsgcSLHDSAParams; const aPublicKey, aMessage, aSignature, aContext: TBytes): Boolean;

 

密钥格式:SubjectPublicKeyInfo、PKCS#8 与 PEM

 

FIPS 算法所使用的原始字节串并不是一种存储格式,任何其他实现都无法读取它们。 若要把密钥写入磁盘、放进证书,或者交给另一个库,请把它导出为标准容器:公钥使用 X.509 SubjectPublicKeyInfo,私钥使用 PKCS#8 OneAsymmetricKey,两者都可以再套上 PEM 封装(RFC 7468), 标签分别为 PUBLIC KEY 和 PRIVATE KEY。 这三种方案都提供同样的八个函数,每一个都遵循定义其编码方式的那份 RFC。

 

方案 容器 RFC DER 函数 PEM 函数
ML-DSA RFC 9881 sgcMLDSA_ExportSubjectPublicKeyInfo、sgcMLDSA_ImportSubjectPublicKeyInfo、sgcMLDSA_ExportPrivateKeyInfo、sgcMLDSA_ImportPrivateKeyInfo sgcMLDSA_ExportPublicKeyPEM、sgcMLDSA_ImportPublicKeyPEM、sgcMLDSA_ExportPrivateKeyPEM、sgcMLDSA_ImportPrivateKeyPEM
ML-KEM RFC 9935 sgcMLKEM_ExportSubjectPublicKeyInfo、sgcMLKEM_ImportSubjectPublicKeyInfo、sgcMLKEM_ExportPrivateKeyInfo、sgcMLKEM_ImportPrivateKeyInfo sgcMLKEM_ExportPublicKeyPEM、sgcMLKEM_ImportPublicKeyPEM、sgcMLKEM_ExportPrivateKeyPEM、sgcMLKEM_ImportPrivateKeyPEM
SLH-DSA RFC 9909 sgcSLHDSA_ExportSubjectPublicKeyInfo、sgcSLHDSA_ImportSubjectPublicKeyInfo、sgcSLHDSA_ExportPrivateKeyInfo、sgcSLHDSA_ImportPrivateKeyInfo sgcSLHDSA_ExportPublicKeyPEM、sgcSLHDSA_ImportPublicKeyPEM、sgcSLHDSA_ExportPrivateKeyPEM、sgcSLHDSA_ImportPrivateKeyPEM

 

算法标识符携带 NIST 的对象标识符,并且参数部分缺省,因此参数集会随密钥一起传递,导入时会把它返回。 sgcMLDSA_OID、 sgcMLKEM_OID 和 sgcSLHDSA_OID 给出点分形式的字符串,相应的 ParamsFromOID 函数则可以反向映射回参数集。

 

ML-DSA 与 ML-KEM 有三种私钥形式。

导出函数接受一个 aFormat 参数,其类型 TsgcPQCPrivateKeyFormat 声明在 sgcCrypto_DER 中。 默认值 pqkfSeed 只写入种子, ML-DSA 为 32 字节,ML-KEM 为 64 字节的 d || z; 这是两份 RFC 都推荐的形式,而且体积远小于其他形式。 pqkfExpanded 写入展开后的 FIPS 私钥。pqkfBoth 两者都写入,当展开后的密钥并不是该种子展开的结果时会引发异常,这正是两份 RFC 所称的畸形密钥。

 

事后无法再从展开后的密钥还原出种子,因此当您打算存储推荐的形式时,请在生成密钥时就用 sgcMLDSA_GenerateKeyPairAndSeed 或 sgcMLKEM_GenerateKeyPairAndSeed 把它取出来。SLH-DSA 只有一种私钥形式,即 SK.seed || SK.prf || PK.seed || PK.root, 因此它的导出函数没有格式参数。

 


function sgcMLDSA_ExportPrivateKeyPEM(aParams: TsgcMLDSAParams; const aSeed, aPrivateKey: TBytes;
  aFormat: TsgcPQCPrivateKeyFormat = pqkfSeed): string;
function sgcMLDSA_ImportPrivateKeyPEM(const aPEM: string; out aParams: TsgcMLDSAParams;
  out aSeed, aPrivateKey, aPublicKey: TBytes): Boolean;

var
  vParams: TsgcMLDSAParams;
  oSeed, oPub, oPriv: TBytes;
  vPublicPEM, vPrivatePEM: string;
begin
  sgcMLDSA_GenerateKeyPairAndSeed(mldsa65, oSeed, oPub, oPriv);

  vPublicPEM := sgcMLDSA_ExportPublicKeyPEM(mldsa65, oPub);
  vPrivatePEM := sgcMLDSA_ExportPrivateKeyPEM(mldsa65, oSeed, oPriv, pqkfSeed);

  { reading it back: the parameter set comes from the key itself }
  if sgcMLDSA_ImportPrivateKeyPEM(vPrivatePEM, vParams, oSeed, oPriv, oPub) then
    { vParams = mldsa65, and oPriv and oPub are the expansion of oSeed };
end;

 

导入会进行校验,而不是无条件信任。

每个导入函数都返回 False 而不引发异常,并且在以下情况下都会返回 False:DER 畸形、存在多余的尾部数据、 对象标识符未知、本应缺省的参数却出现了,以及密钥长度与参数集不匹配。除结构之外, ML-KEM 公钥还必须通过 FIPS 203 的模数检查,展开后的 ML-KEM 私钥必须通过哈希检查和成对一致性测试, 而展开后的 ML-DSA 私钥会被重新构建出它的公钥,因此其中存储的 t0 和 tr 都必须与之一致。 与私钥不匹配的内嵌公钥会被拒绝。当私钥中保存的是种子时,三个输出都会被填充, 因此调用方无需再单独展开它。

 

sgcSLHDSA_ImportPrivateKeyInfo 和 sgcSLHDSA_ImportPrivateKeyPEM 还接受一个额外的 aVerify 参数,默认值为 True,它会根据私有种子重新计算 PK.root, 并拒绝存储的根值与之不符的密钥。这一次重建的开销相当于一次密钥生成,对 s 参数集来说需要数秒, 因此只有对您自己的应用程序生成的、或者已经信任的密钥才传入 False。 即使它为 False,对象标识符、长度和结构仍然会被检查。

 

库的其余部分读取的正是这些容器。JWT 客户端用来签名的是 PKCS#8 格式的 ML-DSA 私钥,JWT 服务器用来验证的是 SubjectPublicKeyInfo, 而证书与 PKI放入证书或证书签名请求的也正是这两者。

 

选择参数集

 

每一个求取尺寸的函数(sgcMLKEM_PublicKeySize、 sgcMLDSA_SignatureSize、 sgcSLHDSA_SignatureSize 以及各自对应的 PrivateKeySize 函数)都接受与实际操作函数相同的参数枚举,因此协议可以在进行任何密码学运算之前先确定缓冲区大小:

 

参数集 公钥 私钥 密文 / 签名
ML-KEM-5128001632768
ML-KEM-768118424001088
ML-KEM-1024156831681568
ML-DSA-44131225602420
ML-DSA-65195240323309
ML-DSA-87259248964627
SLH-DSA-SHAKE-128s32647856
SLH-DSA-SHAKE-128f326417088
SLH-DSA-SHAKE-192s489616224
SLH-DSA-SHAKE-192f489635664
SLH-DSA-SHAKE-256s6412829792
SLH-DSA-SHAKE-256f6412849856

 

所有尺寸均以字节为单位。无论使用哪种参数集,ML-KEM 的共享密钥始终为 32 字节(sgcMLKEM_SharedSecretSize)。