Develop with pleasure!

福岡でCloudとかBlockchainとか。

BitcoinのP2P層の通信を暗号化するBIP-324

これまでBitcoinP2Pレイヤーの通信は暗号化されておらず平文でメッセージがやりとりされている。基本的にBitcoinの場合、ブロックやトランザクションなどのデータは誰もが共有する台帳データで機密性のあるデータではないから。

ただ、リレーされるデータ自体は公開データであるものの、平文の通信には以下のような課題もある:

  • ノードのP2P接続を観察可能なプレーヤー(ISPなど)に対して、トランザクションソースやタイミングに関する情報を与えることになる。
  • 平文なので途中でデータの改ざんリスクがあり、その検出も難しい。
  • 接続時に固有のマジックバイトで通信が始まるのでBitcoinP2P接続であることを簡単に識別することができる。

BIP-324では、これらに対処するためトランスポート層の通信を暗号化するv2トランスポートプロトコルを定義している。v2トランスポートプロトコルを利用すると、ネットワーク上のデータは一様にランダムなデータと見分けがつかなくなる*1

Bitcoin Coreもv26.0からこのv2トランスポートプロトコルの実験的なサポートを開始している(デフォルトでは無効、また当然接続相手もv2をサポートしている必要がある)。

v2トランスポート

従来のv1トランスポートプロトコルでは、接続先のノードとのTCP接続を行うとすぐに、version/verackメッセージの交換を始めるけど、v2トランスポートプロトコルでは、最初にハンドシェイクを実行して鍵交換を行う。

ハンドシェイク

v2トランスポートを使用する場合、2つのノードは接続時に以下のハンドシェイクを実行する。

1.5往復の通信でハンドシェイクは完了する。

ElligatorSwift公開鍵

ハンドシェイク時にお互いに送信する公開鍵は、Bitcoinでよく使われるsecp256k1鍵のエンコード方式ではなく、ElligatorSwiftという方式でエンコードされている。

鍵交換の際に送信する公開鍵は当然暗号化されていないのと、公開鍵自体は楕円曲線上の点で、その座標は曲線の方程式を満たす必要がありランダムなデータにはならないため、ネットワーク上の通信を見ればそれが公開鍵であるか分析できる可能性がある。そのため、この公開鍵をランダムなバイトストリームにしかみえないようにするために、ElligatorSwiftというエンコード方式を採用している。

Elligator/Elligator 2と同じように、二種類の写像を利用する。

  • 直接写像
    楕円曲線の有限体上の値rを入力として、その楕円曲線上の点P = (u, t)を出力する。
  • 写像
    点Pを入力として、有限体上の値rを出力する関数。

BIP-324ではハンドシェイク時に生成したsecp256k1の一時鍵の公開鍵のx座標を入力として、↑のElligatorSwiftエンコードした公開鍵(64バイト)を取得するようになってる。この公開鍵を受け取った側は、逆写像を使って、元のx座標の値を入手する。

ガベージの役割

ガベージは、最初のメッセージが少なくとも64バイトであるという認識可能なパターンを回避するために付与される任意のデータで、最大4095バイト。

x-only ECDH

共有シークレットの導出に書いているx-only ECDHは、以下の手順で共有シークレットを導出する。

  1. 相手のElligatorSwift公開鍵からx座標を入手し、y座標を計算し、楕円曲線上の点として復元する。
  2. 1の点に自分の秘密鍵を乗算した楕円曲線上の点を導出する。この点のx座標をecdh_point_x32とする。
  3. 2のecdh_point_x32と両者のElligatorSwift公開鍵を使って以下のタグ付きハッシュを計算し、その結果が共有シークレットになる。
secret = sha256_tagged("bip324_ellswift_xonly_ecdh", イニシエーターのElligatorSwift公開鍵 || レスポンダーのElligatorSwift公開鍵 || ecdh_point_x32)
共有シークレットから導出するデータ

ハンドシェイクにより、両者は共有シークレットから以下のデータを導出する。

  • 暗号鍵(32バイト)
    各方向毎に、コンテンツの長さを暗号化するための暗号鍵(initiator_Lresponder_L)と、コンテンツ自体を暗号化する暗号鍵(initiator_Presponder_P
  • セッションID(32バイト)
  • 双方のガベージターミネーター(各16バイト)
    ガベージデータの終端を表すデータ。※ ハンドシェイク後に、ガベージターミネーターを送信したらすぐにversionパケットを送信することができる。受信した4,111バイト以内(ガベージ+ガベージターミネーターの最大値)にガベージターミネーターに合致するデータが存在しない場合は、接続は無視される。

各データは、以下のデータを使ってHKDF-SHA256を使って導出される。

  • ikm(入力のキーマテリアル)= 共有シークレット
  • salt = "bitcoin_v2_shared_secret" + <P2Pメッセージのマジックバイト>
  • info(コンテキスト情報):
    • initiator_L
    • initiator_P
    • responder_L
    • responder_P
    • session_id
    • garbage_terminators

パケットの暗号化

ハンドシェイクで双方ガベージターミネーターを受信したら、それ以降のデータはすべて暗号化される。この暗号化には、以下の2つの暗号プリミティブが使用される。各暗号化パケットは以下のデータで構成される

  • 暗号化データのデータ長を示すデータで、ChaCha20ブロック関数を使って暗号化される。
  • コンテンツの認証付き暗号データでChaCha20-Poly1305 を使って暗号化される。以下の2つのデータで構成される。
    • トランスポート層プロトコルのフラグで構成される1バイトのヘッダー。現在は、ignore bitとして最上位ビットのみが定義されている。他のbitは無視されるが、これは将来のバージョンで変更される可能性あり。
    • 可変長のコンテンツ

各平文は、それぞれ異なるnonceを持つ個別のAEADメッセージとして扱われる。

また、暗号化に使用される鍵は、224個のメッセージ毎に更新され新しい鍵が使用される。

バージョンネゴシエーション

ハンドシェイク後は、両者それぞれ空のバージョンパケットを送信する。将来v2トランスポートプロトコルを拡張する場合、適切なペイロードを持つようになるものと思われる。

ガベージターミネーターを送信後に送られる最初の暗号パケットはこのバージョンパケットか、オプションのデコイパケットになる。

v1だとversionメッセージのサービスフラグでノードのサポート機能をネゴシエーションしてたけど、v2だとこれらのフラグ無いの何故だろう?

アプリケーションメッセージ

BitcoinP2Pメッセージは、v1の場合は、

マジックバイト + 12バイトのASCIIメッセージタイプ(12バイトに満たない場合はゼロ埋め) + メッセージペイロード

という形式で送信されるが、v2の場合メッセージタイプはASCIIデータではなく1〜255の範囲内の数値で指定する(そのため、v1に比べて11バイト分メッセージのデータは小さくなる)。既存の各メッセージに割り当てられるメッセージタイプの値は以下のとおり:

  0 1 2 3
+0 従来のASCIIメッセージタイプ ADDR BLOCK BLOCKTXN
+4 CMPCTBLOCK FEEFILTER FILTERADD FILTERCLEAR
+8 FILTERLOAD GETBLOCK GETBLOCKTXN GETDATA
+12 GETHEADERS HEADERS INV MEMPOOL
+16 MERKLEBLOCK NOTFOUND PING PONG
+20 SENDCMPCT TX GETFILTERS CFILTER
+24 GETCFHEADERS CFHEADERS GETCFCHECKPT CFCHECKPT
+28 ADDRV2

29以降は現状未定義。

v2サポートのシグナリング

v2トランスポートプロトコルをサポートするノードは、addrメッセージのサービスフラグにNODE_P2P_V2 = (1 << 11)をセットすることで、v2トランスポートプロトコルをサポートしていることを通知できる。

*1:パケット長やタイミング分析、能動的な攻撃により、Bitcoinのv2トランスポートが使われていることが分かる可能性はまだ残る