From 60738f078185081f1c38da140fd5104a8c814a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Tue, 21 Jul 2026 12:21:23 +0200 Subject: [PATCH 1/2] Gate the PKI-decrypt key on signer-proven, not the opportunistic cache Resolve the inbound sender key via NodeDB::copyPublicKeyForDecrypt: authoritative (hot/warm) as before, plus a TrafficManagement cold-tier cache key only when it is signer-proven. An unverified TOFU cache key no longer backs pki_encrypted attribution, while a previously-authenticated node evicted to the cache still resolves. Outbound encrypt-to keying is unchanged. --- src/mesh/NodeDB.cpp | 17 +++++++++++++++++ src/mesh/NodeDB.h | 4 ++++ src/mesh/Router.cpp | 9 +++------ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 00eb4b82519..8ee7738a69a 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -3781,6 +3781,23 @@ bool NodeDB::copyPublicKey(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out) return false; } +bool NodeDB::copyPublicKeyForDecrypt(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out) +{ + if (copyPublicKeyAuthoritative(n, out)) + return true; +#if HAS_TRAFFIC_MANAGEMENT + // A cold-tier cache key backs an authenticated decrypt only when signer-proven; unverified TOFU + // cache keys must not. Outbound encryption still uses the opportunistic copyPublicKey(). + bool signerProven = false; + if (trafficManagementModule && trafficManagementModule->copyPublicKey(n, out.bytes, &signerProven) && + signerProven) { + out.size = 32; + return true; + } +#endif + return false; +} + bool NodeDB::isVerifiedSignerForKey(NodeNum n, const uint8_t *key32) { if (!key32) diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 76729578f3d..00c0cc3b6f5 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -364,6 +364,10 @@ class NodeDB /// opportunistic caches) - the pin reference for caches that mirror NodeDB's key hygiene. bool copyPublicKeyAuthoritative(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out); + /// Key for the inbound-decrypt path: authoritative (hot/warm), or a cold-tier cache key only when + /// it is signer-proven. Keeps unverified TOFU cache keys from backing pki_encrypted attribution. + bool copyPublicKeyForDecrypt(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out); + /// True if n is a known XEdDSA signer for exactly `key32` (hot signed bitfield or warm /// signer bit); the key match stops a rotated key inheriting a stale signer verdict. bool isVerifiedSignerForKey(NodeNum n, const uint8_t *key32); diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index f8cd05b5160..8453baffe91 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -624,13 +624,10 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) meshtastic_NodeInfoLite *ourNode = nullptr; if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && rawSize > MESHTASTIC_PKC_OVERHEAD && (ourNode = nodeDB->getMeshNode(p->to)) != nullptr && ourNode->public_key.size > 0) { - // Resolve the sender's public key only for actual PKI-decrypt candidates: prefer NodeDB - // (hot store or warm tier), else a not-yet-committed key held during an in-progress - // key-verification handshake. On a full NodeDB miss, copyPublicKey() falls through to a - // linear scan of TrafficManagement's large NodeInfo cache, so it must not run for every - // encrypted channel packet from an unknown sender - only for packets we might decrypt. + // Authoritative keys (hot/warm), or a signer-proven cold-tier cache key: an unverified TOFU + // cache key must not back authenticated (pki_encrypted, p->from) DM attribution. meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}}; - bool haveRemoteKey = nodeDB->copyPublicKey(p->from, remotePublic); + bool haveRemoteKey = nodeDB->copyPublicKeyForDecrypt(p->from, remotePublic); // A pending key is an unverified identity claim supplied by whoever opened the handshake, so it is // accepted only for the exchange itself (checked after decode). perhapsEncode applies the same rule. bool havePendingKey = false; From 4940b8b9863dd299c0b3d80d3f78d0991b893210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Wed, 22 Jul 2026 09:24:39 +0200 Subject: [PATCH 2/2] trunk fmt --- src/mesh/NodeDB.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index d750fbaa593..63506b51a63 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -3791,8 +3791,7 @@ bool NodeDB::copyPublicKeyForDecrypt(NodeNum n, meshtastic_NodeInfoLite_public_k // A cold-tier cache key backs an authenticated decrypt only when signer-proven; unverified TOFU // cache keys must not. Outbound encryption still uses the opportunistic copyPublicKey(). bool signerProven = false; - if (trafficManagementModule && trafficManagementModule->copyPublicKey(n, out.bytes, &signerProven) && - signerProven) { + if (trafficManagementModule && trafficManagementModule->copyPublicKey(n, out.bytes, &signerProven) && signerProven) { out.size = 32; return true; }