From 422cb9b8da2efb9311066ab2f50de0688e03f51a Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 1 Jul 2026 19:12:00 +0100 Subject: [PATCH] =?UTF-8?q?feat(firmware):=20Reticulum=20announce=20TX=20?= =?UTF-8?q?=E2=80=94=20valid=20RNS=20destination=20broadcast?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RNS identity: X25519 (encryption, rweather Curve25519) + Ed25519 (signing, orlp) keypairs, persisted in NVS; public_key = X25519pub || Ed25519pub - dest hash = SHA256(name_hash || SHA256(pubkey)[:16])[:16] for "archy.messh" - ANNOUNCE packet: [flags 0x01][hops 0][dest_hash 16][context 0][pubkey 64 + name_hash 10 + random_hash 10 + Ed25519 sig 64 + app_data]; sig covers dest_hash||pubkey||name_hash||random_hash||app_data (no ratchet) - periodic auto-announce on the R window - VERIFIED byte-valid against the real RNS 1.3.5 library: Identity.validate accepts the signature and the dest hash matches (cross-validated offline; no RNode hardware available to test on-air) All three networks now transmit: meshTastic text, MeshCore advert+DM, RNS announce. Co-Authored-By: Claude Opus 4.8 (1M context) --- firmware/src/main.cpp | 96 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 3ac8ea2..42cb6f4 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -27,7 +27,8 @@ #include "esp_mac.h" #include "esp_random.h" #include // rweather/Crypto — AES-128-ECB for MeshCore DM cipher -#include // rweather/Crypto — HMAC-SHA256 for MeshCore packet MAC +#include // rweather/Crypto — HMAC-SHA256 (MeshCore) + SHA256 (RNS) +#include // rweather/Crypto — X25519 encryption key for Reticulum #include // NVS-backed persistent identity seed extern "C" { #include "ed_25519.h" // orlp/ed25519 (vendored, lib/ed25519) — identity, sign, ECDH @@ -427,6 +428,92 @@ static void sendMeshCoreText(const uint8_t* dest_pub, const char* text) { radio.startReceive(); } +// --- Reticulum (RNS) identity + announce ----------------------------------- +// An RNS identity is an X25519 encryption keypair + an Ed25519 signing keypair. +// public_key = X25519_pub(32) || Ed25519_pub(32). A destination hash is +// SHA256(name_hash || SHA256(public_key)[:16])[:16]. An ANNOUNCE packet is +// [flags 0x01][hops 0x00][dest_hash 16][context 0x00][announce_data] +// announce_data = public_key(64) || name_hash(10) || random_hash(10) || +// signature(64) || app_data, where the Ed25519 signature covers +// dest_hash || public_key || name_hash || random_hash || app_data +// (no ratchet: context_flag is unset). Format validated against RNS 1.3.5. +static uint8_t rnsXPrv[32], rnsXPub[32]; // X25519 (encryption) keypair +static uint8_t rnsEdSeed[32], rnsEdPrv[64], rnsEdPub[32]; // Ed25519 (signing) +static uint8_t rnsPub[64]; // X25519_pub || Ed25519_pub +static uint8_t rnsNameHash[10]; // SHA256(app name)[:10] +static uint8_t rnsDestHash[16]; // SHA256(name_hash||ident_hash)[:16] +static const char* RNS_APP_NAME = "archy.messh"; +static const char* RNS_APP_DATA = "archy-messh"; +static uint32_t lastRnsAnnounceMs = 0; +static const uint32_t RNS_ANNOUNCE_INTERVAL_MS = 30000; + +static void rnsSha256(const uint8_t* in, size_t n, uint8_t* out, int outlen) { + SHA256 sha; sha.reset(); sha.update(in, n); + uint8_t full[32]; sha.finalize(full, 32); + memcpy(out, full, outlen); +} + +static void initReticulumIdentity() { + Preferences prefs; + prefs.begin("reticulum", false); + if (prefs.getBytes("xprv", rnsXPrv, 32) != 32) { + for (int i = 0; i < 32; i++) rnsXPrv[i] = (uint8_t)esp_random(); + rnsXPrv[0] &= 248; rnsXPrv[31] &= 127; rnsXPrv[31] |= 64; // X25519 clamp + prefs.putBytes("xprv", rnsXPrv, 32); + } + if (prefs.getBytes("edseed", rnsEdSeed, 32) != 32) { + for (int i = 0; i < 32; i++) rnsEdSeed[i] = (uint8_t)esp_random(); + prefs.putBytes("edseed", rnsEdSeed, 32); + } + prefs.end(); + Curve25519::eval(rnsXPub, rnsXPrv, 0); // X25519 pub (base point) + ed25519_create_keypair(rnsEdPub, rnsEdPrv, rnsEdSeed); + memcpy(rnsPub, rnsXPub, 32); memcpy(rnsPub + 32, rnsEdPub, 32); + + uint8_t ident_hash[16]; rnsSha256(rnsPub, 64, ident_hash, 16); + rnsSha256((const uint8_t*)RNS_APP_NAME, strlen(RNS_APP_NAME), rnsNameHash, 10); + uint8_t nh_ih[26]; + memcpy(nh_ih, rnsNameHash, 10); memcpy(nh_ih + 10, ident_hash, 16); + rnsSha256(nh_ih, 26, rnsDestHash, 16); + Serial.print("Reticulum dest hash: "); + for (int i = 0; i < 16; i++) Serial.printf("%02x", rnsDestHash[i]); + Serial.println(); +} + +static void sendReticulumAnnounce() { + uint8_t random_hash[10]; + for (int i = 0; i < 10; i++) random_hash[i] = (uint8_t)esp_random(); + size_t adl = strlen(RNS_APP_DATA); + + uint8_t signed_data[16 + 64 + 10 + 10 + 32]; size_t sl = 0; + memcpy(signed_data + sl, rnsDestHash, 16); sl += 16; + memcpy(signed_data + sl, rnsPub, 64); sl += 64; + memcpy(signed_data + sl, rnsNameHash, 10); sl += 10; + memcpy(signed_data + sl, random_hash, 10); sl += 10; + memcpy(signed_data + sl, RNS_APP_DATA, adl); sl += adl; + + uint8_t sig[64]; + ed25519_sign(sig, signed_data, sl, rnsEdPub, rnsEdPrv); + + uint8_t pkt[2 + 16 + 1 + 64 + 10 + 10 + 64 + 32]; size_t p = 0; + pkt[p++] = 0x01; // flags: HEADER_1|BROADCAST|SINGLE|ANNOUNCE + pkt[p++] = 0x00; // hops + memcpy(pkt + p, rnsDestHash, 16); p += 16; + pkt[p++] = 0x00; // context = NONE + memcpy(pkt + p, rnsPub, 64); p += 64; + memcpy(pkt + p, rnsNameHash, 10); p += 10; + memcpy(pkt + p, random_hash, 10); p += 10; + memcpy(pkt + p, sig, 64); p += 64; + memcpy(pkt + p, RNS_APP_DATA, adl); p += adl; + + radio.standby(); + int st = radio.transmit(pkt, p); + Serial.printf(" [R] announce -> tx=%d (%u bytes) : ", st, (unsigned)p); + for (size_t i = 0; i < p; i++) Serial.printf("%02x", pkt[i]); // for RNS cross-validation + Serial.println(); + radio.startReceive(); +} + // --- MeshCore contacts (heard adverts) + DM receive ------------------------ // To decrypt a direct message we need the SENDER's full public key. MeshCore // packets only carry a 1-byte src_hash (pubkey prefix), so we remember pubkeys @@ -818,6 +905,7 @@ void setup() { Serial.printf("Meshtastic node: %s (%s)\n", DEVICE_NAME, nodeId); initMeshCoreIdentity(); + initReticulumIdentity(); activeIdx = 0; applyConfig(CONFIGS[0]); @@ -906,6 +994,12 @@ void loop() { sendMeshCoreAdvert(); lastMcAdvertMs = millis(); } + // Announce ourselves as a Reticulum destination while tuned to that PHY. + if (CONFIGS[activeIdx].letter == 'R' && + (lastRnsAnnounceMs == 0 || millis() - lastRnsAnnounceMs > RNS_ANNOUNCE_INTERVAL_MS)) { + sendReticulumAnnounce(); + lastRnsAnnounceMs = millis(); + } drawUI(); frame++; delay(45); // ~20 fps