From ec8718f05ec2a75a232187a63897250c69cbc565 Mon Sep 17 00:00:00 2001 From: Julian Oes Date: Thu, 28 May 2026 14:21:49 +1200 Subject: [PATCH] feat(bootloader): Revive secure-boot with example, docs, and various fixes (#27237) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(secure_bootloader): add ed25519 key and signing helpers Scaffolding for PX4 secure-boot firmware signing, split into two self-contained scripts: - generate_signing_keys.py: produces .json (private+public hex) for use by sign_firmware.py and .pub (C-array public key) for inclusion in the bootloader build via CONFIG_PUBLIC_KEYn. Refuses to overwrite existing private-key files. - sign_firmware.py: pads an input .bin to a 4-byte boundary and appends a 64-byte ed25519 signature, producing a file that drops directly into the flash slot described by the image TOC. Optionally appends an R&D certificate binary after the signature. Replaces the signing path of the old Tools/cryptotools.py that was removed along with the log-encryption cleanup; the new layout keeps bootloader-signing tooling separate from log encryption to avoid confusing the two independent crypto surfaces. * fix(bootloader): panic if px4_get_secure_random is ever called sw_crypto's crypto_open() unconditionally references px4_get_secure_random from its XCHACHA20 path, so even a bootloader that only performs ed25519 signature verification (and never touches stream ciphers) pulls in an undefined symbol at link time. The app build resolves it through nuttx_random.c, which is gated behind CONFIG_CRYPTO_RANDOM_POOL and only compiled in when the NuttX random pool is enabled. That config isn't on in the tiny bootloader NuttX defconfig, and enabling it would pull in a pile of kernel code the bootloader doesn't need. Supply the symbol locally, but make it call up_assert() instead of returning zeros. Silently handing out predictable bytes would be a serious security bug if anyone later enables the XCHACHA20 path in the bootloader without wiring up a real RNG. Aborting makes the mistake impossible to miss. * fix(bootloader): add PROTO_VERIFY_SIG opcode for upload-time signature check Today a signed-boot failure is invisible to the uploader: verify_app() runs inside jump_to_app() after PROTO_BOOT has already rebooted the chip, so the host sees a successful upload followed by the device silently staying in the bootloader. There is no protocol-level signal that the image that was just written is not going to run. Add PROTO_VERIFY_SIG (0x39) so the host can ask the bootloader to run find_toc() + verify_app(0) *before* the reboot and get a concrete OK / FAILED / INVALID answer back over the still-open USB connection. The new opcode is gated on BOOTLOADER_USE_SECURITY: bootloaders built without secure boot return cmd_bad (INSYNC/INVALID) so an uploader can tell "I don't know this command" apart from "verification failed". Two subtleties required care: 1. PROG_MULTI deliberately defers the very first word of the app image to a RAM variable (first_word) and only commits it to flash inside PROTO_BOOT, so a partial upload can never become bootable. But verify_app() reads directly from flash, so if we verified before committing first_word, even a valid image would always fail (the first four bytes at APP_LOAD_ADDRESS would still be 0xffffffff). The handler therefore mirrors the first half of the PROTO_BOOT handler: gate on STATE_ALLOWS_REBOOT, program the deferred first word, then run the crypto check. 2. On failure we deliberately do not try to "undo" the first-word write — H7 flash programming granularity makes it impossible to revert in place, and the device was going to end up in the same reject state at the next boot either way. The improvement is purely that the uploader sees the failure before REBOOT instead of after. Chose opcode 0x39 to stay clear of ArduPilot's 0x28 (READ_MULTI) and 0x40 (CHIP_FULL_ERASE), which PX4 does not currently use but which an AP-compatible uploader might. ArduPilot also has bootloader secure boot (monocypher ed25519, like us) but their verification runs at boot time, not upload time — so neither project currently solves the "upload silently wrote a bad image" problem. This puts PX4 ahead on that. * feat(Tools): add --image_signed to mark signed firmware for uploader px_uploader.py only needs to run signature verification over USB for images that actually carry a signature — asking the bootloader to verify an unsigned image would always fail, and the extra round trip is wasted time for the common unsigned case. It therefore needs a reliable way to tell the two apart. We cannot tell by inspecting the bytes: a sign_firmware.py output is just the raw .bin with 64 bytes of ed25519 signature glued on the end, indistinguishable in content from an unsigned image of the same padded length. The natural place to put the flag is the .px4 JSON envelope, which already carries board_id / version / summary / etc. Add --image_signed to px_mkfw.py, which sets "image_signed": true in the emitted JSON. The flag has no effect on what bytes actually end up on the device — it just tells the uploader "this blob has a signature, please verify it before booting". * feat(Tools): verify firmware signature before reboot After PROG_MULTI + GET_CRC, send a new VERIFY_SIG opcode (0x39) to the bootloader to check the ed25519 signature over the freshly flashed image *before* sending REBOOT. This way a signature failure is reported as a clean error from the uploader script, instead of a silent "device stays in bootloader after reboot" that leaves the user guessing. The uploader always probes VERIFY_SIG. The bootloader is the source of truth for whether secure boot is enabled: - INSYNC/OK -> verification passed, proceed to REBOOT - INSYNC/FAILED -> raise; "Signature does not verify against any trusted key" if the firmware claims to be signed, "Secure bootloader rejected an unsigned image" otherwise (= helpful guidance for the common misconfiguration of uploading default firmware to a secureboot bootloader) - INSYNC/INVALID -> bootloader has no secure boot. Quietly proceed unless the firmware metadata says image_signed, in which case raise. - recv timeout -> assume a pre-VERIFY_SIG bootloader, proceed. Surfaces a "Verifying image signature... passed" line on the upload status path when the firmware is marked signed, so users get a clear positive signal that the secure-boot pipeline ran end-to-end. Also drop the redundant logger.error in the upload() loop, which was duplicating the error message printed by the top-level handler in main() for every UploadError path (not specific to verify_signature, but only became obvious once these clean error messages started firing in normal usage). * fix(cmake): fix .px4board variant resolution to require exact match px4_config.cmake matched the requested CONFIG against each candidate .px4board with `MATCHES`, which is a regex partial match. For a config like `px4_fmu-v6x_bootloader_secureboot`, the iteration over .px4board files (alphabetical glob order) would match `bootloader.px4board` first — because "px4_fmu-v6x_bootloader" is a prefix of "px4_fmu-v6x_bootloader_secureboot" — and stop, silently selecting the wrong board config. Use `STREQUAL` instead so each candidate has to match the full requested CONFIG. Existing single-label cases (e.g. exact match on `px4_fmu-v2_default` or `px4_fmu-v2`) are unaffected because they were already exact in practice; this just plugs the prefix-match hole that any future `