Merge branch 'develop' into agent/keyverification-allocation-safety
This commit is contained in:
@@ -1111,6 +1111,23 @@ void setup()
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(SENSECAP_INDICATOR)
|
||||||
|
// The ST7701 panel shares SCK/MOSI/MISO (41/48/47) with the SX1262, and its host is SPI2_HOST,
|
||||||
|
// which on the S3 is the same peripheral as the Arduino `SPI` object (FSPI == SPI2).
|
||||||
|
// LovyanGFX bit-bangs the ST7701 init sequence on those pins, and because this variant builds
|
||||||
|
// with USE_ARDUINO_HAL_GPIO it does so via Arduino pinMode()/digitalWrite(). pinMode() calls
|
||||||
|
// perimanSetPinBus(.., ESP32_BUS_TYPE_GPIO, ..), whose deinit callback (spiDetachBus_SCK) ends
|
||||||
|
// up in spiStopBus() and gates the SPI2 clock. RadioLib then spins forever in spiTransferByte()
|
||||||
|
// waiting on cmd.update, which never clears on a stopped peripheral, and the watchdog fires.
|
||||||
|
//
|
||||||
|
// Restart the bus here, after the panel is up and before the radio is touched. Note that
|
||||||
|
// SPIClass::begin() early-returns when _spi is already non-NULL, so end() first is required.
|
||||||
|
SPI.end();
|
||||||
|
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, -1); // CS is an IO-expander pin, driven by RadioLib
|
||||||
|
SPI.setFrequency(4000000);
|
||||||
|
LOG_DEBUG("SPI2 restarted after ST7701 init (SCK=%d, MISO=%d, MOSI=%d)", LORA_SCK, LORA_MISO, LORA_MOSI);
|
||||||
|
#endif
|
||||||
|
|
||||||
auto rIf = initLoRa();
|
auto rIf = initLoRa();
|
||||||
|
|
||||||
lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation)
|
lateInitVariant(); // Do board specific init (see extra_variants/README.md for documentation)
|
||||||
@@ -1278,6 +1295,9 @@ extern meshtastic_DeviceMetadata getDeviceMetadata()
|
|||||||
|
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
deviceMetadata.hasPKC = true;
|
deviceMetadata.hasPKC = true;
|
||||||
|
#endif
|
||||||
|
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||||
|
deviceMetadata.has_xeddsa = true;
|
||||||
#endif
|
#endif
|
||||||
return deviceMetadata;
|
return deviceMetadata;
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-19
@@ -51,8 +51,8 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
|
|||||||
LOG_DEBUG("Repeated reliable tx");
|
LOG_DEBUG("Repeated reliable tx");
|
||||||
// Check if it's still in the Tx queue, if not, we have to relay it again
|
// Check if it's still in the Tx queue, if not, we have to relay it again
|
||||||
if (!findInTxQueue(p->from, p->id)) {
|
if (!findInTxQueue(p->from, p->id)) {
|
||||||
reprocessPacket(p);
|
if (reprocessPacket(p))
|
||||||
perhapsRebroadcast(p);
|
perhapsRebroadcast(p);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
perhapsCancelDupe(p);
|
perhapsCancelDupe(p);
|
||||||
@@ -68,6 +68,12 @@ bool FloodingRouter::perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p)
|
|||||||
{
|
{
|
||||||
// isRebroadcaster() is duplicated in perhapsRebroadcast(), but this avoids confusing log messages
|
// isRebroadcaster() is duplicated in perhapsRebroadcast(), but this avoids confusing log messages
|
||||||
if (isRebroadcaster() && iface && p->hop_limit > 0) {
|
if (isRebroadcaster() && iface && p->hop_limit > 0) {
|
||||||
|
// Verify the replacement before deleting the valid lower-hop copy waiting in the TX queue.
|
||||||
|
// This is intentionally redundant with ReliableRouter's ingress gate: it keeps this helper
|
||||||
|
// safe if another caller is introduced later.
|
||||||
|
if (passesRoutingAuthGate(const_cast<meshtastic_MeshPacket *>(p)) != RoutingAuthVerdict::ACCEPT)
|
||||||
|
return true;
|
||||||
|
|
||||||
// If we overhear a duplicate copy of the packet with more hops left than the one we are waiting to
|
// If we overhear a duplicate copy of the packet with more hops left than the one we are waiting to
|
||||||
// rebroadcast, then remove the packet currently sitting in the TX queue and use this one instead.
|
// rebroadcast, then remove the packet currently sitting in the TX queue and use this one instead.
|
||||||
uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining
|
uint8_t dropThreshold = p->hop_limit; // remove queued packets that have fewer hops remaining
|
||||||
@@ -75,7 +81,8 @@ bool FloodingRouter::perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p)
|
|||||||
LOG_DEBUG("Processing upgraded packet 0x%08x for rebroadcast with hop limit %d (dropping queued < %d)", p->id,
|
LOG_DEBUG("Processing upgraded packet 0x%08x for rebroadcast with hop limit %d (dropping queued < %d)", p->id,
|
||||||
p->hop_limit, dropThreshold);
|
p->hop_limit, dropThreshold);
|
||||||
|
|
||||||
reprocessPacket(p);
|
if (!reprocessPacket(p))
|
||||||
|
return true;
|
||||||
perhapsRebroadcast(p);
|
perhapsRebroadcast(p);
|
||||||
|
|
||||||
rxDupe++;
|
rxDupe++;
|
||||||
@@ -87,32 +94,24 @@ bool FloodingRouter::perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void FloodingRouter::reprocessPacket(const meshtastic_MeshPacket *p)
|
bool FloodingRouter::reprocessPacket(const meshtastic_MeshPacket *p)
|
||||||
{
|
{
|
||||||
|
if (p->which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
|
||||||
|
auto decodedState = perhapsDecode(const_cast<meshtastic_MeshPacket *>(p));
|
||||||
|
if (decodedState != DecodeState::DECODE_SUCCESS && decodedState != DecodeState::DECODE_OPAQUE)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (nodeDB)
|
if (nodeDB)
|
||||||
nodeDB->updateFrom(*p);
|
nodeDB->updateFrom(*p);
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
#if !MESHTASTIC_EXCLUDE_TRACEROUTE
|
||||||
if (traceRouteModule && p->which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
|
|
||||||
// If we got a packet that is not decoded, try to decode it so we can check for traceroute.
|
|
||||||
auto decodedState = perhapsDecode(const_cast<meshtastic_MeshPacket *>(p));
|
|
||||||
if (decodedState == DecodeState::DECODE_SUCCESS) {
|
|
||||||
// parsing was successful, print for debugging
|
|
||||||
printPacket("reprocessPacket(DUP)", p);
|
|
||||||
} else {
|
|
||||||
// Fatal decoding error, we can't do anything with this packet
|
|
||||||
LOG_WARN(
|
|
||||||
"FloodingRouter::reprocessPacket: Fatal decode error (state=%d, id=0x%08x, from=%u), can't check for traceroute",
|
|
||||||
static_cast<int>(decodedState), p->id, getFrom(p));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
if (traceRouteModule && p->which_payload_variant == meshtastic_MeshPacket_decoded_tag &&
|
||||||
p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) {
|
p->decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP) {
|
||||||
traceRouteModule->processUpgradedPacket(*p);
|
traceRouteModule->processUpgradedPacket(*p);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p)
|
bool FloodingRouter::roleAllowsCancelingDupe(const meshtastic_MeshPacket *p)
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class FloodingRouter : public Router
|
|||||||
bool perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p);
|
bool perhapsHandleUpgradedPacket(const meshtastic_MeshPacket *p);
|
||||||
|
|
||||||
/* Call when we receive a packet that needs some reprocessing, but afterwards should be filtered */
|
/* Call when we receive a packet that needs some reprocessing, but afterwards should be filtered */
|
||||||
void reprocessPacket(const meshtastic_MeshPacket *p);
|
bool reprocessPacket(const meshtastic_MeshPacket *p);
|
||||||
|
|
||||||
// Return false for roles like ROUTER which should always rebroadcast even when we've heard another rebroadcast of
|
// Return false for roles like ROUTER which should always rebroadcast even when we've heard another rebroadcast of
|
||||||
// the same packet
|
// the same packet
|
||||||
|
|||||||
@@ -11,6 +11,25 @@
|
|||||||
|
|
||||||
NextHopRouter::NextHopRouter() {}
|
NextHopRouter::NextHopRouter() {}
|
||||||
|
|
||||||
|
bool NextHopRouter::relayOpaquePacket(const meshtastic_MeshPacket *p)
|
||||||
|
{
|
||||||
|
// Opaque traffic is never admitted to PacketHistory, NodeDB, modules, phone, MQTT, or ACK
|
||||||
|
// handling. Relay only from the immutable outer routing header and let hop exhaustion bound it.
|
||||||
|
const auto mode = config.device.rebroadcast_mode;
|
||||||
|
if (!iface || isToUs(p) || isFromUs(p) || p->id == 0 || p->hop_limit == 0 || !isRebroadcaster() || owner.is_licensed ||
|
||||||
|
!IS_ONE_OF(mode, meshtastic_Config_DeviceConfig_RebroadcastMode_ALL,
|
||||||
|
meshtastic_Config_DeviceConfig_RebroadcastMode_ALL_SKIP_DECODING) ||
|
||||||
|
(p->next_hop != NO_NEXT_HOP_PREFERENCE && p->next_hop != nodeDB->getLastByteOfNodeNum(getNodeNum())))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
meshtastic_MeshPacket *relay = packetPool.allocCopy(*p);
|
||||||
|
if (!relay)
|
||||||
|
return false;
|
||||||
|
relay->hop_limit--;
|
||||||
|
relay->relay_node = nodeDB->getLastByteOfNodeNum(getNodeNum());
|
||||||
|
return Router::send(relay) == ERRNO_OK;
|
||||||
|
}
|
||||||
|
|
||||||
PendingPacket::PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions)
|
PendingPacket::PendingPacket(meshtastic_MeshPacket *p, uint8_t numRetransmissions)
|
||||||
{
|
{
|
||||||
packet = p;
|
packet = p;
|
||||||
@@ -65,16 +84,15 @@ bool NextHopRouter::shouldFilterReceived(const meshtastic_MeshPacket *p)
|
|||||||
LOG_INFO("Fallback to flooding from relay_node=0x%x", p->relay_node);
|
LOG_INFO("Fallback to flooding from relay_node=0x%x", p->relay_node);
|
||||||
// Check if it's still in the Tx queue, if not, we have to relay it again
|
// Check if it's still in the Tx queue, if not, we have to relay it again
|
||||||
if (!findInTxQueue(p->from, p->id)) {
|
if (!findInTxQueue(p->from, p->id)) {
|
||||||
reprocessPacket(p);
|
if (reprocessPacket(p))
|
||||||
perhapsRebroadcast(p);
|
perhapsRebroadcast(p);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bool isRepeated = getHopsAway(*p) == 0;
|
bool isRepeated = getHopsAway(*p) == 0;
|
||||||
// If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again
|
// If repeated and not in Tx queue anymore, try relaying again, or if we are the destination, send the ACK again
|
||||||
if (isRepeated) {
|
if (isRepeated) {
|
||||||
if (!findInTxQueue(p->from, p->id)) {
|
if (!findInTxQueue(p->from, p->id)) {
|
||||||
reprocessPacket(p);
|
if (reprocessPacket(p) && !perhapsRebroadcast(p) && isToUs(p) && p->want_ack) {
|
||||||
if (!perhapsRebroadcast(p) && isToUs(p) && p->want_ack) {
|
|
||||||
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
|
sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ class NextHopRouter : public FloodingRouter
|
|||||||
* @return true to abandon the packet
|
* @return true to abandon the packet
|
||||||
*/
|
*/
|
||||||
virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override;
|
virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override;
|
||||||
|
bool relayOpaquePacket(const meshtastic_MeshPacket *p) override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look for packets we need to relay
|
* Look for packets we need to relay
|
||||||
|
|||||||
@@ -1861,6 +1861,8 @@ bool NodeDB::enforceSatelliteCaps()
|
|||||||
// them if they do); otherwise tracker/sensor/tak_tracker are role-protected.
|
// them if they do); otherwise tracker/sensor/tak_tracker are role-protected.
|
||||||
static uint8_t warmProtectedCategory(const meshtastic_NodeInfoLite &n)
|
static uint8_t warmProtectedCategory(const meshtastic_NodeInfoLite &n)
|
||||||
{
|
{
|
||||||
|
if (nodeInfoLiteHasXeddsaSigned(&n))
|
||||||
|
return static_cast<uint8_t>(WarmProtected::XeddsaSigner);
|
||||||
if (n.bitfield & (NODEINFO_BITFIELD_IS_FAVORITE_MASK | NODEINFO_BITFIELD_IS_IGNORED_MASK |
|
if (n.bitfield & (NODEINFO_BITFIELD_IS_FAVORITE_MASK | NODEINFO_BITFIELD_IS_IGNORED_MASK |
|
||||||
NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK))
|
NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK))
|
||||||
return static_cast<uint8_t>(WarmProtected::Flag);
|
return static_cast<uint8_t>(WarmProtected::Flag);
|
||||||
@@ -3934,6 +3936,8 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
|||||||
lite->public_key.size = 32;
|
lite->public_key.size = 32;
|
||||||
memcpy(lite->public_key.bytes, warm.public_key, 32);
|
memcpy(lite->public_key.bytes, warm.public_key, 32);
|
||||||
}
|
}
|
||||||
|
if (warmProtOf(warm) == static_cast<uint8_t>(WarmProtected::XeddsaSigner))
|
||||||
|
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, true);
|
||||||
LOG_MIGRATION("Rehydrated node 0x%08x from warm tier (key=%d)", n, lite->public_key.size == 32);
|
LOG_MIGRATION("Rehydrated node 0x%08x from warm tier (key=%d)", n, lite->public_key.size == 32);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+249
-21
@@ -12,6 +12,8 @@
|
|||||||
#include "mesh-pb-constants.h"
|
#include "mesh-pb-constants.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#include "modules/RoutingModule.h"
|
#include "modules/RoutingModule.h"
|
||||||
|
#include <ErriezCRC32.h>
|
||||||
|
#include <pb_decode.h>
|
||||||
#include <pb_encode.h>
|
#include <pb_encode.h>
|
||||||
#if HAS_TRAFFIC_MANAGEMENT
|
#if HAS_TRAFFIC_MANAGEMENT
|
||||||
#endif
|
#endif
|
||||||
@@ -66,6 +68,79 @@ Allocator<meshtastic_MeshPacket> &packetPool = staticPool;
|
|||||||
|
|
||||||
static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__));
|
static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__));
|
||||||
|
|
||||||
|
struct RoutingAuthCache {
|
||||||
|
bool valid = false;
|
||||||
|
meshtastic_Config_SecurityConfig_PacketSignaturePolicy policy =
|
||||||
|
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_BALANCED;
|
||||||
|
meshtastic_MeshPacket wire = meshtastic_MeshPacket_init_zero;
|
||||||
|
meshtastic_MeshPacket authenticated = meshtastic_MeshPacket_init_zero;
|
||||||
|
};
|
||||||
|
static RoutingAuthCache routingAuthCache;
|
||||||
|
static concurrency::Lock *routingAuthCacheLock;
|
||||||
|
static uint32_t routingAuthEvaluations;
|
||||||
|
|
||||||
|
static bool routingAuthCacheMatches(const meshtastic_MeshPacket &packet)
|
||||||
|
{
|
||||||
|
if (!routingAuthCacheLock)
|
||||||
|
return false;
|
||||||
|
concurrency::LockGuard guard(routingAuthCacheLock);
|
||||||
|
if (!routingAuthCache.valid)
|
||||||
|
return false;
|
||||||
|
if (routingAuthCache.policy != config.security.packet_signature_policy ||
|
||||||
|
memcmp(&routingAuthCache.wire, &packet, sizeof(packet)) != 0) {
|
||||||
|
routingAuthCache.valid = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void storeRoutingAuthCache(const meshtastic_MeshPacket &wire, const meshtastic_MeshPacket &authenticated)
|
||||||
|
{
|
||||||
|
concurrency::LockGuard guard(routingAuthCacheLock);
|
||||||
|
routingAuthCache.wire = wire;
|
||||||
|
routingAuthCache.authenticated = authenticated;
|
||||||
|
routingAuthCache.policy = config.security.packet_signature_policy;
|
||||||
|
routingAuthCache.valid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool applyRoutingAuthCache(meshtastic_MeshPacket *packet)
|
||||||
|
{
|
||||||
|
if (!routingAuthCacheLock)
|
||||||
|
return false;
|
||||||
|
concurrency::LockGuard guard(routingAuthCacheLock);
|
||||||
|
if (!routingAuthCache.valid || routingAuthCache.policy != config.security.packet_signature_policy ||
|
||||||
|
memcmp(&routingAuthCache.wire, packet, sizeof(*packet)) != 0) {
|
||||||
|
routingAuthCache.valid = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*packet = routingAuthCache.authenticated;
|
||||||
|
routingAuthCache.valid = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clearRoutingAuthCache()
|
||||||
|
{
|
||||||
|
if (!routingAuthCacheLock)
|
||||||
|
return;
|
||||||
|
concurrency::LockGuard guard(routingAuthCacheLock);
|
||||||
|
routingAuthCache.valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef PIO_UNIT_TESTING
|
||||||
|
uint32_t routingAuthEvaluationCount()
|
||||||
|
{
|
||||||
|
return routingAuthEvaluations;
|
||||||
|
}
|
||||||
|
void resetRoutingAuthEvaluationCount()
|
||||||
|
{
|
||||||
|
routingAuthEvaluations = 0;
|
||||||
|
if (routingAuthCacheLock) {
|
||||||
|
concurrency::LockGuard guard(routingAuthCacheLock);
|
||||||
|
routingAuthCache.valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
@@ -84,6 +159,8 @@ Router::Router() : concurrency::OSThread("Router"), fromRadioQueue(MAX_RX_FROMRA
|
|||||||
// init Lockguard for crypt operations
|
// init Lockguard for crypt operations
|
||||||
assert(!cryptLock);
|
assert(!cryptLock);
|
||||||
cryptLock = new concurrency::Lock();
|
cryptLock = new concurrency::Lock();
|
||||||
|
if (!routingAuthCacheLock)
|
||||||
|
routingAuthCacheLock = new concurrency::Lock();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
|
bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
|
||||||
@@ -248,8 +325,10 @@ ErrorCode Router::sendLocal(meshtastic_MeshPacket *p, RxSource src)
|
|||||||
// No need to deliver externally if the destination is the local node
|
// No need to deliver externally if the destination is the local node
|
||||||
if (isToUs(p)) {
|
if (isToUs(p)) {
|
||||||
printPacket("Enqueued local", p);
|
printPacket("Enqueued local", p);
|
||||||
enqueueReceivedMessage(p);
|
// Preserve the trusted origin explicitly. Queueing used to erase src and make a local
|
||||||
return ERRNO_OK;
|
// phone/module packet indistinguishable from remote already-decoded ingress.
|
||||||
|
handleReceived(p, src);
|
||||||
|
return ERRNO_SHOULD_RELEASE;
|
||||||
} else if (!iface) {
|
} else if (!iface) {
|
||||||
// We must be sending to remote nodes also, fail if no interface found
|
// We must be sending to remote nodes also, fail if no interface found
|
||||||
abortSendAndNak(meshtastic_Routing_Error_NO_INTERFACE, p);
|
abortSendAndNak(meshtastic_Routing_Error_NO_INTERFACE, p);
|
||||||
@@ -500,18 +579,55 @@ static bool canonicalSignableSize(meshtastic_Data *d, size_t *size)
|
|||||||
return pb_get_encoded_size(size, &meshtastic_Data_msg, d);
|
return pb_get_encoded_size(size, &meshtastic_Data_msg, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class NodeInfoBootstrapResult { NOT_APPLICABLE, VERIFIED, INVALID };
|
||||||
|
|
||||||
|
static NodeInfoBootstrapResult verifyFirstContactNodeInfo(meshtastic_MeshPacket *p)
|
||||||
|
{
|
||||||
|
if (p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP)
|
||||||
|
return NodeInfoBootstrapResult::NOT_APPLICABLE;
|
||||||
|
|
||||||
|
meshtastic_User user = meshtastic_User_init_zero;
|
||||||
|
if (!pb_decode_from_bytes(p->decoded.payload.bytes, p->decoded.payload.size, &meshtastic_User_msg, &user) ||
|
||||||
|
user.public_key.size != 32 || crc32Buffer(user.public_key.bytes, user.public_key.size) != p->from ||
|
||||||
|
!crypto->xeddsa_verify(user.public_key.bytes, p->from, p->id, p->decoded.portnum, p->decoded.payload.bytes,
|
||||||
|
p->decoded.payload.size, p->decoded.xeddsa_signature.bytes)) {
|
||||||
|
return NodeInfoBootstrapResult::INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getOrCreateMeshNode(p->from);
|
||||||
|
if (!node)
|
||||||
|
return NodeInfoBootstrapResult::INVALID;
|
||||||
|
node->public_key.size = user.public_key.size;
|
||||||
|
memcpy(node->public_key.bytes, user.public_key.bytes, user.public_key.size);
|
||||||
|
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, true);
|
||||||
|
p->xeddsa_signed = true;
|
||||||
|
LOG_DEBUG("Verified first-contact XEdDSA NodeInfo from 0x%08x", p->from);
|
||||||
|
return NodeInfoBootstrapResult::VERIFIED;
|
||||||
|
}
|
||||||
|
|
||||||
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
|
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
|
||||||
{
|
{
|
||||||
|
const auto policy = config.security.packet_signature_policy;
|
||||||
|
const bool strict = policy == meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT;
|
||||||
|
const bool compatible = policy == meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_COMPATIBLE;
|
||||||
|
|
||||||
// Only a signature we verify below may mark this packet signed; never trust an inbound flag.
|
// Only a signature we verify below may mark this packet signed; never trust an inbound flag.
|
||||||
p->xeddsa_signed = false;
|
p->xeddsa_signed = false;
|
||||||
if (p->decoded.xeddsa_signature.size == XEDDSA_SIGNATURE_SIZE) {
|
if (p->decoded.xeddsa_signature.size == XEDDSA_SIGNATURE_SIZE) {
|
||||||
|
meshtastic_NodeInfoLite_public_key_t senderKey = {0, {0}};
|
||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->from);
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->from);
|
||||||
if (node && node->public_key.size == 32) {
|
if (nodeDB->copyPublicKey(p->from, senderKey)) {
|
||||||
p->xeddsa_signed =
|
p->xeddsa_signed =
|
||||||
crypto->xeddsa_verify(node->public_key.bytes, p->from, p->id, p->decoded.portnum, p->decoded.payload.bytes,
|
crypto->xeddsa_verify(senderKey.bytes, p->from, p->id, p->decoded.portnum, p->decoded.payload.bytes,
|
||||||
p->decoded.payload.size, p->decoded.xeddsa_signature.bytes);
|
p->decoded.payload.size, p->decoded.xeddsa_signature.bytes);
|
||||||
if (p->xeddsa_signed) {
|
if (p->xeddsa_signed) {
|
||||||
// Learn this node as a signer, so a later unsigned signable broadcast from it is dropped
|
// Learn this node as a signer, so a later unsigned signable broadcast from it is dropped
|
||||||
|
// A warm-tier key must be re-admitted before setting the signer bit; otherwise Balanced
|
||||||
|
// forgets downgrade protection as soon as the node is evicted from the hot store.
|
||||||
|
if (!node)
|
||||||
|
node = nodeDB->getOrCreateMeshNode(p->from);
|
||||||
|
if (!node)
|
||||||
|
return false;
|
||||||
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, true);
|
nodeInfoLiteSetBit(node, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, true);
|
||||||
LOG_DEBUG("Verified XEdDSA signature from 0x%08x", p->from);
|
LOG_DEBUG("Verified XEdDSA signature from 0x%08x", p->from);
|
||||||
} else {
|
} else {
|
||||||
@@ -519,7 +635,16 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
const auto bootstrap = verifyFirstContactNodeInfo(p);
|
||||||
|
if (bootstrap == NodeInfoBootstrapResult::INVALID) {
|
||||||
|
LOG_WARN("Invalid first-contact XEdDSA NodeInfo from 0x%08x, dropping", p->from);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (bootstrap == NodeInfoBootstrapResult::VERIFIED)
|
||||||
|
return true;
|
||||||
LOG_DEBUG("No public key for 0x%08x, cannot verify XEdDSA signature", p->from);
|
LOG_DEBUG("No public key for 0x%08x, cannot verify XEdDSA signature", p->from);
|
||||||
|
if (strict)
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} else if (p->decoded.xeddsa_signature.size != 0) {
|
} else if (p->decoded.xeddsa_signature.size != 0) {
|
||||||
// A signature field that is neither empty nor a full 64 bytes is malformed - honest
|
// A signature field that is neither empty nor a full 64 bytes is malformed - honest
|
||||||
@@ -530,15 +655,24 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
|
|||||||
p->from);
|
p->from);
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// Truly unsigned (signature size 0) - only reject the class a signing node always signs: a
|
if (p->pki_encrypted)
|
||||||
// non-PKI broadcast whose signed encoding would still fit the LoRa frame. Size p->decoded
|
return true;
|
||||||
// canonically so this counts the same fields the sender's signedDataFits() gate counted;
|
if (strict) {
|
||||||
// adding XEDDSA_SIGNATURE_FIELD_BYTES to that unsigned base mirrors it exactly, whatever
|
LOG_WARN("Dropping unsigned packet from 0x%08x in Strict signature mode", p->from);
|
||||||
// fields the Data carried. Unicast/PKI packets and broadcasts too big to carry a signature
|
return false;
|
||||||
// are never signed, so they must not be hard-failed here even for a known signer.
|
}
|
||||||
|
if (compatible)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// In Balanced, preserve legacy unsigned-unicast compatibility and only reject the class a
|
||||||
|
// signing node always signs: a non-PKI broadcast whose signed encoding would still fit the
|
||||||
|
// LoRa frame. Canonical sizing removes unknown protobuf fields before mirroring the
|
||||||
|
// sender-side signedDataFits() gate, so this counts the same fields that gate counted.
|
||||||
|
// Unicast packets and broadcasts too big to carry a signature are never signed, so they
|
||||||
|
// must not be hard-failed here even for a known signer (PKI already returned above).
|
||||||
// isKnownXeddsaSigner consults the warm tier too: a signer evicted from the hot store
|
// isKnownXeddsaSigner consults the warm tier too: a signer evicted from the hot store
|
||||||
// must not become impersonatable via unsigned broadcasts until it is re-heard.
|
// must not become impersonatable via unsigned broadcasts until it is re-heard.
|
||||||
if (nodeDB->isKnownXeddsaSigner(p->from) && !p->pki_encrypted && isBroadcast(p->to)) {
|
if (nodeDB->isKnownXeddsaSigner(p->from) && isBroadcast(p->to)) {
|
||||||
size_t canonicalSize;
|
size_t canonicalSize;
|
||||||
if (!canonicalSignableSize(&p->decoded, &canonicalSize))
|
if (!canonicalSignableSize(&p->decoded, &canonicalSize))
|
||||||
return true; // can't size it; never drop on a sizing failure
|
return true; // can't size it; never drop on a sizing failure
|
||||||
@@ -552,6 +686,55 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
RoutingAuthVerdict passesRoutingAuthGate(meshtastic_MeshPacket *p)
|
||||||
|
{
|
||||||
|
// Routing still needs the original encrypted representation for byte-for-byte relay and for
|
||||||
|
// MQTT uplink. Authenticate a copy here; handleReceived() performs the normal in-place decode
|
||||||
|
// only after stateful routing filters have completed.
|
||||||
|
if (routingAuthCacheMatches(*p))
|
||||||
|
return RoutingAuthVerdict::ACCEPT;
|
||||||
|
|
||||||
|
meshtastic_MeshPacket wire = *p;
|
||||||
|
meshtastic_MeshPacket authCandidate = *p;
|
||||||
|
routingAuthEvaluations++;
|
||||||
|
if (authCandidate.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||||
|
// Already-decoded remote ingress (notably Portduino SimRadio) did not pass through a
|
||||||
|
// decryptor. Never trust serialized local authentication metadata on that boundary.
|
||||||
|
authCandidate.pki_encrypted = false;
|
||||||
|
authCandidate.public_key.size = 0;
|
||||||
|
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||||
|
concurrency::LockGuard g(cryptLock);
|
||||||
|
if (!checkXeddsaReceivePolicy(&authCandidate)) {
|
||||||
|
LOG_WARN("Already-decoded packet rejected by signature policy before routing state update");
|
||||||
|
return RoutingAuthVerdict::REJECT;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
p->xeddsa_signed = authCandidate.xeddsa_signed;
|
||||||
|
wire = *p;
|
||||||
|
storeRoutingAuthCache(wire, authCandidate);
|
||||||
|
return RoutingAuthVerdict::ACCEPT;
|
||||||
|
}
|
||||||
|
const DecodeState state = perhapsDecode(&authCandidate);
|
||||||
|
if (state == DecodeState::DECODE_POLICY_REJECT) {
|
||||||
|
LOG_WARN("Packet rejected by signature policy before routing state update");
|
||||||
|
return RoutingAuthVerdict::REJECT;
|
||||||
|
}
|
||||||
|
if (state == DecodeState::DECODE_FATAL) {
|
||||||
|
LOG_WARN("Fatal decode error before routing state update");
|
||||||
|
return RoutingAuthVerdict::REJECT;
|
||||||
|
}
|
||||||
|
if (state == DecodeState::DECODE_FAILURE) {
|
||||||
|
LOG_WARN("Decryptable packet failed decoding/authentication before routing state update");
|
||||||
|
return RoutingAuthVerdict::REJECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only an explicit unknown-channel result remains eligible for opaque relay.
|
||||||
|
if (state == DecodeState::DECODE_OPAQUE)
|
||||||
|
return RoutingAuthVerdict::OPAQUE_RELAY_ONLY;
|
||||||
|
storeRoutingAuthCache(wire, authCandidate);
|
||||||
|
return RoutingAuthVerdict::ACCEPT;
|
||||||
|
}
|
||||||
|
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
// The fallback costs three X25519 ops before the AEAD tag is checked. Budget is global because p->from is
|
// The fallback costs three X25519 ops before the AEAD tag is checked. Budget is global because p->from is
|
||||||
// attacker-controlled; successful runs refund, and their key is then persisted for the fast path.
|
// attacker-controlled; successful runs refund, and their key is then persisted for the fast path.
|
||||||
@@ -613,17 +796,25 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
|||||||
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag)
|
if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag)
|
||||||
return DecodeState::DECODE_SUCCESS; // If packet was already decoded just return
|
return DecodeState::DECODE_SUCCESS; // If packet was already decoded just return
|
||||||
|
|
||||||
|
// Authentication metadata is local-only. Re-establish it below only after successful PKI decryption.
|
||||||
|
p->pki_encrypted = false;
|
||||||
|
p->public_key.size = 0;
|
||||||
|
|
||||||
size_t rawSize = p->encrypted.size;
|
size_t rawSize = p->encrypted.size;
|
||||||
if (rawSize > sizeof(bytes)) {
|
if (rawSize > sizeof(bytes)) {
|
||||||
LOG_ERROR("Packet too large to attempt decryption! (rawSize=%d > 256)", rawSize);
|
LOG_ERROR("Packet too large to attempt decryption! (rawSize=%d > 256)", rawSize);
|
||||||
return DecodeState::DECODE_FATAL;
|
return DecodeState::DECODE_FATAL;
|
||||||
}
|
}
|
||||||
bool decrypted = false;
|
bool decrypted = false;
|
||||||
|
bool pkiAttempted = false;
|
||||||
|
bool matchedChannel = false;
|
||||||
ChannelIndex chIndex = 0;
|
ChannelIndex chIndex = 0;
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
meshtastic_NodeInfoLite *ourNode = nullptr;
|
meshtastic_NodeInfoLite *ourNode = nullptr;
|
||||||
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && rawSize > MESHTASTIC_PKC_OVERHEAD &&
|
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) {
|
(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
|
// 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
|
// (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
|
// key-verification handshake. On a full NodeDB miss, copyPublicKey() falls through to a
|
||||||
@@ -708,6 +899,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
|||||||
for (chIndex = 0; chIndex < channels.getNumChannels(); chIndex++) {
|
for (chIndex = 0; chIndex < channels.getNumChannels(); chIndex++) {
|
||||||
// Try to use this hash/channel pair
|
// Try to use this hash/channel pair
|
||||||
if (channels.decryptForHash(chIndex, p->channel)) {
|
if (channels.decryptForHash(chIndex, p->channel)) {
|
||||||
|
matchedChannel = true;
|
||||||
// we have to copy into a scratch buffer, because these bytes are a union with the decoded protobuf. Create a
|
// we have to copy into a scratch buffer, because these bytes are a union with the decoded protobuf. Create a
|
||||||
// fresh copy for each decrypt attempt.
|
// fresh copy for each decrypt attempt.
|
||||||
memcpy(bytes, p->encrypted.bytes, rawSize);
|
memcpy(bytes, p->encrypted.bytes, rawSize);
|
||||||
@@ -743,10 +935,9 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
|||||||
p->channel = chIndex; // change to store the index instead of the hash
|
p->channel = chIndex; // change to store the index instead of the hash
|
||||||
|
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||||
// Runs before the bitfield merge below: that merge can set want_response, adding wire bytes
|
// Run before merging local-only bitfield state into the decoded Data.
|
||||||
// the sender never encoded and skewing the policy's sizing of p->decoded.
|
|
||||||
if (!checkXeddsaReceivePolicy(p))
|
if (!checkXeddsaReceivePolicy(p))
|
||||||
return DecodeState::DECODE_FAILURE;
|
return DecodeState::DECODE_POLICY_REJECT;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (p->decoded.has_bitfield)
|
if (p->decoded.has_bitfield)
|
||||||
@@ -804,7 +995,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
|||||||
return DecodeState::DECODE_SUCCESS;
|
return DecodeState::DECODE_SUCCESS;
|
||||||
} else {
|
} else {
|
||||||
LOG_WARN("No suitable channel found for decoding, hash was 0x%x!", p->channel);
|
LOG_WARN("No suitable channel found for decoding, hash was 0x%x!", p->channel);
|
||||||
return DecodeState::DECODE_FAILURE;
|
return (matchedChannel || pkiAttempted) ? DecodeState::DECODE_FAILURE : DecodeState::DECODE_OPAQUE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1015,8 +1206,6 @@ NodeNum Router::getNodeNum()
|
|||||||
void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
||||||
{
|
{
|
||||||
bool skipHandle = false;
|
bool skipHandle = false;
|
||||||
// Also, we should set the time from the ISR and it should have msec level resolution
|
|
||||||
p->rx_time = getValidTime(RTCQualityFromNet); // store the arrival timestamp for the phone
|
|
||||||
|
|
||||||
// Store a copy of the encrypted packet for MQTT.
|
// Store a copy of the encrypted packet for MQTT.
|
||||||
// Local, not a class member: handleReceived re-enters itself when a module
|
// Local, not a class member: handleReceived re-enters itself when a module
|
||||||
@@ -1027,12 +1216,31 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
|||||||
meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p);
|
meshtastic_MeshPacket *p_encrypted = packetPool.allocCopy(*p);
|
||||||
DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted);
|
DEBUG_HEAP_AFTER("Router::handleReceived", p_encrypted);
|
||||||
|
|
||||||
|
// Consume the decoded/authenticated handoff after preserving the exact encrypted packet and
|
||||||
|
// before mutating any packet fields that participate in the exact cache match.
|
||||||
|
if (src == RX_SRC_RADIO)
|
||||||
|
applyRoutingAuthCache(p);
|
||||||
|
|
||||||
|
// Also, we should set the time from the ISR and it should have msec level resolution.
|
||||||
|
// Keep the decoded working packet and encrypted MQTT copy on the same local arrival timestamp.
|
||||||
|
const uint32_t rxTime = getValidTime(RTCQualityFromNet);
|
||||||
|
p->rx_time = rxTime;
|
||||||
|
if (p_encrypted)
|
||||||
|
p_encrypted->rx_time = rxTime;
|
||||||
|
|
||||||
// Take those raw bytes and convert them back into a well structured protobuf we can understand
|
// Take those raw bytes and convert them back into a well structured protobuf we can understand
|
||||||
auto decodedState = perhapsDecode(p);
|
auto decodedState = perhapsDecode(p);
|
||||||
if (decodedState == DecodeState::DECODE_FATAL) {
|
if (decodedState == DecodeState::DECODE_FATAL || decodedState == DecodeState::DECODE_POLICY_REJECT ||
|
||||||
|
decodedState == DecodeState::DECODE_FAILURE) {
|
||||||
// Fatal decoding error, we can't do anything with this packet
|
// Fatal decoding error, we can't do anything with this packet
|
||||||
LOG_WARN("Fatal decode error, dropping packet");
|
LOG_WARN(decodedState == DecodeState::DECODE_POLICY_REJECT
|
||||||
cancelSending(p->from, p->id);
|
? "Packet rejected by signature policy"
|
||||||
|
: (decodedState == DecodeState::DECODE_FATAL ? "Fatal decode error, dropping packet"
|
||||||
|
: "Decryptable packet failed decoding, dropping packet"));
|
||||||
|
// A policy rejection is attacker-controlled input and must not cancel a valid pending
|
||||||
|
// transmission with the same (from, id). Preserve the pre-existing fatal-decode behavior.
|
||||||
|
if (decodedState == DecodeState::DECODE_FATAL)
|
||||||
|
cancelSending(p->from, p->id);
|
||||||
skipHandle = true;
|
skipHandle = true;
|
||||||
} else if (decodedState == DecodeState::DECODE_SUCCESS) {
|
} else if (decodedState == DecodeState::DECODE_SUCCESS) {
|
||||||
// parsing was successful, queue for our recipient
|
// parsing was successful, queue for our recipient
|
||||||
@@ -1108,7 +1316,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src)
|
|||||||
} else {
|
} else {
|
||||||
// Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM not
|
// Mark as pki_encrypted if it is not yet decoded and MQTT encryption is also enabled, hash matches and it's a DM not
|
||||||
// to us (because we would be able to decrypt it)
|
// to us (because we would be able to decrypt it)
|
||||||
if (decodedState == DecodeState::DECODE_FAILURE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 &&
|
if (decodedState == DecodeState::DECODE_OPAQUE && moduleConfig.mqtt.encryption_enabled && p->channel == 0x00 &&
|
||||||
!isBroadcast(p->to) && !isToUs(p))
|
!isBroadcast(p->to) && !isToUs(p))
|
||||||
p_encrypted->pki_encrypted = true;
|
p_encrypted->pki_encrypted = true;
|
||||||
// After potentially altering it, publish received message to MQTT if we're not the original transmitter of the packet
|
// After potentially altering it, publish received message to MQTT if we're not the original transmitter of the packet
|
||||||
@@ -1157,6 +1365,7 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
|||||||
#endif
|
#endif
|
||||||
// assert(radioConfig.has_preferences);
|
// assert(radioConfig.has_preferences);
|
||||||
if (is_in_repeated(config.lora.ignore_incoming, p->from)) {
|
if (is_in_repeated(config.lora.ignore_incoming, p->from)) {
|
||||||
|
clearRoutingAuthCache();
|
||||||
LOG_DEBUG("Ignore msg, 0x%08x is in our ignore list", p->from);
|
LOG_DEBUG("Ignore msg, 0x%08x is in our ignore list", p->from);
|
||||||
packetPool.release(p);
|
packetPool.release(p);
|
||||||
return;
|
return;
|
||||||
@@ -1164,30 +1373,49 @@ void Router::perhapsHandleReceived(meshtastic_MeshPacket *p)
|
|||||||
|
|
||||||
meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from);
|
meshtastic_NodeInfoLite const *node = nodeDB->getMeshNode(p->from);
|
||||||
if (nodeInfoLiteIsIgnored(node)) {
|
if (nodeInfoLiteIsIgnored(node)) {
|
||||||
|
clearRoutingAuthCache();
|
||||||
LOG_DEBUG("Ignore msg, 0x%08x is ignored", p->from);
|
LOG_DEBUG("Ignore msg, 0x%08x is ignored", p->from);
|
||||||
packetPool.release(p);
|
packetPool.release(p);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p->from == NODENUM_BROADCAST) {
|
if (p->from == NODENUM_BROADCAST) {
|
||||||
|
clearRoutingAuthCache();
|
||||||
LOG_DEBUG("Ignore msg from broadcast address");
|
LOG_DEBUG("Ignore msg from broadcast address");
|
||||||
packetPool.release(p);
|
packetPool.release(p);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.lora.ignore_mqtt && p->via_mqtt) {
|
if (config.lora.ignore_mqtt && p->via_mqtt) {
|
||||||
|
clearRoutingAuthCache();
|
||||||
LOG_DEBUG("Msg came in via MQTT from 0x%08x", p->from);
|
LOG_DEBUG("Msg came in via MQTT from 0x%08x", p->from);
|
||||||
packetPool.release(p);
|
packetPool.release(p);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldDropPacketForPreHop(*p)) {
|
if (shouldDropPacketForPreHop(*p)) {
|
||||||
|
clearRoutingAuthCache();
|
||||||
logHopStartDrop(*p, "pre-hop drop");
|
logHopStartDrop(*p, "pre-hop drop");
|
||||||
packetPool.release(p);
|
packetPool.release(p);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decrypt and authenticate before Reliable/Flooding/NextHop filters can update retry
|
||||||
|
// timers, packet history, implicit ACK state, cancellation, or relay queues. A packet for
|
||||||
|
// an unknown channel passes as opaque traffic and retains the existing relay behavior.
|
||||||
|
const auto authVerdict = passesRoutingAuthGate(p);
|
||||||
|
if (authVerdict == RoutingAuthVerdict::REJECT) {
|
||||||
|
packetPool.release(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (authVerdict == RoutingAuthVerdict::OPAQUE_RELAY_ONLY) {
|
||||||
|
relayOpaquePacket(p);
|
||||||
|
packetPool.release(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldFilterReceived(p)) {
|
if (shouldFilterReceived(p)) {
|
||||||
|
clearRoutingAuthCache();
|
||||||
LOG_DEBUG("Incoming msg was filtered from 0x%08x", p->from);
|
LOG_DEBUG("Incoming msg was filtered from 0x%08x", p->from);
|
||||||
packetPool.release(p);
|
packetPool.release(p);
|
||||||
return;
|
return;
|
||||||
|
|||||||
+14
-16
@@ -112,6 +112,9 @@ class Router : protected concurrency::OSThread, protected PacketHistory
|
|||||||
*/
|
*/
|
||||||
virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) { return false; }
|
virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) { return false; }
|
||||||
|
|
||||||
|
/** Relay an opaque packet without admitting it to local routing/history state. */
|
||||||
|
virtual bool relayOpaquePacket(const meshtastic_MeshPacket *) { return false; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if hop_limit should be decremented for a relay operation.
|
* Determine if hop_limit should be decremented for a relay operation.
|
||||||
* Returns false (preserve hop_limit) only if all conditions are met:
|
* Returns false (preserve hop_limit) only if all conditions are met:
|
||||||
@@ -161,7 +164,8 @@ class Router : protected concurrency::OSThread, protected PacketHistory
|
|||||||
void abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p);
|
void abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p);
|
||||||
};
|
};
|
||||||
|
|
||||||
enum DecodeState { DECODE_SUCCESS, DECODE_FAILURE, DECODE_FATAL };
|
enum DecodeState { DECODE_SUCCESS, DECODE_FAILURE, DECODE_OPAQUE, DECODE_FATAL, DECODE_POLICY_REJECT };
|
||||||
|
enum class RoutingAuthVerdict { ACCEPT, OPAQUE_RELAY_ONLY, REJECT };
|
||||||
|
|
||||||
/** FIXME - move this into a mesh packet class
|
/** FIXME - move this into a mesh packet class
|
||||||
* Remove any encryption and decode the protobufs inside this packet (if necessary).
|
* Remove any encryption and decode the protobufs inside this packet (if necessary).
|
||||||
@@ -170,26 +174,20 @@ enum DecodeState { DECODE_SUCCESS, DECODE_FAILURE, DECODE_FATAL };
|
|||||||
*/
|
*/
|
||||||
DecodeState perhapsDecode(meshtastic_MeshPacket *p);
|
DecodeState perhapsDecode(meshtastic_MeshPacket *p);
|
||||||
|
|
||||||
|
/** Apply receive authentication before routing state mutation; unknown-channel packets may remain opaque relay-only. */
|
||||||
|
RoutingAuthVerdict passesRoutingAuthGate(meshtastic_MeshPacket *p);
|
||||||
|
#ifdef PIO_UNIT_TESTING
|
||||||
|
uint32_t routingAuthEvaluationCount();
|
||||||
|
void resetRoutingAuthEvaluationCount();
|
||||||
|
#endif
|
||||||
|
|
||||||
/** Return 0 for success or a Routing_Error code for failure
|
/** Return 0 for success or a Routing_Error code for failure
|
||||||
*/
|
*/
|
||||||
meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p);
|
meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p);
|
||||||
|
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||||
/** XEdDSA receive-side signature policy. When the packet carries a 64-byte signature *and* the
|
/** Enforce the configured XEdDSA receive policy. The caller must hold cryptLock.
|
||||||
* sender's public key is known, verify it: on success learn the sender's signer bit, on failure
|
* Returns false when the packet must be dropped. */
|
||||||
* drop. If the key is unknown the signature is left unverified and the packet passes. A signature
|
|
||||||
* of any other non-zero length is treated as malformed and dropped. For unsigned packets, enforce
|
|
||||||
* downgrade protection: drop a non-PKI broadcast from a known signer whose signed encoding would
|
|
||||||
* still fit a LoRa frame (unicast, PKI, and oversized broadcasts always pass).
|
|
||||||
*
|
|
||||||
* The fit test sizes p->decoded with the real encoder, so it measures the fields the sender
|
|
||||||
* encoded rather than any raw wire length.
|
|
||||||
*
|
|
||||||
* The caller MUST hold cryptLock: verification runs through the shared CryptoEngine key cache.
|
|
||||||
* (perhapsDecode already holds it; other call sites must take it themselves.)
|
|
||||||
*
|
|
||||||
* @return false if the packet must be dropped.
|
|
||||||
*/
|
|
||||||
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p);
|
bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ static constexpr uint32_t WARM_SIGNER_MASK = 0x01u;
|
|||||||
enum class WarmFormat : uint8_t { Current, V2, V1 };
|
enum class WarmFormat : uint8_t { Current, V2, V1 };
|
||||||
|
|
||||||
// Protected category cached alongside role so consumers needn't re-derive the mapping.
|
// Protected category cached alongside role so consumers needn't re-derive the mapping.
|
||||||
enum class WarmProtected : uint8_t { None = 0, Role = 1, Flag = 2 };
|
enum class WarmProtected : uint8_t { None = 0, Role = 1, Flag = 2, XeddsaSigner = 3 };
|
||||||
|
|
||||||
inline uint32_t warmPackLastHeard(uint32_t lastHeard, uint8_t role, uint8_t prot, bool signer)
|
inline uint32_t warmPackLastHeard(uint32_t lastHeard, uint8_t role, uint8_t prot, bool signer)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ class UdpMulticastHandler final
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP;
|
mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP;
|
||||||
// Preserve the whole MeshPacket as received: while payload_variant is encrypted, `channel` is a hash (and is 0 for
|
// Authentication metadata is local-only; Router re-establishes it after successful PKI decryption.
|
||||||
// PKI DMs), so it must be copied verbatim for the router to attempt PKI/channel decryption. Keep
|
mp.pki_encrypted = false;
|
||||||
// pki_encrypted/public_key too so downstream auth/metadata can reflect PKI usage correctly.
|
mp.public_key.size = 0;
|
||||||
UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp);
|
UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp);
|
||||||
// Unset received SNR/RSSI
|
// Unset received SNR/RSSI
|
||||||
p->rx_snr = 0;
|
p->rx_snr = 0;
|
||||||
|
|||||||
@@ -1136,6 +1136,16 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers)
|
|||||||
rotated.private_key = incoming.private_key;
|
rotated.private_key = incoming.private_key;
|
||||||
incoming = rotated;
|
incoming = rotated;
|
||||||
}
|
}
|
||||||
|
#if MESHTASTIC_EXCLUDE_PKI || MESHTASTIC_EXCLUDE_XEDDSA
|
||||||
|
if (incoming.packet_signature_policy !=
|
||||||
|
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_BALANCED) {
|
||||||
|
incoming.packet_signature_policy =
|
||||||
|
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_BALANCED;
|
||||||
|
const char *warning = "Packet authenticity policy is unavailable on this firmware build";
|
||||||
|
LOG_WARN(warning);
|
||||||
|
sendWarning(warning);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
config.security = incoming;
|
config.security = incoming;
|
||||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) && !(MESHTASTIC_EXCLUDE_PKI)
|
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) && !(MESHTASTIC_EXCLUDE_PKI)
|
||||||
// First provisioning (no key) generates one; a private key supplied without its public key derives it.
|
// First provisioning (no key) generates one; a private key supplied without its public key derives it.
|
||||||
|
|||||||
+6
-9
@@ -150,7 +150,8 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
|
|||||||
p->hop_limit = e.packet->hop_limit;
|
p->hop_limit = e.packet->hop_limit;
|
||||||
p->hop_start = e.packet->hop_start;
|
p->hop_start = e.packet->hop_start;
|
||||||
p->want_ack = e.packet->want_ack;
|
p->want_ack = e.packet->want_ack;
|
||||||
p->via_mqtt = true; // Mark that the packet was received via MQTT
|
p->via_mqtt = true; // Mark that the packet was received via MQTT
|
||||||
|
p->pki_encrypted = false; // Only local AES-CCM decryption may establish PKI authentication.
|
||||||
p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT;
|
p->transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MQTT;
|
||||||
p->which_payload_variant = e.packet->which_payload_variant;
|
p->which_payload_variant = e.packet->which_payload_variant;
|
||||||
memcpy(&p->decoded, &e.packet->decoded, std::max(sizeof(p->decoded), sizeof(p->encrypted)));
|
memcpy(&p->decoded, &e.packet->decoded, std::max(sizeof(p->decoded), sizeof(p->encrypted)));
|
||||||
@@ -175,12 +176,9 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
|
|||||||
// impersonate a signing node with unsigned broadcasts. Hold cryptLock like the RF path
|
// impersonate a signing node with unsigned broadcasts. Hold cryptLock like the RF path
|
||||||
// (perhapsDecode) does - checkXeddsaReceivePolicy -> xeddsa_verify mutates shared
|
// (perhapsDecode) does - checkXeddsaReceivePolicy -> xeddsa_verify mutates shared
|
||||||
// CryptoEngine cache state, and MQTT ingress can run on a different task.
|
// CryptoEngine cache state, and MQTT ingress can run on a different task.
|
||||||
{
|
if (passesRoutingAuthGate(p.get()) != RoutingAuthVerdict::ACCEPT) {
|
||||||
concurrency::LockGuard g(cryptLock);
|
LOG_INFO("Ignore decoded message failing XEdDSA policy");
|
||||||
if (!checkXeddsaReceivePolicy(p.get())) {
|
return;
|
||||||
LOG_INFO("Ignore decoded message failing XEdDSA policy");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -193,8 +191,7 @@ inline void onReceiveProto(char *topic, byte *payload, size_t length)
|
|||||||
// likely they discovered each other via a channel we have downlink enabled for
|
// likely they discovered each other via a channel we have downlink enabled for
|
||||||
if (isToUs(p.get()) || (nodeInfoLiteHasUser(tx) && nodeInfoLiteHasUser(rx)))
|
if (isToUs(p.get()) || (nodeInfoLiteHasUser(tx) && nodeInfoLiteHasUser(rx)))
|
||||||
router->enqueueReceivedMessage(p.release());
|
router->enqueueReceivedMessage(p.release());
|
||||||
} else if (router &&
|
} else if (router && passesRoutingAuthGate(p.get()) == RoutingAuthVerdict::ACCEPT)
|
||||||
perhapsDecode(p.get()) == DecodeState::DECODE_SUCCESS) // ignore messages if we don't have the channel key
|
|
||||||
router->enqueueReceivedMessage(p.release());
|
router->enqueueReceivedMessage(p.release());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
#include "support/MockMeshService.h"
|
#include "support/MockMeshService.h"
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
|
#ifdef ARCH_PORTDUINO
|
||||||
|
#include "platform/portduino/PortduinoGlue.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
static constexpr NodeNum LOCAL_NODE = 0x0A0A0A0A;
|
static constexpr NodeNum LOCAL_NODE = 0x0A0A0A0A;
|
||||||
static constexpr NodeNum ADMIN_NODE = 0x0B0B0B0B; // authorized admin, sends remote admin to us
|
static constexpr NodeNum ADMIN_NODE = 0x0B0B0B0B; // authorized admin, sends remote admin to us
|
||||||
static constexpr NodeNum QUERIED_NODE = 0x0C0C0C0C; // a remote we send admin requests to
|
static constexpr NodeNum QUERIED_NODE = 0x0C0C0C0C; // a remote we send admin requests to
|
||||||
@@ -157,6 +161,14 @@ void setUp(void)
|
|||||||
nodeDB = mockNodeDB;
|
nodeDB = mockNodeDB;
|
||||||
myNodeInfo.my_node_num = LOCAL_NODE;
|
myNodeInfo.my_node_num = LOCAL_NODE;
|
||||||
|
|
||||||
|
#ifdef ARCH_PORTDUINO
|
||||||
|
// The native test harness boots Portduino in simulated mode, and wouldEncryptWithPKC()
|
||||||
|
// hard-disables PKC whenever force_simradio is set. Left true, no outgoing admin request is
|
||||||
|
// ever key-pinned, so the pinning tests below cannot exercise what they are asserting.
|
||||||
|
// Model a real (non-sim) device instead.
|
||||||
|
portduino_config.force_simradio = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
config = meshtastic_LocalConfig_init_zero;
|
config = meshtastic_LocalConfig_init_zero;
|
||||||
// A real device always holds a private key; without one perhapsEncode never picks PKC.
|
// A real device always holds a private key; without one perhapsEncode never picks PKC.
|
||||||
config.security.private_key.size = 32;
|
config.security.private_key.size = 32;
|
||||||
|
|||||||
@@ -159,7 +159,8 @@ void test_E1_perhaps_decode_fuzz(void)
|
|||||||
|
|
||||||
DecodeState st = perhapsDecode(&p);
|
DecodeState st = perhapsDecode(&p);
|
||||||
// Any verdict is fine; the contract is that arbitrary ciphertext never crashes the pipeline.
|
// Any verdict is fine; the contract is that arbitrary ciphertext never crashes the pipeline.
|
||||||
TEST_ASSERT_TRUE(st == DECODE_SUCCESS || st == DECODE_FAILURE || st == DECODE_FATAL);
|
TEST_ASSERT_TRUE(st == DECODE_SUCCESS || st == DECODE_FAILURE || st == DECODE_OPAQUE || st == DECODE_FATAL ||
|
||||||
|
st == DECODE_POLICY_REJECT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,15 @@ class MockRouter : public Router
|
|||||||
class MockMeshService : public MeshService
|
class MockMeshService : public MeshService
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
// No PhoneAPI reader exists in these tests, so packets the receive pipeline forwards to the phone
|
||||||
|
// (MeshService::sendToPhone enqueues pooled copies into toPhoneQueue) would leak at teardown. Drain
|
||||||
|
// the queue like the phone would. This surfaced once sendLocal() began dispatching local packets
|
||||||
|
// through handleReceived() directly rather than via the (mock-overridden) enqueueReceivedMessage().
|
||||||
|
~MockMeshService()
|
||||||
|
{
|
||||||
|
while (meshtastic_MeshPacket *p = getForPhone())
|
||||||
|
releaseToPool(p);
|
||||||
|
}
|
||||||
void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) override
|
void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) override
|
||||||
{
|
{
|
||||||
messages_.emplace_back(*m);
|
messages_.emplace_back(*m);
|
||||||
@@ -334,6 +343,7 @@ const meshtastic_MeshPacket encrypted = {
|
|||||||
// Initialize mocks and configuration before running each test.
|
// Initialize mocks and configuration before running each test.
|
||||||
void setUp(void)
|
void setUp(void)
|
||||||
{
|
{
|
||||||
|
config = meshtastic_LocalConfig_init_zero;
|
||||||
moduleConfig.mqtt =
|
moduleConfig.mqtt =
|
||||||
meshtastic_ModuleConfig_MQTTConfig{.enabled = true, .map_reporting_enabled = true, .has_map_report_settings = true};
|
meshtastic_ModuleConfig_MQTTConfig{.enabled = true, .map_reporting_enabled = true, .has_map_report_settings = true};
|
||||||
moduleConfig.mqtt.map_report_settings = meshtastic_ModuleConfig_MapReportSettings{
|
moduleConfig.mqtt.map_report_settings = meshtastic_ModuleConfig_MapReportSettings{
|
||||||
@@ -627,6 +637,8 @@ void test_receiveWithoutChannelDownlink(void)
|
|||||||
// Test receiving an encrypted MeshPacket on the PKI topic.
|
// Test receiving an encrypted MeshPacket on the PKI topic.
|
||||||
void test_receiveEncryptedPKITopicToUs(void)
|
void test_receiveEncryptedPKITopicToUs(void)
|
||||||
{
|
{
|
||||||
|
config.security.packet_signature_policy =
|
||||||
|
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT;
|
||||||
meshtastic_MeshPacket e = encrypted;
|
meshtastic_MeshPacket e = encrypted;
|
||||||
e.to = myNodeInfo.my_node_num;
|
e.to = myNodeInfo.my_node_num;
|
||||||
|
|
||||||
@@ -718,6 +730,8 @@ static meshtastic_MeshPacket makeDecodedBroadcast()
|
|||||||
// signing node (audit F3).
|
// signing node (audit F3).
|
||||||
void test_receiveDropsUnsignedBroadcastFromSigner(void)
|
void test_receiveDropsUnsignedBroadcastFromSigner(void)
|
||||||
{
|
{
|
||||||
|
config.security.packet_signature_policy =
|
||||||
|
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_BALANCED;
|
||||||
mockNodeDB->emptyNode.bitfield |= NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK;
|
mockNodeDB->emptyNode.bitfield |= NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK;
|
||||||
|
|
||||||
const meshtastic_MeshPacket p = makeDecodedBroadcast();
|
const meshtastic_MeshPacket p = makeDecodedBroadcast();
|
||||||
@@ -729,6 +743,8 @@ void test_receiveDropsUnsignedBroadcastFromSigner(void)
|
|||||||
// The same unsigned broadcast from a node never seen signing is accepted.
|
// The same unsigned broadcast from a node never seen signing is accepted.
|
||||||
void test_receiveAcceptsUnsignedBroadcastFromNonSigner(void)
|
void test_receiveAcceptsUnsignedBroadcastFromNonSigner(void)
|
||||||
{
|
{
|
||||||
|
config.security.packet_signature_policy =
|
||||||
|
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_BALANCED;
|
||||||
const meshtastic_MeshPacket p = makeDecodedBroadcast();
|
const meshtastic_MeshPacket p = makeDecodedBroadcast();
|
||||||
unitTest->publish(&p);
|
unitTest->publish(&p);
|
||||||
|
|
||||||
@@ -760,6 +776,8 @@ void test_receiveVerifiesSignedDecodedDownlink(void)
|
|||||||
// A decoded downlink carrying a signature that fails verification is dropped.
|
// A decoded downlink carrying a signature that fails verification is dropped.
|
||||||
void test_receiveDropsBadSignatureOnDecodedDownlink(void)
|
void test_receiveDropsBadSignatureOnDecodedDownlink(void)
|
||||||
{
|
{
|
||||||
|
config.security.packet_signature_policy =
|
||||||
|
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_COMPATIBLE;
|
||||||
uint8_t pub[32], priv[32];
|
uint8_t pub[32], priv[32];
|
||||||
crypto->generateKeyPair(pub, priv);
|
crypto->generateKeyPair(pub, priv);
|
||||||
mockNodeDB->emptyNode.public_key.size = 32;
|
mockNodeDB->emptyNode.public_key.size = 32;
|
||||||
@@ -775,6 +793,53 @@ void test_receiveDropsBadSignatureOnDecodedDownlink(void)
|
|||||||
|
|
||||||
TEST_ASSERT_TRUE(mockRouter->packets_.empty());
|
TEST_ASSERT_TRUE(mockRouter->packets_.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void test_receiveCompatibleAcceptsUnsignedBroadcastFromSigner(void)
|
||||||
|
{
|
||||||
|
config.security.packet_signature_policy =
|
||||||
|
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_COMPATIBLE;
|
||||||
|
mockNodeDB->emptyNode.bitfield |= NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK;
|
||||||
|
|
||||||
|
const meshtastic_MeshPacket p = makeDecodedBroadcast();
|
||||||
|
unitTest->publish(&p);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(1, mockRouter->packets_.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_receiveStrictDropsUnsignedPortnumsAndUnicast(void)
|
||||||
|
{
|
||||||
|
config.security.packet_signature_policy =
|
||||||
|
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT;
|
||||||
|
const meshtastic_PortNum ports[] = {
|
||||||
|
meshtastic_PortNum_TEXT_MESSAGE_APP, meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_TELEMETRY_APP,
|
||||||
|
meshtastic_PortNum_NODEINFO_APP, meshtastic_PortNum_WAYPOINT_APP,
|
||||||
|
};
|
||||||
|
for (const auto port : ports) {
|
||||||
|
meshtastic_MeshPacket p = makeDecodedBroadcast();
|
||||||
|
p.decoded.portnum = port;
|
||||||
|
unitTest->publish(&p);
|
||||||
|
}
|
||||||
|
|
||||||
|
meshtastic_MeshPacket unicast = makeDecodedBroadcast();
|
||||||
|
unicast.to = myNodeInfo.my_node_num;
|
||||||
|
unicast.decoded.portnum = meshtastic_PortNum_POSITION_APP;
|
||||||
|
unitTest->publish(&unicast);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(mockRouter->packets_.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// A plaintext broker assertion is not evidence that AES-CCM authentication succeeded locally.
|
||||||
|
void test_receiveStrictDoesNotTrustDecodedPkiFlag(void)
|
||||||
|
{
|
||||||
|
config.security.packet_signature_policy =
|
||||||
|
meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_STRICT;
|
||||||
|
meshtastic_MeshPacket p = makeDecodedBroadcast();
|
||||||
|
p.to = myNodeInfo.my_node_num;
|
||||||
|
p.pki_encrypted = true;
|
||||||
|
unitTest->publish(&p);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(mockRouter->packets_.empty());
|
||||||
|
}
|
||||||
#endif // !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
#endif // !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
||||||
|
|
||||||
// Only the same fields that are transmitted over LoRa should be set in MQTT messages.
|
// Only the same fields that are transmitted over LoRa should be set in MQTT messages.
|
||||||
@@ -1096,6 +1161,9 @@ void setup()
|
|||||||
RUN_TEST(test_receiveAcceptsUnsignedBroadcastFromNonSigner);
|
RUN_TEST(test_receiveAcceptsUnsignedBroadcastFromNonSigner);
|
||||||
RUN_TEST(test_receiveVerifiesSignedDecodedDownlink);
|
RUN_TEST(test_receiveVerifiesSignedDecodedDownlink);
|
||||||
RUN_TEST(test_receiveDropsBadSignatureOnDecodedDownlink);
|
RUN_TEST(test_receiveDropsBadSignatureOnDecodedDownlink);
|
||||||
|
RUN_TEST(test_receiveCompatibleAcceptsUnsignedBroadcastFromSigner);
|
||||||
|
RUN_TEST(test_receiveStrictDropsUnsignedPortnumsAndUnicast);
|
||||||
|
RUN_TEST(test_receiveStrictDoesNotTrustDecodedPkiFlag);
|
||||||
#endif
|
#endif
|
||||||
RUN_TEST(test_receiveIgnoresUnexpectedFields);
|
RUN_TEST(test_receiveIgnoresUnexpectedFields);
|
||||||
RUN_TEST(test_receiveIgnoresInvalidHopLimit);
|
RUN_TEST(test_receiveIgnoresInvalidHopLimit);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -16,5 +16,8 @@ build_flags =
|
|||||||
-DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1
|
-DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1
|
||||||
-DMESHTASTIC_EXCLUDE_I2C=1
|
-DMESHTASTIC_EXCLUDE_I2C=1
|
||||||
-DMESHTASTIC_EXCLUDE_GPS=1
|
-DMESHTASTIC_EXCLUDE_GPS=1
|
||||||
|
build_unflags =
|
||||||
|
${arduino_base.build_unflags}
|
||||||
|
-DMESHTASTIC_EXCLUDE_XEDDSA=1
|
||||||
|
|
||||||
upload_port = stlink
|
upload_port = stlink
|
||||||
@@ -15,6 +15,9 @@ build_flags =
|
|||||||
-DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1
|
-DMESHTASTIC_EXCLUDE_AIR_QUALITY_SENSOR=1
|
||||||
-DMESHTASTIC_EXCLUDE_I2C=1
|
-DMESHTASTIC_EXCLUDE_I2C=1
|
||||||
-DMESHTASTIC_EXCLUDE_GPS=1
|
-DMESHTASTIC_EXCLUDE_GPS=1
|
||||||
|
build_unflags =
|
||||||
|
${arduino_base.build_unflags}
|
||||||
|
-DMESHTASTIC_EXCLUDE_XEDDSA=1
|
||||||
|
|
||||||
lib_deps =
|
lib_deps =
|
||||||
${stm32_base.lib_deps}
|
${stm32_base.lib_deps}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ build_flags =
|
|||||||
-DMESHTASTIC_EXCLUDE_BLUETOOTH=1
|
-DMESHTASTIC_EXCLUDE_BLUETOOTH=1
|
||||||
-DMESHTASTIC_EXCLUDE_WIFI=1
|
-DMESHTASTIC_EXCLUDE_WIFI=1
|
||||||
-DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space.
|
-DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space.
|
||||||
-DMESHTASTIC_EXCLUDE_XEDDSA=1 ; The Ed25519 signing code does not fit in the 256KB flash. Packets are sent unsigned, like pre-XEdDSA firmware.
|
-DMESHTASTIC_EXCLUDE_XEDDSA=1 ; Individual STM32WL variants opt in after size validation.
|
||||||
-DMESHTASTIC_EXCLUDE_PKT_HISTORY_HASH=1
|
-DMESHTASTIC_EXCLUDE_PKT_HISTORY_HASH=1
|
||||||
-DMESHTASTIC_EXCLUDE_RANGETEST=1
|
-DMESHTASTIC_EXCLUDE_RANGETEST=1
|
||||||
-DMESHTASTIC_EXCLUDE_WAYPOINT=1
|
-DMESHTASTIC_EXCLUDE_WAYPOINT=1
|
||||||
|
|||||||
Reference in New Issue
Block a user