mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-17 00:34:20 +00:00
Harden signed SD and bootloader OTA
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
/* S140 v6 application layout for the MeshTower SD OTA target. */
|
||||
|
||||
SEARCH_DIR(.)
|
||||
GROUP(-lgcc -lc -lnosys)
|
||||
|
||||
MEMORY
|
||||
{
|
||||
FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xED000 - 0x26000
|
||||
|
||||
/* 8-byte retained clock state followed by the 72-byte MOTASDA2 authorization record. */
|
||||
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 80
|
||||
|
||||
/* SRAM required by SoftDevice depends on configured BLE resources. */
|
||||
RAM (rwx) : ORIGIN = 0x20006000 + 80, LENGTH = 0x20040000 - 0x20006000 - 80
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
{
|
||||
. = ALIGN(4);
|
||||
.persistent (NOLOAD) :
|
||||
{
|
||||
KEEP(*(.persistent_magic))
|
||||
KEEP(*(.persistent_data))
|
||||
. = ALIGN(4);
|
||||
} > PERSISTENT_RAM
|
||||
|
||||
ASSERT(SIZEOF(.persistent) == 8,
|
||||
"MeshTower SD OTA retained clock prefix must remain exactly 8 bytes")
|
||||
|
||||
. = ALIGN(4);
|
||||
.svc_data :
|
||||
{
|
||||
PROVIDE(__start_svc_data = .);
|
||||
KEEP(*(.svc_data))
|
||||
PROVIDE(__stop_svc_data = .);
|
||||
} > RAM
|
||||
|
||||
.fs_data :
|
||||
{
|
||||
PROVIDE(__start_fs_data = .);
|
||||
KEEP(*(.fs_data))
|
||||
PROVIDE(__stop_fs_data = .);
|
||||
} > RAM
|
||||
}
|
||||
|
||||
INCLUDE "nrf52_common.ld"
|
||||
@@ -69,6 +69,11 @@ while the same link carries framed folder requests. BLE remains available.
|
||||
| nRF52 repeater, external QSPI | Full image or in-place delta | QSPI-aware exact-board OTAFIX bootloader and matched repeater build | Builds a full mOTA; adding `--base` requests a delta |
|
||||
| MeshTower V2 nRF52, microSD | Full image or in-place delta | SD-aware exact-board OTAFIX bootloader and compatible card | Builds a full mOTA; adding `--base` requests a delta |
|
||||
|
||||
MeshTower V2 microSD application and bootloader containers require an Ed25519 signature from a key in the
|
||||
device allowlist. Supply `--sign-key` when the runner must build a container, and install the BLM2-capable
|
||||
SD-aware bootloader locally before attempting application or bootloader OTA. Preview.12 requires USB/BLE
|
||||
DFU or SWD; see [MeshTower V2 microSD LoRa OTA](ota_meshtower_v2_sdcard.md).
|
||||
|
||||
The firmware inside a raw ZIP must have a valid MeshCore `EndF` trailer. An
|
||||
ESP32 merged/factory image is not an application image and is rejected. A
|
||||
generic vendor DFU ZIP may also be unusable if it does not contain the raw
|
||||
|
||||
@@ -18,6 +18,11 @@ Matched QSPI repeater targets and the MeshTower V2 microSD target can also insta
|
||||
complete container stays off-chip. See [nRF52 repeater OTA with external QSPI](ota_nrf52_qspi.md) and
|
||||
[MeshTower V2 microSD LoRa OTA](ota_meshtower_v2_sdcard.md).
|
||||
|
||||
MeshTower V2 microSD application and bootloader containers must be Ed25519-signed by a key in the device
|
||||
allowlist; pass `--sign-key` when building one. Its BLM2-capable SD-aware bootloader is also required before
|
||||
ordinary application or bootloader OTA can use the reset-retained authorization record. Upgrade preview.12
|
||||
locally over USB/BLE DFU or SWD first; there is no raw-card compatibility handoff.
|
||||
|
||||
## Temporary OTA channel used in this guide
|
||||
|
||||
| Setting | Value |
|
||||
|
||||
@@ -22,14 +22,16 @@ LoRa radio pinout.
|
||||
|
||||
## Card requirements
|
||||
|
||||
Use a FAT16, FAT32, or exFAT card with an MBR partition table whose first
|
||||
partition starts after sector 1. This is the normal layout produced by most SD
|
||||
formatters. GPT and unpartitioned "super-floppy" layouts are rejected.
|
||||
Use a FAT16, FAT32, or exFAT card accepted by the bundled SdFat version. The
|
||||
normal update path imposes no additional MBR/sector-1 layout requirement;
|
||||
filesystem layouts SdFat cannot mount are still rejected. MeshCore creates
|
||||
`/meshcore-ota.mota` as a contiguous file and passes its exact sector range to
|
||||
OTAFIX in reset-retained MCU RAM.
|
||||
|
||||
MeshCore creates `/meshcore-ota.mota` as a contiguous file. Sector 1, which is
|
||||
outside the partition, holds a checksummed bootloader handoff record. The
|
||||
firmware validates this gap before writing it; an incompatible card layout
|
||||
fails safely without modifying sector 1.
|
||||
MeshCore never reads or writes raw sector 1 and does not infer ownership from
|
||||
blank card sectors. Both application and bootloader OTA require a locally
|
||||
provisioned BLM2-capable bootloader that understands the retained-RAM record.
|
||||
Preview.12 must first be upgraded over USB/BLE DFU or SWD.
|
||||
|
||||
## Capacity and update types
|
||||
|
||||
@@ -43,7 +45,8 @@ For this S140 v6 target, the maximum application image including its `EndF`
|
||||
trailer is `0xC7000` bytes (815,104 bytes). To package a full self-update:
|
||||
|
||||
```bash
|
||||
motatool build --fw ./Heltec_tower_v2_sdcard-new.hex --out-dir ./motas
|
||||
motatool build --fw ./Heltec_tower_v2_sdcard-new.hex \
|
||||
--sign-key ./trusted-signer.key --out-dir ./motas
|
||||
motatool verify ./motas/*.mota
|
||||
```
|
||||
|
||||
@@ -57,19 +60,23 @@ motatool build \
|
||||
--fw ./Heltec_tower_v2_sdcard-new.hex \
|
||||
--patch-type in-place \
|
||||
--inplace-memory 0xC7000 \
|
||||
--sign-key ./trusted-signer.key \
|
||||
--out-dir ./motas
|
||||
motatool verify ./motas/*.mota
|
||||
```
|
||||
|
||||
The download is resumable because the partial `.mota` stays on the card.
|
||||
Once it reaches `ready`, `ota install` performs the final verification,
|
||||
publishes the bootloader handoff, and reboots. Keep the card inserted through
|
||||
the reboot and installation.
|
||||
publishes a one-reset authorization record, and reboots. SD application
|
||||
installation requires a valid signature from a key in the node's allowlist.
|
||||
Keep the card inserted through the reboot and installation.
|
||||
|
||||
The SD-aware bootloader is mandatory. `ota install` refuses to reboot if the
|
||||
bootloader capability marker does not advertise SD staging and the selected
|
||||
codec. Existing `Heltec_tower_v2_repeater` firmware continues to use the
|
||||
internal-flash delta path and is unchanged.
|
||||
The BLM2 retained-auth SD-aware bootloader is mandatory for application OTA.
|
||||
`ota install` refuses to reboot if its continuity metadata does not match the
|
||||
running S140 family/FWID/application layout or if its capability marker does
|
||||
not advertise SD staging and the selected codec. Existing
|
||||
`Heltec_tower_v2_repeater` firmware continues to use the internal-flash delta
|
||||
path and is unchanged.
|
||||
|
||||
## Signed bootloader update
|
||||
|
||||
@@ -78,14 +85,15 @@ It requires an already installed exact-board ABI-3 OTAFIX bootloader whose one
|
||||
unambiguous capability marker is exactly `0x09` (`SD|BOOT_UPDATE`) and whose
|
||||
codec mask is `0x0005` (`FULL|INPLACE`). Requiring both application codecs
|
||||
prevents a bootloader self-update from disabling either normal SD application
|
||||
path. It cannot bootstrap a stock or older bootloader; provision that
|
||||
prerequisite once over USB/BLE DFU or SWD.
|
||||
path. A stock bootloader still requires USB/BLE DFU or SWD. The sole remote
|
||||
bootstrap is intentionally absent: preview.12 and any other legacy-v1 image
|
||||
must be upgraded locally before either SD application or bootloader OTA.
|
||||
|
||||
The signed v3 container is exactly 41,330 bytes and carries a 40 KiB candidate
|
||||
for the installed `239A0071 / TOWER_V2_OTA` identity (boot target `1150F50E`).
|
||||
It uses the same contiguous `/meshcore-ota.mota` file and sector-1 handoff as an
|
||||
application update. GPREGRET `0x6B` plus the distinct SD source marker `0x53`
|
||||
selects the bootloader path; LoRa transport remains payload type `0x0C`.
|
||||
It uses the same contiguous `/meshcore-ota.mota` file as an application update.
|
||||
GPREGRET `0x6B` plus the distinct SD source marker `0x53` selects the bootloader
|
||||
path; LoRa transport remains payload type `0x0C`.
|
||||
|
||||
The application linker still ends at `0xED000`, but OTAFIX needs
|
||||
`0xE0000..0xEA000` as temporary scratch while replacing itself. Before a boot
|
||||
@@ -102,17 +110,33 @@ EndF-inclusive running image and must end by `0xE0000`; an undersized or
|
||||
oversized record refuses the operation and requires local DFU/SWD.
|
||||
|
||||
Because the card is removable, approval is bound to the exact bytes that were
|
||||
authenticated. MeshCore verifies the signature and signer allowlist over one
|
||||
exact local manifest copy, requires the streamed manifest on SD to remain
|
||||
byte-identical, and verifies the complete payload against that manifest. It
|
||||
then writes `APRV` and syncs the card. Before publishing the raw-sector
|
||||
handoff, it writes and readback-verifies a temporary 64-byte `MOTASDBL` token
|
||||
at `0xE0000` containing the exact container length and that signed manifest's
|
||||
`image_hash`. OTAFIX requires the parsed SD manifest, streamed payload, and
|
||||
final scratch image to match the same token. Replacing or changing the card
|
||||
after verification can therefore only make the boot update fail; it cannot
|
||||
authorize different bootloader bytes. The token page becomes the first
|
||||
scratch page during a successful update and is not permanently reserved.
|
||||
authenticated. For both fmt2 application and fmt3 bootloader packages,
|
||||
MeshCore authenticates one exact signed manifest, requires every streamed
|
||||
manifest byte to match it, and verifies the leaves, payload, and image. During
|
||||
that same pass it hashes the entire container with only the mutable four-byte
|
||||
`APRV` field normalized to zero. After writing and syncing `APRV`, MeshCore
|
||||
publishes a 72-byte `MOTASDA2` record at reset-retained RAM address
|
||||
`0x20006008`. The record binds package purpose/format, exact LBA range, card
|
||||
size, container length, and that normalized SHA-256. OTAFIX consumes and clears
|
||||
the record before reading the card; changing the card or file can only fail.
|
||||
A power loss erases the authorization and also fails closed.
|
||||
|
||||
For fmt3, MeshCore additionally writes and readback-verifies the temporary
|
||||
64-byte `MOTASDBL` token at `0xE0000`, binding the exact total and signed
|
||||
manifest `image_hash`. OTAFIX requires the parsed manifest, streamed payload,
|
||||
and final scratch image to match it. The token page becomes scratch during a
|
||||
successful boot update and is not permanently reserved.
|
||||
|
||||
Every new candidate also carries a backward-compatible `BLM2`/`SOFT`
|
||||
continuity extension next to its legacy embedded manifest. The complete
|
||||
76-byte envelope is fixed at final raw-image offset `0x9FB4`; a relocated copy
|
||||
is not a valid candidate. Its embedded
|
||||
bootloader version must equal the outer package version, match the runtime
|
||||
SoftDevice family/FWID, application base, and layout ABI, and be strictly newer
|
||||
than an installed BLM2 version. Preview values use low bytes `1..254` and a
|
||||
stable release uses `0xFF`; zero-preview and all-ones versions are invalid.
|
||||
Remote bootloader rollback is not supported; use local DFU/SWD when rollback
|
||||
or migration is intentional.
|
||||
|
||||
Bootloader packages are never autofetched or autoinstalled. Select and confirm
|
||||
one exact package manually:
|
||||
@@ -235,9 +259,9 @@ clean replacement instead of treating path existence as a valid cache hit.
|
||||
To prepare and load the card:
|
||||
|
||||
1. Format it as described under [Card requirements](#card-requirements). The
|
||||
easiest way to guarantee the expected layout is to insert it in the
|
||||
repeater, run `set sdcard format`, power the repeater off, and then move the
|
||||
card to the computer. Formatting destroys the existing card contents.
|
||||
easiest route is to insert it in the repeater, run `set sdcard format`, power
|
||||
the repeater off, and then move the card to the computer. Formatting destroys
|
||||
the existing card contents.
|
||||
2. Mount the card on the computer and create a directory named `mota` at the
|
||||
filesystem root. Do not use a nested directory such as `/firmware/mota`.
|
||||
3. Run `motatool verify FILE.mota` for every source file. Do not copy a file
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
Selected nRF52840 repeater LoRa-OTA builds can replace their matching OTAFIX
|
||||
bootloader without replacing the running application. This is a privileged
|
||||
maintenance path, not a normal firmware update. It cannot bootstrap a stock or
|
||||
older bootloader: install the exact ABI-3 self-update-capable OTAFIX build once
|
||||
over USB/BLE DFU or SWD.
|
||||
maintenance path, not a normal firmware update. A stock bootloader must first
|
||||
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.
|
||||
|
||||
## Storage layouts
|
||||
|
||||
@@ -28,17 +29,29 @@ replace `0xF4000..0xFE000`. The application linker remains at `0xED000`; a
|
||||
future application extending above `0xE0000` can still use application mOTA
|
||||
but must update its bootloader through local DFU/SWD.
|
||||
|
||||
The removable SD authorization is fail-closed. MeshCore directly authenticates
|
||||
one exact manifest, requires the streamed manifest on SD to remain
|
||||
byte-identical, and verifies its complete payload. After writing and syncing
|
||||
`APRV`, but before publishing the raw-sector handoff, it writes and
|
||||
readback-verifies a 64-byte `MOTASDBL` token at `0xE0000`. The token contains
|
||||
the exact container length and the authenticated signed manifest's
|
||||
`image_hash`. OTAFIX requires that same hash in the parsed manifest, the
|
||||
streamed raw payload, and the final scratch image. A card swap or mutation
|
||||
after application verification therefore causes refusal rather than
|
||||
authorizing different bytes; the token page is consumed as scratch during a
|
||||
successful update and is not a permanent reservation.
|
||||
The removable SD authorization is fail-closed for both application and
|
||||
bootloader packages. MeshCore directly authenticates one exact signed
|
||||
manifest, requires the streamed copy to remain byte-identical, verifies all
|
||||
leaves/payload/image bytes, and computes a normalized full-container SHA-256 in
|
||||
that same pass (`APRV` bytes are treated as zero). After syncing `APRV`, it
|
||||
publishes a 72-byte `MOTASDA2` record in reset-retained RAM at `0x20006008`.
|
||||
That record binds purpose/format, first LBA, sector count, total length, card
|
||||
sector count, and the normalized digest. OTAFIX copies, validates, and clears
|
||||
the record before SD access. A card swap/mutation or a power cycle therefore
|
||||
fails closed; there is no general sector-1 ownership inference.
|
||||
|
||||
For fmt3, MeshCore also writes and readback-verifies a 64-byte `MOTASDBL` token
|
||||
at `0xE0000`. That token contains the exact container length and the exact
|
||||
authenticated signed manifest `image_hash`. OTAFIX requires the same hash in
|
||||
the parsed manifest, streamed payload, and final scratch image. The page is
|
||||
consumed as scratch during success and is not permanently reserved.
|
||||
|
||||
The first BLM2-capable MeshTower bootloader cannot be delivered to preview.12
|
||||
through the retained-RAM protocol that preview.12 predates. MeshCore does not
|
||||
write a raw-sector compatibility record because a blank sector is not proof of
|
||||
ownership. Upgrade preview.12 through USB/BLE DFU or SWD. Both fmt2 application
|
||||
and fmt3 bootloader OTA then require the installed BLM2 metadata to match the
|
||||
live SoftDevice/application layout; neither MeshCore nor OTAFIX uses sector 1.
|
||||
|
||||
The internal path does not change the application linker or permanently set
|
||||
aside separate app-OTA, boot-package, and scratch regions. The ordinary
|
||||
@@ -105,6 +118,30 @@ duplicates and collisions with application target IDs. Generic image parsing
|
||||
can inspect a future canonical identity, but signing/building a package fails
|
||||
until that exact identity is in the qualified inventory.
|
||||
|
||||
## Embedded continuity and version policy
|
||||
|
||||
Every remotely supplied successor retains the legacy 44-byte CRC-valid `BLMF`
|
||||
v1 record for identity continuity and diagnostics. Immediately after that
|
||||
record is a 32-byte `BLM2`/`SOFT` extension. The complete 76-byte envelope is
|
||||
fixed at the final raw-image offset `0x9FB4`; relocated candidates are refused,
|
||||
while installed legacy-v1 discovery remains a generic diagnostic scan. The extension carries the actual packed
|
||||
bootloader version, SoftDevice family and FWID, application base, layout ABI,
|
||||
and zero compatibility/reserved fields; the legacy whole-image CRC covers the
|
||||
extension too. Scanners first count CRC-valid 44-byte base manifests, then
|
||||
interpret continuity only after one base identity remains. A malformed claimed
|
||||
extension therefore cannot make a duplicate identity disappear, and a sole
|
||||
half-present extension is rejected rather than treated as legacy.
|
||||
|
||||
MeshCore requires the candidate extension, exact equality between its embedded
|
||||
version and the signed outer mOTA version, and exact agreement with the running
|
||||
SoftDevice family/FWID/application layout. Qualified internal/QSPI paths may
|
||||
bootstrap a CRC-valid installed legacy-v1 image once; the SD path never does
|
||||
and requires local BLM2 provisioning. Once the installed bootloader has BLM2
|
||||
metadata, the candidate version must be strictly greater. Preview low bytes are `1..254`, a
|
||||
stable release uses `0xFF`, and low-byte zero or all-ones values are rejected.
|
||||
There is no remote rollback/migration override; intentional rollback uses
|
||||
USB/BLE DFU or SWD.
|
||||
|
||||
The `no_external_sensors` profiles omit optional/add-on sensor and GPS packages
|
||||
to preserve flash headroom. Board-integrated GPS can be retained by a board's
|
||||
recipe. The RAK3401 lean target specifically omits RAK12501/add-on GPS; use the
|
||||
@@ -156,9 +193,12 @@ Before writing `APRV`, the application authenticates and authorizes the
|
||||
package: exact v3 geometry, trusted Ed25519 signer, signed/embedded identity,
|
||||
one unambiguous capability marker, embedded CRC, sane vectors, complete
|
||||
Merkle/payload/image hashes, storage-specific safe live placement, and the
|
||||
typed MID/hash confirmation. On SD, `APRV` and the internal signed-image-hash
|
||||
token are synced and read back before the handoff becomes visible. OTAFIX consumes that application-written
|
||||
authorization and then
|
||||
typed MID/hash confirmation. The manifest root and target inside the received
|
||||
manifest must also equal the MID/target that opened the fetch before any store
|
||||
is allocated. On SD, `APRV`, the retained geometry/container authorization,
|
||||
and the internal signed-image-hash token are published in that order before
|
||||
the reset trigger. OTAFIX consumes that application-written authorization and
|
||||
then
|
||||
independently rechecks the safety/integrity subset: strict v3 structure,
|
||||
canonical identity/capabilities, vectors, full payload SHA, embedded manifest
|
||||
CRC, the applicable live `EndF`/bank-settings no-overlap geometry, the SD token
|
||||
@@ -173,13 +213,13 @@ headroom is insufficient, an unsupported external/ExtraFS role owns the target,
|
||||
the exact installed capability marker is absent or ambiguous, identity cannot be derived
|
||||
unambiguously, or any package check fails. Before the storage-specific
|
||||
scratch/copy step, the application and bootloader are unchanged. OTAFIX
|
||||
consumes the trigger and, for internal/QSPI storage, clears approval before its
|
||||
first scratch erase, so an interrupted operation cannot retry a partly
|
||||
consumed package. The SD backend cannot rewrite raw card sectors: its `APRV`
|
||||
and checksummed handoff may persist, but the one-shot GPREGRET trigger is
|
||||
consumed before validation and they remain inert unless the running app again
|
||||
authenticates and explicitly re-arms that package. Use USB/BLE DFU or SWD for
|
||||
initial provisioning and local recovery.
|
||||
consumes the trigger and authorization before its first destructive action, so
|
||||
an interrupted operation cannot automatically retry a partly consumed
|
||||
package. On SD, the retained-RAM authorization is zero-consumed before media
|
||||
access; `APRV` and the fmt3 flash token may persist but are inert without a new
|
||||
explicit authenticated re-arm and reset trigger. No SD raw-sector handoff is
|
||||
created or consumed.
|
||||
Use USB/BLE DFU or SWD for initial provisioning and local recovery.
|
||||
|
||||
For XIAO and ordinary external-QSPI details, see
|
||||
[nRF52 repeater LoRa OTA with external QSPI](ota_nrf52_qspi.md).
|
||||
|
||||
@@ -212,9 +212,13 @@ The final command must reproduce both the complete staged manifest ID and the
|
||||
first eight bytes of its signed image hash. Before approval the application
|
||||
again checks package/root/payload/image hashes, Ed25519 signature and trusted
|
||||
allowlist, exact signed `XIAO_BL_...` ID, installed and incoming embedded
|
||||
manifest/CRC/name/board identity, vector table, and ABI-3 QSPI+boot-update
|
||||
continuity. A signed older bootloader that would remove remote update support
|
||||
is refused.
|
||||
manifest/CRC/name/board identity, vector table, ABI-3 QSPI+boot-update marker,
|
||||
and the adjacent CRC-covered `BLM2`/`SOFT` continuity extension at the exact
|
||||
final-image offset `0x9FB4`. The embedded
|
||||
boot version must equal the signed outer package version, the SoftDevice
|
||||
family/FWID/application base/layout ABI must match the running platform, and
|
||||
a successor to installed BLM2 must be strictly newer. Low-byte-zero and
|
||||
all-ones versions are invalid. Remote rollback is refused; use local DFU/SWD.
|
||||
|
||||
After the reply drains, GPREGRET `0x6B` and GPREGRET2 `0x51` enter the special
|
||||
OTAFIX path. `APRV` carries the app's signature/allowlist and explicit operator
|
||||
|
||||
+44
-12
@@ -271,14 +271,25 @@ A v3 bootloader package has a deliberately narrow, non-extensible profile:
|
||||
- signed `hw_id` exactly `XIAO_BL_28860044`/`XIAO_BL_28860045` for deployed XIAO, or the zero-padded
|
||||
32-byte `NRF_BL_<BOARD_ID>_<DEVICE_NAME>` for a generic target;
|
||||
- a sane nRF52840 vector table, exactly one CRC-valid embedded manifest v1 with the exact board/name pair,
|
||||
followed by the required CRC-covered `BLM2`/`SOFT` continuity extension (embedded boot version,
|
||||
SoftDevice family/FWID, application base, and layout ABI), with that complete 76-byte envelope at the
|
||||
canonical final-image offset `0x9FB4`,
|
||||
and exactly one `MOTABLDR` marker advertising ABI >= 3, both application codecs (`FULL|INPLACE`, mask
|
||||
`0x0005`), boot-update continuity, and the exact storage flags for the application layout (`0x09`
|
||||
MeshTower V2 SD, `0x0E` XIAO QSPI, or `0x0A` shared internal staging).
|
||||
|
||||
The incoming embedded identity must exactly match the installed CRC-valid bootloader identity. Both scans
|
||||
consider every aligned structurally valid candidate so magic bytes in a literal pool cannot shadow the real
|
||||
manifest. A package must be signed by a key already in the device's trusted allowlist; unlike ordinary
|
||||
manifest. Duplicate accounting counts each CRC-valid 44-byte base record before interpreting adjacent
|
||||
continuity metadata, so a corrupt or half-present `BLM2` extension cannot hide a second identity; after
|
||||
exactly one base record is selected, malformed claimed continuity fails closed. A package must be signed by
|
||||
a key already in the device's trusted allowlist; unlike ordinary
|
||||
application packages, there is no unsigned manual-install exception.
|
||||
The signed outer `fw_version` must equal the embedded boot version. Qualified internal/QSPI targets may
|
||||
bootstrap a CRC-valid legacy-v1 installed bootloader once; MeshTower SD instead requires local BLM2
|
||||
provisioning because it has no safe legacy media handoff. After bootstrap every remote successor must be strictly newer and match the live
|
||||
SoftDevice/application layout. Low-byte zero and all-ones boot versions are invalid. Remote rollback has no
|
||||
override and must use local DFU/SWD.
|
||||
|
||||
---
|
||||
|
||||
@@ -360,7 +371,13 @@ There is no separate availability structure. **Block `i` is present <=> `leaves[
|
||||
the staged container after a reboot - re-parse the stored manifest, recompute geometry, count present
|
||||
blocks, continue fetching the holes (or jump straight to COMPLETE). The checkpoint cadence (persist progress
|
||||
every N committed blocks) is runtime-tunable (`ota config checkpoint <N>`, 0 = only finalized containers
|
||||
resume). Stores keep `leaves[]` in RAM until flush and never auto-GC, preserving resumable progress.
|
||||
resume). Boot-time adoption is an automatic fetch decision: current `autofetch` must be enabled, the stored
|
||||
target must equal the node target, policy `signed` requires the signed bit, and an enabled running-version
|
||||
floor requires a strictly newer manifest. An explicit MID pull may deliberately resume an older or unsigned
|
||||
package and keeps target `0` as a MID-only wildcard. Stores keep `leaves[]` in RAM until flush and never
|
||||
auto-GC, preserving resumable progress. The debug/operator equivalent is `ota dev resume <MID8>`; after a
|
||||
reboot the MID is mandatory, while a no-argument form may only reuse a still-active session MID. It never
|
||||
uses the `nullptr` automatic-adoption path, so a malformed MID or no active MID fails closed.
|
||||
|
||||
**Flash-store note (RX-safe writes):** a flash page-erase halts the CPU (~85 ms on nRF52) and starves LoRa
|
||||
RX, so the flash stores (`OtaStoreFlashNrf52`/`OtaStoreFlashEsp32`) **coalesce writes to the erase unit**
|
||||
@@ -494,6 +511,11 @@ fetcher server (any node that has the mid)
|
||||
when all blocks present: verify full merkle_root + image_hash -> COMPLETE
|
||||
```
|
||||
|
||||
Before allocating or writing the selected store, the receiver parses the reassembled manifest and requires
|
||||
its `merkle_root` to equal the requested/wire `manifest_id` and its `target_id` to equal the catalog or
|
||||
explicit-pull target that opened the receive slot. The wire envelope and HAVE row are advisory; they cannot
|
||||
label and stage a different manifest.
|
||||
|
||||
### 8.4 Message bodies (transfer)
|
||||
|
||||
All offsets after the 1-byte type. Encoders/decoders in `OtaProtocol.cpp`; constants in `OtaManager.h`.
|
||||
@@ -630,8 +652,13 @@ transmission per hop.
|
||||
independent of signature.
|
||||
- **Signing & allowlist:** a node keeps a runtime allowlist of trusted Ed25519 signer pubkeys (none embedded
|
||||
in firmware; `ota key add/list/rm`). A `.mota` is eligible for **auto-install** only if signed by an
|
||||
allowlisted key, the signature verifies, and `image_hash` matches. Manual install permits unsigned packages,
|
||||
allowlisted key, the signature verifies, `image_hash` matches, and its nonzero signed `fw_version` is
|
||||
strictly greater than the running hash-valid EndF version. Both catalog admission and final automatic
|
||||
apply enforce the version floor, so a lying HAVE row cannot bypass it. Manual `ota install` is the explicit
|
||||
equal-version/rollback override and generally permits unsigned packages,
|
||||
but a package that claims to be signed must have a valid signature from an allowlisted key or it is rejected.
|
||||
The removable-SD target is stricter: every application install needs a valid allowlisted signature because
|
||||
the app mints an authenticated one-reset media authorization for OTAFIX.
|
||||
**Transfer needs no trust** - blocks are content-addressed against the manifest's merkle root.
|
||||
- **Policies (persisted):** `autofetch` in {off, any, signed} (default off) gates automatic block fetching of
|
||||
own-target adverts; `autoinstall` in {off, trusted} (default off) gates auto-apply of a COMPLETE signed +
|
||||
@@ -783,7 +810,7 @@ ota qspi | storage QSPI nRF52 only: JEDEC/SR1/stage/latched stor
|
||||
ota folder | fold [on|off] attach/detach an external .mota folder (host daemon) ; bare = list
|
||||
ota config | cfg | set [autofetch|autoinstall|checkpoint] ... show/set persisted policy
|
||||
ota key | keys [add|rm <hex>] trusted signer allowlist ; bare = list
|
||||
ota dev ... bring-up helpers (stage/recv/serve/verify)
|
||||
ota dev ... bring-up helpers (stage/recv/serve/resume <MID8>/verify)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -840,22 +867,27 @@ ota dev ... bring-up helpers (stage/recv/serve/verify)
|
||||
Internal-bootloader-self-update builds are a stricter exception: because their ordinary linker may extend
|
||||
through `0xED000`, an absent/corrupt live `EndF` disables every internal staging pull before erase instead
|
||||
of trusting the legacy 608 KiB estimate.
|
||||
- **MeshTower V2 SD nRF52:** the application stores a contiguous `/meshcore-ota.mota` on microSD and
|
||||
publishes its raw sector range in a checksummed handoff record outside the MBR partition. The matching
|
||||
bootloader reads the card without mounting FAT, supports either a full image or an in-place delta,
|
||||
- **MeshTower V2 SD nRF52:** the application stores a contiguous `/meshcore-ota.mota` on microSD. After
|
||||
authenticating one exact signed manifest and verifying the leaves/payload/image, it hashes the exact full
|
||||
container with only `APRV` normalized to zero and publishes a 72-byte reset-retained `MOTASDA2` record.
|
||||
That record binds app-vs-boot purpose, format, raw sector range, total/card geometry, and normalized digest;
|
||||
OTAFIX consumes and clears it before reading the card. There is no normal sector-1 handoff or additional
|
||||
OTA-specific partition-layout requirement beyond what the bundled SdFat can mount. The matching
|
||||
bootloader reads the authorized sectors without mounting FAT, supports either a full image or an in-place delta,
|
||||
verifies the staged/full result hash, and never writes through `0xED000` where InternalFS begins. The
|
||||
exact SD repeater also accepts a manually selected, signed v3 bootloader package when installed and
|
||||
candidate markers are exactly `0x09` (`SD|BOOT_UPDATE`). MeshCore streams the same strict identity,
|
||||
CRC, vector, signature, MID/hash-confirmation, and complete-image checks from the SD file. GPREGRET
|
||||
`0x6B` plus GPREGRET2 `0x53` selects this privileged path. Both MeshCore and OTAFIX require a hash-valid
|
||||
live `EndF` ending by `0xE0000`; when a nonzero boot-settings bank CRC is active, its recorded size must
|
||||
also cover that complete live image and stop by `0xE0000`. MeshCore authenticates one exact manifest,
|
||||
verifies the streamed SD copy is byte-identical, writes and syncs `APRV`, then writes a readback-checked
|
||||
`MOTASDBL` token at `0xE0000` containing the exact total and signed `image_hash` before publishing the
|
||||
raw-sector handoff. OTAFIX binds the parsed manifest, streamed payload, and final scratch image to that
|
||||
also cover that complete live image and stop by `0xE0000`. For fmt3 MeshCore additionally writes a readback-checked
|
||||
`MOTASDBL` token at `0xE0000` containing the exact total and signed `image_hash`. OTAFIX binds the parsed manifest,
|
||||
streamed payload, and final scratch image to that
|
||||
token, so a removable-media change can only fail closed. OTAFIX then uses `0xE0000..0xEA000` as temporary scratch; the normal
|
||||
application linker remains at `0xED000` and ordinary application updates do not inherit this scratch
|
||||
headroom restriction.
|
||||
headroom restriction. Both fmt2 application apply and fmt3 bootloader apply require installed BLM2
|
||||
continuity matching the live S140 FWID/application layout. Preview.12 must be upgraded locally over
|
||||
USB/BLE DFU or SWD. MeshCore never writes a raw sector-1 handoff.
|
||||
- **Matched external-QSPI nRF52 repeaters:** the application reserves the board's dedicated QSPI NOR as a
|
||||
raw store beginning at offset zero. It obtains a 1-16 MiB capacity from JEDEC, checkpoints payload before
|
||||
leaf metadata, and verifies each erased/programmed page. GPREGRET2 `0x51` selects QSPI only when the
|
||||
|
||||
+36
-14
@@ -211,8 +211,11 @@ ota install
|
||||
|
||||
The node verifies the firmware one last time, and if everything checks out it installs it and **reboots
|
||||
into the new version**. If the check fails, it tells you why and does **not** install. Unsigned images
|
||||
install only through this explicit command. A signed image whose signer is not in the device allowlist is
|
||||
rejected; trusted signed images can auto-install only when that policy is enabled.
|
||||
normally install only through this explicit command. The MeshTower V2 SD target is stricter and requires an
|
||||
allowlisted signature even for a manual application install, because it authorizes removable-media bytes for
|
||||
the bootloader. A signed image whose signer is not in the device allowlist is rejected; trusted signed images
|
||||
can auto-install only when that policy is enabled and the signed version is strictly newer than the running
|
||||
hash-valid EndF version. Manual `ota install` remains the deliberate equal-version/rollback override.
|
||||
|
||||
After it reboots, run `ota status` to confirm the new version.
|
||||
|
||||
@@ -246,10 +249,13 @@ Copy both confirmation values exactly from the second `ota bootloader` reply.
|
||||
Ordinary `ota install` deliberately refuses this package. The privileged
|
||||
command requires an exact 40 KiB candidate payload in the fixed 41,330-byte
|
||||
container, a valid exact embedded identity/CRC and vector table, continued
|
||||
boot-update support, and a valid signature from a key already in `ota key`'s
|
||||
trusted allowlist. It preserves the running application while OTAFIX replaces
|
||||
itself; `blup:C8` in post-reboot `ota status` means success. Any node lacking
|
||||
this command or those capabilities must update its bootloader locally instead.
|
||||
boot-update support, required `BLM2`/`SOFT` continuity metadata at canonical
|
||||
raw-image offset `0x9FB4`, a boot version
|
||||
that exactly matches the package and is newer than installed BLM2, and a valid
|
||||
signature from a key already in `ota key`'s trusted allowlist. It preserves the
|
||||
running application while OTAFIX replaces itself; `blup:C8` in post-reboot
|
||||
`ota status` means success. Remote rollback is refused. Any node lacking this
|
||||
command or those capabilities must update its bootloader locally instead.
|
||||
|
||||
On an internal-flash target, the package shares the ordinary store below
|
||||
`0xED000` and bottom-aligns at `0xE2000`; there is no separate reserved scratch
|
||||
@@ -260,9 +266,14 @@ On the MeshTower V2 SD target, the application linker remains at `0xED000`, but
|
||||
bootloader replacement needs temporary scratch beginning at `0xE0000`. A
|
||||
hash-valid live `EndF` must therefore prove the complete current image ends by
|
||||
`0xE0000`. A CRC-bound boot-settings bank must cover that complete image while
|
||||
also ending by `0xE0000`. MeshCore binds approval to the exact authenticated
|
||||
signed image hash in an internal token before handing the removable SD card to
|
||||
OTAFIX, so a later card change fails instead of authorizing different bytes.
|
||||
also ending by `0xE0000`. MeshCore binds both application and bootloader SD
|
||||
approval to purpose, exact raw geometry, and a normalized SHA-256 in a
|
||||
reset-retained `MOTASDA2` record. Boot updates also bind the exact authenticated
|
||||
signed image hash in the E0000 token. OTAFIX consumes the retained record before
|
||||
media access, so a later card change or power cycle fails instead of authorizing
|
||||
different bytes. MeshCore never claims or writes raw sector 1. Both application
|
||||
and bootloader OTA wait until a matching BLM2 bootloader has been provisioned
|
||||
locally; preview.12 requires USB/BLE DFU or SWD first.
|
||||
Larger applications can continue to use normal application mOTA;
|
||||
only bootloader self-update is refused.
|
||||
See [the nRF52 bootloader-update guide](ota_nrf52_bootloader_update.md) for the
|
||||
@@ -309,7 +320,7 @@ ota config autofetch any # auto-DOWNLOAD any compatible update for this n
|
||||
ota config autofetch signed # auto-download only signed updates
|
||||
ota config autofetch off # back to manual (default)
|
||||
|
||||
ota config autoinstall trusted # auto-INSTALL a downloaded update IF it's signed by a key you trust
|
||||
ota config autoinstall trusted # auto-INSTALL only a trusted signed version newer than the running EndF
|
||||
ota config autoinstall off # never auto-install (default)
|
||||
|
||||
ota config advert 1440 # re-advertise every N minutes while temp radio is running
|
||||
@@ -321,8 +332,18 @@ ota config hops 0 # only exchange OTA with directly-connected node
|
||||
ota config # show the current settings
|
||||
```
|
||||
|
||||
These policies also govern automatic adoption of an interrupted staged download after reboot. `off` leaves
|
||||
it untouched, `signed` requires the stored manifest's signed flag, and automatic resume requires the stored
|
||||
target to match this node and its version to be newer than the running valid EndF. Reissuing an explicit
|
||||
`ota pull <MID8>` remains the deliberate override for an older or unsigned partial.
|
||||
For bring-up/debugging, `ota dev resume <MID8>` performs the same explicit MID-bound re-adoption without
|
||||
starting a new network fetch. After reboot it requires the MID; bare `ota dev resume` is accepted only while
|
||||
an active/requested session MID still exists, and malformed or missing identifiers are rejected.
|
||||
|
||||
Recommended for most people: leave both **off** and update by hand. Use `autoinstall trusted` only once
|
||||
you've added the signer's key (next section) and you trust them to push updates unattended.
|
||||
you've added the signer's key (next section) and you trust them to push updates unattended. Automatic
|
||||
admission and final apply both reject zero, equal, or older signed versions; a dishonest catalog version
|
||||
cannot bypass the manifest check. Use manual `ota pull` plus `ota install` for an intentional rollback.
|
||||
|
||||
The MeshTower V2 SD OTA target has a separate, default-on **archive** policy. It saves all mOTAs it sees
|
||||
to the SD card so the repeater can seed them later; this does not install them and does not change the
|
||||
@@ -345,9 +366,10 @@ ota key list # show trusted signers
|
||||
ota key rm <public-key-hex> # stop trusting one
|
||||
```
|
||||
|
||||
Only updates signed by a trusted key are eligible for auto-install. Manual `ota install` permits an unsigned
|
||||
package after all integrity, hardware, base, and bootloader checks pass. A signed package whose signer is not
|
||||
in the device allowlist is rejected rather than silently treated as unsigned.
|
||||
Only strictly newer updates signed by a trusted key are eligible for auto-install. Manual `ota install`
|
||||
permits an unsigned package after all integrity, hardware, base, and bootloader checks pass, except on the
|
||||
MeshTower V2 removable-SD path where all application installs must be signed and allowlisted. A signed
|
||||
package whose signer is not in the device allowlist is rejected rather than silently treated as unsigned.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -415,7 +415,7 @@ void __attribute__((noinline)) Mesh::serviceLoopMaintenance() {
|
||||
&& !oc.manager.fetched_is_bootloader()) {
|
||||
_ota_autoinstall_tried = true;
|
||||
char msg[100];
|
||||
oc.apply_fetched(msg); // arms + sets apply_pending only if signed & allowlisted; refused otherwise
|
||||
oc.apply_fetched_trusted_auto(msg); // signed/trusted and strictly newer; manual install may override
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -107,7 +107,9 @@ void WiFiOtaSeeder::loop() {
|
||||
seeder_client.remoteIP().toString().c_str());
|
||||
context.set_folder_dest(&folder_store, link_info);
|
||||
if (context.manager.fetchState() == OtaManager::PAUSED) {
|
||||
context.manager.resumeStaged(nullptr);
|
||||
// This continues the active host-selected MID; it is not a boot-time
|
||||
// automatic adoption governed by application autofetch policy.
|
||||
context.manager.resumeStaged(context.manager.fetchManifestId());
|
||||
}
|
||||
context.manager.announce();
|
||||
Serial.printf("OTA seeder client connected (%s)\n", link_info);
|
||||
|
||||
@@ -462,7 +462,9 @@ bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, Ap
|
||||
|
||||
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_QSPI_STORE)
|
||||
#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());
|
||||
@@ -641,9 +643,24 @@ bool ota_rescue_mota_nrf52(const uint8_t* buf, uint32_t len, const SignerAllowli
|
||||
#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];
|
||||
@@ -695,14 +712,24 @@ static bool ota_apply_mota_nrf52_external(Store& store, const SignerAllowlist& a
|
||||
return false;
|
||||
}
|
||||
|
||||
VerifyResult vr = ota_verify(static_cast<const OtaStore&>(store), allow);
|
||||
// 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) {
|
||||
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) {
|
||||
@@ -738,7 +765,7 @@ static bool ota_apply_mota_nrf52_external(Store& store, const SignerAllowlist& a
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!store.approve_for_bootloader()) {
|
||||
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;
|
||||
@@ -751,80 +778,39 @@ static bool ota_apply_mota_nrf52_external(Store& store, const SignerAllowlist& a
|
||||
}
|
||||
#endif
|
||||
|
||||
#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)) {
|
||||
uint32_t len = OTA_BOOT_IMAGE_SIZE - off;
|
||||
if (len > sizeof(buf)) len = sizeof(buf);
|
||||
if (!store.read(payload_off + off, buf, len)) return false;
|
||||
for (uint32_t i = 0; i < len; i++) {
|
||||
const uint32_t pos = off + i;
|
||||
const uint8_t value = pos >= identity.manifest_offset + 40u &&
|
||||
pos < identity.manifest_offset + 44u ? 0 : buf[i];
|
||||
crc = ota_boot_crc32_update(crc, value);
|
||||
}
|
||||
}
|
||||
return ~crc == identity.crc32;
|
||||
#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;
|
||||
}
|
||||
|
||||
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.
|
||||
static const uint32_t STEP = 512;
|
||||
uint8_t buf[STEP + OTA_BOOT_MANIFEST_SIZE - 1];
|
||||
uint8_t valid_identities = 0;
|
||||
uint8_t valid_caps = 0;
|
||||
for (uint32_t base = 0; base < OTA_BOOT_IMAGE_SIZE; base += STEP) {
|
||||
uint32_t len = OTA_BOOT_IMAGE_SIZE - base;
|
||||
if (len > sizeof(buf)) len = sizeof(buf);
|
||||
if (!store.read(payload_off + base, buf, len)) return false;
|
||||
const uint32_t starts = (OTA_BOOT_IMAGE_SIZE - base < STEP)
|
||||
? OTA_BOOT_IMAGE_SIZE - base : STEP;
|
||||
for (uint32_t local = 0; local < starts; local++) {
|
||||
const uint32_t absolute = base + local;
|
||||
if ((absolute & 3u) == 0 && local + 16u <= len) {
|
||||
OtaBootloaderCapsMarker parsed;
|
||||
if (ota_bootloader_caps_marker_parse(buf + local, parsed) &&
|
||||
parsed.apply_abi >= MOTA_BOOT_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) {
|
||||
if (++valid_caps != 1u) return false;
|
||||
if (parsed.storage_flags == ota_bootloader_update_storage_flags()) caps = parsed;
|
||||
}
|
||||
}
|
||||
if ((absolute & 3u) != 0 || local + OTA_BOOT_MANIFEST_SIZE > len) continue;
|
||||
OtaBootloaderIdentity 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;
|
||||
}
|
||||
}
|
||||
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";
|
||||
}
|
||||
return valid_identities == 1u && valid_caps == 1u &&
|
||||
candidate.present && candidate.crc_ok && caps.present;
|
||||
}
|
||||
|
||||
#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);
|
||||
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 image_hash[32],
|
||||
const uint8_t container_hash[32]) {
|
||||
(void)image_hash;
|
||||
(void)container_hash;
|
||||
return store.approve_for_bootloader();
|
||||
}
|
||||
#endif
|
||||
@@ -893,7 +879,7 @@ static bool ota_prepare_bootloader_update_external(Store& store,
|
||||
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.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)
|
||||
@@ -909,12 +895,23 @@ static bool ota_prepare_bootloader_update_external(Store& store,
|
||||
}
|
||||
OtaBootloaderIdentity candidate;
|
||||
OtaBootloaderCapsMarker candidate_caps;
|
||||
if (!external_bootloader_image_metadata(store, payload_off, candidate, 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)) {
|
||||
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;
|
||||
}
|
||||
@@ -926,7 +923,19 @@ static bool ota_prepare_bootloader_update_external(Store& store,
|
||||
#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)
|
||||
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)
|
||||
@@ -937,6 +946,14 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreSdNrf52& store,
|
||||
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(
|
||||
@@ -956,7 +973,7 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreSdNrf52& store,
|
||||
#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);
|
||||
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_QSPI, "QSPI", false, st, msg);
|
||||
}
|
||||
|
||||
#if defined(OTA_QSPI_BOOTLOADER_UPDATE)
|
||||
@@ -1043,7 +1060,8 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreFlashNrf52& store,
|
||||
}
|
||||
OtaBootloaderIdentity candidate;
|
||||
OtaBootloaderCapsMarker candidate_caps;
|
||||
if (!ota_bootloader_identity_from_image(image, OTA_BOOT_IMAGE_SIZE, candidate) ||
|
||||
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,
|
||||
@@ -1051,6 +1069,15 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreFlashNrf52& store,
|
||||
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;
|
||||
|
||||
@@ -94,6 +94,7 @@ inline OtaBlCaps ota_bl_caps_scan_aligned(const uint8_t* bytes, size_t len,
|
||||
OtaBlCaps c;
|
||||
if (!bytes) return c;
|
||||
uint8_t privileged_count = 0;
|
||||
bool exact_privileged = false;
|
||||
for (size_t off = 0; off + 16u <= len; off += 4u) {
|
||||
const uint8_t* p = bytes + off;
|
||||
if (p[0] != OTA_BL_MAGIC[0] || memcmp(p, OTA_BL_MAGIC, 8) != 0) continue;
|
||||
@@ -103,18 +104,21 @@ 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 &&
|
||||
if (require_boot_update && 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
|
||||
// two copies of the expected exact profile). Malformed magic/literal
|
||||
// decoys were filtered above and do not count.
|
||||
// not silently choose between two otherwise valid structures, even if
|
||||
// one claims a different known storage profile (or only BOOT_UPDATE).
|
||||
// Malformed magic/literal decoys were filtered above and do not count.
|
||||
if (++privileged_count != 1u) return OtaBlCaps();
|
||||
c.present = true;
|
||||
c.apply_abi = abi;
|
||||
c.codec_mask = codecs;
|
||||
c.storage_flags = storage;
|
||||
if (exact_profile) {
|
||||
exact_privileged = true;
|
||||
c.present = true;
|
||||
c.apply_abi = abi;
|
||||
c.codec_mask = codecs;
|
||||
c.storage_flags = storage;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ota_bl_caps_prefer(c, abi, storage, require_boot_update)) {
|
||||
@@ -124,7 +128,8 @@ inline OtaBlCaps ota_bl_caps_scan_aligned(const uint8_t* bytes, size_t len,
|
||||
c.storage_flags = storage;
|
||||
}
|
||||
}
|
||||
if (require_boot_update && privileged_count != 1u) return OtaBlCaps();
|
||||
if (require_boot_update &&
|
||||
(privileged_count != 1u || !exact_privileged)) return OtaBlCaps();
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +52,20 @@ static const uint32_t OTA_BOOT_MANIFEST_MAGIC0 = 0x464D4C42UL; // bytes "BLMF"
|
||||
static const uint32_t OTA_BOOT_MANIFEST_MAGIC1 = 0x31435243UL; // bytes "CRC1"
|
||||
static const uint16_t OTA_BOOT_MANIFEST_VERSION = 1;
|
||||
static const uint16_t OTA_BOOT_MANIFEST_SIZE = 44;
|
||||
static const uint32_t OTA_BOOT_CONTINUITY_MAGIC0 = 0x324D4C42UL; // bytes "BLM2"
|
||||
static const uint32_t OTA_BOOT_CONTINUITY_MAGIC1 = 0x54464F53UL; // bytes "SOFT"
|
||||
static const uint16_t OTA_BOOT_CONTINUITY_VERSION = 2;
|
||||
static const uint16_t OTA_BOOT_CONTINUITY_SIZE = 32;
|
||||
static const uint16_t OTA_BOOT_CONTINUITY_FAMILY_S140 = 140;
|
||||
static const uint16_t OTA_BOOT_CONTINUITY_LAYOUT_ABI = 1;
|
||||
static const uint32_t OTA_BOOT_CONTINUITY_OFFSET = OTA_BOOT_MANIFEST_SIZE;
|
||||
static const uint32_t OTA_BOOT_ENVELOPE_SIZE =
|
||||
OTA_BOOT_MANIFEST_SIZE + OTA_BOOT_CONTINUITY_SIZE;
|
||||
static const uint8_t OTA_BOOT_DEVICE_NAME_SIZE = 16;
|
||||
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_CANDIDATE_MANIFEST_OFFSET =
|
||||
OTA_BOOT_IMAGE_SIZE - OTA_BOOT_ENVELOPE_SIZE; // final 76 bytes: 0x9FB4
|
||||
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;
|
||||
@@ -133,6 +144,13 @@ struct OtaBootloaderIdentity {
|
||||
uint32_t board_id = 0;
|
||||
char device_name[OTA_BOOT_DEVICE_NAME_SIZE + 1] = {0};
|
||||
uint32_t crc32 = 0;
|
||||
bool continuity_present = false;
|
||||
uint32_t boot_version = 0;
|
||||
uint16_t softdevice_family = 0;
|
||||
uint16_t softdevice_fwid = 0;
|
||||
uint32_t app_base = 0;
|
||||
uint16_t layout_abi = 0;
|
||||
uint16_t compat_flags = 0;
|
||||
};
|
||||
|
||||
// Accumulate a stream of already CRC-validated candidates without silently
|
||||
@@ -160,6 +178,12 @@ inline uint32_t ota_boot_rd32(const uint8_t* p) {
|
||||
((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
inline bool ota_bootloader_version_valid(uint32_t version) {
|
||||
// Boot builds use preview 1..254 and 0xFF for a stable release. Zero is not
|
||||
// a boot release, and all-ones must not establish an un-upgradable floor.
|
||||
return version != 0 && version != UINT32_MAX && (version & 0xFFu) != 0;
|
||||
}
|
||||
|
||||
inline bool ota_xiao_bootloader_board_id(uint32_t board_id) {
|
||||
return board_id == OTA_XIAO_BOARD_ID_BASE || board_id == OTA_XIAO_BOARD_ID_SENSE;
|
||||
}
|
||||
@@ -246,11 +270,16 @@ inline uint32_t ota_boot_image_crc32(const uint8_t* image, size_t image_size,
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
// Parse one candidate header. The caller independently validates the complete-region CRC so a magic
|
||||
// string in a literal pool cannot establish an identity.
|
||||
inline bool ota_bootloader_manifest_parse(const uint8_t* raw, uint32_t raw_offset,
|
||||
OtaBootloaderIdentity& out) {
|
||||
if (!raw || ota_boot_rd32(raw) != OTA_BOOT_MANIFEST_MAGIC0 ||
|
||||
// Parse only the legacy-v1 identity envelope. The complete-image CRC is checked
|
||||
// by the caller. Identity scanners deliberately count this base record before
|
||||
// interpreting an adjacent BLM2 extension: deployed legacy bootloaders do the
|
||||
// same, so a CRC-valid base record with a corrupt extension must not disappear
|
||||
// from duplicate-identity accounting.
|
||||
inline bool ota_bootloader_manifest_base_parse(const uint8_t* raw, uint32_t raw_offset,
|
||||
OtaBootloaderIdentity& out,
|
||||
size_t available = OTA_BOOT_MANIFEST_SIZE) {
|
||||
if (!raw || available < OTA_BOOT_MANIFEST_SIZE ||
|
||||
ota_boot_rd32(raw) != OTA_BOOT_MANIFEST_MAGIC0 ||
|
||||
ota_boot_rd32(raw + 4) != OTA_BOOT_MANIFEST_MAGIC1 ||
|
||||
ota_boot_rd16(raw + 8) != OTA_BOOT_MANIFEST_VERSION ||
|
||||
ota_boot_rd16(raw + 10) != OTA_BOOT_MANIFEST_SIZE ||
|
||||
@@ -271,6 +300,52 @@ inline bool ota_bootloader_manifest_parse(const uint8_t* raw, uint32_t raw_offse
|
||||
return true;
|
||||
}
|
||||
|
||||
// Interpret continuity metadata only after the scanner has established that
|
||||
// exactly one CRC-valid base identity exists. Neither magic word means a true
|
||||
// legacy-v1 record. One magic word, truncated metadata, or invalid claimed
|
||||
// metadata fails closed instead of silently downgrading to legacy behavior.
|
||||
inline bool ota_bootloader_continuity_parse(const uint8_t* raw, size_t available,
|
||||
OtaBootloaderIdentity& out) {
|
||||
out.continuity_present = false;
|
||||
out.boot_version = 0;
|
||||
out.softdevice_family = 0;
|
||||
out.softdevice_fwid = 0;
|
||||
out.app_base = 0;
|
||||
out.layout_abi = 0;
|
||||
out.compat_flags = 0;
|
||||
if (!raw || available <= OTA_BOOT_MANIFEST_SIZE) return true;
|
||||
|
||||
const bool magic0 = available >= OTA_BOOT_MANIFEST_SIZE + 4u &&
|
||||
ota_boot_rd32(raw + 44) == OTA_BOOT_CONTINUITY_MAGIC0;
|
||||
const bool magic1 = available >= OTA_BOOT_MANIFEST_SIZE + 8u &&
|
||||
ota_boot_rd32(raw + 48) == OTA_BOOT_CONTINUITY_MAGIC1;
|
||||
if (!magic0 && !magic1) return true;
|
||||
if (available < OTA_BOOT_ENVELOPE_SIZE || !magic0 || !magic1 ||
|
||||
ota_boot_rd16(raw + 52) != OTA_BOOT_CONTINUITY_VERSION ||
|
||||
ota_boot_rd16(raw + 54) != OTA_BOOT_CONTINUITY_SIZE ||
|
||||
!ota_bootloader_version_valid(ota_boot_rd32(raw + 56)) ||
|
||||
ota_boot_rd16(raw + 60) == 0 || ota_boot_rd16(raw + 62) == 0 ||
|
||||
ota_boot_rd32(raw + 64) == 0 || ota_boot_rd16(raw + 68) == 0 ||
|
||||
ota_boot_rd16(raw + 70) != 0 || ota_boot_rd32(raw + 72) != 0) return false;
|
||||
out.continuity_present = true;
|
||||
out.boot_version = ota_boot_rd32(raw + 56);
|
||||
out.softdevice_family = ota_boot_rd16(raw + 60);
|
||||
out.softdevice_fwid = ota_boot_rd16(raw + 62);
|
||||
out.app_base = ota_boot_rd32(raw + 64);
|
||||
out.layout_abi = ota_boot_rd16(raw + 68);
|
||||
out.compat_flags = ota_boot_rd16(raw + 70);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convenience parser for a single already-selected envelope. Whole-image
|
||||
// scanners must use the base parser for duplicate counting first.
|
||||
inline bool ota_bootloader_manifest_parse(const uint8_t* raw, uint32_t raw_offset,
|
||||
OtaBootloaderIdentity& out,
|
||||
size_t available = OTA_BOOT_MANIFEST_SIZE) {
|
||||
return ota_bootloader_manifest_base_parse(raw, raw_offset, out, available) &&
|
||||
ota_bootloader_continuity_parse(raw, available, out);
|
||||
}
|
||||
|
||||
inline bool ota_bootloader_hw_id(const OtaBootloaderIdentity& identity, uint8_t out[32]) {
|
||||
return identity.present && identity.crc_ok &&
|
||||
ota_bootloader_hw_id(identity.board_id,
|
||||
@@ -292,12 +367,30 @@ inline bool ota_bootloader_identity_from_image(const uint8_t* image, size_t imag
|
||||
uint8_t valid_count = 0;
|
||||
for (size_t offset = 0; offset + OTA_BOOT_MANIFEST_SIZE <= image_size; offset += 4) {
|
||||
OtaBootloaderIdentity candidate;
|
||||
if (!ota_bootloader_manifest_parse(image + offset, (uint32_t)offset, candidate)) continue;
|
||||
if (!ota_bootloader_manifest_base_parse(image + offset, (uint32_t)offset, candidate,
|
||||
image_size - offset)) continue;
|
||||
candidate.crc_ok = ota_boot_image_crc32(image, image_size, offset + 40) == candidate.crc32;
|
||||
if (!candidate.crc_ok) continue;
|
||||
if (!ota_bootloader_identity_add_unique(candidate, out, valid_count)) return false;
|
||||
}
|
||||
return valid_count == 1u;
|
||||
return valid_count == 1u &&
|
||||
ota_bootloader_continuity_parse(image + out.manifest_offset,
|
||||
image_size - out.manifest_offset, out);
|
||||
}
|
||||
|
||||
inline bool ota_bootloader_candidate_identity_canonical(
|
||||
const OtaBootloaderIdentity& identity) {
|
||||
return identity.present && identity.crc_ok && identity.continuity_present &&
|
||||
identity.manifest_offset == OTA_BOOT_CANDIDATE_MANIFEST_OFFSET;
|
||||
}
|
||||
|
||||
// New candidates place the complete v1+v2 envelope at one canonical offset.
|
||||
// Installed legacy-v1 discovery above intentionally remains a whole-image
|
||||
// scan for diagnostics and qualified non-SD transition paths.
|
||||
inline bool ota_bootloader_candidate_identity_from_image(
|
||||
const uint8_t* image, size_t image_size, OtaBootloaderIdentity& out) {
|
||||
return ota_bootloader_identity_from_image(image, image_size, out) &&
|
||||
ota_bootloader_candidate_identity_canonical(out);
|
||||
}
|
||||
|
||||
inline bool ota_bootloader_identity_matches(const OtaBootloaderIdentity& a,
|
||||
@@ -308,6 +401,79 @@ inline bool ota_bootloader_identity_matches(const OtaBootloaderIdentity& a,
|
||||
memcmp(a.device_name, b.device_name, OTA_BOOT_DEVICE_NAME_SIZE) == 0;
|
||||
}
|
||||
|
||||
enum OtaBootloaderContinuityGate : uint8_t {
|
||||
OTA_BOOT_CONTINUITY_OK = 0,
|
||||
OTA_BOOT_CONTINUITY_CANDIDATE_MISSING,
|
||||
OTA_BOOT_CONTINUITY_OUTER_VERSION,
|
||||
OTA_BOOT_CONTINUITY_PLATFORM,
|
||||
OTA_BOOT_CONTINUITY_INSTALLED_PLATFORM,
|
||||
OTA_BOOT_CONTINUITY_NOT_NEWER,
|
||||
};
|
||||
|
||||
inline bool ota_bootloader_continuity_matches_runtime(
|
||||
const OtaBootloaderIdentity& identity,
|
||||
uint16_t runtime_softdevice_family,
|
||||
uint16_t runtime_softdevice_fwid,
|
||||
uint32_t runtime_app_base,
|
||||
uint16_t runtime_layout_abi) {
|
||||
return identity.continuity_present &&
|
||||
ota_bootloader_version_valid(identity.boot_version) &&
|
||||
identity.softdevice_family == runtime_softdevice_family &&
|
||||
identity.softdevice_fwid == runtime_softdevice_fwid &&
|
||||
identity.app_base == runtime_app_base &&
|
||||
identity.layout_abi == runtime_layout_abi && identity.compat_flags == 0;
|
||||
}
|
||||
|
||||
// SD OTAFIX accepts geometry only from the reset-retained authorization
|
||||
// record. A legacy BLMF-v1 bootloader predates that protocol and must be
|
||||
// upgraded locally; neither application nor bootloader OTA may fall back to a
|
||||
// raw card sector.
|
||||
inline bool ota_bootloader_sd_retained_auth_ready(
|
||||
const OtaBootloaderIdentity& identity,
|
||||
uint16_t runtime_softdevice_family,
|
||||
uint16_t runtime_softdevice_fwid,
|
||||
uint32_t runtime_app_base,
|
||||
uint16_t runtime_layout_abi) {
|
||||
return identity.present && identity.crc_ok &&
|
||||
ota_bootloader_continuity_matches_runtime(
|
||||
identity, runtime_softdevice_family, runtime_softdevice_fwid,
|
||||
runtime_app_base, runtime_layout_abi);
|
||||
}
|
||||
|
||||
// Qualified internal/QSPI paths may bootstrap a legacy-v1 installed manifest
|
||||
// once because deployed builds do not carry continuity metadata. SD callers
|
||||
// independently require retained-auth continuity before entering this gate.
|
||||
// Every remotely supplied successor must carry the extension. Once v2 is
|
||||
// installed, remote updates are strictly monotonic; rollback/migration remains
|
||||
// a local DFU/SWD operation.
|
||||
inline OtaBootloaderContinuityGate ota_bootloader_continuity_gate(
|
||||
const OtaBootloaderIdentity& installed,
|
||||
const OtaBootloaderIdentity& candidate,
|
||||
uint32_t outer_fw_version,
|
||||
uint16_t runtime_softdevice_family,
|
||||
uint16_t runtime_softdevice_fwid,
|
||||
uint32_t runtime_app_base,
|
||||
uint16_t runtime_layout_abi) {
|
||||
if (!candidate.continuity_present)
|
||||
return OTA_BOOT_CONTINUITY_CANDIDATE_MISSING;
|
||||
if (!ota_bootloader_version_valid(candidate.boot_version) ||
|
||||
outer_fw_version != candidate.boot_version)
|
||||
return OTA_BOOT_CONTINUITY_OUTER_VERSION;
|
||||
if (!ota_bootloader_continuity_matches_runtime(
|
||||
candidate, runtime_softdevice_family, runtime_softdevice_fwid,
|
||||
runtime_app_base, runtime_layout_abi))
|
||||
return OTA_BOOT_CONTINUITY_PLATFORM;
|
||||
if (installed.continuity_present) {
|
||||
if (!ota_bootloader_continuity_matches_runtime(
|
||||
installed, runtime_softdevice_family, runtime_softdevice_fwid,
|
||||
runtime_app_base, runtime_layout_abi))
|
||||
return OTA_BOOT_CONTINUITY_INSTALLED_PLATFORM;
|
||||
if (candidate.boot_version <= installed.boot_version)
|
||||
return OTA_BOOT_CONTINUITY_NOT_NEWER;
|
||||
}
|
||||
return OTA_BOOT_CONTINUITY_OK;
|
||||
}
|
||||
|
||||
// Cheap first-line rejection of a malformed raw bootloader region. OTAFIX repeats this check before
|
||||
// touching internal flash. MSP may point one byte beyond RAM; the reset vector must be Thumb code in
|
||||
// the exact bootloader image range.
|
||||
@@ -331,12 +497,13 @@ struct OtaBootloaderCapsMarker {
|
||||
inline bool ota_bootloader_caps_marker_parse(const uint8_t raw[16],
|
||||
OtaBootloaderCapsMarker& out) {
|
||||
static const uint8_t magic[8] = {'M','O','T','A','B','L','D','R'};
|
||||
if (!raw || memcmp(raw, magic, sizeof(magic)) != 0 ||
|
||||
raw[8] == 0 || (raw[8] == 0xFF && raw[9] == 0xFF) ||
|
||||
if (!raw || memcmp(raw, magic, sizeof(magic)) != 0) return false;
|
||||
const uint16_t apply_abi = ota_boot_rd16(raw + 8);
|
||||
if (apply_abi == 0 || apply_abi == UINT16_MAX ||
|
||||
(raw[10] == 0 && raw[11] == 0) || (raw[12] & ~OTA_BL_STORAGE_KNOWN) != 0 ||
|
||||
raw[13] != 0 || raw[14] != 0 || raw[15] != 0) return false;
|
||||
out.present = true;
|
||||
out.apply_abi = ota_boot_rd16(raw + 8);
|
||||
out.apply_abi = apply_abi;
|
||||
out.codec_mask = ota_boot_rd16(raw + 10);
|
||||
out.storage_flags = raw[12];
|
||||
return true;
|
||||
@@ -362,6 +529,84 @@ inline bool ota_bootloader_caps_from_image(const uint8_t* image, size_t image_si
|
||||
return valid_count == 1u && out.present;
|
||||
}
|
||||
|
||||
// Stream the bootloader payload through an arbitrary random-access store. Keeping
|
||||
// this scanner beside the memory-image parser makes the canonical-envelope and
|
||||
// duplicate-marker policy directly testable and identical for SD/QSPI staging.
|
||||
template <typename Store>
|
||||
inline bool ota_bootloader_external_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)) {
|
||||
uint32_t len = OTA_BOOT_IMAGE_SIZE - off;
|
||||
if (len > sizeof(buf)) len = sizeof(buf);
|
||||
if (!store.read(payload_off + off, buf, len)) return false;
|
||||
for (uint32_t i = 0; i < len; ++i) {
|
||||
const uint32_t pos = off + i;
|
||||
const uint8_t value = pos >= identity.manifest_offset + 40u &&
|
||||
pos < identity.manifest_offset + 44u ? 0 : buf[i];
|
||||
crc = ota_boot_crc32_update(crc, value);
|
||||
}
|
||||
}
|
||||
return ~crc == identity.crc32;
|
||||
}
|
||||
|
||||
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) {
|
||||
candidate = OtaBootloaderIdentity();
|
||||
caps = OtaBootloaderCapsMarker();
|
||||
if (exact_storage_flags == 0u) return false;
|
||||
|
||||
static const uint32_t STEP = 512u;
|
||||
uint8_t buf[STEP + OTA_BOOT_ENVELOPE_SIZE - 1u];
|
||||
uint8_t valid_identities = 0;
|
||||
uint8_t valid_caps = 0;
|
||||
for (uint32_t base = 0; base < OTA_BOOT_IMAGE_SIZE; base += STEP) {
|
||||
uint32_t len = OTA_BOOT_IMAGE_SIZE - base;
|
||||
if (len > sizeof(buf)) len = sizeof(buf);
|
||||
if (!store.read(payload_off + base, buf, len)) return false;
|
||||
const uint32_t starts = (OTA_BOOT_IMAGE_SIZE - base < STEP)
|
||||
? OTA_BOOT_IMAGE_SIZE - base : STEP;
|
||||
for (uint32_t local = 0; local < starts; ++local) {
|
||||
const uint32_t absolute = base + local;
|
||||
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.codec_mask & OTA_BL_REQUIRED_APP_CODEC_MASK) ==
|
||||
OTA_BL_REQUIRED_APP_CODEC_MASK &&
|
||||
(parsed.storage_flags & OTA_BL_STORAGE_BOOT_UPDATE) != 0u) {
|
||||
if (++valid_caps != 1u) return false;
|
||||
if (parsed.storage_flags == exact_storage_flags) caps = parsed;
|
||||
}
|
||||
}
|
||||
if ((absolute & 3u) != 0u ||
|
||||
local + OTA_BOOT_MANIFEST_SIZE > len) continue;
|
||||
OtaBootloaderIdentity parsed;
|
||||
if (!ota_bootloader_manifest_base_parse(
|
||||
buf + local, absolute, parsed, len - local)) continue;
|
||||
if (ota_bootloader_external_crc_ok(store, payload_off, parsed)) {
|
||||
parsed.crc_ok = true;
|
||||
if (!ota_bootloader_identity_add_unique(
|
||||
parsed, candidate, valid_identities)) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (valid_identities != 1u || valid_caps != 1u || !caps.present ||
|
||||
candidate.manifest_offset > OTA_BOOT_IMAGE_SIZE - OTA_BOOT_MANIFEST_SIZE)
|
||||
return false;
|
||||
const uint32_t available = OTA_BOOT_IMAGE_SIZE - candidate.manifest_offset;
|
||||
uint8_t envelope[OTA_BOOT_ENVELOPE_SIZE];
|
||||
uint32_t envelope_len = available;
|
||||
if (envelope_len > sizeof(envelope)) envelope_len = sizeof(envelope);
|
||||
if (!store.read(payload_off + candidate.manifest_offset, envelope, envelope_len) ||
|
||||
!ota_bootloader_continuity_parse(envelope, envelope_len, candidate)) return false;
|
||||
return ota_bootloader_candidate_identity_canonical(candidate);
|
||||
}
|
||||
|
||||
enum OtaBootloaderConfirmGate : uint8_t {
|
||||
OTA_BOOT_CONFIRM_OK = 0,
|
||||
OTA_BOOT_CONFIRM_NOT_BOOT_PACKAGE,
|
||||
|
||||
@@ -109,6 +109,7 @@ static const char* fetch_error_word(OtaManager::FetchError error) {
|
||||
switch (error) {
|
||||
case OtaManager::FETCH_ERROR_MANIFEST: return "invalid manifest";
|
||||
case OtaManager::FETCH_ERROR_HASH_ALGO: return "unsupported hash";
|
||||
case OtaManager::FETCH_ERROR_VERSION: return "not a newer version";
|
||||
case OtaManager::FETCH_ERROR_CODEC: return "unsupported codec";
|
||||
case OtaManager::FETCH_ERROR_GEOMETRY: return "invalid geometry";
|
||||
case OtaManager::FETCH_ERROR_TOO_LARGE: return "image too large";
|
||||
@@ -1051,8 +1052,23 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
|
||||
sprintf(reply, "OK serving | root=%d payload=%d img=%d sig=%d trust=%d",
|
||||
r.root_ok, r.payload_ok, r.image_ok, r.sig_ok, r.trusted);
|
||||
|
||||
} else if (strncmp(d, "resume", 6) == 0) { // re-adopt a container already staged in flash (test/debug)
|
||||
bool ok = c.manager.resumeStaged(nullptr);
|
||||
} else if (is_cmd(d, "resume", &d)) { // explicit re-adopt of a known/active staged MID (test/debug)
|
||||
uint8_t mid[4];
|
||||
if (*d) {
|
||||
if (!parse_hex_exact(d, mid, sizeof(mid))) {
|
||||
strcpy(reply, "ERR usage: ota dev resume [mid8]");
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// With no argument, reuse only an active/requested session MID. After a reboot the operator must
|
||||
// name the persisted MID explicitly; nullptr is reserved for policy-governed automatic adoption.
|
||||
if (c.manager.fetchState() == OtaManager::IDLE) {
|
||||
strcpy(reply, "ERR usage: ota dev resume <mid8>");
|
||||
return true;
|
||||
}
|
||||
memcpy(mid, c.manager.fetchManifestId(), sizeof(mid));
|
||||
}
|
||||
bool ok = c.manager.resumeStagedExplicit(mid, 0);
|
||||
sprintf(reply, "%s resume: sess=%c %u/%u", ok ? "OK" : "ERR", fstate_char(c.manager.fetchState()),
|
||||
(unsigned)c.manager.blocksHave(), (unsigned)c.manager.blocksTotal());
|
||||
|
||||
@@ -1125,7 +1141,7 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
|
||||
strcpy(reply, "OK cleared");
|
||||
|
||||
} else {
|
||||
strcpy(reply, "ota dev: stage|recv|serve|announce|verify|want|apply slot|manifest|verify|commit|clear");
|
||||
strcpy(reply, "ota dev: stage|recv|serve|announce|resume [mid8]|verify|want|apply|clear");
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
|
||||
@@ -146,7 +146,13 @@ struct OtaContext {
|
||||
// so the deferred-reboot path (mesh loop) takes over. Caller ensures the fetch is COMPLETE. Shared by
|
||||
// manual `ota applydelta` and the auto-install path.
|
||||
bool apply_fetched(char* msg) {
|
||||
return apply_fetched_impl(nullptr, msg);
|
||||
return apply_fetched_impl(nullptr, false, msg);
|
||||
}
|
||||
|
||||
// Automatic installation has an additional anti-replay/rollback gate. Manual `ota install` remains
|
||||
// the deliberate operator override for equal or older signed firmware.
|
||||
bool apply_fetched_trusted_auto(char* msg) {
|
||||
return apply_fetched_impl(nullptr, true, msg);
|
||||
}
|
||||
|
||||
// Explicit recovery entry point for an internal-flash nRF52 whose normal EndF validation fails. This
|
||||
@@ -158,11 +164,11 @@ struct OtaContext {
|
||||
msg[95] = 0;
|
||||
return false;
|
||||
#else
|
||||
return apply_fetched_impl(operator_base_hash, msg);
|
||||
return apply_fetched_impl(operator_base_hash, false, msg);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool apply_fetched_impl(const uint8_t* rescue_base_hash, char* msg) {
|
||||
bool apply_fetched_impl(const uint8_t* rescue_base_hash, bool trusted_auto, char* msg) {
|
||||
if (fetch_to_folder) {
|
||||
strncpy(msg, "refused: completed fetch was captured to a folder, not local install storage", 96);
|
||||
msg[95] = 0;
|
||||
@@ -209,6 +215,21 @@ struct OtaContext {
|
||||
strncpy(msg, "refused: use ota bootloader install <MID8> <HASH16>", 96);
|
||||
msg[95] = 0; return false;
|
||||
}
|
||||
if (trusted_auto) {
|
||||
SelfFwInfo self;
|
||||
if (!mm.is_signed()) {
|
||||
strncpy(msg, "refused: trusted auto-install requires a signed package", 96);
|
||||
msg[95] = 0; return false;
|
||||
}
|
||||
if (!ota_self_firmware(self) || !self.valid ||
|
||||
!ota_trusted_auto_version_allows(self.fw_version, mm.fw_version)) {
|
||||
snprintf(msg, 96,
|
||||
"refused: trusted auto-install is forward-only (running=%08lX candidate=%08lX); use ota install to override",
|
||||
(unsigned long)(self.valid ? self.fw_version : 0),
|
||||
(unsigned long)mm.fw_version);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!hwMatches(mm.hw_id)) {
|
||||
char want[33] = {0}; memcpy(want, mm.hw_id, 32);
|
||||
snprintf(msg, 96, "refused: .mota hw_id '%.32s' != this device '%s' (incompatible hardware)", want, hw_id);
|
||||
@@ -501,6 +522,7 @@ struct OtaContext {
|
||||
target_id = 0;
|
||||
#endif
|
||||
manager.begin(target_id, send, ctx);
|
||||
manager.set_auto_version_floor(_fi.valid ? _fi.fw_version : 0, true);
|
||||
fetch_to_folder = false;
|
||||
if (hw) { strncpy(hw_id, hw, sizeof(hw_id) - 1); hw_id[sizeof(hw_id) - 1] = 0; }
|
||||
// a node only fetches firmware it can apply: ESP32 A/B -> sequential, nRF52 single-slot -> in-place
|
||||
|
||||
@@ -68,6 +68,12 @@ struct FwVersion {
|
||||
}
|
||||
};
|
||||
|
||||
inline bool ota_trusted_auto_version_allows(uint32_t running_version,
|
||||
uint32_t candidate_version) {
|
||||
return running_version != 0 && candidate_version != 0 &&
|
||||
candidate_version > running_version;
|
||||
}
|
||||
|
||||
// ---- hash truncations -----------------------------------------------------
|
||||
static const uint8_t MH4 = 4; // sha2-256:4 (merkle leaves/nodes/root/proofs)
|
||||
static const uint8_t MH8 = 8; // sha2-256:8 (base/EndF body hash)
|
||||
|
||||
@@ -901,7 +901,7 @@ void OtaManager::handleHave(const uint8_t* m, uint16_t n) {
|
||||
if (c.seeder_have[k] > c.have_max) c.have_max = c.seeder_have[k];
|
||||
if (c.seeder_last_ms[k] > c.last_ms) c.last_ms = c.seeder_last_ms[k];
|
||||
}
|
||||
if (wantRow(mid, target, codec, flags)) startFetch(mid, target);
|
||||
if (wantRow(mid, target, fwver, codec, flags)) startFetch(mid, target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -916,7 +916,8 @@ void OtaManager::deferCatalog(const uint8_t mid[4], uint32_t until_ms) {
|
||||
}
|
||||
}
|
||||
|
||||
bool OtaManager::wantRow(const uint8_t* mid, uint32_t target, uint8_t codec, uint8_t flags) const {
|
||||
bool OtaManager::wantRow(const uint8_t* mid, uint32_t target, uint32_t fw_version,
|
||||
uint8_t codec, uint8_t flags) const {
|
||||
if (!_fetch || _fstate == FETCHING || _fstate == WANT_MANIFEST
|
||||
|| _fstate == WANT_LEAVES || _fstate == VERIFYING_STAGED
|
||||
|| _fstate == PAUSED) return false; // busy
|
||||
@@ -931,6 +932,8 @@ bool OtaManager::wantRow(const uint8_t* mid, uint32_t target, uint8_t codec, uin
|
||||
if (_autofetch == AUTOFETCH_OFF) return false; // discover only
|
||||
if (target != _target) return false; // auto-fetch = our own target
|
||||
if (_autofetch == AUTOFETCH_SIGNED && !(flags & MFLAG_SIGNED)) return false; // signed-only policy
|
||||
if (_enforce_auto_version &&
|
||||
!ota_trusted_auto_version_allows(_running_fw_version, fw_version)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1068,6 +1071,7 @@ void OtaManager::clearFetchIntent() {
|
||||
_desired_target = 0;
|
||||
_have_desired_mid = false;
|
||||
memset(_desired_mid, 0, sizeof(_desired_mid));
|
||||
_fexpected_target = 0;
|
||||
}
|
||||
|
||||
void OtaManager::failFetch(FetchError error) {
|
||||
@@ -1121,11 +1125,11 @@ OtaManager::PullResult OtaManager::pull_archive(const uint8_t* mid, uint32_t tar
|
||||
|
||||
// Begin (or resume) fetching a chosen mid: try a staged-partial resume first, else request the manifest.
|
||||
OtaManager::PullResult OtaManager::startFetch(const uint8_t* mid, uint32_t target, bool validate) {
|
||||
(void)target;
|
||||
if (!mid) return PULL_BAD_MID;
|
||||
if (!_fetch) return PULL_NO_STORE;
|
||||
if (fetchActive()) return PULL_BUSY;
|
||||
_fetch_error = FETCH_ERROR_NONE;
|
||||
_fexpected_target = target;
|
||||
_validate = validate; // motatool folder-capture warm-start (seed leaf-diff)
|
||||
// A validate pull is a FRESH seed capture, not a resume: the store already holds the seed's payload (not a
|
||||
// real partial), so never adopt it via resumeStaged - always re-begin and run the manifest->leaves->diff.
|
||||
@@ -1142,6 +1146,20 @@ OtaManager::PullResult OtaManager::startFetch(const uint8_t* mid, uint32_t targe
|
||||
return PULL_STARTED;
|
||||
}
|
||||
|
||||
bool OtaManager::resumeStagedExplicit(const uint8_t* want_mid, uint32_t expected_target) {
|
||||
if (!want_mid || !_fetch || fetchActive()) return false;
|
||||
_fetch_error = FETCH_ERROR_NONE;
|
||||
_validate = false;
|
||||
_archive_fetch = false;
|
||||
_desired_target = expected_target;
|
||||
memcpy(_desired_mid, want_mid, sizeof(_desired_mid));
|
||||
_have_desired_mid = true;
|
||||
_fexpected_target = expected_target;
|
||||
const bool adopted = resumeStaged(want_mid);
|
||||
if (!adopted) clearFetchIntent();
|
||||
return adopted;
|
||||
}
|
||||
|
||||
void OtaManager::handleManifest(const uint8_t* m, uint16_t n) {
|
||||
ManifestMsg mm;
|
||||
if (!decode_manifest(m, n, mm) || !_fetch) return;
|
||||
@@ -1174,6 +1192,24 @@ void OtaManager::handleManifest(const uint8_t* m, uint16_t n) {
|
||||
if (mfl != MOTA_MFL) { failFetch(FETCH_ERROR_MANIFEST); return; } // fixed 197-byte manifest
|
||||
MotaManifest parsed;
|
||||
if (!mota_parse_manifest(mf, mfl, parsed)) { failFetch(FETCH_ERROR_MANIFEST); return; }
|
||||
// The OTA_MANIFEST envelope is supplied by the peer and is not an authenticated description of the
|
||||
// bytes inside it. Bind both identifiers from the parsed manifest to the catalog row / explicit pull
|
||||
// that opened this receive slot before the store is planned, erased, or written. Otherwise a peer can
|
||||
// label an unrelated manifest with the requested MID and make us stage a different target/package.
|
||||
if (memcmp(parsed.merkle_root, _fid, sizeof(_fid)) != 0 ||
|
||||
(_fexpected_target != 0 && parsed.target_id != _fexpected_target)) {
|
||||
failFetch(FETCH_ERROR_MANIFEST); return;
|
||||
}
|
||||
// A HAVE row is advisory and may lie about its version. Repeat the
|
||||
// forward-only check against the parsed manifest before touching storage.
|
||||
const bool automatic_fetch = !_have_desired_mid && _desired_target == 0 && !_archive_fetch;
|
||||
if (_enforce_auto_version && automatic_fetch &&
|
||||
!ota_trusted_auto_version_allows(_running_fw_version, parsed.fw_version)) {
|
||||
failFetch(FETCH_ERROR_VERSION); return;
|
||||
}
|
||||
if (automatic_fetch && _autofetch == AUTOFETCH_SIGNED && !parsed.is_signed()) {
|
||||
failFetch(FETCH_ERROR_MANIFEST); return;
|
||||
}
|
||||
if (parsed.hash_algo != HASH_ALGO_SHA256) { failFetch(FETCH_ERROR_HASH_ALGO); return; }
|
||||
if (!_archive_fetch &&
|
||||
!fetchCodecOk(parsed.codec_id, parsed.is_bootloader(), _have_desired_mid)) {
|
||||
@@ -1190,7 +1226,7 @@ void OtaManager::handleManifest(const uint8_t* m, uint16_t n) {
|
||||
if (_archive_fetch && (uint64_t)bc * 4 > OTA_PROOFGEN_SCRATCH) {
|
||||
failFetch(FETCH_ERROR_TOO_LARGE); return; // retaining an image we cannot seed is useless
|
||||
}
|
||||
memcpy(_froot, mf + 20, 4);
|
||||
memcpy(_froot, parsed.merkle_root, sizeof(_froot));
|
||||
|
||||
uint32_t leaves_off = 8 + mfl;
|
||||
uint32_t payload_off = leaves_off + bc * 4;
|
||||
@@ -1324,6 +1360,18 @@ bool OtaManager::resumeStaged(const uint8_t* want_mid) {
|
||||
MotaManifest m;
|
||||
if (!_fetch->read(8, mbuf, mread) || !mota_parse_manifest(mbuf, mread, m)) return false;
|
||||
if (want_mid && memcmp(m.merkle_root, want_mid, 4) != 0) return false; // a different fw is staged
|
||||
const bool automatic_resume = want_mid == nullptr;
|
||||
// A boot-time resume is an automatic fetch decision and therefore belongs
|
||||
// only to this node. An explicit pull retains target=0 as an intentional
|
||||
// MID-only wildcard instead of silently narrowing it to the local target.
|
||||
const uint32_t expected_target = automatic_resume ? _target : _fexpected_target;
|
||||
if (expected_target != 0 && m.target_id != expected_target) return false;
|
||||
if (automatic_resume) {
|
||||
if (_autofetch == AUTOFETCH_OFF) return false;
|
||||
if (_autofetch == AUTOFETCH_SIGNED && !m.is_signed()) return false;
|
||||
if (_enforce_auto_version &&
|
||||
!ota_trusted_auto_version_allows(_running_fw_version, m.fw_version)) return false;
|
||||
}
|
||||
if (m.hash_algo != HASH_ALGO_SHA256) return false;
|
||||
// A bootloader partial is resumed only as part of the same explicit MID pull.
|
||||
// Boot-time resumeStaged(nullptr) must not silently re-adopt privileged data.
|
||||
|
||||
@@ -221,6 +221,7 @@ public:
|
||||
FETCH_ERROR_NONE,
|
||||
FETCH_ERROR_MANIFEST,
|
||||
FETCH_ERROR_HASH_ALGO,
|
||||
FETCH_ERROR_VERSION,
|
||||
FETCH_ERROR_CODEC,
|
||||
FETCH_ERROR_GEOMETRY,
|
||||
FETCH_ERROR_TOO_LARGE,
|
||||
@@ -302,13 +303,19 @@ public:
|
||||
// --- fetch --- Provide the staging store; fetching starts on a matching OTA_ADV.
|
||||
void set_fetch_store(OtaStore* s) { _fetch = s; }
|
||||
|
||||
// Resume a fetch from a container already persisted in the store (after a reboot). want_mid=nullptr
|
||||
// accepts whatever is staged; otherwise only resumes if the staged manifest_id matches. Re-parses the
|
||||
// stored manifest, recomputes geometry, then incrementally rehashes every staged payload block before
|
||||
// continuing FETCHING the holes. A fully staged image also has to reproduce the manifest Merkle root
|
||||
// before it can become COMPLETE. Returns true if it adopted a staged container.
|
||||
// Resume a fetch from a container already persisted in the store (after a reboot). want_mid=nullptr is
|
||||
// automatic adoption and rechecks current autofetch/target/version policy; a non-null MID is an explicit
|
||||
// resume and must match exactly. Re-parses the stored manifest, recomputes geometry, then incrementally
|
||||
// rehashes every staged payload block before continuing FETCHING the holes. A fully staged image also has
|
||||
// to reproduce the manifest Merkle root before it can become COMPLETE. Returns true if it adopted one.
|
||||
bool resumeStaged(const uint8_t* want_mid);
|
||||
|
||||
// Explicit operator/debug re-adoption of one known MID without starting a network fetch. This installs
|
||||
// the same manual intent used by pull(), so target=0 remains a deliberate wildcard and bootloader FULL
|
||||
// containers are eligible for the separately authenticated manual path. Unlike resumeStaged(nullptr),
|
||||
// this never applies boot-time autofetch/version policy.
|
||||
bool resumeStagedExplicit(const uint8_t* want_mid, uint32_t expected_target = 0);
|
||||
|
||||
// Manual cross-target override (decision: deliberate role switch, e.g. companion -> repeater on the
|
||||
// same hardware). Normally a node only auto-fetches its OWN target_id; `want(T)` makes it accept an
|
||||
// ADV for target T instead (T=0 restores auto). The user takes responsibility for HW compatibility;
|
||||
@@ -377,6 +384,10 @@ public:
|
||||
static const uint8_t AUTOFETCH_OFF = 0, AUTOFETCH_ANY = 1, AUTOFETCH_SIGNED = 2;
|
||||
void set_autofetch(uint8_t p) { _autofetch = p; reDiscover(); }
|
||||
uint8_t autofetch() const { return _autofetch; }
|
||||
void set_auto_version_floor(uint32_t running_version, bool enforce = true) {
|
||||
_running_fw_version = running_version;
|
||||
_enforce_auto_version = enforce;
|
||||
}
|
||||
// A persistent archive needs full catalogs even when install-oriented autofetch is off.
|
||||
void set_archive_interest(bool on) {
|
||||
if (_archive_interest != on) { _archive_interest = on; reDiscover(); }
|
||||
@@ -488,7 +499,8 @@ private:
|
||||
bool handleReqProof(const uint8_t* m, uint16_t n);
|
||||
bool handleProof(const uint8_t* m, uint16_t n);
|
||||
PullResult startFetch(const uint8_t* mid, uint32_t target, bool validate = false); // begin/resume a fetch
|
||||
bool wantRow(const uint8_t* mid, uint32_t target, uint8_t codec, uint8_t flags) const; // fetch this row?
|
||||
bool wantRow(const uint8_t* mid, uint32_t target, uint32_t fw_version,
|
||||
uint8_t codec, uint8_t flags) const; // fetch this row?
|
||||
bool fetchActive() const;
|
||||
void clearFetchIntent();
|
||||
void failFetch(FetchError error);
|
||||
@@ -599,6 +611,7 @@ private:
|
||||
FetchError _fetch_error = FETCH_ERROR_NONE;
|
||||
uint8_t _fid[4] = {0};
|
||||
uint8_t _froot[4] = {0};
|
||||
uint32_t _fexpected_target = 0; // catalog/CLI target bound to the requested manifest
|
||||
uint32_t _ftotal = 0, _fpoff = 0, _floff = 0, _fpsize = 0, _fbc = 0, _fbs = 0;
|
||||
uint32_t _have = 0;
|
||||
uint32_t _resume_verify_idx = 0;
|
||||
@@ -616,6 +629,8 @@ private:
|
||||
bool _archive_interest = false; // query full catalogs for the persistent archive
|
||||
uint8_t _seeder_id[4] = {0,0,0,0}; // our node id (pubkey[0:4]) for advert seeder counting
|
||||
uint8_t _autofetch = AUTOFETCH_OFF; // auto-fetch policy (persisted in NodePrefs)
|
||||
uint32_t _running_fw_version = 0; // trusted EndF version used only for automatic admission
|
||||
bool _enforce_auto_version = false; // OtaContext enables this even when EndF/version is unknown
|
||||
uint16_t _checkpoint_blocks = OTA_CHECKPOINT_BLOCKS; // resume checkpoint cadence (persisted)
|
||||
uint16_t _advert_mins = OTA_ADVERT_INTERVAL_MINS; // beacon re-advertise cadence, minutes; 0=off (persisted)
|
||||
uint8_t _max_hops = OTA_HOP_LIMIT_DEFAULT; // OTA flood reach in hops; 0=direct only (persisted)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
// Reset-retained authorization shared byte-for-byte with OTAFIX. This record is intentionally in MCU RAM,
|
||||
// not on removable media: the application publishes it only after authenticating/verifying the exact SD
|
||||
// container, and OTAFIX consumes it before trusting any card geometry or bytes. A power cycle erases it.
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
static const uintptr_t MOTA_SD_AUTH_ADDR = 0x20006008u;
|
||||
static const uint32_t MOTA_SD_AUTH_SECTOR_SIZE = 512u;
|
||||
static const uint16_t MOTA_SD_AUTH_VERSION = 2u;
|
||||
static const uint16_t MOTA_SD_AUTH_LEN = 72u;
|
||||
static const uint8_t MOTA_SD_AUTH_PURPOSE_APP = 1u;
|
||||
static const uint8_t MOTA_SD_AUTH_PURPOSE_BOOTLOADER = 2u;
|
||||
static const uint8_t MOTA_SD_AUTH_MAGIC[8] = {
|
||||
'M', 'O', 'T', 'A', 'S', 'D', 'A', '2'
|
||||
};
|
||||
|
||||
inline uint16_t mota_sd_auth_rd16(const uint8_t* p) {
|
||||
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
|
||||
}
|
||||
|
||||
inline uint32_t mota_sd_auth_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);
|
||||
}
|
||||
|
||||
inline void mota_sd_auth_wr16(uint8_t* p, uint16_t value) {
|
||||
p[0] = (uint8_t)value;
|
||||
p[1] = (uint8_t)(value >> 8);
|
||||
}
|
||||
|
||||
inline void mota_sd_auth_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);
|
||||
}
|
||||
|
||||
inline uint32_t mota_sd_auth_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;
|
||||
}
|
||||
|
||||
inline bool mota_sd_auth_geometry_valid(uint32_t first_lba, uint32_t sector_count,
|
||||
uint32_t container_total, uint32_t card_sectors) {
|
||||
const uint64_t needed = ((uint64_t)container_total + MOTA_SD_AUTH_SECTOR_SIZE - 1u) /
|
||||
MOTA_SD_AUTH_SECTOR_SIZE;
|
||||
return first_lba != 0 && sector_count != 0 && container_total != 0 &&
|
||||
card_sectors != 0 && needed == sector_count &&
|
||||
first_lba < card_sectors && sector_count <= card_sectors - first_lba;
|
||||
}
|
||||
|
||||
inline bool mota_sd_auth_encode(uint8_t out[MOTA_SD_AUTH_LEN], uint8_t purpose,
|
||||
uint8_t format, uint32_t first_lba,
|
||||
uint32_t sector_count, uint32_t container_total,
|
||||
uint32_t card_sectors,
|
||||
const uint8_t container_sha256[32]) {
|
||||
if (!out || !container_sha256 ||
|
||||
(purpose != MOTA_SD_AUTH_PURPOSE_APP &&
|
||||
purpose != MOTA_SD_AUTH_PURPOSE_BOOTLOADER) ||
|
||||
((purpose == MOTA_SD_AUTH_PURPOSE_APP && format != 2u) ||
|
||||
(purpose == MOTA_SD_AUTH_PURPOSE_BOOTLOADER && format != 3u)) ||
|
||||
!mota_sd_auth_geometry_valid(first_lba, sector_count, container_total, card_sectors))
|
||||
return false;
|
||||
memset(out, 0, MOTA_SD_AUTH_LEN);
|
||||
memcpy(out, MOTA_SD_AUTH_MAGIC, sizeof(MOTA_SD_AUTH_MAGIC));
|
||||
mota_sd_auth_wr16(out + 8u, MOTA_SD_AUTH_VERSION);
|
||||
mota_sd_auth_wr16(out + 10u, MOTA_SD_AUTH_LEN);
|
||||
out[12] = purpose;
|
||||
out[13] = format;
|
||||
mota_sd_auth_wr32(out + 16u, first_lba);
|
||||
mota_sd_auth_wr32(out + 20u, sector_count);
|
||||
mota_sd_auth_wr32(out + 24u, container_total);
|
||||
mota_sd_auth_wr32(out + 28u, card_sectors);
|
||||
memcpy(out + 32u, container_sha256, 32u);
|
||||
const uint32_t crc = mota_sd_auth_crc32(out, 64u);
|
||||
mota_sd_auth_wr32(out + 64u, crc);
|
||||
mota_sd_auth_wr32(out + 68u, ~crc);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool mota_sd_auth_valid(const uint8_t record[MOTA_SD_AUTH_LEN]) {
|
||||
if (!record || memcmp(record, MOTA_SD_AUTH_MAGIC, sizeof(MOTA_SD_AUTH_MAGIC)) != 0 ||
|
||||
mota_sd_auth_rd16(record + 8u) != MOTA_SD_AUTH_VERSION ||
|
||||
mota_sd_auth_rd16(record + 10u) != MOTA_SD_AUTH_LEN ||
|
||||
mota_sd_auth_rd16(record + 14u) != 0 ||
|
||||
((record[12] == MOTA_SD_AUTH_PURPOSE_APP && record[13] != 2u) ||
|
||||
(record[12] == MOTA_SD_AUTH_PURPOSE_BOOTLOADER && record[13] != 3u) ||
|
||||
(record[12] != MOTA_SD_AUTH_PURPOSE_APP &&
|
||||
record[12] != MOTA_SD_AUTH_PURPOSE_BOOTLOADER)))
|
||||
return false;
|
||||
const uint32_t crc = mota_sd_auth_rd32(record + 64u);
|
||||
return mota_sd_auth_geometry_valid(mota_sd_auth_rd32(record + 16u),
|
||||
mota_sd_auth_rd32(record + 20u),
|
||||
mota_sd_auth_rd32(record + 24u),
|
||||
mota_sd_auth_rd32(record + 28u)) &&
|
||||
mota_sd_auth_crc32(record, 64u) == crc &&
|
||||
mota_sd_auth_rd32(record + 68u) == ~crc;
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -1,64 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// On-card handoff shared with Adafruit_nRF52_Bootloader_OTAFIX. The staged
|
||||
// .mota is a normal, contiguous filesystem file. Sector 1 is in the unused
|
||||
// gap between the MBR and the first partition and tells the bootloader where
|
||||
// that file's sectors live. Cards without such a gap are rejected.
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
static const uint32_t MOTA_SD_SECTOR_SIZE = 512u;
|
||||
static const uint32_t MOTA_SD_HANDOFF_SECTOR = 1u;
|
||||
static const uint32_t MOTA_SD_HANDOFF_VERSION = 1u;
|
||||
static const uint32_t MOTA_SD_HANDOFF_LEN = 36u;
|
||||
static const uint8_t MOTA_SD_HANDOFF_MAGIC[8] = {
|
||||
'M', 'O', 'T', 'A', 'S', 'D', '0', '1'
|
||||
};
|
||||
|
||||
inline uint32_t mota_sd_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);
|
||||
}
|
||||
|
||||
inline void mota_sd_wr32(uint8_t* p, uint32_t v) {
|
||||
p[0] = (uint8_t)v;
|
||||
p[1] = (uint8_t)(v >> 8);
|
||||
p[2] = (uint8_t)(v >> 16);
|
||||
p[3] = (uint8_t)(v >> 24);
|
||||
}
|
||||
|
||||
inline uint32_t mota_sd_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;
|
||||
}
|
||||
|
||||
inline void mota_sd_encode_handoff(uint8_t sector[MOTA_SD_SECTOR_SIZE],
|
||||
uint32_t first_sector,
|
||||
uint32_t sector_count,
|
||||
uint32_t total_size,
|
||||
uint32_t card_sectors) {
|
||||
// Only own the record bytes. The caller preserves the rest of sector 1 in
|
||||
// case a card formatter placed non-partition metadata there.
|
||||
for (uint32_t i = 0; i < MOTA_SD_HANDOFF_LEN; i++) sector[i] = 0xFF;
|
||||
for (uint8_t i = 0; i < 8; i++) sector[i] = MOTA_SD_HANDOFF_MAGIC[i];
|
||||
mota_sd_wr32(sector + 8, MOTA_SD_HANDOFF_VERSION);
|
||||
mota_sd_wr32(sector + 12, first_sector);
|
||||
mota_sd_wr32(sector + 16, sector_count);
|
||||
mota_sd_wr32(sector + 20, total_size);
|
||||
mota_sd_wr32(sector + 24, ~total_size);
|
||||
mota_sd_wr32(sector + 28, card_sectors);
|
||||
mota_sd_wr32(sector + 32, mota_sd_crc32(sector, 32));
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -12,8 +12,8 @@
|
||||
#include "OtaBootloaderUpdate.h"
|
||||
#include "OtaByteIO.h"
|
||||
#include "OtaFlashLayout_nrf52.h"
|
||||
#include "OtaSdAuthRecord.h"
|
||||
#include "OtaSdBootToken.h"
|
||||
#include "OtaSdHandoff.h"
|
||||
#include "OtaSelf.h"
|
||||
#include "flash/flash_nrf5x.h"
|
||||
|
||||
@@ -27,10 +27,45 @@
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
namespace {
|
||||
|
||||
static void clear_sd_auth_record() {
|
||||
volatile uint8_t* const retained =
|
||||
reinterpret_cast<volatile uint8_t*>(MOTA_SD_AUTH_ADDR);
|
||||
for (uint32_t i = 0; i < MOTA_SD_AUTH_LEN; ++i) retained[i] = 0;
|
||||
__DMB();
|
||||
__DSB();
|
||||
}
|
||||
|
||||
static bool publish_sd_auth_record(const uint8_t record[MOTA_SD_AUTH_LEN]) {
|
||||
volatile uint8_t* const retained =
|
||||
reinterpret_cast<volatile uint8_t*>(MOTA_SD_AUTH_ADDR);
|
||||
// Invalidate magic first, then publish the body and magic last. A reset at any
|
||||
// intermediate instruction therefore leaves a record OTAFIX rejects.
|
||||
for (uint32_t i = 0; i < sizeof(MOTA_SD_AUTH_MAGIC); ++i) retained[i] = 0;
|
||||
__DMB();
|
||||
for (uint32_t i = sizeof(MOTA_SD_AUTH_MAGIC); i < MOTA_SD_AUTH_LEN; ++i)
|
||||
retained[i] = record[i];
|
||||
__DMB();
|
||||
for (uint32_t i = 0; i < sizeof(MOTA_SD_AUTH_MAGIC); ++i) retained[i] = record[i];
|
||||
__DMB();
|
||||
__DSB();
|
||||
for (uint32_t i = 0; i < MOTA_SD_AUTH_LEN; ++i)
|
||||
if (retained[i] != record[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* const OtaStoreSdNrf52::PATH = "/meshcore-ota.mota";
|
||||
|
||||
OtaStoreSdNrf52::OtaStoreSdNrf52()
|
||||
: _sd(new (std::nothrow) SdFs()), _file(new (std::nothrow) FsFile()) {}
|
||||
: _sd(new (std::nothrow) SdFs()), _file(new (std::nothrow) FsFile()) {
|
||||
// If an earlier reset did not enter OTAFIX, stale authorization must not arm a
|
||||
// later unrelated reboot. OTAFIX runs before application constructors and
|
||||
// consumes a valid record on the intended apply boot.
|
||||
clear_sd_auth_record();
|
||||
}
|
||||
|
||||
OtaStoreSdNrf52::~OtaStoreSdNrf52() {
|
||||
if (_file) { _file->close(); delete _file; }
|
||||
@@ -46,8 +81,6 @@ void OtaStoreSdNrf52::resetStoreState() {
|
||||
_total = 0;
|
||||
_first_sector = 0;
|
||||
_allocated_sectors = 0;
|
||||
_partition_start = 0;
|
||||
_partition_end = 0;
|
||||
_planned_bootloader = false;
|
||||
if (_file && *_file) _file->close();
|
||||
if (_sd) _sd->end();
|
||||
@@ -75,15 +108,11 @@ bool OtaStoreSdNrf52::mount() {
|
||||
return false;
|
||||
}
|
||||
_mounted = true;
|
||||
if (!inspect_mbr()) {
|
||||
_sd->end();
|
||||
_mounted = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::formatCard(MainBoard& board) {
|
||||
clear_sd_auth_record();
|
||||
if (!beginCardOnly()) return false;
|
||||
|
||||
board.serviceWatchdog();
|
||||
@@ -100,22 +129,12 @@ bool OtaStoreSdNrf52::formatCard(MainBoard& board) {
|
||||
}
|
||||
|
||||
_mounted = true;
|
||||
if (!inspect_mbr()) {
|
||||
_sd->end();
|
||||
_mounted = false;
|
||||
return false;
|
||||
}
|
||||
if (!invalidate_handoff()) {
|
||||
fail("SD handoff clear failed");
|
||||
_sd->end();
|
||||
_mounted = false;
|
||||
return false;
|
||||
}
|
||||
board.serviceWatchdog();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::eraseCard(MainBoard& board) {
|
||||
clear_sd_auth_record();
|
||||
if (!beginCardOnly()) return false;
|
||||
|
||||
SdCard* card = _sd->card();
|
||||
@@ -312,58 +331,18 @@ bool OtaStoreSdNrf52::listFiles(MainBoard& board, uint16_t page,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::inspect_mbr() {
|
||||
uint8_t sector[MOTA_SD_SECTOR_SIZE];
|
||||
if (!_sd->card() || !_sd->card()->readSector(0, sector)) {
|
||||
fail("SD MBR read failed");
|
||||
return false;
|
||||
}
|
||||
if (sector[510] != 0x55 || sector[511] != 0xAA) {
|
||||
fail("SD must use an MBR partition table");
|
||||
return false;
|
||||
}
|
||||
|
||||
// SdFs mounts the first usable MBR partition. Require its start to leave
|
||||
// sector 1 unused; a protective GPT entry (type 0xEE) is not safe here.
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
const uint8_t* p = sector + 446 + (uint32_t)i * 16;
|
||||
uint8_t type = p[4];
|
||||
uint32_t start = mota_sd_rd32(p + 8);
|
||||
uint32_t count = mota_sd_rd32(p + 12);
|
||||
if (type == 0 || count == 0) continue;
|
||||
if (type == 0xEE || start <= MOTA_SD_HANDOFF_SECTOR ||
|
||||
start >= _sd->card()->sectorCount() ||
|
||||
count > _sd->card()->sectorCount() - start) {
|
||||
fail("SD needs an MBR partition starting after sector 1");
|
||||
return false;
|
||||
}
|
||||
_partition_start = start;
|
||||
_partition_end = start + count;
|
||||
return true;
|
||||
}
|
||||
fail("SD has no usable MBR partition");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::invalidate_handoff() {
|
||||
if (!_mounted || !_sd->card()) return false;
|
||||
uint8_t sector[MOTA_SD_SECTOR_SIZE];
|
||||
if (!_sd->card()->readSector(MOTA_SD_HANDOFF_SECTOR, sector)) return false;
|
||||
memset(sector, 0xFF, MOTA_SD_HANDOFF_LEN);
|
||||
return _sd->card()->writeSector(MOTA_SD_HANDOFF_SECTOR, sector) &&
|
||||
_sd->card()->syncDevice();
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::locate_file() {
|
||||
Sector_t first = 0, last = 0;
|
||||
if (!_file || !_file->contiguousRange(&first, &last) || last < first) {
|
||||
fail("OTA file is not contiguous");
|
||||
return false;
|
||||
}
|
||||
uint64_t need = ((uint64_t)_total + MOTA_SD_SECTOR_SIZE - 1) / MOTA_SD_SECTOR_SIZE;
|
||||
uint64_t need = ((uint64_t)_total + MOTA_SD_AUTH_SECTOR_SIZE - 1) /
|
||||
MOTA_SD_AUTH_SECTOR_SIZE;
|
||||
uint64_t available = (uint64_t)last - first + 1;
|
||||
if (need == 0 || need > available || first < _partition_start ||
|
||||
(uint64_t)first + need > _partition_end) {
|
||||
const uint32_t card_sectors = _sd->card() ? _sd->card()->sectorCount() : 0;
|
||||
if (need == 0 || need > available || first == 0 || card_sectors == 0 ||
|
||||
first >= card_sectors || need > card_sectors - first) {
|
||||
fail("OTA file sector range is invalid");
|
||||
return false;
|
||||
}
|
||||
@@ -401,11 +380,8 @@ bool OtaStoreSdNrf52::begin(uint32_t total_size) {
|
||||
_total = 0;
|
||||
_first_sector = 0;
|
||||
_allocated_sectors = 0;
|
||||
clear_sd_auth_record();
|
||||
if (total_size < 13 || !mount()) return false;
|
||||
if (!invalidate_handoff()) {
|
||||
fail("SD handoff clear failed");
|
||||
return false;
|
||||
}
|
||||
if (*_file) _file->close();
|
||||
_sd->remove(PATH);
|
||||
if (!_file->open(PATH, O_RDWR | O_CREAT | O_EXCL)) {
|
||||
@@ -460,12 +436,13 @@ bool OtaStoreSdNrf52::read(uint32_t offset, uint8_t* buf, uint32_t len) const {
|
||||
|
||||
uint32_t OtaStoreSdNrf52::capacity() const {
|
||||
if (!_mounted || !_sd || !_sd->card()) return 0;
|
||||
uint64_t bytes = (uint64_t)_sd->card()->sectorCount() * MOTA_SD_SECTOR_SIZE;
|
||||
uint64_t bytes = (uint64_t)_sd->card()->sectorCount() * MOTA_SD_AUTH_SECTOR_SIZE;
|
||||
return bytes > UINT32_MAX ? UINT32_MAX : (uint32_t)bytes;
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::finalize() {
|
||||
if (!_file || !*_file || !_file->sync() || !_sd->card()->syncDevice()) {
|
||||
SdCard* const card = _sd ? _sd->card() : nullptr;
|
||||
if (!_file || !*_file || !card || !_file->sync() || !card->syncDevice()) {
|
||||
fail("SD OTA sync failed");
|
||||
return false;
|
||||
}
|
||||
@@ -537,15 +514,22 @@ void OtaStoreSdNrf52::clear() {
|
||||
_planned_bootloader = false;
|
||||
_first_sector = 0;
|
||||
_allocated_sectors = 0;
|
||||
clear_sd_auth_record();
|
||||
if (!mount()) return;
|
||||
invalidate_handoff();
|
||||
if (_file && *_file) _file->close();
|
||||
_sd->remove(PATH);
|
||||
}
|
||||
|
||||
bool OtaStoreSdNrf52::approve_for_bootloader(
|
||||
const uint8_t expected_boot_image_hash[32]) {
|
||||
const uint8_t expected_boot_image_hash[32],
|
||||
const uint8_t authorized_container_hash[32]) {
|
||||
if (!_total || !finalize()) return false;
|
||||
if (!authorized_container_hash) {
|
||||
fail("missing authenticated SD container hash");
|
||||
return false;
|
||||
}
|
||||
SdCard* const card = _sd ? _sd->card() : nullptr;
|
||||
const uint32_t card_sectors = card ? card->sectorCount() : 0;
|
||||
#if defined(OTA_SD_BOOTLOADER_UPDATE)
|
||||
if (_planned_bootloader) {
|
||||
SelfFwInfo fi;
|
||||
@@ -563,7 +547,7 @@ bool OtaStoreSdNrf52::approve_for_bootloader(
|
||||
}
|
||||
#endif
|
||||
if (!write(8 + MOTA_OFF_APPROVAL, APPROVAL_YES, sizeof(APPROVAL_YES)) ||
|
||||
!_file->sync() || !_sd->card()->syncDevice()) {
|
||||
!_file->sync() || !card || !card->syncDevice()) {
|
||||
fail("SD approval write failed");
|
||||
return false;
|
||||
}
|
||||
@@ -591,16 +575,16 @@ bool OtaStoreSdNrf52::approve_for_bootloader(
|
||||
}
|
||||
#endif
|
||||
|
||||
uint8_t sector[MOTA_SD_SECTOR_SIZE];
|
||||
if (!_sd->card()->readSector(MOTA_SD_HANDOFF_SECTOR, sector)) {
|
||||
fail("SD handoff sector read failed");
|
||||
return false;
|
||||
}
|
||||
mota_sd_encode_handoff(sector, _first_sector, _allocated_sectors,
|
||||
_total, _sd->card()->sectorCount());
|
||||
if (!_sd->card()->writeSector(MOTA_SD_HANDOFF_SECTOR, sector) ||
|
||||
!_sd->card()->syncDevice()) {
|
||||
fail("SD bootloader handoff write failed");
|
||||
uint8_t auth[MOTA_SD_AUTH_LEN];
|
||||
const uint8_t purpose = _planned_bootloader
|
||||
? MOTA_SD_AUTH_PURPOSE_BOOTLOADER : MOTA_SD_AUTH_PURPOSE_APP;
|
||||
const uint8_t format = _planned_bootloader ? MOTA_BOOT_FORMAT_VER : MOTA_APP_FORMAT_VER;
|
||||
if (!mota_sd_auth_encode(auth, purpose, format, _first_sector,
|
||||
_allocated_sectors, _total, card_sectors,
|
||||
authorized_container_hash) ||
|
||||
!publish_sd_auth_record(auth)) {
|
||||
clear_sd_auth_record();
|
||||
fail("SD retained authorization publish failed");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -40,12 +40,13 @@ public:
|
||||
|
||||
// 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.
|
||||
// Every package writes APRV, then publishes a reset-retained authorization
|
||||
// record containing geometry plus the normalized SHA-256 produced by the
|
||||
// same verification pass. Bootloader packages additionally bind the exact
|
||||
// authenticated signed image_hash to the E0000 scratch token.
|
||||
bool approve_for_bootloader(
|
||||
const uint8_t expected_boot_image_hash[32] = nullptr);
|
||||
const uint8_t expected_boot_image_hash[32],
|
||||
const uint8_t authorized_container_hash[32]);
|
||||
bool formatCard(MainBoard& board);
|
||||
bool eraseCard(MainBoard& board);
|
||||
bool getSpace(MainBoard& board, uint64_t& used_bytes, uint64_t& free_bytes);
|
||||
@@ -58,9 +59,7 @@ private:
|
||||
|
||||
bool mount();
|
||||
bool beginCardOnly();
|
||||
bool inspect_mbr();
|
||||
bool locate_file();
|
||||
bool invalidate_handoff();
|
||||
void resetStoreState();
|
||||
void fail(const char* message);
|
||||
|
||||
@@ -71,8 +70,6 @@ private:
|
||||
uint32_t _total = 0;
|
||||
uint32_t _first_sector = 0;
|
||||
uint32_t _allocated_sectors = 0;
|
||||
uint32_t _partition_start = 0;
|
||||
uint32_t _partition_end = 0;
|
||||
char _error[80] = {0};
|
||||
};
|
||||
|
||||
|
||||
@@ -60,6 +60,14 @@ static VerifyResult ota_verify_store(const OtaStore& store,
|
||||
}
|
||||
|
||||
r.parsed = true;
|
||||
// Bind the removable-media authorization to the exact stream verified below. Approval is the one mutable
|
||||
// field, so normalize its four bytes to zero: the digest remains stable after APRV is written and synced.
|
||||
SHA256 container_sha;
|
||||
static const uint8_t zero_approval[4] = {0, 0, 0, 0};
|
||||
container_sha.update(hdr, sizeof(hdr));
|
||||
container_sha.update(manifest, MOTA_OFF_APPROVAL);
|
||||
container_sha.update(zero_approval, sizeof(zero_approval));
|
||||
container_sha.update(leaves, leaves_len);
|
||||
uint8_t root[4];
|
||||
merkle_root(root, leaves, m.block_count);
|
||||
r.root_ok = memcmp(root, m.merkle_root, sizeof(root)) == 0;
|
||||
@@ -81,6 +89,7 @@ static VerifyResult ota_verify_store(const OtaStore& store,
|
||||
break;
|
||||
}
|
||||
if (m.is_full()) image_sha.update(block, n);
|
||||
container_sha.update(block, n);
|
||||
off += n;
|
||||
}
|
||||
if (off != m.payload_size) r.payload_ok = false;
|
||||
@@ -92,6 +101,12 @@ static VerifyResult ota_verify_store(const OtaStore& store,
|
||||
r.image_ok = !m.is_full();
|
||||
}
|
||||
|
||||
if (r.root_ok && r.payload_ok && r.image_ok) {
|
||||
container_sha.update(trailer, sizeof(trailer));
|
||||
container_sha.finalize(r.container_hash, sizeof(r.container_hash));
|
||||
r.container_hash_ok = true;
|
||||
}
|
||||
|
||||
r.is_signed = m.is_signed();
|
||||
if (r.is_signed) {
|
||||
mesh::Identity signer(m.signer_pubkey);
|
||||
|
||||
@@ -18,6 +18,8 @@ struct VerifyResult {
|
||||
bool is_signed = false;
|
||||
bool sig_ok = false; // Ed25519 signature valid for signer_pubkey
|
||||
bool trusted = false; // signer_pubkey is in the allowlist
|
||||
bool container_hash_ok = false; // normalized full-container SHA was produced from the verified stream
|
||||
uint8_t container_hash[32] = {0}; // SHA256(container with approval bytes normalized to zero)
|
||||
|
||||
// Integrity holds (safe to keep/serve). For a signed image, the signature must also verify.
|
||||
bool integrity_ok() const { return parsed && root_ok && payload_ok && image_ok && (!is_signed || sig_ok); }
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "helpers/ota/OtaBlInfo.h"
|
||||
#include "helpers/ota/OtaFlashLayout_nrf52.h"
|
||||
#include "helpers/ota/OtaSdBootToken.h"
|
||||
#include "helpers/ota/OtaSdAuthRecord.h"
|
||||
#include "mota_vectors.h" // auto-generated by tools/mota/gen_vectors.py
|
||||
|
||||
extern "C" {
|
||||
@@ -50,6 +51,14 @@ TEST(OtaBootResult, AcceptsOnlyOtafixApplyDiagnostics) {
|
||||
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xBD), 0x00);
|
||||
}
|
||||
|
||||
TEST(OtaPolicy, TrustedAutoInstallIsStrictlyForwardOnly) {
|
||||
EXPECT_FALSE(ota_trusted_auto_version_allows(0, 0x01170104u));
|
||||
EXPECT_FALSE(ota_trusted_auto_version_allows(0x01170103u, 0));
|
||||
EXPECT_FALSE(ota_trusted_auto_version_allows(0x01170103u, 0x01170102u));
|
||||
EXPECT_FALSE(ota_trusted_auto_version_allows(0x01170103u, 0x01170103u));
|
||||
EXPECT_TRUE(ota_trusted_auto_version_allows(0x01170103u, 0x01170104u));
|
||||
}
|
||||
|
||||
TEST(OtaBootPackage, StorageProfilesAndSdGeometryAreExact) {
|
||||
EXPECT_EQ(OTA_BL_PROFILE_SD_BOOT_UPDATE, 0x09u);
|
||||
EXPECT_EQ(OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE, 0x0Au);
|
||||
@@ -89,6 +98,251 @@ TEST(OtaBootPackage, StorageProfilesAndSdGeometryAreExact) {
|
||||
0x00FFu, 0u, 0u, live_size, safe_span));
|
||||
}
|
||||
|
||||
TEST(OtaSdAuth, RetainedRecordBindsPurposeGeometryAndContainerDigest) {
|
||||
uint8_t digest[32];
|
||||
for (uint8_t i = 0; i < sizeof(digest); ++i) digest[i] = (uint8_t)(0x40u + i);
|
||||
uint8_t record[MOTA_SD_AUTH_LEN];
|
||||
const uint32_t total = 41330u;
|
||||
const uint32_t sectors = (total + 511u) / 512u;
|
||||
ASSERT_TRUE(mota_sd_auth_encode(record, MOTA_SD_AUTH_PURPOSE_BOOTLOADER,
|
||||
MOTA_BOOT_FORMAT_VER, 2048u, sectors,
|
||||
total, 8000000u, digest));
|
||||
EXPECT_EQ(0, memcmp(record, "MOTASDA2", 8));
|
||||
EXPECT_EQ(mota_sd_auth_rd16(record + 8), 2u);
|
||||
EXPECT_EQ(mota_sd_auth_rd16(record + 10), 72u);
|
||||
EXPECT_EQ(record[12], MOTA_SD_AUTH_PURPOSE_BOOTLOADER);
|
||||
EXPECT_EQ(record[13], MOTA_BOOT_FORMAT_VER);
|
||||
EXPECT_EQ(mota_sd_auth_rd32(record + 16), 2048u);
|
||||
EXPECT_EQ(mota_sd_auth_rd32(record + 20), sectors);
|
||||
EXPECT_EQ(mota_sd_auth_rd32(record + 24), total);
|
||||
EXPECT_EQ(mota_sd_auth_rd32(record + 28), 8000000u);
|
||||
EXPECT_EQ(0, memcmp(record + 32, digest, sizeof(digest)));
|
||||
EXPECT_TRUE(mota_sd_auth_valid(record));
|
||||
|
||||
record[32] ^= 1u;
|
||||
EXPECT_FALSE(mota_sd_auth_valid(record));
|
||||
record[32] ^= 1u;
|
||||
EXPECT_FALSE(mota_sd_auth_encode(record, MOTA_SD_AUTH_PURPOSE_APP,
|
||||
MOTA_BOOT_FORMAT_VER, 2048u, sectors,
|
||||
total, 8000000u, digest));
|
||||
EXPECT_FALSE(mota_sd_auth_encode(record, MOTA_SD_AUTH_PURPOSE_APP,
|
||||
MOTA_APP_FORMAT_VER, 2048u, sectors + 1u,
|
||||
total, 8000000u, digest));
|
||||
}
|
||||
|
||||
TEST(OtaBootPackage, ContinuityExtensionIsBackwardCompatibleAndStrictlyMonotonic) {
|
||||
uint8_t envelope[OTA_BOOT_ENVELOPE_SIZE] = {0};
|
||||
wr_u32le(envelope, OTA_BOOT_MANIFEST_MAGIC0);
|
||||
wr_u32le(envelope + 4, OTA_BOOT_MANIFEST_MAGIC1);
|
||||
envelope[8] = (uint8_t)OTA_BOOT_MANIFEST_VERSION;
|
||||
envelope[10] = (uint8_t)OTA_BOOT_MANIFEST_SIZE;
|
||||
wr_u32le(envelope + 12, OTA_BOOT_IMAGE_START);
|
||||
wr_u32le(envelope + 16, OTA_BOOT_IMAGE_SIZE);
|
||||
wr_u32le(envelope + 20, 0x239A0071u);
|
||||
memcpy(envelope + 24, "TOWER_V2_OTA", 12);
|
||||
wr_u32le(envelope + 44, OTA_BOOT_CONTINUITY_MAGIC0);
|
||||
wr_u32le(envelope + 48, OTA_BOOT_CONTINUITY_MAGIC1);
|
||||
envelope[52] = (uint8_t)OTA_BOOT_CONTINUITY_VERSION;
|
||||
envelope[54] = (uint8_t)OTA_BOOT_CONTINUITY_SIZE;
|
||||
const uint32_t candidate_version = 0x0117010Du;
|
||||
wr_u32le(envelope + 56, candidate_version);
|
||||
envelope[60] = (uint8_t)OTA_BOOT_CONTINUITY_FAMILY_S140;
|
||||
envelope[62] = 0xB6;
|
||||
wr_u32le(envelope + 64, MOTA_NRF52_APP_BASE_S140_V6);
|
||||
envelope[68] = (uint8_t)OTA_BOOT_CONTINUITY_LAYOUT_ABI;
|
||||
|
||||
OtaBootloaderIdentity candidate;
|
||||
ASSERT_TRUE(ota_bootloader_manifest_parse(envelope, 0x100u, candidate,
|
||||
sizeof(envelope)));
|
||||
ASSERT_TRUE(candidate.continuity_present);
|
||||
EXPECT_EQ(candidate.boot_version, candidate_version);
|
||||
EXPECT_EQ(candidate.softdevice_family, 140u);
|
||||
EXPECT_EQ(candidate.softdevice_fwid, 0x00B6u);
|
||||
EXPECT_EQ(candidate.app_base, MOTA_NRF52_APP_BASE_S140_V6);
|
||||
|
||||
OtaBootloaderIdentity legacy = candidate;
|
||||
legacy.continuity_present = false;
|
||||
legacy.crc_ok = true;
|
||||
EXPECT_FALSE(ota_bootloader_sd_retained_auth_ready(
|
||||
legacy, 140u, 0x00B6u, MOTA_NRF52_APP_BASE_S140_V6, 1u));
|
||||
candidate.crc_ok = true;
|
||||
EXPECT_TRUE(ota_bootloader_sd_retained_auth_ready(
|
||||
candidate, 140u, 0x00B6u, MOTA_NRF52_APP_BASE_S140_V6, 1u));
|
||||
EXPECT_EQ(ota_bootloader_continuity_gate(
|
||||
legacy, candidate, candidate_version, 140u, 0x00B6u,
|
||||
MOTA_NRF52_APP_BASE_S140_V6, 1u),
|
||||
OTA_BOOT_CONTINUITY_OK); // one-time v1 bootstrap
|
||||
EXPECT_EQ(ota_bootloader_continuity_gate(
|
||||
legacy, candidate, candidate_version + 1u, 140u, 0x00B6u,
|
||||
MOTA_NRF52_APP_BASE_S140_V6, 1u),
|
||||
OTA_BOOT_CONTINUITY_OUTER_VERSION);
|
||||
|
||||
OtaBootloaderIdentity installed = candidate;
|
||||
installed.boot_version = candidate_version - 1u;
|
||||
EXPECT_EQ(ota_bootloader_continuity_gate(
|
||||
installed, candidate, candidate_version, 140u, 0x00B6u,
|
||||
MOTA_NRF52_APP_BASE_S140_V6, 1u),
|
||||
OTA_BOOT_CONTINUITY_OK);
|
||||
installed.boot_version = candidate_version;
|
||||
EXPECT_EQ(ota_bootloader_continuity_gate(
|
||||
installed, candidate, candidate_version, 140u, 0x00B6u,
|
||||
MOTA_NRF52_APP_BASE_S140_V6, 1u),
|
||||
OTA_BOOT_CONTINUITY_NOT_NEWER);
|
||||
EXPECT_EQ(ota_bootloader_continuity_gate(
|
||||
legacy, candidate, candidate_version, 140u, 0x00B7u,
|
||||
MOTA_NRF52_APP_BASE_S140_V6, 1u),
|
||||
OTA_BOOT_CONTINUITY_PLATFORM);
|
||||
|
||||
EXPECT_FALSE(ota_bootloader_version_valid(0x02040100u));
|
||||
EXPECT_FALSE(ota_bootloader_version_valid(UINT32_MAX));
|
||||
EXPECT_TRUE(ota_bootloader_version_valid(0x020401FFu));
|
||||
candidate.boot_version = UINT32_MAX;
|
||||
EXPECT_EQ(ota_bootloader_continuity_gate(
|
||||
legacy, candidate, UINT32_MAX, 140u, 0x00B6u,
|
||||
MOTA_NRF52_APP_BASE_S140_V6, 1u),
|
||||
OTA_BOOT_CONTINUITY_OUTER_VERSION);
|
||||
candidate.boot_version = candidate_version;
|
||||
|
||||
envelope[48] ^= 1u;
|
||||
EXPECT_FALSE(ota_bootloader_manifest_parse(envelope, 0x100u, candidate,
|
||||
sizeof(envelope)));
|
||||
}
|
||||
|
||||
TEST(OtaBootPackage, CandidateContinuityEnvelopeHasOneCanonicalFinalOffset) {
|
||||
auto make_image = [](uint32_t manifest_offset) {
|
||||
std::vector<uint8_t> image(OTA_BOOT_IMAGE_SIZE, 0xFF);
|
||||
wr_u32le(image.data(), OTA_NRF52840_RAM_END);
|
||||
wr_u32le(image.data() + 4, OTA_BOOT_IMAGE_START + 0x101u);
|
||||
uint8_t* caps = image.data() + 0x400u;
|
||||
memset(caps, 0, 16u);
|
||||
memcpy(caps, "MOTABLDR", 8u);
|
||||
caps[8] = MOTA_BOOT_FORMAT_VER;
|
||||
caps[10] = (uint8_t)OTA_BL_REQUIRED_APP_CODEC_MASK;
|
||||
caps[12] = OTA_BL_PROFILE_SD_BOOT_UPDATE;
|
||||
uint8_t* envelope = image.data() + manifest_offset;
|
||||
memset(envelope, 0, OTA_BOOT_ENVELOPE_SIZE);
|
||||
wr_u32le(envelope, OTA_BOOT_MANIFEST_MAGIC0);
|
||||
wr_u32le(envelope + 4, OTA_BOOT_MANIFEST_MAGIC1);
|
||||
envelope[8] = (uint8_t)OTA_BOOT_MANIFEST_VERSION;
|
||||
envelope[10] = (uint8_t)OTA_BOOT_MANIFEST_SIZE;
|
||||
wr_u32le(envelope + 12, OTA_BOOT_IMAGE_START);
|
||||
wr_u32le(envelope + 16, OTA_BOOT_IMAGE_SIZE);
|
||||
wr_u32le(envelope + 20, OTA_XIAO_BOARD_ID_BASE);
|
||||
memcpy(envelope + 24, OTA_XIAO_BOOT_DEVICE_NAME,
|
||||
OTA_BOOT_DEVICE_NAME_SIZE);
|
||||
wr_u32le(envelope + 44, OTA_BOOT_CONTINUITY_MAGIC0);
|
||||
wr_u32le(envelope + 48, OTA_BOOT_CONTINUITY_MAGIC1);
|
||||
envelope[52] = (uint8_t)OTA_BOOT_CONTINUITY_VERSION;
|
||||
envelope[54] = (uint8_t)OTA_BOOT_CONTINUITY_SIZE;
|
||||
wr_u32le(envelope + 56, 0x0117010Du);
|
||||
envelope[60] = (uint8_t)OTA_BOOT_CONTINUITY_FAMILY_S140;
|
||||
envelope[62] = 0x23u;
|
||||
envelope[63] = 0x01u;
|
||||
wr_u32le(envelope + 64, MOTA_NRF52_APP_BASE_S140_V7);
|
||||
envelope[68] = (uint8_t)OTA_BOOT_CONTINUITY_LAYOUT_ABI;
|
||||
wr_u32le(envelope + 40,
|
||||
ota_boot_image_crc32(image.data(), image.size(), manifest_offset + 40));
|
||||
return image;
|
||||
};
|
||||
|
||||
OtaBootloaderIdentity identity;
|
||||
auto canonical = make_image(OTA_BOOT_CANDIDATE_MANIFEST_OFFSET);
|
||||
ASSERT_TRUE(ota_bootloader_candidate_identity_from_image(
|
||||
canonical.data(), canonical.size(), identity));
|
||||
EXPECT_TRUE(ota_bootloader_candidate_identity_canonical(identity));
|
||||
EXPECT_EQ(identity.manifest_offset, 0x9FB4u);
|
||||
|
||||
auto relocated = make_image(0x8000u);
|
||||
ASSERT_TRUE(ota_bootloader_identity_from_image(
|
||||
relocated.data(), relocated.size(), identity)); // installed/legacy scanner remains generic
|
||||
EXPECT_FALSE(ota_bootloader_candidate_identity_canonical(identity));
|
||||
EXPECT_FALSE(ota_bootloader_candidate_identity_from_image(
|
||||
relocated.data(), relocated.size(), identity));
|
||||
|
||||
OtaStoreRam<OTA_BOOT_IMAGE_SIZE> store;
|
||||
OtaBootloaderCapsMarker caps;
|
||||
ASSERT_TRUE(store.begin((uint32_t)canonical.size()));
|
||||
ASSERT_TRUE(store.write(0, canonical.data(), (uint32_t)canonical.size()));
|
||||
EXPECT_TRUE(ota_bootloader_external_image_metadata(
|
||||
store, 0, OTA_BL_PROFILE_SD_BOOT_UPDATE, identity, caps));
|
||||
EXPECT_EQ(identity.manifest_offset, OTA_BOOT_CANDIDATE_MANIFEST_OFFSET);
|
||||
|
||||
// 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
|
||||
// discard the decoy and disagree with a deployed legacy updater.
|
||||
auto ambiguous = canonical;
|
||||
const uint32_t corrupt_decoy = 0x200u;
|
||||
memcpy(ambiguous.data() + corrupt_decoy,
|
||||
ambiguous.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET,
|
||||
OTA_BOOT_MANIFEST_SIZE);
|
||||
wr_u32le(ambiguous.data() + corrupt_decoy + 44, OTA_BOOT_CONTINUITY_MAGIC0);
|
||||
wr_u32le(ambiguous.data() + corrupt_decoy + 48, 0x21444142u); // "BAD!"
|
||||
wr_u32le(ambiguous.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40,
|
||||
0x614E59E1u);
|
||||
wr_u32le(ambiguous.data() + corrupt_decoy + 40, 0x4A03F7A2u);
|
||||
ASSERT_EQ(ota_boot_image_crc32(
|
||||
ambiguous.data(), ambiguous.size(),
|
||||
OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40), 0x614E59E1u);
|
||||
ASSERT_EQ(ota_boot_image_crc32(
|
||||
ambiguous.data(), ambiguous.size(), corrupt_decoy + 40),
|
||||
0x4A03F7A2u);
|
||||
EXPECT_FALSE(ota_bootloader_candidate_identity_from_image(
|
||||
ambiguous.data(), ambiguous.size(), identity));
|
||||
ASSERT_TRUE(store.begin((uint32_t)ambiguous.size()));
|
||||
ASSERT_TRUE(store.write(0, ambiguous.data(), (uint32_t)ambiguous.size()));
|
||||
EXPECT_FALSE(ota_bootloader_external_image_metadata(
|
||||
store, 0, OTA_BL_PROFILE_SD_BOOT_UPDATE, identity, caps));
|
||||
|
||||
// The same malformed extension is harmless when its base CRC is invalid;
|
||||
// only the canonical CRC-valid identity is selected and interpreted.
|
||||
auto bad_crc_decoy = canonical;
|
||||
memcpy(bad_crc_decoy.data() + corrupt_decoy,
|
||||
bad_crc_decoy.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET,
|
||||
OTA_BOOT_MANIFEST_SIZE);
|
||||
wr_u32le(bad_crc_decoy.data() + corrupt_decoy + 44,
|
||||
OTA_BOOT_CONTINUITY_MAGIC0);
|
||||
wr_u32le(bad_crc_decoy.data() + corrupt_decoy + 48, 0x21444142u);
|
||||
wr_u32le(bad_crc_decoy.data() + corrupt_decoy + 40, 0xA5A5A5A5u);
|
||||
wr_u32le(bad_crc_decoy.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40, 0u);
|
||||
wr_u32le(bad_crc_decoy.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40,
|
||||
ota_boot_image_crc32(
|
||||
bad_crc_decoy.data(), bad_crc_decoy.size(),
|
||||
OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40));
|
||||
ASSERT_NE(ota_boot_image_crc32(
|
||||
bad_crc_decoy.data(), bad_crc_decoy.size(), corrupt_decoy + 40),
|
||||
0xA5A5A5A5u);
|
||||
EXPECT_TRUE(ota_bootloader_candidate_identity_from_image(
|
||||
bad_crc_decoy.data(), bad_crc_decoy.size(), identity));
|
||||
ASSERT_TRUE(store.begin((uint32_t)bad_crc_decoy.size()));
|
||||
ASSERT_TRUE(store.write(0, bad_crc_decoy.data(),
|
||||
(uint32_t)bad_crc_decoy.size()));
|
||||
EXPECT_TRUE(ota_bootloader_external_image_metadata(
|
||||
store, 0, OTA_BL_PROFILE_SD_BOOT_UPDATE, identity, caps));
|
||||
|
||||
// A sole CRC-valid identity that claims a corrupt extension is not a legacy
|
||||
// image; installed diagnostics and candidate validation both fail closed.
|
||||
auto sole_half = canonical;
|
||||
wr_u32le(sole_half.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 48,
|
||||
0x21444142u);
|
||||
wr_u32le(sole_half.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40, 0u);
|
||||
wr_u32le(sole_half.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40,
|
||||
ota_boot_image_crc32(
|
||||
sole_half.data(), sole_half.size(),
|
||||
OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40));
|
||||
EXPECT_FALSE(ota_bootloader_identity_from_image(
|
||||
sole_half.data(), sole_half.size(), identity));
|
||||
ASSERT_TRUE(store.begin((uint32_t)sole_half.size()));
|
||||
ASSERT_TRUE(store.write(0, sole_half.data(), (uint32_t)sole_half.size()));
|
||||
EXPECT_FALSE(ota_bootloader_external_image_metadata(
|
||||
store, 0, OTA_BL_PROFILE_SD_BOOT_UPDATE, identity, caps));
|
||||
|
||||
ASSERT_TRUE(store.begin((uint32_t)relocated.size()));
|
||||
ASSERT_TRUE(store.write(0, relocated.data(), (uint32_t)relocated.size()));
|
||||
EXPECT_FALSE(ota_bootloader_external_image_metadata(
|
||||
store, 0, OTA_BL_PROFILE_SD_BOOT_UPDATE, identity, caps));
|
||||
}
|
||||
|
||||
TEST(OtaBootPackage, SdApprovalTokenBindsTheAuthenticatedImageHash) {
|
||||
uint8_t hash[32];
|
||||
for (uint8_t i = 0; i < sizeof(hash); i++) hash[i] = i;
|
||||
@@ -279,6 +533,14 @@ TEST(OtaBootPackage, EmbeddedIdentityVectorCapsAndExplicitConfirmationGate) {
|
||||
EXPECT_TRUE(ota_bootloader_caps_marker_parse(marker, caps));
|
||||
marker[15] = 1;
|
||||
EXPECT_FALSE(ota_bootloader_caps_marker_parse(marker, caps));
|
||||
marker[15] = 0;
|
||||
marker[8] = 0; marker[9] = 1; // ABI 0x0100: valid little-endian u16
|
||||
ASSERT_TRUE(ota_bootloader_caps_marker_parse(marker, caps));
|
||||
EXPECT_EQ(caps.apply_abi, 0x0100u);
|
||||
marker[8] = 0; marker[9] = 0;
|
||||
EXPECT_FALSE(ota_bootloader_caps_marker_parse(marker, caps));
|
||||
marker[8] = 0xFF; marker[9] = 0xFF;
|
||||
EXPECT_FALSE(ota_bootloader_caps_marker_parse(marker, caps));
|
||||
|
||||
std::vector<uint8_t> internal_caps_image(64, 0xFF);
|
||||
const uint8_t internal_marker[16] = {
|
||||
@@ -342,8 +604,8 @@ TEST(OtaBootPackage, CapabilityScannerRejectsAnOtherwiseValidUnalignedMarker) {
|
||||
const uint8_t qspi_profile =
|
||||
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI |
|
||||
OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
const uint8_t marker[16] = {'M','O','T','A','B','L','D','R', 3,0, 5,0,
|
||||
qspi_profile, 0,0,0};
|
||||
uint8_t marker[16] = {'M','O','T','A','B','L','D','R', 3,0, 5,0,
|
||||
qspi_profile, 0,0,0};
|
||||
uint8_t image[64];
|
||||
memset(image, 0xFF, sizeof(image));
|
||||
memcpy(image + 1, marker, sizeof(marker));
|
||||
@@ -356,6 +618,17 @@ TEST(OtaBootPackage, CapabilityScannerRejectsAnOtherwiseValidUnalignedMarker) {
|
||||
EXPECT_EQ(caps.apply_abi, 3u);
|
||||
EXPECT_EQ(caps.storage_flags, qspi_profile);
|
||||
|
||||
marker[8] = 0; marker[9] = 1;
|
||||
memset(image, 0xFF, sizeof(image));
|
||||
memcpy(image + 4, marker, sizeof(marker));
|
||||
const OtaBlCaps high_abi =
|
||||
ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile);
|
||||
ASSERT_TRUE(high_abi.present);
|
||||
EXPECT_EQ(high_abi.apply_abi, 0x0100u);
|
||||
marker[8] = 3; marker[9] = 0;
|
||||
memset(image, 0xFF, sizeof(image));
|
||||
memcpy(image + 4, marker, sizeof(marker));
|
||||
|
||||
// A malformed aligned magic decoy is ignored, but a second fully valid
|
||||
// privileged marker makes the installed capability identity ambiguous.
|
||||
memcpy(image + 24, marker, sizeof(marker));
|
||||
@@ -365,12 +638,19 @@ TEST(OtaBootPackage, CapabilityScannerRejectsAnOtherwiseValidUnalignedMarker) {
|
||||
image[24 + 13] = 1;
|
||||
EXPECT_TRUE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
||||
|
||||
// A structurally valid marker for the other storage profile is not an
|
||||
// installed-profile candidate and cannot shadow the exact marker.
|
||||
// Any second structurally valid privileged marker is ambiguous, including
|
||||
// another known storage profile or the bare BOOT_UPDATE capability.
|
||||
memcpy(image + 24, marker, sizeof(marker));
|
||||
image[24 + 12] = OTA_BL_STORAGE_STAGE_CEILING |
|
||||
OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
EXPECT_TRUE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
||||
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
||||
image[24 + 12] = OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
||||
|
||||
memset(image, 0xFF, sizeof(image));
|
||||
memcpy(image + 4, marker, sizeof(marker));
|
||||
image[4 + 12] = OTA_BL_STORAGE_BOOT_UPDATE;
|
||||
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
||||
}
|
||||
|
||||
class FakeMotaSeederStream : public Stream {
|
||||
@@ -1764,6 +2044,50 @@ TEST(OtaTransfer, RejectsUnsupportedManifestHashAlgorithm) {
|
||||
EXPECT_EQ(client.fetchState(), OtaManager::FAILED);
|
||||
}
|
||||
|
||||
TEST(OtaTransfer, RejectsEnvelopeMidThatDoesNotMatchParsedManifestRootBeforeStaging) {
|
||||
MotaManifest manifest;
|
||||
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
||||
std::array<uint8_t, MOTA_MFL> bytes;
|
||||
memcpy(bytes.data(), manifest.manifest_start, bytes.size());
|
||||
bytes[20] ^= 0x80; // parsed merkle_root; the peer keeps the requested MID in the wire envelope
|
||||
|
||||
OtaManager client;
|
||||
OtaStoreRam<4096> store;
|
||||
CapturedMessages sent;
|
||||
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
||||
client.set_fetch_store(&store);
|
||||
ASSERT_EQ(client.pull(manifest.merkle_root, manifest.target_id), OtaManager::PULL_STARTED);
|
||||
|
||||
deliver_manifest_fragment(client, manifest.merkle_root, 0, bytes.data(), OTA_MF_FRAG);
|
||||
deliver_manifest_fragment(client, manifest.merkle_root, 1, bytes.data() + OTA_MF_FRAG,
|
||||
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
||||
EXPECT_EQ(client.fetchState(), OtaManager::FAILED);
|
||||
EXPECT_EQ(client.fetchError(), OtaManager::FETCH_ERROR_MANIFEST);
|
||||
EXPECT_EQ(store.staged_size(), 0u);
|
||||
}
|
||||
|
||||
TEST(OtaTransfer, RejectsParsedManifestTargetThatDoesNotMatchRequestedCatalogTargetBeforeStaging) {
|
||||
MotaManifest manifest;
|
||||
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
||||
std::array<uint8_t, MOTA_MFL> bytes;
|
||||
memcpy(bytes.data(), manifest.manifest_start, bytes.size());
|
||||
wr_u32le(bytes.data() + 3, manifest.target_id ^ 0x01000000u);
|
||||
|
||||
OtaManager client;
|
||||
OtaStoreRam<4096> store;
|
||||
CapturedMessages sent;
|
||||
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
||||
client.set_fetch_store(&store);
|
||||
ASSERT_EQ(client.pull(manifest.merkle_root, manifest.target_id), OtaManager::PULL_STARTED);
|
||||
|
||||
deliver_manifest_fragment(client, manifest.merkle_root, 0, bytes.data(), OTA_MF_FRAG);
|
||||
deliver_manifest_fragment(client, manifest.merkle_root, 1, bytes.data() + OTA_MF_FRAG,
|
||||
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
||||
EXPECT_EQ(client.fetchState(), OtaManager::FAILED);
|
||||
EXPECT_EQ(client.fetchError(), OtaManager::FETCH_ERROR_MANIFEST);
|
||||
EXPECT_EQ(store.staged_size(), 0u);
|
||||
}
|
||||
|
||||
// Same end-to-end transfer, but with 1 KB logical blocks: each block is delivered as several
|
||||
// self-describing DATA fragments (frag_off), reassembled by the client, then its merkle PROOF is
|
||||
// requested + verified separately before the block is committed. Exercises the multi-fragment path.
|
||||
@@ -2017,6 +2341,7 @@ TEST(OtaTransfer, ResumeAfterReboot) {
|
||||
SendTo to_server2{&server};
|
||||
client2.begin(SIM_TARGET_ID, sim_send, &to_server2);
|
||||
client2.set_fetch_store(&store);
|
||||
client2.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
||||
ASSERT_TRUE(client2.resumeStaged(nullptr)); // adopt whatever is staged
|
||||
EXPECT_EQ(client2.fetchState(), OtaManager::VERIFYING_STAGED);
|
||||
finish_staged_verification(client2);
|
||||
@@ -2030,6 +2355,131 @@ TEST(OtaTransfer, ResumeAfterReboot) {
|
||||
EXPECT_EQ(0, std::memcmp(store.data(), SIM_MOTA_1K, SIM_MOTA_1K_LEN)); // byte-identical to the original
|
||||
}
|
||||
|
||||
TEST(OtaTransfer, BootResumeHonorsCurrentAutofetchTargetAndVersionPolicy) {
|
||||
MotaManifest manifest;
|
||||
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
||||
ASSERT_TRUE(manifest.is_signed());
|
||||
auto stage = [](OtaStoreRam<4096>& store, const uint8_t* bytes,
|
||||
uint32_t length) {
|
||||
return store.begin(length) && store.write(0, bytes, length);
|
||||
};
|
||||
|
||||
OtaStoreRam<4096> off_store;
|
||||
ASSERT_TRUE(stage(off_store, SIM_MOTA, SIM_MOTA_LEN));
|
||||
OtaManager off;
|
||||
off.begin(manifest.target_id, nullptr, nullptr);
|
||||
off.set_fetch_store(&off_store);
|
||||
EXPECT_FALSE(off.resumeStaged(nullptr));
|
||||
|
||||
std::vector<uint8_t> unsigned_mota(SIM_MOTA, SIM_MOTA + SIM_MOTA_LEN);
|
||||
unsigned_mota[8u + 1u] &= (uint8_t)~MFLAG_SIGNED;
|
||||
memset(unsigned_mota.data() + 8u + 97u, 0, 32u + 64u);
|
||||
MotaManifest unsigned_manifest;
|
||||
ASSERT_TRUE(mota_parse(unsigned_mota.data(),
|
||||
(uint32_t)unsigned_mota.size(), unsigned_manifest));
|
||||
ASSERT_FALSE(unsigned_manifest.is_signed());
|
||||
OtaStoreRam<4096> unsigned_store;
|
||||
ASSERT_TRUE(stage(unsigned_store, unsigned_mota.data(),
|
||||
(uint32_t)unsigned_mota.size()));
|
||||
OtaManager signed_only;
|
||||
signed_only.begin(manifest.target_id, nullptr, nullptr);
|
||||
signed_only.set_fetch_store(&unsigned_store);
|
||||
signed_only.set_autofetch(OtaManager::AUTOFETCH_SIGNED);
|
||||
signed_only.set_auto_version_floor(manifest.fw_version - 1u, true);
|
||||
EXPECT_FALSE(signed_only.resumeStaged(nullptr));
|
||||
|
||||
OtaStoreRam<4096> replay_store;
|
||||
ASSERT_TRUE(stage(replay_store, SIM_MOTA, SIM_MOTA_LEN));
|
||||
OtaManager replay;
|
||||
replay.begin(manifest.target_id, nullptr, nullptr);
|
||||
replay.set_fetch_store(&replay_store);
|
||||
replay.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
||||
replay.set_auto_version_floor(manifest.fw_version, true);
|
||||
EXPECT_FALSE(replay.resumeStaged(nullptr));
|
||||
|
||||
OtaStoreRam<4096> wrong_target_store;
|
||||
ASSERT_TRUE(stage(wrong_target_store, SIM_MOTA, SIM_MOTA_LEN));
|
||||
OtaManager wrong_target;
|
||||
wrong_target.begin(manifest.target_id ^ 1u, nullptr, nullptr);
|
||||
wrong_target.set_fetch_store(&wrong_target_store);
|
||||
wrong_target.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
||||
wrong_target.set_auto_version_floor(manifest.fw_version - 1u, true);
|
||||
EXPECT_FALSE(wrong_target.resumeStaged(nullptr));
|
||||
|
||||
OtaStoreRam<4096> newer_store;
|
||||
ASSERT_TRUE(stage(newer_store, SIM_MOTA, SIM_MOTA_LEN));
|
||||
OtaManager newer;
|
||||
newer.begin(manifest.target_id, nullptr, nullptr);
|
||||
newer.set_fetch_store(&newer_store);
|
||||
newer.set_autofetch(OtaManager::AUTOFETCH_SIGNED);
|
||||
newer.set_auto_version_floor(manifest.fw_version - 1u, true);
|
||||
EXPECT_TRUE(newer.resumeStaged(nullptr));
|
||||
EXPECT_EQ(newer.fetchState(), OtaManager::VERIFYING_STAGED);
|
||||
}
|
||||
|
||||
TEST(OtaTransfer, ExplicitResumePreservesTargetZeroWildcard) {
|
||||
MotaManifest manifest;
|
||||
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
||||
auto stage = [](OtaStoreRam<4096>& store) {
|
||||
return store.begin(SIM_MOTA_LEN) &&
|
||||
store.write(0, SIM_MOTA, SIM_MOTA_LEN);
|
||||
};
|
||||
|
||||
OtaStoreRam<4096> manual_store;
|
||||
ASSERT_TRUE(stage(manual_store));
|
||||
OtaManager manual;
|
||||
manual.begin(manifest.target_id ^ 1u, nullptr, nullptr);
|
||||
manual.set_fetch_store(&manual_store);
|
||||
EXPECT_EQ(manual.pull(manifest.merkle_root, 0), OtaManager::PULL_RESUMED);
|
||||
|
||||
OtaStoreRam<4096> archive_store;
|
||||
ASSERT_TRUE(stage(archive_store));
|
||||
OtaManager archive;
|
||||
archive.begin(manifest.target_id ^ 1u, nullptr, nullptr);
|
||||
archive.set_fetch_store(&archive_store);
|
||||
EXPECT_EQ(archive.pull_archive(manifest.merkle_root, 0),
|
||||
OtaManager::PULL_RESUMED);
|
||||
|
||||
OtaStoreRam<4096> mismatch_store;
|
||||
ASSERT_TRUE(stage(mismatch_store));
|
||||
OtaManager mismatch;
|
||||
mismatch.begin(manifest.target_id, nullptr, nullptr);
|
||||
mismatch.set_fetch_store(&mismatch_store);
|
||||
EXPECT_EQ(mismatch.pull(manifest.merkle_root, manifest.target_id ^ 1u),
|
||||
OtaManager::PULL_STARTED);
|
||||
EXPECT_EQ(mismatch.fetchState(), OtaManager::WANT_MANIFEST);
|
||||
}
|
||||
|
||||
TEST(OtaTransfer, ExplicitDebugResumeUsesManualIntentNotAutofetchPolicy) {
|
||||
MotaManifest manifest;
|
||||
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
||||
OtaStoreRam<4096> store;
|
||||
ASSERT_TRUE(store.begin(SIM_MOTA_LEN));
|
||||
ASSERT_TRUE(store.write(0, SIM_MOTA, SIM_MOTA_LEN));
|
||||
|
||||
// A deliberate debug/operator resume remains possible with autofetch disabled, an equal running
|
||||
// version, and a cross-target wildcard. Those same properties are rejected by resumeStaged(nullptr).
|
||||
OtaManager explicit_resume;
|
||||
explicit_resume.begin(manifest.target_id ^ 1u, nullptr, nullptr);
|
||||
explicit_resume.set_fetch_store(&store);
|
||||
explicit_resume.set_autofetch(OtaManager::AUTOFETCH_OFF);
|
||||
explicit_resume.set_auto_version_floor(manifest.fw_version, true);
|
||||
ASSERT_TRUE(explicit_resume.resumeStagedExplicit(manifest.merkle_root, 0));
|
||||
EXPECT_EQ(explicit_resume.fetchState(), OtaManager::VERIFYING_STAGED);
|
||||
|
||||
OtaStoreRam<4096> wrong_mid_store;
|
||||
ASSERT_TRUE(wrong_mid_store.begin(SIM_MOTA_LEN));
|
||||
ASSERT_TRUE(wrong_mid_store.write(0, SIM_MOTA, SIM_MOTA_LEN));
|
||||
OtaManager wrong_mid;
|
||||
wrong_mid.begin(manifest.target_id, nullptr, nullptr);
|
||||
wrong_mid.set_fetch_store(&wrong_mid_store);
|
||||
uint8_t other_mid[4];
|
||||
memcpy(other_mid, manifest.merkle_root, sizeof(other_mid));
|
||||
other_mid[0] ^= 1u;
|
||||
EXPECT_FALSE(wrong_mid.resumeStagedExplicit(other_mid, 0));
|
||||
EXPECT_EQ(wrong_mid.fetchState(), OtaManager::IDLE);
|
||||
}
|
||||
|
||||
TEST(OtaTransfer, ResumeRehashesPayloadBeforeTrustingPresentLeaf) {
|
||||
g_q.clear();
|
||||
OtaManager server, client;
|
||||
@@ -2056,6 +2506,7 @@ TEST(OtaTransfer, ResumeRehashesPayloadBeforeTrustingPresentLeaf) {
|
||||
OtaManager resumed;
|
||||
resumed.begin(SIM_TARGET_ID, nullptr, nullptr);
|
||||
resumed.set_fetch_store(&store);
|
||||
resumed.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
||||
ASSERT_TRUE(resumed.resumeStaged(nullptr));
|
||||
finish_staged_verification(resumed);
|
||||
EXPECT_EQ(resumed.fetchState(), OtaManager::FETCHING);
|
||||
@@ -2087,6 +2538,7 @@ TEST(OtaTransfer, ResumeReadFailureCanNeverBecomeComplete) {
|
||||
OtaManager resumed;
|
||||
resumed.begin(SIM_TARGET_ID, nullptr, nullptr);
|
||||
resumed.set_fetch_store(&store);
|
||||
resumed.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
||||
ASSERT_TRUE(resumed.resumeStaged(nullptr));
|
||||
finish_staged_verification(resumed);
|
||||
EXPECT_EQ(resumed.fetchState(), OtaManager::FAILED);
|
||||
@@ -2256,6 +2708,88 @@ TEST(OtaTransfer, ManualPullReportsIncompatibleManifestInsteadOfGoingIdle) {
|
||||
EXPECT_EQ(store.staged_size(), 0u); // compatibility is rejected before the store is begun
|
||||
}
|
||||
|
||||
TEST(OtaTransfer, AutomaticAdmissionRejectsReplayAndRechecksLyingHaveVersion) {
|
||||
MotaManifest manifest;
|
||||
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
||||
OtaManager client;
|
||||
OtaStoreRam<4096> store;
|
||||
CapturedMessages sent;
|
||||
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
||||
client.set_fetch_store(&store);
|
||||
client.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
||||
client.set_auto_version_floor(manifest.fw_version, true);
|
||||
|
||||
uint8_t wire[64];
|
||||
client.on_message(wire, make_have1(wire, sizeof(wire), manifest.merkle_root,
|
||||
manifest.target_id, manifest.fw_version,
|
||||
manifest.codec_id, manifest.flags));
|
||||
EXPECT_EQ(client.fetchState(), OtaManager::IDLE); // equal-version replay rejected at catalog admission
|
||||
|
||||
client.on_message(wire, make_have1(wire, sizeof(wire), manifest.merkle_root,
|
||||
manifest.target_id, manifest.fw_version + 1u,
|
||||
manifest.codec_id, manifest.flags));
|
||||
ASSERT_EQ(client.fetchState(), OtaManager::WANT_MANIFEST);
|
||||
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
||||
manifest.manifest_start, OTA_MF_FRAG);
|
||||
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
||||
manifest.manifest_start + OTA_MF_FRAG,
|
||||
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
||||
EXPECT_EQ(client.fetchState(), OtaManager::FAILED); // parsed version defeats the lying HAVE row
|
||||
EXPECT_EQ(client.fetchError(), OtaManager::FETCH_ERROR_VERSION);
|
||||
EXPECT_EQ(store.staged_size(), 0u);
|
||||
}
|
||||
|
||||
TEST(OtaTransfer, SignedOnlyAutofetchRechecksTheParsedManifestFlag) {
|
||||
MotaManifest manifest;
|
||||
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
||||
ASSERT_TRUE(manifest.is_signed());
|
||||
std::array<uint8_t, MOTA_MFL> unsigned_bytes;
|
||||
memcpy(unsigned_bytes.data(), manifest.manifest_start, unsigned_bytes.size());
|
||||
unsigned_bytes[1] &= (uint8_t)~MFLAG_SIGNED;
|
||||
OtaManager client;
|
||||
OtaStoreRam<4096> store;
|
||||
CapturedMessages sent;
|
||||
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
||||
client.set_fetch_store(&store);
|
||||
client.set_autofetch(OtaManager::AUTOFETCH_SIGNED);
|
||||
client.set_auto_version_floor(manifest.fw_version - 1u, true);
|
||||
|
||||
uint8_t wire[64];
|
||||
client.on_message(wire, make_have1(wire, sizeof(wire), manifest.merkle_root,
|
||||
manifest.target_id, manifest.fw_version,
|
||||
manifest.codec_id,
|
||||
(uint8_t)(manifest.flags | MFLAG_SIGNED)));
|
||||
ASSERT_EQ(client.fetchState(), OtaManager::WANT_MANIFEST);
|
||||
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
||||
unsigned_bytes.data(), OTA_MF_FRAG);
|
||||
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
||||
unsigned_bytes.data() + OTA_MF_FRAG,
|
||||
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
||||
EXPECT_EQ(client.fetchState(), OtaManager::FAILED);
|
||||
EXPECT_EQ(client.fetchError(), OtaManager::FETCH_ERROR_MANIFEST);
|
||||
EXPECT_EQ(store.staged_size(), 0u);
|
||||
}
|
||||
|
||||
TEST(OtaTransfer, ExplicitManualPullMayOverrideAutomaticVersionFloor) {
|
||||
MotaManifest manifest;
|
||||
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
||||
OtaManager client;
|
||||
OtaStoreRam<4096> store;
|
||||
CapturedMessages sent;
|
||||
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
||||
client.set_fetch_store(&store);
|
||||
client.set_auto_version_floor(manifest.fw_version, true);
|
||||
|
||||
ASSERT_EQ(client.pull(manifest.merkle_root, manifest.target_id), OtaManager::PULL_STARTED);
|
||||
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
||||
manifest.manifest_start, OTA_MF_FRAG);
|
||||
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
||||
manifest.manifest_start + OTA_MF_FRAG,
|
||||
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
||||
EXPECT_EQ(client.fetchState(), OtaManager::FETCHING);
|
||||
EXPECT_GT(store.staged_size(), 0u);
|
||||
}
|
||||
|
||||
TEST(OtaTransfer, PullAdmissionReportsNoStoreAndBusyWithoutReplacingIntent) {
|
||||
g_q.clear();
|
||||
OtaManager client;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "helpers/ota/OtaFlashLayout_nrf52.h"
|
||||
#include "helpers/ota/OtaStoreQspiNrf52.h"
|
||||
#include "helpers/ota/OtaSdHandoff.h"
|
||||
|
||||
using namespace mesh::ota;
|
||||
|
||||
@@ -325,22 +324,3 @@ TEST(OtaFlashPlan, LeavesOutputUntouchedOnReject) {
|
||||
EXPECT_FALSE(mota_nrf52_stage_plan(CAP_V6 + 1, APP_V6, APP_END_V6, LEGACY, start));
|
||||
EXPECT_EQ(start, 0x1234ABCDu);
|
||||
}
|
||||
|
||||
TEST(OtaSdHandoff, EncodesChecksummedRecordAndPreservesSectorTail) {
|
||||
uint8_t sector[MOTA_SD_SECTOR_SIZE];
|
||||
std::memset(sector, 0xA5, sizeof(sector));
|
||||
mota_sd_encode_handoff(sector, 2048, 1234, 630000, 8000000);
|
||||
|
||||
EXPECT_EQ(0, std::memcmp(sector, MOTA_SD_HANDOFF_MAGIC, 8));
|
||||
EXPECT_EQ(mota_sd_rd32(sector + 8), MOTA_SD_HANDOFF_VERSION);
|
||||
EXPECT_EQ(mota_sd_rd32(sector + 12), 2048u);
|
||||
EXPECT_EQ(mota_sd_rd32(sector + 16), 1234u);
|
||||
EXPECT_EQ(mota_sd_rd32(sector + 20), 630000u);
|
||||
EXPECT_EQ(mota_sd_rd32(sector + 24), ~630000u);
|
||||
EXPECT_EQ(mota_sd_rd32(sector + 28), 8000000u);
|
||||
EXPECT_EQ(mota_sd_rd32(sector + 32), mota_sd_crc32(sector, 32));
|
||||
EXPECT_EQ(sector[MOTA_SD_HANDOFF_LEN], 0xA5); // bytes outside our record are untouched
|
||||
|
||||
sector[20] ^= 1u;
|
||||
EXPECT_NE(mota_sd_rd32(sector + 32), mota_sd_crc32(sector, 32));
|
||||
}
|
||||
|
||||
+128
-18
@@ -90,6 +90,13 @@ XIAO_BOOT_IMAGE_SIZE = 0x0000A000
|
||||
XIAO_BOOT_MANIFEST_MAGIC = b"BLMFCRC1"
|
||||
XIAO_BOOT_MANIFEST_VERSION = 1
|
||||
XIAO_BOOT_MANIFEST_SIZE = 44
|
||||
BOOT_CONTINUITY_MAGIC = b"BLM2SOFT"
|
||||
BOOT_CONTINUITY_VERSION = 2
|
||||
BOOT_CONTINUITY_SIZE = 32
|
||||
BOOT_CONTINUITY_FAMILY_S140 = 140
|
||||
BOOT_CONTINUITY_LAYOUT_ABI = 1
|
||||
BOOT_ENVELOPE_SIZE = XIAO_BOOT_MANIFEST_SIZE + BOOT_CONTINUITY_SIZE
|
||||
BOOT_CANDIDATE_MANIFEST_OFFSET = XIAO_BOOT_IMAGE_SIZE - BOOT_ENVELOPE_SIZE
|
||||
XIAO_BOOT_DEVICE_NAME = b"XIAO_DFU".ljust(16, b"\0")
|
||||
XIAO_BOOT_CAPS_MAGIC = b"MOTABLDR"
|
||||
BOOT_STORAGE_SD = 0x01
|
||||
@@ -133,6 +140,21 @@ SD_BOOTLOADER_IDENTITIES = (
|
||||
(0x239A0071, "TOWER_V2_OTA"),
|
||||
)
|
||||
|
||||
# Exact runtime continuity profiles for signed, remotely installable
|
||||
# bootloaders. A CRC-valid BLM2 envelope is not sufficient for package
|
||||
# creation: signing the wrong SoftDevice/application layout only produces a
|
||||
# package every qualified device will refuse. XIAO/Sense, Minew MX25LE01, and
|
||||
# T1000-E run S140 v7; the remaining inventory (including both Tower storage
|
||||
# profiles) runs S140 v6.
|
||||
BOOTLOADER_S140_V7_IDENTITIES = (
|
||||
(XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
|
||||
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU"),
|
||||
(0x239A0029, "MX25_DFU"),
|
||||
(0x28860057, "T1KE_DFU"),
|
||||
)
|
||||
BOOTLOADER_S140_V6_FWID = 0x00B6
|
||||
BOOTLOADER_S140_V7_FWID = 0x0123
|
||||
|
||||
# MeshTower V2's SD-backed OTA target keeps the staged .mota off-chip, so the application may use the
|
||||
# complete S140 v6 application region up to InternalFS instead of leaving room for internal staging.
|
||||
# This is deliberately target-specific: other nRF52 OTA builds still need NRF52_INPLACE_MEMORY above.
|
||||
@@ -550,11 +572,21 @@ class BootloaderIdentity:
|
||||
board_id: int
|
||||
device_name: str
|
||||
crc32: int
|
||||
boot_version: Optional[int] = None
|
||||
softdevice_family: Optional[int] = None
|
||||
softdevice_fwid: Optional[int] = None
|
||||
app_base: Optional[int] = None
|
||||
layout_abi: Optional[int] = None
|
||||
compat_flags: Optional[int] = None
|
||||
|
||||
|
||||
XiaoBootloaderIdentity = BootloaderIdentity # compatibility for callers of the original reference API
|
||||
|
||||
|
||||
def bootloader_version_valid(version: int) -> bool:
|
||||
return version not in (0, 0xFFFFFFFF) and version & 0xFF != 0
|
||||
|
||||
|
||||
def _bootloader_device_name(board_id: int, name_raw: bytes) -> Optional[str]:
|
||||
if len(name_raw) != 16 or board_id in (0, 0xFFFFFFFF):
|
||||
return None
|
||||
@@ -618,6 +650,33 @@ def bootloader_identity_is_buildable(board_id: int, device_name: str) -> bool:
|
||||
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU")))
|
||||
|
||||
|
||||
def bootloader_qualified_platform_profile(board_id: int, device_name: str):
|
||||
"""Return exact (family, FWID, app_base, layout ABI), or None if unqualified."""
|
||||
identity = (board_id, device_name)
|
||||
if not bootloader_identity_is_buildable(*identity):
|
||||
return None
|
||||
if identity in BOOTLOADER_S140_V7_IDENTITIES:
|
||||
return (BOOT_CONTINUITY_FAMILY_S140, BOOTLOADER_S140_V7_FWID,
|
||||
NRF52_APP_BASE_S140_V7, BOOT_CONTINUITY_LAYOUT_ABI)
|
||||
return (BOOT_CONTINUITY_FAMILY_S140, BOOTLOADER_S140_V6_FWID,
|
||||
NRF52_APP_BASE_S140_V6, BOOT_CONTINUITY_LAYOUT_ABI)
|
||||
|
||||
|
||||
def bootloader_qualified_storage_profiles(board_id: int, device_name: str):
|
||||
"""Return the exact allowed capability-marker profiles for one identity."""
|
||||
identity = (board_id, device_name)
|
||||
if identity in ((XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
|
||||
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU")):
|
||||
return (BOOT_STORAGE_QSPI_UPDATE,)
|
||||
if identity in SD_BOOTLOADER_IDENTITIES:
|
||||
# One embedded Tower identity supports two distinct application
|
||||
# targets; its marker selects either the shared internal slot or SD.
|
||||
return (BOOT_STORAGE_INTERNAL_UPDATE, BOOT_STORAGE_SD_UPDATE)
|
||||
if identity in INTERNAL_BOOTLOADER_IDENTITIES:
|
||||
return (BOOT_STORAGE_INTERNAL_UPDATE,)
|
||||
return None
|
||||
|
||||
|
||||
def _bootloader_vector_sane(image: bytes) -> bool:
|
||||
if len(image) < 8:
|
||||
return False
|
||||
@@ -628,7 +687,13 @@ def _bootloader_vector_sane(image: bytes) -> bool:
|
||||
|
||||
|
||||
def parse_bootloader_identity(image: bytes) -> Optional[BootloaderIdentity]:
|
||||
"""Find exactly one CRC-valid embedded v1 manifest, continuing past invalid decoys."""
|
||||
"""Find one CRC-valid base identity, then validate its optional BLM2 extension.
|
||||
|
||||
Duplicate accounting intentionally mirrors deployed legacy bootloaders: a
|
||||
CRC-valid BLMF-v1 record counts even when adjacent claimed BLM2 metadata is
|
||||
malformed. Otherwise a corrupt extension could hide a second privileged
|
||||
identity from a new packager while an installed legacy updater rejects it.
|
||||
"""
|
||||
if len(image) != XIAO_BOOT_IMAGE_SIZE:
|
||||
return None
|
||||
found = None
|
||||
@@ -649,11 +714,32 @@ def parse_bootloader_identity(image: bytes) -> Optional[BootloaderIdentity]:
|
||||
import zlib
|
||||
if zlib.crc32(crc_image) & 0xFFFFFFFF != stored_crc:
|
||||
continue
|
||||
candidate = BootloaderIdentity(off, start, image_size, board_id, device_name, stored_crc)
|
||||
candidate = BootloaderIdentity(off, start, image_size, board_id, device_name,
|
||||
stored_crc)
|
||||
if found is not None:
|
||||
return None
|
||||
found = candidate
|
||||
return found
|
||||
if found is None:
|
||||
return None
|
||||
|
||||
ext = image[found.manifest_offset + XIAO_BOOT_MANIFEST_SIZE:
|
||||
found.manifest_offset + BOOT_ENVELOPE_SIZE]
|
||||
magic0 = len(ext) >= 4 and ext[:4] == BOOT_CONTINUITY_MAGIC[:4]
|
||||
magic1 = len(ext) >= 8 and ext[4:8] == BOOT_CONTINUITY_MAGIC[4:8]
|
||||
if not magic0 and not magic1:
|
||||
return found
|
||||
if len(ext) != BOOT_CONTINUITY_SIZE or not magic0 or not magic1:
|
||||
return None
|
||||
ext_version, ext_size = struct.unpack_from("<HH", ext, 8)
|
||||
boot_version, family, fwid, app_base, layout_abi, compat, reserved = \
|
||||
struct.unpack_from("<IHHIHHI", ext, 12)
|
||||
if (ext_version != BOOT_CONTINUITY_VERSION or ext_size != BOOT_CONTINUITY_SIZE or
|
||||
not bootloader_version_valid(boot_version) or family == 0 or fwid == 0 or
|
||||
app_base == 0 or layout_abi == 0 or compat != 0 or reserved != 0):
|
||||
return None
|
||||
return BootloaderIdentity(found.manifest_offset, found.image_start, found.image_size,
|
||||
found.board_id, found.device_name, found.crc32,
|
||||
boot_version, family, fwid, app_base, layout_abi, compat)
|
||||
|
||||
|
||||
def parse_xiao_bootloader_identity(image: bytes) -> Optional[XiaoBootloaderIdentity]:
|
||||
@@ -666,6 +752,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."""
|
||||
found = None
|
||||
valid_count = 0
|
||||
for off in range(0, len(image) - 16 + 1, 4):
|
||||
if image[off:off + 8] != XIAO_BOOT_CAPS_MAGIC:
|
||||
continue
|
||||
@@ -674,13 +761,15 @@ def bootloader_caps_storage(image: bytes) -> Optional[int]:
|
||||
if (abi < 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
|
||||
storage not in (BOOT_STORAGE_SD_UPDATE, BOOT_STORAGE_QSPI_UPDATE,
|
||||
BOOT_STORAGE_INTERNAL_UPDATE)):
|
||||
not storage & XIAO_BOOT_STORAGE_UPDATE):
|
||||
continue
|
||||
if found is not None:
|
||||
valid_count += 1
|
||||
if valid_count != 1:
|
||||
return None
|
||||
found = storage
|
||||
return found
|
||||
if storage in (BOOT_STORAGE_SD_UPDATE, BOOT_STORAGE_QSPI_UPDATE,
|
||||
BOOT_STORAGE_INTERNAL_UPDATE):
|
||||
found = storage
|
||||
return found if valid_count == 1 else None
|
||||
|
||||
|
||||
def xiao_bootloader_caps_ok(image: bytes) -> bool:
|
||||
@@ -696,22 +785,38 @@ def validate_bootloader_image(image: bytes, target_id: Optional[int] = None,
|
||||
identity = parse_bootloader_identity(image)
|
||||
if identity is None:
|
||||
raise ValueError("bootloader embedded manifest/CRC is invalid or ambiguous")
|
||||
if identity.boot_version is None:
|
||||
raise ValueError("bootloader lacks the BLM2/SOFT continuity extension")
|
||||
if identity.manifest_offset != BOOT_CANDIDATE_MANIFEST_OFFSET:
|
||||
raise ValueError(
|
||||
f"bootloader continuity envelope must be at exact offset "
|
||||
f"0x{BOOT_CANDIDATE_MANIFEST_OFFSET:04X}")
|
||||
expected_target = bootloader_target_id(identity.board_id, identity.device_name)
|
||||
if target_id is not None and target_id != expected_target:
|
||||
raise ValueError("bootloader target ID does not match embedded identity")
|
||||
expected_hw = bootloader_hw_id(identity.board_id, identity.device_name)
|
||||
if signed_hw_id is not None and bytes(signed_hw_id) != expected_hw:
|
||||
raise ValueError("bootloader signed hw_id does not match embedded identity")
|
||||
if identity.board_id in (XIAO_BOOT_BOARD_ID_BASE, XIAO_BOOT_BOARD_ID_SENSE):
|
||||
expected_storage = (BOOT_STORAGE_QSPI_UPDATE,)
|
||||
elif (identity.board_id, identity.device_name) in SD_BOOTLOADER_IDENTITIES:
|
||||
expected_storage = (BOOT_STORAGE_INTERNAL_UPDATE, BOOT_STORAGE_SD_UPDATE)
|
||||
else:
|
||||
expected_storage = bootloader_qualified_storage_profiles(
|
||||
identity.board_id, identity.device_name)
|
||||
if expected_storage is None:
|
||||
# Keep generic parsing/inspection useful for future canonical images;
|
||||
# only the signing/build path below admits the curated inventory.
|
||||
expected_storage = (BOOT_STORAGE_INTERNAL_UPDATE,)
|
||||
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}")
|
||||
expected_platform = bootloader_qualified_platform_profile(
|
||||
identity.board_id, identity.device_name)
|
||||
if expected_platform is not None:
|
||||
actual_platform = (identity.softdevice_family, identity.softdevice_fwid,
|
||||
identity.app_base, identity.layout_abi)
|
||||
if actual_platform != expected_platform:
|
||||
family, fwid, app_base, layout_abi = expected_platform
|
||||
raise ValueError(
|
||||
"bootloader continuity platform does not match qualified "
|
||||
f"profile S{family}/0x{fwid:04X}/0x{app_base:X}/ABI{layout_abi}")
|
||||
return identity
|
||||
|
||||
|
||||
@@ -745,10 +850,11 @@ def build_manifest(*, target_id: int, fw_version: int, image_size: int, payload:
|
||||
audit_bootloader_target_inventory()
|
||||
if not bootloader_identity_is_buildable(identity.board_id, identity.device_name):
|
||||
raise ValueError("bootloader embedded identity is not in the qualified build inventory")
|
||||
if (fw_version == 0 or not is_full or codec_id != CODEC_FULL or
|
||||
if (not bootloader_version_valid(fw_version) or
|
||||
fw_version != identity.boot_version or not is_full or codec_id != CODEC_FULL or
|
||||
image_size != XIAO_BOOT_IMAGE_SIZE or
|
||||
block_size != DEFAULT_BLOCK_SIZE or base_hash not in (None, b"\0" * 8)):
|
||||
raise ValueError("bootloader package needs a nonzero version and a 40 KiB CODEC_FULL image with 1 KiB blocks")
|
||||
raise ValueError("bootloader package version must equal BLM2 metadata and use a 40 KiB CODEC_FULL image with 1 KiB blocks")
|
||||
if sign_priv is None:
|
||||
raise ValueError("bootloader package must be signed")
|
||||
expected_hw = bootloader_hw_id(identity.board_id, identity.device_name)
|
||||
@@ -847,13 +953,15 @@ def parse_container(blob: bytes) -> Parsed:
|
||||
if rest != b"":
|
||||
raise ValueError("trailing bytes after payload")
|
||||
if m.is_bootloader:
|
||||
if (m.fw_version == 0 or m.codec_id != CODEC_FULL or
|
||||
if (not bootloader_version_valid(m.fw_version) or m.codec_id != CODEC_FULL or
|
||||
m.image_size != XIAO_BOOT_IMAGE_SIZE or
|
||||
m.payload_size != XIAO_BOOT_IMAGE_SIZE or
|
||||
m.block_size_log2 != 10 or m.block_count != 40 or m.base_hash != b"\0" * 8):
|
||||
raise ValueError("v3 bootloader manifest geometry/identity is invalid")
|
||||
try:
|
||||
validate_bootloader_image(payload, m.target_id, m.hw_id)
|
||||
identity = validate_bootloader_image(payload, m.target_id, m.hw_id)
|
||||
if m.fw_version != identity.boot_version:
|
||||
raise ValueError("outer bootloader version does not match BLM2 metadata")
|
||||
except (UnicodeDecodeError, ValueError) as exc:
|
||||
raise ValueError(f"v3 bootloader image identity/capability is invalid: {exc}") from exc
|
||||
return Parsed(manifest=m, payload=payload, total_size=total)
|
||||
@@ -871,7 +979,9 @@ def verify(parsed: Parsed, *, expect_pub: Optional[bytes] = None,
|
||||
|
||||
if m.is_bootloader:
|
||||
try:
|
||||
validate_bootloader_image(payload, m.target_id, m.hw_id)
|
||||
identity = validate_bootloader_image(payload, m.target_id, m.hw_id)
|
||||
if m.fw_version != identity.boot_version:
|
||||
problems.append("outer bootloader version does not match BLM2 metadata")
|
||||
except (UnicodeDecodeError, ValueError) as exc:
|
||||
problems.append(f"bootloader image contract: {exc}")
|
||||
|
||||
|
||||
+217
-14
@@ -208,7 +208,20 @@ def test_nrf52_layout_record_roundtrip_and_policy():
|
||||
pass
|
||||
|
||||
|
||||
def _xiao_bootloader_image(board_id=ml.XIAO_BOOT_BOARD_ID_BASE):
|
||||
def _write_boot_continuity(image, offset, *, version=0x0117010D,
|
||||
family=ml.BOOT_CONTINUITY_FAMILY_S140,
|
||||
fwid=None, app_base=ml.NRF52_APP_BASE_S140_V7,
|
||||
layout_abi=ml.BOOT_CONTINUITY_LAYOUT_ABI):
|
||||
if fwid is None:
|
||||
fwid = 0x0123 if app_base == ml.NRF52_APP_BASE_S140_V7 else 0x00B6
|
||||
struct.pack_into("<8sHHIHHIHHI", image, offset + ml.XIAO_BOOT_MANIFEST_SIZE,
|
||||
ml.BOOT_CONTINUITY_MAGIC, ml.BOOT_CONTINUITY_VERSION,
|
||||
ml.BOOT_CONTINUITY_SIZE, version,
|
||||
family, fwid, app_base, layout_abi, 0, 0)
|
||||
|
||||
|
||||
def _xiao_bootloader_image(board_id=ml.XIAO_BOOT_BOARD_ID_BASE,
|
||||
boot_version=0x0117010D):
|
||||
import zlib
|
||||
image = bytearray(b"\xff" * ml.XIAO_BOOT_IMAGE_SIZE)
|
||||
struct.pack_into("<II", image, 0, 0x20040000, ml.XIAO_BOOT_IMAGE_START + 0x101)
|
||||
@@ -216,16 +229,19 @@ def _xiao_bootloader_image(board_id=ml.XIAO_BOOT_BOARD_ID_BASE):
|
||||
ml.BOOT_FORMAT_VER, ml.BOOT_REQUIRED_APP_CODEC_MASK,
|
||||
ml.BOOT_STORAGE_QSPI_UPDATE)
|
||||
name = ml.XIAO_BOOT_DEVICE_NAME
|
||||
struct.pack_into("<8sHHIII16sI", image, 0x100, ml.XIAO_BOOT_MANIFEST_MAGIC,
|
||||
off = ml.BOOT_CANDIDATE_MANIFEST_OFFSET
|
||||
struct.pack_into("<8sHHIII16sI", image, off, ml.XIAO_BOOT_MANIFEST_MAGIC,
|
||||
ml.XIAO_BOOT_MANIFEST_VERSION, ml.XIAO_BOOT_MANIFEST_SIZE,
|
||||
ml.XIAO_BOOT_IMAGE_START, ml.XIAO_BOOT_IMAGE_SIZE, board_id, name, 0)
|
||||
_write_boot_continuity(image, off, version=boot_version)
|
||||
crc = zlib.crc32(image) & 0xFFFFFFFF
|
||||
struct.pack_into("<I", image, 0x100 + 40, crc)
|
||||
struct.pack_into("<I", image, off + 40, crc)
|
||||
return bytes(image)
|
||||
|
||||
|
||||
def _generic_bootloader_image(board_id=0x239A0029, device_name="3401_DFU",
|
||||
storage=ml.BOOT_STORAGE_INTERNAL_UPDATE):
|
||||
storage=ml.BOOT_STORAGE_INTERNAL_UPDATE,
|
||||
boot_version=0x0117010D):
|
||||
import zlib
|
||||
image = bytearray(b"\xff" * ml.XIAO_BOOT_IMAGE_SIZE)
|
||||
struct.pack_into("<II", image, 0, 0x20040000, ml.XIAO_BOOT_IMAGE_START + 0x101)
|
||||
@@ -233,13 +249,27 @@ def _generic_bootloader_image(board_id=0x239A0029, device_name="3401_DFU",
|
||||
ml.BOOT_FORMAT_VER, ml.BOOT_REQUIRED_APP_CODEC_MASK,
|
||||
storage)
|
||||
name = device_name.encode("ascii").ljust(16, b"\0")
|
||||
struct.pack_into("<8sHHIII16sI", image, 0x100, ml.XIAO_BOOT_MANIFEST_MAGIC,
|
||||
off = ml.BOOT_CANDIDATE_MANIFEST_OFFSET
|
||||
struct.pack_into("<8sHHIII16sI", image, off, ml.XIAO_BOOT_MANIFEST_MAGIC,
|
||||
ml.XIAO_BOOT_MANIFEST_VERSION, ml.XIAO_BOOT_MANIFEST_SIZE,
|
||||
ml.XIAO_BOOT_IMAGE_START, ml.XIAO_BOOT_IMAGE_SIZE, board_id, name, 0)
|
||||
struct.pack_into("<I", image, 0x100 + 40, zlib.crc32(image) & 0xFFFFFFFF)
|
||||
_write_boot_continuity(image, off, version=boot_version,
|
||||
app_base=ml.NRF52_APP_BASE_S140_V6)
|
||||
struct.pack_into("<I", image, off + 40, zlib.crc32(image) & 0xFFFFFFFF)
|
||||
return bytes(image)
|
||||
|
||||
|
||||
def _rewrite_boot_image(image, mutate):
|
||||
import zlib
|
||||
rewritten = bytearray(image)
|
||||
mutate(rewritten)
|
||||
off = ml.BOOT_CANDIDATE_MANIFEST_OFFSET
|
||||
rewritten[off + 40:off + 44] = b"\0" * 4
|
||||
struct.pack_into("<I", rewritten, off + 40,
|
||||
zlib.crc32(rewritten) & 0xFFFFFFFF)
|
||||
return bytes(rewritten)
|
||||
|
||||
|
||||
def _xiao_manifest_crc(image, offset):
|
||||
import zlib
|
||||
trial = bytearray(image)
|
||||
@@ -294,7 +324,8 @@ def _set_two_valid_xiao_manifest_crcs(image, first, second):
|
||||
|
||||
|
||||
def test_xiao_bootloader_identity_skips_bad_decoy_and_rejects_two_valid_manifests():
|
||||
real_offset, decoy_offset, second_offset = 0x100, 0x20, 0x200
|
||||
real_offset = ml.BOOT_CANDIDATE_MANIFEST_OFFSET
|
||||
decoy_offset, second_offset = 0x20, 0x200
|
||||
image = bytearray(_xiao_bootloader_image())
|
||||
image[decoy_offset:decoy_offset + ml.XIAO_BOOT_MANIFEST_SIZE] = \
|
||||
image[real_offset:real_offset + ml.XIAO_BOOT_MANIFEST_SIZE]
|
||||
@@ -319,6 +350,37 @@ def test_xiao_bootloader_identity_skips_bad_decoy_and_rejects_two_valid_manifest
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Base identity counting happens before adjacent continuity parsing. A
|
||||
# CRC-valid decoy cannot hide merely by claiming one corrupt BLM2 magic;
|
||||
# this must match deployed legacy-updater duplicate semantics.
|
||||
corrupt_extension = bytearray(_xiao_bootloader_image())
|
||||
corrupt_extension[second_offset:second_offset + ml.XIAO_BOOT_MANIFEST_SIZE] = \
|
||||
corrupt_extension[real_offset:real_offset + ml.XIAO_BOOT_MANIFEST_SIZE]
|
||||
corrupt_extension[second_offset + ml.XIAO_BOOT_MANIFEST_SIZE:
|
||||
second_offset + ml.XIAO_BOOT_MANIFEST_SIZE + 8] = b"BLM2BAD!"
|
||||
_set_two_valid_xiao_manifest_crcs(corrupt_extension, real_offset, second_offset)
|
||||
assert ml.parse_xiao_bootloader_identity(bytes(corrupt_extension)) is None
|
||||
try:
|
||||
ml.validate_xiao_bootloader_image(bytes(corrupt_extension))
|
||||
assert False, "CRC-valid identity with corrupt BLM2 decoy was hidden"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# The same malformed extension does not matter when the associated base
|
||||
# CRC is invalid; the one canonical CRC-valid identity remains usable.
|
||||
bad_crc_extension = bytearray(_xiao_bootloader_image())
|
||||
bad_crc_extension[second_offset:second_offset + ml.XIAO_BOOT_MANIFEST_SIZE] = \
|
||||
bad_crc_extension[real_offset:real_offset + ml.XIAO_BOOT_MANIFEST_SIZE]
|
||||
bad_crc_extension[second_offset + ml.XIAO_BOOT_MANIFEST_SIZE:
|
||||
second_offset + ml.XIAO_BOOT_MANIFEST_SIZE + 8] = b"BLM2BAD!"
|
||||
struct.pack_into("<I", bad_crc_extension, second_offset + 40, 0xA5A5A5A5)
|
||||
struct.pack_into("<I", bad_crc_extension, real_offset + 40,
|
||||
_xiao_manifest_crc(bad_crc_extension, real_offset))
|
||||
assert _xiao_manifest_crc(bad_crc_extension, second_offset) != 0xA5A5A5A5
|
||||
parsed = ml.parse_xiao_bootloader_identity(bytes(bad_crc_extension))
|
||||
assert parsed is not None and parsed.manifest_offset == real_offset
|
||||
ml.validate_xiao_bootloader_image(bytes(bad_crc_extension))
|
||||
|
||||
|
||||
def test_xiao_bootloader_caps_rejects_malformed_or_unaligned_markers():
|
||||
def caps(*, offset=0, abi=ml.BOOT_FORMAT_VER,
|
||||
@@ -342,6 +404,18 @@ def test_xiao_bootloader_caps_rejects_malformed_or_unaligned_markers():
|
||||
ml.BOOT_FORMAT_VER, ml.BOOT_REQUIRED_APP_CODEC_MASK,
|
||||
ml.BOOT_STORAGE_INTERNAL_UPDATE, b"\0\0\0")
|
||||
assert ml.bootloader_caps_storage(bytes(duplicate)) is None
|
||||
privileged_ambiguous = bytearray(caps())
|
||||
struct.pack_into("<8sHHB3s", privileged_ambiguous, 24,
|
||||
ml.XIAO_BOOT_CAPS_MAGIC, ml.BOOT_FORMAT_VER,
|
||||
ml.BOOT_REQUIRED_APP_CODEC_MASK,
|
||||
ml.XIAO_BOOT_STORAGE_UPDATE, b"\0\0\0")
|
||||
assert ml.bootloader_caps_storage(bytes(privileged_ambiguous)) is None
|
||||
malformed_decoy = bytearray(caps())
|
||||
struct.pack_into("<8sHHB3s", malformed_decoy, 24,
|
||||
ml.XIAO_BOOT_CAPS_MAGIC, ml.BOOT_FORMAT_VER,
|
||||
ml.BOOT_REQUIRED_APP_CODEC_MASK, 0x1C, b"\0\0\0")
|
||||
assert ml.bootloader_caps_storage(bytes(malformed_decoy)) == \
|
||||
ml.BOOT_STORAGE_QSPI_UPDATE
|
||||
|
||||
|
||||
def test_bootloader_v3_build_parse_and_strict_contract():
|
||||
@@ -350,7 +424,7 @@ def test_bootloader_v3_build_parse_and_strict_contract():
|
||||
identity = ml.validate_xiao_bootloader_image(image, ml.XIAO_BOOT_BOARD_ID_BASE)
|
||||
assert identity.board_id == ml.XIAO_BOOT_BOARD_ID_BASE
|
||||
m = ml.build_manifest(
|
||||
target_id=identity.board_id, fw_version=ml.pack_version("1.0.0"),
|
||||
target_id=identity.board_id, 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)
|
||||
@@ -394,7 +468,7 @@ def test_generic_internal_bootloader_build_parse_and_strict_contract():
|
||||
assert ml.bootloader_hw_id(identity.board_id, identity.device_name) == expected_hw
|
||||
|
||||
manifest = ml.build_manifest(
|
||||
target_id=target, fw_version=ml.pack_version("1.0.0"),
|
||||
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)
|
||||
@@ -425,7 +499,7 @@ def test_meshtower_sd_bootloader_profile_builds_the_same_exact_identity():
|
||||
target = ml.bootloader_target_id(identity.board_id, identity.device_name)
|
||||
assert target == 0x1150F50E
|
||||
manifest = ml.build_manifest(
|
||||
target_id=target, fw_version=ml.pack_version("1.0.0"),
|
||||
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)
|
||||
@@ -442,6 +516,11 @@ def test_meshtower_sd_bootloader_build_flag_is_exact_target_only():
|
||||
section = section.split("[env:", 1)[0]
|
||||
assert "-D OTA_SD_STORE=1" in section
|
||||
assert "-D OTA_SD_BOOTLOADER_UPDATE=1" in section
|
||||
store = (root / "src/helpers/ota/OtaStoreSdNrf52.cpp").read_text(encoding="utf-8")
|
||||
assert "readSector(" not in store
|
||||
assert "writeSector(" not in store
|
||||
assert "OtaSdHandoff" not in store
|
||||
assert not (root / "src/helpers/ota/OtaSdHandoff.h").exists()
|
||||
|
||||
|
||||
def test_bootloader_build_inventory_is_unique_and_disjoint_from_app_targets():
|
||||
@@ -491,10 +570,83 @@ def test_bootloader_build_inventory_is_unique_and_disjoint_from_app_targets():
|
||||
pass
|
||||
|
||||
|
||||
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
|
||||
for board_id, device_name in qualified:
|
||||
family, fwid, app_base, layout_abi = \
|
||||
ml.bootloader_qualified_platform_profile(board_id, device_name)
|
||||
storages = ml.bootloader_qualified_storage_profiles(board_id, device_name)
|
||||
assert family == ml.BOOT_CONTINUITY_FAMILY_S140
|
||||
assert layout_abi == ml.BOOT_CONTINUITY_LAYOUT_ABI
|
||||
if (board_id, device_name) in ml.BOOTLOADER_S140_V7_IDENTITIES:
|
||||
assert (fwid, app_base) == (ml.BOOTLOADER_S140_V7_FWID,
|
||||
ml.NRF52_APP_BASE_S140_V7)
|
||||
else:
|
||||
assert (fwid, app_base) == (ml.BOOTLOADER_S140_V6_FWID,
|
||||
ml.NRF52_APP_BASE_S140_V6)
|
||||
for storage in storages:
|
||||
if board_id in (ml.XIAO_BOOT_BOARD_ID_BASE,
|
||||
ml.XIAO_BOOT_BOARD_ID_SENSE):
|
||||
image = _xiao_bootloader_image(board_id)
|
||||
else:
|
||||
image = bytearray(_generic_bootloader_image(
|
||||
board_id=board_id, device_name=device_name, storage=storage))
|
||||
off = ml.BOOT_CANDIDATE_MANIFEST_OFFSET
|
||||
_write_boot_continuity(image, off, fwid=fwid,
|
||||
app_base=app_base)
|
||||
image = _rewrite_boot_image(bytes(image), lambda raw: None)
|
||||
parsed = ml.validate_bootloader_image(image)
|
||||
assert (parsed.softdevice_family, parsed.softdevice_fwid,
|
||||
parsed.app_base, parsed.layout_abi) == \
|
||||
(family, fwid, app_base, layout_abi)
|
||||
assert ml.bootloader_caps_storage(image) == storage
|
||||
|
||||
# Every individually well-formed but wrong continuity field must fail the
|
||||
# qualified signing validator, for both v6 and v7 inventory profiles.
|
||||
for image in (_generic_bootloader_image(), _xiao_bootloader_image()):
|
||||
off = ml.BOOT_CANDIDATE_MANIFEST_OFFSET
|
||||
for field_offset, field_format, wrong in (
|
||||
(60, "<H", 141),
|
||||
(62, "<H", 0xBEEF),
|
||||
(64, "<I", 0x28000),
|
||||
(68, "<H", 2),
|
||||
):
|
||||
invalid = _rewrite_boot_image(
|
||||
image, lambda raw, at=field_offset, fmt=field_format, value=wrong:
|
||||
struct.pack_into(fmt, raw, off + at, value))
|
||||
try:
|
||||
ml.validate_bootloader_image(invalid)
|
||||
assert False, f"qualified image accepted wrong continuity field +{field_offset}"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Capability storage is part of the profile too. Tower alone admits both
|
||||
# internal and SD; QSPI is XIAO-only and non-Tower internals cannot claim SD.
|
||||
for invalid in (
|
||||
_generic_bootloader_image(storage=ml.BOOT_STORAGE_SD_UPDATE),
|
||||
_generic_bootloader_image(board_id=0x239A0071,
|
||||
device_name="TOWER_V2_OTA",
|
||||
storage=ml.BOOT_STORAGE_QSPI_UPDATE),
|
||||
_rewrite_boot_image(
|
||||
_xiao_bootloader_image(),
|
||||
lambda raw: raw.__setitem__(0x80 + 12,
|
||||
ml.BOOT_STORAGE_INTERNAL_UPDATE)),
|
||||
):
|
||||
try:
|
||||
ml.validate_bootloader_image(invalid)
|
||||
assert False, "qualified identity accepted wrong storage profile"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def test_bootloader_builder_rejects_wrong_identity_geometry_and_continuity():
|
||||
priv = Ed25519PrivateKey.generate()
|
||||
image = _xiao_bootloader_image()
|
||||
kwargs = dict(target_id=ml.XIAO_BOOT_BOARD_ID_BASE, fw_version=1,
|
||||
kwargs = dict(target_id=ml.XIAO_BOOT_BOARD_ID_BASE,
|
||||
fw_version=ml.validate_xiao_bootloader_image(image).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)
|
||||
@@ -516,11 +668,62 @@ def test_bootloader_builder_rejects_wrong_identity_geometry_and_continuity():
|
||||
assert False, "bad embedded CRC accepted"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
missing_extension = _rewrite_boot_image(
|
||||
image, lambda raw: raw.__setitem__(
|
||||
slice(ml.BOOT_CANDIDATE_MANIFEST_OFFSET + 44,
|
||||
ml.BOOT_CANDIDATE_MANIFEST_OFFSET + 76), b"\xff" * 32))
|
||||
half_extension = _rewrite_boot_image(
|
||||
image, lambda raw: raw.__setitem__(
|
||||
slice(ml.BOOT_CANDIDATE_MANIFEST_OFFSET + 48,
|
||||
ml.BOOT_CANDIDATE_MANIFEST_OFFSET + 52), b"FAIL"))
|
||||
low_byte_zero = _rewrite_boot_image(
|
||||
image, lambda raw: struct.pack_into(
|
||||
"<I", raw, ml.BOOT_CANDIDATE_MANIFEST_OFFSET + 56, 0x02040100))
|
||||
all_ones = _rewrite_boot_image(
|
||||
image, lambda raw: struct.pack_into(
|
||||
"<I", raw, ml.BOOT_CANDIDATE_MANIFEST_OFFSET + 56, 0xFFFFFFFF))
|
||||
for invalid, why in (
|
||||
(missing_extension, "missing continuity extension"),
|
||||
(half_extension, "half-present continuity extension"),
|
||||
(low_byte_zero, "invalid preview zero"),
|
||||
(all_ones, "un-upgradable all-ones version"),
|
||||
):
|
||||
try:
|
||||
ml.validate_xiao_bootloader_image(invalid)
|
||||
assert False, f"accepted {why}"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
import zlib
|
||||
wrong_offset = bytearray(image)
|
||||
canonical = ml.BOOT_CANDIDATE_MANIFEST_OFFSET
|
||||
relocated = 0x8000
|
||||
envelope = bytes(wrong_offset[canonical:canonical + ml.BOOT_ENVELOPE_SIZE])
|
||||
wrong_offset[canonical:canonical + ml.BOOT_ENVELOPE_SIZE] = \
|
||||
b"\xff" * ml.BOOT_ENVELOPE_SIZE
|
||||
wrong_offset[relocated:relocated + ml.BOOT_ENVELOPE_SIZE] = envelope
|
||||
wrong_offset[relocated + 40:relocated + 44] = b"\0" * 4
|
||||
struct.pack_into("<I", wrong_offset, relocated + 40,
|
||||
zlib.crc32(wrong_offset) & 0xFFFFFFFF)
|
||||
parsed_relocated = ml.parse_xiao_bootloader_identity(bytes(wrong_offset))
|
||||
assert parsed_relocated is not None and parsed_relocated.manifest_offset == relocated
|
||||
try:
|
||||
ml.validate_xiao_bootloader_image(bytes(wrong_offset))
|
||||
assert False, "otherwise-valid BLM2 envelope at a noncanonical offset accepted"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
ml.build_manifest(**(kwargs | {"fw_version": kwargs["fw_version"] + 1}))
|
||||
assert False, "outer version differing from embedded BLM2 accepted"
|
||||
except ValueError:
|
||||
pass
|
||||
wrong_name = bytearray(image)
|
||||
wrong_name[0x100 + 24:0x100 + 40] = b"FRIENDLY NAME".ljust(16, b"\0")
|
||||
wrong_name[0x100 + 40:0x100 + 44] = b"\0" * 4
|
||||
struct.pack_into("<I", wrong_name, 0x100 + 40, zlib.crc32(wrong_name) & 0xFFFFFFFF)
|
||||
off = ml.BOOT_CANDIDATE_MANIFEST_OFFSET
|
||||
wrong_name[off + 24:off + 40] = b"FRIENDLY NAME".ljust(16, b"\0")
|
||||
wrong_name[off + 40:off + 44] = b"\0" * 4
|
||||
struct.pack_into("<I", wrong_name, off + 40, zlib.crc32(wrong_name) & 0xFFFFFFFF)
|
||||
try:
|
||||
ml.validate_xiao_bootloader_image(bytes(wrong_name))
|
||||
assert False, "noncanonical embedded device name accepted"
|
||||
|
||||
@@ -53,6 +53,7 @@ build_flags =
|
||||
; packages when OTAFIX advertises the matching SD self-update profile.
|
||||
[Heltec_tower_v2_sdcard]
|
||||
extends = Heltec_tower_v2
|
||||
board_build.ldscript = boards/nrf52840_s140_v6_sd_ota.ld
|
||||
build_flags =
|
||||
${Heltec_tower_v2.build_flags}
|
||||
-D HELTEC_TOWER_V2_SDCARD=1
|
||||
|
||||
Reference in New Issue
Block a user