* Test commit for XEdDSA support * Update to Crypto lib in Meshtatic org * Generate a new node identity on key generation (#7628) * Generate a new node identity on key generation * Fixes * Fixes * Fixes * Messed up * Fixes * Update src/modules/AdminModule.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/mesh/NodeDB.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Figured it out! * Cleanup * Update src/mesh/NodeDB.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/mesh/NodeDB.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/AdminModule.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update crypto commit hash * Some fixes for xeddsa pr (#9610) * fix: add null check for getMeshNode() in NodeInfoModule getMeshNode() can return nullptr for unknown nodes. Dereferencing without a check crashes the firmware when receiving NodeInfo from a node not yet in the database. * fix: enforce XEdDSA signature verification and prevent stripping Previously, failed signature verification still allowed the packet through, making signatures purely cosmetic. Now: - Failed verification drops the packet (DECODE_FAILURE) - Successfully verified nodes get HAS_XEDDSA_SIGNED bitfield set - Unsigned packets from previously-signing nodes are rejected - Log levels reduced from WARN/ERROR to DEBUG/WARN as appropriate * fix: include packet metadata in XEdDSA signature The signature now covers [fromNode | packetId | portnum | payload] instead of just the payload bytes. This prevents: - Replay attacks (different packetId fails verification) - Reattribution (different fromNode fails verification) - Portnum redirection (different portnum fails verification) Also adds a key initialization check to xeddsa_sign (returns false if XEdDSA keys are all zeros) and checks the return value in the encode path. * fix: handle existing key pair in AdminModule security config When a user provides both a valid private key and public key via admin config, the crypto engine's DH private key and owner public key were never loaded. DMs and XEdDSA signing would silently break. Add an else branch to load both keys into the crypto engine. * perf: cache Ed25519 public key conversion in xeddsa_verify curve_to_ed_pub() performs field element parsing, inversion, and multiplication on every call. Since packets from the same node tend to arrive in bursts, a single-entry cache avoids repeating this expensive conversion for consecutive packets from one sender. * fix: skip identity cleanup when node number is unchanged createNewIdentity() was called on every generateCryptoKeyPair(), including normal boots where the same key is regenerated. This caused unnecessary NodeDB writes and old-node cleanup logic to run when the node number hadn't actually changed. Also fixes only zeroing byte[0] of the old node's public key instead of clearing the entire array. * fix: replace hardcoded 120 with derived XEDDSA_SIGNATURE_SIZE constant The payload size check for XEdDSA signing used a magic number (120). Replace with a derivation from DATA_PAYLOAD_LEN and XEDDSA_SIGNATURE_SIZE so the limit adjusts automatically if constants change. This also increases the max signable payload from 120 to 169 bytes, which is still safe since the actual encoded size is checked after pb_encode. * fix: add const qualifiers to XEdDSA verify and curve_to_ed_pub inputs pubKey, payload, and signature parameters in xeddsa_verify are input-only and should not be modified. Same for curve_pubkey in curve_to_ed_pub. * chore: remove commented-out old Crypto dependency in portduino.ini * Leave out the admin module change for now --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> * trunk * protobuf re-update * Protobufs * Merge resolution fix * Put XEDDSA on the right bit * NodeDB update to new nodeInfoLite accessors, etc * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Refine unsigned packet rejection logic in Router (#10534) * use hardware random to fill the first 32 signature bytes with entropy prior to signing. * Add XEdDSA packet-signing policy tests and update dependencies for macos * Minor fixes * integrate XEdDSA support and update dependencies across multiple modules --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Wessel <github@weebl.me> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
232 lines
9.3 KiB
C++
232 lines
9.3 KiB
C++
#include "NodeInfoModule.h"
|
|
#include "Default.h"
|
|
#include "MeshService.h"
|
|
#include "NodeDB.h"
|
|
#include "NodeStatus.h"
|
|
#include "RTC.h"
|
|
#include "Router.h"
|
|
#include "TransmitHistory.h"
|
|
#include "configuration.h"
|
|
#include "main.h"
|
|
#include <Throttle.h>
|
|
#include <algorithm>
|
|
|
|
#ifndef USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS
|
|
#define USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS (12 * 60 * 60)
|
|
#endif
|
|
|
|
NodeInfoModule *nodeInfoModule;
|
|
|
|
static constexpr uint32_t NodeInfoReplySuppressSeconds = USERPREFS_NODEINFO_REPLY_SUPPRESS_SECS;
|
|
|
|
bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_User *pptr)
|
|
{
|
|
suppressReplyForCurrentRequest = false;
|
|
|
|
if (mp.from == nodeDB->getNodeNum()) {
|
|
LOG_WARN("Ignoring packet supposed to be from our own node: %08x", mp.from);
|
|
return false;
|
|
}
|
|
|
|
auto p = *pptr;
|
|
|
|
// Suppress replies to senders we've replied to recently (12H window)
|
|
if (mp.decoded.want_response && !isFromUs(&mp)) {
|
|
const NodeNum sender = getFrom(&mp);
|
|
const uint32_t now = mp.rx_time ? mp.rx_time : getTime();
|
|
auto it = lastNodeInfoSeen.find(sender);
|
|
if (it != lastNodeInfoSeen.end()) {
|
|
uint32_t sinceLast = now >= it->second ? now - it->second : 0;
|
|
if (sinceLast < NodeInfoReplySuppressSeconds) {
|
|
suppressReplyForCurrentRequest = true;
|
|
}
|
|
}
|
|
lastNodeInfoSeen[sender] = now;
|
|
pruneLastNodeInfoCache();
|
|
}
|
|
|
|
if (p.is_licensed != owner.is_licensed) {
|
|
LOG_WARN("Invalid nodeInfo detected, is_licensed mismatch!");
|
|
return true;
|
|
}
|
|
NodeNum sourceNum = getFrom(&mp);
|
|
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(sourceNum);
|
|
if (node && nodeInfoLiteHasXeddsaSigned(node) && !mp.xeddsa_signed) {
|
|
LOG_WARN("Dropping unsigned NodeInfo from node 0x%08x that previously signed", sourceNum);
|
|
return true;
|
|
}
|
|
|
|
// Coerce user.id to be derived from the node number
|
|
snprintf(p.id, sizeof(p.id), "!%08x", getFrom(&mp));
|
|
|
|
bool hasChanged = nodeDB->updateUser(getFrom(&mp), p, mp.channel);
|
|
|
|
bool wasBroadcast = isBroadcast(mp.to);
|
|
|
|
// LOG_DEBUG("did encode");
|
|
// if user has changed while packet was not for us, inform phone
|
|
if (hasChanged && !wasBroadcast && !isToUs(&mp)) {
|
|
auto packetCopy = packetPool.allocCopy(mp); // Keep a copy of the packet for later analysis
|
|
|
|
// Re-encode the user protobuf, as we have stripped out the user.id
|
|
packetCopy->decoded.payload.size = pb_encode_to_bytes(
|
|
packetCopy->decoded.payload.bytes, sizeof(packetCopy->decoded.payload.bytes), &meshtastic_User_msg, &p);
|
|
|
|
service->sendToPhone(packetCopy);
|
|
}
|
|
|
|
pruneLastNodeInfoCache();
|
|
|
|
// LOG_DEBUG("did handleReceived");
|
|
return false; // Let others look at this message also if they want
|
|
}
|
|
|
|
void NodeInfoModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic_User *p)
|
|
{
|
|
// Coerce user.id to be derived from the node number
|
|
snprintf(p->id, sizeof(p->id), "!%08x", getFrom(&mp));
|
|
|
|
// Re-encode the altered protobuf back into the packet
|
|
mp.decoded.payload.size =
|
|
pb_encode_to_bytes(mp.decoded.payload.bytes, sizeof(mp.decoded.payload.bytes), &meshtastic_User_msg, p);
|
|
}
|
|
|
|
void NodeInfoModule::sendOurNodeInfo(NodeNum dest, bool wantReplies, uint8_t channel, bool _shorterTimeout)
|
|
{
|
|
// cancel any not yet sent (now stale) position packets
|
|
if (prevPacketId) // if we wrap around to zero, we'll simply fail to cancel in that rare case (no big deal)
|
|
service->cancelSending(prevPacketId);
|
|
shorterTimeout = _shorterTimeout;
|
|
DEBUG_HEAP_BEFORE;
|
|
meshtastic_MeshPacket *p = allocReply();
|
|
DEBUG_HEAP_AFTER("NodeInfoModule::sendOurNodeInfo", p);
|
|
|
|
if (p) { // Check whether we didn't ignore it
|
|
p->to = dest;
|
|
bool requestWantResponse = (config.device.role != meshtastic_Config_DeviceConfig_Role_TRACKER &&
|
|
config.device.role != meshtastic_Config_DeviceConfig_Role_SENSOR) &&
|
|
wantReplies;
|
|
|
|
p->decoded.want_response = requestWantResponse;
|
|
if (_shorterTimeout)
|
|
p->priority = meshtastic_MeshPacket_Priority_DEFAULT;
|
|
else
|
|
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
|
if (channel > 0) {
|
|
LOG_DEBUG("Send ourNodeInfo to channel %d", channel);
|
|
p->channel = channel;
|
|
}
|
|
|
|
prevPacketId = p->id;
|
|
|
|
service->sendToMesh(p);
|
|
shorterTimeout = false;
|
|
}
|
|
}
|
|
|
|
void NodeInfoModule::triggerImmediateNodeInfoCheck()
|
|
{
|
|
LOG_DEBUG("NodeInfo: scheduling immediate periodic check");
|
|
setIntervalFromNow(0);
|
|
}
|
|
|
|
meshtastic_MeshPacket *NodeInfoModule::allocReply()
|
|
{
|
|
// Only apply suppression when actually replying to someone else's request, not for periodic broadcasts.
|
|
const bool isReplyingToExternalRequest = currentRequest &&
|
|
currentRequest->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
|
currentRequest->decoded.portnum == meshtastic_PortNum_NODEINFO_APP &&
|
|
currentRequest->decoded.want_response && !isFromUs(currentRequest);
|
|
|
|
if (suppressReplyForCurrentRequest && isReplyingToExternalRequest) {
|
|
LOG_DEBUG("Skip send NodeInfo since we heard the requester <12h ago");
|
|
ignoreRequest = true;
|
|
suppressReplyForCurrentRequest = false;
|
|
return NULL;
|
|
}
|
|
|
|
if (!airTime->isTxAllowedChannelUtil(false)) {
|
|
ignoreRequest = true; // Mark it as ignored for MeshModule
|
|
LOG_DEBUG("Skip send NodeInfo > 40%% ch. util");
|
|
return NULL;
|
|
}
|
|
|
|
// Use graduated scaling based on active mesh size (10 minute base, scales with congestion coefficient)
|
|
uint32_t timeoutMs = Default::getConfiguredOrDefaultMsScaled(0, 10 * 60, nodeStatus->getNumOnline());
|
|
uint32_t lastNodeInfo = transmitHistory ? transmitHistory->getLastSentToMeshMillis(meshtastic_PortNum_NODEINFO_APP) : 0;
|
|
if (!shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, timeoutMs)) {
|
|
LOG_DEBUG("Skip send NodeInfo since we sent it <%us ago", timeoutMs / 1000);
|
|
ignoreRequest = true; // Mark it as ignored for MeshModule
|
|
return NULL;
|
|
} else if (shorterTimeout && lastNodeInfo && Throttle::isWithinTimespanMs(lastNodeInfo, 60 * 1000)) {
|
|
// For interactive/urgent requests (e.g., user-triggered or implicit requests), use a shorter 60s timeout
|
|
LOG_DEBUG("Skip send NodeInfo since we sent it <60s ago");
|
|
ignoreRequest = true;
|
|
return NULL;
|
|
} else {
|
|
ignoreRequest = false; // Don't ignore requests anymore
|
|
meshtastic_User u = owner; // deliberate copy: the licensed strip below must not clobber the global owner state
|
|
|
|
// Strip the public key if the user is licensed
|
|
if (u.is_licensed && u.public_key.size > 0) {
|
|
memset(u.public_key.bytes, 0, sizeof(u.public_key.bytes));
|
|
u.public_key.size = 0;
|
|
}
|
|
|
|
// FIXME: Clear the user.id field since it should be derived from node number on the receiving end
|
|
// u.id[0] = '\0';
|
|
|
|
// Ensure our user.id is derived correctly
|
|
strcpy(u.id, nodeDB->getNodeId().c_str());
|
|
|
|
LOG_INFO("Send owner %s/%s/%s", u.id, u.long_name, u.short_name);
|
|
if (transmitHistory)
|
|
transmitHistory->setLastSentToMesh(meshtastic_PortNum_NODEINFO_APP);
|
|
return allocDataProtobuf(u);
|
|
}
|
|
}
|
|
|
|
void NodeInfoModule::pruneLastNodeInfoCache()
|
|
{
|
|
if (!nodeDB || !nodeDB->meshNodes)
|
|
return;
|
|
|
|
const size_t maxEntries = nodeDB->meshNodes->size();
|
|
|
|
for (auto it = lastNodeInfoSeen.begin(); it != lastNodeInfoSeen.end();) {
|
|
if (!nodeDB->getMeshNode(it->first)) {
|
|
it = lastNodeInfoSeen.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
|
|
while (!lastNodeInfoSeen.empty() && lastNodeInfoSeen.size() > maxEntries) {
|
|
auto oldestIt = std::min_element(lastNodeInfoSeen.begin(), lastNodeInfoSeen.end(),
|
|
[](const std::pair<const NodeNum, uint32_t> &lhs,
|
|
const std::pair<const NodeNum, uint32_t> &rhs) { return lhs.second < rhs.second; });
|
|
lastNodeInfoSeen.erase(oldestIt);
|
|
}
|
|
}
|
|
|
|
NodeInfoModule::NodeInfoModule()
|
|
: ProtobufModule("nodeinfo", meshtastic_PortNum_NODEINFO_APP, &meshtastic_User_msg), concurrency::OSThread("NodeInfo")
|
|
{
|
|
isPromiscuous = true; // We always want to update our nodedb, even if we are sniffing on others
|
|
|
|
setIntervalFromNow(setStartDelay()); // Send our initial owner announcement 30 seconds
|
|
// after we start (to give network time to setup)
|
|
}
|
|
|
|
int32_t NodeInfoModule::runOnce()
|
|
{
|
|
// If we changed channels, ask everyone else for their latest info
|
|
bool requestReplies = currentGeneration != radioGeneration;
|
|
currentGeneration = radioGeneration;
|
|
|
|
if (airTime->isTxAllowedAirUtil() && config.device.role != meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) {
|
|
LOG_INFO("Send our nodeinfo to mesh (wantReplies=%d)", requestReplies);
|
|
sendOurNodeInfo(NODENUM_BROADCAST, requestReplies); // Send our info (don't request replies)
|
|
}
|
|
return Default::getConfiguredOrDefaultMs(config.device.node_info_broadcast_secs, default_node_info_broadcast_secs);
|
|
} |