鍵導出関数(KDF)の原理:HKDF の extract-and-expand
1本の共有鍵から用途別の鍵を量産する仕組みを原理から理解できます。HKDF の extract と expand を分けて押さえれば、salt と info の役割や TLS 1.3 の鍵スケジュールが腑に落ちます。
- 1.KDF は偏った鍵素材(DH 共有値など)から、暗号用途にそのまま使える固定長の擬似ランダム鍵を1つ以上導出する。HKDF は HMAC を土台に extract(抽出)と expand(伸長)の2段で構成される。
- 2.extract は salt を鍵、入力鍵素材 IKM をメッセージとして HMAC を回し、エントロピーを均した固定長の擬似ランダム鍵 PRK へ凝縮する。expand は PRK と info ラベルから任意長の出力鍵を反復生成する。
- 3.info によるコンテキスト分離で、同じ PRK から導いた「暗号鍵」と「IV」が衝突せず独立になる。TLS 1.3 の鍵スケジュールは HKDF-Extract と HKDF-Expand-Label の連鎖で全鍵を生成する。
KDF が解く問題:鍵素材は鍵ではない
鍵交換の結果として手に入る生の値は、そのまま暗号鍵には使えません。たとえば Diffie–Hellman の共有値 g^(ab) は、攻撃者から秘匿されてはいても、ビットの分布が一様ではない(特定ビットに偏りや構造が残る)。一方 AES や AEAD が前提とするのは「全ビットが一様ランダムに見える固定長の鍵」です。この溝を埋めるのが **KDF(Key Derivation Function/鍵導出関数)**で、役割は2つあります。
- 平滑化:偏った・構造を持つ入力鍵素材(IKM: Input Keying Material)から、統計的に一様な擬似ランダム鍵を取り出す。
- 伸長と分割:1つの素材から、暗号鍵・IV・MAC 鍵といった複数の独立な鍵を必要な長さだけ作り分ける。
同じ「KDF」でも、Argon2 や PBKDF2 のようなパスワードハッシュは低エントロピーなパスワードを総当たりから守るために、わざと計算を重くします。対して HKDF が扱う IKM は DH 共有値のように既に高エントロピーで、目的は速い平滑化と分割。だから HKDF は意図的な低速化(ストレッチング)を一切しません。両者を取り違えると、パスワードに HKDF を使って脆弱になる、あるいは鍵分割に Argon2 を使って無駄に遅くする、という事故になります。
extract-and-expand:なぜ2段に分けるのか
HKDF(RFC 5869)の設計思想は、KDF の仕事を性質の異なる2フェーズに明確に分離したことです。土台はすべて HMAC(ハッシュ関数を鍵付きで二重に回す MAC)で、HMAC の擬似ランダム関数(PRF)としての性質に安全性を帰着させます。
| フェーズ | やること | 入力 → 出力 |
|---|---|---|
| Extract(抽出) | 偏った IKM のエントロピーを均し、固定長の擬似ランダム鍵に凝縮 | salt, IKM → PRK(ハッシュ長の固定値) |
| Expand(伸長) | PRK から用途別ラベルごとに任意長の鍵を反復生成 | PRK, info, L → OKM(Lバイト) |
分離の利点は、入力が既に一様な場合は extract を省ける点にあります。乱数生成器の出力のように IKM が初めから擬似ランダムなら、平滑化は不要で expand だけ回せばよい。逆に DH 共有値のように偏りがある場合だけ extract を挟む。1つの関数に押し込めず2段にしたことで、状況に応じた最適化と、各段の安全性証明の切り分けが可能になりました。
Extract:エントロピーを HMAC で凝縮する
extract フェーズは驚くほど単純で、HMAC をそのまま使います。salt を HMAC の鍵に、IKM をメッセージに据えるだけです。
HKDF-Extract(salt, IKM):
PRK = HMAC-Hash(key = salt, message = IKM)
return PRK # 出力は Hash の出力長(SHA-256 なら 32 バイト)
ここでの HMAC は「メッセージ認証」のためではなく、ランダム性抽出器(randomness extractor)として働きます。IKM のどこにエントロピーが偏って分布していても、HMAC の撹拌で全 PRK ビットへ均され、結果は一様ランダムと区別できない固定長 PRK になります。salt が秘密である必要はありません。salt は抽出器を「ランダムに選ぶ」ための公開ソルトで、特定の IKM 分布に対する最悪ケースを避け、抽出の安定性を高める役割です。
salt を渡さない場合、HKDF は salt をハッシュ長分のゼロ列で代用します(RFC 5869 の既定動作)。これでも extract は機能しますが、可能ならランダムまたはセッション固有の salt を与える方が、抽出器の質が理論上向上します。TLS 1.3 では各段の salt に「前段の鍵スケジュール値」を流し込み、ハンドシェイクの履歴を鍵に織り込みます。salt は秘密鍵ではないので、攻撃者に知られても安全性は崩れません。
Expand:PRK を info ラベルで伸長・分割する
expand フェーズは、固定長の PRK から任意長 L バイトの出力鍵 OKM を作ります。ここでも中身は HMAC で、前ブロックを次の入力にフィードバックする反復(カウンタ付き)です。
HKDF-Expand(PRK, info, L):
N = ceil(L / HashLen) # 必要ブロック数
T(0) = 空文字列
T(i) = HMAC-Hash(PRK, T(i-1) || info || byte(i)) # i = 1..N
OKM = (T(1) || T(2) || ... || T(N)) の先頭 L バイト
return OKM
各ブロック T(i) は前ブロック T(i-1) と info、そして 1 バイトのカウンタ i を連結して HMAC にかけます。前ブロックを混ぜるフィードバックにより、出力ブロック間に独立性が生まれます。L が大きくてもブロックを継ぎ足すだけで伸ばせますが、カウンタが 1 バイトのため最大 255 ブロック(SHA-256 なら 255×32 = 8160 バイト)という上限がある点は実装上の注意です。
info の本質:コンテキスト分離
HKDF を「ただの鍵伸長」ではなく実用的なものにしているのが info パラメータです。info は用途を識別するラベルで、同じ PRK からでも info が違えば暗号的に独立した別の鍵が出ます。
同じ PRK から3つの鍵を作り分ける:
enc_key = HKDF-Expand(PRK, info = "app data key", 32)
iv = HKDF-Expand(PRK, info = "app data iv", 12)
mac_key = HKDF-Expand(PRK, info = "app data mac", 32)
# info が違えば HMAC 入力が変わり、出力同士に相関は生じない
このコンテキスト分離(domain separation)が無いと、ある用途の鍵を別用途に流用したときに鍵が衝突・干渉し、片方の漏洩が他方を脅かす「クロスプロトコル攻撃」の温床になります。info にプロトコル名・バージョン・用途・場合によってはセッション識別子を埋め込むことで、「この鍵はこの文脈でしか出てこない」ことを暗号的に保証できます。salt がエントロピーの抽出器を選ぶのに対し、info は出力の名前空間を分けるものだと整理すると役割が明確になります。
salt は extract に渡してエントロピー抽出の質を左右し、info は expand に渡して出力鍵の用途を分けます。投入するフェーズも目的も別物です。とくに「複数の鍵を1回の expand でまとめて切り出す」場合(例:64 バイト出力を前半 32 バイトを暗号鍵、後半 32 バイトを MAC 鍵に分ける)は、それら全体が1つの info コンテキストに属することを意識する必要があります。用途ごとに完全な独立性が欲しいなら、info を分けて expand を別々に呼ぶのが安全側です。
TLS 1.3 の鍵スケジュール:extract と expand の連鎖
HKDF が最も体系的に使われているのが TLS 1.3 の鍵スケジュールです。ハンドシェイクの各段で HKDF-Extract と、info を構造化した HKDF-Expand-Label を交互に連鎖させ、すべてのトラフィック鍵を導出します。
TLS 1.3 鍵スケジュール(概念):
Early Secret = HKDF-Extract(salt = 0, IKM = PSK or 0)
│ Derive-Secret(..., "derived")
Handshake Secret = HKDF-Extract(salt = ↑derived, IKM = (EC)DHE 共有値)
│ Derive-Secret(..., "derived")
Master Secret = HKDF-Extract(salt = ↑derived, IKM = 0)
各 Secret から HKDF-Expand-Label で用途別鍵を導出:
c hs traffic / s hs traffic / c ap traffic / s ap traffic ...
ポイントは3つです。第一に、各段の extract の IKM が異なること——PSK、DH 共有値、ゼロを段階的に注入し、利用可能な秘密を漏れなく鍵に織り込みます。第二に、前段の出力を Derive-Secret(..., "derived") で変換して次段の salt に渡すことで、鍵スケジュール全体が一本の鎖になり、履歴を改ざんすると以降の鍵が総崩れになります。第三に、HKDF-Expand-Label の info にはラベル文字列とハンドシェイクのトランスクリプトハッシュが入り、そのハンドシェイクでしか同じ鍵が出ないよう束縛されます。
TLS 1.3 は expand の info をそのまま生文字列にせず、length(2バイト) || "tls13 " + label || context という構造化バイト列に固定します(RFC 8446)。ラベルに必ず tls13 接頭辞を付けることで、TLS の鍵導出が他プロトコルの HKDF 利用と名前空間レベルで分離されます。これは前述のコンテキスト分離を、プロトコル設計として徹底した例です。
まとめ
KDF は「秘匿されてはいるが鍵としては使えない素材」を、用途別の一様ランダム鍵へ変換する道具です。HKDF はその仕事を **extract(salt を鍵に HMAC でエントロピーを凝縮し PRK を作る)**と **expand(PRK と info ラベルから任意長の鍵を反復生成する)**の2段に分け、入力が既に一様なら extract を省ける柔軟性を得ました。salt はエントロピー抽出器を選ぶ公開値、info は出力鍵の名前空間を分けるラベル——この役割分担を押さえると、TLS 1.3 が HKDF-Extract と HKDF-Expand-Label を連鎖させて全鍵を1本の鎖から導く設計が一貫して見えてきます。
鍵導出の土台となる HMAC とハッシュ構造は ハッシュ関数の内部構造、導出された鍵を消費する側は AEAD の設計原理、鍵交換で IKM を生む仕組みは Diffie–Hellman 鍵交換 を合わせて参照してください。
セキュリティ Article
鍵導出関数(KDF)の原理:HKDF の extract-and-expandを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
暗号
比較で見る軸
難易度: advanced / カテゴリ: セキュリティ / タグ数: 5
導入後に効く点
extract は salt を鍵、入力鍵素材 IKM をメッセージとして HMAC を回し、エントロピーを均した固定長の擬似ランダム鍵 PRK へ凝縮する。expand は PRK と info ラベルから任意長の出力鍵を反復生成する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- セキュリティ
- タグ数
- 5
判断チェックリスト
- 自社の用途が「暗号 / KDF」に近いか確認する。
- 強みである「KDF は偏った鍵素材(DH 共有値など)から、暗号用途にそのまま使える固定長の擬似ランダム鍵を1つ以上導出する。HKDF は HMAC を土台に extract(抽出)と expand(伸長)の2段で構成される。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。