From 649f1961715cd67c7ac26b759212272a349a98c8 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 1 Jul 2026 21:59:44 +0100 Subject: [PATCH] =?UTF-8?q?feat(firmware):=20robust=20Reticulum=20announce?= =?UTF-8?q?=20RX=20=E2=80=94=20decode=20live=20RNode=20announces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - try both packet alignments: real RNode transmissions arrive with a 1-byte leading offset vs a raw RNS packet, so parse at offset 0 and 1 and take the alignment whose dest-hash validates - accept on validated dest-hash (SHA256(name_hash||SHA256(pub)[:16])[:16]) and treat the Ed25519 signature as a bonus: announces with large app_data get truncated at the LoRa 255B MTU, which breaks the signature but not the pubkey<->destination binding; report sig ok vs unverified(truncated) - verified end-to-end on real hardware: decoded a live RNS RNode's (.116) announces over the air with sig=ok, dest + display name extracted All three networks now confirmed on real hardware: meshTastic, MeshCore, RNS. Co-Authored-By: Claude Opus 4.8 (1M context) --- firmware/src/main.cpp | 74 ++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index a3ec3b3..183dd21 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -534,64 +534,66 @@ static void rnsStoreDest(const uint8_t* hash, const uint8_t* pub, const char* ap rnsDests[slot].app[sizeof(rnsDests[slot].app) - 1] = 0; } -// Parse a received Reticulum packet: decode + verify ANNOUNCE, register dest. -static void handleReticulumPacket(const uint8_t* buf, size_t len) { - if (len < 19) return; - uint8_t flags = buf[0]; +// Try to parse an ANNOUNCE starting at buf[off]. Accept on a validated dest +// hash (SHA256(name_hash || SHA256(pub)[:16])[:16]), which proves the pubkey↔ +// destination binding and stays intact even when the announce is truncated at +// the LoRa MTU. The Ed25519 signature is checked as a bonus but not required: +// real RNode announces with large app_data (e.g. archy's ARCHY:2:{ed}:{x25519}, +// 336B) get cut at 255B on-air, which breaks the signature but not the binding. +static bool tryParseRnsAnnounce(const uint8_t* buf, size_t len, size_t off) { + if (len < off + 19) return false; + uint8_t flags = buf[off]; + if ((flags & 0x03) != 0x01) return false; // packet_type ANNOUNCE uint8_t header_type = (flags >> 6) & 0x01; uint8_t context_flag = (flags >> 5) & 0x01; - uint8_t ptype = flags & 0x03; // packet_type (2 bits) - size_t i = 2; // skip flags + hops - if (header_type == 0x01) i += 16; // HEADER_2 carries a transport_id - if (i + 16 + 1 > len) return; - const uint8_t* dest_hash = buf + i; i += 16; - i += 1; // context byte + size_t i = off + 2; // skip flags + hops + if (header_type == 0x01) i += 16; // HEADER_2 transport_id + if (i + 17 > len) return false; + const uint8_t* dest_hash = buf + i; i += 16 + 1; // dest_hash + context const uint8_t* data = buf + i; int data_len = (int)len - (int)i; - - if (ptype != 0x01) return; // only ANNOUNCE for now int ratch = context_flag ? 32 : 0; - if (data_len < 64 + 10 + 10 + ratch + 64) return; - const uint8_t* pub = data; - const uint8_t* name_hash = data + 64; + if (data_len < 64 + 10 + 10 + ratch + 64) return false; + const uint8_t* pub = data; + const uint8_t* name_hash = data + 64; + + uint8_t ih[16]; rnsSha256(pub, 64, ih, 16); + uint8_t nh_ih[26]; memcpy(nh_ih, name_hash, 10); memcpy(nh_ih + 10, ih, 16); + uint8_t expect[16]; rnsSha256(nh_ih, 26, expect, 16); + if (memcmp(expect, dest_hash, 16) != 0) return false; // not a valid announce here + const uint8_t* random_hash = data + 74; const uint8_t* ratchet = data + 84; const uint8_t* signature = data + 84 + ratch; const uint8_t* app_data = data + 84 + ratch + 64; int app_len = data_len - (84 + ratch + 64); - - // signed_data = dest_hash || pub || name_hash || random_hash || ratchet || app_data - uint8_t sd[16 + 64 + 10 + 10 + 32 + 64]; size_t sl = 0; + uint8_t sd[16 + 64 + 10 + 10 + 32 + 200]; size_t sl = 0; memcpy(sd + sl, dest_hash, 16); sl += 16; memcpy(sd + sl, pub, 64); sl += 64; memcpy(sd + sl, name_hash, 10); sl += 10; memcpy(sd + sl, random_hash, 10); sl += 10; if (ratch) { memcpy(sd + sl, ratchet, 32); sl += 32; } - int app_take = app_len; if (app_take > 64) app_take = 64; - if (app_take > 0) { memcpy(sd + sl, app_data, app_take); sl += app_take; } - - // Verify the Ed25519 signature with the announced signing key (pub[32:64]). - if (!ed25519_verify(signature, sd, sl, pub + 32)) { - Serial.println(" [R] announce with bad signature — dropped"); - return; - } - // Confirm dest hash = SHA256(name_hash || SHA256(pub)[:16])[:16]. - uint8_t ih[16]; rnsSha256(pub, 64, ih, 16); - uint8_t nh_ih[26]; memcpy(nh_ih, name_hash, 10); memcpy(nh_ih + 10, ih, 16); - uint8_t expect[16]; rnsSha256(nh_ih, 26, expect, 16); - if (memcmp(expect, dest_hash, 16) != 0) { - Serial.println(" [R] announce dest-hash mismatch — dropped"); - return; - } + int cap = app_len; if (cap > 200) cap = 200; + if (cap > 0) { memcpy(sd + sl, app_data, cap); sl += cap; } + bool sig_ok = ed25519_verify(signature, sd, sl, pub + 32); char app[24] = {0}; - int an = app_take < 23 ? app_take : 23; if (an > 0) memcpy(app, app_data, an); + int an = app_len < 23 ? app_len : 23; if (an > 0) memcpy(app, app_data, an); rnsStoreDest(dest_hash, pub, app); strncpy(lastMsg, app[0] ? app : "announce", sizeof(lastMsg) - 1); lastMsg[sizeof(lastMsg) - 1] = 0; lastMsgNet = 'R'; lastMsgMs = millis(); Serial.printf(" decoded[R](announce): dest="); for (int k = 0; k < 8; k++) Serial.printf("%02x", dest_hash[k]); - Serial.printf(" app_data=\"%s\"\n", app); + Serial.printf(" sig=%s app=\"%s\"\n", sig_ok ? "ok" : "unverified(truncated)", app); + return true; +} + +// Parse a received Reticulum packet. Real RNode transmissions arrive with a +// 1-byte leading offset vs a raw RNS packet, so try both alignments and take +// the one whose dest-hash validates. +static void handleReticulumPacket(const uint8_t* buf, size_t len) { + if (tryParseRnsAnnounce(buf, len, 0)) return; + tryParseRnsAnnounce(buf, len, 1); } // --- MeshCore contacts (heard adverts) + DM receive ------------------------