十分に大きな量子コンピュータが実現すれば、公開鍵暗号ページにある RSA、X25519/X448、そしてすべての曲線上の ECDSA/ECDH は破られてしまいます。これらの数学的基盤はいずれも Shor のアルゴリズムに耐えられません。この問題は、そのようなマシンが今日存在している必要はなく、既に今日から問題になります。攻撃者は今のうちに暗号化された通信を記録しておき、そのようなマシンが実現した時点で復号するという戦略を取ることができ、これは一般に「今のうちに収集し、後で復号する」と呼ばれます。数日ではなく数年にわたって機密を保つ必要があるものは、既にポスト量子鍵交換への移行を進めておくべきです。
NIST は3つの方式を標準化しており、いずれもここに実装されていて、すべて 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 バイトを得ます。3つのパラメータセットがあり、大きいものほどセキュリティマージンが広く、鍵も大きくなります。
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には3つのハイブリッドファミリーが含まれています。上記のsgcHybrid_関数は元々の sgcCrypto 形式であり、互換性のために残されています。X-Wingは標準化された汎用のハイブリッド KEM で、X25519 と ML-KEM-768 を組み合わせたものです。TLS 以外の新しいコードではこれを使用してください。3つ目のファミリーは、TLS 1.3 のハイブリッド鍵共有を構築するためだけのものです。
X-Wing(draft-connolly-cfrg-xwing-kem-10)が保持する秘密は1つだけです。秘密鍵は 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 ではありません。共有秘密は2つの半分を単純に連結したものであり、これが安全なのは、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 1.3 と TLS 1.2」「TLS 1.3 のみ」「TLS 1.2 のみ」を選択肢とする TLS バージョンセレクターと暗号スイートセレクターがあり、ステータス行にはネゴシエートされたバージョン、グループ、暗号スイートが表示されます。サーバーデモは ML-DSA-44 証明書で署名できますが、TLS 1.2 には ML-DSA の署名方式がないため、これは TLS 1.3 のクライアントにのみ提供されます。
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 の一部ではありません。2つの Boolean 関数は、ピアからの入力が不正な場合、すべての出力を空にした上で False を返します。鍵共有の長さが誤っている場合、ML-KEM のカプセル化鍵が FIPS 203 のチェックに失敗した場合、点が曲線上にない場合、X25519 の結果がすべてゼロの場合です。RFC 10024 は、これらのいずれの場合もillegal_parameterアラートで接続を中断することを要求しています。
X25519 を土台とするもの、つまりsgcHybrid_関数、X-Wing、X25519MLKEM768グループは、SGC_CRYPTO_FIPSビルドではコンパイル対象から除外されます。FIPS モードを参照してください。2つの SecP グループは残ります。P-256、P-384、ML-KEM はいずれも承認済みのアルゴリズムだからです。グループが利用できるものと決め付けるのではなく、提示または受け入れる前にsgcTLSHybrid_Availableを呼び出してください。
ML-DSA(署名に推奨)
署名の生成ではマスキングベクトルを引き、署名の構築を試みます。試行が鍵に関する情報を漏らしてしまう場合は再試行が発生し、平均的な試行回数は少ないものの固定ではないため、署名にかかる時間は一定ではありません。これはこの方式に固有のものであり、鍵に関するサイドチャネルではありません。署名は大きく、ML-DSA-44 では 2420 バイトである一方、Ed25519 では 64 バイトです。公開鍵も 1312 バイトに対して 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 はハッシュ関数のみに基づいており、将来のどの時点であっても破られる格子問題は存在しません。ただし、この保証には代償があります。署名のサイズは、最小のもの(SLH-DSA-SHAKE-128s)で 7856 バイトから、最大のもの(SLH-DSA-SHAKE-256f)で 49856 バイトまでの幅があります。s(small)パラメータセットでの署名は数十万回のハッシュ演算を行い、数十秒かかることもあります。一方f(fast)セットは、署名サイズがおよそ2倍になる代わりに1秒からおよそ7秒で署名できます。検証はどのパラメータセットでも高速なため、対話的なプロトコルよりも、ファームウェアやコード署名のように署名はまれで検証が頻繁なユースケースに適しています。
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です。3つの方式はいずれも同じ8つの関数を公開しており、それぞれのエンコードを定める 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 には3つの秘密鍵形式があります。
エクスポート関数は、sgcCrypto_DERで宣言されているTsgcPQCPrivateKeyFormat型のaFormatパラメータを受け取ります。デフォルトの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の1つだけなので、そのエクスポートは形式のパラメータを取りません。
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 を返します。DER が不正な場合、末尾に余分なデータがある場合、オブジェクト識別子が未知の場合、パラメータが省略されるべきところに存在する場合、鍵の長さがパラメータセットと一致しない場合に False を返します。構造以外にも、ML-KEM の公開鍵は FIPS 203 の法チェックに合格しなければならず、展開された ML-KEM の秘密鍵はハッシュチェックとペアワイズ整合性テストに合格しなければならず、展開された ML-DSA の秘密鍵は公開鍵に再構築され、保存されているt0とtrの両方がそれと一致しなければなりません。秘密鍵と一致しない公開鍵が埋め込まれている場合は拒否されます。秘密鍵がシードを保持している場合は3つの出力すべてが埋められて返されるため、呼び出し側が別途展開する必要はありません。
sgcSLHDSA_ImportPrivateKeyInfoとsgcSLHDSA_ImportPrivateKeyPEMは、デフォルトが True であるaVerifyという追加のパラメータを取ります。これは秘密のシードからPK.rootを再計算し、保存されているルートが異なる鍵を拒否します。この再構築には鍵の生成と同じだけのコストがかかり、sパラメータセットでは数秒に及びます。そのため、False を渡すのは自分のアプリケーションが生成した鍵や、既に信頼している鍵に限ってください。False の場合でも、オブジェクト識別子、長さ、構造は引き続きチェックされます。
ライブラリの他の部分が読み取るのはこれらのコンテナです。JWTクライアントが署名に使用するのは PKCS#8 の ML-DSA 秘密鍵であり、JWT サーバーが検証に使用するのは SubjectPublicKeyInfo です。また、証明書 & PKIが証明書や証明書署名リクエストに入れるのもこの両方です。
パラメータセットの選択
サイズを取得する各関数(sgcMLKEM_PublicKeySize、sgcMLDSA_SignatureSize、sgcSLHDSA_SignatureSizeと、それぞれのPrivateKeySize版)は、操作そのものと同じパラメータの列挙型を受け取るため、実際の暗号処理を行う前にプロトコルがバッファのサイズを決定できます。
| パラメータセット | 公開鍵 | 秘密鍵 | 暗号文 / 署名 |
| ML-KEM-512 | 800 | 1632 | 768 |
| ML-KEM-768 | 1184 | 2400 | 1088 |
| ML-KEM-1024 | 1568 | 3168 | 1568 |
| ML-DSA-44 | 1312 | 2560 | 2420 |
| ML-DSA-65 | 1952 | 4032 | 3309 |
| ML-DSA-87 | 2592 | 4896 | 4627 |
| SLH-DSA-SHAKE-128s | 32 | 64 | 7856 |
| SLH-DSA-SHAKE-128f | 32 | 64 | 17088 |
| SLH-DSA-SHAKE-192s | 48 | 96 | 16224 |
| SLH-DSA-SHAKE-192f | 48 | 96 | 35664 |
| SLH-DSA-SHAKE-256s | 64 | 128 | 29792 |
| SLH-DSA-SHAKE-256f | 64 | 128 | 49856 |
すべてのサイズはバイト単位です。ML-KEM の共有秘密は、パラメータセットにかかわらず常に 32 バイトです(sgcMLKEM_SharedSecretSize)。