一覧SC-07-3 · 公開鍵と電子署名

X25519による鍵共有と鍵導出

公開値、共有値、導出鍵がどの計算で結ばれるかを説明します。

説明

鍵交換では、双方が自分の秘密値と相手の公開値を使い、同じ共有値を計算します。共有値をそのまま何にでも使うのではなく、用途や接続の情報を含む鍵導出関数から実際の鍵を作ります。

RFCのX25519試料で双方の共有値を照合し、HKDFで送信・受信向けの鍵を作ります。公開値のすり替えと一時鍵の変更も試し、相手の認証が必要な理由を確認します。

公開値、共有値、導出鍵がどの計算で結ばれるかを説明します。

1双方の公開値を表示する

AliceとBobは異なる32バイトの秘密試料から公開値を計算します。秘密値そのものを送信する必要はありません。

from sc07 import * alice,bob = participants()独立した二者の試料鍵を用意する ap = alice.public_key() bp = bob.public_key() print(ap.public_bytes_raw()Aliceの公開値を表示する.hex()) print(bp.public_bytes_raw().hex())

それぞれ32バイトの公開値です。これらを交換して共有値を求めます。

2Alice側で共有値を計算する

自分の秘密値とBobの公開値を使います。

shared_a = alice.exchange(bp)Aliceの秘密値とBobの公開値から計算する print(shared_a.hex())

この32バイトはまだアプリの暗号化鍵ではなく、鍵導出への入力です。

3Bob側の計算と照合する

Bobは自分の秘密値とAliceの公開値を使います。

shared_b = bob.exchange(ap)Bob側で共有値を計算する print(shared_b.hex()) print(shared_a == shared_b双方の結果を比較する)

同じ共有値になりました。秘密値を送信しなくても、双方の計算が一致します。

4用途ラベルを入れて鍵を導出する

HKDF-SHA-256で16バイトを導出します。この演習では公開saltを32ゼロバイトとし、infoで方向を分けます。

send = derive(shared_a,b"demo/client-to-server")送信方向の鍵を導出する receive = derive(shared_a,b"demo/server-to-client")反対方向の鍵を導出する print(send.hex(),receive.hex(),send == receive)

同じ共有値でもinfoが違うため別の鍵になります。文字列の綴りまで双方で合意する必要があります。

5相手側で同じ方向の鍵を計算する

Bobがクライアントから受け取る鍵には、同じclient-to-serverのラベルを使います。

print(derive(shared_b,b"demo/client-to-server")Bob側で同じ方向の鍵を導出する == send) print(derive(shared_b,b"demo/client-to-server ") == send)

正しいラベルではTrue、末尾空白を加えるとFalseです。ローカルな送受信名ではなくプロトコルで定めた方向を使います。

6相手の公開値をすり替える

第三者の試料鍵を用意し、Bobの公開値の代わりに渡します。

mallory = x25519.X25519PrivateKey.from_private_bytes(bytes(range(32))) substituted = alice.exchange(mallory.public_key())すり替えた公開値で共有値を計算する print(substituted == shared_a) print(substituted == mallory.exchange(ap)第三者側の計算と照合する)

Bobとの共有値とは違い、第三者との計算には一致します。鍵交換の計算成功だけでは、相手がBobだと確認できません。

7新しい一時鍵で共有値を作る

次の接続では別の一時鍵を使う例です。ここでは再現用の固定試料で比較します。

fresh = x25519.X25519PrivateKey.from_private_bytes(bytes(range(32,64))) next_shared = fresh.exchange(bp)新しい一時鍵から共有値を計算する print(next_shared == shared_a) print(next_shared == bob.exchange(fresh.public_key())相手側の値と照合する)

新しい共有値も双方で一致し、前の値とは異なります。前方秘匿性には一時秘密値の適切な生成・破棄と、認証されたプロトコル全体の設計が必要です。

まとめ

秘密値と相手の公開値から、双方が同じ共有値を求めます。

用途や方向を含む鍵導出で鍵を分けます。

公開値の相手を認証しなければ、鍵交換だけでなりすましを防げません。

操作履歴と解説を振り返れます。

Python 3.15 / cryptography 50.0.2 / 公開試料と固定時刻

1
from sc07 import * alice,bob = participants() ap = alice.public_key() bp = bob.public_key() print(ap.public_bytes_raw().hex()) print(bp.public_bytes_raw().hex())
8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a
de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f
2
shared_a = alice.exchange(bp) print(shared_a.hex())
4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742
3
shared_b = bob.exchange(ap) print(shared_b.hex()) print(shared_a == shared_b)
4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742
True
4
send = derive(shared_a,b"demo/client-to-server") receive = derive(shared_a,b"demo/server-to-client") print(send.hex(),receive.hex(),send == receive)
44d94c8a864980210e1c30eb4e5cf370 86751dade466eb345bd31904eec9f48a False
5
print(derive(shared_b,b"demo/client-to-server") == send) print(derive(shared_b,b"demo/client-to-server ") == send)
True
False
6
mallory = x25519.X25519PrivateKey.from_private_bytes(bytes(range(32))) substituted = alice.exchange(mallory.public_key()) print(substituted == shared_a) print(substituted == mallory.exchange(ap))
False
True
7
fresh = x25519.X25519PrivateKey.from_private_bytes(bytes(range(32,64))) next_shared = fresh.exchange(bp) print(next_shared == shared_a) print(next_shared == bob.exchange(fresh.public_key()))
False
True

演習完了