Chapter 10
鍵導出 — 1 つの秘密から、用途別の鍵を作る
この章のゴール.
psa_key_derivation_*の操作オブジェクトを使って HKDF で鍵を導出でき、 「秘密・ソルト・情報」の 3 つの入力の役割を説明でき、output_bytesとoutput_keyの使い分け、および PBKDF2 でパスワードから鍵を作る方法を理解すること。この章で使う既出の用語(定義は各リンク先). PSA(01 章 1 節)、操作オブジェクト(02 章 5 節)、algorithm(03 章 11 節)、type(03 章 11 節)、usage(03 章 11 節)、DER(04 章 2 節)、ハッシュ(05 章 1 節)、乱数(05 章 1 節)、CMAC(06 章 1 節)、AES(07 章 1 節)、カウンタ(08 章 4 節)、認証(09 章 1 節)
1. なぜ鍵を「導出」するのか
暗号の原則に「1 つの鍵は 1 つの用途に」がある(03 章)。 しかし装置が持てる秘密は多くない——工場で書き込まれた 1 つのマスター秘密、あるいは鍵合意(次章)で得た 1 つの共有秘密である。
鍵導出関数(KDF、Key Derivation Function)は、 1 つの秘密から、用途ごとに異なる、互いに関係の分からない複数の鍵を決定論的に作る。 「送信用の AES 鍵」「受信用の AES 鍵」「MAC 鍵」を 1 つの共有秘密から作り、どれか 1 つが漏れても他は分からない。
もう 1 つの役割は「素材を鍵の形に整える」ことである。 ECDH の共有秘密(次章)は数学的な構造を持つ 32 バイトで、そのまま AES 鍵に使うのは推奨されない。 KDF を通すことで、一様な乱数に見える鍵になる。
2. アルゴリズム
| アルゴリズム定数 | 入力 | 用途 |
|---|---|---|
PSA_ALG_HKDF(hash) | SECRET、SALT(任意)、INFO | 標準(RFC 5869)。共有秘密やマスター鍵から鍵を作る |
PSA_ALG_HKDF_EXTRACT(hash) / PSA_ALG_HKDF_EXPAND(hash) | HKDF の前半 / 後半だけ | TLS 1.3 のように 2 段階を別々に呼ぶプロトコル |
PSA_ALG_PBKDF2_HMAC(hash) | PASSWORD、SALT、COST(反復回数) | パスワードから鍵を作る。遅くすることで総当たりを防ぐ |
PSA_ALG_PBKDF2_AES_CMAC_PRF_128 | 同上 | AES しかない環境の PBKDF2 |
PSA_ALG_TLS12_PRF(hash) / PSA_ALG_TLS12_PSK_TO_MS(hash) | SEED、SECRET、LABEL | TLS 1.2 の PRF(擬似乱数関数)。ライブラリの内部。ライブラリが使う |
PSA_ALG_KEY_AGREEMENT(ka, kdf) | 鍵合意 + 導出(次章) | ECDH の結果を直接 KDF に入れる |
HKDF-SHA256 を覚えれば、日常の 9 割は足りる。
3. HKDF の 3 つの入力
HKDF は 2 段階で動く。
| 入力 | PSA のステップ定数 | 役割 | 秘密か | 省略できるか |
|---|---|---|---|---|
| IKM(入力鍵素材) | PSA_KEY_DERIVATION_INPUT_SECRET | 元になる秘密(マスター鍵、共有秘密) | 秘密 | 不可 |
| salt(ソルト) | PSA_KEY_DERIVATION_INPUT_SALT | 抽出を「かき混ぜる」乱数。同じ IKM でも salt が違えば別の鍵になる | 公開でよい | 可(省略時はゼロ列) |
| info(情報、文脈) | PSA_KEY_DERIVATION_INPUT_INFO | 用途を表すラベル。"tx-key"、"rx-key"、"mac-key" など。これが鍵を分ける | 公開 | 可だが、必ず入れる |
info に用途を書くことが、「1 つの秘密から独立な複数の鍵」を実現する仕組みそのものである。 info = "device-A→server / encrypt / v1" のように、方向・用途・バージョンを文字列で入れる。 これを入れ忘れると、送信鍵と受信鍵が同じ値になり、自分の送ったメッセージを自分で復号できる(反射攻撃)。
4. 操作オブジェクトの使い方
鍵導出は、他の分割操作と少し違う形をしている。 update ではなく input_* で素材を入れ、finish ではなく output_* で結果を取り出す。 output は何回でも呼べ、呼ぶたびに続きのバイト列が出てくる。
psa_key_derivation_operation_t op = PSA_KEY_DERIVATION_OPERATION_INIT;
psa_status_t psa_key_derivation_setup(psa_key_derivation_operation_t *op, psa_algorithm_t alg);
psa_status_t psa_key_derivation_input_bytes(psa_key_derivation_operation_t *op,
psa_key_derivation_step_t step,
const uint8_t *data, size_t data_length);
psa_status_t psa_key_derivation_input_key(psa_key_derivation_operation_t *op,
psa_key_derivation_step_t step, psa_key_id_t key);
psa_status_t psa_key_derivation_input_integer(psa_key_derivation_operation_t *op,
psa_key_derivation_step_t step, uint64_t value);
psa_status_t psa_key_derivation_set_capacity(psa_key_derivation_operation_t *op, size_t capacity);
psa_status_t psa_key_derivation_output_bytes(psa_key_derivation_operation_t *op,
uint8_t *output, size_t output_length);
psa_status_t psa_key_derivation_output_key(const psa_key_attributes_t *attributes,
psa_key_derivation_operation_t *op, psa_key_id_t *key);
psa_status_t psa_key_derivation_abort(psa_key_derivation_operation_t *op);手順:
setup(&op, PSA_ALG_HKDF(PSA_ALG_SHA_256))- 入力を決まった順序で入れる。HKDF は SALT → SECRET → INFO の順(salt は secret より前でなければならない。info は secret の後)
- 秘密は
input_key()で鍵 IDとして入れるのが本来の形(PSA_KEY_TYPE_DERIVEとPSA_KEY_USAGE_DERIVEを持つ鍵) input_bytes()で生のバイト列を入れることもできるが、秘密の値がアプリケーションのメモリに存在することになる
- 秘密は
output_key()で鍵として取り出す(属性を指定。値はアプリケーションに渡らない)か、output_bytes()でバイト列として取り出すabort()
psa_status_t derive_session_keys(psa_key_id_t master, const uint8_t *salt, size_t salt_len,
psa_key_id_t *tx_key, psa_key_id_t *rx_key)
{
psa_key_derivation_operation_t op = PSA_KEY_DERIVATION_OPERATION_INIT;
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
psa_status_t st;
/* 取り出す鍵の属性: AES-128-GCM、暗号化専用 */
psa_set_key_type(&attr, PSA_KEY_TYPE_AES);
psa_set_key_bits(&attr, 128);
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_ENCRYPT);
psa_set_key_algorithm(&attr, PSA_ALG_GCM);
st = psa_key_derivation_setup(&op, PSA_ALG_HKDF(PSA_ALG_SHA_256)); if (st) goto cleanup;
st = psa_key_derivation_input_bytes(&op, PSA_KEY_DERIVATION_INPUT_SALT, salt, salt_len); if (st) goto cleanup;
st = psa_key_derivation_input_key(&op, PSA_KEY_DERIVATION_INPUT_SECRET, master); if (st) goto cleanup;
st = psa_key_derivation_input_bytes(&op, PSA_KEY_DERIVATION_INPUT_INFO,
(const uint8_t *)"session-tx", 10); if (st) goto cleanup;
st = psa_key_derivation_output_key(&attr, &op, tx_key); if (st) goto cleanup;
psa_key_derivation_abort(&op);
/* 受信鍵は info を変えてもう一度 */
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_DECRYPT);
st = psa_key_derivation_setup(&op, PSA_ALG_HKDF(PSA_ALG_SHA_256)); if (st) goto cleanup;
st = psa_key_derivation_input_bytes(&op, PSA_KEY_DERIVATION_INPUT_SALT, salt, salt_len); if (st) goto cleanup;
st = psa_key_derivation_input_key(&op, PSA_KEY_DERIVATION_INPUT_SECRET, master); if (st) goto cleanup;
st = psa_key_derivation_input_bytes(&op, PSA_KEY_DERIVATION_INPUT_INFO,
(const uint8_t *)"session-rx", 10); if (st) goto cleanup;
st = psa_key_derivation_output_key(&attr, &op, rx_key);
cleanup:
psa_key_derivation_abort(&op);
psa_reset_key_attributes(&attr);
return st;
}if (st) goto cleanup; は PSA_SUCCESS が 0 であることを利用した短縮形である。
1 つの操作から 2 つの鍵を続けて取り出す方法.
上の例では操作を 2 回作り直しているが、1 つの操作から
output_key()を 2 回呼んで、続きのバイト列を別の鍵にすることもできる。 HKDF の出力は 1 本の長いバイト列であり、最初の 16 バイトを tx 鍵、次の 16 バイトを rx 鍵とするのは TLS 1.2 の流儀である。 ただしその場合 info は共通になるので、「用途ごとに info を変える」設計と「1 本の出力を切り分ける」設計のどちらかに統一する。 前者のほうが、プロトコルの仕様書に書きやすい。
5. output_key と output_bytes の使い分け
psa_key_derivation_output_key() | psa_key_derivation_output_bytes() | |
|---|---|---|
| 結果 | 鍵 ID(値は PSA の中) | バイト列(アプリケーションのメモリ) |
| 使う場面 | 導出した鍵を PSA で使う(AEAD、MAC、次の導出) | プロトコルが「バイト列」を要求する(IV、TLS の鍵ブロック、他ライブラリに渡す) |
| 安全性 | 高い(値が外に出ない) | 値がメモリに出る。使ったら memset で消す |
秘密の入力が input_key のとき | 可 | 可(ただし秘密が鍵 ID で入力された場合、実装によっては NOT_PERMITTED。03 章の VERIFY_DERIVATION 参照) |
原則は output_key である。 output_bytes は「PSA の外に渡す必要があるとき」だけに限る。
容量(capacity)——1 つの操作から取り出せる合計バイト数には上限がある(HKDF-SHA256 で 255 × 32 = 8160 バイト)。 psa_key_derivation_set_capacity() で上限を自分で狭めることもでき、「この操作からは 32 バイトしか出させない」という安全装置になる。 上限を超えて取り出そうとすると PSA_ERROR_INSUFFICIENT_DATA。
6. PBKDF2 — パスワードから鍵を作る
人間が覚えるパスワードは、エントロピー(予測の難しさ)が低い。 HKDF でそのまま鍵にすると、攻撃者は辞書の単語を片端から試せる(1 秒に数百万回)。
PBKDF2(Password-Based KDF 2)は、HMAC を 何万回も反復することで 1 回の試行を意図的に遅くする。
psa_key_derivation_setup(&op, PSA_ALG_PBKDF2_HMAC(PSA_ALG_SHA_256));
psa_key_derivation_input_integer(&op, PSA_KEY_DERIVATION_INPUT_COST, 100000); /* 反復回数。先に入れる */
psa_key_derivation_input_bytes(&op, PSA_KEY_DERIVATION_INPUT_SALT, salt, 16); /* 装置ごとの乱数。保存する */
psa_key_derivation_input_bytes(&op, PSA_KEY_DERIVATION_INPUT_PASSWORD, pw, pw_len); /* または input_key で PASSWORD 型の鍵 */
psa_key_derivation_output_key(&attr, &op, &key);- 入力順は COST → SALT → PASSWORD
- 反復回数(COST)は「装置で 0.5 秒くらいかかる値」が目安。PC なら 60 万回、Cortex-M4 では 1 万〜数万回が現実的
- ソルトは装置ごとに乱数で作り、パスワードハッシュと一緒に保存する(同じパスワードでも装置ごとに違う鍵になる。事前計算表を無効化する)
- パスワードの検証は、導出した鍵と保存値を比べる。
psa_key_derivation_verify_bytes()/verify_key()を使うと一定時間で比較できる(PSA_KEY_USAGE_VERIFY_DERIVATIONが要る)
7. 設計での使い方
7.1 装置ごとの鍵を、工場のマスター鍵から
工場で装置ごとに別の鍵を書き込むのは手間である。 代わりに、1 つのマスター鍵 + 装置のシリアル番号(個体ごとの製造番号)を info にした HKDFで装置鍵を作れば、サーバ側は同じ計算でどの装置の鍵も再現できる。 ただしマスター鍵がサーバから漏れると全装置が危ないので、マスター鍵は HSM(ハードウェアセキュリティモジュール)に置く。 装置側は、装置鍵だけを永続鍵として持ち、マスター鍵を持たない。
7.2 セッション鍵の更新
通信開始時に共有秘密(鍵合意、次章)から HKDF でセッション鍵を作り、 一定回数の通信ごとに info にカウンタを入れて作り直す。 「鍵の寿命」(08 章)の問題を構造的に解く。
7.3 やってはいけないこと
- ハッシュ 1 回(
SHA-256(secret ∥ "tx"))で済ませる。HKDF の extract を飛ばすと、秘密に偏りがあるとき安全性が落ちる - info を省略する
- パスワードを HKDF に入れる(PBKDF2 を使う)
output_bytesの出力を消さずに放置する
8. 手を動かす
HKDF の入力を変えて出力を見る
9. 仕様書に逃がす
| 関数・定数 | 用途 |
|---|---|
psa_key_derivation_verify_bytes() / psa_key_derivation_verify_key() | 導出結果を既知の値と一定時間で比較(パスワード検証) |
psa_key_derivation_get_capacity() | 残り容量の取得 |
PSA_KEY_DERIVATION_INPUT_LABEL / SEED / CONTEXT / OTHER_SECRET | TLS 1.2 PRF や SP 800-108 用のステップ |
PSA_ALG_SP800_108_COUNTER_HMAC(hash) / PSA_ALG_SP800_108_COUNTER_CMAC | NIST SP 800-108 の KDF(1.2 で追加)。一部の規格が要求 |
PSA_ALG_TLS12_ECJPAKE_TO_PMS | Thread / Matter(スマートホームの無線規格)のコミッショニング(新しい機器をネットワークに登録する手順。EC-JPAKE というパスワード認証方式を使う)の内部 |
PSA_KEY_DERIVATION_UNLIMITED_CAPACITY | 容量の既定値 |
この章のポイント
- KDF は 1 つの秘密から、用途ごとに独立な複数の鍵を作る。標準は HKDF-SHA256
- 入力は SALT → SECRET → INFO の順。info に用途を書くことが鍵を分ける仕組み
- 秘密は
input_key()で鍵 ID として入れ、結果はoutput_key()で鍵として取り出す。バイト列にするのは PSA の外に渡すときだけ - パスワードは HKDF ではなく PBKDF2(COST → SALT → PASSWORD)
- 容量(capacity)で取り出せる量に安全装置をかけられる