Add MeshTower SD bootloader updates over LoRa

This commit is contained in:
mikecarper committed 2026-08-21 09:55:15 -07:00
1 parent 9e4d246ac6
commit 3d78d9c846
26 files changed
+763 -114

No files matched your search

+107 -38
View File
@@ -485,6 +485,8 @@ 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);
@@ -749,22 +751,10 @@ static bool ota_apply_mota_nrf52_external(Store& store, const SignerAllowlist& a
}
#endif
#if defined(OTA_SD_STORE)
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
ApplyState& st, char* msg) {
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_SD, "SD", st, msg);
}
#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", st, msg);
}
#if defined(OTA_QSPI_BOOTLOADER_UPDATE)
static bool qspi_bootloader_crc_ok(OtaStoreQspiNrf52& store, uint32_t payload_off,
const OtaBootloaderIdentity& identity) {
#if defined(OTA_SD_BOOTLOADER_UPDATE) || defined(OTA_QSPI_BOOTLOADER_UPDATE)
template <typename Store>
static bool external_bootloader_crc_ok(Store& store, uint32_t payload_off,
const OtaBootloaderIdentity& identity) {
uint8_t buf[512];
uint32_t crc = UINT32_MAX;
for (uint32_t off = 0; off < OTA_BOOT_IMAGE_SIZE; off += sizeof(buf)) {
@@ -781,10 +771,10 @@ static bool qspi_bootloader_crc_ok(OtaStoreQspiNrf52& store, uint32_t payload_of
return ~crc == identity.crc32;
}
static bool qspi_bootloader_image_metadata(OtaStoreQspiNrf52& store, uint32_t payload_off,
const OtaBootloaderIdentity& installed,
OtaBootloaderIdentity& candidate,
OtaBootloaderCapsMarker& caps) {
template <typename Store>
static bool external_bootloader_image_metadata(Store& store, uint32_t payload_off,
OtaBootloaderIdentity& candidate,
OtaBootloaderCapsMarker& caps) {
// Keep enough overlap to catch a 44-byte manifest or 16-byte capability marker split across a
// window. Every aligned manifest candidate is considered; a magic string in a literal pool is not
// a terminal failure.
@@ -804,7 +794,8 @@ static bool qspi_bootloader_image_metadata(OtaStoreQspiNrf52& store, uint32_t pa
OtaBootloaderCapsMarker parsed;
if (ota_bootloader_caps_marker_parse(buf + local, parsed) &&
parsed.apply_abi >= MOTA_BOOT_FORMAT_VER &&
(parsed.codec_mask & (1u << CODEC_FULL)) != 0 &&
(parsed.codec_mask & OTA_BL_REQUIRED_APP_CODEC_MASK) ==
OTA_BL_REQUIRED_APP_CODEC_MASK &&
(parsed.storage_flags & OTA_BL_STORAGE_BOOT_UPDATE) != 0) {
if (++valid_caps != 1u) return false;
if (parsed.storage_flags == ota_bootloader_update_storage_flags()) caps = parsed;
@@ -812,10 +803,8 @@ static bool qspi_bootloader_image_metadata(OtaStoreQspiNrf52& store, uint32_t pa
}
if ((absolute & 3u) != 0 || local + OTA_BOOT_MANIFEST_SIZE > len) continue;
OtaBootloaderIdentity parsed;
if (!ota_bootloader_manifest_parse(buf + local, absolute, parsed) ||
parsed.board_id != installed.board_id ||
memcmp(parsed.device_name, installed.device_name, OTA_BOOT_DEVICE_NAME_SIZE) != 0) continue;
if (qspi_bootloader_crc_ok(store, payload_off, parsed)) {
if (!ota_bootloader_manifest_parse(buf + local, absolute, parsed)) continue;
if (external_bootloader_crc_ok(store, payload_off, parsed)) {
parsed.crc_ok = true;
if (!ota_bootloader_identity_add_unique(parsed, candidate, valid_identities)) return false;
}
@@ -825,13 +814,30 @@ static bool qspi_bootloader_image_metadata(OtaStoreQspiNrf52& store, uint32_t pa
candidate.present && candidate.crc_ok && caps.present;
}
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) {
#if defined(OTA_SD_BOOTLOADER_UPDATE)
static bool external_bootloader_approve(OtaStoreSdNrf52& store,
const uint8_t image_hash[32]) {
return store.approve_for_bootloader(image_hash);
}
#endif
#if defined(OTA_QSPI_BOOTLOADER_UPDATE)
static bool external_bootloader_approve(OtaStoreQspiNrf52& store,
const uint8_t image_hash[32]) {
(void)image_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();
@@ -839,12 +845,23 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
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))) {
strcpy(msg, "QSPI bootloader container parse failed"); return false;
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;
@@ -859,7 +876,8 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
const OtaBlCaps current_caps = ota_bootloader_caps();
if (!ota_bootloader_self_update_caps_valid(current_caps)) {
strcpy(msg, "installed bootloader cannot safely self-update from QSPI"); return false;
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;
@@ -868,7 +886,8 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
}
const uint32_t payload_off = (uint32_t)payload_off64;
VerifyResult vr = ota_verify(static_cast<const OtaStore&>(store), allow);
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;
@@ -890,18 +909,68 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
}
OtaBootloaderIdentity candidate;
OtaBootloaderCapsMarker candidate_caps;
if (!qspi_bootloader_image_metadata(store, payload_off, installed, candidate, candidate_caps) ||
if (!external_bootloader_image_metadata(store, payload_off, candidate, candidate_caps) ||
!ota_bootloader_identity_matches(installed, candidate)) {
strcpy(msg, "candidate bootloader identity/capability/CRC mismatch"); return false;
}
st.slot_ok = true;
if (!store.approve_for_bootloader()) {
snprintf(msg, CAP, "QSPI bootloader handoff failed: %s", store.last_error()); return false;
if (!external_bootloader_approve(store, m.image_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) {
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_SD, "SD", 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) {
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", 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)
+9
View File
@@ -86,6 +86,15 @@ bool ota_rescue_mota_nrf52(const uint8_t* buf, uint32_t len,
class OtaStoreSdNrf52;
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store,
const SignerAllowlist& allow, ApplyState& st, char* 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);
#endif
#endif
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
class OtaStoreQspiNrf52;
+23 -6
View File
@@ -34,16 +34,29 @@ static const uint8_t OTA_BL_STORAGE_STAGE_CEILING = 0x02;
static const uint8_t OTA_BL_STORAGE_QSPI = 0x04;
static const uint8_t OTA_BL_STORAGE_BOOT_UPDATE = 0x08;
static const uint8_t OTA_BL_STORAGE_KNOWN = 0x0F;
static const uint8_t OTA_BL_PROFILE_SD_BOOT_UPDATE =
OTA_BL_STORAGE_SD | OTA_BL_STORAGE_BOOT_UPDATE;
static const uint8_t OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE =
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE;
static const uint8_t OTA_BL_PROFILE_QSPI_BOOT_UPDATE =
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI |
OTA_BL_STORAGE_BOOT_UPDATE;
// A successor must retain both application update paths used by qualified
// external stores: bit 0 CODEC_FULL and bit 2 CODEC_DETOOLS_INPLACE.
static const uint16_t OTA_BL_REQUIRED_APP_CODEC_MASK = 0x0005u;
inline uint8_t ota_bootloader_update_storage_flags() {
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
// No external-storage bit means the ordinary internal flash store. The
// same store holds either an app delta or a boot package; BOOT_UPDATE marks
// only the privileged package capability, not a second storage backend.
return OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE;
return OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE;
#elif defined(OTA_QSPI_BOOTLOADER_UPDATE)
return OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI |
OTA_BL_STORAGE_BOOT_UPDATE;
return OTA_BL_PROFILE_QSPI_BOOT_UPDATE;
#elif defined(OTA_SD_BOOTLOADER_UPDATE)
// The complete container remains in the contiguous SD staging file. No
// internal stage-ceiling capability is implied by this external store.
return OTA_BL_PROFILE_SD_BOOT_UPDATE;
#else
return 0;
#endif
@@ -52,7 +65,9 @@ inline uint8_t ota_bootloader_update_storage_flags() {
inline bool ota_bootloader_self_update_caps_valid(const OtaBlCaps& c) {
const uint8_t required = ota_bootloader_update_storage_flags();
return required != 0 && c.present && c.apply_abi >= 3u &&
(c.codec_mask & 1u) != 0 && c.storage_flags == required;
(c.codec_mask & OTA_BL_REQUIRED_APP_CODEC_MASK) ==
OTA_BL_REQUIRED_APP_CODEC_MASK &&
c.storage_flags == required;
}
// Prefer a continuity-capable marker over a numerically newer legacy-looking candidate. Bootloader
@@ -88,7 +103,8 @@ inline OtaBlCaps ota_bl_caps_scan_aligned(const uint8_t* bytes, size_t len,
if (abi == 0 || abi == 0xFFFFu || codecs == 0 || (storage & ~OTA_BL_STORAGE_KNOWN) != 0 ||
p[13] != 0 || p[14] != 0 || p[15] != 0) continue;
const bool exact_profile = exact_storage_flags == 0 || storage == exact_storage_flags;
if (require_boot_update && exact_profile && abi >= 3u && (codecs & 1u) != 0 &&
if (require_boot_update && exact_profile && abi >= 3u &&
(codecs & OTA_BL_REQUIRED_APP_CODEC_MASK) == OTA_BL_REQUIRED_APP_CODEC_MASK &&
(storage & OTA_BL_STORAGE_BOOT_UPDATE) != 0) {
// A self-update build must have one unambiguous privileged marker. Do
// not silently choose between two otherwise valid structures (including
@@ -118,7 +134,8 @@ inline OtaBlCaps ota_bootloader_caps() {
const uint8_t* lo = (const uint8_t*)(uintptr_t)MOTA_NRF52_BL_START;
const uint8_t* hi = (const uint8_t*)(uintptr_t)MOTA_NRF52_BL_END;
return ota_bl_caps_scan_aligned(lo, (size_t)(hi - lo),
#if defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
#if defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
defined(OTA_SD_BOOTLOADER_UPDATE)
true, ota_bootloader_update_storage_flags()
#else
false
+70 -1
View File
@@ -31,6 +31,18 @@
#endif
#endif
#if defined(OTA_SD_BOOTLOADER_UPDATE)
#if !defined(NRF52_PLATFORM) || !defined(OTA_SD_STORE)
#error "OTA_SD_BOOTLOADER_UPDATE requires nRF52 SD staging"
#endif
#if !defined(HELTEC_TOWER_V2_SDCARD)
#error "OTA_SD_BOOTLOADER_UPDATE is restricted to the qualified MeshTower V2 SD target"
#endif
#if defined(OTA_FLASH_STORE) || defined(OTA_QSPI_STORE) || defined(QSPIFLASH)
#error "SD bootloader staging cannot share another OTA/filesystem store"
#endif
#endif
namespace mesh {
namespace ota {
@@ -45,9 +57,65 @@ static const uint32_t OTA_BOOT_IMAGE_START = 0x000F4000UL;
static const uint32_t OTA_BOOT_IMAGE_SIZE = 0x0000A000UL; // F4000..FE000, padded to exactly 40 KiB
static const uint32_t OTA_BOOT_SCRATCH_START = 0x000E0000UL;
static const uint32_t OTA_BOOT_SCRATCH_END = 0x000EA000UL;
static const uint32_t OTA_NRF52840_BOOT_SETTINGS_ADDRESS = 0x000FF000UL;
static const uint16_t OTA_BOOT_BANK_VALID_APP = 0x0001u;
static const uint16_t OTA_BOOT_BANK_ERASED_FALLBACK = 0xFFFFu;
static const uint32_t OTA_NRF52840_RAM_START = 0x20000000UL;
static const uint32_t OTA_NRF52840_RAM_END = 0x20040000UL;
inline bool ota_bootloader_image_geometry_valid(uint32_t image_size,
uint32_t payload_size) {
return image_size == OTA_BOOT_IMAGE_SIZE && payload_size == OTA_BOOT_IMAGE_SIZE;
}
// External SD keeps the normal ED000 application linker, but OTAFIX needs the
// XIAO-sized E0000..EA000 internal scratch range temporarily while replacing
// itself. A hash-valid live EndF must prove the complete running image ends
// before that scratch range; a linker maximum alone is not sufficient.
inline bool ota_bootloader_scratch_headroom_valid(bool self_valid,
uint32_t app_base,
uint32_t image_len,
uint32_t scratch_start) {
return self_valid && app_base < scratch_start && image_len != 0 &&
image_len <= scratch_start - app_base;
}
// The bootloader may validate BANK_VALID_APP over bank_0_size on every reset.
// Erasing the SD authorization-token page must not change bytes covered by a
// nonzero stored CRC. A CRC-bound bank must cover the complete hash-valid live
// image but stop by scratch_start; a smaller recorded size would disagree with
// OTAFIX even if it did not overlap scratch. Debugger/UF2 installs can have
// erased settings, and a valid-app record with CRC zero explicitly disables
// that CRC check.
inline bool ota_bootloader_scratch_bank_geometry_valid(uint16_t bank_0,
uint16_t bank_0_crc,
uint32_t bank_0_size,
uint32_t live_image_size,
uint32_t safe_span) {
if (bank_0 == OTA_BOOT_BANK_ERASED_FALLBACK) return true;
if (bank_0 != OTA_BOOT_BANK_VALID_APP) return false;
if (bank_0_crc == 0u) return true;
return live_image_size != 0u && bank_0_size >= live_image_size &&
bank_0_size <= safe_span;
}
#if defined(NRF52_PLATFORM)
inline bool ota_bootloader_live_bank_preserves_scratch(uint32_t app_base,
uint32_t live_image_size,
uint32_t scratch_start) {
if (app_base >= scratch_start) return false;
const volatile uint8_t* raw =
(const volatile uint8_t*)(uintptr_t)OTA_NRF52840_BOOT_SETTINGS_ADDRESS;
const uint16_t bank_0 = (uint16_t)(raw[0] | ((uint16_t)raw[1] << 8));
const uint16_t bank_0_crc = (uint16_t)(raw[2] | ((uint16_t)raw[3] << 8));
const uint32_t bank_0_size = (uint32_t)raw[8] | ((uint32_t)raw[9] << 8) |
((uint32_t)raw[10] << 16) | ((uint32_t)raw[11] << 24);
return ota_bootloader_scratch_bank_geometry_valid(
bank_0, bank_0_crc, bank_0_size, live_image_size,
scratch_start - app_base);
}
#endif
// Only the two official XIAO nRF52840 OTAFIX identities are eligible. Runtime matching against the
// installed, CRC-valid manifest distinguishes the base and Sense modules even for carrier-board aliases.
static const uint32_t OTA_XIAO_BOARD_ID_BASE = 0x28860044UL;
@@ -284,7 +352,8 @@ inline bool ota_bootloader_caps_from_image(const uint8_t* image, size_t image_si
OtaBootloaderCapsMarker parsed;
if (!ota_bootloader_caps_marker_parse(image + off, parsed) ||
parsed.apply_abi < MOTA_BOOT_FORMAT_VER ||
(parsed.codec_mask & (1u << CODEC_FULL)) == 0 ||
(parsed.codec_mask & OTA_BL_REQUIRED_APP_CODEC_MASK) !=
OTA_BL_REQUIRED_APP_CODEC_MASK ||
(parsed.storage_flags & OTA_BL_STORAGE_BOOT_UPDATE) == 0) continue;
if (++valid_count != 1u) return false; // privileged marker identity must be unambiguous
if (parsed.storage_flags != exact_storage_flags) continue;
+42 -3
View File
@@ -189,6 +189,16 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | qspi | "
"folder | config | key");
#endif
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
#if defined(OTA_SD_BOOTLOADER_UPDATE)
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | bootloader | cancel | announce | self | "
"folder | cache | config | key");
#else
strcpy(reply,
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | folder | "
"cache | config | key");
#endif
#else
snprintf(reply, 160,
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | folder | "
@@ -328,6 +338,16 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
!defined(OTA_QSPI_STORE)
SelfFwInfo list_self;
bool list_has_endf = ota_self_firmware(list_self) && list_self.valid;
#endif
#if defined(NRF52_PLATFORM) && defined(OTA_SD_BOOTLOADER_UPDATE)
SelfFwInfo list_sd_self;
const bool list_sd_headroom = ota_self_firmware(list_sd_self) &&
ota_bootloader_scratch_headroom_valid(
list_sd_self.valid, mota_nrf52_app_base(), list_sd_self.image_len,
OTA_BOOT_SCRATCH_START) &&
ota_bootloader_live_bank_preserves_scratch(
mota_nrf52_app_base(), list_sd_self.image_len,
OTA_BOOT_SCRATCH_START);
#endif
uint32_t now = millis(); int shown = 0;
uint16_t first = (uint16_t)(page - 1) * PAGE_SIZE;
@@ -355,12 +375,16 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
if (boot_package) {
bool installable = false;
#if defined(NRF52_PLATFORM) && \
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
defined(OTA_SD_BOOTLOADER_UPDATE))
const OtaBootloaderIdentity& bid = c.bootloaderIdentity();
installable = h->flags == (MFLAG_FULL | MFLAG_SIGNED | MFLAG_BOOTLOADER) &&
h->codec == CODEC_FULL && bid.present && bid.crc_ok &&
h->target_id == ota_bootloader_target_id(bid) &&
ota_bootloader_self_update_caps_valid(list_bl);
#if defined(OTA_SD_BOOTLOADER_UPDATE)
installable = installable && list_sd_headroom;
#endif
#endif
fit = installable ? "yours" : "bootloader unsupported";
} else if (myt && h->target_id == myt) {
@@ -489,7 +513,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
#else
if (selboot) {
#if defined(NRF52_PLATFORM) && \
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
defined(OTA_SD_BOOTLOADER_UPDATE))
const OtaBootloaderIdentity& bid = c.bootloaderIdentity();
const OtaBlCaps& bl = c.bootloaderCaps();
if (selflags != (MFLAG_FULL | MFLAG_SIGNED | MFLAG_BOOTLOADER) ||
@@ -505,6 +530,19 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
strcpy(reply, "ERR installed bootloader lacks safe LoRa bootloader-update support");
return true;
}
#if defined(OTA_SD_BOOTLOADER_UPDATE)
SelfFwInfo sd_self;
if (!ota_self_firmware(sd_self) ||
!ota_bootloader_scratch_headroom_valid(
sd_self.valid, mota_nrf52_app_base(), sd_self.image_len,
OTA_BOOT_SCRATCH_START) ||
!ota_bootloader_live_bank_preserves_scratch(
mota_nrf52_app_base(), sd_self.image_len,
OTA_BOOT_SCRATCH_START)) {
strcpy(reply, "ERR running firmware/settings do not preserve E0000 scratch; use local DFU/SWD");
return true;
}
#endif
#else
strcpy(reply, "ERR this build cannot install bootloader packages; use folder capture or USB DFU");
return true;
@@ -716,7 +754,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
} else if (is_cmd(a, "bootloader|blupdate", &rest)) {
#if defined(NRF52_PLATFORM) && !defined(OTA_SEEDER_ONLY) && \
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
defined(OTA_SD_BOOTLOADER_UPDATE))
const OtaBootloaderIdentity& bid = c.bootloaderIdentity();
const OtaBlCaps& bl = c.bootloaderCaps();
if (*rest == 0 || strcmp(rest, "status") == 0) {
+6 -3
View File
@@ -35,7 +35,8 @@
// Per-device OTA singleton shared by the CLI (OtaCli) and the mesh adapter (the example's MyMesh).
// Holds the session engine, a persistent staging store (fetch), a RAM serve buffer, and the signer
// allowlist. nRF52 uses one of three stores selected by the exact target: internal flash for in-place
// deltas, raw microSD, or dedicated raw QSPI for full images and in-place deltas. Flash-backed stores
// deltas, raw microSD, or dedicated raw QSPI for full images and in-place deltas. One exact microSD
// target also accepts explicitly confirmed signed bootloader packages. Flash-backed stores
// coalesce writes at their erase-page boundary and checkpoint payload before leaf metadata, so an
// interrupted fetch can be verified and resumed without trusting stale progress markers.
@@ -238,7 +239,8 @@ struct OtaContext {
bool apply_fetched_bootloader(const uint8_t operator_mid[4],
const uint8_t operator_hash8[8], char* msg) {
#if defined(NRF52_PLATFORM) && !defined(OTA_SEEDER_ONLY) && \
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
defined(OTA_SD_BOOTLOADER_UPDATE))
if (fetch_to_folder) {
strncpy(msg, "refused: bootloader package is not in local install storage", 96);
msg[95] = 0; return false;
@@ -518,7 +520,8 @@ struct OtaContext {
manager.set_apply_codec2(CODEC_DETOOLS_INPLACE); // also accepted -> a single in-place .mota fits both
#endif
#if defined(NRF52_PLATFORM) && \
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE))
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
defined(OTA_SD_BOOTLOADER_UPDATE))
manager.set_accept_bootloader(true);
#else
manager.set_accept_bootloader(false);
+1
View File
@@ -52,6 +52,7 @@ static const uint8_t GPREGRET_OTA_BOOTLOADER_UPDATE = 0x6Bu;
static const uint8_t GPREGRET2_OTA_STAGE_LEGACY = 0xD4u;
static const uint8_t GPREGRET2_OTA_STAGE_EXPANDED = 0xEDu;
static const uint8_t GPREGRET2_OTA_STAGE_QSPI = 0x51u;
static const uint8_t GPREGRET2_OTA_STAGE_SD = 0x53u;
inline uint8_t mota_nrf52_flash_stage_handoff(uint32_t effective_stage_ceiling) {
if (effective_stage_ceiling == MOTA_NRF52_STAGE_CEILING_EXPANDED)
+73
View File
@@ -0,0 +1,73 @@
// Internal authorization token for a bootloader package staged on removable SD.
// Keep byte-identical with OTAFIX src/ota_sd_boot_token.h.
#ifndef OTA_SD_BOOT_TOKEN_H_
#define OTA_SD_BOOT_TOKEN_H_
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#define MOTA_SD_BOOT_TOKEN_VERSION 1u
#define MOTA_SD_BOOT_TOKEN_LEN 64u
#define MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET 24u
#define MOTA_SD_BOOT_TOKEN_CRC_OFFSET 56u
static const uint8_t MOTA_SD_BOOT_TOKEN_MAGIC[8] = {
'M', 'O', 'T', 'A', 'S', 'D', 'B', 'L'
};
static inline uint32_t mota_sd_boot_token_rd32(const uint8_t *p) {
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
static inline void mota_sd_boot_token_wr32(uint8_t *p, uint32_t value) {
p[0] = (uint8_t)value;
p[1] = (uint8_t)(value >> 8);
p[2] = (uint8_t)(value >> 16);
p[3] = (uint8_t)(value >> 24);
}
static inline uint32_t mota_sd_boot_token_crc32(const uint8_t *data, size_t len) {
uint32_t crc = 0xFFFFFFFFu;
for (size_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t bit = 0; bit < 8; bit++) {
crc = (crc >> 1) ^ (0xEDB88320u & (uint32_t)-(int32_t)(crc & 1u));
}
}
return ~crc;
}
// image_hash is copied from the exact signed manifest buffer authenticated by
// the application. It binds the only bootloader bytes that the MBR can install.
static inline void mota_sd_boot_token_encode(uint8_t out[MOTA_SD_BOOT_TOKEN_LEN],
uint32_t total,
const uint8_t image_hash[32]) {
memset(out, 0, MOTA_SD_BOOT_TOKEN_LEN);
memcpy(out, MOTA_SD_BOOT_TOKEN_MAGIC, sizeof(MOTA_SD_BOOT_TOKEN_MAGIC));
mota_sd_boot_token_wr32(out + 8u, MOTA_SD_BOOT_TOKEN_VERSION);
mota_sd_boot_token_wr32(out + 12u, MOTA_SD_BOOT_TOKEN_LEN);
mota_sd_boot_token_wr32(out + 16u, total);
mota_sd_boot_token_wr32(out + 20u, ~total);
memcpy(out + MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET, image_hash, 32u);
const uint32_t crc = mota_sd_boot_token_crc32(out, MOTA_SD_BOOT_TOKEN_CRC_OFFSET);
mota_sd_boot_token_wr32(out + MOTA_SD_BOOT_TOKEN_CRC_OFFSET, crc);
mota_sd_boot_token_wr32(out + MOTA_SD_BOOT_TOKEN_CRC_OFFSET + 4u, ~crc);
}
static inline int mota_sd_boot_token_valid(const uint8_t token[MOTA_SD_BOOT_TOKEN_LEN],
uint32_t total,
const uint8_t image_hash[32]) {
const uint32_t crc = mota_sd_boot_token_rd32(token + MOTA_SD_BOOT_TOKEN_CRC_OFFSET);
return memcmp(token, MOTA_SD_BOOT_TOKEN_MAGIC, sizeof(MOTA_SD_BOOT_TOKEN_MAGIC)) == 0 &&
mota_sd_boot_token_rd32(token + 8u) == MOTA_SD_BOOT_TOKEN_VERSION &&
mota_sd_boot_token_rd32(token + 12u) == MOTA_SD_BOOT_TOKEN_LEN &&
mota_sd_boot_token_rd32(token + 16u) == total &&
mota_sd_boot_token_rd32(token + 20u) == ~total &&
memcmp(token + MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET, image_hash, 32u) == 0 &&
mota_sd_boot_token_crc32(token, MOTA_SD_BOOT_TOKEN_CRC_OFFSET) == crc &&
mota_sd_boot_token_rd32(token + MOTA_SD_BOOT_TOKEN_CRC_OFFSET + 4u) == ~crc;
}
#endif // OTA_SD_BOOT_TOKEN_H_
+87 -2
View File
@@ -9,9 +9,13 @@
#include <stdlib.h>
#include <string.h>
#include "MeshCore.h"
#include "OtaBootloaderUpdate.h"
#include "OtaByteIO.h"
#include "OtaFlashLayout_nrf52.h"
#include "OtaSdBootToken.h"
#include "OtaSdHandoff.h"
#include "OtaSelf.h"
#include "flash/flash_nrf5x.h"
#ifndef OTA_SD_CS_PIN
#define OTA_SD_CS_PIN PIN_SPI1_NSS
@@ -44,6 +48,7 @@ void OtaStoreSdNrf52::resetStoreState() {
_allocated_sectors = 0;
_partition_start = 0;
_partition_end = 0;
_planned_bootloader = false;
if (_file && *_file) _file->close();
if (_sd) _sd->end();
_mounted = false;
@@ -370,7 +375,19 @@ bool OtaStoreSdNrf52::locate_file() {
bool OtaStoreSdNrf52::plan_layout(bool, uint32_t image_size,
uint32_t, uint32_t payload_size,
bool is_bootloader) {
if (is_bootloader) return false;
_planned_bootloader = false;
if (is_bootloader) {
#if defined(OTA_SD_BOOTLOADER_UPDATE)
if (ota_bootloader_image_geometry_valid(image_size, payload_size)) {
_planned_bootloader = true;
return true;
}
fail("bootloader package geometry mismatch");
#else
fail("bootloader update is not enabled for this SD target");
#endif
return false;
}
const uint32_t app_base = mota_nrf52_app_base();
if (image_size == 0 || payload_size == 0 || app_base >= MOTA_NRF52_APP_END ||
image_size > MOTA_NRF52_APP_END - app_base) {
@@ -464,6 +481,7 @@ void OtaStoreSdNrf52::checkpoint() {
bool OtaStoreSdNrf52::reopen() {
_total = 0;
_planned_bootloader = false;
if (!mount()) return false;
if (*_file) _file->close();
if (!_file->open(PATH, O_RDWR)) return false;
@@ -480,6 +498,32 @@ bool OtaStoreSdNrf52::reopen() {
return false;
}
_total = total;
uint8_t manifest[MOTA_MFL];
MotaManifest parsed;
if (total < 8u + sizeof(manifest) + 5u || !_file->seekSet(8u) ||
_file->read(manifest, sizeof(manifest)) != (int)sizeof(manifest) ||
!mota_parse_manifest(manifest, sizeof(manifest), parsed)) {
_total = 0;
_file->close();
return false;
}
if (parsed.is_bootloader()) {
#if defined(OTA_SD_BOOTLOADER_UPDATE)
const uint64_t payload_off = 8u + sizeof(manifest) +
(uint64_t)parsed.block_count * 4u;
if (!ota_bootloader_image_geometry_valid(parsed.image_size, parsed.payload_size) ||
payload_off + parsed.payload_size + 5u != total) {
_total = 0;
_file->close();
return false;
}
_planned_bootloader = true;
#else
_total = 0;
_file->close();
return false;
#endif
}
if (!locate_file()) {
_total = 0;
_file->close();
@@ -490,6 +534,7 @@ bool OtaStoreSdNrf52::reopen() {
void OtaStoreSdNrf52::clear() {
_total = 0;
_planned_bootloader = false;
_first_sector = 0;
_allocated_sectors = 0;
if (!mount()) return;
@@ -498,14 +543,54 @@ void OtaStoreSdNrf52::clear() {
_sd->remove(PATH);
}
bool OtaStoreSdNrf52::approve_for_bootloader() {
bool OtaStoreSdNrf52::approve_for_bootloader(
const uint8_t expected_boot_image_hash[32]) {
if (!_total || !finalize()) return false;
#if defined(OTA_SD_BOOTLOADER_UPDATE)
if (_planned_bootloader) {
SelfFwInfo fi;
if (!expected_boot_image_hash ||
_total != MOTA_NRF52_BOOT_CONTAINER_SIZE ||
!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)) {
fail("running firmware/settings do not preserve boot scratch");
return false;
}
}
#endif
if (!write(8 + MOTA_OFF_APPROVAL, APPROVAL_YES, sizeof(APPROVAL_YES)) ||
!_file->sync() || !_sd->card()->syncDevice()) {
fail("SD approval write failed");
return false;
}
#if defined(OTA_SD_BOOTLOADER_UPDATE)
if (_planned_bootloader) {
uint8_t token[MOTA_SD_BOOT_TOKEN_LEN];
mota_sd_boot_token_encode(token, _total, expected_boot_image_hash);
// This page is outside the hash-valid live app and below InternalFS. The
// token is temporary: OTAFIX validates it against the current SD bytes,
// then consumes the page as the first bootloader scratch page.
flash_nrf5x_flush();
if (!flash_nrf5x_erase(OTA_BOOT_SCRATCH_START) ||
flash_nrf5x_write(OTA_BOOT_SCRATCH_START, token, sizeof(token)) < 0) {
fail("SD bootloader token flash write failed");
return false;
}
flash_nrf5x_flush();
if (memcmp((const void*)(uintptr_t)OTA_BOOT_SCRATCH_START,
token, sizeof(token)) != 0) {
fail("SD bootloader token flash verify failed");
return false;
}
}
#endif
uint8_t sector[MOTA_SD_SECTOR_SIZE];
if (!_sd->card()->readSector(MOTA_SD_HANDOFF_SECTOR, sector)) {
fail("SD handoff sector read failed");
+11 -3
View File
@@ -18,6 +18,8 @@ class OtaCacheSdNrf52;
// Persistent nRF52840 OTA store backed by the MeshTower V2 microSD socket.
// The .mota remains a normal file, but is preallocated contiguously so the
// bootloader can read it by raw sectors without embedding a FAT implementation.
// The exact self-update build uses this same one-file store for either an
// application package or an explicitly confirmed signed bootloader package.
class OtaStoreSdNrf52 : public OtaStore {
public:
OtaStoreSdNrf52();
@@ -36,9 +38,14 @@ public:
uint32_t payload_off, uint32_t payload_size,
bool is_bootloader) override;
// Called only after the app has verified payload, base, signature and trust.
// Writes APRV into the file, then publishes the raw-sector handoff record.
bool approve_for_bootloader();
// Called only after the app has verified package kind, payload, applicable
// base/identity, signature, trust, and storage-specific safety geometry.
// For a bootloader package it writes APRV, binds the exact authenticated
// signed image_hash to an internal-flash token, and only then publishes the
// raw-sector handoff. Application packages retain the ordinary APRV+handoff
// path and omit expected_boot_image_hash.
bool approve_for_bootloader(
const uint8_t expected_boot_image_hash[32] = nullptr);
bool formatCard(MainBoard& board);
bool eraseCard(MainBoard& board);
bool getSpace(MainBoard& board, uint64_t& used_bytes, uint64_t& free_bytes);
@@ -60,6 +67,7 @@ private:
SdFs* _sd = nullptr;
mutable FsFile* _file = nullptr;
bool _mounted = false;
bool _planned_bootloader = false;
uint32_t _total = 0;
uint32_t _first_sector = 0;
uint32_t _allocated_sectors = 0;
+15 -1
View File
@@ -28,7 +28,9 @@ VerifyResult ota_verify(const uint8_t* buf, uint32_t len, const SignerAllowlist&
return r;
}
VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow) {
static VerifyResult ota_verify_store(const OtaStore& store,
const SignerAllowlist& allow,
const uint8_t* expected_manifest) {
VerifyResult r;
const uint32_t total = store.staged_size();
uint8_t hdr[8], manifest[MOTA_MFL], trailer[5];
@@ -36,6 +38,8 @@ VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow) {
!store.read(0, hdr, sizeof(hdr)) ||
memcmp(hdr, MOTA_MAGIC, 4) != 0 || rd_u32le(hdr + 4) != total ||
!store.read(8, manifest, sizeof(manifest)) ||
(expected_manifest &&
memcmp(manifest, expected_manifest, sizeof(manifest)) != 0) ||
!store.read(total - 5, trailer, sizeof(trailer)) ||
memcmp(trailer, MOTA_TRAILER, sizeof(trailer)) != 0) return r;
@@ -99,5 +103,15 @@ VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow) {
return r;
}
VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow) {
return ota_verify_store(store, allow, nullptr);
}
VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow,
const uint8_t expected_manifest[MOTA_MFL]) {
if (!expected_manifest) return VerifyResult();
return ota_verify_store(store, allow, expected_manifest);
}
} // namespace ota
} // namespace mesh
+6
View File
@@ -31,5 +31,11 @@ VerifyResult ota_verify(const uint8_t* buf, uint32_t len, const SignerAllowlist&
// contiguous memory view (notably the MeshTower V2 SD-backed nRF52 store).
VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow);
// As above, but the store's manifest must be byte-identical to an already
// authenticated local copy. This binds removable-media payload verification
// to the exact signed anchors later written into an internal approval token.
VerifyResult ota_verify(const OtaStore& store, const SignerAllowlist& allow,
const uint8_t expected_manifest[MOTA_MFL]);
} // namespace ota
} // namespace mesh