Files
HaloKeymind/src/helpers/ota/OtaApply.cpp
T

1113 lines
54 KiB
C++

#include "OtaApply.h"
#include "OtaFormat.h"
#include "MotaContainer.h"
#include "Identity.h"
#include "OtaByteIO.h"
#include <string.h>
#if defined(ESP32_PLATFORM)
#include <SHA256.h> // rweather streaming SHA-256 (for hashing the slot in chunks)
#include "esp_ota_ops.h"
#include "esp_partition.h"
#include "esp_system.h"
extern "C" {
#include "detools/detools.h" // vendored detools 0.53.0 embeddable decoder (CRLE-only build)
}
#if defined(OTA_FLASH_STORE)
#include "OtaStoreFlashEsp32.h" // flash-staged container (delta patch / full image in the slot)
#include "OtaSelf.h" // SelfFwInfo / ota_self_firmware (running-image base_hash gate)
#endif
#elif defined(NRF52_PLATFORM)
#include "OtaVerify.h"
#include "OtaSelf.h"
#include "OtaFlashLayout_nrf52.h"
#include "OtaBlInfo.h" // read the bootloader capability marker before arming an apply
#include "flash/flash_nrf5x.h" // Adafruit core internal-flash driver (has its own extern "C")
#include "nrf.h"
#include "nrf_soc.h"
#include "nrf_sdm.h"
#if defined(OTA_SD_STORE)
#include "OtaStoreSdNrf52.h"
#endif
#if defined(OTA_QSPI_STORE)
#include "OtaStoreQspiNrf52.h"
#endif
#if defined(OTA_FLASH_STORE)
#include "OtaStoreFlashNrf52.h"
#endif
#endif
namespace mesh {
namespace ota {
#if defined(ESP32_PLATFORM)
bool ota_apply_slot_info(uint32_t* addr, uint32_t* size) {
const esp_partition_t* p = esp_ota_get_next_update_partition(nullptr);
if (!p) return false;
if (addr) *addr = p->address;
if (size) *size = p->size;
return true;
}
bool ota_apply_set_manifest(const uint8_t* mf, uint32_t len, const SignerAllowlist& allow, ApplyState& st) {
st = ApplyState();
ota_apply_slot_info(&st.slot_addr, &st.slot_size);
MotaManifest m;
if (!mota_parse_manifest(mf, len, m)) return false;
if (m.is_bootloader() || !m.is_full()) return false; // boot packages never enter an application slot
st.image_size = m.image_size;
memcpy(st.image_hash, m.image_hash, 32);
st.manifest_ok = true;
if (m.is_signed()) {
mesh::Identity signer(m.signer_pubkey);
st.sig_ok = signer.verify(m.signature, m.manifest_start, (int)m.signed_len);
st.trusted = st.sig_ok && allow.contains(m.signer_pubkey);
}
return true;
}
bool ota_apply_verify_slot(ApplyState& st) {
st.slot_ok = false;
if (!st.manifest_ok || st.image_size == 0 || st.image_size > st.slot_size) return false;
const esp_partition_t* p = esp_ota_get_next_update_partition(nullptr);
if (!p) return false;
SHA256 sha;
uint8_t buf[512];
uint32_t off = 0;
while (off < st.image_size) {
uint32_t n = st.image_size - off; if (n > sizeof(buf)) n = sizeof(buf);
if (esp_partition_read(p, off, buf, n) != ESP_OK) return false;
sha.update(buf, n);
off += n;
}
uint8_t h[32];
sha.finalize(h, 32);
st.slot_ok = (memcmp(h, st.image_hash, 32) == 0);
return st.slot_ok;
}
bool ota_apply_commit() {
const esp_partition_t* p = esp_ota_get_next_update_partition(nullptr);
if (!p) return false;
if (esp_ota_set_boot_partition(p) != ESP_OK) return false;
esp_restart(); // does not return
return true;
}
// --- detools callback context -----------------------------------------------------------------
// The delta base is the running OTA slot; the reconstructed image is streamed into the inactive slot
// via esp_ota_write (sequential, append-only -- matches detools' sequential output ordering) and
// hashed on the fly so we can check it against the signed manifest image_hash before arming.
struct DetoolsCtx {
const esp_partition_t* base; // delta base (running image), read at absolute `from_pos`
long from_pos; // absolute byte offset into `base`
#if defined(OTA_FLASH_STORE)
OtaStoreFlashEsp32* store; // staged container; patch = payload region [patch_base, +patch_len)
uint32_t patch_base; // container offset where the payload (patch) begins
#else
const uint8_t* patch; // .mota payload held wholly in RAM (RAM store; bring-up/host)
#endif
uint32_t patch_len;
uint32_t patch_pos;
esp_ota_handle_t out; // inactive slot write handle
SHA256* sha; // running hash of the reconstructed output
uint32_t out_pos; // #bytes written to the output slot
bool io_ok;
};
static int dt_from_read(void* arg, uint8_t* buf, size_t size) {
DetoolsCtx* c = (DetoolsCtx*)arg;
if (c->from_pos < 0 || (uint32_t)(c->from_pos) + size > c->base->size) return -DETOOLS_IO_FAILED;
if (esp_partition_read(c->base, (size_t)c->from_pos, buf, size) != ESP_OK) { c->io_ok = false; return -DETOOLS_IO_FAILED; }
c->from_pos += (long)size;
return DETOOLS_OK;
}
static int dt_from_seek(void* arg, int offset) { // detools uses relative seeks
DetoolsCtx* c = (DetoolsCtx*)arg;
c->from_pos += offset;
if (c->from_pos < 0 || (uint32_t)c->from_pos > c->base->size) return -DETOOLS_IO_FAILED;
return DETOOLS_OK;
}
static int dt_patch_read(void* arg, uint8_t* buf, size_t size) {
DetoolsCtx* c = (DetoolsCtx*)arg;
if (c->patch_pos + size > c->patch_len) return -DETOOLS_IO_FAILED;
#if defined(OTA_FLASH_STORE)
if (!c->store->read(c->patch_base + c->patch_pos, buf, size)) { c->io_ok = false; return -DETOOLS_IO_FAILED; }
#else
memcpy(buf, c->patch + c->patch_pos, size);
#endif
c->patch_pos += (uint32_t)size;
return DETOOLS_OK;
}
static int dt_to_write(void* arg, const uint8_t* buf, size_t size) {
DetoolsCtx* c = (DetoolsCtx*)arg;
if (esp_ota_write(c->out, buf, size) != ESP_OK) { c->io_ok = false; return -DETOOLS_IO_FAILED; }
c->sha->update(buf, size);
c->out_pos += (uint32_t)size;
return DETOOLS_OK;
}
#if defined(OTA_FLASH_STORE)
// --- in-place delta on ESP32 (codec 2) ----------------------------------------------------------
// A single in-place `.mota` can target BOTH nRF52 (bootloader applies it) and ESP32. On ESP32 the
// inactive slot is used as the in-place working memory: we copy the running image (the base) into the
// slot's bottom-staged-container-FREE region [0, write_start), then run detools' in-place decoder over
// that region (it reads the base, erases segments, writes the target back), reading the patch from the
// staged container's payload (which lives at/below write_start, disjoint from the working region). The
// decoded image is hashed against the signed image_hash BEFORE arming, so a bad decode never boots; the
// callbacks are bounded to [0, write_start) so they fail gracefully instead of touching the patch.
// (Sequential is still preferred on ESP32 - it streams straight to the slot with no base-copy; in-place
// exists only for single-artifact distribution. Requires the patch built with --inplace-segment 4096.)
struct InPlaceCtx {
const esp_partition_t* slot; // in-place working memory = slot[0, mem_max)
uint32_t mem_max; // = container write_start; accesses beyond this are refused
OtaStoreFlashEsp32* store; // staged container; patch = payload region [patch_base, +patch_len)
uint32_t patch_base, patch_len, patch_pos;
int step; // detools resume cursor (RAM; no cross-reboot resume of the apply)
bool io_ok;
const char* fail; // first failure point (diagnostic), nullptr until set
uint32_t fa, fn; int frc; // failing addr / len / esp_err
};
static inline int ip_fail(InPlaceCtx* c, const char* w, uint32_t a, size_t n, int rc) {
if (!c->fail) { c->fail = w; c->fa = a; c->fn = (uint32_t)n; c->frc = rc; }
c->io_ok = false; return -DETOOLS_IO_FAILED;
}
static int ip_mem_read(void* a, void* dst, uintptr_t src, size_t n) {
InPlaceCtx* c = (InPlaceCtx*)a;
if ((uint32_t)src + n > c->mem_max) return ip_fail(c, "rd>max", (uint32_t)src, n, 0);
int rc = esp_partition_read(c->slot, (size_t)src, dst, n);
if (rc != ESP_OK) return ip_fail(c, "rd", (uint32_t)src, n, rc);
return DETOOLS_OK;
}
static int ip_mem_write(void* a, uintptr_t dst, void* src, size_t n) {
InPlaceCtx* c = (InPlaceCtx*)a;
if ((uint32_t)dst + n > c->mem_max) return ip_fail(c, "wr>max", (uint32_t)dst, n, 0);
int rc = esp_partition_write(c->slot, (size_t)dst, src, n);
if (rc != ESP_OK) return ip_fail(c, "wr", (uint32_t)dst, n, rc);
return DETOOLS_OK;
}
static int ip_mem_erase(void* a, uintptr_t addr, size_t n) {
InPlaceCtx* c = (InPlaceCtx*)a;
// esp_partition_erase_range requires a SECTOR-aligned size; detools' final in-place segment is partial
// (the image tail past the last full sector). addr is sector-aligned (== --inplace-segment), so round
// the length UP to a full sector. The over-erased bytes are scratch beyond image_size (never hashed),
// and - since detools processes high->low and erases-before-writing - they are never live patch data.
const uint32_t SEC = 4096;
if ((uint32_t)addr % SEC != 0) return ip_fail(c, "er!align", (uint32_t)addr, n, 0);
uint32_t len = ((uint32_t)n + SEC - 1) & ~(SEC - 1);
if ((uint32_t)addr + len > c->mem_max) return ip_fail(c, "er>max", (uint32_t)addr, len, 0);
int rc = esp_partition_erase_range(c->slot, (size_t)addr, len);
if (rc != ESP_OK) return ip_fail(c, "er", (uint32_t)addr, len, rc);
return DETOOLS_OK;
}
static int ip_step_set(void* a, int s) { ((InPlaceCtx*)a)->step = s; return DETOOLS_OK; }
static int ip_step_get(void* a, int* s) { *s = ((InPlaceCtx*)a)->step; return DETOOLS_OK; }
static int ip_patch_read(void* a, uint8_t* b, size_t n) {
InPlaceCtx* c = (InPlaceCtx*)a;
if (c->patch_pos + n > c->patch_len) return ip_fail(c, "patch>len", c->patch_pos, n, 0);
if (!c->store->read(c->patch_base + c->patch_pos, b, n)) return ip_fail(c, "patch_rd", c->patch_pos, n, 0);
c->patch_pos += (uint32_t)n;
return DETOOLS_OK;
}
static bool esp32_inplace_apply(OtaStoreFlashEsp32& store, const MotaManifest& m, ApplyState& st, char* msg) {
const esp_partition_t* slot = store.partition();
const esp_partition_t* base = esp_ota_get_running_partition();
if (!slot || !base) { strcpy(msg, "no slot/base partition"); return false; }
SelfFwInfo fi;
if (!ota_self_firmware(fi) || !fi.valid) { strcpy(msg, "cannot read running firmware (no EndF)"); return false; }
if (!m.base_hash || memcmp(m.base_hash, fi.body_hash, 8) != 0) { strcpy(msg, "not built for the running firmware (base mismatch)"); return false; }
uint32_t mem_max = store.write_start(); // working region [0, mem_max); the patch sits at/above it
if (mem_max == 0) { strcpy(msg, "in-place needs a bottom-staged container"); return false; }
if (fi.image_len > mem_max || m.image_size > mem_max) { strcpy(msg, "in-place region too small for base/image"); return false; }
// load the base (running image) into the working region [0, base_len), sector by sector (erase + copy)
uint8_t buf[512];
for (uint32_t off = 0; off < fi.image_len; ) {
uint32_t sec = off & ~(4096u - 1);
if (esp_partition_erase_range(slot, sec, 4096) != ESP_OK) { strcpy(msg, "base erase failed"); return false; }
uint32_t secend = sec + 4096; if (secend > fi.image_len) secend = fi.image_len;
for (uint32_t p = (off > sec ? off : sec); p < secend; ) {
uint32_t n = secend - p; if (n > sizeof(buf)) n = sizeof(buf);
if (esp_partition_read(base, p, buf, n) != ESP_OK || esp_partition_write(slot, p, buf, n) != ESP_OK) {
strcpy(msg, "base copy failed"); return false; }
p += n;
}
off = secend;
}
// patch in place over the working region; patch streamed from the staged container payload
InPlaceCtx c;
c.slot = slot; c.mem_max = mem_max; c.store = &store;
c.patch_base = store.meta_bytes(); c.patch_len = m.payload_size; c.patch_pos = 0; c.step = 0; c.io_ok = true;
c.fail = nullptr; c.fa = c.fn = 0; c.frc = 0;
int r = detools_apply_patch_in_place_callbacks(ip_mem_read, ip_mem_write, ip_mem_erase,
ip_step_set, ip_step_get, ip_patch_read,
(size_t)m.payload_size, &c);
if (r < 0 || !c.io_ok) {
if (c.fail) sprintf(msg, "in-place decode err %d @%s a=%u n=%u rc=%d max=%u", r, c.fail,
(unsigned)c.fa, (unsigned)c.fn, c.frc, (unsigned)mem_max);
else sprintf(msg, "in-place decode err %d", r);
return false;
}
if ((uint32_t)r != m.image_size) { sprintf(msg, "in-place size %u!=%u", (unsigned)r, (unsigned)m.image_size); return false; }
// verify the decoded slot image against the signed image_hash BEFORE arming (mismatch -> never boots)
SHA256 sha;
for (uint32_t off = 0; off < m.image_size; ) {
uint32_t n = m.image_size - off; if (n > sizeof(buf)) n = sizeof(buf);
if (esp_partition_read(slot, off, buf, n) != ESP_OK) { strcpy(msg, "slot read failed"); return false; }
sha.update(buf, n); off += n;
}
uint8_t hh[32]; sha.finalize(hh, 32);
st.slot_ok = (memcmp(hh, m.image_hash, 32) == 0);
if (!st.slot_ok) { strcpy(msg, "image_hash MISMATCH after in-place decode"); return false; }
if (esp_ota_set_boot_partition(slot) != ESP_OK) { strcpy(msg, "set_boot failed"); return false; }
sprintf(msg, "verified%s; in-place decoded %u B, image hash OK - armed, rebooting to apply",
m.is_signed() ? " (signer trusted)" : " (unsigned)", (unsigned)m.image_size);
return true;
}
// Apply the `.mota` staged in the inactive slot by OtaStoreFlashEsp32 (no contiguous RAM copy).
// FULL: the payload was streamed straight to slot offset 0 during the fetch -> hash the slot image
// and compare to the signed image_hash, then arm. No decode, no copy.
// DELTA (sequential): base = the running slot; the patch is read from the staged payload region (the
// slot's bottom); the reconstructed image is written to the inactive slot via esp_ota_write and
// hashed vs image_hash. esp_ota_begin only erases [0, image_size], which the fetch-time fit
// check kept below the bottom-staged container, so the patch survives while we decode over it.
// DELTA (in-place): copy the running image into the slot's working region then patch in place
// (esp32_inplace_apply); image_hash-gated before arming. Lets one in-place .mota target both
// ESP32 and nRF52. Sequential is still preferred on ESP32 (no base-copy).
// The result is verified (signature/trust up front, image_hash after) and the slot armed; the caller
// reboots once the confirmation reply has gone out.
bool ota_apply_detools_mota(OtaStoreFlashEsp32& store, const SignerAllowlist& allow, ApplyState& st, char* msg) {
st = ApplyState();
const esp_partition_t* slot = store.partition();
if (!slot || store.staged_size() < 16) { strcpy(msg, "no staged update"); return false; }
st.slot_addr = slot->address; st.slot_size = slot->size;
// read + parse the manifest out of the staged container (header = MAGIC(4) + total(4))
uint8_t hdr[8];
if (!store.read(0, hdr, 8) || memcmp(hdr, MOTA_MAGIC, 4) != 0) { strcpy(msg, "bad container"); return false; }
uint8_t mfbuf[256];
uint32_t mflen = store.meta_bytes() > 8 ? store.meta_bytes() - 8 : 0; // manifest+leaves; cap to mfbuf
if (mflen > sizeof(mfbuf)) mflen = sizeof(mfbuf);
MotaManifest m;
if (mflen < 57 || !store.read(8, mfbuf, mflen) || !mota_parse_manifest(mfbuf, mflen, m)) {
strcpy(msg, "manifest parse failed"); return false; }
if (m.is_bootloader()) { strcpy(msg, "bootloader package requires explicit bootloader install"); return false; }
st.image_size = m.image_size; memcpy(st.image_hash, m.image_hash, 32); st.manifest_ok = true;
if (m.image_size == 0 || m.image_size > slot->size) { strcpy(msg, "image > slot"); return false; }
// signature / trust BEFORE arming an untrusted image (image_hash below is the target-firmware gate)
if (m.is_signed()) {
mesh::Identity signer(m.signer_pubkey);
st.sig_ok = signer.verify(m.signature, m.manifest_start, (int)m.signed_len);
st.trusted = st.sig_ok && allow.contains(m.signer_pubkey);
if (!st.sig_ok) { strcpy(msg, "bad signature"); return false; }
if (!st.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; }
}
// ---- FULL: payload already in slot[0]; verify hash + arm ----
if (m.is_full()) {
SHA256 sha; uint8_t buf[512];
for (uint32_t off = 0; off < m.image_size; ) {
uint32_t n = m.image_size - off; if (n > sizeof(buf)) n = sizeof(buf);
if (esp_partition_read(slot, off, buf, n) != ESP_OK) { strcpy(msg, "slot read failed"); return false; }
sha.update(buf, n); off += n;
}
uint8_t hh[32]; sha.finalize(hh, 32);
st.slot_ok = (memcmp(hh, m.image_hash, 32) == 0);
if (!st.slot_ok) { strcpy(msg, "image_hash MISMATCH (slot)"); return false; }
if (esp_ota_set_boot_partition(slot) != ESP_OK) { strcpy(msg, "set_boot failed"); return false; }
sprintf(msg, "verified%s full image %u B in slot - armed, rebooting to apply",
m.is_signed() ? " (trusted)" : " (unsigned)", (unsigned)m.image_size);
return true;
}
// ---- DELTA ----
if (m.codec_id == CODEC_DETOOLS_INPLACE) return esp32_inplace_apply(store, m, st, msg); // single-artifact codec
if (m.codec_id != CODEC_DETOOLS_SEQUENTIAL) { strcpy(msg, "unknown delta codec"); return false; }
// delta must be built for the running firmware (cheap early gate; image_hash is the definitive check)
if (m.base_hash) {
SelfFwInfo fi;
if (!ota_self_firmware(fi) || !fi.valid) { strcpy(msg, "cannot read running firmware (no EndF)"); return false; }
if (memcmp(m.base_hash, fi.body_hash, 8) != 0) { strcpy(msg, "delta not built for the running firmware (base mismatch)"); return false; }
}
const esp_partition_t* base = esp_ota_get_running_partition();
if (!base) { strcpy(msg, "no running partition"); return false; }
esp_ota_handle_t h;
if (esp_ota_begin(slot, m.image_size, &h) != ESP_OK) { strcpy(msg, "ota_begin failed"); return false; }
SHA256 sha;
DetoolsCtx ctx;
ctx.base = base; ctx.from_pos = 0;
ctx.store = &store; ctx.patch_base = store.meta_bytes(); ctx.patch_len = m.payload_size; ctx.patch_pos = 0;
ctx.out = h; ctx.sha = &sha; ctx.out_pos = 0; ctx.io_ok = true;
int r = detools_apply_patch_callbacks(dt_from_read, dt_from_seek, dt_patch_read,
(size_t)m.payload_size, dt_to_write, &ctx);
if (r < 0 || !ctx.io_ok) { esp_ota_abort(h); sprintf(msg, "detools err %d @%u/%u",
ctx.io_ok ? r : -DETOOLS_IO_FAILED, (unsigned)ctx.out_pos, (unsigned)m.image_size); return false; }
if ((uint32_t)r != m.image_size || ctx.out_pos != m.image_size) {
esp_ota_abort(h); sprintf(msg, "size mismatch %u!=%u", (unsigned)ctx.out_pos, (unsigned)m.image_size); return false; }
uint8_t hh[32]; sha.finalize(hh, 32);
st.slot_ok = (memcmp(hh, m.image_hash, 32) == 0);
if (!st.slot_ok) { esp_ota_abort(h); strcpy(msg, "image_hash MISMATCH after decode"); return false; }
if (esp_ota_end(h) != ESP_OK) { strcpy(msg, "ota_end failed"); return false; }
if (esp_ota_set_boot_partition(slot) != ESP_OK) { strcpy(msg, "set_boot failed"); return false; }
sprintf(msg, "verified%s; decoded %u B, image hash OK - armed, rebooting to apply",
m.is_signed() ? " (signer trusted)" : " (unsigned)", (unsigned)m.image_size);
return true;
}
#else // !OTA_FLASH_STORE: RAM-staged apply (whole .mota in a contiguous RAM buffer; bring-up/host)
bool ota_apply_detools_mota(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow,
ApplyState& st, char* msg) {
st = ApplyState();
MotaManifest m;
if (!mota_parse(buf, len, m)) { strcpy(msg, "no valid .mota (parse failed)"); return false; }
if (m.is_bootloader()) { strcpy(msg, "bootloader package cannot enter an application slot"); return false; }
if (m.is_full() || m.codec_id != CODEC_DETOOLS_SEQUENTIAL) { strcpy(msg, "not a detools-sequential delta"); return false; }
st.image_size = m.image_size;
memcpy(st.image_hash, m.image_hash, 32);
st.manifest_ok = true;
if (m.is_signed()) {
mesh::Identity signer(m.signer_pubkey);
st.sig_ok = signer.verify(m.signature, m.manifest_start, (int)m.signed_len);
st.trusted = st.sig_ok && allow.contains(m.signer_pubkey);
if (!st.sig_ok) { strcpy(msg, "bad signature"); return false; }
if (!st.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; }
}
const esp_partition_t* base = esp_ota_get_running_partition();
const esp_partition_t* out = esp_ota_get_next_update_partition(nullptr);
if (!base || !out) { strcpy(msg, "no A/B slot"); return false; }
st.slot_addr = out->address; st.slot_size = out->size;
if (m.image_size > out->size) { strcpy(msg, "image > slot"); return false; }
esp_ota_handle_t h;
if (esp_ota_begin(out, m.image_size, &h) != ESP_OK) { strcpy(msg, "ota_begin failed"); return false; }
SHA256 sha;
DetoolsCtx ctx;
ctx.base = base; ctx.from_pos = 0;
ctx.patch = m.payload; ctx.patch_len = m.payload_size; ctx.patch_pos = 0;
ctx.out = h; ctx.sha = &sha; ctx.out_pos = 0; ctx.io_ok = true;
int r = detools_apply_patch_callbacks(dt_from_read, dt_from_seek, dt_patch_read,
(size_t)m.payload_size, dt_to_write, &ctx);
if (r < 0 || !ctx.io_ok) { esp_ota_abort(h); sprintf(msg, "detools err %d @%u/%u",
ctx.io_ok ? r : -DETOOLS_IO_FAILED, (unsigned)ctx.out_pos, (unsigned)m.image_size); return false; }
if ((uint32_t)r != m.image_size || ctx.out_pos != m.image_size) {
esp_ota_abort(h); sprintf(msg, "size mismatch %u!=%u", (unsigned)ctx.out_pos, (unsigned)m.image_size); return false; }
uint8_t hh[32]; sha.finalize(hh, 32);
st.slot_ok = (memcmp(hh, m.image_hash, 32) == 0);
if (!st.slot_ok) { esp_ota_abort(h); strcpy(msg, "image_hash MISMATCH after decode"); return false; }
if (esp_ota_end(h) != ESP_OK) { strcpy(msg, "ota_end failed"); return false; }
if (esp_ota_set_boot_partition(out) != ESP_OK) { strcpy(msg, "set_boot failed"); return false; }
sprintf(msg, "verified%s; decoded %u B, image hash OK - armed, rebooting to apply",
m.is_signed() ? " (signer trusted)" : " (unsigned)", (unsigned)m.image_size);
return true;
}
#endif // OTA_FLASH_STORE
bool ota_apply_mota_nrf52(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) {
st = ApplyState(); strcpy(msg, "nRF52-only (ESP32 uses ota_apply_detools_mota)"); return false;
}
bool ota_rescue_mota_nrf52(const uint8_t*, uint32_t, const SignerAllowlist&, const uint8_t*,
uint32_t, ApplyState& st, char* msg) {
st = ApplyState(); strcpy(msg, "rescue is internal-flash nRF52-only"); return false;
}
void ota_reboot_to_apply() { esp_restart(); } // boots the slot armed by ota_apply_detools_mota; no return
void ota_reboot_to_bootloader_update() {}
bool ota_installed_bootloader_identity(OtaBootloaderIdentity& out) {
out = OtaBootloaderIdentity(); return false;
}
#elif defined(NRF52_PLATFORM) // single-slot: verify + mark APPROVED + hand off to the bootloader
// Capture a real OTAFIX result before board power-management initialization
// consumes GPREGRET2. Ignore shutdown reasons and staging handoff markers that
// share the register but are not bootloader apply results.
static uint8_t g_bootloader_last_rc = 0;
static void __attribute__((constructor(101))) ota_capture_bootloader_last_rc() {
g_bootloader_last_rc = ota_nrf52_boot_result_or_zero((uint8_t)NRF_POWER->GPREGRET2);
}
struct InplacePatchDims {
uint32_t memory = 0;
uint32_t segment = 0;
uint32_t shift = 0;
uint32_t from = 0;
uint32_t to = 0;
};
static bool parse_inplace_patch_dims(const uint8_t* payload, uint32_t payload_len,
InplacePatchDims& d) {
if (!payload || payload_len < 2 || ((payload[0] >> 4) & 0x07u) != 1u) return false;
ByteReader r(payload, payload_len);
r.u8(); // patch type/compression header
return r.detools_size(d.memory) && r.detools_size(d.segment) && r.detools_size(d.shift) &&
r.detools_size(d.from) && r.detools_size(d.to) && r.ok;
}
// ESP32 A/B-only entry points are unsupported on nRF52.
bool ota_apply_slot_info(uint32_t*, uint32_t*) { return false; }
bool ota_apply_set_manifest(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st) { st = ApplyState(); return false; }
bool ota_apply_verify_slot(ApplyState&) { return false; }
bool ota_apply_commit() { return false; }
bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) { st = ApplyState(); strcpy(msg, "use ota_apply_mota_nrf52"); return false; }
void ota_reboot_to_apply() { // public: set the apply magic + reset (does not return)
uint8_t stage_handoff = GPREGRET2_OTA_STAGE_LEGACY;
#if defined(OTA_SD_STORE)
stage_handoff = GPREGRET2_OTA_STAGE_SD;
#elif defined(OTA_QSPI_STORE)
stage_handoff = GPREGRET2_OTA_STAGE_QSPI;
#elif defined(OTA_FLASH_STORE)
stage_handoff = mota_nrf52_flash_stage_handoff(ota_nrf52_effective_stage_ceiling());
#endif
uint8_t sd_en = 0;
sd_softdevice_is_enabled(&sd_en);
if (sd_en) { // POWER is SD-restricted while the SoftDevice runs
sd_power_gpregret_clr(1, 0xFFFFFFFF);
sd_power_gpregret_set(1, stage_handoff);
sd_power_gpregret_clr(0, 0xFFFFFFFF);
sd_power_gpregret_set(0, GPREGRET_OTA_APPLY);
} else {
NRF_POWER->GPREGRET2 = stage_handoff;
NRF_POWER->GPREGRET = GPREGRET_OTA_APPLY;
}
NVIC_SystemReset(); // does not return
}
void ota_reboot_to_bootloader_update() {
uint8_t source = GPREGRET2_OTA_STAGE_QSPI;
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
source = GPREGRET2_OTA_STAGE_EXPANDED;
#elif defined(OTA_SD_BOOTLOADER_UPDATE)
source = GPREGRET2_OTA_STAGE_SD;
#endif
uint8_t sd_en = 0;
sd_softdevice_is_enabled(&sd_en);
if (sd_en) {
sd_power_gpregret_clr(1, 0xFFFFFFFF);
sd_power_gpregret_set(1, source);
sd_power_gpregret_clr(0, 0xFFFFFFFF);
sd_power_gpregret_set(0, GPREGRET_OTA_BOOTLOADER_UPDATE);
} else {
NRF_POWER->GPREGRET2 = source;
NRF_POWER->GPREGRET = GPREGRET_OTA_BOOTLOADER_UPDATE;
}
NVIC_SystemReset();
}
uint8_t ota_bootloader_last_rc() { return g_bootloader_last_rc; }
bool ota_installed_bootloader_identity(OtaBootloaderIdentity& out) {
return ota_bootloader_identity_from_image(
(const uint8_t*)(uintptr_t)OTA_BOOT_IMAGE_START, OTA_BOOT_IMAGE_SIZE, out);
}
static bool ota_apply_mota_nrf52_impl(const uint8_t* buf, uint32_t len,
const SignerAllowlist& allow,
const uint8_t* rescue_base_hash,
uint32_t local_target_id,
ApplyState& st, char* msg) {
st = ApplyState();
MotaManifest m;
if (!mota_parse(buf, len, m)) { strcpy(msg, "parse failed"); return false; }
if (m.is_bootloader()) { strcpy(msg, "use explicit ota bootloader install"); return false; }
if (m.is_full() || m.codec_id != CODEC_DETOOLS_INPLACE) { strcpy(msg, "not an in-place delta"); return false; }
st.image_size = m.image_size;
memcpy(st.image_hash, m.image_hash, 32);
st.manifest_ok = true;
// 0) THIS device's bootloader must be able to apply this .mota - otherwise staging + approving + rebooting
// just bounces back unchanged (a legacy/stock/older-OTAFIX bootloader). Refuse here, before any reboot.
{
OtaBlCaps bl = ota_bootloader_app_caps();
if (!bl.present) { strcpy(msg, "this bootloader has no OTA-apply support - update the bootloader first"); return false; }
if (bl.apply_abi < m.format_ver || !(bl.codec_mask & (1u << m.codec_id))) {
snprintf(msg, 159, "bootloader too old to apply this update (bl abi=%u codecs=0x%x; need fmt>=%u codec=%u) - update the bootloader",
bl.apply_abi, bl.codec_mask, m.format_ver, m.codec_id);
return false;
}
}
// Gated verification, in order, returning the FIRST failing reason (the bootloader re-checks integrity
// again before booting, so authenticity is gated here and re-validated there):
VerifyResult vr = ota_verify(buf, len, allow);
st.sig_ok = vr.sig_ok; st.trusted = vr.trusted;
// 1) downloaded payload: the fetched blocks must match the manifest's merkle root (intact + complete)
if (!vr.root_ok || !vr.payload_ok || !vr.image_ok) {
strcpy(msg, "payload hash mismatch (incomplete or corrupt .mota)"); return false;
}
// 2) target firmware. Normally the delta must match the validated EndF body hash. The explicit rescue
// path exists only for a firmware whose normal EndF validation is broken: it requires the operator's
// exact package base hash and the package's target_id. It then delegates the actual running-image
// base check to the bootloader, which performs that check before its first application write.
SelfFwInfo fi;
const bool self_valid = ota_self_firmware(fi) && fi.valid;
if (rescue_base_hash) {
switch (ota_nrf52_rescue_gate(self_valid, m.base_hash, rescue_base_hash,
m.target_id, local_target_id)) {
case NRF52_RESCUE_SELF_VALID:
strcpy(msg, "EndF OK; use ota install"); return false;
case NRF52_RESCUE_BASE_MISSING:
case NRF52_RESCUE_BASE_MISMATCH:
strcpy(msg, "rescue base mismatch"); return false;
case NRF52_RESCUE_TARGET_MISMATCH:
strcpy(msg, "rescue target mismatch"); return false;
case NRF52_RESCUE_OK:
break;
}
} else {
if (!self_valid) { strcpy(msg, "cannot read running firmware (no EndF)"); return false; }
if (!m.base_hash || memcmp(m.base_hash, fi.body_hash, 8) != 0) {
strcpy(msg, "not built for the running firmware (base mismatch)"); return false;
}
}
st.slot_ok = true;
// 3) signature (only if the .mota is signed): valid Ed25519 AND signer in this device's allowlist
if (vr.is_signed) {
if (!vr.sig_ok) { strcpy(msg, "bad signature"); return false; }
if (!vr.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; }
}
// 4) detools geometry. memory_size is selected by motatool for this exact staged address; reject a
// mismatched/legacy package before writing APRV so the bootloader never starts a doomed in-place apply.
{
InplacePatchDims d;
if (!parse_inplace_patch_dims(m.payload, m.payload_size, d)) {
strcpy(msg, "bad in-place patch header"); return false;
}
if (d.memory == 0 || d.segment != MOTA_NRF52_FLASH_PAGE || d.shift > d.memory ||
d.shift % d.segment != 0 || d.from > d.memory - d.shift || d.to > d.memory ||
d.to != m.image_size || (self_valid && d.from != fi.image_len)) {
strcpy(msg, "invalid in-place patch geometry"); return false;
}
#if defined(OTA_FLASH_STORE)
const uint32_t app_base = mota_nrf52_app_base();
const uint32_t app_ceiling = mota_nrf52_application_ceiling();
const uint32_t mota_start = (uint32_t)(uintptr_t)buf;
const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling();
if (!mota_nrf52_target_image_fits(app_base, m.image_size, app_ceiling)) {
strcpy(msg, "target image exceeds linked application region");
return false;
}
if (mota_start >= stage_ceiling ||
!mota_nrf52_internal_patch_workspace_valid(
d.memory, app_base, mota_start, m.image_size, app_ceiling)) {
snprintf(msg, 159, "patch memory 0x%x exceeds staging at 0x%x",
(unsigned)d.memory, (unsigned)mota_start);
return false;
}
#endif
}
// mark the staged manifest APPROVED in flash (buf is the memory-mapped staging region, so
// m.approval is a real flash address). NOR-clear over the erased 0xFFFFFFFF -> "APRV".
uint32_t approval_addr = (uint32_t)(uintptr_t)m.approval;
if (flash_nrf5x_write(approval_addr, APPROVAL_YES, 4) < 0) { strcpy(msg, "approval write failed"); return false; }
flash_nrf5x_flush();
if (memcmp((const void*)(uintptr_t)approval_addr, APPROVAL_YES, 4) != 0) { strcpy(msg, "approval not set"); return false; }
// Approved. Do NOT reset here - return so the caller can deliver `msg` to the operator first; the
// deferred ota_reboot_to_apply() (after the reply is sent) does the actual handoff to the bootloader.
if (rescue_base_hash) {
strcpy(msg, "rescue armed; bootloader checks base");
} else {
sprintf(msg, "verified%s; applying - rebooting into bootloader once this reply is sent",
vr.is_signed ? " (signer trusted)" : " (unsigned)");
}
return true;
}
bool ota_apply_mota_nrf52(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow,
ApplyState& st, char* msg) {
return ota_apply_mota_nrf52_impl(buf, len, allow, nullptr, 0, st, msg);
}
bool ota_rescue_mota_nrf52(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow,
const uint8_t operator_base_hash[8], uint32_t local_target_id,
ApplyState& st, char* msg) {
return ota_apply_mota_nrf52_impl(buf, len, allow, operator_base_hash, local_target_id, st, msg);
}
#if defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE)
static const size_t NRF52_APPLY_MSG_CAP = 96;
#if defined(OTA_SD_STORE)
static bool external_application_approve(OtaStoreSdNrf52& store,
const uint8_t container_hash[32]) {
return store.approve_for_bootloader(nullptr, container_hash);
}
#endif
#if defined(OTA_QSPI_STORE)
static bool external_application_approve(OtaStoreQspiNrf52& store,
const uint8_t[32]) {
return store.approve_for_bootloader();
}
#endif
template <typename Store>
static bool ota_apply_mota_nrf52_external(Store& store, const SignerAllowlist& allow,
uint8_t storage_flag, const char* storage_name,
bool require_trusted_signature,
ApplyState& st, char* msg) {
st = ApplyState();
uint8_t hdr[8], manifest[MOTA_MFL];
uint32_t total = store.staged_size();
if (total < 8 + MOTA_MFL + 5 || !store.read(0, hdr, sizeof(hdr)) ||
memcmp(hdr, MOTA_MAGIC, 4) != 0 || rd_u32le(hdr + 4) != total ||
!store.read(8, manifest, sizeof(manifest))) {
snprintf(msg, NRF52_APPLY_MSG_CAP, "%s container parse failed", storage_name);
return false;
}
MotaManifest m;
if (!mota_parse_manifest(manifest, sizeof(manifest), m)) {
snprintf(msg, NRF52_APPLY_MSG_CAP, "%s manifest parse failed", storage_name);
return false;
}
if (m.is_bootloader()) {
strcpy(msg, "use explicit ota bootloader install");
return false;
}
const bool full = m.is_full() && m.codec_id == CODEC_FULL &&
m.payload_size == m.image_size;
const bool delta = !m.is_full() && m.codec_id == CODEC_DETOOLS_INPLACE;
if (!full && !delta) {
snprintf(msg, NRF52_APPLY_MSG_CAP,
"nRF52 %s bootloader accepts full or in-place delta only", storage_name);
return false;
}
const uint32_t app_base = mota_nrf52_app_base();
const uint32_t app_ceiling = mota_nrf52_application_ceiling();
if (m.image_size == 0 || app_base >= app_ceiling ||
m.image_size > app_ceiling - app_base) {
strcpy(msg, "image exceeds nRF52 application region");
return false;
}
st.image_size = m.image_size;
memcpy(st.image_hash, m.image_hash, sizeof(st.image_hash));
st.manifest_ok = true;
OtaBlCaps bl = ota_bootloader_app_caps();
if (!bl.present || !(bl.storage_flags & storage_flag)) {
snprintf(msg, NRF52_APPLY_MSG_CAP,
"this bootloader has no %s OTA support - update the bootloader first", storage_name);
return false;
}
if (bl.apply_abi < m.format_ver || !(bl.codec_mask & (1u << m.codec_id))) {
snprintf(msg, NRF52_APPLY_MSG_CAP,
"bootloader cannot apply this %s update (abi=%u codecs=0x%x; need fmt=%u codec=%u)",
storage_name, bl.apply_abi, bl.codec_mask, m.format_ver, m.codec_id);
return false;
}
// Require the streamed manifest to remain byte-identical to the local copy
// parsed above. The verifier also computes the normalized full-container
// digest during that same integrity/authentication pass for removable SD.
VerifyResult vr = ota_verify(static_cast<const OtaStore&>(store), allow, manifest);
st.sig_ok = vr.sig_ok;
st.trusted = vr.trusted;
if (!vr.root_ok || !vr.payload_ok || !vr.image_ok || !vr.container_hash_ok) {
snprintf(msg, NRF52_APPLY_MSG_CAP,
"payload hash mismatch (incomplete or corrupt %s .mota)", storage_name);
return false;
}
if (require_trusted_signature && !vr.auto_appliable()) {
if (!vr.is_signed || !vr.sig_ok)
strcpy(msg, "SD application OTA requires a valid signed manifest");
else
strcpy(msg, "SD application OTA signer is not in the trusted allowlist");
return false;
}
SelfFwInfo fi;
if (delta) {
if (!ota_self_firmware(fi) || !fi.valid) {
strcpy(msg, "cannot read running firmware (no EndF)");
return false;
}
if (!m.base_hash || memcmp(m.base_hash, fi.body_hash, 8) != 0) {
strcpy(msg, "not built for the running firmware (base mismatch)");
return false;
}
}
st.slot_ok = true;
if (vr.is_signed) {
if (!vr.sig_ok) { strcpy(msg, "bad signature"); return false; }
if (!vr.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; }
}
if (delta) {
const uint64_t payload_off64 = 8u + MOTA_MFL + (uint64_t)m.block_count * 4u;
uint8_t patch_header[32]; // fixed byte + five detools varints (at most 26 bytes for uint32)
uint32_t header_len = m.payload_size < sizeof(patch_header) ? m.payload_size : sizeof(patch_header);
InplacePatchDims d;
if (payload_off64 > UINT32_MAX || payload_off64 + m.payload_size + 5u != total ||
!store.read((uint32_t)payload_off64, patch_header, header_len) ||
!parse_inplace_patch_dims(patch_header, header_len, d)) {
strcpy(msg, "bad in-place patch header");
return false;
}
if (!mota_nrf52_external_patch_geometry_valid(
d.memory, d.segment, d.shift, d.from, d.to,
app_ceiling - app_base, fi.image_len, m.image_size)) {
strcpy(msg, "invalid in-place patch geometry");
return false;
}
}
if (!external_application_approve(store, vr.container_hash)) {
snprintf(msg, NRF52_APPLY_MSG_CAP, "%s handoff failed: %s", storage_name,
store.last_error());
return false;
}
snprintf(msg, NRF52_APPLY_MSG_CAP,
"verified%s %s image on %s; rebooting into bootloader once this reply is sent",
vr.is_signed ? " (signer trusted)" : " (unsigned)", full ? "full" : "delta",
storage_name);
return true;
}
#endif
#if defined(OTA_SD_BOOTLOADER_UPDATE) || defined(OTA_QSPI_BOOTLOADER_UPDATE) || \
defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
static uint16_t ota_runtime_softdevice_fwid() {
// Nordic's S140 information structure lives at MBR(0x1000) + 0x2000;
// FWID is the little-endian uint16 at +0x0C.
return *(const volatile uint16_t*)(uintptr_t)0x0000300Cu;
}
static const char* ota_bootloader_continuity_error(OtaBootloaderContinuityGate gate) {
switch (gate) {
case OTA_BOOT_CONTINUITY_CANDIDATE_MISSING: return "candidate lacks boot continuity metadata";
case OTA_BOOT_CONTINUITY_OUTER_VERSION: return "candidate embedded/package version mismatch";
case OTA_BOOT_CONTINUITY_PLATFORM: return "candidate SoftDevice/application layout mismatch";
case OTA_BOOT_CONTINUITY_INSTALLED_PLATFORM: return "installed boot continuity metadata mismatch";
case OTA_BOOT_CONTINUITY_NOT_NEWER: return "candidate bootloader is not newer";
default: return "boot continuity mismatch";
}
}
#if defined(OTA_SD_BOOTLOADER_UPDATE)
static bool external_bootloader_approve(OtaStoreSdNrf52& store,
const uint8_t image_hash[32],
const uint8_t container_hash[32]) {
return store.approve_for_bootloader(image_hash, container_hash);
}
#endif
#if defined(OTA_QSPI_BOOTLOADER_UPDATE)
static bool external_bootloader_approve(OtaStoreQspiNrf52& store,
const uint8_t image_hash[32],
const uint8_t container_hash[32]) {
(void)image_hash;
(void)container_hash;
return store.approve_for_bootloader();
}
#endif
template <typename Store>
static bool ota_prepare_bootloader_update_external(Store& store,
const SignerAllowlist& allow,
const OtaBootloaderIdentity& installed,
const uint8_t actual_mid[4],
const uint8_t operator_mid[4],
const uint8_t operator_hash8[8],
const char* storage_name,
ApplyState& st, char* msg) {
static const size_t CAP = 96;
st = ApplyState();
const uint32_t total = store.staged_size();
uint8_t hdr[8], manifest[MOTA_MFL];
if (total < 8u + MOTA_MFL + 5u || !store.read(0, hdr, sizeof(hdr)) ||
memcmp(hdr, MOTA_MAGIC, sizeof(MOTA_MAGIC)) != 0 || rd_u32le(hdr + 4) != total ||
!store.read(8, manifest, sizeof(manifest))) {
snprintf(msg, CAP, "%s bootloader container parse failed", storage_name); return false;
}
MotaManifest m;
if (!mota_parse_manifest(manifest, sizeof(manifest), m)) {
strcpy(msg, "not a valid v3 bootloader package"); return false;
}
// Authenticate this exact local manifest copy before any removable-media
// reread. The streaming verifier below requires byte identity with it, and
// the SD approval token copies its signed image_hash.
mesh::Identity authenticated_signer(m.signer_pubkey);
if (!authenticated_signer.verify(
m.signature, m.manifest_start, (int)m.signed_len)) {
strcpy(msg, "bootloader package signature invalid"); return false;
}
if (!allow.contains(m.signer_pubkey)) {
strcpy(msg, "bootloader signer is not in the trusted allowlist"); return false;
}
switch (ota_bootloader_confirmation_gate(m, installed, actual_mid, operator_mid,
operator_hash8)) {
case OTA_BOOT_CONFIRM_OK: break;
case OTA_BOOT_CONFIRM_NOT_BOOT_PACKAGE: strcpy(msg, "not a bootloader package"); return false;
case OTA_BOOT_CONFIRM_GEOMETRY: strcpy(msg, "bootloader package geometry mismatch"); return false;
case OTA_BOOT_CONFIRM_LOCAL_IDENTITY: strcpy(msg, "installed bootloader identity is invalid"); return false;
case OTA_BOOT_CONFIRM_TARGET: strcpy(msg, "bootloader board ID mismatch"); return false;
case OTA_BOOT_CONFIRM_HW_ID: strcpy(msg, "bootloader signed hw_id mismatch"); return false;
case OTA_BOOT_CONFIRM_MID: strcpy(msg, "bootloader MID confirmation mismatch"); return false;
case OTA_BOOT_CONFIRM_IMAGE_HASH: strcpy(msg, "bootloader image-hash confirmation mismatch"); return false;
}
const OtaBlCaps current_caps = ota_bootloader_update_caps();
if (!ota_bootloader_self_update_caps_valid(current_caps)) {
snprintf(msg, CAP, "installed bootloader cannot safely self-update from %s", storage_name);
return false;
}
const uint64_t payload_off64 = 8u + MOTA_MFL + (uint64_t)m.block_count * 4u;
if (payload_off64 > UINT32_MAX || payload_off64 + OTA_BOOT_IMAGE_SIZE + 5u != total) {
strcpy(msg, "bootloader container layout mismatch"); return false;
}
const uint32_t payload_off = (uint32_t)payload_off64;
VerifyResult vr = ota_verify(
static_cast<const OtaStore&>(store), allow, manifest);
st.manifest_ok = vr.parsed;
st.sig_ok = vr.sig_ok;
st.trusted = vr.trusted;
st.image_size = m.image_size;
memcpy(st.image_hash, m.image_hash, sizeof(st.image_hash));
if (!vr.auto_appliable() || !vr.container_hash_ok) {
if (!vr.root_ok || !vr.payload_ok || !vr.image_ok)
strcpy(msg, "bootloader payload hash mismatch");
else if (!vr.sig_ok)
strcpy(msg, "bootloader package signature invalid");
else
strcpy(msg, "bootloader signer is not in the trusted allowlist");
return false;
}
uint8_t vectors[8];
if (!store.read(payload_off, vectors, sizeof(vectors)) || !ota_bootloader_vector_sane(vectors)) {
strcpy(msg, "bootloader vector table is invalid"); return false;
}
OtaBootloaderIdentity candidate;
OtaBootloaderCapsMarker candidate_caps;
if (!ota_bootloader_external_image_metadata(
store, payload_off, ota_bootloader_update_storage_flags(),
candidate, candidate_caps) ||
!ota_bootloader_identity_matches(installed, candidate)) {
strcpy(msg, "candidate bootloader identity/capability/CRC mismatch"); return false;
}
const OtaBootloaderContinuityGate continuity = ota_bootloader_continuity_gate(
installed, candidate, m.fw_version, OTA_BOOT_CONTINUITY_FAMILY_S140,
ota_runtime_softdevice_fwid(), mota_nrf52_app_base(),
OTA_BOOT_CONTINUITY_LAYOUT_ABI);
if (continuity != OTA_BOOT_CONTINUITY_OK) {
strncpy(msg, ota_bootloader_continuity_error(continuity), CAP);
msg[CAP - 1] = 0;
return false;
}
st.slot_ok = true;
if (!external_bootloader_approve(store, m.image_hash, vr.container_hash)) {
snprintf(msg, CAP, "%s bootloader handoff failed: %s", storage_name, store.last_error());
return false;
}
strcpy(msg, "trusted bootloader verified and armed; rebooting after this reply");
return true;
}
#endif
#if defined(OTA_SD_STORE)
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
ApplyState& st, char* msg) {
#if defined(OTA_SD_BOOTLOADER_UPDATE)
OtaBootloaderIdentity installed;
if (!ota_installed_bootloader_identity(installed) ||
!ota_bootloader_sd_retained_auth_ready(
installed, OTA_BOOT_CONTINUITY_FAMILY_S140,
ota_runtime_softdevice_fwid(), mota_nrf52_app_base(),
OTA_BOOT_CONTINUITY_LAYOUT_ABI)) {
st = ApplyState();
strcpy(msg, "SD application OTA requires the BLM2 retained-auth bootloader");
return false;
}
#endif
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_SD, "SD", true, st, msg);
}
#if defined(OTA_SD_BOOTLOADER_UPDATE)
bool ota_prepare_bootloader_update_nrf52(OtaStoreSdNrf52& store,
const SignerAllowlist& allow,
const OtaBootloaderIdentity& installed,
const uint8_t actual_mid[4],
const uint8_t operator_mid[4],
const uint8_t operator_hash8[8],
ApplyState& st, char* msg) {
if (!ota_bootloader_sd_retained_auth_ready(
installed, OTA_BOOT_CONTINUITY_FAMILY_S140,
ota_runtime_softdevice_fwid(), mota_nrf52_app_base(),
OTA_BOOT_CONTINUITY_LAYOUT_ABI)) {
st = ApplyState();
strcpy(msg, "SD bootloader OTA requires a locally installed BLM2 retained-auth bootloader");
return false;
}
SelfFwInfo fi;
if (!ota_self_firmware(fi) ||
!ota_bootloader_scratch_headroom_valid(
fi.valid, mota_nrf52_app_base(), fi.image_len, OTA_BOOT_SCRATCH_START) ||
!ota_bootloader_live_bank_preserves_scratch(
mota_nrf52_app_base(), fi.image_len, OTA_BOOT_SCRATCH_START)) {
st = ApplyState();
strcpy(msg, "running firmware/settings do not preserve E0000 scratch");
return false;
}
return ota_prepare_bootloader_update_external(
store, allow, installed, actual_mid, operator_mid, operator_hash8, "SD", st, msg);
}
#endif
#endif
#if defined(OTA_QSPI_STORE)
bool ota_apply_mota_nrf52(OtaStoreQspiNrf52& store, const SignerAllowlist& allow,
ApplyState& st, char* msg) {
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_QSPI, "QSPI", false, st, msg);
}
#if defined(OTA_QSPI_BOOTLOADER_UPDATE)
bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
const SignerAllowlist& allow,
const OtaBootloaderIdentity& installed,
const uint8_t actual_mid[4],
const uint8_t operator_mid[4],
const uint8_t operator_hash8[8],
ApplyState& st, char* msg) {
return ota_prepare_bootloader_update_external(
store, allow, installed, actual_mid, operator_mid, operator_hash8, "QSPI", st, msg);
}
#endif
#endif
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) && defined(OTA_FLASH_STORE)
bool ota_prepare_bootloader_update_nrf52(OtaStoreFlashNrf52& store,
const SignerAllowlist& allow,
const OtaBootloaderIdentity& installed,
const uint8_t actual_mid[4],
const uint8_t operator_mid[4],
const uint8_t operator_hash8[8],
ApplyState& st, char* msg) {
static const size_t CAP = 96;
st = ApplyState();
const uint32_t total = store.staged_size();
const uint8_t* container = store.data();
MotaManifest m;
if (!container || total < 8u + MOTA_MFL + 5u || !mota_parse(container, total, m)) {
strcpy(msg, "internal bootloader container parse failed"); return false;
}
switch (ota_bootloader_confirmation_gate(m, installed, actual_mid, operator_mid,
operator_hash8)) {
case OTA_BOOT_CONFIRM_OK: break;
case OTA_BOOT_CONFIRM_NOT_BOOT_PACKAGE: strcpy(msg, "not a bootloader package"); return false;
case OTA_BOOT_CONFIRM_GEOMETRY: strcpy(msg, "bootloader package geometry mismatch"); return false;
case OTA_BOOT_CONFIRM_LOCAL_IDENTITY: strcpy(msg, "installed bootloader identity is invalid"); return false;
case OTA_BOOT_CONFIRM_TARGET: strcpy(msg, "bootloader target identity mismatch"); return false;
case OTA_BOOT_CONFIRM_HW_ID: strcpy(msg, "bootloader signed hw_id mismatch"); return false;
case OTA_BOOT_CONFIRM_MID: strcpy(msg, "bootloader MID confirmation mismatch"); return false;
case OTA_BOOT_CONFIRM_IMAGE_HASH: strcpy(msg, "bootloader image-hash confirmation mismatch"); return false;
}
const OtaBlCaps current_caps = ota_bootloader_update_caps();
if (!ota_bootloader_self_update_caps_valid(current_caps) ||
current_caps.storage_flags != (OTA_BL_STORAGE_STAGE_CEILING |
OTA_BL_STORAGE_BOOT_UPDATE)) {
strcpy(msg, "installed bootloader cannot safely self-update from internal flash"); return false;
}
const uint64_t payload_off64 = 8u + MOTA_MFL + (uint64_t)m.block_count * 4u;
SelfFwInfo fi;
uint32_t expected_start;
const uint32_t app_base = mota_nrf52_app_base();
if (payload_off64 > UINT32_MAX || payload_off64 + OTA_BOOT_IMAGE_SIZE + 5u != total ||
!ota_self_firmware(fi) || !fi.valid || fi.image_len > UINT32_MAX - app_base ||
!mota_nrf52_shared_boot_stage_plan(
total, app_base, true, app_base + fi.image_len, expected_start) ||
store.write_start() != expected_start) {
strcpy(msg, "internal bootloader container placement mismatch"); return false;
}
const uint32_t payload_off = (uint32_t)payload_off64;
VerifyResult vr = ota_verify(container, total, allow);
st.manifest_ok = vr.parsed;
st.sig_ok = vr.sig_ok;
st.trusted = vr.trusted;
st.image_size = m.image_size;
memcpy(st.image_hash, m.image_hash, sizeof(st.image_hash));
if (!vr.auto_appliable()) {
if (!vr.root_ok || !vr.payload_ok || !vr.image_ok)
strcpy(msg, "bootloader payload hash mismatch");
else if (!vr.sig_ok)
strcpy(msg, "bootloader package signature invalid");
else
strcpy(msg, "bootloader signer is not in the trusted allowlist");
return false;
}
const uint8_t* image = container + payload_off;
if (!ota_bootloader_vector_sane(image)) {
strcpy(msg, "bootloader vector table is invalid"); return false;
}
OtaBootloaderIdentity candidate;
OtaBootloaderCapsMarker candidate_caps;
if (!ota_bootloader_candidate_identity_from_image(
image, OTA_BOOT_IMAGE_SIZE, candidate) ||
!ota_bootloader_identity_matches(installed, candidate) ||
!ota_bootloader_caps_from_image(
image, OTA_BOOT_IMAGE_SIZE,
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE,
candidate_caps)) {
strcpy(msg, "candidate bootloader identity/capability/CRC mismatch"); return false;
}
const OtaBootloaderContinuityGate continuity = ota_bootloader_continuity_gate(
installed, candidate, m.fw_version, OTA_BOOT_CONTINUITY_FAMILY_S140,
ota_runtime_softdevice_fwid(), mota_nrf52_app_base(),
OTA_BOOT_CONTINUITY_LAYOUT_ABI);
if (continuity != OTA_BOOT_CONTINUITY_OK) {
strncpy(msg, ota_bootloader_continuity_error(continuity), CAP);
msg[CAP - 1] = 0;
return false;
}
st.slot_ok = true;
if (!store.approve_for_bootloader()) {
snprintf(msg, CAP, "internal bootloader handoff failed: %s", store.last_error()); return false;
}
strcpy(msg, "trusted bootloader verified in internal flash; rebooting after this reply");
return true;
}
#endif
#else // native / other platforms
bool ota_apply_slot_info(uint32_t*, uint32_t*) { return false; }
bool ota_apply_set_manifest(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st) { st = ApplyState(); return false; }
bool ota_apply_verify_slot(ApplyState&) { return false; }
bool ota_apply_commit() { return false; }
bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) { st = ApplyState(); strcpy(msg, "unsupported"); return false; }
bool ota_apply_mota_nrf52(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) { st = ApplyState(); strcpy(msg, "unsupported"); return false; }
bool ota_rescue_mota_nrf52(const uint8_t*, uint32_t, const SignerAllowlist&, const uint8_t*,
uint32_t, ApplyState& st, char* msg) {
st = ApplyState(); strcpy(msg, "unsupported"); return false;
}
void ota_reboot_to_apply() {}
void ota_reboot_to_bootloader_update() {}
bool ota_installed_bootloader_identity(OtaBootloaderIdentity& out) {
out = OtaBootloaderIdentity(); return false;
}
uint8_t ota_bootloader_last_rc() { return 0; }
#endif
} // namespace ota
} // namespace mesh