Revert adaptive RAK app-owned bootloader recovery after ACL test
This commit is contained in:
1 parent
a007346561
commit
ea40663f09
17 files changed
+49
-521
No files matched your search
@@ -149,7 +149,7 @@ MIN_MESHCLI_VERSION = (1, 6, 0)
|
||||
# Tested format-3 inspect/verify/serve support. Keep automatic repairs pinned;
|
||||
# neither a package nor a moving branch may choose code to install on the host.
|
||||
MOTATOOL_REPAIR_REPOSITORY = "https://github.com/mikecarper/motatool.git"
|
||||
MOTATOOL_REPAIR_REVISION = "beff192aec341077f6d9d4b4938e47323775b9eb"
|
||||
MOTATOOL_REPAIR_REVISION = "8c38369e7d35ad50cf74261869676d52dd24adf7"
|
||||
MOTATOOL_REPAIR_TIMEOUT_SECONDS = 60 * 60
|
||||
# The pinned lockfile, not motatool's older package-level rust-version: zeroize
|
||||
# needs edition 2024 (1.85), and idna_adapter/ICU 2.2 require Rust 1.86.
|
||||
@@ -1085,15 +1085,11 @@ def compatible_mota(info: MotaInfo, target: TargetInfo) -> tuple[bool, str]:
|
||||
f"bootloader target {info.target_id:08X} hw={info.hw_id}, need "
|
||||
f"{target.boot_target_id:08X} hw={target.boot_hw_id}"
|
||||
)
|
||||
recovery = (target.boot_storage == 0x16 and
|
||||
info.bootloader_storage == 0x16)
|
||||
if target.bootloader_abi is None or target.bootloader_abi < (2 if recovery else 3):
|
||||
return False, "bootloader install requires a capable installed OTAFIX profile"
|
||||
if target.bootloader_abi is None or target.bootloader_abi < 3:
|
||||
return False, "bootloader self-update requires installed ABI 3 or newer"
|
||||
if target.bootloader_codecs != 0x5:
|
||||
return False, "bootloader self-update requires FULL|INPLACE codecs 0x5"
|
||||
expected_storage = (0x16 if recovery else
|
||||
0x09 if target.nrf_sd else
|
||||
0x0E if target.nrf_qspi else 0x0A)
|
||||
expected_storage = 0x09 if target.nrf_sd else 0x0E if target.nrf_qspi else 0x0A
|
||||
if (
|
||||
target.boot_storage != expected_storage
|
||||
or info.bootloader_storage != expected_storage
|
||||
@@ -2769,7 +2765,7 @@ def query_bootloader_target(
|
||||
if match is None:
|
||||
raise OtaError(
|
||||
f"destination cannot stage a bootloader update: {reply}; "
|
||||
"requires an exact-board update or adaptive RAK recovery application"
|
||||
"requires exact-board ABI-3 self-update bootloader and capable application"
|
||||
)
|
||||
board, target_id, name, _crc, abi, caps = match.groups()
|
||||
library = bootloader_library()
|
||||
@@ -2783,9 +2779,7 @@ def query_bootloader_target(
|
||||
raise OtaError(f"invalid destination bootloader identity: {exc}") from exc
|
||||
if derived_target != int(target_id, 16):
|
||||
raise OtaError("destination bootloader board/name does not match its target ID")
|
||||
recovery = int(caps, 16) == 0x16 and (int(board, 16), name) in \
|
||||
library.RAK_AUTO_RECOVERY_IDENTITIES
|
||||
if int(abi) < (2 if recovery else 3) or int(abi) != target.bootloader_abi:
|
||||
if int(abi) < 3 or int(abi) != target.bootloader_abi:
|
||||
raise OtaError("destination reports inconsistent or unsupported bootloader ABI")
|
||||
return replace(
|
||||
target, boot_target_id=derived_target, boot_hw_id=hw_id,
|
||||
|
||||
@@ -21,7 +21,7 @@ import lora_ota as ota
|
||||
from test_lora_ota import firmware, mota_blob, prepare_args, target, VERSION_NEW
|
||||
|
||||
|
||||
def boot_blob(*, storage=0x0A, version=0x0117010D, rak_name="3401_AUTO_DFU"):
|
||||
def boot_blob(*, storage=0x0A, version=0x0117010D):
|
||||
"""Build a signed test-only candidate using the canonical reference builder."""
|
||||
ml = ota.bootloader_library()
|
||||
board, name, app_base = 0x239A0029, "3401_DFU", ml.NRF52_APP_BASE_S140_V6
|
||||
@@ -30,12 +30,9 @@ def boot_blob(*, storage=0x0A, version=0x0117010D, rak_name="3401_AUTO_DFU"):
|
||||
name = ml.XIAO_BOOT_DEVICE_NAME.rstrip(b"\0").decode("ascii")
|
||||
elif storage == 0x09:
|
||||
board, name = 0x239A0071, "TOWER_V2_OTA"
|
||||
elif storage == 0x16:
|
||||
board, name = 0x239A0029, rak_name
|
||||
image = bytearray(b"\xff" * ml.XIAO_BOOT_IMAGE_SIZE)
|
||||
struct.pack_into("<II", image, 0, 0x20040000, ml.XIAO_BOOT_IMAGE_START + 0x101)
|
||||
struct.pack_into("<8sHHB3x", image, 0x80, ml.XIAO_BOOT_CAPS_MAGIC,
|
||||
2 if storage == 0x16 else 3, 5, storage)
|
||||
struct.pack_into("<8sHHB3x", image, 0x80, ml.XIAO_BOOT_CAPS_MAGIC, 3, 5, storage)
|
||||
offset = ml.BOOT_CANDIDATE_MANIFEST_OFFSET
|
||||
struct.pack_into(
|
||||
"<8sHHIII16sI", image, offset, ml.XIAO_BOOT_MANIFEST_MAGIC,
|
||||
@@ -62,8 +59,7 @@ def boot_blob(*, storage=0x0A, version=0x0117010D, rak_name="3401_AUTO_DFU"):
|
||||
|
||||
def boot_target(package):
|
||||
return replace(
|
||||
target(platform="nrf52", boot_codecs=5),
|
||||
bootloader_abi=2 if package.bootloader_storage == 0x16 else 3,
|
||||
target(platform="nrf52", boot_codecs=5), bootloader_abi=3,
|
||||
boot_target_id=package.target_id, boot_hw_id=package.hw_id,
|
||||
boot_storage=package.bootloader_storage,
|
||||
nrf_sd=package.bootloader_storage == 9,
|
||||
@@ -86,31 +82,11 @@ class BootloaderStagingTests(unittest.TestCase):
|
||||
self.assertEqual(package.payload_size, 40960)
|
||||
|
||||
def test_all_supported_storage_profiles(self):
|
||||
for storage in (10, 14, 9, 22):
|
||||
for storage in (10, 14, 9):
|
||||
with self.subTest(storage=storage):
|
||||
package = ota.parse_mota(boot_blob(storage=storage))
|
||||
self.assertEqual(ota.compatible_mota(package, boot_target(package)), (True, ""))
|
||||
|
||||
def test_adaptive_rak_recovery_accepts_internal_and_nor_storage(self):
|
||||
for name in ("3401_AUTO_DFU", "4631_AUTO_DFU"):
|
||||
package = ota.parse_mota(boot_blob(storage=0x16, rak_name=name))
|
||||
for external in (False, True):
|
||||
live = replace(boot_target(package), nrf_qspi=external)
|
||||
self.assertEqual(ota.compatible_mota(package, live), (True, ""))
|
||||
live = replace(boot_target(package), bootloader_abi=1)
|
||||
self.assertFalse(ota.compatible_mota(package, live)[0])
|
||||
|
||||
def test_adaptive_rak_bootloader_status_uses_app_owned_recovery(self):
|
||||
controller = mock.Mock()
|
||||
controller.remote_command.return_value = (
|
||||
"BL board=239A0029 target=D04AB3AB name=3401_AUTO_DFU "
|
||||
"crc=12345678 abi=2 caps=16 | staged:none mid=- hash=-"
|
||||
)
|
||||
live = replace(target(platform="nrf52", boot_codecs=5), bootloader_abi=2)
|
||||
result = ota.query_bootloader_target(controller, live)
|
||||
self.assertEqual(result.boot_target_id, 0xD04AB3AB)
|
||||
self.assertEqual(result.boot_storage, 0x16)
|
||||
|
||||
def test_malformed_bootloaders_fail_closed(self):
|
||||
for offset in (9, 10, 11, 27, 28, 105, 137, 201, 205, 600):
|
||||
with self.subTest(offset=offset):
|
||||
|
||||
@@ -63,11 +63,9 @@ targeting a deployed receiver that predates the extended transport descriptor.
|
||||
Passing `bootloader=True` is a deliberately narrow reference-only path: it
|
||||
requires a nonzero version, a signed exact 40 KiB OTAFIX region, 1024-byte
|
||||
blocks, full codec, zero base hash, a sane vector table, an exact embedded
|
||||
manifest/CRC identity, and a marker retaining both application codecs
|
||||
(`FULL|INPLACE`, mask `0x0005`). Self-update profiles require ABI 3 and the
|
||||
exact boot-update storage capability. The two adaptive RAK recovery profiles
|
||||
use ABI 2 and exact `0x16` application capabilities; their MeshCore
|
||||
application verifies and copies the image through the MBR. Deployed XIAO packages retain their
|
||||
manifest/CRC identity, and an ABI-3 marker retaining both application codecs
|
||||
(`FULL|INPLACE`, mask `0x0005`), the selected storage, and boot-update
|
||||
capabilities. Deployed XIAO packages retain their
|
||||
`XIAO_BL_28860044`/`XIAO_BL_28860045` IDs. Generic packages derive the padded
|
||||
`NRF_BL_<BOARD_ID>_<DEVICE_NAME>` ID and collision-checked wire target from the
|
||||
full embedded manifest pair. Parsing and `verify()` repeat those gates; magic
|
||||
@@ -78,8 +76,7 @@ builder accepts only identities in the qualified inventory. That inventory
|
||||
currently covers the shared-internal nRF52840 targets, the two deployed XIAO
|
||||
raw-QSPI identities, and the exact MeshTower V2 identity whose candidate
|
||||
marker may select either its internal (`0x0A`) or microSD (`0x09`) application
|
||||
layout, plus the two exact adaptive RAK identities (`3401_AUTO_DFU` and
|
||||
`4631_AUTO_DFU`). The test suite verifies those boot target IDs are unique and disjoint
|
||||
layout. The test suite verifies those boot target IDs are unique and disjoint
|
||||
from the generated application target table.
|
||||
|
||||
This library does not authorize a device update. A capable node will only arm
|
||||
|
||||
+8
-28
@@ -108,15 +108,11 @@ BOOT_STORAGE_SD = 0x01
|
||||
BOOT_STORAGE_STAGE_CEILING = 0x02
|
||||
XIAO_BOOT_STORAGE_QSPI = 0x04
|
||||
XIAO_BOOT_STORAGE_UPDATE = 0x08
|
||||
BOOT_STORAGE_KNOWN = 0x1F
|
||||
BOOT_STORAGE_HEADER_W25 = 0x10
|
||||
BOOT_STORAGE_KNOWN = 0x0F
|
||||
BOOT_STORAGE_SD_UPDATE = BOOT_STORAGE_SD | XIAO_BOOT_STORAGE_UPDATE
|
||||
BOOT_STORAGE_QSPI_UPDATE = (BOOT_STORAGE_STAGE_CEILING | XIAO_BOOT_STORAGE_QSPI |
|
||||
XIAO_BOOT_STORAGE_UPDATE)
|
||||
BOOT_STORAGE_INTERNAL_UPDATE = BOOT_STORAGE_STAGE_CEILING | XIAO_BOOT_STORAGE_UPDATE
|
||||
BOOT_STORAGE_RAK_AUTO_RECOVERY = (BOOT_STORAGE_STAGE_CEILING |
|
||||
XIAO_BOOT_STORAGE_QSPI |
|
||||
BOOT_STORAGE_HEADER_W25)
|
||||
BOOT_REQUIRED_APP_CODEC_MASK = ((1 << CODEC_FULL) |
|
||||
(1 << CODEC_DETOOLS_INPLACE))
|
||||
|
||||
@@ -142,13 +138,6 @@ INTERNAL_BOOTLOADER_IDENTITIES = (
|
||||
(0x239A0029, "RTAG_DFU"),
|
||||
)
|
||||
|
||||
# These adaptive loaders do not self-update. Their exact MeshCore application
|
||||
# authenticates a format-3 package and uses the MBR to replace the bootloader.
|
||||
RAK_AUTO_RECOVERY_IDENTITIES = (
|
||||
(0x239A0029, "3401_AUTO_DFU"),
|
||||
(0x239A0029, "4631_AUTO_DFU"),
|
||||
)
|
||||
|
||||
# MeshTower V2 has both a lean internal-store application and an exact SD-store
|
||||
# application. They intentionally retain the same embedded bootloader identity
|
||||
# and signed wire target; the candidate capability marker selects the matching
|
||||
@@ -707,7 +696,7 @@ def audit_bootloader_target_inventory(application_target_ids=()) -> dict:
|
||||
identities = (
|
||||
(XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
|
||||
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU"),
|
||||
) + INTERNAL_BOOTLOADER_IDENTITIES + RAK_AUTO_RECOVERY_IDENTITIES
|
||||
) + INTERNAL_BOOTLOADER_IDENTITIES
|
||||
app_ids = set(application_target_ids)
|
||||
targets = {}
|
||||
for identity in identities:
|
||||
@@ -725,7 +714,6 @@ def audit_bootloader_target_inventory(application_target_ids=()) -> dict:
|
||||
|
||||
def bootloader_identity_is_buildable(board_id: int, device_name: str) -> bool:
|
||||
return ((board_id, device_name) in INTERNAL_BOOTLOADER_IDENTITIES or
|
||||
(board_id, device_name) in RAK_AUTO_RECOVERY_IDENTITIES or
|
||||
(board_id, device_name) in (
|
||||
(XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
|
||||
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU")))
|
||||
@@ -755,8 +743,6 @@ def bootloader_qualified_storage_profiles(board_id: int, device_name: str):
|
||||
return (BOOT_STORAGE_INTERNAL_UPDATE, BOOT_STORAGE_SD_UPDATE)
|
||||
if identity in INTERNAL_BOOTLOADER_IDENTITIES:
|
||||
return (BOOT_STORAGE_INTERNAL_UPDATE,)
|
||||
if identity in RAK_AUTO_RECOVERY_IDENTITIES:
|
||||
return (BOOT_STORAGE_RAK_AUTO_RECOVERY,)
|
||||
return None
|
||||
|
||||
|
||||
@@ -833,7 +819,7 @@ def parse_xiao_bootloader_identity(image: bytes) -> Optional[XiaoBootloaderIdent
|
||||
|
||||
|
||||
def bootloader_caps_storage(image: bytes) -> Optional[int]:
|
||||
"""Return one exact qualified self-update or app-owned recovery marker."""
|
||||
"""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):
|
||||
@@ -841,22 +827,16 @@ def bootloader_caps_storage(image: bytes) -> Optional[int]:
|
||||
continue
|
||||
abi, codecs = struct.unpack_from("<HH", image, off + 8)
|
||||
storage = image[off + 12]
|
||||
if (abi < (APP_FORMAT_VER if storage == BOOT_STORAGE_RAK_AUTO_RECOVERY
|
||||
else BOOT_FORMAT_VER) or abi == 0xFFFF or
|
||||
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
|
||||
(storage & BOOT_STORAGE_HEADER_W25 and
|
||||
storage != BOOT_STORAGE_RAK_AUTO_RECOVERY) or
|
||||
image[off + 13:off + 16] != b"\0\0\0" or
|
||||
(not storage & XIAO_BOOT_STORAGE_UPDATE and
|
||||
storage != BOOT_STORAGE_RAK_AUTO_RECOVERY)):
|
||||
storage & ~BOOT_STORAGE_KNOWN or image[off + 13:off + 16] != b"\0\0\0" or
|
||||
not storage & XIAO_BOOT_STORAGE_UPDATE):
|
||||
continue
|
||||
valid_count += 1
|
||||
if valid_count != 1:
|
||||
return None
|
||||
if storage in (BOOT_STORAGE_SD_UPDATE, BOOT_STORAGE_QSPI_UPDATE,
|
||||
BOOT_STORAGE_INTERNAL_UPDATE,
|
||||
BOOT_STORAGE_RAK_AUTO_RECOVERY):
|
||||
BOOT_STORAGE_INTERNAL_UPDATE):
|
||||
found = storage
|
||||
return found if valid_count == 1 else None
|
||||
|
||||
@@ -895,7 +875,7 @@ def validate_bootloader_image(image: bytes, target_id: Optional[int] = None,
|
||||
actual_storage = bootloader_caps_storage(image)
|
||||
if actual_storage not in expected_storage:
|
||||
expected = "/".join(f"0x{value:02X}" for value in expected_storage)
|
||||
raise ValueError(f"bootloader lacks exact qualified update/recovery capabilities {expected}")
|
||||
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:
|
||||
|
||||
+4
-36
@@ -1328,12 +1328,12 @@ def _xiao_bootloader_image(board_id=ml.XIAO_BOOT_BOARD_ID_BASE,
|
||||
|
||||
def _generic_bootloader_image(board_id=0x239A0029, device_name="3401_DFU",
|
||||
storage=ml.BOOT_STORAGE_INTERNAL_UPDATE,
|
||||
boot_version=0x0117010D, apply_abi=ml.BOOT_FORMAT_VER):
|
||||
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)
|
||||
struct.pack_into("<8sHHB3x", image, 0x80, ml.XIAO_BOOT_CAPS_MAGIC,
|
||||
apply_abi, ml.BOOT_REQUIRED_APP_CODEC_MASK,
|
||||
ml.BOOT_FORMAT_VER, ml.BOOT_REQUIRED_APP_CODEC_MASK,
|
||||
storage)
|
||||
name = device_name.encode("ascii").ljust(16, b"\0")
|
||||
off = ml.BOOT_CANDIDATE_MANIFEST_OFFSET
|
||||
@@ -1575,36 +1575,6 @@ def test_generic_internal_bootloader_build_parse_and_strict_contract():
|
||||
pass
|
||||
|
||||
|
||||
def test_adaptive_rak_recovery_bootloader_packages_are_exact():
|
||||
priv = Ed25519PrivateKey.generate()
|
||||
for board_id, name in ml.RAK_AUTO_RECOVERY_IDENTITIES:
|
||||
image = _generic_bootloader_image(
|
||||
board_id=board_id, device_name=name,
|
||||
storage=ml.BOOT_STORAGE_RAK_AUTO_RECOVERY,
|
||||
apply_abi=ml.APP_FORMAT_VER)
|
||||
identity = ml.validate_bootloader_image(image)
|
||||
target = ml.bootloader_target_id(board_id, name)
|
||||
manifest = ml.build_manifest(
|
||||
target_id=target, fw_version=identity.boot_version,
|
||||
image_size=len(image), payload=image, block_size=1024,
|
||||
image_hash=ml.mh32(image), codec_id=ml.CODEC_FULL,
|
||||
is_full=True, sign_priv=priv, bootloader=True)
|
||||
parsed = ml.parse_container(ml.build_container(manifest, image))
|
||||
assert ml.verify(parsed, expect_pub=ml.ed25519_public_bytes(priv)) == []
|
||||
assert parsed.manifest.target_id == target
|
||||
assert ml.bootloader_caps_storage(image) == ml.BOOT_STORAGE_RAK_AUTO_RECOVERY
|
||||
|
||||
for wrong in (ml.BOOT_STORAGE_INTERNAL_UPDATE,
|
||||
ml.BOOT_STORAGE_QSPI_UPDATE):
|
||||
invalid = _generic_bootloader_image(
|
||||
board_id=board_id, device_name=name, storage=wrong)
|
||||
try:
|
||||
ml.validate_bootloader_image(invalid)
|
||||
assert False, "adaptive recovery accepted wrong loader capabilities"
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def test_gat562_internal_bootloader_profile_is_exact():
|
||||
priv = Ed25519PrivateKey.generate()
|
||||
image = _generic_bootloader_image(device_name="GAT562_DFU")
|
||||
@@ -1702,9 +1672,7 @@ def test_bootloader_build_inventory_is_unique_and_disjoint_from_app_targets():
|
||||
for value in re.findall(r"\{ 0x([0-9a-fA-F]{8}),", target_header)
|
||||
}
|
||||
audited = ml.audit_bootloader_target_inventory(app_targets)
|
||||
assert len(audited) == len(expected) + 4 # two adaptive RAKs and two XIAOs
|
||||
assert audited[0xD04AB3AB] == (0x239A0029, "3401_AUTO_DFU")
|
||||
assert audited[0xFEEAFD1B] == (0x239A0029, "4631_AUTO_DFU")
|
||||
assert len(audited) == len(expected) + 2 # generic internal plus two legacy XIAOs
|
||||
|
||||
# Generic parsing remains useful for inspecting a future candidate, but a
|
||||
# signing/build call fails closed until its exact identity is qualified.
|
||||
@@ -1726,7 +1694,7 @@ def test_qualified_bootloader_platform_and_storage_profiles_are_exact():
|
||||
qualified = (
|
||||
(ml.XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
|
||||
(ml.XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU"),
|
||||
) + ml.INTERNAL_BOOTLOADER_IDENTITIES + ml.RAK_AUTO_RECOVERY_IDENTITIES
|
||||
) + 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)
|
||||
|
||||
Reference in new issue
Block a user