From f09f2b1366b6fdb972cc9fa493ffafeaa67b7b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Wed, 22 Jul 2026 11:12:06 +0200 Subject: [PATCH] Gate the PKI-decrypt key on signer-proven, not the opportunistic cache (#11116) --- src/mesh/NodeDB.cpp | 16 ++++++++++++++++ src/mesh/NodeDB.h | 4 ++++ src/mesh/Router.cpp | 12 ++++++------ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 0ecfb73d7..63506b51a 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -3783,6 +3783,22 @@ 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 76729578f..00c0cc3b6 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 bf02d8e7b..439541004 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -815,13 +815,13 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p) (ourNode = nodeDB->getMeshNode(p->to)) != nullptr && ourNode->public_key.size > 0) { pkiAttempted = true; LOG_DEBUG("Attempt PKI decryption"); - // 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. + // Resolve the sender's key only for actual PKI-decrypt candidates, not every encrypted channel + // packet: copyPublicKeyForDecrypt() can fall through to a linear scan of TrafficManagement's large + // NodeInfo cache. It returns authoritative keys (hot/warm), or a cold-tier cache key only when it is + // signer-proven - 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;