Merge adaptive RAK LoRa bootloader recovery

Merge application-owned signed bootloader recovery for the adaptive RAK3401 and RAK4631 profiles.
This commit is contained in:
mikecarper authored and GitHub committed 2026-09-27 00:32:19 -07:00
commit 2d03e098cf
17 files changed
+504 -41

No files matched your search

+39
View File
@@ -16,6 +16,15 @@ be replaced with the exact ABI-3 self-update-capable OTAFIX build over USB/BLE
DFU or SWD. MeshTower SD also requires the BLM2 retained-auth version to be
provisioned locally before either application or bootloader OTA.
The adaptive RAK3401 and RAK4631 loaders are an exception: their 2.4.8
application updater has ABI 2 and capability flags `0x16`, with no bootloader
self-update code. The two `*_repeater_unified_lora_ota` MeshCore builds now
include an application-owned recovery installer. Install that application
through its normal exact-base application mOTA path first. It can then fetch
an exact-board signed format-3 bootloader package from internal flash or a
matched external NOR and invoke Nordic's MBR itself. A node that cannot run
MeshCore and receive LoRa cannot use this path; use local UF2/DFU or SWD.
## Storage layouts
| Layout | Application limit | Staged boot package | Work area | Handoff / capability |
@@ -23,6 +32,36 @@ provisioned locally before either application or bootloader OTA.
| XIAO-module raw QSPI | below `0xE0000` | external QSPI offset 0 | dedicated internal `0xE0000..0xEA000` scratch | source `0x51`, flags `0x0E` |
| Qualified internal-flash target | normal `0xED000` flash limit; top 64 KiB SRAM reserved for application-delta staging | shared internal slot, exact start `0xE2000` | the same eleven-page slot; no second flash reservation | source `0xED`, flags `0x0A` |
| MeshTower V2 microSD | normal `0xED000` limit | contiguous `/meshcore-ota.mota` | dynamic internal `0xE0000..0xEA000` scratch; live image must end by `0xE0000` | source `0x53`, flags `0x09` |
| Adaptive RAK3401/RAK4631 recovery application | normal `0xED000` flash limit; live EndF and bank CRC must stop by `0xE2000` | internal `0xE2000` slot or exact matched NOR | internal `0xE2000..0xEC000` raw copy | application calls MBR; installed loader flags `0x16` |
For adaptive RAK recovery, `ota bootloader status` reports the installed
`3401_AUTO_DFU` or `4631_AUTO_DFU` identity. The host must build a signed
format-3 package from a *newer* exact-board OTAFIX image with the same `0x16`
capability marker and BLM2 continuity metadata. The package is full, so it
does not depend on the installed bootloader bytes. Its target IDs are
`D04AB3AB` and `FEEAFD1B`, respectively. The application requires a trusted
signer, matching board name/ID and SoftDevice layout, complete Merkle and
SHA-256 verification, explicit MID and image-hash confirmation, and a
hash-valid live EndF plus safe bank settings before writing scratch. It
copies each page with readback, hashes the final source again after the LoRa
reply drains, then calls `SD_MBR_COMMAND_COPY_BL`. The old bootloader remains
untouched until the MBR call. An installed 2.4.8 BLM2 loader rejects an equal
or older version; a 2.4.8 image is therefore not a recovery upgrade for it.
If scratch verification or the MBR call fails, the application resets without
the bootloader-update trigger and reports `blup:CE` on its next OTA status.
Build a candidate only from the matching **newer** OTAFIX bootloader HEX:
```bash
motatool build-bootloader --fw <rak3401-auto-bootloader.hex> \
--board wiscore_rak3401_auto --sign <trusted-private-key> \
--out <rak3401-recovery.mota>
# For RAK4631 use --board wiscore_rak4631_auto and its own exact-board HEX.
```
Verify the signed file with the corresponding public key, serve it over LoRa,
pull its MID manually, and run the same `ota bootloader install <MID8> <HASH16>`
confirmation shown below. A temporary or untrusted test signing key cannot
install on a node whose allowlist does not contain its public key.
The exact SD target is
`Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors`. Its normal
+1
View File
@@ -271,6 +271,7 @@ build_flags =
[nrf52_wisblock_auto_ota]
build_flags =
-D OTA_RAK_AUTO_STORE=1
-D OTA_RAK_BOOTLOADER_RECOVERY=1
-D OTA_QSPI_STORE=1
-D OTA_FLASH_STORE=1
-D OTA_QSPI_SHARED_WISBLOCK_SPI=1
+183
View File
@@ -18,6 +18,7 @@
#include "OtaSelf.h" // SelfFwInfo / ota_self_firmware (running-image base_hash gate)
#endif
#elif defined(NRF52_PLATFORM)
#include <SHA256.h>
#include "OtaVerify.h"
#include "OtaSelf.h"
#include "OtaFlashLayout_nrf52.h"
@@ -26,7 +27,9 @@
#include "nrf.h"
#include "nrf_soc.h"
#include "nrf_sdm.h"
#include "nrf_mbr.h"
#include "../nrf52/SoftDeviceState.h"
#include <stdlib.h>
#if defined(OTA_SD_STORE)
#include "OtaStoreSdNrf52.h"
#endif
@@ -468,6 +471,31 @@ static OtaStoreFlashNrf52* g_nrf52_apply_store = nullptr;
#if defined(OTA_QSPI_STORE)
static uint8_t g_nrf52_qspi_handoff = GPREGRET2_OTA_STAGE_QSPI;
#endif
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
// Set only after the exact-board signed image has been copied to the internal
// source and independently hashed there. Mesh drains the LoRa reply first.
static bool g_rak_recovery_ready = false;
static uint8_t g_rak_recovery_hash[32] = {0};
static bool ota_rak_recovery_source_hash_matches() {
SHA256 sha;
const uint8_t* image = (const uint8_t*)(uintptr_t)MOTA_NRF52_SHARED_BOOT_STAGE_START;
for (uint32_t off = 0; off < OTA_BOOT_IMAGE_SIZE; off += 512u)
sha.update(image + off, 512u);
uint8_t hash[32];
sha.finalize(hash, sizeof(hash));
return memcmp(hash, g_rak_recovery_hash, sizeof(hash)) == 0;
}
static bool ota_rak_mbr_addresses_ready() {
const uint32_t mbr_boot = *(const volatile uint32_t*)(uintptr_t)MBR_BOOTLOADER_ADDR;
const uint32_t mbr_params = *(const volatile uint32_t*)(uintptr_t)MBR_PARAM_PAGE_ADDR;
const uint32_t boot = mbr_boot == UINT32_MAX ? NRF_UICR->NRFFW[0] : mbr_boot;
const uint32_t params = mbr_params == UINT32_MAX ? NRF_UICR->NRFFW[1] : mbr_params;
return boot == OTA_BOOT_IMAGE_START &&
params == OTA_BOOT_IMAGE_START + OTA_BOOT_IMAGE_SIZE;
}
#endif
static void ota_nrf52_set_reset_handoff(uint8_t request, uint8_t source) {
uint8_t sd_en = 0;
@@ -540,6 +568,40 @@ void ota_reboot_to_apply() { // public: set the apply magic +
}
void ota_reboot_to_bootloader_update() {
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
if (g_rak_recovery_ready) {
// COPY_BL succeeds by resetting directly into the new bootloader. Never
// hand 0x6B to an older adaptive loader: it cannot consume this package.
if (!ota_rak_mbr_addresses_ready() ||
!ota_rak_recovery_source_hash_matches()) {
g_rak_recovery_ready = false;
ota_nrf52_set_reset_handoff(0u, GPREGRET2_RAK_RECOVERY_FAILED);
NVIC_SystemReset();
return;
}
uint8_t sd_enabled = 0;
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS ||
(sd_enabled && sd_softdevice_disable() != NRF_SUCCESS)) {
g_rak_recovery_ready = false;
ota_nrf52_set_reset_handoff(0u, GPREGRET2_RAK_RECOVERY_FAILED);
NVIC_SystemReset();
return;
}
sd_mbr_command_t command = {};
command.command = SD_MBR_COMMAND_COPY_BL;
command.params.copy_bl.bl_src =
(uint32_t*)(uintptr_t)MOTA_NRF52_SHARED_BOOT_STAGE_START;
command.params.copy_bl.bl_len = OTA_BOOT_IMAGE_SIZE / sizeof(uint32_t);
NRF_POWER->GPREGRET = 0u;
NRF_POWER->GPREGRET2 = 0u;
(void)sd_mbr_command(&command); // success never returns
g_rak_recovery_ready = false;
NRF_POWER->GPREGRET = 0u;
NRF_POWER->GPREGRET2 = GPREGRET2_RAK_RECOVERY_FAILED;
NVIC_SystemReset();
return;
}
#endif
uint8_t source = GPREGRET2_OTA_STAGE_QSPI;
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
source = GPREGRET2_OTA_STAGE_EXPANDED;
@@ -1014,6 +1076,127 @@ static const char* ota_bootloader_continuity_error(OtaBootloaderContinuityGate g
}
}
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
bool ota_prepare_rak_bootloader_recovery(OtaStore& store, bool internal,
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) {
st = ApplyState();
g_rak_recovery_ready = false;
const uint32_t total = store.staged_size();
uint8_t header[8], manifest[MOTA_MFL];
MotaManifest m;
if (total != MOTA_NRF52_BOOT_CONTAINER_SIZE ||
!store.read(0, header, sizeof(header)) ||
memcmp(header, MOTA_MAGIC, sizeof(MOTA_MAGIC)) != 0 ||
rd_u32le(header + 4) != total ||
!store.read(8, manifest, sizeof(manifest)) ||
!mota_parse_manifest(manifest, sizeof(manifest), m)) {
strcpy(msg, "recovery bootloader container is invalid"); return false;
}
if (ota_bootloader_confirmation_gate(m, installed, actual_mid, operator_mid,
operator_hash8) != OTA_BOOT_CONFIRM_OK) {
strcpy(msg, "recovery bootloader identity/MID/hash confirmation failed"); return false;
}
const OtaBlCaps current = ota_bootloader_app_caps();
const uint8_t rak_flags = OTA_BL_PROFILE_RAK_AUTO;
if (!current.present || current.storage_flags != rak_flags ||
current.apply_abi < MOTA_APP_FORMAT_VER ||
(current.codec_mask & OTA_BL_REQUIRED_APP_CODEC_MASK) !=
OTA_BL_REQUIRED_APP_CODEC_MASK) {
strcpy(msg, "installed bootloader is not the adaptive RAK OTAFIX profile"); return false;
}
SelfFwInfo fi;
const uint32_t app_base = mota_nrf52_app_base();
if (!ota_self_firmware(fi) ||
!ota_bootloader_scratch_headroom_valid(
fi.valid, app_base, fi.image_len, MOTA_NRF52_SHARED_BOOT_STAGE_START) ||
!ota_bootloader_live_bank_preserves_scratch(
app_base, fi.image_len, MOTA_NRF52_SHARED_BOOT_STAGE_START) ||
ota_nrf52_effective_stage_ceiling() != MOTA_NRF52_APP_END ||
!ota_rak_mbr_addresses_ready()) {
strcpy(msg, "running app/settings or MBR do not preserve bootloader scratch"); return false;
}
if (internal) {
OtaStoreFlashNrf52& flash = static_cast<OtaStoreFlashNrf52&>(store);
if (flash.write_start() != MOTA_NRF52_SHARED_BOOT_STAGE_START ||
!flash.data() || flash.is_hybrid()) {
strcpy(msg, "internal recovery container placement is invalid"); return false;
}
}
const uint32_t payload_off = 8u + MOTA_MFL + (uint32_t)m.block_count * 4u;
if (payload_off != 365u || payload_off + OTA_BOOT_IMAGE_SIZE + 5u != total) {
strcpy(msg, "recovery payload layout is invalid"); return false;
}
mesh::Identity signer(m.signer_pubkey);
if (!signer.verify(m.signature, m.manifest_start, (int)m.signed_len) ||
!allow.contains(m.signer_pubkey)) {
strcpy(msg, "recovery bootloader signer is not trusted"); return false;
}
const VerifyResult verified = ota_verify(store, allow, manifest);
st.manifest_ok = verified.parsed;
st.sig_ok = verified.sig_ok;
st.trusted = verified.trusted;
st.image_size = m.image_size;
memcpy(st.image_hash, m.image_hash, sizeof(st.image_hash));
if (!verified.auto_appliable() || !verified.container_hash_ok) {
strcpy(msg, "recovery bootloader package failed full verification"); return false;
}
uint8_t vectors[8];
OtaBootloaderIdentity candidate;
OtaBootloaderCapsMarker candidate_caps;
if (!store.read(payload_off, vectors, sizeof(vectors)) ||
!ota_bootloader_vector_sane(vectors) ||
!ota_bootloader_external_image_metadata(
store, payload_off, rak_flags, candidate, candidate_caps, false) ||
!ota_bootloader_identity_matches(installed, candidate) ||
candidate_caps.apply_abi < current.apply_abi ||
(candidate_caps.codec_mask & current.codec_mask) != current.codec_mask) {
strcpy(msg, "recovery candidate identity/capabilities/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(), app_base, OTA_BOOT_CONTINUITY_LAYOUT_ABI);
if (continuity != OTA_BOOT_CONTINUITY_OK) {
strncpy(msg, ota_bootloader_continuity_error(continuity), 96);
msg[95] = 0; return false;
}
uint8_t* page = static_cast<uint8_t*>(malloc(MOTA_NRF52_FLASH_PAGE));
if (!page) { strcpy(msg, "not enough RAM for recovery copy"); return false; }
bool copied = true;
for (uint32_t off = 0; off < OTA_BOOT_IMAGE_SIZE; off += MOTA_NRF52_FLASH_PAGE) {
const uint32_t dest = MOTA_NRF52_SHARED_BOOT_STAGE_START + off;
// Internal packages overlap the destination by 365 bytes. Read the next
// complete source page before erasing the lower destination page.
if (!store.read(payload_off + off, page, MOTA_NRF52_FLASH_PAGE) ||
flash_nrf5x_write(dest, page, MOTA_NRF52_FLASH_PAGE) < 0) {
copied = false; break;
}
flash_nrf5x_flush();
if (memcmp((const void*)(uintptr_t)dest, page, MOTA_NRF52_FLASH_PAGE) != 0) {
copied = false; break;
}
}
free(page);
if (!copied) {
strcpy(msg, "recovery scratch copy/readback failed; bootloader unchanged"); return false;
}
memcpy(g_rak_recovery_hash, m.image_hash, sizeof(g_rak_recovery_hash));
if (!ota_rak_recovery_source_hash_matches()) {
strcpy(msg, "recovery scratch hash mismatch; bootloader unchanged"); return false;
}
st.slot_ok = true;
g_rak_recovery_ready = true;
strcpy(msg, "signed bootloader copied and verified; MBR install follows this reply");
return true;
}
#endif
#if defined(OTA_SD_BOOTLOADER_UPDATE)
static bool external_bootloader_approve(OtaStoreSdNrf52& store,
const uint8_t image_hash[32],
+15
View File
@@ -15,6 +15,8 @@
namespace mesh {
namespace ota {
class OtaStore;
struct ApplyState {
bool manifest_ok = false;
bool sig_ok = false;
@@ -131,6 +133,19 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreFlashNrf52& store,
ApplyState& st, char* msg);
#endif
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
// Application-owned recovery for adaptive RAK loaders lacking BOOT_UPDATE.
// Copies a verified full bootloader from either active store into internal
// scratch; deferred commit invokes Nordic's MBR after the LoRa reply drains.
bool ota_prepare_rak_bootloader_recovery(OtaStore& store, bool internal,
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
// Commit the (already approved/armed) update and reboot into it - does NOT return. Call this only after
// a successful ota_apply_* AND after the confirmation reply has been delivered, so the operator knows
// the apply started (over LoRa the device then goes silent while the bootloader applies). nRF52: set
+3
View File
@@ -93,6 +93,9 @@ static const uint8_t OTA_BL_PROFILE_INTERNAL_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;
static const uint8_t OTA_BL_PROFILE_RAK_AUTO =
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI |
OTA_BL_STORAGE_HEADER_W25;
// 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;
+21 -6
View File
@@ -32,6 +32,17 @@
#endif
#endif
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
#if !defined(NRF52_PLATFORM) || !defined(OTA_RAK_AUTO_STORE) || \
!defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || !defined(OTA_FLASH_STORE) || \
!defined(OTA_QSPI_STORE)
#error "RAK application recovery requires the adaptive nRF52 shared-slot profile"
#endif
#if !defined(RAK_3401) && !defined(RAK_4631)
#error "RAK application recovery is restricted to the two qualified RAK profiles"
#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"
@@ -512,17 +523,19 @@ inline bool ota_bootloader_caps_marker_parse(const uint8_t raw[16],
inline bool ota_bootloader_caps_from_image(const uint8_t* image, size_t image_size,
uint8_t exact_storage_flags,
OtaBootloaderCapsMarker& out) {
OtaBootloaderCapsMarker& out,
bool require_boot_update = true) {
out = OtaBootloaderCapsMarker();
if (!image || exact_storage_flags == 0) return false;
uint8_t valid_count = 0;
for (size_t off = 0; off + 16u <= image_size; off += 4u) {
OtaBootloaderCapsMarker parsed;
if (!ota_bootloader_caps_marker_parse(image + off, parsed) ||
parsed.apply_abi < MOTA_BOOT_FORMAT_VER ||
parsed.apply_abi < (require_boot_update ? MOTA_BOOT_FORMAT_VER : MOTA_APP_FORMAT_VER) ||
(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;
((parsed.storage_flags & OTA_BL_STORAGE_BOOT_UPDATE) != 0u) !=
require_boot_update) continue;
if (++valid_count != 1u) return false; // privileged marker identity must be unambiguous
if (parsed.storage_flags != exact_storage_flags) continue;
out = parsed;
@@ -556,7 +569,8 @@ inline bool ota_bootloader_external_crc_ok(
template <typename Store>
inline bool ota_bootloader_external_image_metadata(
Store& store, uint32_t payload_off, uint8_t exact_storage_flags,
OtaBootloaderIdentity& candidate, OtaBootloaderCapsMarker& caps) {
OtaBootloaderIdentity& candidate, OtaBootloaderCapsMarker& caps,
bool require_boot_update = true) {
candidate = OtaBootloaderIdentity();
caps = OtaBootloaderCapsMarker();
if (exact_storage_flags == 0u) return false;
@@ -576,10 +590,11 @@ inline bool ota_bootloader_external_image_metadata(
if ((absolute & 3u) == 0u && local + 16u <= len) {
OtaBootloaderCapsMarker parsed;
if (ota_bootloader_caps_marker_parse(buf + local, parsed) &&
parsed.apply_abi >= MOTA_BOOT_FORMAT_VER &&
parsed.apply_abi >= (require_boot_update ? MOTA_BOOT_FORMAT_VER : MOTA_APP_FORMAT_VER) &&
(parsed.codec_mask & OTA_BL_REQUIRED_APP_CODEC_MASK) ==
OTA_BL_REQUIRED_APP_CODEC_MASK &&
(parsed.storage_flags & OTA_BL_STORAGE_BOOT_UPDATE) != 0u) {
((parsed.storage_flags & OTA_BL_STORAGE_BOOT_UPDATE) != 0u) ==
require_boot_update) {
if (++valid_caps != 1u) return false;
if (parsed.storage_flags == exact_storage_flags) caps = parsed;
}
+34 -2
View File
@@ -400,11 +400,17 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
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_update);
h->target_id == ota_bootloader_target_id(bid);
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
installable = installable &&
list_bl.storage_flags == OTA_BL_PROFILE_RAK_AUTO &&
(c.fetch_store.usesExternal() || c.fetch_store.usesInternal());
#else
installable = installable && ota_bootloader_self_update_caps_valid(list_bl_update);
#if defined(OTA_SD_BOOTLOADER_UPDATE)
installable = installable && list_sd_headroom;
#endif
#endif
#endif
fit = installable ? "yours" : "bootloader unsupported";
} else if (myt && h->target_id == myt) {
@@ -541,7 +547,11 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
defined(OTA_SD_BOOTLOADER_UPDATE))
const OtaBootloaderIdentity& bid = c.bootloaderIdentity();
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
const OtaBlCaps& bl = c.bootloaderAppCaps();
#else
const OtaBlCaps& bl = c.bootloaderUpdateCaps();
#endif
if (selflags != (MFLAG_FULL | MFLAG_SIGNED | MFLAG_BOOTLOADER) ||
selcodec != CODEC_FULL) {
strcpy(reply, "ERR malformed bootloader catalog row; capture it to folder for inspection");
@@ -551,10 +561,17 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
strcpy(reply, "ERR bootloader package does not match this installed bootloader identity");
return true;
}
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
if (bl.storage_flags != OTA_BL_PROFILE_RAK_AUTO) {
strcpy(reply, "ERR installed bootloader is not the adaptive RAK profile");
return true;
}
#else
if (!ota_bootloader_self_update_caps_valid(bl)) {
strcpy(reply, "ERR installed bootloader lacks safe LoRa bootloader-update support");
return true;
}
#endif
#if defined(OTA_SD_BOOTLOADER_UPDATE)
SelfFwInfo sd_self;
if (!ota_self_firmware(sd_self) ||
@@ -579,13 +596,21 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
return true;
}
#if defined(NRF52_PLATFORM)
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
const OtaBlCaps& bl = c.bootloaderAppCaps();
#else
const OtaBlCaps& bl = selboot ? c.bootloaderUpdateCaps()
: c.bootloaderAppCaps();
#endif
if (!bl.present) {
strcpy(reply, "ERR bootloader has no mOTA apply support; update it over USB first");
return true;
}
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
const uint8_t need_abi = MOTA_APP_FORMAT_VER;
#else
const uint8_t need_abi = selboot ? MOTA_BOOT_FORMAT_VER : MOTA_APP_FORMAT_VER;
#endif
if (bl.apply_abi < need_abi || selcodec >= 16 || !(bl.codec_mask & (1u << selcodec))) {
snprintf(reply, 160, "ERR bootloader cannot apply mOTA ABI %u codec %u (has abi=%u codecs=0x%x)",
need_abi, (unsigned)selcodec, bl.apply_abi, bl.codec_mask);
@@ -864,10 +889,17 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
strcpy(reply, "Bootloader update unavailable: installed embedded manifest/CRC is invalid");
return true;
}
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
if (bl.storage_flags != OTA_BL_PROFILE_RAK_AUTO) {
strcpy(reply, "Bootloader recovery unavailable: installed loader is not adaptive RAK OTAFIX");
return true;
}
#else
if (!ota_bootloader_self_update_caps_valid(bl)) {
strcpy(reply, "Bootloader update unavailable: installed bootloader supports application OTA only");
return true;
}
#endif
MotaManifest staged;
char midhx[9] = "-", hashhx[17] = "-";
const bool ready = c.manager.fetchState() == OtaManager::COMPLETE &&
+21
View File
@@ -359,6 +359,19 @@ struct OtaContext {
}
const OtaBootloaderIdentity& installed = bootloaderIdentity();
#if defined(OTA_RAK_AUTO_STORE)
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
if (!fetch_store.usesInternal() && !fetch_store.usesExternal()) {
strncpy(msg, fetch_store.selectionReason(), 96);
msg[95] = 0; return false;
}
const bool internal = fetch_store.usesInternal();
OtaStore& recovery_store = internal
? static_cast<OtaStore&>(fetch_store.internalStore())
: static_cast<OtaStore&>(fetch_store.externalStore());
bool ok = ota_prepare_rak_bootloader_recovery(
recovery_store, internal, allow, installed,
manager.fetchManifestId(), operator_mid, operator_hash8, apply_st, msg);
#else
if (!fetch_store.usesInternal()) {
strncpy(msg, "bootloader LoRa update needs the internal OTAFIX profile", 96);
msg[95] = 0; return false;
@@ -366,6 +379,7 @@ struct OtaContext {
bool ok = ota_prepare_bootloader_update_nrf52(
fetch_store.internalStore(), allow, installed, manager.fetchManifestId(), operator_mid,
operator_hash8, apply_st, msg);
#endif
#else
bool ok = ota_prepare_bootloader_update_nrf52(
fetch_store, allow, installed, manager.fetchManifestId(), operator_mid,
@@ -704,8 +718,15 @@ struct OtaContext {
(defined(OTA_QSPI_BOOTLOADER_UPDATE) || defined(OTA_INTERNAL_BOOTLOADER_UPDATE) || \
defined(OTA_SD_BOOTLOADER_UPDATE))
#if defined(OTA_RAK_AUTO_STORE)
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
manager.set_accept_bootloader(
(fetch_store.usesInternal() || fetch_store.usesExternal()) &&
bootloaderIdentity().crc_ok &&
bootloaderAppCaps().storage_flags == OTA_BL_PROFILE_RAK_AUTO);
#else
manager.set_accept_bootloader(fetch_store.usesInternal() &&
ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps()));
#endif
#else
manager.set_accept_bootloader(true);
#endif
+1
View File
@@ -52,6 +52,7 @@ static const uint32_t MOTA_NRF52_FLASH_PAGE = 4096u;
static const uint32_t MOTA_NRF52_CONTAINER_MIN_SIZE = 8u + 197u + 5u;
static const uint8_t GPREGRET_OTA_APPLY = 0x6Au; // distinct from DFU magics 0x57/0x4E/0xA8
static const uint8_t GPREGRET_OTA_BOOTLOADER_UPDATE = 0x6Bu;
static const uint8_t GPREGRET2_RAK_RECOVERY_FAILED = 0xCEu;
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;
+29 -5
View File
@@ -4,6 +4,7 @@
#include "OtaSelf.h"
#include "OtaBlInfo.h"
#include "OtaBootloaderUpdate.h"
#include "MotaContainer.h"
#include "OtaDebug.h"
#include "OtaByteIO.h" // align_down / rd_u32le (flash-page geometry + header read)
@@ -154,7 +155,11 @@ bool OtaStoreFlashNrf52::plan_layout(bool is_full, uint32_t image_size,
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
if (!is_full || image_size != 40u * 1024u || payload_size != 40u * 1024u ||
payload_off != 365u || total64 != MOTA_NRF52_BOOT_CONTAINER_SIZE ||
!ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps()))
!(ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps())
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
|| ota_bootloader_app_caps().storage_flags == OTA_BL_PROFILE_RAK_AUTO
#endif
))
return false;
// This is the release-blocking no-EndF gate: package kind is known before
@@ -165,7 +170,12 @@ bool OtaStoreFlashNrf52::plan_layout(bool is_full, uint32_t image_size,
const uint32_t app_base = mota_nrf52_app_base();
if (!ota_self_firmware(fi) || !fi.valid || fi.image_len > UINT32_MAX - app_base ||
!mota_nrf52_shared_boot_stage_plan(
(uint32_t)total64, app_base, true, app_base + fi.image_len, start))
(uint32_t)total64, app_base, true, app_base + fi.image_len, start)
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
|| !ota_bootloader_live_bank_preserves_scratch(
app_base, fi.image_len, MOTA_NRF52_SHARED_BOOT_STAGE_START)
#endif
)
return false;
_planned_bootloader = true;
_planned_total = (uint32_t)total64;
@@ -345,7 +355,12 @@ bool OtaStoreFlashNrf52::begin(uint32_t total_size) {
!ota_self_firmware(fi) || !fi.valid || fi.image_len > UINT32_MAX - app_base ||
!mota_nrf52_shared_boot_stage_plan(
total_size, app_base, true, app_base + fi.image_len, checked_start) ||
checked_start != planned_start)
checked_start != planned_start
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
|| !ota_bootloader_live_bank_preserves_scratch(
app_base, fi.image_len, MOTA_NRF52_SHARED_BOOT_STAGE_START)
#endif
)
return false;
app_end = app_base + fi.image_len;
#else
@@ -597,10 +612,19 @@ bool OtaStoreFlashNrf52::reopenFor(const uint8_t* want_mid, uint32_t expected_ta
if (bootloader) {
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
SelfFwInfo fi;
if (!ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps()) ||
if (!(ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps())
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
|| ota_bootloader_app_caps().storage_flags == OTA_BL_PROFILE_RAK_AUTO
#endif
) ||
!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, want) || want != start)
total, app_base, true, app_base + fi.image_len, want) || want != start
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
|| !ota_bootloader_live_bank_preserves_scratch(
app_base, fi.image_len, MOTA_NRF52_SHARED_BOOT_STAGE_START)
#endif
)
continue;
#else
continue;
+26 -2
View File
@@ -4,6 +4,10 @@
#include "OtaByteIO.h"
#include "OtaFlashLayout_nrf52.h"
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
#include "OtaBootloaderUpdate.h"
#include "OtaSelf.h"
#endif
#include "hal/nrf_gpio.h"
#include "hal/nrf_qspi.h"
#include "nrf.h"
@@ -885,8 +889,9 @@ uint32_t OtaStoreQspiNrf52::capacity() const {
return result;
}
bool OtaStoreQspiNrf52::plan_layout(bool, uint32_t image_size, uint32_t,
uint32_t payload_size, bool) {
bool OtaStoreQspiNrf52::plan_layout(bool is_full, uint32_t image_size,
uint32_t payload_off, uint32_t payload_size,
bool is_bootloader) {
// This is the start of a new manifest admission attempt. Clear diagnostics
// left by the normal empty-store reopen probe, then latch the first error
// from this attempt until the operator reads it or starts another attempt.
@@ -895,6 +900,25 @@ bool OtaStoreQspiNrf52::plan_layout(bool, uint32_t image_size, uint32_t,
_stage = OtaQspiStage::IDLE;
const uint32_t app_base = mota_nrf52_app_base();
const uint32_t app_ceiling = mota_nrf52_application_ceiling();
#if defined(OTA_RAK_BOOTLOADER_RECOVERY)
if (is_bootloader) {
SelfFwInfo fi;
if (!is_full || image_size != OTA_BOOT_IMAGE_SIZE ||
payload_size != OTA_BOOT_IMAGE_SIZE || payload_off != 365u ||
ota_nrf52_effective_stage_ceiling() != MOTA_NRF52_APP_END ||
!ota_self_firmware(fi) ||
!ota_bootloader_scratch_headroom_valid(
fi.valid, app_base, fi.image_len, MOTA_NRF52_SHARED_BOOT_STAGE_START) ||
!ota_bootloader_live_bank_preserves_scratch(
app_base, fi.image_len, MOTA_NRF52_SHARED_BOOT_STAGE_START)) {
fail("recovery scratch is unavailable");
releaseFlash();
return false;
}
}
#else
(void)is_full; (void)payload_off; (void)is_bootloader;
#endif
if (image_size == 0 || payload_size == 0 || app_base >= app_ceiling ||
image_size > app_ceiling - app_base) {
fail("image exceeds nRF52 application region");
+20
View File
@@ -278,6 +278,26 @@ TEST(OtaBootPackage, CandidateContinuityEnvelopeHasOneCanonicalFinalOffset) {
store, 0, OTA_BL_PROFILE_SD_BOOT_UPDATE, identity, caps));
EXPECT_EQ(identity.manifest_offset, OTA_BOOT_CANDIDATE_MANIFEST_OFFSET);
// Adaptive RAK recovery is authenticated by the application, so its
// candidate retains ABI 2 and the exact 0x16 application capability marker.
auto rak_recovery = canonical;
rak_recovery[0x400u + 8u] = MOTA_APP_FORMAT_VER;
rak_recovery[0x400u + 12u] = OTA_BL_STORAGE_STAGE_CEILING |
OTA_BL_STORAGE_QSPI |
OTA_BL_STORAGE_HEADER_W25;
wr_u32le(rak_recovery.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40u,
0u);
wr_u32le(rak_recovery.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40u,
ota_boot_image_crc32(rak_recovery.data(), rak_recovery.size(),
OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40u));
ASSERT_TRUE(store.begin((uint32_t)rak_recovery.size()));
ASSERT_TRUE(store.write(0, rak_recovery.data(), (uint32_t)rak_recovery.size()));
EXPECT_TRUE(ota_bootloader_external_image_metadata(
store, 0, rak_recovery[0x400u + 12u], identity, caps, false));
EXPECT_EQ(caps.apply_abi, MOTA_APP_FORMAT_VER);
EXPECT_FALSE(ota_bootloader_external_image_metadata(
store, 0, rak_recovery[0x400u + 12u], identity, caps));
// A second CRC-valid base identity still counts when its adjacent BLM2
// extension is only half present. These coupled values are the fixed point
// for this deterministic image. Counting continuity first would wrongly
+12 -6
View File
@@ -149,7 +149,7 @@ MIN_MESHCLI_VERSION = (1, 6, 0)
# Tested format-3 inspect/verify/serve support. Keep automatic repairs pinned;
# neither a package nor a moving branch may choose code to install on the host.
MOTATOOL_REPAIR_REPOSITORY = "https://github.com/mikecarper/motatool.git"
MOTATOOL_REPAIR_REVISION = "8c38369e7d35ad50cf74261869676d52dd24adf7"
MOTATOOL_REPAIR_REVISION = "beff192aec341077f6d9d4b4938e47323775b9eb"
MOTATOOL_REPAIR_TIMEOUT_SECONDS = 60 * 60
# The pinned lockfile, not motatool's older package-level rust-version: zeroize
# needs edition 2024 (1.85), and idna_adapter/ICU 2.2 require Rust 1.86.
@@ -1085,11 +1085,15 @@ def compatible_mota(info: MotaInfo, target: TargetInfo) -> tuple[bool, str]:
f"bootloader target {info.target_id:08X} hw={info.hw_id}, need "
f"{target.boot_target_id:08X} hw={target.boot_hw_id}"
)
if target.bootloader_abi is None or target.bootloader_abi < 3:
return False, "bootloader self-update requires installed ABI 3 or newer"
recovery = (target.boot_storage == 0x16 and
info.bootloader_storage == 0x16)
if target.bootloader_abi is None or target.bootloader_abi < (2 if recovery else 3):
return False, "bootloader install requires a capable installed OTAFIX profile"
if target.bootloader_codecs != 0x5:
return False, "bootloader self-update requires FULL|INPLACE codecs 0x5"
expected_storage = 0x09 if target.nrf_sd else 0x0E if target.nrf_qspi else 0x0A
expected_storage = (0x16 if recovery else
0x09 if target.nrf_sd else
0x0E if target.nrf_qspi else 0x0A)
if (
target.boot_storage != expected_storage
or info.bootloader_storage != expected_storage
@@ -2765,7 +2769,7 @@ def query_bootloader_target(
if match is None:
raise OtaError(
f"destination cannot stage a bootloader update: {reply}; "
"requires exact-board ABI-3 self-update bootloader and capable application"
"requires an exact-board update or adaptive RAK recovery application"
)
board, target_id, name, _crc, abi, caps = match.groups()
library = bootloader_library()
@@ -2779,7 +2783,9 @@ def query_bootloader_target(
raise OtaError(f"invalid destination bootloader identity: {exc}") from exc
if derived_target != int(target_id, 16):
raise OtaError("destination bootloader board/name does not match its target ID")
if int(abi) < 3 or int(abi) != target.bootloader_abi:
recovery = int(caps, 16) == 0x16 and (int(board, 16), name) in \
library.RAK_AUTO_RECOVERY_IDENTITIES
if int(abi) < (2 if recovery else 3) or int(abi) != target.bootloader_abi:
raise OtaError("destination reports inconsistent or unsupported bootloader ABI")
return replace(
target, boot_target_id=derived_target, boot_hw_id=hw_id,
+28 -4
View File
@@ -21,7 +21,7 @@ import lora_ota as ota
from test_lora_ota import firmware, mota_blob, prepare_args, target, VERSION_NEW
def boot_blob(*, storage=0x0A, version=0x0117010D):
def boot_blob(*, storage=0x0A, version=0x0117010D, rak_name="3401_AUTO_DFU"):
"""Build a signed test-only candidate using the canonical reference builder."""
ml = ota.bootloader_library()
board, name, app_base = 0x239A0029, "3401_DFU", ml.NRF52_APP_BASE_S140_V6
@@ -30,9 +30,12 @@ def boot_blob(*, storage=0x0A, version=0x0117010D):
name = ml.XIAO_BOOT_DEVICE_NAME.rstrip(b"\0").decode("ascii")
elif storage == 0x09:
board, name = 0x239A0071, "TOWER_V2_OTA"
elif storage == 0x16:
board, name = 0x239A0029, rak_name
image = bytearray(b"\xff" * ml.XIAO_BOOT_IMAGE_SIZE)
struct.pack_into("<II", image, 0, 0x20040000, ml.XIAO_BOOT_IMAGE_START + 0x101)
struct.pack_into("<8sHHB3x", image, 0x80, ml.XIAO_BOOT_CAPS_MAGIC, 3, 5, storage)
struct.pack_into("<8sHHB3x", image, 0x80, ml.XIAO_BOOT_CAPS_MAGIC,
2 if storage == 0x16 else 3, 5, storage)
offset = ml.BOOT_CANDIDATE_MANIFEST_OFFSET
struct.pack_into(
"<8sHHIII16sI", image, offset, ml.XIAO_BOOT_MANIFEST_MAGIC,
@@ -59,7 +62,8 @@ def boot_blob(*, storage=0x0A, version=0x0117010D):
def boot_target(package):
return replace(
target(platform="nrf52", boot_codecs=5), bootloader_abi=3,
target(platform="nrf52", boot_codecs=5),
bootloader_abi=2 if package.bootloader_storage == 0x16 else 3,
boot_target_id=package.target_id, boot_hw_id=package.hw_id,
boot_storage=package.bootloader_storage,
nrf_sd=package.bootloader_storage == 9,
@@ -82,11 +86,31 @@ class BootloaderStagingTests(unittest.TestCase):
self.assertEqual(package.payload_size, 40960)
def test_all_supported_storage_profiles(self):
for storage in (10, 14, 9):
for storage in (10, 14, 9, 22):
with self.subTest(storage=storage):
package = ota.parse_mota(boot_blob(storage=storage))
self.assertEqual(ota.compatible_mota(package, boot_target(package)), (True, ""))
def test_adaptive_rak_recovery_accepts_internal_and_nor_storage(self):
for name in ("3401_AUTO_DFU", "4631_AUTO_DFU"):
package = ota.parse_mota(boot_blob(storage=0x16, rak_name=name))
for external in (False, True):
live = replace(boot_target(package), nrf_qspi=external)
self.assertEqual(ota.compatible_mota(package, live), (True, ""))
live = replace(boot_target(package), bootloader_abi=1)
self.assertFalse(ota.compatible_mota(package, live)[0])
def test_adaptive_rak_bootloader_status_uses_app_owned_recovery(self):
controller = mock.Mock()
controller.remote_command.return_value = (
"BL board=239A0029 target=D04AB3AB name=3401_AUTO_DFU "
"crc=12345678 abi=2 caps=16 | staged:none mid=- hash=-"
)
live = replace(target(platform="nrf52", boot_codecs=5), bootloader_abi=2)
result = ota.query_bootloader_target(controller, live)
self.assertEqual(result.boot_target_id, 0xD04AB3AB)
self.assertEqual(result.boot_storage, 0x16)
def test_malformed_bootloaders_fail_closed(self):
for offset in (9, 10, 11, 27, 28, 105, 137, 201, 205, 600):
with self.subTest(offset=offset):
+7 -4
View File
@@ -63,9 +63,11 @@ targeting a deployed receiver that predates the extended transport descriptor.
Passing `bootloader=True` is a deliberately narrow reference-only path: it
requires a nonzero version, a signed exact 40 KiB OTAFIX region, 1024-byte
blocks, full codec, zero base hash, a sane vector table, an exact embedded
manifest/CRC identity, and an ABI-3 marker retaining both application codecs
(`FULL|INPLACE`, mask `0x0005`), the selected storage, and boot-update
capabilities. Deployed XIAO packages retain their
manifest/CRC identity, and a marker retaining both application codecs
(`FULL|INPLACE`, mask `0x0005`). Self-update profiles require ABI 3 and the
exact boot-update storage capability. The two adaptive RAK recovery profiles
use ABI 2 and exact `0x16` application capabilities; their MeshCore
application verifies and copies the image through the MBR. Deployed XIAO packages retain their
`XIAO_BL_28860044`/`XIAO_BL_28860045` IDs. Generic packages derive the padded
`NRF_BL_<BOARD_ID>_<DEVICE_NAME>` ID and collision-checked wire target from the
full embedded manifest pair. Parsing and `verify()` repeat those gates; magic
@@ -76,7 +78,8 @@ builder accepts only identities in the qualified inventory. That inventory
currently covers the shared-internal nRF52840 targets, the two deployed XIAO
raw-QSPI identities, and the exact MeshTower V2 identity whose candidate
marker may select either its internal (`0x0A`) or microSD (`0x09`) application
layout. The test suite verifies those boot target IDs are unique and disjoint
layout, plus the two exact adaptive RAK identities (`3401_AUTO_DFU` and
`4631_AUTO_DFU`). The test suite verifies those boot target IDs are unique and disjoint
from the generated application target table.
This library does not authorize a device update. A capable node will only arm
+28 -8
View File
@@ -108,11 +108,15 @@ BOOT_STORAGE_SD = 0x01
BOOT_STORAGE_STAGE_CEILING = 0x02
XIAO_BOOT_STORAGE_QSPI = 0x04
XIAO_BOOT_STORAGE_UPDATE = 0x08
BOOT_STORAGE_KNOWN = 0x0F
BOOT_STORAGE_KNOWN = 0x1F
BOOT_STORAGE_HEADER_W25 = 0x10
BOOT_STORAGE_SD_UPDATE = BOOT_STORAGE_SD | XIAO_BOOT_STORAGE_UPDATE
BOOT_STORAGE_QSPI_UPDATE = (BOOT_STORAGE_STAGE_CEILING | XIAO_BOOT_STORAGE_QSPI |
XIAO_BOOT_STORAGE_UPDATE)
BOOT_STORAGE_INTERNAL_UPDATE = BOOT_STORAGE_STAGE_CEILING | XIAO_BOOT_STORAGE_UPDATE
BOOT_STORAGE_RAK_AUTO_RECOVERY = (BOOT_STORAGE_STAGE_CEILING |
XIAO_BOOT_STORAGE_QSPI |
BOOT_STORAGE_HEADER_W25)
BOOT_REQUIRED_APP_CODEC_MASK = ((1 << CODEC_FULL) |
(1 << CODEC_DETOOLS_INPLACE))
@@ -138,6 +142,13 @@ INTERNAL_BOOTLOADER_IDENTITIES = (
(0x239A0029, "RTAG_DFU"),
)
# These adaptive loaders do not self-update. Their exact MeshCore application
# authenticates a format-3 package and uses the MBR to replace the bootloader.
RAK_AUTO_RECOVERY_IDENTITIES = (
(0x239A0029, "3401_AUTO_DFU"),
(0x239A0029, "4631_AUTO_DFU"),
)
# MeshTower V2 has both a lean internal-store application and an exact SD-store
# application. They intentionally retain the same embedded bootloader identity
# and signed wire target; the candidate capability marker selects the matching
@@ -696,7 +707,7 @@ def audit_bootloader_target_inventory(application_target_ids=()) -> dict:
identities = (
(XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU"),
) + INTERNAL_BOOTLOADER_IDENTITIES
) + INTERNAL_BOOTLOADER_IDENTITIES + RAK_AUTO_RECOVERY_IDENTITIES
app_ids = set(application_target_ids)
targets = {}
for identity in identities:
@@ -714,6 +725,7 @@ def audit_bootloader_target_inventory(application_target_ids=()) -> dict:
def bootloader_identity_is_buildable(board_id: int, device_name: str) -> bool:
return ((board_id, device_name) in INTERNAL_BOOTLOADER_IDENTITIES or
(board_id, device_name) in RAK_AUTO_RECOVERY_IDENTITIES or
(board_id, device_name) in (
(XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU")))
@@ -743,6 +755,8 @@ def bootloader_qualified_storage_profiles(board_id: int, device_name: str):
return (BOOT_STORAGE_INTERNAL_UPDATE, BOOT_STORAGE_SD_UPDATE)
if identity in INTERNAL_BOOTLOADER_IDENTITIES:
return (BOOT_STORAGE_INTERNAL_UPDATE,)
if identity in RAK_AUTO_RECOVERY_IDENTITIES:
return (BOOT_STORAGE_RAK_AUTO_RECOVERY,)
return None
@@ -819,7 +833,7 @@ def parse_xiao_bootloader_identity(image: bytes) -> Optional[XiaoBootloaderIdent
def bootloader_caps_storage(image: bytes) -> Optional[int]:
"""Return the one exact supported self-update storage marker, or None."""
"""Return one exact qualified self-update or app-owned recovery marker."""
found = None
valid_count = 0
for off in range(0, len(image) - 16 + 1, 4):
@@ -827,16 +841,22 @@ def bootloader_caps_storage(image: bytes) -> Optional[int]:
continue
abi, codecs = struct.unpack_from("<HH", image, off + 8)
storage = image[off + 12]
if (abi < BOOT_FORMAT_VER or abi == 0xFFFF or
if (abi < (APP_FORMAT_VER if storage == BOOT_STORAGE_RAK_AUTO_RECOVERY
else BOOT_FORMAT_VER) or abi == 0xFFFF or
codecs & BOOT_REQUIRED_APP_CODEC_MASK != BOOT_REQUIRED_APP_CODEC_MASK or
storage & ~BOOT_STORAGE_KNOWN or image[off + 13:off + 16] != b"\0\0\0" or
not storage & XIAO_BOOT_STORAGE_UPDATE):
storage & ~BOOT_STORAGE_KNOWN or
(storage & BOOT_STORAGE_HEADER_W25 and
storage != BOOT_STORAGE_RAK_AUTO_RECOVERY) or
image[off + 13:off + 16] != b"\0\0\0" or
(not storage & XIAO_BOOT_STORAGE_UPDATE and
storage != BOOT_STORAGE_RAK_AUTO_RECOVERY)):
continue
valid_count += 1
if valid_count != 1:
return None
if storage in (BOOT_STORAGE_SD_UPDATE, BOOT_STORAGE_QSPI_UPDATE,
BOOT_STORAGE_INTERNAL_UPDATE):
BOOT_STORAGE_INTERNAL_UPDATE,
BOOT_STORAGE_RAK_AUTO_RECOVERY):
found = storage
return found if valid_count == 1 else None
@@ -875,7 +895,7 @@ def validate_bootloader_image(image: bytes, target_id: Optional[int] = None,
actual_storage = bootloader_caps_storage(image)
if actual_storage not in expected_storage:
expected = "/".join(f"0x{value:02X}" for value in expected_storage)
raise ValueError(f"bootloader lacks exact ABI 3 self-update capabilities {expected}")
raise ValueError(f"bootloader lacks exact qualified update/recovery capabilities {expected}")
expected_platform = bootloader_qualified_platform_profile(
identity.board_id, identity.device_name)
if expected_platform is not None:
+36 -4
View File
@@ -1328,12 +1328,12 @@ def _xiao_bootloader_image(board_id=ml.XIAO_BOOT_BOARD_ID_BASE,
def _generic_bootloader_image(board_id=0x239A0029, device_name="3401_DFU",
storage=ml.BOOT_STORAGE_INTERNAL_UPDATE,
boot_version=0x0117010D):
boot_version=0x0117010D, apply_abi=ml.BOOT_FORMAT_VER):
import zlib
image = bytearray(b"\xff" * ml.XIAO_BOOT_IMAGE_SIZE)
struct.pack_into("<II", image, 0, 0x20040000, ml.XIAO_BOOT_IMAGE_START + 0x101)
struct.pack_into("<8sHHB3x", image, 0x80, ml.XIAO_BOOT_CAPS_MAGIC,
ml.BOOT_FORMAT_VER, ml.BOOT_REQUIRED_APP_CODEC_MASK,
apply_abi, ml.BOOT_REQUIRED_APP_CODEC_MASK,
storage)
name = device_name.encode("ascii").ljust(16, b"\0")
off = ml.BOOT_CANDIDATE_MANIFEST_OFFSET
@@ -1575,6 +1575,36 @@ def test_generic_internal_bootloader_build_parse_and_strict_contract():
pass
def test_adaptive_rak_recovery_bootloader_packages_are_exact():
priv = Ed25519PrivateKey.generate()
for board_id, name in ml.RAK_AUTO_RECOVERY_IDENTITIES:
image = _generic_bootloader_image(
board_id=board_id, device_name=name,
storage=ml.BOOT_STORAGE_RAK_AUTO_RECOVERY,
apply_abi=ml.APP_FORMAT_VER)
identity = ml.validate_bootloader_image(image)
target = ml.bootloader_target_id(board_id, name)
manifest = ml.build_manifest(
target_id=target, fw_version=identity.boot_version,
image_size=len(image), payload=image, block_size=1024,
image_hash=ml.mh32(image), codec_id=ml.CODEC_FULL,
is_full=True, sign_priv=priv, bootloader=True)
parsed = ml.parse_container(ml.build_container(manifest, image))
assert ml.verify(parsed, expect_pub=ml.ed25519_public_bytes(priv)) == []
assert parsed.manifest.target_id == target
assert ml.bootloader_caps_storage(image) == ml.BOOT_STORAGE_RAK_AUTO_RECOVERY
for wrong in (ml.BOOT_STORAGE_INTERNAL_UPDATE,
ml.BOOT_STORAGE_QSPI_UPDATE):
invalid = _generic_bootloader_image(
board_id=board_id, device_name=name, storage=wrong)
try:
ml.validate_bootloader_image(invalid)
assert False, "adaptive recovery accepted wrong loader capabilities"
except ValueError:
pass
def test_gat562_internal_bootloader_profile_is_exact():
priv = Ed25519PrivateKey.generate()
image = _generic_bootloader_image(device_name="GAT562_DFU")
@@ -1672,7 +1702,9 @@ def test_bootloader_build_inventory_is_unique_and_disjoint_from_app_targets():
for value in re.findall(r"\{ 0x([0-9a-fA-F]{8}),", target_header)
}
audited = ml.audit_bootloader_target_inventory(app_targets)
assert len(audited) == len(expected) + 2 # generic internal plus two legacy XIAOs
assert len(audited) == len(expected) + 4 # two adaptive RAKs and two XIAOs
assert audited[0xD04AB3AB] == (0x239A0029, "3401_AUTO_DFU")
assert audited[0xFEEAFD1B] == (0x239A0029, "4631_AUTO_DFU")
# Generic parsing remains useful for inspecting a future candidate, but a
# signing/build call fails closed until its exact identity is qualified.
@@ -1694,7 +1726,7 @@ def test_qualified_bootloader_platform_and_storage_profiles_are_exact():
qualified = (
(ml.XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
(ml.XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU"),
) + ml.INTERNAL_BOOTLOADER_IDENTITIES
) + ml.INTERNAL_BOOTLOADER_IDENTITIES + ml.RAK_AUTO_RECOVERY_IDENTITIES
for board_id, device_name in qualified:
family, fwid, app_base, layout_abi = \
ml.bootloader_qualified_platform_profile(board_id, device_name)