ota: .mota container format, merkle tree, EndF self-identity, signature verify

This commit is contained in:
Valentin Kivachuk Burda
2026-06-29 20:17:07 +02:00
parent 1869b57d62
commit 07b20f042e
16 changed files with 1291 additions and 0 deletions
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <string.h>
// Block-availability helpers (docs/ota_protocol.md §7).
//
// Availability is *derived* from the staged manifest's leaves[]: block i is present iff its 4-byte
// leaf slot is non-erased (!= FF FF FF FF). No separate persistent structure. A compact bitmap
// (1 bit/block) is used on the wire (OTA_HAVE) and as an in-RAM cache. All ops are caller-buffer
// based; no allocation.
namespace mesh {
namespace ota {
inline bool leaf_present(const uint8_t* leaves, uint32_t i) {
const uint8_t* p = leaves + (size_t)i * 4;
return !(p[0] == 0xFF && p[1] == 0xFF && p[2] == 0xFF && p[3] == 0xFF);
}
inline uint32_t bitmap_bytes(uint32_t block_count) { return (block_count + 7) / 8; }
inline bool bitmap_get(const uint8_t* bm, uint32_t i) {
return (bm[i >> 3] >> (i & 7)) & 1;
}
inline void bitmap_set(uint8_t* bm, uint32_t i, bool v) {
uint8_t mask = (uint8_t)(1u << (i & 7));
if (v) bm[i >> 3] |= mask; else bm[i >> 3] &= (uint8_t)~mask;
}
// Build a bitmap (caller buffer >= bitmap_bytes(count)) from leaves[].
inline void leaves_to_bitmap(const uint8_t* leaves, uint32_t count, uint8_t* bm_out) {
memset(bm_out, 0, bitmap_bytes(count));
for (uint32_t i = 0; i < count; i++)
if (leaf_present(leaves, i)) bitmap_set(bm_out, i, true);
}
inline uint32_t count_present(const uint8_t* leaves, uint32_t count) {
uint32_t n = 0;
for (uint32_t i = 0; i < count; i++) if (leaf_present(leaves, i)) n++;
return n;
}
inline bool all_present(const uint8_t* leaves, uint32_t count) {
for (uint32_t i = 0; i < count; i++) if (!leaf_present(leaves, i)) return false;
return true;
}
} // namespace ota
} // namespace mesh
+42
View File
@@ -0,0 +1,42 @@
#include "FirmwareInfo.h"
#include "Multihash.h"
#include "OtaByteIO.h"
#include <string.h>
namespace mesh {
namespace ota {
bool find_self_firmware(const uint8_t* region, uint32_t region_len,
SelfFwInfo& out, bool verify_body) {
out = SelfFwInfo();
if (!region || region_len < ENDF_LEN) return false;
for (uint32_t off = 0; off + ENDF_LEN <= region_len; off++) {
if (region[off] != ENDF_MAGIC[0]) continue; // cheap pre-filter ('E')
if (memcmp(region + off, ENDF_MAGIC, 4) != 0) continue;
uint32_t body_len = rd_u32le(region + off + 4);
if (body_len != off) continue; // trailer must sit right after the body
if (verify_body) {
uint8_t h[8];
mh8(h, region, body_len);
if (memcmp(h, region + off + 8, 8) != 0) continue; // coincidental marker — keep scanning
}
out.valid = true;
out.endf_offset = off;
out.body_len = body_len;
out.image_len = off + ENDF_LEN;
memcpy(out.body_hash, region + off + 8, 8);
// Fixed 56-byte trailer: the self-describing identity follows body_hash at constant offsets
// (fw_version@16, target_id@20, hw_id@24..56). Zero/"" means "unknown".
out.fw_version = rd_u32le(region + off + 16);
out.target_id = rd_u32le(region + off + 20);
memcpy(out.hw_id, region + off + 24, 32);
out.hw_id[32] = 0;
return true;
}
return false;
}
} // namespace ota
} // namespace mesh
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#include "OtaFormat.h"
// Locate the EndF trailer in a firmware image to learn the running firmware's size + identity
// (docs/ota_protocol.md §2). Portable: operates on a contiguous, readable region — on nRF52/ESP32
// the application flash is memory-mapped, so the region pointer is just (const uint8_t*)APP_BASE.
namespace mesh {
namespace ota {
struct SelfFwInfo {
bool valid = false;
uint32_t body_len = 0; // firmware body length (excludes the EndF trailer)
uint32_t image_len = 0; // body_len + ENDF_LEN (what a delta base / full image hashes over)
uint32_t endf_offset = 0; // offset of the "EndF" marker within the region (== body_len)
uint8_t body_hash[8] = {0}; // sha2-256:8 of the body (read from EndF; == a delta's base_hash)
// Self-describing identity — always present in the fixed 56-byte trailer (zero/"" means "unknown",
// e.g. a dev build with no dotted version).
uint32_t fw_version = 0; // packed MAJOR<<24|MINOR<<16|PATCH<<8|pre
uint32_t target_id = 0; // sha2-256:4(env) as uint32 — hw+role+partition (fetch routing)
char hw_id[33] = {0}; // readable hardware tag (NUL-terminated), e.g. "RAK4631"
};
// Scan `region[0..region_len)` for the firmware's EndF trailer. The body starts at offset 0, so the
// trailer's offset must equal its stored body_len — this uniquely identifies the running firmware's
// EndF even if a staged `.mota` (which contains its own embedded EndF) sits higher in the region.
// If `verify_body` is true the body hash is recomputed and must match (rules out coincidental markers).
bool find_self_firmware(const uint8_t* region, uint32_t region_len,
SelfFwInfo& out, bool verify_body = false);
} // namespace ota
} // namespace mesh
+110
View File
@@ -0,0 +1,110 @@
#include "MerkleTree.h"
#include "Multihash.h"
#include <string.h>
namespace mesh {
namespace ota {
void merkle_leaf(uint8_t out[4], const uint8_t* block, uint32_t block_len) {
mh4(out, block, block_len);
}
void merkle_combine(uint8_t out[4], const uint8_t* left, const uint8_t* right) {
sha256_trunc2(out, 4, left, 4, right, 4);
}
// Root via binary-counter / Merkle-Mountain-Range with right-to-left bagging.
// Equivalent to the level-by-level "pair adjacent, promote lone last (left||right)" reduction
// (verified against the reference implementation across many counts in the native tests).
void merkle_root(uint8_t out[4], const uint8_t* leaves, uint32_t count) {
if (count == 0) { memset(out, 0, 4); return; }
if (count == 1) { memcpy(out, leaves, 4); return; }
uint8_t peaks[32][4];
bool valid[32] = { false };
for (uint32_t i = 0; i < count; i++) {
uint8_t cur[4];
memcpy(cur, leaves + (size_t)i * 4, 4);
uint32_t level = 0;
while (valid[level]) { // carry: combine with the pending peak at this level
merkle_combine(cur, peaks[level], cur); // peak is earlier (left), cur is right
valid[level] = false;
level++;
}
memcpy(peaks[level], cur, 4);
valid[level] = true;
}
// bag peaks right-to-left: acc starts at the lowest set level (rightmost peak)
int level = 0;
while (level < 32 && !valid[level]) level++;
uint8_t acc[4];
memcpy(acc, peaks[level], 4);
for (int l = level + 1; l < 32; l++) {
if (valid[l]) merkle_combine(acc, peaks[l], acc); // higher peak is left, acc is right
}
memcpy(out, acc, 4);
}
bool merkle_verify(const uint8_t* block, uint32_t block_len, uint32_t index,
const uint8_t* siblings, uint8_t n_siblings,
const uint8_t root[4], uint32_t count) {
uint8_t leaf[4];
merkle_leaf(leaf, block, block_len);
return merkle_verify_from_leaf(leaf, index, siblings, n_siblings, root, count);
}
bool merkle_verify_from_leaf(const uint8_t leaf[4], uint32_t index,
const uint8_t* siblings, uint8_t n_siblings,
const uint8_t root[4], uint32_t count) {
if (count == 0 || index >= count) return false;
uint8_t h[4];
memcpy(h, leaf, 4);
uint32_t idx = index;
uint32_t n = count;
uint8_t p = 0;
while (n > 1) {
bool is_last_odd = (n & 1u) && (idx == n - 1);
if (!is_last_odd) {
if (p >= n_siblings) return false;
const uint8_t* sib = siblings + (size_t)p * 4;
p++;
if (idx & 1u) merkle_combine(h, sib, h); // odd index -> sibling on the left
else merkle_combine(h, h, sib); // even index -> sibling on the right
}
idx >>= 1;
n = (n + 1) >> 1;
}
return (p == n_siblings) && (memcmp(h, root, 4) == 0);
}
uint8_t merkle_gen_proof(const uint8_t* leaves, uint32_t count, uint32_t index,
uint8_t* scratch, uint8_t* out_siblings) {
if (count == 0 || index >= count) return 0;
memcpy(scratch, leaves, (size_t)count * 4);
uint32_t n = count, idx = index;
uint8_t p = 0;
while (n > 1) {
bool is_last_odd = (n & 1u) && (idx == n - 1);
if (!is_last_odd) {
uint32_t s = (idx & 1u) ? idx - 1 : idx + 1;
memcpy(out_siblings + (size_t)p * 4, scratch + (size_t)s * 4, 4);
p++;
}
// reduce one level in place (parent m written from children 2m,2m+1; m <= i so it's safe)
uint32_t m = 0;
for (uint32_t i = 0; i < n; i += 2) {
if (i + 1 < n) merkle_combine(scratch + (size_t)m * 4, scratch + (size_t)i * 4, scratch + (size_t)(i + 1) * 4);
else memmove(scratch + (size_t)m * 4, scratch + (size_t)i * 4, 4);
m++;
}
idx >>= 1;
n = (n + 1) >> 1;
}
return p;
}
} // namespace ota
} // namespace mesh
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
// Merkle tree over PAYLOAD blocks, sha2-256:4 (4-byte) leaves/nodes. See docs/ota_protocol.md §6.
//
// Scheme: leaf = H(block); node = H(left || right); on an odd level the last node is promoted
// unchanged (no duplication). Root = single remaining node.
//
// No dynamic allocation: the root is computed with an O(log count) "binary counter" of partial
// peaks (<= 32 levels => 128 bytes of stack). Proofs carry only sibling digests; the left/right
// direction is derived from the block index + total count (no direction bits on the wire).
namespace mesh {
namespace ota {
// leaf digest of one payload block
void merkle_leaf(uint8_t out[4], const uint8_t* block, uint32_t block_len);
// parent of two 4-byte children
void merkle_combine(uint8_t out[4], const uint8_t* left, const uint8_t* right);
// root over `count` contiguous 4-byte leaf digests (leaves[count*4]). count >= 1.
void merkle_root(uint8_t out[4], const uint8_t* leaves, uint32_t count);
// Verify that `block` is block `index` of a `count`-block payload whose tree has the given `root`.
// `siblings` is n_siblings contiguous 4-byte digests, ordered leaf->root (promoted levels omitted;
// left/right direction derived from index + count).
bool merkle_verify(const uint8_t* block, uint32_t block_len, uint32_t index,
const uint8_t* siblings, uint8_t n_siblings,
const uint8_t root[4], uint32_t count);
// Same, but starting from a precomputed 4-byte leaf digest (skips the H(block) step).
bool merkle_verify_from_leaf(const uint8_t leaf[4], uint32_t index,
const uint8_t* siblings, uint8_t n_siblings,
const uint8_t root[4], uint32_t count);
// Generate the proof (ordered sibling digests) for block `index`, for a server holding leaves[].
// `scratch` must be >= count*4 bytes (working buffer); `out_siblings` >= 32*4 bytes.
// Returns the number of 4-byte siblings written. Output matches the wire form merkle_verify expects.
uint8_t merkle_gen_proof(const uint8_t* leaves, uint32_t count, uint32_t index,
uint8_t* scratch, uint8_t* out_siblings);
} // namespace ota
} // namespace mesh
+83
View File
@@ -0,0 +1,83 @@
#include "MotaContainer.h"
#include "MerkleTree.h"
#include "Multihash.h"
#include "OtaByteIO.h"
#include <string.h>
namespace mesh {
namespace ota {
bool MotaManifest::is_approved() const {
return approval && memcmp(approval, APPROVAL_YES, 4) == 0;
}
// Fixed-layout parse (docs/ota_protocol.md §4): every field sits at a constant offset — base_hash(8),
// signer_pubkey(32) and signature(64) are ALWAYS present (zero-filled when not applicable), so there are
// no conditionals. Only leaves[]/payload (after `approval`) is variable, read by the caller. The signature
// always covers manifest[0, MOTA_SIGNED_LEN). Returns false on any over-read or bad format_ver.
static bool parse_manifest_fields(ByteReader& r, MotaManifest& out) {
out.format_ver = r.u8();
if (out.format_ver != MOTA_FORMAT_VER) return false;
out.flags = r.u8();
out.hash_algo = r.u8();
out.target_id = r.u32();
out.fw_version = r.u32();
out.image_size = r.u32();
out.payload_size = r.u32();
out.block_size_log2 = r.u8();
out.merkle_root = r.take(4);
out.image_hash = r.take(32);
out.codec_id = r.u8();
out.hw_id = r.take(32); // 32-byte NUL-padded hardware tag (signed)
out.base_hash = r.take(8); // always present (zero for a full image)
out.signer_pubkey = r.take(32); // always present (zero when unsigned)
out.signed_len = MOTA_SIGNED_LEN; // signature always covers manifest[0, 129)
out.signature = r.take(64); // always present (zero when unsigned)
out.approval = r.take(4);
if (!r.ok) return false;
if (out.block_size_log2 == 0 || out.block_size_log2 > 24 || out.payload_size == 0) return false;
out.block_count = (out.payload_size + out.block_size() - 1) / out.block_size();
// block_idx is uint16 on the wire; capping here also keeps block_count*4 (leaves length) from overflowing.
return out.block_count != 0 && out.block_count <= 0xFFFFu;
}
bool mota_parse(const uint8_t* buf, uint32_t len, MotaManifest& out) {
out = MotaManifest();
if (len < 4 + 4 + 5) return false;
if (memcmp(buf, MOTA_MAGIC, 4) != 0) return false;
if (memcmp(buf + len - 5, MOTA_TRAILER, 5) != 0) return false;
if (rd_u32le(buf + 4) != len) return false; // MOTA_TOTAL_SIZE must equal the actual length
ByteReader r(buf, len - 5); // everything up to (not incl.) the trailer
r.skip(4 + 4); // MAGIC + MOTA_TOTAL_SIZE (already validated)
out.manifest_start = buf + 8;
if (!parse_manifest_fields(r, out)) return false;
out.leaves = r.take(out.block_count * 4);
out.payload = r.take(out.payload_size);
if (!r.ok) return false;
return r.pos() == len - 5; // payload must end exactly at the trailer
}
bool mota_parse_manifest(const uint8_t* mf, uint32_t len, MotaManifest& out) {
out = MotaManifest();
out.manifest_start = mf;
ByteReader r(mf, len); // a standalone manifest = container bytes [8, leaves)
return parse_manifest_fields(r, out);
}
bool mota_check_root(const MotaManifest& m) {
if (!m.leaves || m.block_count == 0) return false;
uint8_t root[4];
merkle_root(root, m.leaves, m.block_count);
return memcmp(root, m.merkle_root, 4) == 0;
}
bool mota_check_image_hash_full(const MotaManifest& m) {
if (!m.is_full() || !m.payload || !m.image_hash) return false;
uint8_t h[32];
mh32(h, m.payload, m.payload_size);
return memcmp(h, m.image_hash, 32) == 0;
}
} // namespace ota
} // namespace mesh
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#include "OtaFormat.h"
// Parse/validate a `.mota` container that is fully present in a RAM buffer (docs/ota_protocol.md
// §3-§4). Variable-length parts are referenced by pointer into the caller's buffer — no copies, no
// allocation. (Device flash-backed staging gets a streaming variant in a later milestone; the field
// layout here is the single source of truth.)
namespace mesh {
namespace ota {
struct MotaManifest {
uint8_t format_ver = 0;
uint8_t flags = 0;
uint8_t hash_algo = 0;
uint32_t target_id = 0;
uint32_t fw_version = 0;
uint32_t image_size = 0;
uint32_t payload_size = 0;
uint8_t block_size_log2 = 0;
uint8_t codec_id = 0;
uint32_t block_count = 0;
// Fixed layout (docs/ota_protocol.md §4): every field below sits at a constant offset and is ALWAYS
// present; base_hash/signer_pubkey/signature are zero-filled when not applicable (full / unsigned).
const uint8_t* merkle_root = nullptr; // 4 @20
const uint8_t* image_hash = nullptr; // 32 @24
const uint8_t* hw_id = nullptr; // 32 @57 (NUL-padded ASCII hardware tag; signed)
const uint8_t* base_hash = nullptr; // 8 @89 (zero for a full image)
const uint8_t* signer_pubkey = nullptr; // 32 @97 (zero when unsigned)
const uint8_t* signature = nullptr; // 64 @129 (zero when unsigned)
const uint8_t* approval = nullptr; // 4 @193
const uint8_t* leaves = nullptr; // 4 * block_count (the only variable-length field)
const uint8_t* payload = nullptr; // payload_size
const uint8_t* manifest_start = nullptr;// first manifest byte (== start of the signed region)
uint32_t signed_len = 0; // = MOTA_SIGNED_LEN (129): bytes the signature covers
bool is_full() const { return flags & MFLAG_FULL; }
bool is_signed() const { return flags & MFLAG_SIGNED; }
uint32_t block_size() const { return 1u << block_size_log2; }
bool is_approved() const;
};
// Parse a whole container in `buf[len]`. Returns true on success and fills `out` with pointers into
// `buf`. Validates MAGIC, TRAILER, MOTA_TOTAL_SIZE, format_ver, and internal length consistency.
bool mota_parse(const uint8_t* buf, uint32_t len, MotaManifest& out);
// Parse a standalone manifest (the bytes [manifest_start, leaves) of a container, i.e. without the
// MAGIC/TOTAL_SIZE framing, leaves[] or payload). Used by the apply path, which receives the manifest
// separately from the image. Sets the fixed fields + signer/signature + signed_len; leaves/payload
// are left null.
bool mota_parse_manifest(const uint8_t* mf, uint32_t len, MotaManifest& out);
// Recompute the merkle root from the manifest's leaves[] and compare to the merkle_root field.
bool mota_check_root(const MotaManifest& m);
// For FULL images only: check sha2-256:32(payload) == image_hash.
bool mota_check_image_hash_full(const MotaManifest& m);
} // namespace ota
} // namespace mesh
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#include "Utils.h" // mesh::Utils::sha256 (real on device; real host SHA-256 via test/mocks)
#include "OtaFormat.h"
// Thin multihash helpers: SHA-256 truncated to N bytes. No state, no allocation.
namespace mesh {
namespace ota {
inline void sha256_trunc(uint8_t* out, size_t out_len, const uint8_t* data, size_t len) {
mesh::Utils::sha256(out, out_len, data, (int)len);
}
inline void sha256_trunc2(uint8_t* out, size_t out_len,
const uint8_t* a, size_t a_len,
const uint8_t* b, size_t b_len) {
mesh::Utils::sha256(out, out_len, a, (int)a_len, b, (int)b_len);
}
inline void mh4(uint8_t out[4], const uint8_t* data, size_t len) { sha256_trunc(out, 4, data, len); }
inline void mh8(uint8_t out[8], const uint8_t* data, size_t len) { sha256_trunc(out, 8, data, len); }
inline void mh32(uint8_t out[32], const uint8_t* data, size_t len){ sha256_trunc(out, 32, data, len); }
} // namespace ota
} // namespace mesh
+58
View File
@@ -0,0 +1,58 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
// A tiny bounds-checked little-endian cursor for reading the `.mota` container (docs/ota_protocol.md §3-§4)
// in a self-documenting way: each field is read by name in order, instead of hand-computed byte offsets
// (`p[0]`, `rd_u32(p+3)`, `p += 89`, `NEED(n)` ...). Any over-read flips `ok` false and yields zero/null, so
// callers parse the whole struct then check `r.ok` once. 32-bit offsets (a container can be >64 KB; the
// 16-byte LoRa wire messages keep their own uint16 cursor in OtaProtocol.cpp). No allocation; `take()`
// returns a pointer INTO the caller's buffer (zero-copy), matching the manifest's by-pointer fields.
namespace mesh {
namespace ota {
// Little-endian scalar read/write for RANDOM-access fields (a specific offset into a buffer, e.g. a wire
// row or a fixed manifest slot). Sequential parsing should prefer the ByteReader cursor below. These
// replace the per-file `rd_u32`/`wr_u32` helpers that were copy-pasted across the OTA sources.
inline uint16_t rd_u16le(const uint8_t* p) { return (uint16_t)p[0] | ((uint16_t)p[1] << 8); }
inline uint32_t rd_u32le(const uint8_t* p) {
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
inline void wr_u16le(uint8_t* p, uint16_t v) { p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); }
inline void wr_u32le(uint8_t* p, uint32_t v) {
p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24);
}
// Round to a multiple of `unit` (a power of two — a flash sector/page size). Names the `& ~(unit-1)`
// idiom so flash-geometry math in the stores reads as intent (align down / align up).
inline uint32_t align_down(uint32_t x, uint32_t unit) { return x & ~(unit - 1); }
inline uint32_t align_up(uint32_t x, uint32_t unit) { return (x + unit - 1) & ~(unit - 1); }
struct ByteReader {
const uint8_t* p;
uint32_t len;
uint32_t n = 0;
bool ok = true;
ByteReader(const uint8_t* buf, uint32_t length) : p(buf), len(length) {}
uint32_t pos() const { return n; }
bool fits(uint32_t k) const { return ok && (uint64_t)n + k <= len; }
uint8_t u8() { if (!fits(1)) { ok = false; return 0; } return p[n++]; }
uint32_t u32() { // little-endian
if (!fits(4)) { ok = false; return 0; }
uint32_t v = rd_u32le(p + n); n += 4; return v;
}
// Borrow `k` bytes at the cursor (e.g. merkle_root[4], leaves[4*BC]) and advance; null on overflow.
const uint8_t* take(uint32_t k) {
if (!fits(k)) { ok = false; return nullptr; }
const uint8_t* r = p + n; n += k; return r;
}
void skip(uint32_t k) { if (!fits(k)) { ok = false; return; } n += k; }
};
} // namespace ota
} // namespace mesh
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
// On-the-wire constants for the MeshCore OTA `.mota` container and protocol.
// Normative definition: docs/ota_protocol.md (v1). Mirrors tools/mota/motalib.py.
//
// Portable: no Arduino / RadioLib includes. Compiles on the native host (unit tests) and on device.
namespace mesh {
namespace ota {
// ---- container framing ----------------------------------------------------
static const uint8_t MOTA_MAGIC[4] = { 'm', 'O', 'T', 'A' }; // 6D 4F 54 41
static const uint8_t MOTA_TRAILER[5] = { 'v', 'k', '4', '9', '6' }; // 76 6B 34 39 36
static const uint8_t ENDF_MAGIC[4] = { 'E', 'n', 'd', 'F' }; // 45 6E 64 46
// Fixed 56-byte trailer (docs/ota_protocol.md §2): marker(4) body_len(4) body_hash8(8) + a self-describing
// identity block fw_version(4) target_id(4) hw_id(32). No optional/variable parts.
static const uint32_t ENDF_LEN = 56;
// ---- manifest -------------------------------------------------------------
static const uint8_t MOTA_FORMAT_VER = 2; // fixed-layout manifest (see offsets below)
static const uint8_t HASH_ALGO_SHA256 = 0x12; // multihash code
// Fixed manifest layout (manifest-minus-leaves) — every field is present at a constant offset, so the
// parser is plain offset reads (docs/ota_protocol.md §4). base_hash/signer_pubkey/signature are always
// present (zero-filled when not applicable); only leaves[] (after `approval`) is variable.
static const uint32_t MOTA_OFF_BASE_HASH = 89; // 8 (zero for a full image)
static const uint32_t MOTA_OFF_SIGNER = 97; // 32 (zero when unsigned)
static const uint32_t MOTA_OFF_SIGNATURE = 129; // 64 (zero when unsigned) — covers manifest[0,129)
static const uint32_t MOTA_OFF_APPROVAL = 193; // 4
static const uint32_t MOTA_MFL = 197; // manifest-minus-leaves length (constant)
static const uint32_t MOTA_SIGNED_LEN = 129; // bytes the signature covers (manifest[0, signer_end))
// hw_id: a fixed 32-byte, NUL-padded ASCII string naming the hardware a firmware can boot on (e.g.
// "RAK4631", "Heltec_v3"). Same hw_id == bootable-compatible (a role switch on the same board keeps it;
// different MCU/board differs). It sits in the SIGNED region of the manifest, so it can't be tampered.
// The applier refuses a `.mota` whose hw_id differs from the device's own (brick-safety, esp. for a manual
// cross-target `ota dev want`). An empty hw_id on either side = "unknown", and the check is skipped.
static const uint8_t MOTA_HW_ID_LEN = 32;
static const uint8_t MFLAG_FULL = 0x01; // 0 = delta/partial, 1 = full image
static const uint8_t MFLAG_SIGNED = 0x02;
static const uint8_t CODEC_FULL = 0;
static const uint8_t CODEC_DETOOLS_SEQUENTIAL = 1;
static const uint8_t CODEC_DETOOLS_INPLACE = 2;
// ---- firmware version ------------------------------------------------------
// The comparable uint32 carried in the manifest + EndF identity: MAJOR<<24 | MINOR<<16 | PATCH<<8 | PRE.
// 0 == "unknown" (e.g. a dev build with no dotted version). Defined once here so the pack/unpack layout
// isn't re-derived with raw shifts at each call site (OtaSelf builds it, OtaCli renders it).
struct FwVersion {
uint8_t major, minor, patch, prerelease;
static FwVersion unpack(uint32_t v) {
return { (uint8_t)(v >> 24), (uint8_t)(v >> 16), (uint8_t)(v >> 8), (uint8_t)v };
}
uint32_t pack() const {
return ((uint32_t)major << 24) | ((uint32_t)minor << 16) | ((uint32_t)patch << 8) | prerelease;
}
};
// ---- hash truncations -----------------------------------------------------
static const uint8_t MH4 = 4; // sha2-256:4 (merkle leaves/nodes/root/proofs)
static const uint8_t MH8 = 8; // sha2-256:8 (base/EndF body hash)
static const uint8_t MH32 = 32; // sha2-256:32 (image security anchor)
// ---- approval marker (manifest field, after the signature) ----------------
static const uint8_t APPROVAL_NOT[4] = { 0xFF, 0xFF, 0xFF, 0xFF };
static const uint8_t APPROVAL_YES[4] = { 'A', 'P', 'R', 'V' }; // 41 50 52 56
// ---- LoRa protocol --------------------------------------------------------
// The packet payload type is PAYLOAD_TYPE_OTA (0x0C), defined in src/Packet.h for the core dispatch.
enum OtaMsgType : uint8_t {
OTA_ADV = 0x01,
OTA_QUERY = 0x02,
OTA_HAVE = 0x03,
OTA_GET_MANIFEST = 0x04,
OTA_MANIFEST = 0x05,
OTA_REQ = 0x06, // request a window of blocks' DATA fragments
OTA_DATA = 0x07, // one fragment of a block's data (self-describing by frag_off; no proof)
OTA_REQ_PROOF = 0x08, // request the merkle proof for one block (data + proof are fetched separately)
OTA_PROOF = 0x09, // the merkle proof for one block
};
static const uint16_t OTA_DEFAULT_BLOCK_SIZE = 1024;
static const uint8_t OTA_DEFAULT_HOP_LIMIT = 3;
} // namespace ota
} // namespace mesh
+186
View File
@@ -0,0 +1,186 @@
#include "OtaSelf.h"
#include "FirmwareInfo.h"
#include "OtaByteIO.h"
#include <string.h>
#if defined(ESP32_PLATFORM)
#include "esp_ota_ops.h"
#include "esp_partition.h"
#elif defined(NRF52_PLATFORM)
#include "OtaFlashLayout_nrf52.h"
#endif
#if defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)
#include "OtaContext.h" // serve our own fw from flash (cache leaves, read payload on demand)
#include "MerkleTree.h"
#include <SHA256.h>
#include <stdlib.h>
#ifndef OTA_SELF_LEAVES_MAX
#define OTA_SELF_LEAVES_MAX 65536u // cap heap for cached leaves (~16k blocks @1 KB = up to ~16 MB image)
#endif
#endif
namespace mesh {
namespace ota {
#if defined(ESP32_PLATFORM)
// Scan the running app partition for the firmware's EndF trailer using esp_partition_read (stable
// across IDF versions — no mmap). Same rule as find_self_firmware(): the marker's absolute offset
// must equal its stored body_len, which uniquely identifies the running firmware's own trailer.
bool ota_self_firmware(SelfFwInfo& out) {
out = SelfFwInfo();
const esp_partition_t* p = esp_ota_get_running_partition();
if (!p) return false;
const uint32_t CH = 512;
uint8_t buf[CH + ENDF_LEN]; // overlap so a marker spanning a chunk edge is still seen
for (uint32_t base = 0; base + ENDF_LEN <= p->size; base += CH) {
uint32_t want = CH + ENDF_LEN;
if (base + want > p->size) want = p->size - base;
if (esp_partition_read(p, base, buf, want) != ESP_OK) return false;
for (uint32_t i = 0; i + ENDF_LEN <= want; i++) {
if (buf[i] != ENDF_MAGIC[0]) continue;
if (memcmp(buf + i, ENDF_MAGIC, 4) != 0) continue;
uint32_t body_len = rd_u32le(buf + i + 4);
if (body_len != base + i) continue; // must sit immediately after a body of that length
out.valid = true;
out.endf_offset = body_len;
out.body_len = body_len;
out.image_len = body_len + ENDF_LEN;
memcpy(out.body_hash, buf + i + 8, 8);
// Fixed 56-byte trailer: re-read it whole at the marker (it may straddle the chunk window, so the
// identity fields aren't reliably in `buf`) and pull identity from constant offsets (docs §2).
uint8_t tr[ENDF_LEN];
if (body_len + ENDF_LEN <= p->size &&
esp_partition_read(p, body_len, tr, ENDF_LEN) == ESP_OK) {
out.fw_version = (uint32_t)tr[16] | ((uint32_t)tr[17]<<8) | ((uint32_t)tr[18]<<16) | ((uint32_t)tr[19]<<24);
out.target_id = (uint32_t)tr[20] | ((uint32_t)tr[21]<<8) | ((uint32_t)tr[22]<<16) | ((uint32_t)tr[23]<<24);
memcpy(out.hw_id, tr + 24, 32); out.hw_id[32] = 0;
}
return true;
}
}
return false;
}
#elif defined(NRF52_PLATFORM)
// nRF52 internal flash is memory-mapped, so the running app is directly scannable. The body starts at
// APP_BASE; find_self_firmware() picks the EndF whose stored body_len equals its offset (the running
// firmware's own trailer), ignoring any staged `.mota` (which carries its own embedded EndF) higher up.
bool ota_self_firmware(SelfFwInfo& out) {
const uint8_t* region = (const uint8_t*)(uintptr_t)MOTA_NRF52_APP_BASE;
uint32_t region_len = MOTA_NRF52_FS_START - MOTA_NRF52_APP_BASE;
return find_self_firmware(region, region_len, out, /*verify_body=*/true);
}
#else
bool ota_self_firmware(SelfFwInfo& out) {
// STM32/RP2040: app-region access lands with their apply path.
out = SelfFwInfo();
return false;
}
#endif
#if defined(ESP32_PLATFORM)
bool ota_self_read(uint32_t off, uint8_t* buf, uint32_t len) {
const esp_partition_t* p = esp_ota_get_running_partition();
return p && esp_partition_read(p, off, buf, len) == ESP_OK;
}
#elif defined(NRF52_PLATFORM)
bool ota_self_read(uint32_t off, uint8_t* buf, uint32_t len) {
if ((uint64_t)MOTA_NRF52_APP_BASE + off + len > MOTA_NRF52_FS_START) return false;
memcpy(buf, (const uint8_t*)(uintptr_t)(MOTA_NRF52_APP_BASE + off), len);
return true;
}
#else
bool ota_self_read(uint32_t, uint8_t*, uint32_t) { return false; }
#endif
#if defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)
static bool self_read_cb(void* ctx, uint32_t off, uint8_t* buf, uint32_t len) {
(void)ctx; return ota_self_read(off, buf, len);
}
// Build (once) the full-image manifest + merkle leaves for the running firmware, cache them in `c`, and
// hand the manager a flash-read callback for the payload. The image is read ONCE here to compute the
// leaves + image_hash; thereafter a block REQ reads only that block (proof comes from the cached leaves).
// Pack the first "MAJOR.MINOR.PATCH" found in `s` into the comparable uint32 the manifest uses
// (MAJOR<<24 | MINOR<<16 | PATCH<<8). Returns 0 if there's no dotted number (e.g. a "dev-<sha>" build).
static uint32_t parse_fw_version(const char* s) {
if (!s) return 0;
for (; *s; s++) { // find the start of a "d.d" run
if (*s < '0' || *s > '9') continue;
const char* p = s; uint32_t a = 0, b = 0, d = 0; int dots = 0;
uint32_t* cur = &a;
for (; *p; p++) {
if (*p >= '0' && *p <= '9') { *cur = *cur * 10 + (uint32_t)(*p - '0'); }
else if (*p == '.' && dots < 2) { dots++; cur = (dots == 1) ? &b : &d; }
else break;
}
if (dots >= 1) return FwVersion{ (uint8_t)a, (uint8_t)b, (uint8_t)d, 0 }.pack();
s = p - 1; // a bare number, no dots — keep scanning
}
return 0;
}
bool ota_serve_self(OtaContext& c, uint32_t fw_version) {
// Derive our version from the build string when the caller didn't supply one, so the mOTA we advertise
// carries a real version (was hard-coded 0 -> peers saw "v0.0.0"). A dev build with no dotted number
// still reads 0 — the self-describing EndF identity (docs) is the durable fix for that.
#ifdef FIRMWARE_VERSION
if (fw_version == 0) fw_version = parse_fw_version(FIRMWARE_VERSION);
#endif
SelfFwInfo fi;
if (!ota_self_firmware(fi) || !fi.valid) return false;
// 1 KB logical blocks (delivered as multiple LoRa fragments): 8x fewer merkle leaves than 128 B, so a
// ~530 KB image is ~518 blocks (proof-gen scratch ~2 KB) instead of ~4150 (which overflowed the scratch).
const uint32_t image_size = fi.image_len, BS = OTA_DEFAULT_BLOCK_SIZE;
const uint32_t bc = (image_size + BS - 1) / BS;
if ((uint64_t)bc * 4 > OTA_SELF_LEAVES_MAX) return false;
free(c.serve_self_leaves); free(c.serve_self_proof);
c.serve_self_leaves = (uint8_t*)malloc((size_t)bc * 4);
c.serve_self_proof = (uint8_t*)malloc((size_t)bc * 4); // proof-gen working buffer (sized to OUR image)
if (!c.serve_self_leaves || !c.serve_self_proof) {
free(c.serve_self_leaves); free(c.serve_self_proof);
c.serve_self_leaves = c.serve_self_proof = nullptr;
return false;
}
SHA256 sha; uint8_t blk[BS];
for (uint32_t i = 0, off = 0; i < bc; i++, off += BS) {
uint32_t blen = (off + BS <= image_size) ? BS : (image_size - off);
if (!ota_self_read(off, blk, blen)) {
free(c.serve_self_leaves); free(c.serve_self_proof);
c.serve_self_leaves = c.serve_self_proof = nullptr;
return false;
}
merkle_leaf(c.serve_self_leaves + (size_t)i * 4, blk, blen);
sha.update(blk, blen);
}
uint8_t image_hash[32]; sha.finalize(image_hash, 32);
uint8_t root[4]; merkle_root(root, c.serve_self_leaves, bc);
// Prefer the SELF-DESCRIBING identity embedded in our own EndF (docs §2) over build flags / the param —
// it's correct regardless of how the firmware was built (build.sh injection, IDE, etc.).
uint32_t out_target = fi.target_id ? fi.target_id : c.manager.target();
uint32_t out_ver = fi.fw_version ? fi.fw_version : fw_version;
const char* out_hw = fi.hw_id[0] ? fi.hw_id : c.hw_id;
// Assemble the fixed-layout manifest-minus-leaves (full, unsigned) = MOTA_MFL bytes. base_hash(89),
// signer_pubkey(97) and signature(129) stay zero-filled (full + unsigned); only `approval` is set.
uint8_t* m = c.serve_self_manifest;
memset(m, 0, MOTA_MFL);
m[0] = MOTA_FORMAT_VER; m[1] = MFLAG_FULL; m[2] = HASH_ALGO_SHA256;
wr_u32le(m + 3, out_target); wr_u32le(m + 7, out_ver);
wr_u32le(m + 11, image_size); wr_u32le(m + 15, image_size); // full: payload == image
m[19] = 10; // block_size_log2 = 10 (1024 B logical block)
memcpy(m + 20, root, 4);
memcpy(m + 24, image_hash, 32);
m[56] = CODEC_FULL;
memcpy(m + 57, out_hw, strlen(out_hw) < 32 ? strlen(out_hw) : 32); // hw_id[32] (NUL-padded by memset)
memcpy(m + MOTA_OFF_APPROVAL, APPROVAL_NOT, 4); // approval (fetching device's apply-gate handles it)
return c.manager.serve_self(m, MOTA_MFL, c.serve_self_leaves, bc,
c.serve_self_proof, (size_t)bc * 4, self_read_cb, nullptr);
}
#endif
} // namespace ota
} // namespace mesh
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include "FirmwareInfo.h"
// Device-side accessor for the running firmware's own image (to read its EndF trailer).
// Per-platform: ESP32 memory-maps the running app partition; other platforms TBD (nRF52 uses the
// bootloader-apply path, so its app-region wiring lands with that work). Not compiled on the native
// host — the portable scan logic in FirmwareInfo.{h,cpp} is what gets unit-tested there.
namespace mesh {
namespace ota {
// Locate this firmware's EndF trailer in its own flash image. Returns false if unsupported on this
// platform or no valid EndF is present (e.g. firmware built without the EndF build hook).
bool ota_self_firmware(SelfFwInfo& out);
// Read `len` bytes of the running firmware image at offset `off` (ESP32: running partition via
// esp_partition_read; nRF52: memory-mapped app region). false on unsupported platforms.
bool ota_self_read(uint32_t off, uint8_t* buf, uint32_t len);
// Compute (once) + cache our running firmware's manifest + merkle leaves in `c`, then serve it from
// flash as a full `.mota` (payload read on demand per block; only metadata held in RAM). Returns false
// if no EndF / image too big / OOM. Device platforms only.
struct OtaContext;
bool ota_serve_self(OtaContext& c, uint32_t fw_version); // target = this node's own (c.manager.target())
} // namespace ota
} // namespace mesh
+339
View File
@@ -0,0 +1,339 @@
#pragma once
#include <stdint.h>
// AUTO-GENERATED by tools/mota/gen_targets.py — do not edit by hand.
// 319 OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env name, LE uint32)
// to the human-readable env name, so a node/tool can name a target seen over the air WITHOUT
// transmitting the string in the .mota / LoRa protocol. Regenerate when the OTA env set changes.
namespace mesh { namespace ota {
// target_id -> env name, or nullptr if unknown. Linear scan (table is small; lookups are rare).
inline const char* ota_target_env_name(uint32_t target_id) {
static const struct { uint32_t id; const char* env; } T[] = {
{ 0x149bcb23, "Ebyte_EoRa-S3_companion_radio_ble" },
{ 0x30d4e442, "Ebyte_EoRa-S3_companion_radio_usb" },
{ 0x263a8d80, "Ebyte_EoRa-S3_kiss_modem" },
{ 0x9070ae2c, "Ebyte_EoRa-S3_Repeater" },
{ 0xf190d834, "Ebyte_EoRa-S3_room_server" },
{ 0x32890982, "Ebyte_EoRa-S3_terminal_chat" },
{ 0xacb599d5, "GAT562_30S_Mesh_Kit_companion_radio_ble" },
{ 0x82d3b273, "GAT562_30S_Mesh_Kit_companion_radio_usb" },
{ 0xe96a6af8, "GAT562_30S_Mesh_Kit_kiss_modem" },
{ 0x70d8478d, "GAT562_30S_Mesh_Kit_repeater" },
{ 0xf9986af5, "GAT562_30S_Mesh_Kit_room_server" },
{ 0x98519a1d, "GAT562_Mesh_EVB_Pro_kiss_modem" },
{ 0x06b71719, "GAT562_Mesh_EVB_Pro_repeater" },
{ 0x4c0461c8, "GAT562_Mesh_EVB_Pro_room_server" },
{ 0x4fa88992, "GAT562_Mesh_Tracker_Pro_companion_radio_ble" },
{ 0xcd6604d0, "GAT562_Mesh_Tracker_Pro_companion_radio_usb" },
{ 0xc54ea258, "GAT562_Mesh_Tracker_Pro_kiss_modem" },
{ 0x0b3985a8, "GAT562_Mesh_Tracker_Pro_repeater" },
{ 0x03d9704a, "GAT562_Mesh_Tracker_Pro_room_server" },
{ 0x8456f753, "GAT562_Mesh_Watch13_companion_radio_ble" },
{ 0xc8104197, "GAT562_Mesh_Watch13_kiss_modem" },
{ 0xf5eb61ec, "Generic_E22_kiss_modem" },
{ 0x70d2d590, "Generic_E22_sx1262_repeater" },
{ 0xe1a7ffdf, "Generic_E22_sx1262_repeater_bridge_espnow" },
{ 0xd5e04824, "Generic_E22_sx1268_repeater" },
{ 0x61a7b2dd, "Generic_E22_sx1268_repeater_bridge_espnow" },
{ 0xa416bb20, "Generic_ESPNOW_comp_radio_usb" },
{ 0x875f3744, "Generic_ESPNOW_repeatr" },
{ 0xdaf349be, "Generic_ESPNOW_room_svr" },
{ 0x93b93199, "Generic_ESPNOW_terminal_chat" },
{ 0x5293da13, "Heltec_ct62_companion_radio_ble" },
{ 0xf2eec98c, "Heltec_ct62_companion_radio_usb" },
{ 0x48a986f1, "Heltec_ct62_kiss_modem" },
{ 0xd2cf978c, "Heltec_ct62_repeater" },
{ 0x53626334, "Heltec_ct62_repeater_bridge_espnow" },
{ 0xefe4f19f, "Heltec_ct62_sensor" },
{ 0xfcaf3730, "Heltec_E213_companion_radio_ble" },
{ 0xd90deef7, "Heltec_E213_companion_radio_usb" },
{ 0x2d2a4089, "Heltec_E213_kiss_modem" },
{ 0x89ea5c4e, "Heltec_E213_repeater" },
{ 0xc0eb712a, "Heltec_E213_repeater_bridge_espnow" },
{ 0x67293f43, "Heltec_E213_room_server" },
{ 0x3f364910, "Heltec_E290_companion_ble" },
{ 0xac4b4eb5, "Heltec_E290_companion_usb" },
{ 0xe077fb8d, "Heltec_E290_kiss_modem" },
{ 0x2bbe7fd8, "Heltec_E290_repeater" },
{ 0x9e40157c, "Heltec_E290_repeater_bridge_espnow" },
{ 0xb12e6e01, "Heltec_E290_room_server" },
{ 0x32bcdd14, "Heltec_t114_companion_radio_ble" },
{ 0xfe937f14, "Heltec_t114_companion_radio_usb" },
{ 0x5b9b1933, "Heltec_t114_kiss_modem" },
{ 0x191f3545, "Heltec_t114_repeater" },
{ 0x1b4399de, "Heltec_t114_repeater_bridge_rs232" },
{ 0xb1153422, "Heltec_t114_room_server" },
{ 0x075ca3f9, "Heltec_t114_without_display_companion_radio_ble" },
{ 0x2f9f8e7d, "Heltec_t114_without_display_companion_radio_usb" },
{ 0xc9dc5d8c, "Heltec_t114_without_display_repeater" },
{ 0x34b0e5f3, "Heltec_t114_without_display_repeater_bridge_rs232" },
{ 0xcfa0e233, "Heltec_t114_without_display_room_server" },
{ 0x17d5e1ff, "Heltec_T190_companion_radio_ble_" },
{ 0xc99313c6, "Heltec_T190_companion_radio_usb_" },
{ 0x21a73973, "Heltec_T190_kiss_modem" },
{ 0x3067f656, "Heltec_T190_repeater_" },
{ 0xfdb25057, "Heltec_T190_repeater_bridge_espnow_" },
{ 0x0f1767cd, "Heltec_T190_room_server_" },
{ 0x73387372, "heltec_tracker_v2_companion_radio_ble" },
{ 0xe6eeffe1, "heltec_tracker_v2_companion_radio_usb" },
{ 0x35f4e7ac, "heltec_tracker_v2_companion_radio_wifi" },
{ 0xb62a1e75, "heltec_tracker_v2_kiss_modem" },
{ 0x9e64845d, "heltec_tracker_v2_repeater" },
{ 0x19232e8b, "heltec_tracker_v2_repeater_bridge_espnow" },
{ 0x38b948b4, "heltec_tracker_v2_room_server" },
{ 0xed21ca4f, "heltec_tracker_v2_sensor" },
{ 0x9f5f39a7, "heltec_tracker_v2_terminal_chat" },
{ 0x081d2219, "Heltec_v2_companion_radio_ble" },
{ 0xc283ad60, "Heltec_v2_companion_radio_usb" },
{ 0x4fbdc1e1, "Heltec_v2_companion_radio_wifi" },
{ 0xc797fd73, "Heltec_v2_kiss_modem" },
{ 0xab9871c8, "Heltec_v2_repeater" },
{ 0x5caaf9bf, "Heltec_v2_repeater_bridge_espnow" },
{ 0x2075687c, "Heltec_v2_room_server" },
{ 0x5d726067, "Heltec_v2_terminal_chat" },
{ 0x9969bbc9, "Heltec_v3_companion_radio_ble" },
{ 0x22f900de, "Heltec_v3_companion_radio_usb" },
{ 0xaca0e153, "Heltec_v3_companion_radio_wifi" },
{ 0xcb1c2e65, "Heltec_v3_kiss_modem" },
{ 0x9f2a6b84, "Heltec_v3_ota_test" },
{ 0xd1b29b18, "Heltec_v3_repeater" },
{ 0x644bb68d, "Heltec_v3_repeater_bridge_espnow" },
{ 0xd19a9759, "Heltec_v3_repeater_bridge_rs232" },
{ 0xc59d294e, "Heltec_v3_room_server" },
{ 0x21519537, "Heltec_v3_sensor" },
{ 0x382bb181, "Heltec_v3_terminal_chat" },
{ 0x01d8f124, "heltec_v4_companion_radio_ble" },
{ 0x1a4d0096, "heltec_v4_companion_radio_usb" },
{ 0x34240e36, "heltec_v4_companion_radio_wifi" },
{ 0xd522a14f, "heltec_v4_expansionkit_repeater" },
{ 0xf75feb3e, "heltec_v4_kiss_modem" },
{ 0xe792a051, "heltec_v4_repeater" },
{ 0x2d5ea842, "heltec_v4_repeater_bridge_espnow" },
{ 0xeed78ce4, "heltec_v4_room_server" },
{ 0x0b6847ed, "heltec_v4_sensor" },
{ 0xab3e1ad3, "heltec_v4_terminal_chat" },
{ 0x60fca2ae, "heltec_v4_tft_companion_radio_ble" },
{ 0xf6b40343, "heltec_v4_tft_companion_radio_usb" },
{ 0x76640bcc, "heltec_v4_tft_companion_radio_wifi" },
{ 0x87513d56, "heltec_v4_tft_repeater" },
{ 0xca812b41, "heltec_v4_tft_repeater_bridge_espnow" },
{ 0x18594231, "heltec_v4_tft_room_server" },
{ 0x188f3ac1, "heltec_v4_tft_sensor" },
{ 0x402055ec, "heltec_v4_tft_terminal_chat" },
{ 0x12393f1f, "Heltec_Wireless_Paper_companion_radio_ble" },
{ 0xed3ef5b8, "Heltec_Wireless_Paper_companion_radio_usb" },
{ 0x8ea86381, "Heltec_Wireless_Paper_kiss_modem" },
{ 0x09f57289, "Heltec_Wireless_Paper_repeater" },
{ 0x148aeaab, "Heltec_Wireless_Paper_repeater_bridge_espnow" },
{ 0x6e9f7fec, "Heltec_Wireless_Paper_room_server" },
{ 0x6a25d024, "Heltec_Wireless_Tracker_companion_radio_ble" },
{ 0xc4a11710, "Heltec_Wireless_Tracker_companion_radio_usb" },
{ 0x753947a4, "Heltec_Wireless_Tracker_kiss_modem" },
{ 0xe6fa98b4, "Heltec_Wireless_Tracker_repeater" },
{ 0xf8c27cf4, "Heltec_Wireless_Tracker_repeater_bridge_espnow" },
{ 0x90e227d9, "Heltec_Wireless_Tracker_room_server" },
{ 0x812f5e52, "Heltec_WSL3_companion_radio_ble" },
{ 0x1ddd15c8, "Heltec_WSL3_companion_radio_usb" },
{ 0x49d67d0d, "Heltec_WSL3_companion_radio_wifi" },
{ 0xbc003322, "Heltec_WSL3_repeater" },
{ 0x835b70c0, "Heltec_WSL3_repeater_bridge_espnow" },
{ 0xd7460575, "Heltec_WSL3_repeater_bridge_rs232" },
{ 0x33e2c171, "Heltec_WSL3_room_server" },
{ 0xa165ae99, "Heltec_WSL3_sensor" },
{ 0x967ccaee, "LilyGo_T3S3_sx1262_companion_radio_ble" },
{ 0x5f7c7688, "LilyGo_T3S3_sx1262_companion_radio_usb" },
{ 0xbd04e6e7, "LilyGo_T3S3_sx1262_kiss_modem" },
{ 0x1aeac884, "LilyGo_T3S3_sx1262_repeater" },
{ 0x07849209, "LilyGo_T3S3_sx1262_repeater_bridge_espnow" },
{ 0xbfb60e7c, "LilyGo_T3S3_sx1262_room_server" },
{ 0x96036a68, "LilyGo_T3S3_sx1262_terminal_chat" },
{ 0xb76a745c, "LilyGo_T3S3_sx1276_companion_radio_ble" },
{ 0x43023193, "LilyGo_T3S3_sx1276_companion_radio_usb" },
{ 0xa7d97db7, "LilyGo_T3S3_sx1276_kiss_modem" },
{ 0x468a133e, "LilyGo_T3S3_sx1276_repeater" },
{ 0xb59ea2ed, "LilyGo_T3S3_sx1276_repeater_bridge_espnow" },
{ 0xbe372c6d, "LilyGo_T3S3_sx1276_room_server" },
{ 0x6215e9eb, "LilyGo_T3S3_sx1276_terminal_chat" },
{ 0x3898ea56, "LilyGo_TBeam_1W_companion_radio_ble" },
{ 0x4f722e70, "LilyGo_TBeam_1W_companion_radio_usb" },
{ 0x694eed25, "LilyGo_TBeam_1W_companion_radio_wifi" },
{ 0x85b8b9d7, "LilyGo_TBeam_1W_kiss_modem" },
{ 0xe8421e36, "LilyGo_TBeam_1W_repeater" },
{ 0x82eaaf9e, "LilyGo_TBeam_1W_repeater_bridge_espnow" },
{ 0xe4a2de74, "LilyGo_TBeam_1W_room_server" },
{ 0xaeb637f1, "LilyGo_TDeck_companion_radio_ble" },
{ 0x86813271, "LilyGo_TDeck_companion_radio_usb" },
{ 0x67b2ff84, "LilyGo_TDeck_kiss_modem" },
{ 0xd0e2e616, "LilyGo_TDeck_repeater" },
{ 0x3f9edfe3, "LilyGo_TETH_Elite_sx1262_companion_radio_ble" },
{ 0xc23f8082, "LilyGo_TETH_Elite_sx1262_companion_radio_usb" },
{ 0x8f256e56, "LilyGo_TETH_Elite_sx1262_repeater" },
{ 0xf1407f39, "LilyGo_TETH_Elite_sx1262_room_server" },
{ 0xe90a3181, "LilyGo_Tlora_C6_companion_radio_ble_" },
{ 0x3b1b4237, "LilyGo_Tlora_C6_kiss_modem" },
{ 0x21c6d08d, "LilyGo_Tlora_C6_repeater_" },
{ 0x34302c14, "LilyGo_Tlora_C6_room_server_" },
{ 0x268d6fc1, "LilyGo_TLora_V2_1_1_6_companion_radio_ble" },
{ 0x3c172d27, "LilyGo_TLora_V2_1_1_6_companion_radio_usb" },
{ 0xf7c5d584, "LilyGo_TLora_V2_1_1_6_companion_radio_wifi" },
{ 0x81960cc0, "LilyGo_TLora_V2_1_1_6_kiss_modem" },
{ 0x504020ea, "LilyGo_TLora_V2_1_1_6_repeater" },
{ 0x8e5c0c88, "LilyGo_TLora_V2_1_1_6_repeater_bridge_espnow" },
{ 0x4e42e0ad, "LilyGo_TLora_V2_1_1_6_repeater_bridge_rs232" },
{ 0xa2ce002f, "LilyGo_TLora_V2_1_1_6_room_server" },
{ 0x6f16479e, "LilyGo_TLora_V2_1_1_6_terminal_chat" },
{ 0x1018e5d1, "M5Stack_Unit_C6L_companion_radio_ble" },
{ 0xc0ab5040, "M5Stack_Unit_C6L_companion_radio_usb" },
{ 0xa2bf90bd, "M5Stack_Unit_C6L_kiss_modem" },
{ 0xe6bff8f9, "M5Stack_Unit_C6L_repeater" },
{ 0xb9cd26cb, "M5Stack_Unit_C6L_room_server" },
{ 0xdbfe2675, "Meshadventurer_sx1262_companion_radio_ble" },
{ 0xb00241d0, "Meshadventurer_sx1262_companion_radio_usb" },
{ 0x88e77fee, "Meshadventurer_sx1262_kiss_modem" },
{ 0xae2d9caa, "Meshadventurer_sx1262_repeater" },
{ 0xecea445b, "Meshadventurer_sx1262_repeater_bridge_espnow" },
{ 0xa0043e00, "Meshadventurer_sx1262_room_server" },
{ 0x26bbf4ed, "Meshadventurer_sx1262_terminal_chat" },
{ 0xdcc0f158, "Meshadventurer_sx1268_companion_radio_ble" },
{ 0xe3320463, "Meshadventurer_sx1268_companion_radio_usb" },
{ 0x852b928a, "Meshadventurer_sx1268_kiss_modem" },
{ 0x0ec870e9, "Meshadventurer_sx1268_repeater" },
{ 0xc845072e, "Meshadventurer_sx1268_repeater_bridge_espnow" },
{ 0x8f00c1f2, "Meshadventurer_sx1268_room_server" },
{ 0xcea95e20, "Meshadventurer_sx1268_terminal_chat" },
{ 0xd45277ff, "Meshimi_companion_radio_ble_" },
{ 0x2cad9a9d, "Meshimi_repeater_" },
{ 0xf1c1641c, "nibble_screen_connect_companion_radio_ble" },
{ 0x8dcc5d89, "nibble_screen_connect_companion_radio_usb" },
{ 0x6139a846, "nibble_screen_connect_companion_radio_wifi" },
{ 0xc0dbcae4, "nibble_screen_connect_kiss_modem" },
{ 0xcd0bdd06, "nibble_screen_connect_repeater" },
{ 0x9ecb53b1, "nibble_screen_connect_repeater_bridge_espnow" },
{ 0x82f6d321, "nibble_screen_connect_room_server" },
{ 0xb10261c8, "nibble_screen_connect_terminal_chat" },
{ 0xdba69401, "R1Neo_companion_radio_ble" },
{ 0xcb6555ee, "R1Neo_companion_radio_usb" },
{ 0xd60daf25, "R1Neo_kiss_modem" },
{ 0x410bd800, "R1Neo_repeater" },
{ 0xd8641710, "R1Neo_room_server" },
{ 0x4d0b1601, "R1Neo_sensor" },
{ 0xab2ac09e, "R1Neo_terminal_chat" },
{ 0xa17fc4f7, "RAK_3112_companion_radio_ble" },
{ 0x90c4eb3f, "RAK_3112_companion_radio_usb" },
{ 0x19d9ce91, "RAK_3112_companion_radio_wifi" },
{ 0x121cc72f, "RAK_3112_kiss_modem" },
{ 0xfa8510fa, "RAK_3112_repeater" },
{ 0xd34d8461, "RAK_3112_repeater_bridge_espnow" },
{ 0x98077550, "RAK_3112_repeater_bridge_rs232" },
{ 0x2ed959aa, "RAK_3112_room_server" },
{ 0xced7e127, "RAK_3112_sensor" },
{ 0x6925aaf0, "RAK_3112_terminal_chat" },
{ 0x9f4d58ac, "RAK_4631_companion_radio_ble" },
{ 0x48d3b5d4, "RAK_4631_companion_radio_usb" },
{ 0x14e6965d, "RAK_4631_kiss_modem" },
{ 0x04d413fd, "RAK_4631_repeater" },
{ 0x22ea8497, "RAK_4631_repeater_bridge_rs232_serial1" },
{ 0x21bae522, "RAK_4631_repeater_bridge_rs232_serial2" },
{ 0x626d80ed, "RAK_4631_room_server" },
{ 0x8b1d6850, "RAK_4631_sensor" },
{ 0xf1d3c5a8, "RAK_4631_terminal_chat" },
{ 0xc6d55752, "RAK_WisMesh_Tag_companion_radio_ble" },
{ 0x60683191, "RAK_WisMesh_Tag_companion_radio_usb" },
{ 0x8a7cb79b, "RAK_WisMesh_Tag_kiss_modem" },
{ 0x4fa4ae55, "RAK_WisMesh_Tag_repeater" },
{ 0x9cb0b232, "RAK_WisMesh_Tag_room_server" },
{ 0x51fa1a49, "RAK_WisMesh_Tag_sensor" },
{ 0xd904c2d5, "SenseCapIndicator-ESPNow_comp_radio_usb" },
{ 0xd0df8303, "Station_G2_companion_radio_ble" },
{ 0x5c1a54f8, "Station_G2_companion_radio_usb" },
{ 0x79cc029e, "Station_G2_companion_radio_wifi" },
{ 0x41197b92, "Station_G2_kiss_modem" },
{ 0x05747eb6, "Station_G2_logging_repeater" },
{ 0x75587f24, "Station_G2_logging_repeater_bridge_espnow" },
{ 0xf2128c81, "Station_G2_repeater" },
{ 0x1b34e7b4, "Station_G2_repeater_bridge_espnow" },
{ 0x73c4a019, "Station_G2_room_server" },
{ 0x8e549407, "Station_G3_ESP32_companion_radio_ble" },
{ 0x1deac038, "Station_G3_ESP32_companion_radio_usb" },
{ 0x2a2f303b, "Station_G3_ESP32_companion_radio_wifi" },
{ 0x0ba4453d, "Station_G3_ESP32_kiss_modem" },
{ 0xf8d1958f, "Station_G3_ESP32_logging_repeater" },
{ 0x3c49caf8, "Station_G3_ESP32_repeater" },
{ 0x91878dd8, "Station_G3_ESP32_room_server" },
{ 0x16482630, "T_Beam_S3_Supreme_SX1262_companion_radio_ble" },
{ 0x01c43d49, "T_Beam_S3_Supreme_SX1262_companion_radio_wifi" },
{ 0x51fe3801, "T_Beam_S3_Supreme_SX1262_kiss_modem" },
{ 0x857149e5, "T_Beam_S3_Supreme_SX1262_repeater" },
{ 0xd9d64938, "T_Beam_S3_Supreme_SX1262_repeater_bridge_espnow" },
{ 0x9952fbe3, "T_Beam_S3_Supreme_SX1262_room_server" },
{ 0xdc27c37c, "Tbeam_SX1262_companion_radio_ble" },
{ 0xb27514a3, "Tbeam_SX1262_kiss_modem" },
{ 0xe9ce038c, "Tbeam_SX1262_repeater" },
{ 0x06406dd4, "Tbeam_SX1262_repeater_bridge_espnow" },
{ 0x189660b3, "Tbeam_SX1262_room_server" },
{ 0x2082078d, "Tbeam_SX1276_companion_radio_ble" },
{ 0x3750e80d, "Tbeam_SX1276_kiss_modem" },
{ 0xe4b49973, "Tbeam_SX1276_repeater" },
{ 0x0c0c4605, "Tbeam_SX1276_repeater_bridge_espnow" },
{ 0x5a79bac1, "Tbeam_SX1276_room_server" },
{ 0x8eaa289e, "Tenstar_C3_sx1262_kiss_modem" },
{ 0x70e91bd9, "Tenstar_C3_sx1262_repeater" },
{ 0x5a7e9c2d, "Tenstar_C3_sx1262_repeater_bridge_espnow" },
{ 0x1211b151, "Tenstar_C3_sx1268_kiss_modem" },
{ 0x0a32044b, "Tenstar_C3_sx1268_repeater" },
{ 0xa13fe853, "Tenstar_C3_sx1268_repeater_bridge_espnow" },
{ 0xd7598668, "ThinkNode_M2_companion_radio_ble" },
{ 0x413287d8, "ThinkNode_M2_companion_radio_serial" },
{ 0xe8cd2377, "ThinkNode_M2_companion_radio_usb" },
{ 0x3309bdaf, "ThinkNode_M2_companion_radio_wifi" },
{ 0x7330afbb, "ThinkNode_M2_kiss_modem" },
{ 0x9df48ce6, "ThinkNode_M2_Repeater" },
{ 0xa5cb783a, "ThinkNode_M2_Repeater_bridge_espnow" },
{ 0x4b9f8283, "ThinkNode_M2_room_server" },
{ 0x311846c9, "ThinkNode_M2_terminal_chat" },
{ 0xf2d61a1c, "ThinkNode_M5_companion_radio_ble" },
{ 0x1da30414, "ThinkNode_M5_companion_radio_serial" },
{ 0xae8fe1c9, "ThinkNode_M5_companion_radio_usb" },
{ 0x67acd73f, "ThinkNode_M5_companion_radio_wifi" },
{ 0xfb118ed8, "ThinkNode_M5_kiss_modem" },
{ 0xe4213729, "ThinkNode_M5_Repeater" },
{ 0x1298038d, "ThinkNode_M5_Repeater_bridge_espnow" },
{ 0x2c47acff, "ThinkNode_M5_room_server" },
{ 0x7d641646, "ThinkNode_M5_terminal_chat" },
{ 0x37443a5b, "WHY2025_badge_companion_radio_ble_" },
{ 0x603384a7, "WHY2025_badge_repeater_" },
{ 0x67843a5f, "Xiao_C3_companion_radio_ble" },
{ 0xfad357ab, "Xiao_C3_companion_radio_usb" },
{ 0x16af5c67, "Xiao_C3_companion_radio_wifi" },
{ 0x8c105104, "Xiao_C3_kiss_modem" },
{ 0xa80042a0, "Xiao_C3_repeater" },
{ 0x4bff0748, "Xiao_C3_room_server" },
{ 0x77c8f2b5, "Xiao_C6_companion_radio_ble_" },
{ 0x71ee0e15, "Xiao_C6_kiss_modem" },
{ 0x8824cd0c, "Xiao_C6_repeater_" },
{ 0x6658b418, "Xiao_S3_companion_radio_ble" },
{ 0x964f4f5c, "Xiao_S3_companion_radio_usb" },
{ 0x0a5dc760, "Xiao_S3_kiss_modem" },
{ 0xd08496ce, "Xiao_S3_repeater" },
{ 0x293ac2c8, "Xiao_S3_repeater_bridge_espnow" },
{ 0x60ebfa87, "Xiao_S3_room_server" },
{ 0x60f5fdc2, "Xiao_S3_sensor" },
{ 0xe8f79d22, "Xiao_S3_WIO_companion_radio_ble" },
{ 0x9f27be15, "Xiao_S3_WIO_companion_radio_serial" },
{ 0x396fecc7, "Xiao_S3_WIO_companion_radio_usb" },
{ 0xf94e3407, "Xiao_S3_WIO_companion_radio_wifi" },
{ 0xe1930103, "Xiao_S3_WIO_kiss_modem" },
{ 0x9f19bcd1, "Xiao_S3_WIO_repeater" },
{ 0xfcebff61, "Xiao_S3_WIO_repeater_bridge_espnow" },
{ 0xccb734e2, "Xiao_S3_WIO_room_server" },
{ 0xf967a300, "Xiao_S3_WIO_sensor" },
{ 0xa0cc1c36, "Xiao_S3_WIO_terminal_chat" },
};
for (unsigned i = 0; i < sizeof(T) / sizeof(T[0]); i++)
if (T[i].id == target_id) return T[i].env;
return nullptr;
}
} } // namespace mesh::ota
+27
View File
@@ -0,0 +1,27 @@
#include "OtaVerify.h"
#include "MerkleTree.h"
#include "Multihash.h"
#include "Identity.h"
namespace mesh {
namespace ota {
VerifyResult ota_verify(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow) {
VerifyResult r;
MotaManifest m;
if (!mota_parse(buf, len, m)) return r;
r.parsed = true;
r.root_ok = mota_check_root(m);
r.image_ok = m.is_full() ? mota_check_image_hash_full(m)
: true; // delta image_hash needs the base; verified at apply time
r.is_signed = m.is_signed();
if (r.is_signed) {
mesh::Identity signer(m.signer_pubkey);
r.sig_ok = signer.verify(m.signature, m.manifest_start, (int)m.signed_len);
r.trusted = r.sig_ok && allow.contains(m.signer_pubkey);
}
return r;
}
} // namespace ota
} // namespace mesh
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include "MotaContainer.h"
#include "SignerAllowlist.h"
// Full verification of a staged `.mota` (device-side: uses Ed25519 via mesh::Identity, so NOT compiled
// on the native host — the portable integrity checks live in MotaContainer and are unit-tested there).
namespace mesh {
namespace ota {
struct VerifyResult {
bool parsed = false; // container + manifest parsed
bool root_ok = false; // merkle_root recomputed from leaves[] matches
bool image_ok = false; // full: sha2-256(payload)==image_hash; delta: deferred to apply (set true)
bool is_signed = false;
bool sig_ok = false; // Ed25519 signature valid for signer_pubkey
bool trusted = false; // signer_pubkey is in the allowlist
// Integrity holds (safe to keep/serve). For a signed image, the signature must also verify.
bool integrity_ok() const { return parsed && root_ok && image_ok && (!is_signed || sig_ok); }
// Eligible for AUTO-apply: integrity + signed by an allowlisted key (decision D2).
bool auto_appliable() const { return integrity_ok() && is_signed && sig_ok && trusted; }
};
VerifyResult ota_verify(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow);
} // namespace ota
} // namespace mesh
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <string.h>
// Runtime-managed allowlist of trusted Ed25519 firmware-signer public keys (docs/ota_protocol.md §9,
// decision D2 + Q1: no key embedded in firmware; only allowlist-signed firmware may auto-apply).
// Portable + fixed-capacity (no dynamic allocation). Persistence (load/save) is layered on per-platform.
namespace mesh {
namespace ota {
#ifndef MAX_OTA_SIGNERS
#define MAX_OTA_SIGNERS 4
#endif
class SignerAllowlist {
uint8_t _keys[MAX_OTA_SIGNERS][32];
uint8_t _count = 0;
public:
void clear() { _count = 0; }
uint8_t count() const { return _count; }
const uint8_t* get(uint8_t i) const { return (i < _count) ? _keys[i] : nullptr; }
bool contains(const uint8_t* pub) const {
for (uint8_t i = 0; i < _count; i++)
if (memcmp(_keys[i], pub, 32) == 0) return true;
return false;
}
// Add a key (idempotent). Returns false if the list is full.
bool add(const uint8_t* pub) {
if (contains(pub)) return true;
if (_count >= MAX_OTA_SIGNERS) return false;
memcpy(_keys[_count++], pub, 32);
return true;
}
bool remove(const uint8_t* pub) {
for (uint8_t i = 0; i < _count; i++) {
if (memcmp(_keys[i], pub, 32) == 0) {
memmove(_keys[i], _keys[i + 1], (size_t)(_count - i - 1) * 32);
_count--;
return true;
}
}
return false;
}
// Serialize as: count(1) || key0(32) || key1(32) ... Returns bytes written.
uint32_t serialize(uint8_t* out, uint32_t max_len) const {
uint32_t need = 1 + (uint32_t)_count * 32;
if (max_len < need) return 0;
out[0] = _count;
memcpy(out + 1, _keys, (size_t)_count * 32);
return need;
}
bool deserialize(const uint8_t* in, uint32_t len) {
if (len < 1) return false;
uint8_t n = in[0];
if (n > MAX_OTA_SIGNERS || (uint32_t)1 + n * 32 > len) return false;
_count = n;
memcpy(_keys, in + 1, (size_t)n * 32);
return true;
}
};
} // namespace ota
} // namespace mesh