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 / 公開試料と固定時刻
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
shared_a = alice.exchange(bp)
print(shared_a.hex())4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742
shared_b = bob.exchange(ap)
print(shared_b.hex())
print(shared_a == shared_b)4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742 True
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
print(derive(shared_b,b"demo/client-to-server") == send)
print(derive(shared_b,b"demo/client-to-server ") == send)True False
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
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
演習完了