From c736435d7081bc307e240a98574d1f24d267e860 Mon Sep 17 00:00:00 2001 From: Michael Lynch Date: Sat, 25 Apr 2026 00:04:29 +0000 Subject: [PATCH] Test advert parsing via mesh receive path --- platformio.ini | 6 + test/mocks/Arduino.h | 10 + test/mocks/Ed25519.h | 18 + test/mocks/SHA256.h | 40 +- test/mocks/Stream.h | 11 +- test/test_utils/test_advert_data.cpp | 551 +++++++++++++++++++++------ 6 files changed, 502 insertions(+), 134 deletions(-) create mode 100644 test/mocks/Ed25519.h diff --git a/platformio.ini b/platformio.ini index 7ff42af8d..8e30c804a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -163,7 +163,13 @@ build_flags = -std=c++17 test_build_src = yes build_src_filter = -<*> + +<../src/Dispatcher.cpp> + +<../src/Identity.cpp> + +<../src/Mesh.cpp> + +<../src/Packet.cpp> +<../src/Utils.cpp> +<../src/helpers/AdvertDataHelpers.cpp> + +<../src/helpers/BaseChatMesh.cpp> + +<../src/helpers/TxtDataHelpers.cpp> lib_deps = google/googletest @ 1.17.0 diff --git a/test/mocks/Arduino.h b/test/mocks/Arduino.h index d265a16e6..d35b9fb9e 100644 --- a/test/mocks/Arduino.h +++ b/test/mocks/Arduino.h @@ -1,6 +1,16 @@ #pragma once +#include #include #include #include #include + +inline char* ltoa(long value, char* buffer, int base) { + if (base == 10) { + snprintf(buffer, 32, "%ld", value); + } else { + buffer[0] = 0; + } + return buffer; +} diff --git a/test/mocks/Ed25519.h b/test/mocks/Ed25519.h new file mode 100644 index 000000000..1a57f5d1e --- /dev/null +++ b/test/mocks/Ed25519.h @@ -0,0 +1,18 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif +#include +#ifdef __cplusplus +} +#endif + +#include + +class Ed25519 { +public: + static bool verify(const uint8_t* sig, const uint8_t* pub_key, const uint8_t* message, int msg_len) { + return ed25519_verify(sig, message, msg_len, pub_key) != 0; + } +}; diff --git a/test/mocks/SHA256.h b/test/mocks/SHA256.h index b6e551a07..0c783dc02 100644 --- a/test/mocks/SHA256.h +++ b/test/mocks/SHA256.h @@ -2,13 +2,41 @@ #include #include +#include -// Mock SHA256 class for testing -// Provides minimal interface to allow Utils.cpp to compile class SHA256 { + uint32_t state_ = 2166136261u; + public: - void update(const uint8_t* data, size_t len) {} - void finalize(uint8_t* hash, size_t hashLen) {} - void resetHMAC(const uint8_t* key, size_t keyLen) {} - void finalizeHMAC(const uint8_t* key, size_t keyLen, uint8_t* hash, size_t hashLen) {} + void update(const void* data, size_t len) { + update(static_cast(data), len); + } + + void update(const uint8_t* data, size_t len) { + for (size_t i = 0; i < len; ++i) { + state_ ^= data[i]; + state_ *= 16777619u; + state_ += 0x9E3779B9u; + } + } + + void finalize(uint8_t* hash, size_t hashLen) { + uint32_t value = state_; + for (size_t i = 0; i < hashLen; ++i) { + value ^= value >> 13; + value *= 1274126177u; + hash[i] = static_cast((value >> ((i & 3) * 8)) & 0xFF); + } + } + + void resetHMAC(const uint8_t* key, size_t keyLen) { + state_ = 2166136261u; + update(key, keyLen); + state_ ^= 0x36363636u; + } + + void finalizeHMAC(const uint8_t* key, size_t keyLen, uint8_t* hash, size_t hashLen) { + update(key, keyLen); + finalize(hash, hashLen); + } }; diff --git a/test/mocks/Stream.h b/test/mocks/Stream.h index 195a30297..5cc399eb3 100644 --- a/test/mocks/Stream.h +++ b/test/mocks/Stream.h @@ -1,10 +1,13 @@ #pragma once -// Mock Stream class for native testing -// Provides minimal interface needed by Utils.h +#include +#include class Stream { public: - virtual void print(char c) {} - virtual void print(const char* str) {} + virtual size_t readBytes(uint8_t*, size_t) { return 0; } + virtual size_t write(const uint8_t*, size_t len) { return len; } + virtual void print(char) {} + virtual void print(const char*) {} + virtual void println() {} }; diff --git a/test/test_utils/test_advert_data.cpp b/test/test_utils/test_advert_data.cpp index 0de8e92aa..5870fce01 100644 --- a/test/test_utils/test_advert_data.cpp +++ b/test/test_utils/test_advert_data.cpp @@ -1,172 +1,475 @@ #include #include +#include +#include #include -#include "helpers/AdvertDataHelpers.h" +#include "helpers/BaseChatMesh.h" +#include "helpers/SimpleMeshTables.h" namespace { +constexpr char kSenderPrivateKeyHex[] = + "70" + "65e18fd9fabb70c1ed90dca19907de698c88b709ea146eafd93d9b830c7b60" + "c4681193c79bbc39945ba8064104bb618f8fd7a84a0af6f57033d6e8ddcd6471"; +constexpr char kSenderPublicKeyHex[] = + "1ec77175b0918ed206f9ae04ec136d6d5d4315bb26305427f645b492e9350c10"; + void WriteU8(uint8_t* dest, size_t* offset, uint8_t value) { - dest[(*offset)++] = value; + dest[(*offset)++] = value; } void WriteI32Le(uint8_t* dest, size_t* offset, int32_t value) { - const uint32_t raw = static_cast(value); - dest[(*offset)++] = static_cast(raw & 0xFF); - dest[(*offset)++] = static_cast((raw >> 8) & 0xFF); - dest[(*offset)++] = static_cast((raw >> 16) & 0xFF); - dest[(*offset)++] = static_cast((raw >> 24) & 0xFF); + const uint32_t raw = static_cast(value); + dest[(*offset)++] = static_cast(raw & 0xFF); + dest[(*offset)++] = static_cast((raw >> 8) & 0xFF); + dest[(*offset)++] = static_cast((raw >> 16) & 0xFF); + dest[(*offset)++] = static_cast((raw >> 24) & 0xFF); } void WriteBytes(uint8_t* dest, size_t* offset, const uint8_t* bytes, size_t length) { - for (size_t i = 0; i < length; ++i) { - dest[*offset + i] = bytes[i]; - } - *offset += length; + memcpy(dest + *offset, bytes, length); + *offset += length; } template void WriteStringLiteral(uint8_t* dest, size_t* offset, const char (&value)[N]) { - static_assert(N > 0, "string literal must include a null terminator"); - WriteBytes(dest, offset, reinterpret_cast(value), N - 1); + static_assert(N > 0, "string literal must include a null terminator"); + WriteBytes(dest, offset, reinterpret_cast(value), N - 1); } -TEST(AdvertData, RoundTripsNameOnly) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; +class FakeMillis final : public mesh::MillisecondClock { +public: + unsigned long getMillis() override { + return 0; + } +}; + +class FakeRtc final : public mesh::RTCClock { +public: + explicit FakeRtc(uint32_t initial_time) : _time(initial_time) {} + + uint32_t getCurrentTime() override { + return _time; + } + + void setCurrentTime(uint32_t time) override { + _time = time; + } + +private: + uint32_t _time; +}; - // flags/type byte: chat advert with a trailing name field. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); - // name field: raw bytes for "alice", consuming the rest of app_data. - WriteStringLiteral(app_data, &offset, "alice"); +class FakeRng final : public mesh::RNG { +public: + void random(uint8_t* dest, size_t sz) override { + memset(dest, 0x5A, sz); + } +}; - AdvertDataParser parser(app_data, offset); +class FakeRadio final : public mesh::Radio { +public: + int recvRaw(uint8_t*, int) override { + return 0; + } - ASSERT_TRUE(parser.isValid()); - EXPECT_EQ(ADV_TYPE_CHAT, parser.getType()); - EXPECT_TRUE(parser.hasName()); - EXPECT_STREQ("alice", parser.getName()); - EXPECT_FALSE(parser.hasLatLon()); -} + uint32_t getEstAirtimeFor(int) override { + return 1; + } -TEST(AdvertData, RoundTripsNameAndCoordinates) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; - - // flags/type byte: repeater advert with lat/lon followed by a name. - WriteU8(app_data, &offset, ADV_TYPE_REPEATER | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: signed little-endian microdegrees for 37.7749. - WriteI32Le(app_data, &offset, 37774900); - // longitude field: signed little-endian microdegrees for -122.4194. - WriteI32Le(app_data, &offset, -122419400); - // name field: raw bytes for "node" after the coordinate fields. - WriteStringLiteral(app_data, &offset, "node"); - - AdvertDataParser parser(app_data, offset); - - ASSERT_TRUE(parser.isValid()); - EXPECT_EQ(ADV_TYPE_REPEATER, parser.getType()); - EXPECT_TRUE(parser.hasName()); - EXPECT_STREQ("node", parser.getName()); - EXPECT_TRUE(parser.hasLatLon()); - EXPECT_EQ(37774900, parser.getIntLat()); - EXPECT_EQ(-122419400, parser.getIntLon()); - EXPECT_DOUBLE_EQ(37.7749, parser.getLat()); - EXPECT_DOUBLE_EQ(-122.4194, parser.getLon()); -} + float packetScore(float, int) override { + return 1.0f; + } -TEST(AdvertData, RoundTripsCoordinateExtremes) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; - - // flags/type byte: sensor advert with both location fields and a name. - WriteU8(app_data, &offset, ADV_TYPE_SENSOR | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: minimum supported latitude, -90.000000 degrees. - WriteI32Le(app_data, &offset, -90000000); - // longitude field: maximum supported longitude, 180.000000 degrees. - WriteI32Le(app_data, &offset, 180000000); - // name field: raw bytes for "edge". - WriteStringLiteral(app_data, &offset, "edge"); - - AdvertDataParser parser(app_data, offset); - - ASSERT_TRUE(parser.isValid()); - EXPECT_TRUE(parser.hasLatLon()); - EXPECT_EQ(-90000000, parser.getIntLat()); - EXPECT_EQ(180000000, parser.getIntLon()); -} + bool startSendRaw(const uint8_t*, int) override { + return true; + } + + bool isSendComplete() override { + return true; + } + + void onSendFinished() override {} + + bool isInRecvMode() const override { + return true; + } +}; + +class NoopPacketManager final : public mesh::PacketManager { +public: + mesh::Packet* allocNew() override { + return nullptr; + } + + void free(mesh::Packet*) override {} + + void queueOutbound(mesh::Packet*, uint8_t, uint32_t) override {} + + mesh::Packet* getNextOutbound(uint32_t) override { + return nullptr; + } + + int getOutboundCount(uint32_t) const override { + return 0; + } + + int getOutboundTotal() const override { + return 0; + } + + int getFreeCount() const override { + return 0; + } + + mesh::Packet* getOutboundByIdx(int) override { + return nullptr; + } + + mesh::Packet* removeOutboundByIdx(int) override { + return nullptr; + } + + void queueInbound(mesh::Packet*, uint32_t) override {} + + mesh::Packet* getNextInbound(uint32_t) override { + return nullptr; + } +}; + +class TestChatMesh final : public BaseChatMesh { +public: + TestChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, + mesh::PacketManager& mgr, mesh::MeshTables& tables) + : BaseChatMesh(radio, ms, rng, rtc, mgr, tables) {} + + mesh::DispatcherAction recv(mesh::Packet* pkt) { + return onRecvPacket(pkt); + } -TEST(AdvertData, RejectsLongitudeOutsideValidRange) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; + std::optional discovered_contact; - // flags/type byte: chat advert with location and name fields present. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: valid latitude so the failure comes from longitude. - WriteI32Le(app_data, &offset, 37774900); - // longitude field: one microdegree above +180.0, which is invalid. - WriteI32Le(app_data, &offset, 180000001); - // name field: parser should reject before the trailing name matters. - WriteStringLiteral(app_data, &offset, "node"); +protected: + void onDiscoveredContact(ContactInfo& contact, bool, uint8_t, const uint8_t*) override { + discovered_contact = contact; + } - AdvertDataParser parser(app_data, offset); + ContactInfo* processAck(const uint8_t*) override { + return nullptr; + } - EXPECT_FALSE(parser.isValid()); + void onContactPathUpdated(const ContactInfo&) override {} + + void onMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {} + + void onCommandDataRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {} + + void onSignedMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const uint8_t*, const char*) override {} + + uint32_t calcFloodTimeoutMillisFor(uint32_t) const override { + return 0; + } + + uint32_t calcDirectTimeoutMillisFor(uint32_t, uint8_t) const override { + return 0; + } + + void onSendTimeout() override {} + + void onChannelMessageRecv(const mesh::GroupChannel&, mesh::Packet*, uint32_t, const char*) override {} + + uint8_t onContactRequest(const ContactInfo&, uint32_t, const uint8_t*, uint8_t, uint8_t*) override { + return 0; + } + + void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) override {} +}; + +mesh::LocalIdentity MakeSenderIdentity() { + return mesh::LocalIdentity(kSenderPrivateKeyHex, kSenderPublicKeyHex); } -TEST(AdvertData, RejectsLongitudeBelowValidRange) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; +mesh::Packet BuildSignedAdvertPacket(const mesh::LocalIdentity& sender, uint32_t timestamp, + const uint8_t* app_data, uint8_t app_data_len) { + mesh::Packet packet; + + // Wire header: flood-routed advert packet with no path hashes yet. + packet.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_ADVERT << PH_TYPE_SHIFT); + packet.path_len = 0; + + int offset = 0; + // Sender public key: used by the receiver to identify who signed the advert. + memcpy(&packet.payload[offset], sender.pub_key, PUB_KEY_SIZE); + offset += PUB_KEY_SIZE; + + // Advert timestamp: the sender's monotonic advert time used for replay checks. + memcpy(&packet.payload[offset], ×tamp, sizeof(timestamp)); + offset += sizeof(timestamp); + + // Signature field: filled after the signed message bytes are assembled below. + uint8_t* signature = &packet.payload[offset]; + offset += SIGNATURE_SIZE; + + // Raw advert app_data: arbitrary bytes authored by the test, not by createAdvert(). + memcpy(&packet.payload[offset], app_data, app_data_len); + offset += app_data_len; + packet.payload_len = offset; + + uint8_t message[PUB_KEY_SIZE + 4 + MAX_ADVERT_DATA_SIZE]; + int message_len = 0; + memcpy(&message[message_len], sender.pub_key, PUB_KEY_SIZE); + message_len += PUB_KEY_SIZE; + memcpy(&message[message_len], ×tamp, sizeof(timestamp)); + message_len += sizeof(timestamp); + memcpy(&message[message_len], app_data, app_data_len); + message_len += app_data_len; + + sender.sign(signature, message, message_len); + return packet; +} - // flags/type byte: chat advert with location and name fields present. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: valid latitude so the failure comes from longitude. - WriteI32Le(app_data, &offset, 37774900); - // longitude field: one microdegree below -180.0, which is invalid. - WriteI32Le(app_data, &offset, -180000001); - // name field: included to keep the payload shape consistent. - WriteStringLiteral(app_data, &offset, "node"); +TEST(AdvertData, ParsesNameOnlyFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with a trailing name field. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); + // name field: raw bytes for "alice", consuming the rest of app_data. + WriteStringLiteral(app_data, &offset, "alice"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + + mesh.recv(&packet); + + ASSERT_TRUE(mesh.discovered_contact.has_value()); + EXPECT_EQ(ADV_TYPE_CHAT, mesh.discovered_contact->type); + EXPECT_STREQ("alice", mesh.discovered_contact->name); + EXPECT_EQ(1704067201U, mesh.discovered_contact->last_advert_timestamp); + EXPECT_EQ(1704067200U, mesh.discovered_contact->lastmod); + EXPECT_EQ(0, mesh.discovered_contact->gps_lat); + EXPECT_EQ(0, mesh.discovered_contact->gps_lon); +} - AdvertDataParser parser(app_data, offset); +TEST(AdvertData, ParsesNameAndCoordinatesFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: repeater advert with lat/lon followed by a name. + WriteU8(app_data, &offset, ADV_TYPE_REPEATER | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: signed little-endian microdegrees for 37.7749. + WriteI32Le(app_data, &offset, 37774900); + // longitude field: signed little-endian microdegrees for -122.4194. + WriteI32Le(app_data, &offset, -122419400); + // name field: raw bytes for "node" after the coordinate fields. + WriteStringLiteral(app_data, &offset, "node"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + + mesh.recv(&packet); + + ASSERT_TRUE(mesh.discovered_contact.has_value()); + EXPECT_EQ(ADV_TYPE_REPEATER, mesh.discovered_contact->type); + EXPECT_STREQ("node", mesh.discovered_contact->name); + EXPECT_EQ(37774900, mesh.discovered_contact->gps_lat); + EXPECT_EQ(-122419400, mesh.discovered_contact->gps_lon); +} - EXPECT_FALSE(parser.isValid()); +TEST(AdvertData, ParsesCoordinateExtremesFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: sensor advert with both location fields and a name. + WriteU8(app_data, &offset, ADV_TYPE_SENSOR | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: minimum supported latitude, -90.000000 degrees. + WriteI32Le(app_data, &offset, -90000000); + // longitude field: maximum supported longitude, 180.000000 degrees. + WriteI32Le(app_data, &offset, 180000000); + // name field: raw bytes for "edge". + WriteStringLiteral(app_data, &offset, "edge"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + + mesh.recv(&packet); + + ASSERT_TRUE(mesh.discovered_contact.has_value()); + EXPECT_EQ(ADV_TYPE_SENSOR, mesh.discovered_contact->type); + EXPECT_STREQ("edge", mesh.discovered_contact->name); + EXPECT_EQ(-90000000, mesh.discovered_contact->gps_lat); + EXPECT_EQ(180000000, mesh.discovered_contact->gps_lon); } -TEST(AdvertData, RejectsLatitudeOutsideValidRange) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; +TEST(AdvertData, RejectsLongitudeOutsideValidRangeFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with location and name fields present. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: valid latitude so the failure comes from longitude. + WriteI32Le(app_data, &offset, 37774900); + // longitude field: one microdegree above +180.0, which is invalid. + WriteI32Le(app_data, &offset, 180000001); + // name field: parser should reject before the trailing name matters. + WriteStringLiteral(app_data, &offset, "node"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + + mesh.recv(&packet); + + EXPECT_FALSE(mesh.discovered_contact.has_value()); +} - // flags/type byte: chat advert with location and name fields present. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: one microdegree above +90.0, which is invalid. - WriteI32Le(app_data, &offset, 90000001); - // longitude field: valid longitude so the failure comes from latitude. - WriteI32Le(app_data, &offset, -122419400); - // name field: included to keep the payload shape consistent. - WriteStringLiteral(app_data, &offset, "node"); +TEST(AdvertData, RejectsLongitudeBelowValidRangeFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with location and name fields present. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: valid latitude so the failure comes from longitude. + WriteI32Le(app_data, &offset, 37774900); + // longitude field: one microdegree below -180.0, which is invalid. + WriteI32Le(app_data, &offset, -180000001); + // name field: included to keep the payload shape consistent. + WriteStringLiteral(app_data, &offset, "node"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + + mesh.recv(&packet); + + EXPECT_FALSE(mesh.discovered_contact.has_value()); +} - AdvertDataParser parser(app_data, offset); +TEST(AdvertData, RejectsLatitudeOutsideValidRangeFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with location and name fields present. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: one microdegree above +90.0, which is invalid. + WriteI32Le(app_data, &offset, 90000001); + // longitude field: valid longitude so the failure comes from latitude. + WriteI32Le(app_data, &offset, -122419400); + // name field: included to keep the payload shape consistent. + WriteStringLiteral(app_data, &offset, "node"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + + mesh.recv(&packet); + + EXPECT_FALSE(mesh.discovered_contact.has_value()); +} - EXPECT_FALSE(parser.isValid()); +TEST(AdvertData, RejectsLatitudeBelowValidRangeFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with location and name fields present. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); + // latitude field: one microdegree below -90.0, which is invalid. + WriteI32Le(app_data, &offset, -90000001); + // longitude field: valid longitude so the failure comes from latitude. + WriteI32Le(app_data, &offset, -122419400); + // name field: included to keep the payload shape consistent. + WriteStringLiteral(app_data, &offset, "node"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); + + mesh.recv(&packet); + + EXPECT_FALSE(mesh.discovered_contact.has_value()); } -TEST(AdvertData, RejectsLatitudeBelowValidRange) { - uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; - size_t offset = 0; +TEST(AdvertData, RejectsForgedSignatureFromNetworkPacket) { + FakeRadio radio; + FakeMillis ms; + FakeRng rng; + FakeRtc rtc(1704067200U); + NoopPacketManager packet_manager; + SimpleMeshTables tables; + TestChatMesh mesh(radio, ms, rng, rtc, packet_manager, tables); + + uint8_t app_data[MAX_ADVERT_DATA_SIZE] = {}; + size_t offset = 0; + + // flags/type byte: chat advert with a trailing name field. + WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_NAME_MASK); + // name field: raw bytes for "mallory". + WriteStringLiteral(app_data, &offset, "mallory"); + + mesh::LocalIdentity sender = MakeSenderIdentity(); + mesh::Packet packet = BuildSignedAdvertPacket(sender, 1704067201U, app_data, offset); - // flags/type byte: chat advert with location and name fields present. - WriteU8(app_data, &offset, ADV_TYPE_CHAT | ADV_LATLON_MASK | ADV_NAME_MASK); - // latitude field: one microdegree below -90.0, which is invalid. - WriteI32Le(app_data, &offset, -90000001); - // longitude field: valid longitude so the failure comes from latitude. - WriteI32Le(app_data, &offset, -122419400); - // name field: included to keep the payload shape consistent. - WriteStringLiteral(app_data, &offset, "node"); + // Corrupt the signature bytes after signing so verification must fail in Mesh::onRecvPacket(). + packet.payload[PUB_KEY_SIZE + 4] ^= 0xFF; - AdvertDataParser parser(app_data, offset); + mesh.recv(&packet); - EXPECT_FALSE(parser.isValid()); + EXPECT_FALSE(mesh.discovered_contact.has_value()); } } // namespace