diff --git a/src/helpers/ota/BlockBitmap.h b/src/helpers/ota/BlockBitmap.h new file mode 100644 index 00000000..c22835f5 --- /dev/null +++ b/src/helpers/ota/BlockBitmap.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include + +// 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 diff --git a/src/helpers/ota/FirmwareInfo.cpp b/src/helpers/ota/FirmwareInfo.cpp new file mode 100644 index 00000000..c58cbfcc --- /dev/null +++ b/src/helpers/ota/FirmwareInfo.cpp @@ -0,0 +1,42 @@ +#include "FirmwareInfo.h" +#include "Multihash.h" +#include "OtaByteIO.h" +#include + +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 diff --git a/src/helpers/ota/FirmwareInfo.h b/src/helpers/ota/FirmwareInfo.h new file mode 100644 index 00000000..a08bdc75 --- /dev/null +++ b/src/helpers/ota/FirmwareInfo.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#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 diff --git a/src/helpers/ota/MerkleTree.cpp b/src/helpers/ota/MerkleTree.cpp new file mode 100644 index 00000000..8aa1099a --- /dev/null +++ b/src/helpers/ota/MerkleTree.cpp @@ -0,0 +1,110 @@ +#include "MerkleTree.h" +#include "Multihash.h" +#include + +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 diff --git a/src/helpers/ota/MerkleTree.h b/src/helpers/ota/MerkleTree.h new file mode 100644 index 00000000..15987f5b --- /dev/null +++ b/src/helpers/ota/MerkleTree.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +// 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 diff --git a/src/helpers/ota/MotaContainer.cpp b/src/helpers/ota/MotaContainer.cpp new file mode 100644 index 00000000..282d4175 --- /dev/null +++ b/src/helpers/ota/MotaContainer.cpp @@ -0,0 +1,83 @@ +#include "MotaContainer.h" +#include "MerkleTree.h" +#include "Multihash.h" +#include "OtaByteIO.h" +#include + +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 diff --git a/src/helpers/ota/MotaContainer.h b/src/helpers/ota/MotaContainer.h new file mode 100644 index 00000000..4425e62b --- /dev/null +++ b/src/helpers/ota/MotaContainer.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#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 diff --git a/src/helpers/ota/Multihash.h b/src/helpers/ota/Multihash.h new file mode 100644 index 00000000..82164113 --- /dev/null +++ b/src/helpers/ota/Multihash.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#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 diff --git a/src/helpers/ota/OtaByteIO.h b/src/helpers/ota/OtaByteIO.h new file mode 100644 index 00000000..d4834cd8 --- /dev/null +++ b/src/helpers/ota/OtaByteIO.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +// 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 diff --git a/src/helpers/ota/OtaFormat.h b/src/helpers/ota/OtaFormat.h new file mode 100644 index 00000000..6a8da2e2 --- /dev/null +++ b/src/helpers/ota/OtaFormat.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include + +// 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 diff --git a/src/helpers/ota/OtaSelf.cpp b/src/helpers/ota/OtaSelf.cpp new file mode 100644 index 00000000..f1f1902d --- /dev/null +++ b/src/helpers/ota/OtaSelf.cpp @@ -0,0 +1,186 @@ +#include "OtaSelf.h" +#include "FirmwareInfo.h" +#include "OtaByteIO.h" +#include + +#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 + #include + #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-" 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 diff --git a/src/helpers/ota/OtaSelf.h b/src/helpers/ota/OtaSelf.h new file mode 100644 index 00000000..df855c6f --- /dev/null +++ b/src/helpers/ota/OtaSelf.h @@ -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 diff --git a/src/helpers/ota/OtaTargets.h b/src/helpers/ota/OtaTargets.h new file mode 100644 index 00000000..e7ec61ce --- /dev/null +++ b/src/helpers/ota/OtaTargets.h @@ -0,0 +1,339 @@ +#pragma once +#include + +// 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 diff --git a/src/helpers/ota/OtaVerify.cpp b/src/helpers/ota/OtaVerify.cpp new file mode 100644 index 00000000..42b6d53f --- /dev/null +++ b/src/helpers/ota/OtaVerify.cpp @@ -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 diff --git a/src/helpers/ota/OtaVerify.h b/src/helpers/ota/OtaVerify.h new file mode 100644 index 00000000..85fb9683 --- /dev/null +++ b/src/helpers/ota/OtaVerify.h @@ -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 diff --git a/src/helpers/ota/SignerAllowlist.h b/src/helpers/ota/SignerAllowlist.h new file mode 100644 index 00000000..726644fd --- /dev/null +++ b/src/helpers/ota/SignerAllowlist.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include + +// 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