Signing and ingress hardening (#11282)
* Include warm-tier signers in the identity update gate * Fail the send when PKI encryption fails * Require signatures on licensed unicasts * Include warm-tier signers in the NodeInfo downgrade drop * Clamp hop fields on UDP multicast ingress * Address review comments on signing hardening Condense the updateUser rationale to two lines and stop calling the Balanced-mode drop a broadcast now that licensed unicasts reach it.
This commit is contained in:
co-authored by
GitHub
parent
50a37b3a19
commit
df6e67f70b
+3
-4
@@ -3504,10 +3504,9 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
||||
*/
|
||||
bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex, bool xeddsaSigned)
|
||||
{
|
||||
// Only a signed update may change the identity of a node that has proven it signs; our own record is
|
||||
// exempt. Checked before getOrCreateMeshNode so a refused update cannot evict or write the warm tier.
|
||||
const meshtastic_NodeInfoLite *existing = getMeshNode(nodeId);
|
||||
if (nodeId != getNodeNum() && existing && nodeInfoLiteHasXeddsaSigned(existing) && !xeddsaSigned) {
|
||||
// Only a signed update may change the identity of a proven signer; our own record is exempt.
|
||||
// Checked before getOrCreateMeshNode so a refusal cannot evict; isKnownXeddsaSigner covers the warm tier.
|
||||
if (nodeId != getNodeNum() && isKnownXeddsaSigner(nodeId) && !xeddsaSigned) {
|
||||
LOG_WARN("Refusing unsigned identity update for node 0x%08x that previously signed", nodeId);
|
||||
return false;
|
||||
}
|
||||
|
||||
+10
-11
@@ -681,20 +681,14 @@ bool checkXeddsaReceivePolicy(meshtastic_MeshPacket *p)
|
||||
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
|
||||
// must not become impersonatable via unsigned broadcasts until it is re-heard.
|
||||
if (nodeDB->isKnownXeddsaSigner(p->from) && isBroadcast(p->to)) {
|
||||
// Balanced rejects only what a signer always signs: non-PKI broadcasts whose signed encoding
|
||||
// would have fit, plus unicasts on ham where licensed senders sign too. Mirrors perhapsEncode.
|
||||
if (nodeDB->isKnownXeddsaSigner(p->from) && (isBroadcast(p->to) || owner.is_licensed)) {
|
||||
size_t canonicalSize;
|
||||
if (!canonicalSignableSize(&p->decoded, &canonicalSize))
|
||||
return true; // can't size it; never drop on a sizing failure
|
||||
if (canonicalSize + XEDDSA_SIGNATURE_FIELD_BYTES + MESHTASTIC_HEADER_LENGTH <= MAX_LORA_PAYLOAD_LEN) {
|
||||
LOG_WARN("Dropping unsigned broadcast from 0x%08x that previously signed", p->from);
|
||||
LOG_WARN("Dropping unsigned packet from 0x%08x that previously signed", p->from);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1172,7 +1166,12 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
*destKey.bytes);
|
||||
return meshtastic_Routing_Error_PKI_FAILED;
|
||||
}
|
||||
crypto->encryptCurve25519(p->to, getFrom(p), destKey, p->id, numbytes, bytes, p->encrypted.bytes);
|
||||
// On failure encrypted.bytes holds no ciphertext, so continuing would put the plaintext
|
||||
// on the air labelled pki_encrypted.
|
||||
if (!crypto->encryptCurve25519(p->to, getFrom(p), destKey, p->id, numbytes, bytes, p->encrypted.bytes)) {
|
||||
LOG_WARN("PKI encryption failed for destination node 0x%08x", p->to);
|
||||
return meshtastic_Routing_Error_PKI_FAILED;
|
||||
}
|
||||
numbytes += MESHTASTIC_PKC_OVERHEAD;
|
||||
p->channel = 0;
|
||||
p->pki_encrypted = true;
|
||||
|
||||
@@ -79,6 +79,11 @@ class UdpMulticastHandler final
|
||||
LOG_WARN("UDP packet with spoofed local from=0x%08x, dropping", mp.from);
|
||||
return;
|
||||
}
|
||||
// Same clamp the MQTT ingress applies: an out-of-range hop count is not relayable.
|
||||
if (mp.hop_limit > HOP_MAX || mp.hop_start > HOP_MAX) {
|
||||
LOG_WARN("UDP packet with invalid hop_limit(%u) or hop_start(%u), dropping", mp.hop_limit, mp.hop_start);
|
||||
return;
|
||||
}
|
||||
mp.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_MULTICAST_UDP;
|
||||
// Authentication metadata is local-only; Router re-establishes it after successful PKI decryption.
|
||||
mp.pki_encrypted = false;
|
||||
|
||||
@@ -53,10 +53,9 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes
|
||||
return true;
|
||||
}
|
||||
NodeNum sourceNum = getFrom(&mp);
|
||||
const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(sourceNum);
|
||||
// Broadcasts only: senders never sign unicast NodeInfo, so dropping it would break exchanges
|
||||
// with signer nodes. Backstops ingress that skips Router's downgrade drop (e.g. decoded MQTT).
|
||||
if (node && nodeInfoLiteHasXeddsaSigned(node) && !mp.xeddsa_signed && isBroadcast(mp.to)) {
|
||||
// Broadcasts only: unicast NodeInfo is unsigned off ham, so updateUser refuses the identity
|
||||
// write instead. isKnownXeddsaSigner also covers the warm tier.
|
||||
if (nodeDB->isKnownXeddsaSigner(sourceNum) && !mp.xeddsa_signed && isBroadcast(mp.to)) {
|
||||
LOG_WARN("Dropping unsigned NodeInfo broadcast from node 0x%08x that previously signed", sourceNum);
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user