Audit of the XEdDSA packet-signing implementation (#10478) surfaced several issues in when unsigned packets are accepted on receive or emitted on send. This fixes them and adds regression coverage. - Unicast NodeInfo exchange no longer breaks against signer nodes: the NodeInfoModule downgrade drop is gated to broadcasts, since senders never sign unicast (want_response replies, directed exchanges). - Replace the payload-size sign heuristic with an exact encoded-size gate (signedDataFits) and mirror it on the receive side, removing a dead band where 167-168 B broadcasts were signed then failed TOO_LARGE. - Extract the receive policy into checkXeddsaReceivePolicy() and apply it to plaintext-MQTT decoded downlink, which previously skipped signature verification and downgrade protection entirely. - Reject signatures whose length is neither 0 nor 64 as malformed, so a crafted partial signature can't inflate the size estimate and dodge the unsigned-downgrade drop. - Hold cryptLock on the MQTT verify path (shared Ed25519 key cache). - Clear any client-preset signature on packets we originate, on all builds. - Randomized (hedged) signing per the Signal XEdDSA spec: bump the meshtastic/Crypto pin to the build where XEdDSA::sign mixes 32 bytes of caller randomness into the nonce as Z (meshtastic/Crypto#3), and seed those bytes in xeddsa_sign from HardwareRNG (checked, with a seeded-CSPRNG fallback). test_crypto pins that repeated signs differ and both verify. Adds test coverage: test_packet_signing groups A-E (receive matrix, send policy, NodeInfo backstop, encoding invariants, decoded-ingress policy), test_mqtt end-to-end downlink cases, and a test_crypto randomization check.
208 lines
8.2 KiB
C++
208 lines
8.2 KiB
C++
#pragma once
|
|
|
|
#include "Channels.h"
|
|
#include "MemoryPool.h"
|
|
#include "MeshTypes.h"
|
|
#include "Observer.h"
|
|
#include "PacketHistory.h"
|
|
#include "PointerQueue.h"
|
|
#include "RadioInterface.h"
|
|
#include "concurrency/OSThread.h"
|
|
#include <memory>
|
|
|
|
/**
|
|
* A mesh aware router that supports multiple interfaces.
|
|
*/
|
|
class Router : protected concurrency::OSThread, protected PacketHistory
|
|
{
|
|
private:
|
|
/// Packets which have just arrived from the radio, ready to be processed by this service and possibly
|
|
/// forwarded to the phone.
|
|
PointerQueue<meshtastic_MeshPacket> fromRadioQueue;
|
|
|
|
protected:
|
|
std::unique_ptr<RadioInterface> iface = nullptr;
|
|
|
|
public:
|
|
/**
|
|
* Constructor
|
|
*
|
|
*/
|
|
Router();
|
|
|
|
/**
|
|
* Currently we only allow one interface, that may change in the future
|
|
*/
|
|
void addInterface(std::unique_ptr<RadioInterface> _iface) { iface = std::move(_iface); }
|
|
|
|
/**
|
|
* Borrowed (non-owning) access to the radio interface - used by NodeDB
|
|
* after a lockdown unlock so it can push the freshly-loaded config to
|
|
* the SX12xx via reconfigure(). Returns nullptr when no radio has been
|
|
* attached (e.g. ARCH_PORTDUINO simulator before SimRadio bind).
|
|
*/
|
|
RadioInterface *getRadioIface() { return iface.get(); }
|
|
|
|
/**
|
|
* do idle processing
|
|
* Mostly looking in our incoming rxPacket queue and calling handleReceived.
|
|
*/
|
|
virtual int32_t runOnce() override;
|
|
|
|
/**
|
|
* Works like send, but if we are sending to the local node, we directly put the message in the receive queue.
|
|
* This is the primary method used for sending packets, because it handles both the remote and local cases.
|
|
*
|
|
* NOTE: This method will free the provided packet (even if we return an error code)
|
|
*/
|
|
ErrorCode sendLocal(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO);
|
|
|
|
/** Attempt to cancel a previously sent packet. Returns true if a packet was found we could cancel */
|
|
bool cancelSending(NodeNum from, PacketId id);
|
|
|
|
/** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */
|
|
bool findInTxQueue(NodeNum from, PacketId id);
|
|
|
|
/** Allocate and return a meshpacket which defaults as send to broadcast from the current node.
|
|
* The returned packet is guaranteed to have a unique packet ID already assigned
|
|
*/
|
|
[[nodiscard]] meshtastic_MeshPacket *allocForSending();
|
|
|
|
/** Return Underlying interface's TX queue status */
|
|
[[nodiscard]] meshtastic_QueueStatus getQueueStatus();
|
|
|
|
/**
|
|
* @return our local nodenum */
|
|
[[nodiscard]] NodeNum getNodeNum();
|
|
|
|
/** Wake up the router thread ASAP, because we just queued a message for it.
|
|
* FIXME, this is kinda a hack because we don't have a nice way yet to say 'wake us because we are 'blocked on this queue'
|
|
*/
|
|
void setReceivedMessage();
|
|
|
|
/**
|
|
* RadioInterface calls this to queue up packets that have been received from the radio. The router is now responsible for
|
|
* freeing the packet
|
|
*/
|
|
virtual void enqueueReceivedMessage(meshtastic_MeshPacket *p);
|
|
|
|
/**
|
|
* Send a packet on a suitable interface. This routine will
|
|
* later free() the packet to pool. This routine is not allowed to stall.
|
|
* If the txmit queue is full it might return an error
|
|
*
|
|
* NOTE: This method will free the provided packet (even if we return an error code)
|
|
*/
|
|
virtual ErrorCode send(meshtastic_MeshPacket *p);
|
|
virtual ErrorCode rawSend(meshtastic_MeshPacket *p);
|
|
|
|
/* Statistics for the amount of duplicate received packets and the amount of times we cancel a relay because someone did it
|
|
before us */
|
|
uint32_t rxDupe = 0, txRelayCanceled = 0;
|
|
|
|
protected:
|
|
friend class RoutingModule;
|
|
|
|
/**
|
|
* Should this incoming filter be dropped?
|
|
*
|
|
* FIXME, move this into the new RoutingModule and do the filtering there using the regular module logic
|
|
*
|
|
* Called immediately on reception, before any further processing.
|
|
* @return true to abandon the packet
|
|
*/
|
|
virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) { return false; }
|
|
|
|
/**
|
|
* Determine if hop_limit should be decremented for a relay operation.
|
|
* Returns false (preserve hop_limit) only if all conditions are met:
|
|
* - It's NOT the first hop (first hop must always decrement)
|
|
* - Local device is a ROUTER, ROUTER_LATE, or CLIENT_BASE
|
|
* - Previous relay is a favorite ROUTER, ROUTER_LATE, or CLIENT_BASE
|
|
*
|
|
* @param p The packet being relayed
|
|
* @return true if hop_limit should be decremented, false to preserve it
|
|
*/
|
|
bool shouldDecrementHopLimit(const meshtastic_MeshPacket *p);
|
|
|
|
/**
|
|
* Every (non duplicate) packet this node receives will be passed through this method. This allows subclasses to
|
|
* update routing tables etc... based on what we overhear (even for messages not destined to our node)
|
|
*/
|
|
virtual void sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c);
|
|
|
|
/**
|
|
* Send an ack or a nak packet back towards whoever sent idFrom
|
|
*/
|
|
void sendAckNak(meshtastic_Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex, uint8_t hopLimit = 0,
|
|
bool ackWantsAck = false);
|
|
|
|
private:
|
|
/**
|
|
* Called from loop()
|
|
* Handle any packet that is received by an interface on this node.
|
|
* Note: some packets may merely being passed through this node and will be forwarded elsewhere.
|
|
*
|
|
* Note: this packet will never be called for messages sent/generated by this node.
|
|
* Note: this method will free the provided packet.
|
|
*/
|
|
void perhapsHandleReceived(meshtastic_MeshPacket *p);
|
|
|
|
/**
|
|
* Called from perhapsHandleReceived() - allows subclass message delivery behavior.
|
|
* Handle any packet that is received by an interface on this node.
|
|
* Note: some packets may merely being passed through this node and will be forwarded elsewhere.
|
|
*
|
|
* Note: this packet will never be called for messages sent/generated by this node.
|
|
* Note: this method will free the provided packet.
|
|
*/
|
|
void handleReceived(meshtastic_MeshPacket *p, RxSource src = RX_SRC_RADIO);
|
|
|
|
/** Frees the provided packet, and generates a NAK indicating the specifed error while sending */
|
|
void abortSendAndNak(meshtastic_Routing_Error err, meshtastic_MeshPacket *p);
|
|
};
|
|
|
|
enum DecodeState { DECODE_SUCCESS, DECODE_FAILURE, DECODE_FATAL };
|
|
|
|
/** FIXME - move this into a mesh packet class
|
|
* Remove any encryption and decode the protobufs inside this packet (if necessary).
|
|
*
|
|
* @return true for success, false for corrupt packet.
|
|
*/
|
|
DecodeState perhapsDecode(meshtastic_MeshPacket *p);
|
|
|
|
/** Return 0 for success or a Routing_Error code for failure
|
|
*/
|
|
meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p);
|
|
|
|
#if !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
|
/** XEdDSA receive-side signature policy. When the packet carries a 64-byte signature *and* the
|
|
* sender's public key is known, verify it: on success learn the sender's signer bit, on failure
|
|
* 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).
|
|
*
|
|
* encodedDataSize is the wire size of the encoded Data as the sender built it; pass 0 to size
|
|
* p->decoded canonically instead (for already-decoded ingress such as plaintext-MQTT downlink,
|
|
* which bypasses perhapsDecode's crypto path).
|
|
*
|
|
* 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, size_t encodedDataSize = 0);
|
|
#endif
|
|
|
|
extern Router *router;
|
|
|
|
/// Generate a unique packet id
|
|
// FIXME, move this someplace better
|
|
PacketId generatePacketId();
|
|
|
|
#define BITFIELD_WANT_RESPONSE_SHIFT 1
|
|
#define BITFIELD_OK_TO_MQTT_SHIFT 0
|
|
#define BITFIELD_WANT_RESPONSE_MASK (1 << BITFIELD_WANT_RESPONSE_SHIFT)
|
|
#define BITFIELD_OK_TO_MQTT_MASK (1 << BITFIELD_OK_TO_MQTT_SHIFT)
|