PSA APIs 11 · 鍵合意 — 相手と秘密を共有する

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、楕円曲線ディフィー・ヘルマン)である。

手順は対称的で単純である。

  1. 自分は鍵ペア (a, A) を持つ。a が秘密鍵、A = a·G が公開鍵(G は曲線の基準点)
  2. 相手も鍵ペア (b, B) を持つ
  3. 公開鍵 A と B を交換する(盗聴されてよい)
  4. 自分は a·B を、相手は b·A を計算する。a·B = a·b·G = b·a·G = b·A なので同じ点になる
  5. その点の x 座標が共有秘密(P-256 で 32 バイト)

盗聴者は A と B を知っているが、a も b も知らないので a·b·G を計算できない(離散対数問題——公開鍵から秘密鍵の整数を逆算するのが計算量的に不可能であること)。

ECDH — 公開鍵を交換し、同じ点を計算する自分相手鍵ペア (a, A = a·G) を生成鍵ペア (b, B = b·G) を生成A を送る(盗聴されてよい)B を送るa·B = a·b·Gb·A = b·a·G= 同じ点盗聴者は A と B を知るが a も b も知らないので a·b·G を出せない(離散対数問題)
秘密鍵どうしを掛けた点が一致する。x 座標が共有秘密になる

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_agreementpsa_key_derivation_key_agreement
結果共有秘密のバイト列(アプリケーションのメモリ)鍵導出操作の内部(外に出ない)
鍵の属性 algorithmPSA_ALG_ECDHPSA_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 にする。

ECDH の結果を KDF に直接注入するsetup(op, KEY_AGREEMENT(ECDH, HKDF))input_bytes(SALT, 両者の乱数)key_agreement(op, SECRET, my_priv, peer_pub, 65)ECDH がここで走る。共有秘密は外に出ないinput_bytes(INFO, "app-session-v1")output_key(attr, op, &session)AES-256-GCM 鍵としてabort鍵の属性の algorithm は setup のアルゴリズムと完全一致鍵に PSA_ALG_ECDH だけを設定して KEY_AGREEMENT(ECDH, HKDF) の操作に使うと NOT_PERMITTED
psa_raw_key_agreement は共有秘密をバイト列で返すが、通常はこちらの経路を使う

4. 曲線の選択

鍵種別曲線公開鍵の長さ特徴
ECC_KEY_PAIR(SECP_R1) 256 ビットP-25665 バイト(04 ∥ X ∥ Y)標準。TLS、Matter、多くの規格が要求
ECC_KEY_PAIR(MONTGOMERY) 255 ビットX2551932 バイト(u 座標)高速、実装が単純、サイドチャネルに強い。Signal(メッセンジャー)、WireGuard(VPN)、TLS 1.3 で標準
ECC_KEY_PAIR(SECP_R1) 384 ビットP-38497 バイト高い安全性が要る規格(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 章)。

  1. 装置は長期の署名鍵(永続鍵、装置内で生成、公開鍵はサーバに登録済み)を持つ
  2. 通信開始時に一時 ECDH 鍵ペアを作る
  3. 一時公開鍵(と相手の乱数)に長期署名鍵で署名して送る
  4. 相手は登録済みの公開鍵で署名を検証してから、ECDH を行う

これが TLS のハンドシェイクの骨格であり、「署名鍵は長期・永続、ECDH 鍵は一時・揮発」という役割分担は、ほとんどのプロトコルで共通である。

認証付き鍵交換 — 一時鍵 + 長期署名鍵装置サーバ一時 ECDH 鍵ペア生成(揮発)一時公開鍵 ∥ 乱数 ∥ 長期鍵による署名登録済み公開鍵で署名を検証 → 本物の装置サーバの一時公開鍵 ∥ 署名検証 → ECDH → HKDF → セッション鍵同じ計算 → 同じセッション鍵一時鍵は使い終わったら destroy(前方秘匿性)。署名鍵は長期・永続
ECDH だけでは中間者を防げない。一時公開鍵に長期鍵で署名して相手を認証する(TLS の骨格)

もう 1 つの方法は静的 ECDH——装置の長期 ECDH 鍵ペアの公開鍵をサーバに登録しておき、鍵合意の結果を知っていること自体を認証にする(Noise プロトコル——軽量な鍵交換の枠組み——や一部の IoT 規格)。 署名が不要で軽いが、前方秘匿性を得るには一時鍵との組み合わせ(2 回の ECDH)が要る。

7. ハイブリッド暗号化 — 公開鍵で大きなデータを送る

RSA-OAEP(09 章)は 190 バイトしか暗号化できない。 大きなデータを公開鍵で送るときの定石は、

  1. 受信者の公開鍵に対して一時 ECDH を行い、共有秘密から AES 鍵を導出する
  2. その AES 鍵で AEAD でデータを暗号化する
  3. 一時公開鍵 + ノンス + 暗号文 + タグを送る

受信者は自分の秘密鍵と届いた一時公開鍵で同じ 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 スタックの内部で、アプリケーションから直接呼ぶことは少ない

この章のポイント