diff --git a/src/net.cpp b/src/net.cpp index 9a7504a88..ac16ac7f2 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -16,18 +16,19 @@ #include #include #include -#include #include #include #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -64,6 +65,8 @@ static_assert (MAX_BLOCK_RELAY_ONLY_ANCHORS <= static_cast(MAX_BLOCK_REL /** Anchor IP address database file name */ const char* const ANCHORS_DATABASE_FILENAME = "anchors.dat"; +static constexpr uint64_t V2_MAX_CONTENTS_LENGTH = 0x01000000 - 1; // 2^24 - 1 + // How often to dump addresses to peers.dat static constexpr std::chrono::minutes DUMP_PEERS_INTERVAL{15}; @@ -109,6 +112,8 @@ const std::string NET_MESSAGE_TYPE_OTHER = "*other*"; static const uint64_t RANDOMIZER_ID_NETGROUP = 0x6c0edd8036ef4036ULL; // SHA256("netgroup")[0:8] static const uint64_t RANDOMIZER_ID_LOCALHOSTNONCE = 0xd93e69e2bbfa5735ULL; // SHA256("localhostnonce")[0:8] static const uint64_t RANDOMIZER_ID_ADDRCACHE = 0x1cf2e4ddd306dda9ULL; // SHA256("addrcache")[0:8] + +static constexpr uint8_t V2_MAX_MSG_TYPE_LEN = 12; // maximum length for V2 (BIP324) string message types // // Global state variables // @@ -668,7 +673,14 @@ bool CNode::ReceiveMsgBytes(Span msg_bytes, bool& complete) if (m_deserializer->Complete()) { // decompose a transport agnostic CNetMessage from the deserializer bool reject_message{false}; - CNetMessage msg = m_deserializer->GetMessage(time, reject_message); + bool disconnect{false}; + CNetMessage msg = m_deserializer->GetMessage(time, reject_message, disconnect); + + if (disconnect) { + // v2 p2p incorrect MAC tag. Disconnect from peer. + return false; + } + if (reject_message) { // Message deserialization failed. Drop the message but don't disconnect the peer. // store the size of the corrupt message @@ -760,10 +772,12 @@ const uint256& V1TransportDeserializer::GetMessageHash() const return data_hash; } -CNetMessage V1TransportDeserializer::GetMessage(const std::chrono::microseconds time, bool& reject_message) +CNetMessage V1TransportDeserializer::GetMessage(const std::chrono::microseconds time, bool& reject_message, bool& disconnect) { // Initialize out parameter reject_message = false; + disconnect = false; + // decompose a single CNetMessage from the TransportDeserializer CNetMessage msg(std::move(vRecv)); @@ -785,6 +799,7 @@ CNetMessage V1TransportDeserializer::GetMessage(const std::chrono::microseconds HexStr(Span{hash}.first(CMessageHeader::CHECKSUM_SIZE)), HexStr(hdr.pchChecksum), m_node_id); + // TODO: Should we disconnect the v1 peer in this case? reject_message = true; } else if (!hdr.IsCommandValid()) { LogPrint(BCLog::NET, "Header error: Invalid message type (%s, %u bytes), peer=%d\n", @@ -797,7 +812,7 @@ CNetMessage V1TransportDeserializer::GetMessage(const std::chrono::microseconds return msg; } -void V1TransportSerializer::prepareForTransport(CSerializedNetMsg& msg, std::vector& header) const +bool V1TransportSerializer::prepareForTransport(CSerializedNetMsg& msg, std::vector& header) const { // create dbl-sha256 checksum uint256 hash = Hash(msg.data); @@ -809,6 +824,174 @@ void V1TransportSerializer::prepareForTransport(CSerializedNetMsg& msg, std::vec // serialize header header.reserve(CMessageHeader::HEADER_SIZE); CVectorWriter{SER_NETWORK, INIT_PROTO_VERSION, header, 0, hdr}; + return true; +} + +int V2TransportDeserializer::readHeader(Span pkt_bytes) +{ + // copy data to temporary parsing buffer + const size_t remaining = BIP324_LENGTH_FIELD_LEN - m_hdr_pos; + const size_t copy_bytes = std::min(remaining, pkt_bytes.size()); + + memcpy(&vRecv[m_hdr_pos], pkt_bytes.data(), copy_bytes); + m_hdr_pos += copy_bytes; + + // if we don't have the encrypted length yet, exit + if (m_hdr_pos < BIP324_LENGTH_FIELD_LEN) { + return copy_bytes; + } + + // we have the 3 bytes encrypted packet length at this point + std::array encrypted_pkt_len; + memcpy(encrypted_pkt_len.data(), vRecv.data(), BIP324_LENGTH_FIELD_LEN); + + // the encrypted packet data = bip324 header + contents (message type + message payload) + m_contents_size = m_cipher_suite->DecryptLength(encrypted_pkt_len); + + // m_contents_size is the size of the p2p message + if (m_contents_size > V2_MAX_CONTENTS_LENGTH) { + return -1; + } + + // switch state to reading message data + m_in_data = true; + + return copy_bytes; +} + +int V2TransportDeserializer::readData(Span pkt_bytes) +{ + // Read the BIP324 encrypted packet data. + const size_t remaining = BIP324_HEADER_LEN + m_contents_size + RFC8439_EXPANSION - m_data_pos; + const size_t copy_bytes = std::min(remaining, pkt_bytes.size()); + + // extend buffer, respect previous copied encrypted length + if (vRecv.size() < BIP324_LENGTH_FIELD_LEN + m_data_pos + copy_bytes) { + // Allocate up to 256 KiB ahead, but never more than the total message size. + vRecv.resize(BIP324_LENGTH_FIELD_LEN + std::min(BIP324_HEADER_LEN + m_contents_size, m_data_pos + copy_bytes + 256 * 1024) + RFC8439_EXPANSION, std::byte{0x00}); + } + + memcpy(&vRecv[BIP324_LENGTH_FIELD_LEN + m_data_pos], pkt_bytes.data(), copy_bytes); + m_data_pos += copy_bytes; + + return copy_bytes; +} + +CNetMessage V2TransportDeserializer::GetMessage(const std::chrono::microseconds time, bool& reject_message, bool& disconnect) +{ + const size_t min_contents_size = 1; // BIP324 1-byte message type id is the minimum contents + + // Initialize out parameters + reject_message = (vRecv.size() < V2_MIN_PACKET_LENGTH + min_contents_size); + disconnect = false; + + // In v2, vRecv contains: + // 3 bytes of encrypted packet length + // 1-byte encrypted bip324 header + // variable length encrypted contents(message type and message payload) and + // mac tag + assert(Complete()); + + std::string msg_type; + + BIP324HeaderFlags flags; + size_t msg_type_size = 1; // at least one byte needed for message type + if (m_cipher_suite->Crypt({}, + Span{reinterpret_cast(vRecv.data() + BIP324_LENGTH_FIELD_LEN), BIP324_HEADER_LEN + m_contents_size + RFC8439_EXPANSION}, + Span{reinterpret_cast(vRecv.data()), m_contents_size}, flags, false)) { + // MAC check was successful + vRecv.resize(m_contents_size); + reject_message = reject_message || (BIP324HeaderFlags(BIP324_IGNORE & flags) != BIP324_NONE); + + if (!reject_message) { + uint8_t size_or_shortid = 0; + try { + vRecv >> size_or_shortid; + } catch (const std::ios_base::failure&) { + LogPrint(BCLog::NET, "Invalid message type, peer=%d\n", m_node_id); + reject_message = true; + } + + if (size_or_shortid > 0 && size_or_shortid <= V2_MAX_MSG_TYPE_LEN && vRecv.size() >= size_or_shortid) { + // first byte is a number between 1 and 12. Must be a string command. + // use direct read since we already read the varlen size + msg_type.resize(size_or_shortid); + vRecv.read(MakeWritableByteSpan(msg_type)); + msg_type_size += size_or_shortid; + } else { + auto mtype = GetMessageTypeFromShortID(size_or_shortid); + if (mtype.has_value()) { + msg_type = mtype.value(); + } else { + // unknown-short-id results in a valid but unknown message (will be skipped) + msg_type = "unknown-" + ToString(size_or_shortid); + } + } + } + } else { + // Invalid mac tag + LogPrint(BCLog::NET, "Invalid v2 mac tag, peer=%d\n", m_node_id); + disconnect = true; + reject_message = true; + } + + // we'll always return a CNetMessage (even if decryption fails) + // decompose a single CNetMessage from the TransportDeserializer + CNetMessage msg(std::move(vRecv)); + msg.m_type = msg_type; + msg.m_time = time; + + if (!reject_message) { + msg.m_message_size = m_contents_size - msg_type_size; + msg.m_raw_message_size = V2_MIN_PACKET_LENGTH + m_contents_size; // raw wire size + } + + Reset(); + return msg; +} + +bool V2TransportSerializer::prepareForTransport(CSerializedNetMsg& msg, std::vector& header) const +{ + size_t serialized_msg_type_size = 1; // short-IDs are 1 byte + std::optional short_msg_type = GetShortIDFromMessageType(msg.m_type); + if (!short_msg_type) { + // message type without an assigned short-ID + assert(msg.m_type.size() <= V2_MAX_MSG_TYPE_LEN); + // encode as varstr, max 12 chars + serialized_msg_type_size = ::GetSerializeSize(msg.m_type, PROTOCOL_VERSION); + } + + std::vector msg_type_bytes(serialized_msg_type_size); + // append the short-ID or the varstr of the msg type + CVectorWriter vector_writer(SER_NETWORK, INIT_PROTO_VERSION, msg_type_bytes, 0); + if (short_msg_type) { + // append the single byte short ID + vector_writer << short_msg_type.value(); + } else { + // or the ASCII command string + vector_writer << msg.m_type; + } + + // insert message type directly into the CSerializedNetMsg data buffer (insert at begin) + // TODO: if we refactor the BIP324CipherSuite::Crypt() function to allow separate buffers for + // the message type and payload we could avoid a insert and thus a potential reallocation + msg.data.insert(msg.data.begin(), msg_type_bytes.begin(), msg_type_bytes.end()); + + auto contents_size = msg.data.size(); + auto encrypted_pkt_size = V2_MIN_PACKET_LENGTH + contents_size; + // resize the message buffer to make space for the MAC tag + msg.data.resize(encrypted_pkt_size, 0); + + BIP324HeaderFlags flags{BIP324_NONE}; + // encrypt the payload, this should always succeed (controlled buffers, don't check the MAC during encrypting) + auto success = m_cipher_suite->Crypt({}, + Span{reinterpret_cast(msg.data.data()), contents_size}, + Span{reinterpret_cast(msg.data.data()), encrypted_pkt_size}, + flags, true); + if (!success) { + LogPrint(BCLog::NET, "error in v2 p2p encryption for message type: %s\n", msg.m_type); + } + return success; } size_t CConnman::SocketSendData(CNode& node) const @@ -2790,7 +2973,10 @@ void CConnman::PushMessage(CNode* pnode, CSerializedNetMsg&& msg) // make sure we use the appropriate network transport format std::vector serializedHeader; - pnode->m_serializer->prepareForTransport(msg, serializedHeader); + if (!pnode->m_serializer->prepareForTransport(msg, serializedHeader)) { + return; + } + size_t nTotalSize = nMessageSize + serializedHeader.size(); size_t nBytesSent = 0; diff --git a/src/net.h b/src/net.h index 31d17ea76..d63a0d28a 100644 --- a/src/net.h +++ b/src/net.h @@ -9,15 +9,18 @@ #include #include #include -#include #include +#include +#include #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -31,6 +34,7 @@ #include #include +#include #include #include #include @@ -89,6 +93,8 @@ static constexpr bool DEFAULT_FIXEDSEEDS{true}; static const size_t DEFAULT_MAXRECEIVEBUFFER = 5 * 1000; static const size_t DEFAULT_MAXSENDBUFFER = 1 * 1000; +static constexpr size_t V2_MIN_PACKET_LENGTH = BIP324_LENGTH_FIELD_LEN + BIP324_HEADER_LEN + RFC8439_EXPANSION; + typedef int64_t NodeId; struct AddedNodeInfo @@ -254,7 +260,7 @@ class TransportDeserializer { /** read and deserialize data, advances msg_bytes data pointer */ virtual int Read(Span& msg_bytes) = 0; // decomposes a message from the context - virtual CNetMessage GetMessage(std::chrono::microseconds time, bool& reject_message) = 0; + virtual CNetMessage GetMessage(std::chrono::microseconds time, bool& reject_message, bool& disconnect) = 0; virtual ~TransportDeserializer() {} }; @@ -318,7 +324,65 @@ class V1TransportDeserializer final : public TransportDeserializer } return ret; } - CNetMessage GetMessage(std::chrono::microseconds time, bool& reject_message) override; + CNetMessage GetMessage(std::chrono::microseconds time, bool& reject_message, bool& disconnect) override; +}; + +/** V2TransportDeserializer is a transport deserializer after BIP324 */ +class V2TransportDeserializer final : public TransportDeserializer +{ +private: + std::unique_ptr m_cipher_suite; + const NodeId m_node_id; // Only for logging + bool m_in_data = false; // parsing header (false) or data (true) + size_t m_contents_size = 0; // expected message size + CDataStream vRecv; // received message data (encrypted length, encrypted contents and MAC tag) + size_t m_hdr_pos = 0; // read pos in header + size_t m_data_pos = 0; // read pos in data + +public: + V2TransportDeserializer(const NodeId node_id, + const BIP324Key& key_l, + const BIP324Key& key_p) + : m_cipher_suite(new BIP324CipherSuite(key_l, key_p)), + m_node_id(node_id), + vRecv(SER_NETWORK, INIT_PROTO_VERSION) + { + Reset(); + } + + void Reset() + { + vRecv.clear(); + vRecv.resize(BIP324_LENGTH_FIELD_LEN); + m_in_data = false; + m_hdr_pos = 0; + m_contents_size = 0; + m_data_pos = 0; + } + bool Complete() const override + { + if (!m_in_data) { + return false; + } + return (BIP324_HEADER_LEN + m_contents_size + RFC8439_EXPANSION == m_data_pos); + } + void SetVersion(int nVersionIn) override + { + vRecv.SetVersion(nVersionIn); + } + int readHeader(Span pkt_bytes); + int readData(Span pkt_bytes); + int Read(Span& pkt_bytes) override + { + int ret = m_in_data ? readData(pkt_bytes) : readHeader(pkt_bytes); + if (ret < 0) { + Reset(); + } else { + pkt_bytes = pkt_bytes.subspan(ret); + } + return ret; + } + CNetMessage GetMessage(const std::chrono::microseconds time, bool& reject_message, bool& disconnect) override; }; /** The TransportSerializer prepares messages for the network transport @@ -326,13 +390,27 @@ class V1TransportDeserializer final : public TransportDeserializer class TransportSerializer { public: // prepare message for transport (header construction, error-correction computation, payload encryption, etc.) - virtual void prepareForTransport(CSerializedNetMsg& msg, std::vector& header) const = 0; + virtual bool prepareForTransport(CSerializedNetMsg& msg, std::vector& header) const = 0; virtual ~TransportSerializer() {} }; -class V1TransportSerializer : public TransportSerializer { +class V1TransportSerializer : public TransportSerializer +{ +public: + bool prepareForTransport(CSerializedNetMsg& msg, std::vector& header) const override; +}; + +class V2TransportSerializer : public TransportSerializer +{ +private: + std::unique_ptr m_cipher_suite; + public: - void prepareForTransport(CSerializedNetMsg& msg, std::vector& header) const override; + V2TransportSerializer(const BIP324Key& key_L, + const BIP324Key& key_P) + : m_cipher_suite(new BIP324CipherSuite(key_L, key_P)) {} + // prepare for next message + bool prepareForTransport(CSerializedNetMsg& msg, std::vector& header) const override; }; struct CNodeOptions diff --git a/src/test/fuzz/p2p_transport_serialization.cpp b/src/test/fuzz/p2p_transport_serialization.cpp index 96254aa22..1ba35fd13 100644 --- a/src/test/fuzz/p2p_transport_serialization.cpp +++ b/src/test/fuzz/p2p_transport_serialization.cpp @@ -69,7 +69,8 @@ FUZZ_TARGET_INIT(p2p_transport_serialization, initialize_p2p_transport_serializa if (deserializer.Complete()) { const std::chrono::microseconds m_time{std::numeric_limits::max()}; bool reject_message{false}; - CNetMessage msg = deserializer.GetMessage(m_time, reject_message); + bool disconnect{false}; + CNetMessage msg = deserializer.GetMessage(m_time, reject_message, disconnect); assert(msg.m_type.size() <= CMessageHeader::COMMAND_SIZE); assert(msg.m_raw_message_size <= mutable_msg_bytes.size()); assert(msg.m_raw_message_size == CMessageHeader::HEADER_SIZE + msg.m_message_size); diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 5a97e9429..0e30ec13d 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -905,4 +906,89 @@ BOOST_AUTO_TEST_CASE(initial_advertise_from_version_message) TestOnlyResetTimeData(); } +void message_serialize_deserialize_test(bool v2, const std::vector& test_msgs) +{ + // use keys with all zeros + BIP324Key key_L, key_P; + memset(key_L.data(), 1, BIP324_KEY_LEN); + memset(key_P.data(), 2, BIP324_KEY_LEN); + + // construct the serializers + std::unique_ptr serializer; + std::unique_ptr deserializer; + + if (v2) { + serializer = std::make_unique(V2TransportSerializer(key_L, key_P)); + deserializer = std::make_unique(V2TransportDeserializer((NodeId)0, key_L, key_P)); + } else { + serializer = std::make_unique(V1TransportSerializer()); + deserializer = std::make_unique(V1TransportDeserializer(Params(), (NodeId)0, SER_NETWORK, INIT_PROTO_VERSION)); + } + // run 100 times through all messages with the same cipher suite instances + for (unsigned int i = 0; i < 100; i++) { + for (const CSerializedNetMsg& msg_orig : test_msgs) { + // bypass the copy protection + CSerializedNetMsg msg; + msg.data = msg_orig.data; + msg.m_type = msg_orig.m_type; + + std::vector serialized_header; + serializer->prepareForTransport(msg, serialized_header); + + // read two times + // first: read header + size_t read_bytes{0}; + Span span_header(serialized_header.data(), serialized_header.size()); + if (serialized_header.size() > 0) read_bytes += deserializer->Read(span_header); + // second: read the encrypted payload (if required) + Span span_msg(msg.data.data(), msg.data.size()); + if (msg.data.size() > 0) read_bytes += deserializer->Read(span_msg); + if (msg.data.size() > read_bytes) { + Span span_msg(msg.data.data() + read_bytes, msg.data.size() - read_bytes); + read_bytes += deserializer->Read(span_msg); + } + // message must be complete + BOOST_CHECK(deserializer->Complete()); + BOOST_CHECK_EQUAL(read_bytes, msg.data.size() + serialized_header.size()); + + bool reject_message{true}; + bool disconnect{true}; + CNetMessage result{deserializer->GetMessage(GetTime(), reject_message, disconnect)}; + BOOST_CHECK(!reject_message); + BOOST_CHECK(!disconnect); + BOOST_CHECK_EQUAL(result.m_type, msg_orig.m_type); + BOOST_CHECK_EQUAL(result.m_message_size, msg_orig.data.size()); + if (!msg_orig.data.empty()) { + BOOST_CHECK_EQUAL(0, memcmp(result.m_recv.data(), msg_orig.data.data(), msg_orig.data.size())); + } + } + } +} + +BOOST_AUTO_TEST_CASE(net_v2) +{ + // create some messages where we perform serialization and deserialization + std::vector test_msgs; + test_msgs.push_back(CNetMsgMaker(INIT_PROTO_VERSION).Make(NetMsgType::VERACK)); + test_msgs.push_back(CNetMsgMaker(INIT_PROTO_VERSION).Make(NetMsgType::VERSION, PROTOCOL_VERSION, (int)NODE_NETWORK, 123, CAddress(CService(), NODE_NONE), CAddress(CService(), NODE_NONE), 123, "foobar", 500000, true)); + test_msgs.push_back(CNetMsgMaker(INIT_PROTO_VERSION).Make(NetMsgType::PING, 123456)); + CDataStream stream(ParseHex("020000000001013107ca31e1950a9b44b75ce3e8f30127e4d823ed8add1263a1cc8adcc8e49164000000001716001487835ecf51ea0351ef266d216a7e7a3e74b84b4efeffffff02082268590000000017a9144a94391b99e672b03f56d3f60800ef28bc304c4f8700ca9a3b0000000017a9146d5df9e79f752e3c53fc468db89cafda4f7d00cb87024730440220677de5b11a5617d541ba06a1fa5921ab6b4509f8028b23f18ab8c01c5eb1fcfb02202fe382e6e87653f60ff157aeb3a18fc888736720f27ced546b0b77431edabdb0012102608c772598e9645933a86bcd662a3b939e02fb3e77966c9713db5648d5ba8a0006010000"), SER_NETWORK, PROTOCOL_VERSION); + test_msgs.push_back(CNetMsgMaker(INIT_PROTO_VERSION).Make(NetMsgType::TX, CTransaction(deserialize, stream))); + std::vector vInv; + for (unsigned int i = 0; i < 1000; i++) { + vInv.push_back(CInv(MSG_BLOCK, Params().GenesisBlock().GetHash())); + } + test_msgs.push_back(CNetMsgMaker(INIT_PROTO_VERSION).Make(NetMsgType::INV, vInv)); + + // add a dummy message + std::string dummy; + for (unsigned int i = 0; i < 100; i++) { + dummy += "020000000001013107ca31e1950a9b44b75ce3e8f30127e4d823ed8add1263a1cc8adcc8e49164000000001716001487835ecf51ea0351ef266d216a7e7a3e74b84b4efeffffff02082268590000000017a9144a94391b99e672b03f56d3f60800ef28bc304c4f8700ca9a3b0000000017a9146d5df9e79f752e3c53fc468db89cafda4f7d00cb87024730440220677de5b11a5617d541ba06a1fa5921ab6b4509f8028b23f18ab8c01c5eb1fcfb02202fe382e6e87653f60ff157aeb3a18fc888736720f27ced546b0b77431edabdb0012102608c772598e9645933a86bcd662a3b939e02fb3e77966c9713db5648d5ba8a0006010000"; + } + test_msgs.push_back(CNetMsgMaker(INIT_PROTO_VERSION).Make("foobar", dummy)); + + message_serialize_deserialize_test(true, test_msgs); + message_serialize_deserialize_test(false, test_msgs); +} + BOOST_AUTO_TEST_SUITE_END()