Chapter 11
鍵合意 — 相手と秘密を共有する
この章のゴール.
psa_raw_key_agreement()とpsa_key_derivation_key_agreement()で ECDH を実行でき、 なぜ生の共有秘密をそのまま鍵にしてはいけないのかを説明でき、 「一時鍵 + 署名」の組み合わせで前方秘匿性と認証を両立した鍵交換を設計できるようになること。この章で使う既出の用語(定義は各リンク先). PSA(01 章 1 節)、ビット(02 章 4 節)、操作オブジェクト(02 章 5 節)、algorithm(03 章 11 節)、type(03 章 11 節)、usage(03 章 11 節)、DER(04 章 2 節)、generate(04 章 10 節)、import(04 章 10 節)、乱数(05 章 1 節)、AES(07 章 1 節)、ノンス(08 章 1 節)、認証(09 章 1 節)、HKDF(10 章)、salt(10 章 3 節)、パスワード(10 章 2 節)、鍵導出関数(10 章 1 節)
1. 鍵合意とは何か
これまでの章では「共有鍵がすでにある」前提だった。 鍵合意(key agreement)は、盗聴されている経路で、盗聴者に知られない共有秘密を作る仕組みである。 現代の実装はほぼすべて ECDH(Elliptic Curve Diffie-Hellman、楕円曲線ディフィー・ヘルマン)である。
手順は対称的で単純である。
- 自分は鍵ペア (a, A) を持つ。a が秘密鍵、A = a·G が公開鍵(G は曲線の基準点)
- 相手も鍵ペア (b, B) を持つ
- 公開鍵 A と B を交換する(盗聴されてよい)
- 自分は a·B を、相手は b·A を計算する。a·B = a·b·G = b·a·G = b·A なので同じ点になる
- その点の x 座標が共有秘密(P-256 で 32 バイト)
盗聴者は A と B を知っているが、a も b も知らないので a·b·G を計算できない(離散対数問題——公開鍵から秘密鍵の整数を逆算するのが計算量的に不可能であること)。
2. 2 つの関数
/* 生の共有秘密をバイト列で取り出す */
psa_status_t psa_raw_key_agreement(psa_algorithm_t alg,
psa_key_id_t private_key,
const uint8_t *peer_key, size_t peer_key_length,
uint8_t *output, size_t output_size, size_t *output_length);
/* 共有秘密を鍵導出操作の SECRET として直接注入する(外に出さない) */
psa_status_t psa_key_derivation_key_agreement(psa_key_derivation_operation_t *op,
psa_key_derivation_step_t step,
psa_key_id_t private_key,
const uint8_t *peer_key, size_t peer_key_length);psa_raw_key_agreement | psa_key_derivation_key_agreement | |
|---|---|---|
| 結果 | 共有秘密のバイト列(アプリケーションのメモリ) | 鍵導出操作の内部(外に出ない) |
| 鍵の属性 algorithm | PSA_ALG_ECDH | PSA_ALG_KEY_AGREEMENT(PSA_ALG_ECDH, PSA_ALG_HKDF(PSA_ALG_SHA_256)) |
| 使う場面 | プロトコルが「生の共有秘密」を別の計算に使う(TLS ライブラリの内部など) | 通常はこちら。共有秘密から直接セッション鍵を導出する |
どちらの場合も、共有秘密をそのまま AES 鍵にしてはいけない。 ECDH の出力は楕円曲線上の点の x 座標であり、一様な乱数ではない(特定のビットに偏りがある)。 また「同じ相手と同じ鍵ペアなら毎回同じ値」になる。 必ず KDF(鍵導出関数、10 章)を通して、用途ごとの鍵にする。 psa_key_derivation_key_agreement() はこの規則を API の形で強制している——共有秘密は KDF の操作オブジェクトにしか入らない。
引数の peer_key は相手の公開鍵のバイト列(04 章の形式。P-256 なら 04 ∥ X ∥ Y の 65 バイト)であり、鍵 ID ではない。 相手の公開鍵を import する必要はない。
3. 典型コード — ECDH + HKDF でセッション鍵
psa_status_t make_session_key(psa_key_id_t my_priv, /* 自分の ECDH 秘密鍵 */
const uint8_t *peer_pub, size_t peer_len, /* 相手の公開鍵 65 B */
const uint8_t *salt, size_t salt_len, /* 両者の乱数を連結したもの等 */
psa_key_id_t *session)
{
psa_key_derivation_operation_t op = PSA_KEY_DERIVATION_OPERATION_INIT;
psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT;
psa_status_t st;
psa_set_key_type(&attr, PSA_KEY_TYPE_AES);
psa_set_key_bits(&attr, 256);
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_ENCRYPT | PSA_KEY_USAGE_DECRYPT);
psa_set_key_algorithm(&attr, PSA_ALG_GCM);
st = psa_key_derivation_setup(&op, PSA_ALG_KEY_AGREEMENT(PSA_ALG_ECDH, 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_key_agreement(&op, PSA_KEY_DERIVATION_INPUT_SECRET,
my_priv, peer_pub, peer_len); /* ← ECDH がここで走る */
if (st) goto cleanup;
st = psa_key_derivation_input_bytes(&op, PSA_KEY_DERIVATION_INPUT_INFO,
(const uint8_t *)"app-session-v1", 14);
if (st) goto cleanup;
st = psa_key_derivation_output_key(&attr, &op, session);
cleanup:
psa_key_derivation_abort(&op);
psa_reset_key_attributes(&attr);
return st;
}自分の ECDH 鍵ペアの属性:
psa_set_key_type(&attr, PSA_KEY_TYPE_ECC_KEY_PAIR(PSA_ECC_FAMILY_SECP_R1));
psa_set_key_bits(&attr, 256);
psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_DERIVE);
psa_set_key_algorithm(&attr, PSA_ALG_KEY_AGREEMENT(PSA_ALG_ECDH, PSA_ALG_HKDF(PSA_ALG_SHA_256)));
psa_generate_key(&attr, &my_priv);
psa_export_public_key(my_priv, my_pub, sizeof my_pub, &my_pub_len); /* 相手に送る */鍵の属性の algorithm は、setup に渡すアルゴリズムと完全に一致していなければならない。 鍵に PSA_ALG_ECDH だけを設定して PSA_ALG_KEY_AGREEMENT(ECDH, HKDF) の操作に使うと NOT_PERMITTED になる。 逆に psa_raw_key_agreement() には PSA_ALG_ECDH を渡し、鍵の属性も PSA_ALG_ECDH にする。
4. 曲線の選択
| 鍵種別 | 曲線 | 公開鍵の長さ | 特徴 |
|---|---|---|---|
ECC_KEY_PAIR(SECP_R1) 256 ビット | P-256 | 65 バイト(04 ∥ X ∥ Y) | 標準。TLS、Matter、多くの規格が要求 |
ECC_KEY_PAIR(MONTGOMERY) 255 ビット | X25519 | 32 バイト(u 座標) | 高速、実装が単純、サイドチャネルに強い。Signal(メッセンジャー)、WireGuard(VPN)、TLS 1.3 で標準 |
ECC_KEY_PAIR(SECP_R1) 384 ビット | P-384 | 97 バイト | 高い安全性が要る規格(CNSA) |
X25519 は署名に使えない(署名は Ed25519 という別の曲線・別の鍵)。 「ECDH は X25519、署名は Ed25519」という組み合わせは、両方とも Curve25519 系だが鍵は別に作る。
5. 一時鍵と前方秘匿性
同じ鍵ペアを使い続けると、その秘密鍵が将来漏れたとき、過去の通信がすべて復号できる。 これを防ぐのが前方秘匿性(forward secrecy)であり、方法は単純で、通信のたびに新しい鍵ペアを作って捨てる(一時鍵、ephemeral key)。
psa_generate_key(&attr, &eph); /* 揮発鍵 */
psa_export_public_key(eph, pub, ...); /* 送る */
make_session_key(eph, peer_pub, ...);
psa_destroy_key(eph); /* 直ちに捨てる */P-256 の鍵生成は Cortex-M4 で数十 ms であり、通信ごとに作っても問題にならないことが多い。
6. 認証の問題 — ECDH だけでは「相手が誰か」分からない
ECDH は「盗聴者に知られない秘密」を作るが、通信相手が本物かどうかは確認しない。 中間者(攻撃者)が双方に対して自分の公開鍵を送れば、両側と別々に鍵合意が成立し、すべてを盗聴・改ざんできる。
解決は、一時公開鍵に署名を付けることである(09 章)。
- 装置は長期の署名鍵(永続鍵、装置内で生成、公開鍵はサーバに登録済み)を持つ
- 通信開始時に一時 ECDH 鍵ペアを作る
- 一時公開鍵(と相手の乱数)に長期署名鍵で署名して送る
- 相手は登録済みの公開鍵で署名を検証してから、ECDH を行う
これが TLS のハンドシェイクの骨格であり、「署名鍵は長期・永続、ECDH 鍵は一時・揮発」という役割分担は、ほとんどのプロトコルで共通である。
もう 1 つの方法は静的 ECDH——装置の長期 ECDH 鍵ペアの公開鍵をサーバに登録しておき、鍵合意の結果を知っていること自体を認証にする(Noise プロトコル——軽量な鍵交換の枠組み——や一部の IoT 規格)。 署名が不要で軽いが、前方秘匿性を得るには一時鍵との組み合わせ(2 回の ECDH)が要る。
7. ハイブリッド暗号化 — 公開鍵で大きなデータを送る
RSA-OAEP(09 章)は 190 バイトしか暗号化できない。 大きなデータを公開鍵で送るときの定石は、
- 受信者の公開鍵に対して一時 ECDH を行い、共有秘密から AES 鍵を導出する
- その AES 鍵で AEAD でデータを暗号化する
- 一時公開鍵 + ノンス + 暗号文 + タグを送る
受信者は自分の秘密鍵と届いた一時公開鍵で同じ AES 鍵を導出し、復号する。 これは ECIES または HPKE(RFC 9180)と呼ばれる構成で、PSA の関数(generate_key → key_derivation_key_agreement → output_key → aead_encrypt)だけで組める。 PSA Crypto API 1.3 には HPKE そのものは入っていない(実装側の拡張として提供されることがある)。
8. 手を動かす
鍵合意で同じ秘密ができるのを見る
9. 仕様書に逃がす
| 関数・定数 | 用途 |
|---|---|
PSA_ALG_FFDH | 有限体(楕円曲線でない古典的な)Diffie-Hellman。PSA_KEY_TYPE_DH_KEY_PAIR(PSA_DH_FAMILY_RFC7919) と組む。古い規格の互換 |
PSA_RAW_KEY_AGREEMENT_OUTPUT_SIZE(type, bits) / PSA_RAW_KEY_AGREEMENT_OUTPUT_MAX_SIZE | 共有秘密のバッファ見積もり |
psa_key_agreement() | 1.2 で追加。共有秘密を鍵として直接作る(KDF を伴わない場合)。psa_raw_key_agreement の鍵 ID 版 |
psa_pake_* | PAKE(パスワード認証鍵交換)。SPAKE2+(Matter のコミッショニング)、EC-JPAKE(Thread)。1.1 の拡張。専用の操作オブジェクト psa_pake_operation_t と psa_pake_cipher_suite_t を使う。使うのは Matter / Thread スタックの内部で、アプリケーションから直接呼ぶことは少ない |
この章のポイント
- 鍵合意は盗聴されても知られない共有秘密を作る。実装は ECDH(P-256 または X25519)
- 共有秘密をそのまま鍵にしない。
psa_key_derivation_key_agreement()で KDF に直接注入するのが標準 - 鍵の属性の algorithm は操作のアルゴリズムと完全一致(
KEY_AGREEMENT(ECDH, HKDF(SHA_256))) - 通信ごとに一時鍵を作って捨てる(前方秘匿性)。署名鍵は長期、ECDH 鍵は一時
- ECDH は相手を認証しない。一時公開鍵に署名を付ける
- 大きなデータの公開鍵暗号化は「ECDH + KDF + AEAD」のハイブリッドで