PSA APIs 10 · 鍵導出 — 1 つの秘密から、用途別の鍵を作る

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 つの秘密から、用途ごとに独立な鍵を作るマスター秘密共有秘密、工場の秘密HKDFsalt, info を変えて送信用 AES 鍵info = "tx"受信用 AES 鍵info = "rx"MAC 鍵info = "mac"どれか 1 つが漏れても他は分からない。もう 1 つの役割は「素材を一様な鍵の形に整える」こと
鍵導出関数は、秘密を増やさずに「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、LABELTLS 1.2 の PRF(擬似乱数関数)。ライブラリの内部。ライブラリが使う
PSA_ALG_KEY_AGREEMENT(ka, kdf)鍵合意 + 導出(次章)ECDH の結果を直接 KDF に入れる

HKDF-SHA256 を覚えれば、日常の 9 割は足りる。

3. HKDF の 3 つの入力

HKDF は 2 段階で動く。

\[ \text{PRK} = \text{HMAC}(\text{salt},\ \text{IKM}) \qquad \text{(extract: 素材を均す)} \]
\[ \text{OKM} = \text{HMAC}(\text{PRK},\ \text{info} \,\|\, 0x01) \,\|\, \text{HMAC}(\text{PRK},\ T_1 \,\|\, \text{info} \,\|\, 0x02) \,\|\, \cdots \qquad \text{(expand: 必要な長さに伸ばす)} \]
入力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" のように、方向・用途・バージョンを文字列で入れる。 これを入れ忘れると、送信鍵と受信鍵が同じ値になり、自分の送ったメッセージを自分で復号できる(反射攻撃)。

HKDF の 2 段階 — extract と expandIKM(SECRET)元の秘密salt(SALT)公開の乱数。省略可extractPRK = HMAC(salt, IKM)PRKexpandHMAC(PRK, info ∥ i)info(INFO)用途のラベルOKM必要な長さの鍵素材PSA の入力順: SALT → SECRET → INFOsalt は secret より前、info は secret より後。順序を違えると BAD_STATE。info に「方向・用途・バージョン」を必ず書く
extract で偏りをならし、expand で用途ラベルを混ぜて必要な長さに伸ばす

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);

手順:

  1. setup(&op, PSA_ALG_HKDF(PSA_ALG_SHA_256))
  2. 入力を決まった順序で入れる。HKDF は SALT → SECRET → INFO の順(salt は secret より前でなければならない。info は secret の後)
    • 秘密は input_key() で鍵 IDとして入れるのが本来の形(PSA_KEY_TYPE_DERIVE と PSA_KEY_USAGE_DERIVE を持つ鍵)
    • input_bytes() で生のバイト列を入れることもできるが、秘密の値がアプリケーションのメモリに存在することになる
  3. output_key() で鍵として取り出す(属性を指定。値はアプリケーションに渡らない)か、output_bytes() でバイト列として取り出す
  4. 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 本の出力を切り分ける」設計のどちらかに統一する。 前者のほうが、プロトコルの仕様書に書きやすい。

鍵導出の操作オブジェクト — input で入れて output で取り出すsetup(op, HKDF(SHA_256))input_bytes(SALT, …)公開の乱数input_key(SECRET, master)鍵 ID で。値を出さないinput_bytes(INFO, "tx")用途ラベルoutput_key(attr, op, &key)鍵として。原則こちらoutput_bytes(op, buf, n)バイト列。PSA の外に渡すときだけ(続けて output 可)同じ操作から続きが出るabort容量(capacity)1 つの操作から取り出せる合計に上限(HKDF-SHA256 で 8160 バイト)。set_capacity で狭めれば安全装置になる。超えると INSUFFICIENT_DATA
他の分割操作と違い、update/finish ではなく input/output。output は何回でも呼べる

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);
PBKDF2 — パスワードには「遅い」KDF を使うパスワードエントロピーが低いsalt(装置ごとの乱数)保存しておくHMAC を COST 回反復10 万回 → 1 回の試行が 0.5 秒鍵(または保存用の検証値)verify_bytes で一定時間比較入力順: COST → SALT → PASSWORD。HKDF にパスワードを入れてはいけない(辞書攻撃に 1 秒で数百万回)
反復回数で総当たりを遅くし、ソルトで事前計算表を無効化する

7. 設計での使い方

7.1 装置ごとの鍵を、工場のマスター鍵から

工場で装置ごとに別の鍵を書き込むのは手間である。 代わりに、1 つのマスター鍵 + 装置のシリアル番号(個体ごとの製造番号)を info にした HKDFで装置鍵を作れば、サーバ側は同じ計算でどの装置の鍵も再現できる。 ただしマスター鍵がサーバから漏れると全装置が危ないので、マスター鍵は HSM(ハードウェアセキュリティモジュール)に置く。 装置側は、装置鍵だけを永続鍵として持ち、マスター鍵を持たない。

7.2 セッション鍵の更新

通信開始時に共有秘密(鍵合意、次章)から HKDF でセッション鍵を作り、 一定回数の通信ごとに info にカウンタを入れて作り直す。 「鍵の寿命」(08 章)の問題を構造的に解く。

7.3 やってはいけないこと

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_SECRETTLS 1.2 PRF や SP 800-108 用のステップ
PSA_ALG_SP800_108_COUNTER_HMAC(hash) / PSA_ALG_SP800_108_COUNTER_CMACNIST SP 800-108 の KDF(1.2 で追加)。一部の規格が要求
PSA_ALG_TLS12_ECJPAKE_TO_PMSThread / Matter(スマートホームの無線規格)のコミッショニング(新しい機器をネットワークに登録する手順。EC-JPAKE というパスワード認証方式を使う)の内部
PSA_KEY_DERIVATION_UNLIMITED_CAPACITY容量の既定値

この章のポイント