高級
BIP-324
v2 P2P Transport Protocol:加密的比特幣節點通訊協議
18 分鐘
概述
BIP-324 定義了一種新的加密 P2P 傳輸協議(v2), 使用橢圓曲線 Diffie-Hellman (ECDH) 密鑰交換和 ChaCha20-Poly1305 加密, 保護節點通訊免受被動監聽和主動篡改攻擊。
啟用版本: Bitcoin Core v26.0(2023年12月)開始支援 BIP-324。 預設啟用,向後兼容 v1 協議。
設計動機
v1 協議的問題
v1 協議安全問題:
1. 明文傳輸
┌─────────────────────────────────────┐
│ 所有 P2P 訊息以明文傳輸 │
│ ISP、政府、攻擊者可以: │
│ - 識別比特幣節點 │
│ - 監視交易傳播 │
│ - 追蹤錢包活動 │
└─────────────────────────────────────┘
2. 可識別的協議特徵
┌─────────────────────────────────────┐
│ 魔數: 0xF9BEB4D9 (mainnet) │
│ 固定的訊息格式 │
│ 容易被防火牆識別和封鎖 │
└─────────────────────────────────────┘
3. 無完整性保護
┌─────────────────────────────────────┐
│ 中間人可以: │
│ - 修改訊息內容 │
│ - 注入假訊息 │
│ - 進行 eclipse 攻擊 │
└─────────────────────────────────────┘ v2 協議的改進
BIP-324 解決方案:
1. 加密通訊
- ECDH 密鑰交換
- ChaCha20-Poly1305 AEAD 加密
- 前向保密性
2. 協議混淆
- 沒有可識別的魔數
- 隨機化的握手
- 可變長度填充
3. 完整性保護
- 認證加密防止篡改
- 序號防止重放攻擊
- 可選的會話 ID 顯示 握手協議
握手流程
v2 握手 (Noise XX-like):
Initiator Responder
│ │
│ ElligatorSwift(pubkey_I) │
│ ─────────────────────────────────>│
│ (64 bytes) │
│ │
│ ElligatorSwift(pubkey_R) │
│ + encrypted(garbage) │
│ <─────────────────────────────────│
│ (64+ bytes) │
│ │
│ encrypted(garbage) │
│ + encrypted(VERSION) │
│ ─────────────────────────────────>│
│ │
│ encrypted(VERSION) │
│ <─────────────────────────────────│
│ │
│ [加密通訊開始] │
│ │
密鑰派生:
shared_secret = ECDH(my_privkey, their_pubkey)
session_keys = HKDF(shared_secret, "bitcoin_v2_shared_secret") ElligatorSwift 編碼
ElligatorSwift:
將 secp256k1 公鑰編碼為偽隨機的 64 字節
目的:
- 公鑰看起來像隨機數據
- 無法區分握手數據和隨機流量
- 抵抗 DPI (深度包檢測)
過程:
1. 取公鑰 (x, y) 座標
2. 應用 Elligator Swift 變換
3. 產生 64 字節偽隨機輸出
特性:
- 雙向映射(可解碼回公鑰)
- 均勻分佈輸出
- 計算高效 加密方案
ChaCha20-Poly1305
AEAD 加密:
┌─────────────────────────────────────┐
│ 密鑰長度: 256 bits │
│ Nonce: 96 bits (12 bytes) │
│ 認證標籤: 128 bits (16 bytes) │
└─────────────────────────────────────┘
加密結構:
┌───────────────────┬──────────────────┐
│ Encrypted Data │ Auth Tag │
│ (variable) │ (16 bytes) │
└───────────────────┴──────────────────┘
Nonce 處理:
- 每個方向獨立的計數器
- 從 0 開始遞增
- 永不重複使用 密鑰派生
import { hkdf } from '@noble/hashes/hkdf';
import { sha256 } from '@noble/hashes/sha256';
interface SessionKeys {
initiatorL: Uint8Array; // 發起者發送密鑰
initiatorP: Uint8Array; // 發起者 MAC 密鑰
responderL: Uint8Array; // 響應者發送密鑰
responderP: Uint8Array; // 響應者 MAC 密鑰
sessionId: Uint8Array; // 會話標識符
}
function deriveSessionKeys(
ecdhSecret: Uint8Array,
initiatorPubkey: Uint8Array,
responderPubkey: Uint8Array
): SessionKeys {
// 構建輸入材料
const salt = new Uint8Array([
...new TextEncoder().encode('bitcoin_v2_shared_secret'),
...initiatorPubkey,
...responderPubkey,
]);
// HKDF 派生
const keyMaterial = hkdf(
sha256,
ecdhSecret,
salt,
new TextEncoder().encode(''),
32 * 4 + 32 // 4 個 32 字節密鑰 + 會話 ID
);
return {
initiatorL: keyMaterial.slice(0, 32),
initiatorP: keyMaterial.slice(32, 64),
responderL: keyMaterial.slice(64, 96),
responderP: keyMaterial.slice(96, 128),
sessionId: keyMaterial.slice(128, 160),
};
} 封包格式
加密封包結構
v2 封包格式:
┌─────────────────────────────────────────────────┐
│ Header (encrypted, 3+ bytes) │
├─────────────────────────────────────────────────┤
│ Length (3 bytes, encrypted) │
│ ├── 實際長度 (24 bits little-endian) │
├─────────────────────────────────────────────────┤
│ Header Auth Tag (16 bytes) │
├─────────────────────────────────────────────────┤
│ Payload (encrypted, variable) │
│ ├── Message Type (1 byte) │
│ ├── Message Data (variable) │
│ ├── Padding (optional) │
├─────────────────────────────────────────────────┤
│ Payload Auth Tag (16 bytes) │
└─────────────────────────────────────────────────┘
總開銷: 19 + 16 = 35 bytes per packet 訊息類型編碼
短訊息 ID (1 byte):
常用訊息使用短 ID 節省頻寬:
0x00: 保留(忽略)
0x01: addr
0x02: block
0x03: blocktxn
0x04: cmpctblock
0x05: feefilter
0x06: filteradd
0x07: filterclear
0x08: filterload
0x09: getblocks
0x0a: getblocktxn
0x0b: getdata
0x0c: getheaders
0x0d: headers
0x0e: inv
0x0f: mempool
0x10: merkleblock
0x11: notfound
0x12: ping
0x13: pong
0x14: sendcmpct
0x15: tx
0x16: getcfilters
0x17: cfilter
0x18: getcfheaders
0x19: cfheaders
0x1a: getcfcheckpt
0x1b: cfcheckpt
0x1c: addrv2
0x1d: sendaddrv2
0x1e: wtxidrelay
0x1f: sendtxrcncl
長訊息 ID (13 bytes):
用於非標準或新訊息類型 TypeScript 實作
握手實作
import * as secp256k1 from '@noble/secp256k1';
import { chacha20poly1305 } from '@noble/ciphers/chacha';
interface V2Handshake {
localPrivkey: Uint8Array;
localPubkey: Uint8Array;
ellswiftLocal: Uint8Array;
sessionKeys?: SessionKeys;
}
// 初始化握手
function initHandshake(): V2Handshake {
const privkey = secp256k1.utils.randomPrivateKey();
const pubkey = secp256k1.getPublicKey(privkey, false);
// ElligatorSwift 編碼
const ellswift = elligatorSwiftEncode(pubkey);
return {
localPrivkey: privkey,
localPubkey: pubkey,
ellswiftLocal: ellswift,
};
}
// ElligatorSwift 編碼(簡化版)
function elligatorSwiftEncode(pubkey: Uint8Array): Uint8Array {
// 實際實作需要完整的 ElligatorSwift 算法
// 這裡使用 secp256k1 庫的實作
const x = pubkey.slice(1, 33);
const y = pubkey.slice(33, 65);
// 產生 64 字節偽隨機輸出
// 實際使用 libsecp256k1 的 ellswift 模組
return new Uint8Array(64);
}
// ElligatorSwift 解碼
function elligatorSwiftDecode(encoded: Uint8Array): Uint8Array {
// 從 64 字節編碼恢復公鑰
return new Uint8Array(65);
}
// 完成握手
function completeHandshake(
handshake: V2Handshake,
remotePubkeyEncoded: Uint8Array,
isInitiator: boolean
): V2Handshake {
// 解碼遠端公鑰
const remotePubkey = elligatorSwiftDecode(remotePubkeyEncoded);
// ECDH
const sharedSecret = secp256k1.getSharedSecret(
handshake.localPrivkey,
remotePubkey
);
// 派生會話密鑰
const [initiatorPub, responderPub] = isInitiator
? [handshake.ellswiftLocal, remotePubkeyEncoded]
: [remotePubkeyEncoded, handshake.ellswiftLocal];
const sessionKeys = deriveSessionKeys(
sharedSecret.slice(1),
initiatorPub,
responderPub
);
return {
...handshake,
sessionKeys,
};
} 加密封包處理
class V2Transport {
private sendKey: Uint8Array;
private recvKey: Uint8Array;
private sendNonce: bigint = 0n;
private recvNonce: bigint = 0n;
constructor(sessionKeys: SessionKeys, isInitiator: boolean) {
if (isInitiator) {
this.sendKey = sessionKeys.initiatorL;
this.recvKey = sessionKeys.responderL;
} else {
this.sendKey = sessionKeys.responderL;
this.recvKey = sessionKeys.initiatorL;
}
}
private getNonce(counter: bigint): Uint8Array {
const nonce = new Uint8Array(12);
const view = new DataView(nonce.buffer);
view.setBigUint64(4, counter, true);
return nonce;
}
encryptPacket(
messageType: number,
payload: Uint8Array,
padding: number = 0
): Uint8Array {
// 構建明文
const plaintext = new Uint8Array(1 + payload.length + padding);
plaintext[0] = messageType;
plaintext.set(payload, 1);
// padding 保持為 0
// 加密長度(3 bytes)
const length = plaintext.length;
const lengthBytes = new Uint8Array(3);
lengthBytes[0] = length & 0xff;
lengthBytes[1] = (length >> 8) & 0xff;
lengthBytes[2] = (length >> 16) & 0xff;
const cipher = chacha20poly1305(this.sendKey, this.getNonce(this.sendNonce));
const encryptedHeader = cipher.encrypt(lengthBytes);
this.sendNonce++;
// 加密 payload
const payloadCipher = chacha20poly1305(
this.sendKey,
this.getNonce(this.sendNonce)
);
const encryptedPayload = payloadCipher.encrypt(plaintext);
this.sendNonce++;
return new Uint8Array([
...encryptedHeader,
...encryptedPayload,
]);
}
decryptPacket(data: Uint8Array): {
messageType: number;
payload: Uint8Array;
} | null {
// 解密長度
const encryptedHeader = data.slice(0, 3 + 16);
const cipher = chacha20poly1305(this.recvKey, this.getNonce(this.recvNonce));
let lengthBytes: Uint8Array;
try {
lengthBytes = cipher.decrypt(encryptedHeader);
} catch {
return null; // 認證失敗
}
this.recvNonce++;
const length =
lengthBytes[0] |
(lengthBytes[1] << 8) |
(lengthBytes[2] << 16);
// 解密 payload
const encryptedPayload = data.slice(3 + 16, 3 + 16 + length + 16);
const payloadCipher = chacha20poly1305(
this.recvKey,
this.getNonce(this.recvNonce)
);
let plaintext: Uint8Array;
try {
plaintext = payloadCipher.decrypt(encryptedPayload);
} catch {
return null;
}
this.recvNonce++;
return {
messageType: plaintext[0],
payload: plaintext.slice(1),
};
}
} 垃圾數據與混淆
垃圾數據 (Garbage):
目的:
- 隱藏協議結構
- 使流量分析更困難
- 允許未來協議演進
規則:
- 握手後發送 0-4095 字節隨機數據
- 接收方忽略所有垃圾數據
- 第一個有效的 VERSION 訊息標記垃圾結束
終止符:
- 使用加密的 garbage_terminator
- 16 字節,派生自共享密鑰
- 解決同步問題 版本協商
VERSION 訊息:
結構:
┌─────────────────────────────────────┐
│ garbage_terminator (16 bytes) │
├─────────────────────────────────────┤
│ encrypted(VERSION_CONTENT) │
└─────────────────────────────────────┘
VERSION_CONTENT:
- 空(目前版本)
- 未來可能包含功能協商
完整握手後:
1. 發送方加密並發送 VERSION
2. 接收方解密並驗證
3. 雙方開始正常加密通訊 配置選項
# bitcoin.conf
# 啟用 v2 傳輸(預設啟用)
v2transport=1
# 只使用 v2 連接(謹慎使用)
v2transport=1
-onlyv2=1
# 驗證 v2 連接
bitcoin-cli getpeerinfo | jq '.[].transport_protocol_type'
# 結果
# "v2" - BIP-324 加密連接
# "v1" - 傳統明文連接 安全性分析
| 威脅 | v1 協議 | v2 協議 |
|---|---|---|
| 被動監聽 | 完全暴露 | 加密保護 |
| 流量分析 | 容易識別 | 混淆處理 |
| 中間人攻擊 | 無保護 | 可檢測* |
| 訊息篡改 | 無保護 | AEAD 認證 |
| 重放攻擊 | 可能 | 序號保護 |
| 協議封鎖 | 容易 | 困難 |
*中間人攻擊需要額外機制(如會話 ID 比對)才能完全防止
會話 ID 驗證
可選的中間人檢測:
會話 ID 是從密鑰交換派生的 32 字節值。
如果雙方通過可信渠道比對會話 ID,可以檢測中間人。
流程:
1. 完成握手
2. 雙方獨立計算會話 ID
3. 通過其他渠道(如語音、已知公鑰)比對
4. 如果匹配,確認無中間人
適用場景:
- 運營商之間的長期連接
- 需要高安全性的節點
- 初始 peer 驗證 最佳實踐
- 啟用 v2:使用 Bitcoin Core v26.0+ 並確保 v2transport=1
- 混合使用:同時支持 v1 和 v2 以保持網路連通性
- 監控連接:檢查 peer 的傳輸協議類型
- 考慮 Tor:v2 + Tor 提供更強的隱私保護
- 會話 ID:對於重要連接,驗證會話 ID
相關資源
已複製連結
討論