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:
Thomas Göttgens
2026-07-29 22:28:25 +00:00
committed by GitHub
parent 50a37b3a19
commit df6e67f70b
4 changed files with 21 additions and 19 deletions
+10 -11
View File
@@ -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;