Go 言語(以下 golang)で、お互いの公開鍵から共通鍵暗号用の共通の秘密鍵を作成したい。
つまり「相手の公開鍵」と「私の秘密鍵」をゴニョゴニョして作成された鍵が、相手も「私の公開鍵」と「相手の秘密鍵」でゴニョゴニョして作成された鍵と同じであれば、2 人の「共通の秘密鍵」として利用できるということです。
GPG(GnuPG)でも作成できる鍵で、GitHub に登録できるアルゴリズム(RSA, ElGamal, DSA, ECDH, ECDSA, EdDSA)のうち、 ECDH(楕円曲線ディフィー・ヘルマン鍵共有)を Golang で利用したい。ついでに Golang で ECDH の鍵ペアも作成したい。
- ECDSA 公開鍵から DH 鍵交換で共通鍵を作成する @ Qiita
- ECDH 公開鍵から DH 鍵交換で共通鍵を作成する ← いまここ
ここでは、相手の公開鍵は安全な方法で取得していると仮定します。
というのも、DH 鍵交換であっても中間者攻撃はありえるからです。例えば、取得した相手の鍵の URL が攻撃者に乗っ取られており、相手も、自分も実は攻撃者と共通秘密鍵を作っていたというケースです。
「本当に相手の鍵であるか」は、直面して(物理接触で)公開鍵を事前に交換しておくのが理想です。しかし、オンラインの場合は、複数のソースから総合的に判断することが重要です。CA を利用したり、公開鍵を OpenPGP のキーサーバー や GitHub に登録 しておき、Keyoxide や Keybase などの活用も検討すると良いでしょう。
TL; DR (今北産業)
import "crypto/ecdh"
// Alice と Bob が事前に合意した共通パラメーター。この例では NIST P384 の
// 曲線を使用する、とする。
// 選択肢は: P256, P384, P521, X25519
// (悩んだら X25519 が良い。文末の「P521 vs X25519?」参照)
paramCommon := ecdh.P384()
import "crypto/rand"
// --------------------------------------------------------------
// Alice の公開鍵暗号のペア鍵作成
// --------------------------------------------------------------
alicePrivKey, err := paramCommon.GenerateKey(rand.Reader)
panicOnErr(err)
fmt.Printf("Alice Priv: %x\n", alicePrivKey.Bytes())
alicePubKey := alicePrivKey.PublicKey()
fmt.Printf("Alice Pub: %x\n", alicePubKey.Bytes())
// --------------------------------------------------------------
// Bob の公開鍵暗号のペア鍵作成
// --------------------------------------------------------------
bobPrivKey, err := paramCommon.GenerateKey(rand.Reader)
panicOnErr(err)
fmt.Printf("Bob Priv: %x\n", bobPrivKey.Bytes())
bobPubKey := bobPrivKey.PublicKey()
fmt.Printf("Bob Pub: %x\n", bobPubKey.Bytes())
// 作成例 (作成するごとに変わる):
// Alice Priv: 0fd7557d2dfd4dedc0b659ac8416333f97900cb1dab53720a66c7adea2b36f88e5a3f3eef490f9b48e66a7d8112c6f3d
// Alice Pub: 044806f4f84614189cc049e282696661516309f75ef20fbaf0cebd2fd4fc2bc2b3f19a25f550a2baefc350861ba470e0fee36478986571e46ba98862a7b2c55b904fc97079b899ec1195052f309d693598ea1ec13d53a6540b8ce640403f6a899d
// Bob Priv: 1ab739460d9e9b18fa5f5494c7667d4f929bb50e79144a6e5dc9549134fe7e1b4ac3eb0af926ab957c019ec16ba1be08
// Bob Pub: 04b742944773613e81b8b84c940e0a6b70d5b1ce9f453311a289bddff8812ed463fed7d246c5915bf777f209a2057ad27afe2dd2cd149d52f499bad1f085f92c590c5004878cdc621056010906b5e3ebf8b7fe3da3a46efee8bd1c09c44d7cf345
// Alice が、自分の秘密鍵と相手(Bob)の公開鍵で 2 者間の共通秘密鍵(aliceSecret)
// を作成。
aliceSecret, err := alicePrivKey.ECDH(bobPubKey)
panicOnErr(err)
fmt.Printf("Alice's Shared Secret: %x\n", aliceSecret)
// Bob が、自分の秘密鍵と相手(Alice)の公開鍵で 2 者間の共通秘密鍵(aliceSecret)
// を作成。
bobSecret, err := bobPrivKey.ECDH(alicePubKey)
panicOnErr(err)
fmt.Printf("Bob's Shared Secret : %x\n", bobSecret)
// 作成例:
// Alice's Shared Secret: f2d46357b7deab81ea3c068e8c3430ab03f3851c6e6dd7790e26e831bcae73b307d69950e22b5677205aafcdc13c138b
// Bob's Shared Secret : f2d46357b7deab81ea3c068e8c3430ab03f3851c6e6dd7790e26e831bcae73b307d69950e22b5677205aafcdc13c138b
- オンラインで動作をみる @ GoPlayground
P521 vs X25519?
ECDH の鍵交換には X25519 を選ぶべし
上記「Alice と Bob の事前合意」で、パラーメータの選択肢にある P256 や P384 などの数値は、内部の計算で使われる素数(メルセンヌ素数)を定めたものです。
- P256 = $2^{256} – 2^{244} + 2^{192} + 2^{96} – 1$
- P384 = $2^{384} – 2^{128} – 2^{96} + 2^{32} – 1$
- P521 = $2^{521} – 1$
- X25519 = $2^{255} - 19$
そして "crypto/ecdh
" のパッケージは、以下の仕様を元に実装されています。
- P256 〜 P521:
- NIST が 2009 年に定めた FIPS 186-3, Section D.2.3 〜 D.2.5
- X25519:
- IETF が 2016 年に定めた RFC 7748, Section 5
上記の、仕様の制定者と制定年に注目ください。前者の FIPS 186-3 は 2009 年に制定されたものの、2013 年に後続の FIPS-186 シリーズ(Digital Signature Standard, デジタル署名標準)にバトンを渡しました。
2024/03 現在、FIPS 186-5(2023/02 制定)が最新なのですが、そこでは Ed25519 がデジタル署名アルゴリズムとして選ばれています。
Golang では Go v1.13 移行から "crypto/ed25519" パッケージで標準実装されていますが、あくまでもデジタル署名を行うためのパッケージに特化しているので、鍵共有の関数は持っていません。
では、同じ 25519
を持つ Ed25519
を使ったものが ECDH の X25519
なのかと言うと 25519
の数値が同じだけで別ものです。
X25519
と Ed25519
の共通点と違い
- 共通点
- どちらも Curve25519 と呼ばれる楕円曲線を利用している
- 違い
以上から、特別な事情がない限り、これから DH 鍵交換を "crypto/ecdh
" のパッケージで利用する場合は X25519
一択ということになります。