在 Delphi 中使用 ML-DSA 签署 JWT

· 组件
在 Delphi 中使用 ML-DSA 签署 JWT

JWT 是一个很小的东西,却影响深远。它是你各个服务之间身份的证明,而对它签名的算法往往是系统中最难更改的部分,因为每一个签发方和每一个验证方都必须同时切换。正因如此,在真正需要之前就了解这个选项的存在是值得的。

sgcWebSockets 2026.10 将 RFC 9964 的后量子 JWT 算法添加到 JWT 客户端和服务器:ML-DSA-44ML-DSA-65ML-DSA-87。它们作为同一属性的三个新增取值,与 HS、RS 和 ES 并列,而你令牌的其他一切都保持不变。

签发令牌

在头部选择算法,为组件提供一个私钥,然后签名:

oJWT := TsgcHTTP_JWT_Client.Create(nil);
oJWT.JWTOptions.Header.alg := jwtMLDSA65;
oJWT.JWTOptions.Algorithms.MLDSA.PrivateKey.Text := vPrivatePEM;
oJWT.JWTOptions.Payload.iss := 'my-service';
oJWT.JWTOptions.Payload.sub := 'user-1';

vToken := oJWT.Sign;

私钥是一个普通的 PKCS#8 PEM。RFC 9881 定义的三种形式都可以被读取,因此无论密钥以 32 字节种子形式存储、以展开密钥形式存储,还是两者兼有,都可以正常工作。

验证令牌

服务器端的形式相同,只是启用的是公钥和对应的算法族:

oServer := TsgcHTTP_JWT_Server.Create(nil);
oServer.JWTOptions.Algorithms.MLDSA.Enabled := True;
oServer.JWTOptions.Algorithms.MLDSA.PublicKey.Text := vPublicPEM;

if oServer.Validate(vToken, vHeader, vPayload, vError) then
  ShowMessage(vPayload);

每个算法族都是单独开关的,因此只签发 ML-DSA 令牌的部署可以关闭其他算法族,而当收到的令牌 alg 被设置为其他值时,会被直接拒绝而不是被检查。

参数集必须匹配

RFC 9964 中有一条规则值得明确指出,因为这是一个原本很容易被忽视的错误:密钥必须属于 alg 所指定的参数集。不匹配的密钥会被拒绝。用错误的密钥签名会引发异常,而用错误的密钥验证则会返回 false 而不是引发异常,因此攻击者无法利用这种差异获取任何信息。

以 JSON Web Keys 形式存在的密钥

RFC 9964 为这些算法注册了一种密钥类型,AKP,库可以对其进行读写,当密钥是发布在 JWKS 端点而不是放在文件里时,这正是你需要的:

uses
  sgcHTTP_JWT_MLDSA;

var
  vJWK, vPublicPEM, vPrivatePEM: string;
begin
  // {"kty":"AKP","alg":"ML-DSA-65","pub":"...","priv":"..."}
  vJWK := sgcMLDSA_ExportPrivateJWK(mldsa65, oSeed);

  // and back again, as the PEM the components read
  sgcMLDSA_ImportJWKAsPEM(vJWK, vPublicPEM, vPrivatePEM);
end;

私有成员是 RFC 所定义的 32 字节种子,读取器对此要求严格:各成员必须是长度恰好正确的规范 base64url 编码,alg 是必需的,并且当存在私钥时,它必须与同一个 JWK 中的公钥相符。任何一项不满足的 JWK 都会被拒绝,而不是被部分加载。

已验证的互操作性

一种签名格式只有在别人也能生成相同字节时才有用。以下两项检查以测试的形式提供,而不仅仅是口头声明:

顺带一提:二进制 HMAC 密钥

在同一轮改动中,一个不相关的限制也被消除了。HS 算法此前将密钥作为文本接收,而真正的 HMAC 密钥是随机字节,其中大部分根本不是合法文本。现在有了第二个属性,可以将密钥以 base64url 形式提供,这与 oct JWK 中 k 成员所使用的编码相同:

oJWT.JWTOptions.Algorithms.HS.SecretBase64URL := 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ...';

一旦设置了它,无论在客户端还是服务器端,它都会优先于文本密钥。

他人写入的声明(Claims)

如果你接受来自其他签发方的令牌,还有一个修复值得了解。一个 JSON 字符串既可以写成原始 UTF-8,也可以将每个非 ASCII 字符都转义,不同的库会做出不同的选择。现在这两种形式都会解析为相同的文本,因此带重音符号的名字无论由哪个签发方生成都能被正确读取,而以转义形式写出的 alg 头部也会被识别为其所表示的算法。

升级

这三种算法是添加在现有列表末尾的新增项,因此已存储的序号含义保持不变,你目前所做的任何事情都不会受到影响。使用 ML-DSA 签名需要 sgcCrypto 包,如果构建中缺少该包,会以明确的信息拒绝这些令牌,而不是以令人费解的方式失败。

延伸阅读

观看视频

eSeGeCe 频道上有一个关于此内容的简短视频

有疑问、反馈,或者需要迁移方面的帮助?与我们联系—你会收到编写这些代码的人的回复。