これまでBitcoinのP2Pレイヤーの通信は暗号化されておらず平文でメッセージがやりとりされている。基本的にBitcoinの場合、ブロックやトランザクションなどのデータは誰もが共有する台帳データで機密性のあるデータではないから。
ただ、リレーされるデータ自体は公開データであるものの、平文の通信には以下のような課題もある:
- ノードのP2P接続を観察可能なプレーヤー(ISPなど)に対して、トランザクションソースやタイミングに関する情報を与えることになる。
- 平文なので途中でデータの改ざんリスクがあり、その検出も難しい。
- 接続時に固有のマジックバイトで通信が始まるのでBitcoinのP2P接続であることを簡単に識別することができる。
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は、以下の手順で共有シークレットを導出する。
- 相手のElligatorSwift公開鍵からx座標を入手し、y座標を計算し、楕円曲線上の点として復元する。
- 1の点に自分の秘密鍵を乗算した楕円曲線上の点を導出する。この点のx座標を
ecdh_point_x32
とする。
- 2の
ecdh_point_x32
と両者のElligatorSwift公開鍵を使って以下のタグ付きハッシュを計算し、その結果が共有シークレットになる。
secret = sha256_tagged("bip324_ellswift_xonly_ecdh", イニシエーターのElligatorSwift公開鍵 || レスポンダーのElligatorSwift公開鍵 || ecdh_point_x32)
共有シークレットから導出するデータ
ハンドシェイクにより、両者は共有シークレットから以下のデータを導出する。
- 暗号鍵(32バイト)
各方向毎に、コンテンツの長さを暗号化するための暗号鍵(initiator_L
、responder_L
)と、コンテンツ自体を暗号化する暗号鍵(initiator_P
、responder_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だとこれらのフラグ無いの何故だろう?
アプリケーションメッセージ
BitcoinのP2Pメッセージは、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トランスポートプロトコルをサポートするノードは、addr
メッセージのサービスフラグにNODE_P2P_V2 = (1 << 11)
をセットすることで、v2トランスポートプロトコルをサポートしていることを通知できる。