feat(firmware): Reticulum announce TX — valid RNS destination broadcast

- 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) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-07-01 19:12:00 +01:00
parent 986ab153e4
commit 422cb9b8da

View File

@ -27,7 +27,8 @@
#include "esp_mac.h"
#include "esp_random.h"
#include <AES.h> // rweather/Crypto — AES-128-ECB for MeshCore DM cipher
#include <SHA256.h> // rweather/Crypto — HMAC-SHA256 for MeshCore packet MAC
#include <SHA256.h> // rweather/Crypto — HMAC-SHA256 (MeshCore) + SHA256 (RNS)
#include <Curve25519.h> // rweather/Crypto — X25519 encryption key for Reticulum
#include <Preferences.h> // 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