feat(bootloader): Revive secure-boot with example, docs, and various fixes (#27237)

* 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 <name>.json (private+public hex)
  for use by sign_firmware.py and <name>.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 `<label>_<suffix>` variant would trip over.

* fix(nuttx): support bootloader_<variant> labels

Boards can ship bootloader variants beyond the default `bootloader`
label — e.g. a `bootloader_secureboot` that adds crypto + keystore
Kconfig on top of the same source tree. Two pieces of build glue
were hardcoded to the exact label `bootloader` and need to relax to
match any `bootloader_*` label:

1. platforms/nuttx/CMakeLists.txt picked the bootloader linker
   script and bootloader-specific library list only when the label
   was exactly `bootloader`. Any other label (including
   `bootloader_secureboot`) silently fell through to the app build,
   producing nonsensical link flags. Match `^bootloader` instead,
   and explicitly set SCRIPT_PREFIX to `bootloader_` so all
   bootloader variants share the single existing linker script
   regardless of their full label.

2. platforms/nuttx/cmake/px4_impl_os.cmake selects the NuttX config
   subdirectory by exact label match, falling back to `nsh` when no
   matching directory exists. For `bootloader_secureboot` that fell
   through to `nsh`, dragging in a full app-style NuttX with cromfs,
   networking, and the full heap subsystem — a 128 KB bootloader
   sector overflowed by ~50 KB. Add an intermediate fallback: if the
   label starts with `bootloader` and a `bootloader/` subdir exists,
   use that instead of `nsh`.

Both changes are backward-compatible: existing single-label
`bootloader` builds take exactly the same path as before. They only
gain the ability for boards to add `bootloader_<suffix>.px4board`
files without duplicating bootloader build wiring.

* feat(build): auto-sign secure-boot images via BOARD_SECUREBOOT

Add a Kconfig pair that boards can opt into:

  CONFIG_BOARD_SECUREBOOT      -- bool: sign the .px4 with ed25519
  CONFIG_BOARD_SECUREBOOT_KEY  -- string: path to JSON private key
                                  (default: Tools/test_keys/test_keys.json)

When set, the .px4 build rule inserts a Tools/secure_bootloader/sign_firmware.py
step between the unsigned .bin and px_mkfw.py, and passes --image_signed
so the .px4 envelope's metadata flags it for the uploader's VERIFY_SIG
step. The unsigned .bin is still produced alongside the .px4, so users
can sign with their own key out-of-tree if they prefer.

A BOARD_SECUREBOOT_KEY environment variable overrides the Kconfig path
at build time, mirroring the override pattern used for CONFIG_PUBLIC_KEYn
in stub_keystore. This makes release builds practical:

  BOARD_SECUREBOOT_KEY=/secure/path/release.json make px4_<board>_secureboot

Relative paths in the Kconfig are resolved against the repo root (not
the build directory) so the same value works regardless of where the
build runs from.

Default builds are byte-identical to before; the new code path is gated
on CONFIG_BOARD_SECUREBOOT being set.

* feat(boards): add secureboot demo variant + docs

Two coordinated build variants demonstrate end-to-end secure boot
on px4_fmu-v6x without touching the default builds:

  px4_fmu-v6x_secureboot              -- the app, with TOC + signing
  px4_fmu-v6x_bootloader_secureboot   -- the matching secure bootloader

App side (secureboot.px4board):
  - Selects nuttx-config/scripts/secureboot-script.ld via
    CONFIG_BOARD_LINKER_PREFIX. The script is a copy of the default
    layout plus a fixed 0x800 reservation past the vector table for
    the image TOC, and an empty .signature section at end-of-FLASH
    so sign_firmware.py knows where the appended ed25519 signature
    will land.
  - Compiles src/toc.c (a four-entry IMAGE_MAIN_TOC matching the
    layout used elsewhere in the tree) into drivers_board, gated on
    PX4_BOARD_LABEL == secureboot so the default build still
    produces the existing layout.
  - Sets CONFIG_BOARD_SECUREBOOT=y so the .px4 build rule signs the
    image with the upstream test key by default.

Bootloader side (bootloader_secureboot.px4board):
  - Enables CONFIG_BOARD_CRYPTO + DRIVERS_SW_CRYPTO + DRIVERS_STUB_KEYSTORE,
    which links monocypher and the stub keystore into the bootloader.
  - Bakes Tools/test_keys/key0.pub in as CONFIG_PUBLIC_KEY0, paired
    with the test_keys.json the app variant signs with.
  - hw_config.h gates BOOTLOADER_USE_SECURITY, BOOTLOADER_SIGNING_ALGORITHM
    (CRYPTO_ED25519), and BOARD_IMAGE_TOC_OFFSET (0x800) on PX4_CRYPTO,
    so they only activate in this variant.

Together the two variants implement the workflow:

  make px4_fmu-v6x_bootloader_secureboot     # build + flash via SWD once
  make px4_fmu-v6x_secureboot upload         # signs with test key, verifies

Bootloader fits in 128 KB (~57 KB used) thanks to --gc-sections
stripping the unused libtomcrypt code; nothing in the bootloader
binary depends on monocypher beyond the ed25519 verifier.

Replace the bundled test key for production with
Tools/secure_bootloader/generate_signing_keys.py output and update
both .px4board files (or set BOARD_SECUREBOOT_KEY at build time) —
see docs/en/advanced_config/bootloader_secure_boot.md.

* fix(boards): cap H7 bootloader linker scripts at 128 KB

Roughly half of the H7 boards in tree had `LENGTH = 2048K` for the
bootloader sector in their bootloader_script.ld, even though the
matching app linker script places APP_LOAD_ADDRESS at 0x08020000 —
i.e. the bootloader actually only owns the first 128 KB sector.

The 2048K constraint is wrong: it lets a bootloader that grew past
the 128 KB sector silently overflow into the app sector at link
time and corrupt the start of the app on flash. The linker should
fail the build instead.

The current default bootloader is ~46 KB, well under 128 KB on every
affected board, so this change is a no-op for default builds. It
just plugs a footgun for anyone adding bootloader features (secure
boot, extra UI, network boot, ...) that would have otherwise
silently grown into the app's flash range.

Boards fixed:

  3dr-style H7 reference boards: cubepilot/cubeorange,
    cubepilot/cubeorangeplus, holybro/durandal-v1, narinfc/h7
  vendor variants: corvon/743v1, cuav/nora, cuav/x7pro,
    gearup/airbrainh743, hkust/nxt-dual, hkust/nxt-v1,
    matek/h743, matek/h743-mini, matek/h743-slim,
    micoair/h743, micoair/h743-aio, micoair/h743-lite,
    micoair/h743-v2, x-mav/ap-h743r1, x-mav/ap-h743v2

Boards already correct (LENGTH = 128K) are unchanged.

* fix(ci): add pynacl to Python requirements

Tools/secure_bootloader/sign_firmware.py uses PyNaCl for ed25519
signing, and the .px4 build rule for boards with CONFIG_BOARD_SECUREBOOT
calls it as part of the normal build (e.g. px4_fmu-v6x_secureboot).
Without pynacl in the dev requirements, those builds fail in CI and
fresh dev setups.

* docs(update): Subedit

* docs(update): Fix example error I made

* fix(docs): document BOOT and SIG1 regions

* fix(platforms): style fix

* fix(ci): workaround to get pip dependency

* fix(ci): fall back to plain pip on older build containers

The voxl2 build image ships a pip that predates --break-system-packages
(added in pip 23.0.1), so the install step blew up with "no such option".
Modern containers enforce PEP 668 and need the flag; older ones don't
support it but also don't enforce PEP 668, so plain pip works there.
Try the modern flag first, fall back if pip rejects it.

Signed-off-by: Julian Oes <julian@oes.ch>

* fix(build): strip BUILD_DIR_SUFFIX from CONFIG in cmake-build

cmake-build was passing the full build-dir name (including any
BUILD_DIR_SUFFIX like _replay or _failsafe_web) as -DCONFIG=, which
the board-lookup in cmake/px4_config.cmake then tried to match
against <vendor>_<model>_<label> .px4board files. That used to work
by accident because px4_config.cmake matched with regex MATCHES, but
since the switch to STREQUAL (needed to disambiguate
bootloader_secureboot from bootloader) the suffixed CONFIG no longer
matches anything and LABEL ends up empty, tripping a CMake error in
kconfig.cmake.

Pass the bare CONFIG and keep the suffix only on the build dir so
both concerns are independent.

Signed-off-by: Julian Oes <julian@oes.ch>

---------

Signed-off-by: Julian Oes <julian@oes.ch>
Co-authored-by: Hamish Willee <hamishwillee@gmail.com>
This commit is contained in:
Julian Oes
2026-05-28 14:21:49 +12:00
committed by GitHub
parent 0acdaacf86
commit ec8718f05e
42 changed files with 880 additions and 39 deletions
+8
View File
@@ -201,6 +201,14 @@ jobs:
ccache -s
ccache -z
- name: Install pynacl for secure-boot signing
# TODO: drop once the build container ships with pynacl preinstalled
# (currently pinned to v1.17.0-rc2 in Tools/ci/build_all_config.yml).
# Modern containers enforce PEP 668 and need --break-system-packages;
# older ones (e.g. voxl2) ship a pip that doesn't know the flag and
# don't need it either, so fall back to plain pip install.
run: pip install pynacl --break-system-packages || pip install pynacl
- name: Building Artifacts for [${{ matrix.targets }}]
run: |
./Tools/ci/build_all_runner.sh ${{matrix.targets}} ${{matrix.arch}}
+25
View File
@@ -143,6 +143,31 @@ config BOARD_CRYPTO
help
Enable PX4 Crypto Support. Select the implementation under drivers
config BOARD_SECUREBOOT
bool "Sign firmware image for secure boot"
default n
help
Run Tools/secure_bootloader/sign_firmware.py over the built .bin
to append an ed25519 signature, and mark the resulting .px4 as
image_signed: true so the uploader runs the bootloader's
VERIFY_SIG step before reboot. The matching public key must be
baked into the bootloader via CONFIG_PUBLIC_KEY0 in the
bootloader's .px4board file.
config BOARD_SECUREBOOT_KEY
string "Path to JSON private key (absolute or relative to repo root)"
depends on BOARD_SECUREBOOT
default "Tools/test_keys/test_keys.json"
help
Private key used by sign_firmware.py to sign the firmware
image. The default points at the upstream development test
key, which is pre-paired with Tools/test_keys/key0.pub. For
production builds, generate a new keypair with
Tools/secure_bootloader/generate_signing_keys.py and update
both this option and the bootloader's CONFIG_PUBLIC_KEY0.
The environment variable BOARD_SECUREBOOT_KEY overrides this
value at build time.
config BOARD_PROTECTED
bool "Memory protection"
help
+4 -1
View File
@@ -177,7 +177,10 @@ endif
# --------------------------------------------------------------------
# describe how to build a cmake config
define cmake-build
$(eval override CMAKE_ARGS += -DCONFIG=$(1))
# Strip BUILD_DIR_SUFFIX (e.g. _replay, _failsafe_web) from CONFIG so the
# board lookup in cmake/px4_config.cmake sees the bare <vendor>_<model>_<label>
# even when the build dir is suffixed for variant builds.
$(eval override CMAKE_ARGS += -DCONFIG=$(patsubst %$(BUILD_DIR_SUFFIX),%,$(1)))
@$(eval BUILD_DIR = "$(SRC_DIR)/build/$(1)")
@# check if the desired cmake configuration matches the cache then CMAKE_CACHE_CHECK stays empty
@$(call cmake-cache-check)
+98 -1
View File
@@ -201,6 +201,7 @@ class BootloaderCommand(IntEnum):
GET_CHIP_DES = 0x2E # rev5+, get chip description in ASCII
GET_VERSION = 0x2F # rev5+, get bootloader version in ASCII
REBOOT = 0x30
VERIFY_SIG = 0x39 # verify signature of programmed image (secure boot)
CHIP_FULL_ERASE = 0x40 # Full erase of flash, rev6+
@@ -294,6 +295,7 @@ class Firmware:
image: bytes = field(init=False)
image_size: int = field(init=False)
image_maxsize: int = field(init=False)
image_signed: bool = field(init=False)
description: dict = field(init=False)
def __post_init__(self):
@@ -331,6 +333,7 @@ class Firmware:
self.board_revision = self.description.get("board_revision", 0)
self.image_size = self.description["image_size"]
self.image_maxsize = self.description["image_maxsize"]
self.image_signed = bool(self.description.get("image_signed", False))
# Decompress image
try:
@@ -1165,6 +1168,81 @@ class BootloaderProtocol:
else:
self.verify_read(firmware, progress_callback)
def verify_signature(self, image_signed: bool) -> None:
"""Ask the bootloader to verify the signature of the programmed image.
Always call this — the bootloader is the source of truth for whether
signature verification is required, not the host. If the bootloader
was built without BOOTLOADER_USE_SECURITY it returns INVALID and
we proceed normally; if it does verify, we surface a clean error
before REBOOT instead of letting the device silently stay in BL.
Args:
image_signed: True if the firmware metadata claimed the image
is signed. Used only to upgrade the "bootloader doesn't
support secure boot" warning into an error: a signed image
being uploaded to a non-secure bootloader is a config
mismatch worth flagging loudly.
Raises:
ProtocolError: If the bootloader reports the signature is bad,
or if a signed image was sent to a non-secure bootloader.
"""
logger.info("Verifying image signature")
self._send_command(BootloaderCommand.VERIFY_SIG)
self.transport.flush()
# ed25519 verification over a multi-megabyte image runs a full
# SHA-512 over every byte in flash, which on a slow-clocked H7
# bootloader with monocypher can comfortably exceed the default
# serial timeout. Give the response a generous window.
try:
insync = self.transport.recv(1, timeout=3.0)
except TimeoutError:
# Old bootloader from before VERIFY_SIG existed: no response,
# just silently dropped the byte. Treat as "no secure boot
# support" and continue. The downside is that a signed image
# going to a really old bootloader won't be caught here.
logger.debug("VERIFY_SIG timed out; assuming pre-secureboot bootloader")
return
if insync[0] != BootloaderResponse.INSYNC:
raise ProtocolError(
f"Expected INSYNC (0x{BootloaderResponse.INSYNC:02X}), "
f"got 0x{insync[0]:02X}",
port=self.transport.port_name,
operation="verify_signature",
)
result = self.transport.recv(1)
if result[0] == BootloaderResponse.OK:
logger.info("Signature verification passed")
return
if result[0] == BootloaderResponse.FAILED:
if image_signed:
msg = ("Signature does not verify against any trusted key "
"in the bootloader.")
else:
msg = ("Secure bootloader rejected an unsigned image. "
"Build with CONFIG_BOARD_SECUREBOOT or flash a "
"non-secure bootloader.")
raise ProtocolError(msg, port=self.transport.port_name)
if result[0] == BootloaderResponse.INVALID:
if image_signed:
raise ProtocolError(
"Image is marked signed but the bootloader has no "
"secure boot support.",
port=self.transport.port_name,
)
logger.debug("Bootloader has no secure boot; skipping verification")
return
raise ProtocolError(
f"Unexpected VERIFY_SIG response: 0x{result[0]:02X}",
port=self.transport.port_name,
operation="verify_signature",
)
def reboot(self) -> None:
"""Reboot into the application.
@@ -1678,7 +1756,10 @@ class Uploader:
last_error = e
continue
except UploadError as e:
logger.error(f"Upload failed on {port}: {e}")
# Don't log here: we re-raise and the top-level handler
# in main() will print the error message. Logging here
# too produces duplicate user-facing output.
logger.debug(f"Upload failed on {port}: {e}")
last_error = e
raise
@@ -1928,6 +2009,22 @@ class Uploader:
# Verify
protocol.verify(firmware, progress_callback=progress.update_verify)
# Always ask the bootloader to verify the signature before reboot.
# The bootloader is the source of truth for whether secure boot
# is enabled — non-secure bootloaders return INVALID and we just
# proceed. Reporting a bad signature here is much more actionable
# than letting the device silently stay in the bootloader.
# The progress bar above uses carriage-return overwrites and
# leaves the cursor on the same line; drop to a fresh line so
# any verify-step output doesn't clobber the progress bar.
if not self.config.json_output:
print()
if firmware.image_signed:
print("Verifying image signature...", end="", flush=True)
protocol.verify_signature(firmware.image_signed)
if not self.config.json_output and firmware.image_signed:
print(" passed")
# Reboot and show summary
protocol.reboot()
progress.finish()
+3
View File
@@ -77,6 +77,7 @@ parser.add_argument("--git_identity", action="store", help="the working director
parser.add_argument("--parameter_xml", action="store", help="the parameters.xml file")
parser.add_argument("--airframe_xml", action="store", help="the airframes.xml file")
parser.add_argument("--image", action="store", help="the firmware image")
parser.add_argument("--image_signed", action="store_true", help="mark the image as signed for secure-boot verification by the uploader")
args = parser.parse_args()
# Fetch the firmware descriptor prototype if specified
@@ -123,5 +124,7 @@ if args.image != None:
bytes = f.read()
desc['image_size'] = len(bytes)
desc['image'] = base64.b64encode(zlib.compress(bytes,9)).decode('utf-8')
if args.image_signed:
desc['image_signed'] = True
print(json.dumps(desc, indent=4))
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""
Generate an ed25519 keypair for PX4 secure bootloader firmware signing.
Produces two files next to each other:
<name>.json - private + public key, used by sign_firmware.py
<name>.pub - public key as a C array, included in the bootloader build
via CONFIG_PUBLIC_KEY0..3
The .json file contains the private key. Do not publish it. Do not lose it:
without it you cannot sign new firmware images for devices that already
trust its public key.
"""
import argparse
import binascii
import json
import sys
import time
from pathlib import Path
import nacl.encoding
import nacl.signing
def write_public_key_header(signing_key, key_name):
raw = signing_key.verify_key.encode(encoder=nacl.encoding.RawEncoder)
lines = []
for i in range(0, len(raw), 8):
chunk = ", ".join(f"0x{b:02x}" for b in raw[i:i + 8])
lines.append(chunk + ",")
body = "\n".join(lines) + "\n"
with open(key_name + ".pub", "w") as f:
f.write("// Public key to verify signed binaries\n")
f.write(body)
def write_key_json(signing_key, key_name):
path = Path(key_name + ".json")
if path.exists():
print(f"ERROR: {path} already exists. Refusing to overwrite.")
print("Remove the file and run again if you really want a new key.")
sys.exit(1)
keys = {
"date": time.asctime(),
"public": signing_key.verify_key.encode(encoder=nacl.encoding.HexEncoder).decode(),
"private": binascii.hexlify(signing_key._seed).decode(),
}
with open(path, "w") as f:
json.dump(keys, f)
def main():
parser = argparse.ArgumentParser(
description="Generate an ed25519 keypair for PX4 firmware signing.",
)
parser.add_argument(
"name",
help="Output base name. Writes <name>.json and <name>.pub.",
)
args = parser.parse_args()
signing_key = nacl.signing.SigningKey.generate()
write_key_json(signing_key, args.name)
write_public_key_header(signing_key, args.name)
print(f"Wrote {args.name}.json (private + public)")
print(f"Wrote {args.name}.pub (public, C array for bootloader build)")
if __name__ == "__main__":
main()
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""
Sign a PX4 firmware .bin with an ed25519 key for the secure bootloader.
The output binary is the input padded to a 4-byte boundary with 0xff,
followed by a 64-byte signature, followed (optionally) by an R&D
certificate appended verbatim.
The signed image lives in the flash slot the bootloader expects to find
the main app at, so the layout looks like:
[ vectors / .text / .data ... ][ pad to 4B ][ 64B signature ][ rdct? ]
The bootloader walks the image TOC, finds the BOOT entry's signature_idx
slot, and verifies it against a key stored in the bootloader's keystore.
"""
import argparse
import binascii
import json
import sys
import nacl.encoding
import nacl.signing
def ed25519_sign(private_key_hex, signee_bin):
signing_key = nacl.signing.SigningKey(private_key_hex, encoder=nacl.encoding.HexEncoder)
signed = signing_key.sign(signee_bin, encoder=nacl.encoding.RawEncoder)
public_key = signing_key.verify_key.encode(encoder=nacl.encoding.RawEncoder)
return signed.signature, public_key
def sign(bin_file_path, key_file_path):
with open(bin_file_path, mode="rb") as f:
signee_bin = f.read()
if len(signee_bin) % 4 != 0:
signee_bin += bytearray(b"\xff") * (4 - len(signee_bin) % 4)
try:
with open(key_file_path, mode="r") as f:
keys = json.load(f)
except OSError:
print(f"ERROR: Key file {key_file_path} not found")
sys.exit(1)
signature, public_key = ed25519_sign(keys["private"], signee_bin)
assert len(signature) == 64
print(f'Binary "{bin_file_path}" signed.')
print("Signature:", binascii.hexlify(signature).decode())
print("Public key:", binascii.hexlify(public_key).decode())
return signee_bin + signature
def main():
parser = argparse.ArgumentParser(
description="Sign a PX4 .bin firmware image with an ed25519 key.",
)
parser.add_argument("signee", help="Input .bin to sign")
parser.add_argument("signed", help="Output signed .bin")
parser.add_argument(
"--key",
default="Tools/test_keys/test_keys.json",
help="Key file produced by generate_signing_keys.py",
)
parser.add_argument(
"--rdct",
default=None,
help="Optional R&D certificate binary, appended after signature",
)
args = parser.parse_args()
if args.key == "Tools/test_keys/test_keys.json":
print("WARNING: Signing with PX4 test key — do not use in production")
signed_bytes = sign(args.signee, args.key)
with open(args.signed, mode="wb") as f:
f.write(signed_bytes)
if args.rdct is not None:
with open(args.rdct, mode="rb") as rdct_in, open(args.signed, mode="ab") as out:
out.write(rdct_in.read())
if __name__ == "__main__":
main()
+1
View File
@@ -18,6 +18,7 @@ psutil
pycryptodome
pygments
pymavlink
pynacl
pyros-genmsg
pyserial
pyulog>=0.5.0
@@ -110,7 +110,7 @@
MEMORY
{
itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
dtcm1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
dtcm2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
sram (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
@@ -109,7 +109,7 @@
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
DTCM1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
DTCM2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
@@ -109,7 +109,7 @@
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
DTCM1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
DTCM2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
@@ -109,7 +109,7 @@
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
DTCM1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
DTCM2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
@@ -109,7 +109,7 @@
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
DTCM1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
DTCM2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
@@ -110,7 +110,7 @@
MEMORY
{
itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
dtcm1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
dtcm2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
sram (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
@@ -110,7 +110,7 @@
MEMORY
{
itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
dtcm1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
dtcm2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
sram (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
@@ -110,7 +110,7 @@
MEMORY
{
itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
dtcm1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
dtcm2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
sram (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
@@ -110,7 +110,7 @@
MEMORY
{
itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
dtcm1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
dtcm2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
sram (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
@@ -110,7 +110,7 @@
MEMORY
{
itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
dtcm1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
dtcm2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
sram (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
@@ -110,7 +110,7 @@
MEMORY
{
itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
dtcm1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
dtcm2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
sram (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
@@ -110,7 +110,7 @@
MEMORY
{
itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
dtcm1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
dtcm2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
sram (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
@@ -110,7 +110,7 @@
MEMORY
{
itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
dtcm1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
dtcm2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
sram (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
@@ -110,7 +110,7 @@
MEMORY
{
itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
dtcm1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
dtcm2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
sram (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
@@ -110,7 +110,7 @@
MEMORY
{
itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
dtcm1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
dtcm2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
sram (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
@@ -110,7 +110,7 @@
MEMORY
{
itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
dtcm1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
dtcm2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
sram (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
@@ -109,7 +109,7 @@
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
DTCM1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
DTCM2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
@@ -0,0 +1,7 @@
CONFIG_BOARD_TOOLCHAIN="arm-none-eabi"
CONFIG_BOARD_ARCHITECTURE="cortex-m7"
CONFIG_BOARD_ROMFSROOT=""
CONFIG_BOARD_CRYPTO=y
CONFIG_DRIVERS_SW_CRYPTO=y
CONFIG_DRIVERS_STUB_KEYSTORE=y
CONFIG_PUBLIC_KEY0="../../../Tools/test_keys/key0.pub"
@@ -0,0 +1,136 @@
/****************************************************************************
* scripts/secureboot-script.ld
*
* Linker script for the px4_fmu-v6x_secureboot variant.
*
* Same layout as script.ld, except:
* - The image table-of-contents (.main_toc, defined by src/toc.c)
* is placed at a fixed offset past the vector table so the
* bootloader can find it via BOARD_IMAGE_TOC_OFFSET (0x800).
* - An empty .signature output section reserves the symbol
* _boot_signature at the end of the FLASH content. The TOC
* references it as the signed-image end address; sign_firmware.py
* appends the 64-byte ed25519 signature there.
*
****************************************************************************/
MEMORY
{
ITCM_RAM (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
FLASH (rx) : ORIGIN = 0x08020000, LENGTH = 1920K
DTCM1_RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
DTCM2_RAM (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
AXI_SRAM (rwx) : ORIGIN = 0x24000000, LENGTH = 512K /* D1 domain AXI bus */
SRAM1 (rwx) : ORIGIN = 0x30000000, LENGTH = 128K /* D2 domain AHB bus */
SRAM2 (rwx) : ORIGIN = 0x30020000, LENGTH = 128K /* D2 domain AHB bus */
SRAM3 (rwx) : ORIGIN = 0x30040000, LENGTH = 32K /* D2 domain AHB bus */
SRAM4 (rwx) : ORIGIN = 0x38000000, LENGTH = 64K /* D3 domain */
BKPRAM (rwx) : ORIGIN = 0x38800000, LENGTH = 4K
}
OUTPUT_ARCH(arm)
EXTERN(_vectors)
ENTRY(_stext)
EXTERN(abort)
EXTERN(board_get_manifest)
EXTERN(_main_toc)
SECTIONS
{
.text : {
_stext = ABSOLUTE(.);
*(.vectors)
/* Place the image TOC at a fixed offset past the vector table
* so the bootloader can find it via BOARD_IMAGE_TOC_OFFSET.
* 0x800 leaves comfortable room past the STM32H7 vector table
* (~0x298 bytes) and lands on a clean 2 KiB boundary.
*/
. = ABSOLUTE(ORIGIN(FLASH) + 0x800);
KEEP(*(.main_toc))
*(.text .text.*)
*(.fixup)
*(.gnu.warning)
*(.rodata .rodata.*)
*(.gnu.linkonce.t.*)
*(.glue_7)
*(.glue_7t)
*(.got)
*(.gcc_except_table)
*(.gnu.linkonce.r.*)
_etext = ABSOLUTE(.);
} > FLASH
.init_section : {
_sinit = ABSOLUTE(.);
KEEP(*(.init_array .init_array.*))
_einit = ABSOLUTE(.);
} > FLASH
.ARM.extab : {
*(.ARM.extab*)
} > FLASH
__exidx_start = ABSOLUTE(.);
.ARM.exidx : {
*(.ARM.exidx*)
} > FLASH
__exidx_end = ABSOLUTE(.);
_eronly = ABSOLUTE(.);
.data : {
_sdata = ABSOLUTE(.);
*(.data .data.*)
*(.gnu.linkonce.d.*)
CONSTRUCTORS
_edata = ABSOLUTE(.);
/* Pad out last section as the STM32H7 Flash write size is 256 bits. 32 bytes */
. = ALIGN(16);
FILL(0xffff)
. += 16;
} > AXI_SRAM AT > FLASH = 0xffff
.bss : {
_sbss = ABSOLUTE(.);
*(.bss .bss.*)
*(.gnu.linkonce.b.*)
*(COMMON)
. = ALIGN(4);
_ebss = ABSOLUTE(.);
} > AXI_SRAM
.sram4_reserve (NOLOAD) :
{
*(.sram4)
. = ALIGN(4);
_sram4_heap_start = ABSOLUTE(.);
} > SRAM4
/* Marker for the end of the signed image. sign_firmware.py
* appends the 64-byte ed25519 signature at this address; the TOC's
* SIG1 entry references the same range so the bootloader hashes
* exactly the bytes that were signed.
*/
.signature : {
_boot_signature = ALIGN(4);
} > FLASH
/* Stabs debugging sections. */
.stab 0 : { *(.stab) }
.stabstr 0 : { *(.stabstr) }
.stab.excl 0 : { *(.stab.excl) }
.stab.exclstr 0 : { *(.stab.exclstr) }
.stab.index 0 : { *(.stab.index) }
.stab.indexstr 0 : { *(.stab.indexstr) }
.comment 0 : { *(.comment) }
.debug_abbrev 0 : { *(.debug_abbrev) }
.debug_info 0 : { *(.debug_info) }
.debug_line 0 : { *(.debug_line) }
.debug_pubnames 0 : { *(.debug_pubnames) }
.debug_aranges 0 : { *(.debug_aranges) }
}
+20
View File
@@ -0,0 +1,20 @@
# Secure-boot variant of the fmu-v6x app.
#
# Inherits everything from default.px4board, then:
# - selects nuttx-config/scripts/secureboot-script.ld as the linker
# script (via CONFIG_BOARD_LINKER_PREFIX, same mechanism
# flash-analysis.px4board uses), which reserves the image TOC
# slot at FLASH+0x800 and a .signature section at end-of-FLASH.
# - the board CMakeLists.txt detects this label and adds src/toc.c
# to drivers_board so the TOC is actually populated.
#
# The output .bin is meant to be signed by Tools/secure_bootloader/sign_firmware.py
# and uploaded to a board running the px4_fmu-v6x_bootloader_secureboot bootloader.
CONFIG_BOARD_LINKER_PREFIX="secureboot"
CONFIG_BOARD_SECUREBOOT=y
# CONFIG_BOARD_SECUREBOOT_KEY defaults to Tools/test_keys/test_keys.json,
# which is pre-paired with Tools/test_keys/key0.pub baked into the
# matching bootloader_secureboot.px4board build. Set the env var
# BOARD_SECUREBOOT_KEY to a custom JSON private key path to override
# without editing this file.
+10 -2
View File
@@ -30,7 +30,7 @@
# POSSIBILITY OF SUCH DAMAGE.
#
############################################################################
if("${PX4_BOARD_LABEL}" STREQUAL "bootloader")
if("${PX4_BOARD_LABEL}" MATCHES "^bootloader")
add_compile_definitions(BOOTLOADER)
add_library(drivers_board
bootloader_main.c
@@ -49,7 +49,7 @@ if("${PX4_BOARD_LABEL}" STREQUAL "bootloader")
target_include_directories(drivers_board PRIVATE ${PX4_SOURCE_DIR}/platforms/nuttx/src/bootloader/common)
else()
add_library(drivers_board
set(_drivers_board_sources
can.c
i2c.cpp
init.cpp
@@ -60,6 +60,14 @@ else()
timer_config.cpp
usb.c
)
# Secure-boot variant: populate the image TOC slot the linker
# script (selected via CONFIG_BOARD_LINKER_PREFIX) reserves.
if("${PX4_BOARD_LABEL}" STREQUAL "secureboot")
list(APPEND _drivers_board_sources toc.c)
endif()
add_library(drivers_board ${_drivers_board_sources})
add_dependencies(drivers_board arch_board_hw_info)
target_link_libraries(drivers_board
+13
View File
@@ -122,4 +122,17 @@
# define BOOT_DEVICES_FILTER_ONUSB USB0_DEV|SERIAL0_DEV|SERIAL1_DEV
#endif
/* Secure-boot variant: when the bootloader build pulls in crypto
* support (CONFIG_BOARD_CRYPTO=y -> PX4_CRYPTO), enable signature
* verification in the bootloader and tell it where to find the TOC
* the app linker reserves. The matching app variant
* (px4_fmu-v6x_secureboot) places the TOC at the same offset.
*/
#if defined(PX4_CRYPTO)
#include <px4_platform_common/crypto_algorithms.h>
#define BOOTLOADER_USE_SECURITY 1
#define BOOTLOADER_SIGNING_ALGORITHM CRYPTO_ED25519
#define BOARD_IMAGE_TOC_OFFSET 0x800
#endif
#endif /* HW_CONFIG_H_ */
+62
View File
@@ -0,0 +1,62 @@
/****************************************************************************
*
* Copyright (C) 2026 PX4 Development Team. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* 3. Neither the name PX4 nor the names of its contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
****************************************************************************/
#include <image_toc.h>
/* (Maximum) size of the signature */
#define SIGNATURE_SIZE 64
extern uint32_t _vectors[];
extern const int *_boot_signature;
#define BOOT_ADDR _vectors
#define BOOT_END ((const void *)&_boot_signature)
#define BOOTSIG_ADDR ((const void *)&_boot_signature)
#define BOOTSIG_END ((const void *)((const uint8_t *)BOOTSIG_ADDR + SIGNATURE_SIZE))
#define RDCT_ADDR BOOTSIG_END
#define RDCT_END ((const void *)((const uint8_t *)BOOTSIG_END + sizeof(image_cert_t)))
#define RDCTSIG_ADDR RDCT_END
#define RDCTSIG_END ((const void *)((const uint8_t *)RDCTSIG_ADDR + SIGNATURE_SIZE))
IMAGE_MAIN_TOC(4) = {
{TOC_START_MAGIC, TOC_VERSION},
{
{"BOOT", BOOT_ADDR, BOOT_END, 0, 1, 0, 0, TOC_FLAG1_BOOT | TOC_FLAG1_CHECK_SIGNATURE},
{"SIG1", BOOTSIG_ADDR, BOOTSIG_END, 0, 0, 0, 0, 0},
{"RDCT", RDCT_ADDR, RDCT_END, 0, 3, 0, 0, TOC_FLAG1_RDCT | TOC_FLAG1_CHECK_SIGNATURE},
{"RDSG", RDCTSIG_ADDR, RDCTSIG_END, 0, 0, 0, 0, 0},
},
TOC_END_MAGIC
};
@@ -110,7 +110,7 @@
MEMORY
{
itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
dtcm1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
dtcm2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
sram (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
@@ -110,7 +110,7 @@
MEMORY
{
itcm (rwx) : ORIGIN = 0x00000000, LENGTH = 64K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 2048K
flash (rx) : ORIGIN = 0x08000000, LENGTH = 128K
dtcm1 (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
dtcm2 (rwx) : ORIGIN = 0x20010000, LENGTH = 64K
sram (rwx) : ORIGIN = 0x24000000, LENGTH = 512K
+2 -2
View File
@@ -71,7 +71,7 @@ if(NOT PX4_CONFIG_FILE)
# <VENDOR>_<MODEL>_<LABEL> (eg px4_fmu-v2_default)
# <VENDOR>_<MODEL>_default (eg px4_fmu-v2) # allow skipping label if "default"
if ((${CONFIG} MATCHES "${vendor}_${model}_${label}") OR # match full vendor, model, label
if ((${CONFIG} STREQUAL "${vendor}_${model}_${label}") OR # match full vendor, model, label
((${label} STREQUAL "default") AND (${CONFIG} STREQUAL "${vendor}_${model}")) # default label can be omitted
)
set(PX4_CONFIG_FILE "${PX4_SOURCE_DIR}/boards/${filename}" CACHE FILEPATH "path to PX4 CONFIG file" FORCE)
@@ -84,7 +84,7 @@ if(NOT PX4_CONFIG_FILE)
# <BOARD>_<LABEL> (eg px4_fmu-v2_default)
# <BOARD>_default (eg px4_fmu-v2) # allow skipping label if "default"
if ((${CONFIG} MATCHES "${board}_${label}") OR # match full board, label
if ((${CONFIG} STREQUAL "${board}_${label}") OR # match full board, label
((${label} STREQUAL "default") AND (${CONFIG} STREQUAL "${board}")) # default label can be omitted
)
set(PX4_CONFIG_FILE "${PX4_SOURCE_DIR}/boards/${filename}" CACHE FILEPATH "path to PX4 CONFIG file" FORCE)
+1
View File
@@ -409,6 +409,7 @@
- [PX4 Ethernet Setup](advanced_config/ethernet_setup.md)
- [Standard Configuration](config/index.md)
- [OEM Configuration](advanced_config/oem.md)
- [Bootloader Secure Boot](advanced_config/bootloader_secure_boot.md)
- [Advanced Configuration](advanced_config/index.md)
- [Using PX4's Navigation Filter (EKF2)](advanced_config/tuning_the_ecl_ekf.md)
- [GNSS-Denied & Degraded Flight](advanced_config/gnss_degraded_or_denied_flight.md)
@@ -0,0 +1,139 @@
# Bootloader Secure Boot
Secure boot is a feature that ensures that only cryptographically authorized PX4 firmware is executed.
This is used by OEMs to ensure that only their validated, tested firmware runs on the vehicle — protecting safety, brand integrity, and regulatory compliance, and stopping others from running unauthorized software on their hardware.
The _PX4 Bootloader_ can verify a cryptographic signature over the PX4 firmware before it is run.
When enabled, only a firmware image signed with a private key whose matching public key is baked into the bootloader will boot.
For any unsigned image, tampered image, or image signed by the wrong key, the device will wait in the bootloader screen (where it can be recovered safely over USB), and will not start the rest of the PX4 flight stack.
::: warning
This feature is intended for OEMs.
If you flash a bootloader that trusts a public key whose private counterpart you have lost, you can no longer sign new firmware for that device and will need a debug probe to recover it.
Keep private keys backed up and never commit them to source control.
:::
## How It Works
PX4's secure boot uses [ed25519](https://en.wikipedia.org/wiki/EdDSA#Ed25519) signatures (via [monocypher](https://monocypher.org)).
The signing key is 32 bytes and the resulting signature is 64 bytes.
Verification is fast (no random number generator is required on the device), and the implementation is small enough to fit in a 128 KB bootloader sector alongside the rest of the bootloader.
A signed firmware image lays out in flash like this:
```txt
+---------------------------+ APP_LOAD_ADDRESS ─┐
| Vector table | │
+---------------------------+ APP_LOAD_ADDRESS │
| Image TOC | + BOARD_IMAGE_TOC_OFFSET │ BOOT region
+---------------------------+ │ (hashed and signed)
| .text / .rodata / .data | │
+---------------------------+ &_boot_signature ─┤
| 64-byte ed25519 signature | │ SIG1
+---------------------------+ ─┘
```
The **Table of Contents (TOC)** is a small data structure compiled into the firmware that tells the bootloader which region to hash and which key slot to verify against.
Its format is defined in [`src/include/image_toc.h`](https://github.com/PX4/PX4-Autopilot/blob/main/src/include/image_toc.h).
For px4_fmu-v6x the TOC declares two entries: **BOOT** (the firmware bytes to verify) and **SIG1** (the 64-byte ed25519 signature to verify them against).
On reset the bootloader reads the TOC at a fixed offset, computes an ed25519 signature over the BOOT region, and compares it to the SIG1 entry.
If the signature verifies it jumps to the app; otherwise it stays in the bootloader and waits for a new upload.
The host-side uploader (`Tools/px4_uploader.py`) also asks the bootloader to verify the signature before sending the final reboot, via a dedicated `VERIFY_SIG` opcode.
This means a signature mismatch is reported as a clean error from `px_uploader.py` instead of a silent "device stays in bootloader after reboot".
## Trying It Out
PX4 ships a secure-boot example for **px4_fmu-v6x**.
This consists of two build variants:
- `px4_fmu-v6x_bootloader_secureboot` — the secure bootloader, with ed25519 verification enabled, and the upstream _test public key_ baked in.
- `px4_fmu-v6x_secureboot` — PX4 firmware, with TOC and automatic signing.
The steps are:
1. Build and flash the secure bootloader (one-time, via SWD)
- Build the bootloader:
```sh
make px4_fmu-v6x_bootloader_secureboot
```
- Flash the resulting `build/px4_fmu-v6x_bootloader_secureboot/px4_fmu-v6x_bootloader_secureboot.elf` via a debug probe.
See [Bootloader Update](../advanced_config/bootloader_update).
::: tip
This is the only step that needs SWD — once the secure bootloader is in place, all firmware updates go over USB.
:::
2. Build and upload signed firmware
```sh
make px4_fmu-v6x_secureboot upload
```
The build produces an unsigned `.bin`, signs it with the upstream test key (`Tools/test_keys/test_keys.json`), wraps it in a `.px4` envelope marked `image_signed: true`, and uploads it.
You will see something like:
```sh
Verify ▕██████████████████████████████▏ 100%
Verifying image signature... passed
Uploaded in 18s
```
If you upload an image that the bootloader can't verify (e.g. an unsigned `.px4`, a tampered `.bin`, or one signed with a different key), `px_uploader.py` reports the failure before reboot:
```sh
Upload failed: Signature verification failed: image will not boot.
The bootloader computed a signature over the flashed image that does not
match any public key it trusts.
```
## Generating Your Own Keys
The default test key is committed to the PX4 tree, so a real deployment must replace it (in order to protect the public key).
Generate a new ed25519 key pair with:
```shell
python3 Tools/secure_bootloader/generate_signing_keys.py /path/to/my_key
```
This writes:
- `my_key.json` — the private key used by `sign_firmware.py`. **Keep this private. Do not commit it.**
- `my_key.pub` — the public key as a C-array fragment, suitable for `#include` in the bootloader's keystore.
Make the following changes use your new keys in the build:
1. **Update the bootloader to trust your public key.**
Edit `boards/px4/fmu-v6x/bootloader_secureboot.px4board` and change `CONFIG_PUBLIC_KEY0` to point at `my_key.pub`. Rebuild and reflash the bootloader via SWD.
2. **Tell the app build to sign with the matching private key.**
Either edit `boards/px4/fmu-v6x/secureboot.px4board` and change `CONFIG_BOARD_SECUREBOOT_KEY` to point at `my_key.json`, or set the environment variable at build time:
```sh
BOARD_SECUREBOOT_KEY=/path/to/my_key.json make px4_fmu-v6x_secureboot upload
```
The keys used must always be the corresponding cryptographic pair — if you flash a bootloader that trusts a key whose private half you don't have, you can only recover via SWD.
## Enabling Secure Boot on a New Board
To enable secure boot up on a board that doesn't already have a `secureboot` variant, you'll need:
- a `toc.c` file placed in the board's `src/` (modeled on `boards/px4/fmu-v6x/src/toc.c`),
- a linker script with `_main_toc` reserved at a fixed offset past the vector table and a `.signature` section at the end of FLASH (see `boards/px4/fmu-v6x/nuttx-config/scripts/secureboot-script.ld`)
- a `secureboot.px4board` setting `CONFIG_BOARD_SECUREBOOT=y` and the linker prefix,
- a `bootloader_secureboot.px4board` enabling `CONFIG_BOARD_CRYPTO`, `CONFIG_DRIVERS_SW_CRYPTO`, `CONFIG_DRIVERS_STUB_KEYSTORE` and the public-key path,
- `BOOTLOADER_USE_SECURITY` + `BOOTLOADER_SIGNING_ALGORITHM` + `BOARD_IMAGE_TOC_OFFSET` defines in the board's `hw_config.h`, gated on `PX4_CRYPTO`.
The fmu-v6x variant files are kept small and self-contained for exactly this reason — they are intended to be copied as a starting point.
## See Also
- [Bootloader Update](../advanced_config/bootloader_update.md)
- [OEM/Factory Configuration](../advanced_config/oem.md)
+11 -10
View File
@@ -2,7 +2,7 @@
The _PX4 Bootloader_ is used to load firmware for [Pixhawk boards](../flight_controller/pixhawk_series.md) (PX4FMU, PX4IO).
Pixhawk controllers usually comes with an appropriate bootloader version pre-installed.
Pixhawk controllers usually come with an appropriate bootloader version pre-installed.
However in some cases it is not present, or an older version is present that needs to be updated, or the board has been bricked and needs to be erased and the bootloader reinstalled.
This topic explains how to build the PX4 bootloader, and several methods for flashing it to a board.
@@ -26,7 +26,7 @@ You can then initiate bootloader update on next restart by setting the parameter
This approach can be used if the [`bl-update` module](../modules/modules_command.md#bl-update) is present in the firmware.
The easiest way to check this is just to see if the [SYS_BL_UPDATE](../advanced_config/parameter_reference.md#SYS_BL_UPDATE) parameter is [found in QGroundControl](../advanced_config/parameters.md#finding-a-parameter).
:::warning
::: warning
Boards that include the module will have the line `CONFIG_SYSTEMCMDS_BL_UPDATE=y` in their `default.px4board` file (for examples [see this search](https://github.com/search?q=repo%3APX4%2FPX4-Autopilot+path%3A**%2Fdefault.px4board+CONFIG_SYSTEMCMDS_BL_UPDATE%3Dy&type=code)).
You can enable this key in your own custom firmware if needed.
:::
@@ -34,20 +34,20 @@ You can enable this key in your own custom firmware if needed.
The steps are:
1. Insert an SD card (enables boot logging to debug any problems).
1. [Update the Firmware](../config/firmware.md#custom) with an image containing the new/desired bootloader.
2. [Update the Firmware](../config/firmware.md#custom) with an image containing the new/desired bootloader.
::: info
The updated bootloader might be included the default firmware for your board or supplied in custom firmware.
The updated bootloader might be included in the default firmware for your board or supplied in custom firmware.
:::
1. Wait for the vehicle to reboot.
1. [Find and enable](../advanced_config/parameters.md) the parameter [SYS_BL_UPDATE](../advanced_config/parameter_reference.md#SYS_BL_UPDATE).
1. Reboot (disconnect/reconnect the board).
3. Wait for the vehicle to reboot.
4. [Find and enable](../advanced_config/parameters.md) the parameter [SYS_BL_UPDATE](../advanced_config/parameter_reference.md#SYS_BL_UPDATE).
5. Reboot (disconnect/reconnect the board).
The bootloader update will only take a few seconds.
Generally at this point you may then want to [update the firmware](../config/firmware.md) again using the correct/newly installed bootloader.
An specific example of this process for updating the [FMUv2 bootloader](#fmuv2-bootloader-update) is given below.
A specific example of this process for updating the [FMUv2 bootloader](#fmuv2-bootloader-update) is given below.
## Building the PX4 Bootloader
@@ -84,7 +84,7 @@ The following steps explain how you can "manually" update the bootloader using a
1. Get a binary containing the bootloader (either from dev team or [build it yourself](#building-the-px4-bootloader)).
2. Get a [Debug Probe](../debug/swd_debug.md#debug-probes-for-px4-hardware).
Connect the probe your PC via USB and setup the `gdbserver`.
Connect the probe to your PC via USB and setup the `gdbserver`.
3. Go into the directory containing the binary and run the command for your target bootloader in the terminal:
- FMUv6X
@@ -142,7 +142,7 @@ The following steps explain how you can "manually" update the bootloader using a
If using a Zubax BugFace BF1 you may need to remove the case in order to connect to the `FMU-DEBUG` port (e.g. on Pixhawk 4 you would do this using a T6 Torx screwdriver).
:::
8. Use the following command to scan for the Pixhawk`s SWD and connect to it:
8. Use the following command to scan for the Pixhawk's SWD and connect to it:
```sh
(gdb) mon swdp_scan
@@ -198,4 +198,5 @@ For boards that are preflashed with Betaflight, see [Bootloader Flashing onto Be
## See Also
- [Bootloader Secure Boot](../advanced_config/bootloader_secure_boot.md)
- [OEM/Factory Configuration](../advanced_config/oem.md)
+2 -1
View File
@@ -1,6 +1,6 @@
# OEM/Factory Configuration
This topic lists configuration and calibration topics that are more relevant to manufacturers/OEMs (though is some cases individual developers may find some relevant).
This topic lists configuration and calibration topics that are more relevant to manufacturers/OEMs (though in some cases individual developers may find some relevant).
- [IMU Factory Calibration](../advanced_config/imu_factory_calibration.md)
- [Sensor Thermal Compensation](../advanced_config/sensor_thermal_calibration.md)
@@ -8,6 +8,7 @@ This topic lists configuration and calibration topics that are more relevant to
- [Advanced Controller Orientation](../advanced_config/advanced_flight_controller_orientation_leveling.md)
- [Static Pressure Buildup](../advanced_config/static_pressure_buildup.md)
- [Bootloader Update](../advanced_config/bootloader_update.md)
- [Bootloader Secure Boot](../advanced_config/bootloader_secure_boot.md)
## See Also
+55 -3
View File
@@ -67,8 +67,15 @@ endif()
set(nuttx_libs)
set(SCRIPT_PREFIX)
if("${PX4_BOARD_LABEL}" STREQUAL "bootloader")
set(SCRIPT_PREFIX ${PX4_BOARD_LABEL}_)
# Match "bootloader" and any "bootloader_<variant>" label (e.g.
# "bootloader_secureboot") so a board can ship a bootloader variant
# with extra Kconfig (crypto, keystore, etc.) without duplicating
# the bootloader build wiring.
if("${PX4_BOARD_LABEL}" MATCHES "^bootloader")
# All bootloader variants share the same linker script
# (bootloader_script.ld); the label suffix only changes which
# Kconfig and which Tools/test_keys/*.pub are baked in.
set(SCRIPT_PREFIX bootloader_)
add_subdirectory(src/bootloader)
list(APPEND nuttx_libs
bootloader
@@ -425,6 +432,49 @@ if (TARGET parameters_xml AND TARGET airframes_xml)
string(REPLACE ".elf" ".px4" fw_package ${PX4_BINARY_DIR}/${FW_NAME})
# Secure-boot variants sign the .bin with sign_firmware.py before
# wrapping it in the .px4 envelope, and tag the envelope so the
# uploader runs VERIFY_SIG against the matching bootloader. The
# resulting .px4 is the only one that will boot on a secure
# bootloader (the unsigned .bin still ships alongside, for users
# who want to sign with a different key out-of-tree).
set(_mkfw_image ${PX4_BINARY_DIR}/${PX4_CONFIG}.bin)
set(_mkfw_signed_flag)
set(_mkfw_extra_depends)
if(CONFIG_BOARD_SECUREBOOT)
# Allow an env var to override the Kconfig default so a release
# build can point at a private key that doesn't live in the
# repo without having to edit the .px4board. Relative paths
# from the .px4board are resolved against the repo root, to
# match how CONFIG_PUBLIC_KEYn paths are written.
if(DEFINED ENV{BOARD_SECUREBOOT_KEY})
set(_secureboot_key $ENV{BOARD_SECUREBOOT_KEY})
else()
set(_secureboot_key ${CONFIG_BOARD_SECUREBOOT_KEY})
endif()
if(NOT IS_ABSOLUTE ${_secureboot_key})
get_filename_component(_secureboot_key
${_secureboot_key} ABSOLUTE BASE_DIR ${PX4_SOURCE_DIR})
endif()
set(_signed_bin ${PX4_BINARY_DIR}/${PX4_CONFIG}_signed.bin)
add_custom_command(
OUTPUT ${_signed_bin}
COMMAND
${PYTHON_EXECUTABLE} ${PX4_SOURCE_DIR}/Tools/secure_bootloader/sign_firmware.py
--key ${_secureboot_key}
${PX4_BINARY_DIR}/${PX4_CONFIG}.bin
${_signed_bin}
DEPENDS ${PX4_BINARY_DIR}/${PX4_CONFIG}.bin
COMMENT "Signing ${PX4_CONFIG}.bin with ${_secureboot_key}"
WORKING_DIRECTORY ${PX4_BINARY_DIR}
)
set(_mkfw_image ${_signed_bin})
set(_mkfw_signed_flag --image_signed)
set(_mkfw_extra_depends ${_signed_bin})
endif()
add_custom_command(
OUTPUT ${fw_package}
COMMAND
@@ -433,9 +483,11 @@ if (TARGET parameters_xml AND TARGET airframes_xml)
--git_identity ${PX4_SOURCE_DIR}
--parameter_xml ${PX4_BINARY_DIR}/parameters.xml
--airframe_xml ${PX4_BINARY_DIR}/airframes.xml
--image ${PX4_BINARY_DIR}/${PX4_CONFIG}.bin > ${fw_package}
${_mkfw_signed_flag}
--image ${_mkfw_image} > ${fw_package}
DEPENDS
${PX4_BINARY_DIR}/${PX4_CONFIG}.bin
${_mkfw_extra_depends}
airframes_xml
parameters_xml
COMMENT "Creating ${fw_package}"
+6
View File
@@ -214,6 +214,12 @@ function(px4_os_prebuild_targets)
if(EXISTS ${PX4_BOARD_DIR}/nuttx-config/${PX4_BOARD_LABEL})
set(NUTTX_CONFIG "${PX4_BOARD_LABEL}" CACHE INTERNAL "NuttX config" FORCE)
elseif("${PX4_BOARD_LABEL}" MATCHES "^bootloader" AND EXISTS ${PX4_BOARD_DIR}/nuttx-config/bootloader)
# Bootloader variants (e.g. bootloader_secureboot) share the
# minimal "bootloader" NuttX config; falling back to "nsh"
# would silently pull in the full app-style NuttX subsystem
# (full heap, fs, net) and overflow the bootloader sector.
set(NUTTX_CONFIG "bootloader" CACHE INTERNAL "NuttX config" FORCE)
else()
set(NUTTX_CONFIG "nsh" CACHE INTERNAL "NuttX config" FORCE)
endif()
@@ -110,6 +110,7 @@
#define PROTO_BOOT 0x30 // boot the application
#define PROTO_DEBUG 0x31 // emit debug information - format not defined
#define PROTO_SET_BAUD 0x33 // set baud rate on uart
#define PROTO_VERIFY_SIG 0x39 // verify the signature of the programmed image
// Reserved for external flash programming
// #define PROTO_EXTF_ERASE 0x34 // Erase sectors from external flash
@@ -1035,6 +1036,74 @@ bootloader(unsigned timeout)
*/
goto cmd_bad;
// verify the signature of the programmed image
//
// command: VERIFY_SIG/EOC
// reply: INSYNC/OK if image verifies
// reply: INSYNC/FAILED if TOC is missing or signature check fails
// reply: INSYNC/INVALID if the bootloader was built without
// BOOTLOADER_USE_SECURITY
//
// The uploader is expected to call this after GET_CRC and before
// BOOT, and only when the firmware metadata claims the image is
// signed. Running it always would add unnecessary verification time
// (and fail noisily) on bootloaders or images that are not secure.
case PROTO_VERIFY_SIG:
if (!wait_for_eoc(2)) {
goto cmd_bad;
}
#ifdef BOOTLOADER_USE_SECURITY
/* Only accept VERIFY_SIG at the same point in the upload
* state machine where we would accept BOOT i.e. after
* PROG_MULTI + GET_CRC have been issued in the right order.
*/
if (first_word != 0xffffffff && (bl_state & STATE_ALLOWS_REBOOT) != STATE_ALLOWS_REBOOT) {
goto cmd_bad;
}
/* During PROG_MULTI the first word of the app was held in a
* RAM variable instead of written to flash, to prevent a
* partial upload from becoming bootable. Signature
* verification has to hash the actual image, so we have to
* commit that word to flash first. This mirrors the first
* half of the BOOT handler below.
*/
if (first_word != 0xffffffff) {
flash_func_write_word(APP_VECTOR_OFFSET, first_word);
if (flash_func_read_word(APP_VECTOR_OFFSET) != first_word) {
goto cmd_fail;
}
first_word = 0xffffffff;
}
{
const image_toc_entry_t *toc_entries;
uint8_t toc_len;
crypto_init();
if (!find_toc(&toc_entries, &toc_len) ||
!verify_app(0, toc_entries)) {
crypto_deinit();
goto cmd_fail;
}
crypto_deinit();
}
break;
#else
/* Signature verification not compiled in — tell the host we
* don't know this opcode so it can warn the user instead of
* silently assuming the image is trusted.
*/
goto cmd_bad;
#endif
// finalise programming and boot the system
//
// command: BOOT/EOC
@@ -38,6 +38,31 @@
#ifdef BOOTLOADER_USE_SECURITY
#include <px4_platform_common/crypto_backend.h>
#include <stddef.h>
#include <stdint.h>
#include <nuttx/arch.h>
/* sw_crypto unconditionally references px4_get_secure_random from the
* XCHACHA20 path in crypto_open(). The bootloader only verifies
* ed25519 signatures and has no reason to generate randomness, but
* the linker still demands the symbol. Supplying it here avoids
* pulling NuttX's random pool (CONFIG_CRYPTO_RANDOM_POOL) into the
* bootloader build just to resolve an unreachable call site.
*
* The implementation panics rather than returning zeros: if anyone
* ever enables bootloader-side encryption without also wiring up a
* real RNG, silently handing out predictable bytes would be a
* serious security bug. up_assert() makes that mistake loud.
*/
size_t px4_get_secure_random(uint8_t *out, size_t outlen)
{
(void)out;
(void)outlen;
up_assert(__FILE__, __LINE__);
return 0;
}
bool verify_app(uint16_t idx, const image_toc_entry_t *toc_entries)
{