TrafficManagement: don't learn identity from a signer's unsigned NodeInfo (#11035)
This commit is contained in:
co-authored by
GitHub
parent
8ebdae9921
commit
ed3afdbc61
@@ -744,8 +744,13 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
|
|||||||
if (cfg.nodeinfo_direct_response_max_hops > 0 && mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP &&
|
if (cfg.nodeinfo_direct_response_max_hops > 0 && mp.decoded.portnum == meshtastic_PortNum_NODEINFO_APP &&
|
||||||
mp.decoded.want_response && !isBroadcast(mp.to) && !isToUs(&mp) && !isFromUs(&mp)) {
|
mp.decoded.want_response && !isBroadcast(mp.to) && !isToUs(&mp) && !isFromUs(&mp)) {
|
||||||
if (shouldRespondToNodeInfo(&mp, true)) {
|
if (shouldRespondToNodeInfo(&mp, true)) {
|
||||||
|
// Unicast NodeInfo is never signed, so a known signer's identity claim here is
|
||||||
|
// unauthenticated: don't overwrite its stored name. The cached response is unaffected.
|
||||||
|
const meshtastic_NodeInfoLite *senderNode = nodeDB->getMeshNode(getFrom(&mp));
|
||||||
|
const bool unauthenticatedSigner = senderNode && nodeInfoLiteHasXeddsaSigned(senderNode) && !mp.xeddsa_signed;
|
||||||
meshtastic_User requester = meshtastic_User_init_zero;
|
meshtastic_User requester = meshtastic_User_init_zero;
|
||||||
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_User_msg, &requester)) {
|
if (!unauthenticatedSigner &&
|
||||||
|
pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_User_msg, &requester)) {
|
||||||
nodeDB->updateUser(getFrom(&mp), requester, mp.channel);
|
nodeDB->updateUser(getFrom(&mp), requester, mp.channel);
|
||||||
}
|
}
|
||||||
logAction("respond", &mp, "nodeinfo-cache");
|
logAction("respond", &mp, "nodeinfo-cache");
|
||||||
|
|||||||
@@ -110,6 +110,21 @@ class MockNodeDB : public NodeDB
|
|||||||
clearCachedNode();
|
clearCachedNode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seed a hot-store node that has been learned as an XEdDSA signer, with a known name, so the
|
||||||
|
// identity-update gate can be exercised. Distinct from setCachedNode() so a separate cached
|
||||||
|
// node (the direct-response target) can coexist.
|
||||||
|
void setSignerHotNode(NodeNum n, const char *longName)
|
||||||
|
{
|
||||||
|
if (meshNodes->size() < 2)
|
||||||
|
meshNodes->resize(2);
|
||||||
|
(*meshNodes)[1] = meshtastic_NodeInfoLite_init_zero;
|
||||||
|
(*meshNodes)[1].num = n;
|
||||||
|
nodeInfoLiteSetBit(&(*meshNodes)[1], NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, true);
|
||||||
|
nodeInfoLiteSetBit(&(*meshNodes)[1], NODEINFO_BITFIELD_HAS_USER_MASK, true);
|
||||||
|
strncpy((*meshNodes)[1].long_name, longName, sizeof((*meshNodes)[1].long_name) - 1);
|
||||||
|
numMeshNodes = 2;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool hasCachedNode = false;
|
bool hasCachedNode = false;
|
||||||
NodeNum cachedNodeNum = 0;
|
NodeNum cachedNodeNum = 0;
|
||||||
@@ -589,6 +604,45 @@ static void test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo(void)
|
|||||||
TEST_ASSERT_EQUAL_UINT8(request.channel, requestor->channel);
|
TEST_ASSERT_EQUAL_UINT8(request.channel, requestor->channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A unicast NodeInfo request is never signed, so a known signer's identity claim on the
|
||||||
|
* direct-response path is unauthenticated. It must not overwrite the stored name (spoofing
|
||||||
|
* defense), while the direct response itself still goes out. The non-signer case
|
||||||
|
* (test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo) is the control: it still learns.
|
||||||
|
*/
|
||||||
|
static void test_tm_nodeinfo_directResponse_ignoresUnsignedSignerIdentity(void)
|
||||||
|
{
|
||||||
|
moduleConfig.traffic_management.nodeinfo_direct_response_max_hops = 10;
|
||||||
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
||||||
|
mockNodeDB->setCachedNode(kTargetNode); // the direct-response target
|
||||||
|
mockNodeDB->setSignerHotNode(kRemoteNode, "victim-real"); // the requester is a known signer
|
||||||
|
|
||||||
|
MockRouter mockRouter;
|
||||||
|
mockRouter.addInterface(std::unique_ptr<RadioInterface>(new MockRadioInterface()));
|
||||||
|
MeshService mockService;
|
||||||
|
router = &mockRouter;
|
||||||
|
service = &mockService;
|
||||||
|
|
||||||
|
TrafficManagementModuleTestShim module;
|
||||||
|
meshtastic_MeshPacket request = makeNodeInfoPacket(kRemoteNode, "attacker-name", "atk");
|
||||||
|
request.to = kTargetNode;
|
||||||
|
request.decoded.want_response = true;
|
||||||
|
request.id = 0x0A0B0C0D;
|
||||||
|
request.hop_start = 3;
|
||||||
|
request.hop_limit = 3;
|
||||||
|
request.xeddsa_signed = false; // unicast: never signed
|
||||||
|
|
||||||
|
ProcessMessage result = module.handleReceived(request);
|
||||||
|
|
||||||
|
// The response still went out (the request was consumed from cache)...
|
||||||
|
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(result));
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(1, module.getStats().nodeinfo_cache_hits);
|
||||||
|
// ...but the known signer's stored name was not overwritten by the unauthenticated claim.
|
||||||
|
const meshtastic_NodeInfoLite *requestor = mockNodeDB->getMeshNode(kRemoteNode);
|
||||||
|
TEST_ASSERT_NOT_NULL(requestor);
|
||||||
|
TEST_ASSERT_EQUAL_STRING("victim-real", requestor->long_name);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify client role only answers direct (0-hop) NodeInfo requests.
|
* Verify client role only answers direct (0-hop) NodeInfo requests.
|
||||||
* Important so clients do not answer relayed requests outside intended scope.
|
* Important so clients do not answer relayed requests outside intended scope.
|
||||||
@@ -1715,6 +1769,7 @@ TM_TEST_ENTRY void setup()
|
|||||||
RUN_TEST(test_tm_nodeinfo_routerClamp_skipsWhenTooManyHops);
|
RUN_TEST(test_tm_nodeinfo_routerClamp_skipsWhenTooManyHops);
|
||||||
RUN_TEST(test_tm_nodeinfo_directResponse_respondsFromCache);
|
RUN_TEST(test_tm_nodeinfo_directResponse_respondsFromCache);
|
||||||
RUN_TEST(test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo);
|
RUN_TEST(test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo);
|
||||||
|
RUN_TEST(test_tm_nodeinfo_directResponse_ignoresUnsignedSignerIdentity);
|
||||||
RUN_TEST(test_tm_nodeinfo_clientClamp_skipsWhenNotDirect);
|
RUN_TEST(test_tm_nodeinfo_clientClamp_skipsWhenNotDirect);
|
||||||
#if !(defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM))
|
#if !(defined(ARCH_ESP32) && defined(BOARD_HAS_PSRAM))
|
||||||
RUN_TEST(test_tm_nodeinfo_directResponse_withoutNodeDbEntry_skips);
|
RUN_TEST(test_tm_nodeinfo_directResponse_withoutNodeDbEntry_skips);
|
||||||
|
|||||||
Reference in New Issue
Block a user