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:
parent
986ab153e4
commit
422cb9b8da
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user