Skip to content

Commit 81eebd6

Browse files
committed
fix(security): reverify lost routing auth handoffs
1 parent 185630c commit 81eebd6

3 files changed

Lines changed: 46 additions & 30 deletions

File tree

src/mesh/FloodingRouter.cpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,7 @@ bool FloodingRouter::perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p)
6868
{
6969
// isRebroadcaster() is duplicated in perhapsRebroadcast(), but this avoids confusing log messages
7070
if (isRebroadcaster() && iface && p->hop_limit > 0) {
71-
// Verify the replacement before deleting the valid lower-hop copy waiting in the TX queue.
72-
// This is intentionally redundant with ReliableRouter's ingress gate: it keeps this helper
73-
// safe if another caller is introduced later.
71+
// Re-authenticate before replacing the queued lower-hop copy so future callers remain safe.
7472
if (passesRoutingAuthGate(p) != RoutingAuthVerdict::ACCEPT)
7573
return true;
7674

src/mesh/Router.cpp

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ static bool routingAuthCacheMatches(const meshtastic_MeshPacket &packet)
9696

9797
static void storeRoutingAuthCache(const meshtastic_MeshPacket &wire, const meshtastic_MeshPacket &authenticated)
9898
{
99+
if (!routingAuthCacheLock)
100+
return;
99101
concurrency::LockGuard guard(routingAuthCacheLock);
100102
routingAuthCache.wire = wire;
101103
routingAuthCache.authenticated = authenticated;
@@ -688,55 +690,60 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
688690
}
689691
#endif
690692

691-
RoutingAuthVerdict passesRoutingAuthGate(const meshtastic_MeshPacket *p)
693+
static RoutingAuthVerdict evaluateRoutingAuth(const meshtastic_MeshPacket &wire, meshtastic_MeshPacket &authenticated)
692694
{
693-
// Routing still needs the original encrypted representation for byte-for-byte relay and for
694-
// MQTT uplink. Authenticate a copy here; handleReceived() performs the normal in-place decode
695-
// only after stateful routing filters have completed.
696-
if (routingAuthCacheMatches(*p))
697-
return RoutingAuthVerdict::ACCEPT;
698-
699-
meshtastic_MeshPacket wire = *p;
700-
meshtastic_MeshPacket authCandidate = *p;
695+
authenticated = wire;
701696
routingAuthEvaluations++;
702-
if (authCandidate.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
697+
if (authenticated.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
703698
// Already-decoded remote ingress (notably Portduino SimRadio) did not pass through a
704699
// decryptor. Never trust serialized local authentication metadata on that boundary.
705-
authCandidate.pki_encrypted = false;
706-
authCandidate.public_key.size = 0;
707-
authCandidate.xeddsa_signed = false;
700+
authenticated.pki_encrypted = false;
701+
authenticated.public_key.size = 0;
702+
authenticated.xeddsa_signed = false;
708703
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
709704
concurrency::LockGuard g(cryptLock);
710-
if (!checkXeddsaReceivePolicy(&authCandidate)) {
711-
LOG_WARN("Already-decoded packet rejected by signature policy");
705+
if (!checkXeddsaReceivePolicy(&authenticated)) {
706+
LOG_WARN("Already-decoded packet rejected by routing signature policy");
712707
return RoutingAuthVerdict::REJECT;
713708
}
714709
#endif
715-
wire = *p;
716-
storeRoutingAuthCache(wire, authCandidate);
717710
return RoutingAuthVerdict::ACCEPT;
718711
}
719-
const DecodeState state = perhapsDecode(&authCandidate);
712+
const DecodeState state = perhapsDecode(&authenticated);
720713
if (state == DecodeState::DECODE_POLICY_REJECT) {
721-
LOG_WARN("Packet rejected by signature policy");
714+
LOG_WARN("Packet rejected by routing signature policy");
722715
return RoutingAuthVerdict::REJECT;
723716
}
724717
if (state == DecodeState::DECODE_FATAL) {
725-
LOG_WARN("Fatal decode error, dropping packet");
718+
LOG_WARN("Fatal decode error during routing authentication");
726719
return RoutingAuthVerdict::REJECT;
727720
}
728721
if (state == DecodeState::DECODE_FAILURE) {
729-
LOG_WARN("Decryptable packet failed decoding, dropping packet");
722+
LOG_WARN("Decryptable packet failed routing authentication");
730723
return RoutingAuthVerdict::REJECT;
731724
}
732725

733726
// Only an explicit unknown-channel result remains eligible for opaque relay.
734727
if (state == DecodeState::DECODE_OPAQUE)
735728
return RoutingAuthVerdict::OPAQUE_RELAY_ONLY;
736-
storeRoutingAuthCache(wire, authCandidate);
737729
return RoutingAuthVerdict::ACCEPT;
738730
}
739731

732+
RoutingAuthVerdict passesRoutingAuthGate(const meshtastic_MeshPacket *p)
733+
{
734+
// Routing still needs the original encrypted representation for byte-for-byte relay and for
735+
// MQTT uplink. Authenticate a copy here; handleReceived() performs the normal in-place decode
736+
// only after stateful routing filters have completed.
737+
if (routingAuthCacheMatches(*p))
738+
return RoutingAuthVerdict::ACCEPT;
739+
740+
meshtastic_MeshPacket authenticated = meshtastic_MeshPacket_init_zero;
741+
const RoutingAuthVerdict verdict = evaluateRoutingAuth(*p, authenticated);
742+
if (verdict == RoutingAuthVerdict::ACCEPT)
743+
storeRoutingAuthCache(*p, authenticated);
744+
return verdict;
745+
}
746+
740747
#if !(MESHTASTIC_EXCLUDE_PKI)
741748
// The fallback costs three X25519 ops before the AEAD tag is checked. Budget is global because p->from is
742749
// attacker-controlled; successful runs refund, and their key is then persisted for the fast path.
@@ -1206,7 +1213,7 @@ NodeNum Router::getNodeNum()
12061213
* Handle any packet that is received by an interface on this node.
12071214
* Note: some packets may merely being passed through this node and will be forwarded elsewhere.
12081215
*/
1209-
void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
1216+
void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src, bool routingAuthRequired)
12101217
{
12111218
bool skipHandle = false;
12121219

@@ -1221,8 +1228,15 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
12211228

12221229
// Consume the decoded/authenticated handoff after preserving the exact encrypted packet and
12231230
// before mutating any packet fields that participate in the exact cache match.
1224-
if (src == RX_SRC_RADIO)
1225-
applyRoutingAuthCache(p);
1231+
if (routingAuthRequired && !applyRoutingAuthCache(p)) {
1232+
meshtastic_MeshPacket authenticated = meshtastic_MeshPacket_init_zero;
1233+
if (evaluateRoutingAuth(*p, authenticated) != RoutingAuthVerdict::ACCEPT) {
1234+
LOG_WARN("Routing authentication handoff was lost and packet re-verification failed");
1235+
packetPool.release(p_encrypted);
1236+
return;
1237+
}
1238+
*p = authenticated;
1239+
}
12261240

12271241
// Also, we should set the time from the ISR and it should have msec level resolution.
12281242
// Keep the decoded working packet and encrypted MQTT copy on the same local arrival timestamp.
@@ -1426,6 +1440,6 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
14261440

14271441
// Note: we avoid calling shouldFilterReceived if we are supposed to ignore certain nodes - because some overrides might
14281442
// cache/learn of the existence of nodes (i.e. FloodRouter) that they should not
1429-
handleReceived(p);
1443+
handleReceived(p, RX_SRC_RADIO, true);
14301444
packetPool.release(p);
14311445
}

src/mesh/Router.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ class Router : protected concurrency::OSThread, protected PacketHistory
9999
before us */
100100
uint32_t rxDupe = 0, txRelayCanceled = 0;
101101

102+
#ifdef PIO_UNIT_TESTING
103+
void handleReceivedAfterRoutingGateForTest(meshtastic_MeshPacket *p) { handleReceived(p, RX_SRC_RADIO, true); }
104+
#endif
105+
102106
protected:
103107
friend class RoutingModule;
104108

@@ -158,7 +162,7 @@ class Router : protected concurrency::OSThread, protected PacketHistory
158162
* Note: this packet will never be called for messages sent/generated by this node.
159163
* Note: this method will free the provided packet.
160164
*/
161-
void handleReceived(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO);
165+
void handleReceived(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO, bool routingAuthRequired = false);
162166

163167
/** Frees the provided packet, and generates a NAK indicating the specifed error while sending */
164168
void abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p);

0 commit comments

Comments
 (0)