Skip to content

Commit ff91db0

Browse files
committed
p2p: BIP324 shapable key exchange
1 parent 310149a commit ff91db0

File tree

5 files changed

+160
-43
lines changed

5 files changed

+160
-43
lines changed

src/net.cpp

+141-31
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ static const uint64_t RANDOMIZER_ID_LOCALHOSTNONCE = 0xd93e69e2bbfa5735ULL; // S
115115
static const uint64_t RANDOMIZER_ID_ADDRCACHE = 0x1cf2e4ddd306dda9ULL; // SHA256("addrcache")[0:8]
116116

117117
static constexpr uint8_t V2_MAX_MSG_TYPE_LEN = 12; // maximum length for V2 (BIP324) string message types
118+
static constexpr size_t V2_MAX_GARBAGE_BYTES = 4095; // maximum length for V2 (BIP324) shapable handshake
118119
//
119120
// Global state variables
120121
//
@@ -700,10 +701,16 @@ void CNode::InitV2P2P(const Span<const std::byte> their_ellswift, const Span<con
700701
if (initiating) {
701702
m_deserializer = std::make_unique<V2TransportDeserializer>(GetId(), v2_keys.responder_L, v2_keys.responder_P, v2_keys.rekey_salt);
702703
m_serializer = std::make_unique<V2TransportSerializer>(v2_keys.initiator_L, v2_keys.initiator_P, v2_keys.rekey_salt);
704+
v2_sent_garbage_terminator = v2_keys.initiator_garbage_terminator;
705+
v2_recv_garbage_terminator = v2_keys.responder_garbage_terminator;
703706
} else {
704707
m_deserializer = std::make_unique<V2TransportDeserializer>(GetId(), v2_keys.initiator_L, v2_keys.initiator_P, v2_keys.rekey_salt);
705708
m_serializer = std::make_unique<V2TransportSerializer>(v2_keys.responder_L, v2_keys.responder_P, v2_keys.rekey_salt);
709+
v2_sent_garbage_terminator = v2_keys.responder_garbage_terminator;
710+
v2_recv_garbage_terminator = v2_keys.initiator_garbage_terminator;
706711
}
712+
// Both peers must keep around a copy of the garbage terminator for the BIP324 shapable handshake
713+
v2_keys_derived = true;
707714
}
708715

709716
void CNode::EnsureInitV2Key(bool initiating)
@@ -737,13 +744,24 @@ bool CNode::ReceiveMsgBytes(Span<const uint8_t> msg_bytes, bool& complete)
737744
// decompose a transport agnostic CNetMessage from the deserializer
738745
bool reject_message{false};
739746
bool disconnect{false};
740-
CNetMessage msg = m_deserializer->GetMessage(time, reject_message, disconnect, {});
747+
748+
std::vector<std::byte> aad;
749+
if (PreferV2Conn() && !m_authenticated_v2_garbage) {
750+
std::copy(v2_garbage_bytes_recd.begin(), v2_garbage_bytes_recd.end(), std::back_inserter(aad));
751+
}
752+
CNetMessage msg = m_deserializer->GetMessage(time, reject_message, disconnect, aad);
741753

742754
if (disconnect) {
743755
// v2 p2p incorrect MAC tag. Disconnect from peer.
744756
return false;
745757
}
746758

759+
if (!aad.empty()) {
760+
// first message to authenticate garbage
761+
m_authenticated_v2_garbage = true;
762+
memory_cleanse(v2_garbage_bytes_recd.data(), v2_garbage_bytes_recd.size());
763+
}
764+
747765
// inbound clients do not know whether the peer is trying to talk v1 or v2.
748766
// if the first message is not VERSION, we reinterpret the bytes as v2 ellswift
749767
if (!m_prefer_p2p_v2 && IsInboundConn() && nVersion == 0 && msg.m_type != NetMsgType::VERSION) {
@@ -785,7 +803,17 @@ int V1TransportDeserializer::readHeader(Span<const uint8_t> msg_bytes)
785803
memcpy(&hdrbuf[nHdrPos], msg_bytes.data(), nCopy);
786804
nHdrPos += nCopy;
787805

788-
// if header incomplete, exit
806+
if (validated_magic_len < CMessageHeader::MESSAGE_START_SIZE) {
807+
auto available = std::min((size_t)nHdrPos, CMessageHeader::MESSAGE_START_SIZE);
808+
if (memcmp(hdrbuf.data(), m_chain_params.MessageStart(), available) != 0) {
809+
LogPrint(BCLog::NET, "Header error: Wrong MessageStart %s received, peer=%d\n", HexStr(hdrbuf), m_node_id);
810+
return -1;
811+
} else {
812+
validated_magic_len = available;
813+
}
814+
}
815+
816+
// don't have complete header yet, exit
789817
if (nHdrPos < CMessageHeader::HEADER_SIZE)
790818
return nCopy;
791819

@@ -798,12 +826,6 @@ int V1TransportDeserializer::readHeader(Span<const uint8_t> msg_bytes)
798826
return -1;
799827
}
800828

801-
// Check start string, network magic
802-
if (memcmp(hdr.pchMessageStart, m_chain_params.MessageStart(), CMessageHeader::MESSAGE_START_SIZE) != 0) {
803-
LogPrint(BCLog::NET, "Header error: Wrong MessageStart %s received, peer=%d\n", HexStr(hdr.pchMessageStart), m_node_id);
804-
return -1;
805-
}
806-
807829
// reject messages larger than MAX_SIZE or MAX_PROTOCOL_MESSAGE_LENGTH
808830
if (hdr.nMessageSize > MAX_SIZE || hdr.nMessageSize > MAX_PROTOCOL_MESSAGE_LENGTH) {
809831
LogPrint(BCLog::NET, "Header error: Size too large (%s, %u bytes), peer=%d\n", SanitizeString(hdr.GetCommand()), hdr.nMessageSize, m_node_id);
@@ -979,9 +1001,14 @@ CNetMessage V2TransportDeserializer::GetMessage(const std::chrono::microseconds
9791001
vRecv.resize(m_contents_size);
9801002
reject_message = reject_message || (BIP324HeaderFlags(BIP324_IGNORE & flags) != BIP324_NONE);
9811003

982-
// The first message we receive is the BIP324 transport version placeholder message.
983-
// Discard it for v2.0 clients.
984-
if (!m_processed_version_placeholder) {
1004+
if (!aad.empty()) {
1005+
// This only happens for the first encrypted message received by an inbound client
1006+
// which is meant to authenticate the garbage bytes for the BIP324 shapable handshake
1007+
// That message is not to be passed to the p2p application layer.
1008+
reject_message = true;
1009+
} else if (!m_processed_version_placeholder) {
1010+
// BIP324 transport version placeholder message.
1011+
// Discard it for v2.0 clients.
9851012
reject_message = true;
9861013
m_processed_version_placeholder = true;
9871014
}
@@ -1568,43 +1595,111 @@ void CConnman::SocketHandlerConnected(const std::vector<CNode*>& nodes,
15681595
}
15691596
nBytes = pnode->m_sock->Recv(pchBuf, sizeof(pchBuf), MSG_DONTWAIT);
15701597
}
1598+
uint8_t* ptr = pchBuf;
15711599
if (nBytes > 0) {
15721600
bool notify = false;
15731601
size_t num_bytes = (size_t)nBytes;
1574-
if (!pnode->ReceiveMsgBytes({pchBuf, num_bytes}, notify)) {
1602+
1603+
// for a v2 outbound peer:
1604+
// prior to InitV2P2P(), pnode->m_deserializer is not init and ReceiveMsgBytes() will return false
1605+
// for a v2 inbound peer:
1606+
// prior to InitV2P2P(), pnode->m_deserializer is init to V1TransportDeserializer, ReceiveMsgBytes will fail due to a malformed header according to the v1 protocol
1607+
// for all v2 peers:
1608+
// after InitV2P2P(), keys are derived, but m_deserializer would simply disconnect on MAC failure until
1609+
// the key exchage phase ends with the garbage terminator
1610+
// after garbage terminator, ReceiveMsgBytes() should work on valid v2 encrypted packets
1611+
if ((pnode->v2_keys_derived && !pnode->v2_garbage_terminated) ||
1612+
!pnode->ReceiveMsgBytes({ptr, num_bytes}, notify)) {
15751613
// when we cannot understand the received bytes, disconnect if:
15761614
// 1. we don't support BIP324 v2, or
15771615
// 2. v2 key exchange is complete, we should understand the bytes, or
15781616
// 3. we've previously received a v1 (or v2) VERSION message from the peer
1579-
// 4. the received bytes are not exactly the size of an ellswift encoding
15801617
if (!gArgs.GetBoolArg("-v2transport", DEFAULT_V2_TRANSPORT) ||
15811618
pnode->v2_key_exchange_complete ||
1582-
pnode->nVersion != 0 ||
1583-
num_bytes < ELLSWIFT_ENCODED_SIZE) {
1619+
pnode->nVersion != 0) {
15841620
pnode->CloseSocketDisconnect();
15851621
} else {
15861622
pnode->EnsureInitV2Key(!pnode->IsInboundConn());
15871623

1588-
pnode->InitV2P2P({AsBytePtr(pchBuf), ELLSWIFT_ENCODED_SIZE}, MakeByteSpan(pnode->ellswift_pubkey), !pnode->IsInboundConn());
1589-
if (pnode->IsInboundConn()) {
1590-
PushV2EllSwiftPubkey(pnode);
1624+
if (!pnode->v2_keys_derived) {
1625+
// If we're the inbound peer, upon receiving any ellswift bytes we send our ellswift key
1626+
if (pnode->IsInboundConn() && pnode->peer_ellswift_buf.empty()) {
1627+
PushV2EllSwiftPubkey(pnode);
1628+
// We now know the peer prefers a BIP324 v2 connection
1629+
pnode->m_prefer_p2p_v2 = true;
1630+
}
1631+
1632+
// Keys are not derived because we don't have the peer ellswift yet, keep buffering.
1633+
auto old_ellswift_sz = pnode->peer_ellswift_buf.size();
1634+
auto more_ellswift_bytes = std::min(ELLSWIFT_ENCODED_SIZE - old_ellswift_sz, num_bytes);
1635+
pnode->peer_ellswift_buf.resize(old_ellswift_sz + more_ellswift_bytes);
1636+
memcpy(pnode->peer_ellswift_buf.data() + old_ellswift_sz, ptr, more_ellswift_bytes);
1637+
1638+
ptr += more_ellswift_bytes;
1639+
num_bytes -= more_ellswift_bytes;
1640+
1641+
if (pnode->peer_ellswift_buf.size() < ELLSWIFT_ENCODED_SIZE)
1642+
continue;
1643+
1644+
// At this point we have the entire peer ellswift, we can derive keys
1645+
// and instantiate the v2 encryption session
1646+
pnode->InitV2P2P(pnode->peer_ellswift_buf, MakeByteSpan(pnode->ellswift_pubkey), !pnode->IsInboundConn());
1647+
1648+
// After keys are exchanged, both peers send the garbage terminator
1649+
// followed by the v2 encrypted message that authenticates the garbage
1650+
// which is currently empty as the mechanism is unused in bitcoin core.
1651+
PushV2GarbageTerminator(pnode);
1652+
CSerializedNetMsg garbage_auth_msg;
1653+
PushMessage(pnode, std::move(garbage_auth_msg));
1654+
// Send empty message again for transport version placeholder
1655+
CSerializedNetMsg transport_version_msg;
1656+
PushMessage(pnode, std::move(transport_version_msg));
15911657
}
15921658

1593-
// Send empty message for transport version placeholder
1594-
CSerializedNetMsg msg;
1595-
PushMessage(pnode, std::move(msg));
1596-
1597-
if (!pnode->IsInboundConn()) {
1598-
// Outbound peer has completed ECDH and can start the P2P protocol
1599-
m_msgproc->InitP2P(*pnode, nLocalServices);
1659+
if (pnode->v2_keys_derived && !pnode->v2_garbage_terminated && num_bytes > 0) {
1660+
// Keep buffering bytes until the garbage terminator
1661+
auto old_size = pnode->v2_garbage_bytes_recd.size();
1662+
auto new_size = old_size + num_bytes;
1663+
1664+
// Might be better to just allocate to V2_MAX_GARBAGE_BYTES at start once the mechanism
1665+
// is actually used
1666+
pnode->v2_garbage_bytes_recd.resize(std::min(new_size, V2_MAX_GARBAGE_BYTES));
1667+
memcpy(pnode->v2_garbage_bytes_recd.data() + old_size, ptr, (new_size - old_size));
1668+
auto it = std::search(pnode->v2_garbage_bytes_recd.begin(), pnode->v2_garbage_bytes_recd.end(),
1669+
pnode->v2_recv_garbage_terminator.begin(), pnode->v2_recv_garbage_terminator.end());
1670+
1671+
if (it != pnode->v2_garbage_bytes_recd.end()) {
1672+
// Found the terminator...
1673+
auto garbage_size = it - pnode->v2_garbage_bytes_recd.begin();
1674+
pnode->v2_garbage_bytes_recd.erase(it, pnode->v2_garbage_bytes_recd.end());
1675+
if (garbage_size <= long{V2_MAX_GARBAGE_BYTES}) {
1676+
// ...in less than the maximum allowed bytes
1677+
auto fwd = garbage_size + pnode->v2_recv_garbage_terminator.size() - old_size;
1678+
ptr += fwd;
1679+
num_bytes -= fwd;
1680+
pnode->v2_garbage_terminated = true;
1681+
} else {
1682+
// ..but more than the maximum allowed bytes were used
1683+
pnode->CloseSocketDisconnect();
1684+
}
1685+
memory_cleanse(pnode->v2_recv_garbage_terminator.data(), pnode->v2_recv_garbage_terminator.size());
1686+
} else if (pnode->v2_garbage_bytes_recd.size() >= V2_MAX_GARBAGE_BYTES) {
1687+
pnode->CloseSocketDisconnect();
1688+
memory_cleanse(pnode->v2_recv_garbage_terminator.data(), pnode->v2_recv_garbage_terminator.size());
1689+
}
16001690
}
16011691

1602-
pnode->v2_key_exchange_complete = true;
1603-
if (num_bytes > ELLSWIFT_ENCODED_SIZE &&
1604-
!pnode->ReceiveMsgBytes(
1605-
{pchBuf + ELLSWIFT_ENCODED_SIZE, (size_t)(nBytes - ELLSWIFT_ENCODED_SIZE)},
1606-
notify)) {
1607-
pnode->CloseSocketDisconnect();
1692+
// after a successful ECDH and shapable handshake garbage termination, process the remaining bytes.
1693+
if (pnode->v2_garbage_terminated) {
1694+
if (num_bytes > 0 && !pnode->ReceiveMsgBytes({ptr, num_bytes}, notify)) {
1695+
pnode->CloseSocketDisconnect();
1696+
} else {
1697+
pnode->v2_key_exchange_complete = true;
1698+
if (!pnode->IsInboundConn()) {
1699+
// Outbound peer has completed key exchange and can start the P2P protocol
1700+
m_msgproc->InitP2P(*pnode, nLocalServices);
1701+
}
1702+
}
16081703
}
16091704
}
16101705
}
@@ -3173,6 +3268,21 @@ void CConnman::PushV2EllSwiftPubkey(CNode* pnode)
31733268
if (nBytesSent) RecordBytesSent(nBytesSent);
31743269
}
31753270

3271+
void CConnman::PushV2GarbageTerminator(CNode* pnode)
3272+
{
3273+
std::vector<unsigned char> terminator_uchars;
3274+
terminator_uchars.resize(pnode->v2_sent_garbage_terminator.size());
3275+
memcpy(terminator_uchars.data(), pnode->v2_sent_garbage_terminator.data(), terminator_uchars.size());
3276+
{
3277+
LOCK(pnode->cs_vSend);
3278+
pnode->nSendSize += pnode->v2_sent_garbage_terminator.size();
3279+
3280+
// We do not have to send immediately because this is followed shortly by the
3281+
// transport version message
3282+
pnode->vSendMsg.push_back(terminator_uchars);
3283+
}
3284+
}
3285+
31763286
bool CConnman::ForNode(NodeId id, std::function<bool(CNode* pnode)> func)
31773287
{
31783288
CNode* found = nullptr;

src/net.h

+9
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ class V1TransportDeserializer final : public TransportDeserializer
289289
CDataStream vRecv; // received message data
290290
unsigned int nHdrPos;
291291
unsigned int nDataPos;
292+
uint8_t validated_magic_len{0};
292293

293294
const uint256& GetMessageHash() const;
294295
int readHeader(Span<const uint8_t> msg_bytes);
@@ -527,6 +528,8 @@ class CNode
527528
std::atomic_bool fPauseRecv{false};
528529
std::atomic_bool fPauseSend{false};
529530
std::atomic_bool v2_key_exchange_complete{false};
531+
std::atomic_bool m_authenticated_v2_garbage{false};
532+
bool v2_garbage_terminated{false};
530533

531534
bool IsOutboundOrBlockRelayConn() const {
532535
switch (m_conn_type) {
@@ -638,6 +641,7 @@ class CNode
638641
std::atomic<std::chrono::microseconds> m_min_ping_time{std::chrono::microseconds::max()};
639642

640643
EllSwiftPubKey ellswift_pubkey;
644+
std::vector<std::byte> peer_ellswift_buf;
641645

642646
CNode(NodeId id,
643647
std::shared_ptr<Sock> sock,
@@ -726,6 +730,10 @@ class CNode
726730

727731
std::list<CNetMessage> vRecvMsg; // Used only by SocketHandler thread
728732
CKey v2_priv_key;
733+
std::array<std::byte, BIP324_GARBAGE_TERMINATOR_LEN> v2_sent_garbage_terminator;
734+
std::array<std::byte, BIP324_GARBAGE_TERMINATOR_LEN> v2_recv_garbage_terminator;
735+
std::vector<std::byte> v2_garbage_bytes_recd;
736+
bool v2_keys_derived{false};
729737

730738
// Our address, as reported by the peer
731739
CService addrLocal GUARDED_BY(m_addr_local_mutex);
@@ -886,6 +894,7 @@ class CConnman
886894

887895
void PushMessage(CNode* pnode, CSerializedNetMsg&& msg) EXCLUSIVE_LOCKS_REQUIRED(!m_total_bytes_sent_mutex);
888896
void PushV2EllSwiftPubkey(CNode* pnode) EXCLUSIVE_LOCKS_REQUIRED(!m_total_bytes_sent_mutex);
897+
void PushV2GarbageTerminator(CNode* pnode);
889898

890899
using NodeFn = std::function<void(CNode*)>;
891900
void ForEachNode(const NodeFn& func)

src/test/fuzz/process_message.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ void fuzz_target(FuzzBufferType buffer, const std::string& LIMIT_TO_MESSAGE_TYPE
8686
if (p2p_node.PreferV2Conn()) {
8787
InitTestV2P2P(fuzzed_data_provider, p2p_node, connman);
8888
const CNetMsgMaker mm{0};
89+
if (p2p_node.IsInboundConn()) {
90+
// pretend to have received the garbage terminator and garbage authentication message
91+
p2p_node.v2_garbage_terminated = true;
92+
p2p_node.m_authenticated_v2_garbage = true;
93+
}
8994
// receive the transport version placeholder message
9095
CSerializedNetMsg dummy{mm.Make(NetMsgType::VERSION)};
9196
(void)connman.ReceiveMsgFrom(p2p_node, dummy);

src/test/fuzz/process_messages.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ FUZZ_TARGET_INIT(process_messages, initialize_process_messages)
5252
if (p2p_node.PreferV2Conn()) {
5353
InitTestV2P2P(fuzzed_data_provider, p2p_node, connman);
5454
const CNetMsgMaker mm{0};
55+
if (p2p_node.IsInboundConn()) {
56+
// pretend to have received the garbage terminator and garbage authentication message
57+
p2p_node.v2_garbage_terminated = true;
58+
p2p_node.m_authenticated_v2_garbage = true;
59+
}
5560
// receive the transport version placeholder message
5661
CSerializedNetMsg dummy{mm.Make(NetMsgType::VERSION)};
5762
(void)connman.ReceiveMsgFrom(p2p_node, dummy);

test/functional/p2p_invalid_messages.py

-12
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ def set_test_params(self):
6262
def run_test(self):
6363
self.test_buffer()
6464
self.test_duplicate_version_msg()
65-
self.test_magic_bytes()
6665
self.test_checksum()
6766
self.test_size()
6867
self.test_msgtype()
@@ -101,17 +100,6 @@ def test_duplicate_version_msg(self):
101100
conn.send_and_ping(msg_version())
102101
self.nodes[0].disconnect_p2ps()
103102

104-
def test_magic_bytes(self):
105-
self.log.info("Test message with invalid magic bytes disconnects peer")
106-
conn = self.nodes[0].add_p2p_connection(P2PDataStore())
107-
with self.nodes[0].assert_debug_log(['Header error: Wrong MessageStart ffffffff received']):
108-
msg = conn.build_message(msg_unrecognized(str_data="d"))
109-
# modify magic bytes
110-
msg = b'\xff' * 4 + msg[4:]
111-
conn.send_raw_message(msg)
112-
conn.wait_for_disconnect(timeout=1)
113-
self.nodes[0].disconnect_p2ps()
114-
115103
def test_checksum(self):
116104
self.log.info("Test message with invalid checksum logs an error")
117105
conn = self.nodes[0].add_p2p_connection(P2PDataStore())

0 commit comments

Comments
 (0)