DelphiでML-DSAを使ってJWTに署名する

· コンポーネント
DelphiでML-DSAを使ってJWTに署名する

JWTは小さなものですが、その影響範囲は広いものです。サービス間でのアイデンティティの証明であり、それに署名するアルゴリズムはシステムの中で最も変更に時間がかかる部分になりがちです。発行者と検証者のすべてが同時に移行しなければならないからです。だからこそ、必要になる前にその選択肢が存在することを知っておく価値があります。

sgcWebSockets 2026.10は、RFC 9964の耐量子JWTアルゴリズムであるML-DSA-44ML-DSA-65ML-DSA-87をJWTクライアントおよびサーバーに追加します。これらはHS、RS、ESと並び、同じプロパティに追加された3つの値として位置づけられ、トークンに関するそれ以外の部分はすべて従来どおりです。

トークンの発行

ヘッダーでアルゴリズムを選択し、コンポーネントに秘密鍵を渡して署名します。

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が定義する3つの形式すべてが読み込めるため、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には、あえて明言しておく価値のある規則が1つあります。そうでなければ見過ごされてしまう間違いだからです。それは、鍵はalgが指定するパラメータセットに属していなければならないということです。一致しない鍵は拒否されます。誤ったパラメータセットで署名すると例外が発生し、誤ったパラメータセットで検証すると例外ではなくfalseが返されるため、攻撃者はその違いから何かを読み取ることはできません。

JSON Web Keyとしての鍵

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は、中途半端に読み込まれるのではなく拒否されます。

実証された相互運用性

署名フォーマットは、他の誰かが同じバイト列を生成できて初めて意味を持ちます。次の2つの確認は、単なる主張ではなくテストとして提供されています。

ついでに: バイナリのHMACシークレット

同じ作業の中で、無関係な制約も1つ解消されました。HSアルゴリズムは鍵をテキストとして受け取っていましたが、実際のHMAC鍵はランダムなバイト列であり、その大部分は有効なテキストとしては成立しません。そこで、oct JWKのkメンバーと同じエンコーディングであるbase64urlとして鍵を受け取る、2つ目のプロパティが用意されました。

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

これが設定されている場合、クライアントとサーバーの両方でテキストのシークレットより優先されます。

他者が書いたクレーム

他の発行者からのトークンを受け入れる場合に知っておく価値のある修正がもう1つあります。JSON文字列は生のUTF-8として書かれることもあれば、非ASCII文字をすべてエスケープして書かれることもあり、ライブラリによって選択が異なります。現在はどちらの形式も同じテキストに解決されるため、アクセント付きの名前はどの発行者が生成したものでも正しく読み取られ、エスケープで書かれたalgヘッダーも、それが表すアルゴリズムとして正しく認識されます。

アップグレード

この3つのアルゴリズムは既存のリストの末尾に追加されたものであるため、保存済みの序数はその意味を保ち、これまで行っていたことは何も変わりません。ML-DSAでの署名にはsgcCryptoパックが必要で、それを含まないビルドでは、不明瞭な失敗ではなく明確なメッセージとともにそれらのトークンを拒否します。

次に読む

動画で見る

eSeGeCe チャンネルにこの内容の短い動画があります。

質問、フィードバック、移行に関するサポートが必要ですか?お問い合わせください—コードを書いた本人から返信が届きます。